diff --git a/LICENSE-STATUS.md b/LICENSE-STATUS.md index b195cf2..4676d00 100644 --- a/LICENSE-STATUS.md +++ b/LICENSE-STATUS.md @@ -2,9 +2,9 @@ The owner-approved first-party license is GNU GPL version 3 only (GPL-3.0-only), selected October 25, 2026. Apache-2.0 was an earlier first-party proposal and remains applicable to third-party components that use it. The complete GPL is in [LICENSE](LICENSE), with scope in [LICENSING.md](LICENSING.md). -The current release target is FrameBridge 0.3.1, installer FrameBridge-0.3.1.deb. Historical RC7 and Community Preview artifacts retain their separate identities. +FrameBridge 0.3.4 has separate Debian 13, Ubuntu 24.04 and Linux Mint 22.x amd64 release profiles. Signature and publication status belong to its accompanying release record. The [official download page](https://framebridge.betancourt.one/download/) and each artifact's release record identify its source and signatures. Historical 0.3.3, 0.3.1, RC7 and Community Preview artifacts retain their separate identities; their versioned filenames do not identify the current installer. -The pinned Python/Rust dependency notices and source delivery were reconciled in the retained native terms review. The 0.3.1 preparation additionally identifies the OpenAI tunnel-client as third-party Apache-2.0 material, retains its notices, and supplies exact h1-verified source archives and notices for its 47 embedded Go modules. Its own development-build source commit is not embedded and remains a provenance limit. +The pinned Python/Rust dependency notices and source delivery were reconciled in the retained native terms review. The retained OpenAI tunnel-client is third-party Apache-2.0 material. Its notices and exact h1-verified source archives and notices for its 47 embedded Go modules remain included. Its own development-build source commit is not embedded and remains a provenance limit. The dependency-source assets retain their historical 0.3.1 filenames; this does not change the FrameBridge release version or assert an independent rebuild of those components. Public delivery is complete only when the actual matching installer, FrameBridge source, dependency-source assets, supplements and notices are downloadable with verified hashes. Private collection does not establish public availability. The final release delivery record is authoritative for staged or public status; this file does not assert that the port is open or public downloads are live. diff --git a/LICENSING.md b/LICENSING.md index f8145bf..affa31a 100644 --- a/LICENSING.md +++ b/LICENSING.md @@ -6,7 +6,7 @@ The owner selected GNU General Public License version 3 only, SPDX identifier GP The grant covers original FrameBridge code and project documentation that the rightsholders are entitled to license. Recipients may use, study, modify and redistribute that material under the GPL. Modified private copies and lawful independent distributions do not require submitting a pull request to this project. Original blog article text explicitly marked for that purpose uses CC-BY-SA-4.0 under [blog/LICENSING.md](blog/LICENSING.md); media and separately licensed components retain their own terms. -[licensing/SOURCE-LICENSE-SCOPE.json](licensing/SOURCE-LICENSE-SCOPE.json) records classification and provenance. File location is not proof of ownership. Preserve [COPYRIGHT.md](COPYRIGHT.md) and applicable notices. +[licensing/SOURCE-LICENSE-SCOPE.json](licensing/SOURCE-LICENSE-SCOPE.json) records historical classification and provenance; the matching release SOURCE-ARCHIVE-MANIFEST.json identifies the complete current source set. File location is not proof of ownership. Preserve [COPYRIGHT.md](COPYRIGHT.md) and applicable notices. ## Third-party material @@ -18,7 +18,7 @@ The tunnel executable identifies its own module as a development build without a ## Source delivery -Distribute the matching FrameBridge 0.3.1 source archive beside its installer, the immutable shared Python/Rust dependency-source asset, the new tunnel-client dependency-source supplement and retained notices. [Source availability](licensing/third-party/SOURCE-AVAILABILITY.md) identifies the files and hashes. +Distribute the matching FrameBridge 0.3.4 source archive beside each of its three distribution-profile installers, the immutable shared Python/Rust dependency-source asset, the new tunnel-client dependency-source supplement and retained notices. [Source availability](licensing/third-party/SOURCE-AVAILABILITY.md) identifies the files and hashes. The unchanged Certifi source arrangement retains its exact upstream source assets, original notice, full MPL-2.0 text and recipient access instructions. The historical Mozilla certdata revision remains unidentified. No independent native-wheel or tunnel-client rebuild is claimed. These are provenance and reproducibility limits, not claims that unavailable proof has been obtained. diff --git a/README-SOURCE-ARCHIVE.md b/README-SOURCE-ARCHIVE.md index 06075e6..fa73134 100644 --- a/README-SOURCE-ARCHIVE.md +++ b/README-SOURCE-ARCHIVE.md @@ -1,37 +1,37 @@ -# FrameBridge 0.3.1 source archive +# FrameBridge 0.3.4 corresponding source -This archive preserves the repository layout needed by the offline builder. -From this directory, run python3 source/packaging/build_expanded.py to build -FrameBridge-0.3.1.deb. The builder writes source/build and source/release; -it does not install or start FrameBridge. Debian's dpkg-deb and the documented -build prerequisites must already be available. +This archive supplies the project source and the reviewed cp313 and cp312 vendor +inputs for Debian 13, Ubuntu 24.04 and Linux Mint 22.x on amd64. Keep its matching +SOURCE-ARCHIVE-MANIFEST.json sidecar beside the archive. Signatures, publication +and actual OS lifecycle acceptance are recorded separately; assembly alone does +not certify them. Historical signed assets retain their immutable identities. -Original FrameBridge material is GNU GPL version 3 only. Read LICENSE, -LICENSING.md, COPYRIGHT.md, LICENSE-STATUS.md and OFFICIAL-RELEASES.md for -scope, attribution, third-party terms and the official-release trust model. +The offline builder needs Python 3, dpkg/dpkg-deb and the documented host build +prerequisites. It never fetches dependencies, installs packages or starts services. +Extract the archive beside its matching sidecar and run from the extracted root: -The project source asset is accompanied by these separate dependency assets: + python3 source/packaging/build_expanded.py --target debian13-amd64-py313 --public-release --source-manifest ../SOURCE-ARCHIVE-MANIFEST.json + python3 source/packaging/build_expanded.py --target ubuntu24.04-amd64-py312 --vendor-dir source/target-vendors/cp312 --public-release --source-manifest ../SOURCE-ARCHIVE-MANIFEST.json + python3 source/packaging/build_expanded.py --target mint22-amd64-py312 --vendor-dir source/target-vendors/cp312 --public-release --source-manifest ../SOURCE-ARCHIVE-MANIFEST.json + +Public-release mode verifies the archive hash, every current project source file +and the selected reviewed vendor identity before assembling a package. It does +not assert an independent rebuild of third-party native wheels or tunnel-client. +--check-vendor validates inputs without staging a package. + +Original FrameBridge material is GNU GPL version 3 only. Third-party material +retains its applicable terms. Read LICENSE, LICENSING.md, COPYRIGHT.md and the +source-delivery records. Distribute these separate, unchanged dependency assets +with the release; their historical names do not identify the installer version: - FrameBridge-0.3.1-Community-Preview1-dependency-sources.tar.gz - 82,606,998 bytes - SHA-256 64540a85bd65eac3f394d8c0a967cacbc00e112676d57d644dcd1bb691c1926a + Bytes: 82606998 + SHA-256: 64540a85bd65eac3f394d8c0a967cacbc00e112676d57d644dcd1bb691c1926a - FrameBridge-0.3.1-tunnel-client-dependency-sources.tar.gz - 26,941,745 bytes - SHA-256 6445d4f0e773aba833fe5c610fbae54102b92de2ce636438f769f21e890c2bd9 + Bytes: 26941745 + SHA-256: 6445d4f0e773aba833fe5c610fbae54102b92de2ce636438f769f21e890c2bd9 -The Preview1 filename deliberately preserves the identity of an immutable -shared asset. Retain licensing/third-party/R2-SUPPLEMENTS.json and its referenced -source notices with the dependency sources. The source indices and -SOURCE-AVAILABILITY.md there record scope and provenance. Do not substitute -unrelated archives. The retained tunnel executable is not claimed to have been -independently rebuilt from an identified own-source revision. - -The blog directory contains licensing policies and the post template. -Editorial article drafts are omitted because they are not source build inputs. - -Only source/evidence/BASELINE.json is included from technical evidence. Runtime -profiles, private QA logs, generated build/release files and Git history are -outside the source archive allowlist. - -A completed archive or builder run is not a declaration of public readiness. -Release checksums, signatures and publication status are recorded separately. +The exact own-source revision of the retained tunnel executable is not asserted. +Retain the R2 source-notice supplements, both dependency indices and original +notices. Private profiles, credentials, QA logs, build outputs, Git history and +unpublished blog drafts are excluded from the archive allowlist. diff --git a/README.md b/README.md index 5503cf6..eb19665 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ Use a central gateway and enrolled nodes to administer Linux computers you control. Run commands and return to persistent Bash terminals; an existing X11 desktop adds optional graphical tools. ChatGPT remains hosted; FrameBridge runs the tools on your Linux computers. -> **Latest signed release: `0.3.3`.** Packages and signed APT repositories are published for Debian 13, Ubuntu 24.04 and Linux Mint 22.x on amd64. Choose the exact distribution profile on the [official download page](https://framebridge.betancourt.one/download/). The signed `v0.3.3` tag and corresponding-source archive identify the released code; the default branch can receive later documentation updates. +> **FrameBridge 0.3.4.** Release builds cover Debian 13, Ubuntu 24.04 and Linux Mint 22.x on amd64. Choose the exact distribution profile on the [official download page](https://framebridge.betancourt.one/download/). Verify the matching release record, package signatures and signed APT metadata before installation. + +This release adds `fb health` / `fb --json health`, confirmed destination identity in Fleet results, native persistent startup improvements and reduced disk writes while idle. The source archive, release packages and external acceptance record identify the exact released code. See the [0.3.4 release notes](docs/RELEASE-NOTES-0.3.4.md). ## Start here @@ -26,14 +28,14 @@ The release has three native Python builds. Choose the package matching the dist | Ubuntu 24.04 | amd64 / x86_64 | 3.12 | `ubuntu24.04-amd64-py312` | | Linux Mint 22.x | amd64 / x86_64 | 3.12 | `mint22-amd64-py312` | -The 0.3.3 packages passed actual signed APT updates and managed-service lifecycle checks on Debian 13, Ubuntu 24.04 and Linux Mint 22.3 VMs. The updates preserved profiles and boot policy, and the services remained ready after the installer exited. Ubuntu 26.04, LMDE 7, other distributions, older releases and other CPU architectures are outside the validated scope of this release. Earlier compatibility experiments are recorded in the [compatibility history](docs/LINUX-COMPATIBILITY-CANDIDATE.md). +Version-specific signed APT update and managed-service lifecycle results belong to the accompanying release acceptance record. Ubuntu 26.04, LMDE 7, other distributions, older releases and other CPU architectures are outside the validated scope of this release. Earlier compatibility experiments are recorded in the [compatibility history](docs/LINUX-COMPATIBILITY-CANDIDATE.md). Verify the signing-key fingerprint, package checksum and detached signature using the instructions on the [official download page](https://framebridge.betancourt.one/download/). Historical release records remain in [Official releases](OFFICIAL-RELEASES.md). A checksum identifies a file; the release signature authenticates it with the independently trusted project key. -For example, with the **Mint** 0.3.3 package downloaded into the current directory: +For example, with the **Mint** 0.3.4 package downloaded into the current directory: ~~~bash -sudo apt install ./FrameBridge-0.3.3-mint22-amd64-py312.deb +sudo apt install ./FrameBridge-0.3.4-mint22-amd64-py312.deb ~~~ Use the corresponding profile filename on Debian or Ubuntu. Review APT's proposed changes for the actual computer. Installation places the software on disk; it does not establish a ChatGPT connection or replace the system Python. @@ -81,6 +83,8 @@ fb help commands --json JSON output is versioned and never prompts. `INTERACTION_REQUIRED` gives an action for the owner's normal terminal. The agent workflow helps an existing shell-based assistant install and diagnose FrameBridge; it does not claim every AI client is a tested replacement for ChatGPT. +For remote work, identify the receiver and execution target separately. `LOCAL` means the current FrameBridge receiver, not a fixed ASUS destination or the computer displaying the conversation. Read the enrolled-node inventory and use a confirmed alias or ID when selecting another computer. Legacy connectors can have different defaults; verify routing before replacing one with another. + `fb` and `framebridge` are names for the same program. Read `fb help` for the English guide and `fb help compatibility` for legacy commands. ## Pause, stop or remove @@ -106,6 +110,8 @@ Use `python3 source/packaging/build_expanded.py --help` for package build profil Read [CONTRIBUTING.md](CONTRIBUTING.md) to propose a change and [SECURITY.md](SECURITY.md) for security reports. Useful compatibility reports name the exact distribution, version, Python version, package profile and tests actually performed. Independent copies retain their applicable software freedoms; they must not be presented as maintainer-approved releases. +The [claims and evidence boundaries](docs/CLAIMS-AND-LIMITS.md) distinguish package installation, connection and operation results. The [operations roadmap](docs/OPERATIONS-ROADMAP.md) describes proposed follow-up work and acceptance checks; it is not a list of features already delivered. + Project home: https://framebridge.betancourt.one/ The public Git repository is `https://framebridge.betancourt.one/git/gerardo/framebridge.git`. For the exact 0.3.3 release, use the signed `v0.3.3` tag or its signed corresponding-source archive from the download page. The repositories, package downloads and source archive are published; normal owner authentication and connection setup are still required on a newly installed computer. diff --git a/RELEASE-NOTES-0.3.3.md b/RELEASE-NOTES-0.3.3.md deleted file mode 100644 index 7f22fd4..0000000 --- a/RELEASE-NOTES-0.3.3.md +++ /dev/null @@ -1,24 +0,0 @@ -# FrameBridge 0.3.3 - -FrameBridge administers Linux computers on a home network through a shared command-line interface for the human administrator and authorized agents. The language model remains hosted. - -## Changes - -- The desktop console reconnects to the existing official console window when the same tmux session contains other windows. It preserves the working shell and unrelated windows, and displays jobs from the selected profile. Recognized legacy desktop entries and console wrappers can be migrated to the packaged command with private backups. -- Forward package upgrades record active managed profiles before replacing files. A detached upgrade worker waits for DPKG to finish, then restarts only those profiles in independent user services. It preserves existing connection processes, profile settings, enrollment and boot policy, and records readiness separately from package installation. Stopped profiles remain stopped. -- The release provides separate amd64 builds and signed APT suites for Debian 13 (`trixie`), Ubuntu 24.04 (`noble`) and Linux Mint 22.x (`mint22`). Use the package for the distribution actually installed; keep its system Python. -- The public documentation uses the canonical website and Gitea repository under `framebridge.betancourt.one` and describes network administration as the primary workflow. Desktop tools remain auxiliary. - -## Updating - -After configuring the matching signed APT source, use the normal package manager to install or update `framebridge-original-expanded`. Inspect `fb status` after an upgrade. A successful APT command confirms package installation; the upgrade worker records runtime restart and readiness independently. An upgrade from a stopped profile does not connect it automatically. - -## Compatibility - -Physical desktop input requires a compatible active X11 session. Native Wayland control is not certified. The guarded x86-64 X11 text backend supports active keyboard group 0 and its documented modifier restrictions. - -Legacy integrations can have a different default execution target from the current receiver. Preserve explicit target routing or use a compatibility adapter when migrating existing clients. Replacing an endpoint without checking its default target is not a compatibility guarantee. - -## Validation - -The release is being validated against the three distribution profiles. Build, signature, APT and live upgrade results are recorded in the accompanying release verification manifest; publication and installation must be verified against the actual signed artifacts. diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index abede3c..05eff48 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,5 +1,9 @@ # FrameBridge release notes +## 0.3.4 + +Native persistent startup improvements, lower idle disk writes, local health reporting and confirmed Fleet destinations. The release has three amd64 distribution profiles and matching source with both Python ABIs. See [the complete 0.3.4 notes](docs/RELEASE-NOTES-0.3.4.md); signatures and actual lifecycle results are recorded with the release artifacts. + ## Unreleased — private production pilot fixes Private pilot validation remains in progress; these changes are not a public release. diff --git a/TARGET-VENDOR-PROVENANCE.json b/TARGET-VENDOR-PROVENANCE.json new file mode 100644 index 0000000..7075050 --- /dev/null +++ b/TARGET-VENDOR-PROVENANCE.json @@ -0,0 +1,451 @@ +{ + "omitted_vendor_abis": [ + "cp314" + ], + "release_profiles": [ + "debian13-amd64-py313", + "ubuntu24.04-amd64-py312", + "mint22-amd64-py312" + ], + "schema": 1, + "validation_status": "release_source", + "vendors": { + "cp312": { + "native_extensions": [ + "_cffi_backend.cpython-312-x86_64-linux-gnu.so", + "cryptography/hazmat/bindings/_rust.abi3.so", + "pydantic_core/_pydantic_core.cpython-312-x86_64-linux-gnu.so", + "rpds/rpds.cpython-312-x86_64-linux-gnu.so" + ], + "packages": [ + { + "name": "annotated-types", + "version": "0.8.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "anyio", + "version": "4.15.1", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "attrs", + "version": "26.1.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "certifi", + "version": "2026.7.22", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "cffi", + "version": "2.1.1", + "wheel_tags": [ + "cp312-cp312-manylinux_2_17_x86_64", + "cp312-cp312-manylinux2014_x86_64" + ] + }, + { + "name": "click", + "version": "8.5.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "cryptography", + "version": "50.0.1", + "wheel_tags": [ + "cp311-abi3-manylinux_2_34_x86_64" + ] + }, + { + "name": "h11", + "version": "0.16.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "httpcore", + "version": "1.0.9", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "httpx", + "version": "0.28.1", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "httpx-sse", + "version": "0.4.3", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "idna", + "version": "3.20", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "jsonschema", + "version": "4.26.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "jsonschema-specifications", + "version": "2025.9.1", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "mcp", + "version": "1.26.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "pycparser", + "version": "3.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "pydantic", + "version": "2.13.5", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "pydantic-core", + "version": "2.46.5", + "wheel_tags": [ + "cp312-cp312-manylinux_2_17_x86_64", + "cp312-cp312-manylinux2014_x86_64" + ] + }, + { + "name": "pydantic-settings", + "version": "2.15.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "pyjwt", + "version": "2.14.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "python-dotenv", + "version": "1.2.3", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "python-multipart", + "version": "0.0.32", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "referencing", + "version": "0.37.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "rpds-py", + "version": "2026.6.3", + "wheel_tags": [ + "cp312-cp312-manylinux_2_17_x86_64", + "cp312-cp312-manylinux2014_x86_64" + ] + }, + { + "name": "sse-starlette", + "version": "3.4.11", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "starlette", + "version": "1.7.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "typing-extensions", + "version": "4.16.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "typing-inspection", + "version": "0.4.4", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "uvicorn", + "version": "0.53.0", + "wheel_tags": [ + "py3-none-any" + ] + } + ], + "path": "source/target-vendors/cp312", + "vendor_sha256": "f4ea1dfb4f71b11eb1a9963bfe55dc6f94e6b10e927f77a7bd9b62433a29ddf1" + }, + "cp313": { + "native_extensions": [ + "_cffi_backend.cpython-313-x86_64-linux-gnu.so", + "cryptography/hazmat/bindings/_rust.abi3.so", + "pydantic_core/_pydantic_core.cpython-313-x86_64-linux-gnu.so", + "rpds/rpds.cpython-313-x86_64-linux-gnu.so" + ], + "packages": [ + { + "name": "annotated-types", + "version": "0.8.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "anyio", + "version": "4.15.1", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "attrs", + "version": "26.1.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "certifi", + "version": "2026.7.22", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "cffi", + "version": "2.1.1", + "wheel_tags": [ + "cp313-cp313-manylinux_2_17_x86_64", + "cp313-cp313-manylinux2014_x86_64" + ] + }, + { + "name": "click", + "version": "8.5.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "cryptography", + "version": "50.0.1", + "wheel_tags": [ + "cp311-abi3-manylinux_2_34_x86_64" + ] + }, + { + "name": "h11", + "version": "0.16.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "httpcore", + "version": "1.0.9", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "httpx", + "version": "0.28.1", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "httpx-sse", + "version": "0.4.3", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "idna", + "version": "3.20", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "jsonschema", + "version": "4.26.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "jsonschema-specifications", + "version": "2025.9.1", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "mcp", + "version": "1.26.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "pycparser", + "version": "3.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "pydantic", + "version": "2.13.5", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "pydantic-core", + "version": "2.46.5", + "wheel_tags": [ + "cp313-cp313-manylinux_2_17_x86_64", + "cp313-cp313-manylinux2014_x86_64" + ] + }, + { + "name": "pydantic-settings", + "version": "2.15.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "pyjwt", + "version": "2.14.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "python-dotenv", + "version": "1.2.3", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "python-multipart", + "version": "0.0.32", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "referencing", + "version": "0.37.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "rpds-py", + "version": "2026.6.3", + "wheel_tags": [ + "cp313-cp313-manylinux_2_17_x86_64", + "cp313-cp313-manylinux2014_x86_64" + ] + }, + { + "name": "sse-starlette", + "version": "3.4.11", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "starlette", + "version": "1.7.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "typing-extensions", + "version": "4.16.0", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "typing-inspection", + "version": "0.4.4", + "wheel_tags": [ + "py3-none-any" + ] + }, + { + "name": "uvicorn", + "version": "0.53.0", + "wheel_tags": [ + "py3-none-any" + ] + } + ], + "path": "source/sources/original-geranas/FrameBridge-Fleet-Hub/vendor", + "vendor_sha256": "80e2917f19003e95f74a6250ee08043cb90681100faf510c0e034843740c0950" + } + }, + "version": "0.3.4" +} diff --git a/docs/CLAIMS-AND-LIMITS.md b/docs/CLAIMS-AND-LIMITS.md index 38629b7..74caf57 100644 --- a/docs/CLAIMS-AND-LIMITS.md +++ b/docs/CLAIMS-AND-LIMITS.md @@ -1,23 +1,36 @@ # Claims and evidence boundaries -The current version is `0.3.1~community.20261026.2`, a separate candidate from RC7 and Preview 1. +The latest signed release is **FrameBridge 0.3.3**, with separate amd64 packages for Debian 13, Ubuntu 24.04 and Linux Mint 22.x. The [official download page](https://framebridge.betancourt.one/download/) identifies the actual artifacts, signatures and corresponding source. A later default-branch documentation change does not rewrite the signed tag or an installed package. + +## Current release scope | Statement | Evidence and boundary | | --- | --- | -| Preview 2 unit suite | 376 tests passed in 22.351 seconds; focused peer regression also passed. | -| Preview 2 package and VM | Pending at documentation freeze; no new VM acceptance or artifact hash is invented here. | -| Preview 1 | Historical record: 368 tests and a reproducible package build. | -| RC7 | Historical record: 361 tests, reproducible package, Debian 13 VM lifecycle and real ChatGPT/console acceptance. | -| ChatGPT connection | Manual owner account consent; the model remains hosted while tools run on selected owner-controlled computers. | -| Persistent console | Session continuity is not a promise that every process survives reboot. | -| Independent FBEXEC tasks | Repeated maintainer experience with non-overlapping work; no certified isolation or simultaneous execution of every command. | -| Graphical control | One controller per shared desktop is a recommendation, not an automatic lock. | +| Distribution profiles | Debian 13 / CPython 3.13, Ubuntu 24.04 / CPython 3.12 and Mint 22.x / CPython 3.12, all amd64. Ubuntu 26.04, LMDE 7, other architectures and other distributions are outside this release's validated scope. | +| Signed APT updates | Actual update and managed-service lifecycle checks passed on Debian 13, Ubuntu 24.04 and Mint 22.3 VMs. This is not acceptance of every physical installation or every future package. | +| Release identity | The signed tag and release record identify released source and bytes. A source checkout, successful build or checksum alone does not establish an official signed release. | +| Source tests | Run `python3 tools/run_tests.py` from a source checkout. A test result applies to the source revision and environment actually tested; historical test counts are not current results. | +| Installed documentation | Read the guide matching the installed version. A newer repository README cannot update an older installed manual. Report contradictory release labels with the package version and file path. | + +## Operation results + +| Statement | Evidence and boundary | +| --- | --- | +| ChatGPT connection | Manual owner account consent; the model remains hosted while tools run on selected owner-controlled computers. Installing the package or assigning an account label does not prove connection. | +| Receiver and target | `LOCAL` denotes the current receiver. Another computer must be selected using a confirmed enrolled alias or ID. A legacy endpoint's default destination is not a routing guarantee for a replacement endpoint. | +| Query and task success | A successful status query or outer tool wrapper does not prove readiness or successful inner execution. Read the target, exit code and operation result, then verify the intended effect. | +| Persistent console | Bash continuity lasts while the session survives; it does not promise survival across reboot. Preserve the existing console and unrelated work unless changing them is authorized. | +| Independent FBEXEC tasks | Repeated maintainer experience with non-overlapping work; no certified isolation or guarantee that every command can run concurrently. | +| Graphical control | Requires a compatible active X11 session and observed capture/input success. Native Wayland control is not certified. One controller per shared desktop is an operating recommendation, not an automatic lock. | +| Real images and demonstrations | Use complete authentic captures. Identify virtual displays and terminal text separately. Conceptual animation is illustration, not evidence of execution. | | sudo | Actual privileges depend on the Unix account and normal authentication; an authorization flag does not grant root. | -| Real images and demo | Use complete authentic captures. A fresh Preview 2 real recording is pending; conceptual animation is illustration. | -| Dependency terms/source | Scoped technical terms review and exact source availability recorded with delivery conditions; component terms remain in force. | +| Recovery | Pause, stop, boot policy, revocation and removal have different effects. A package downgrade can revoke administrative helper grants; retaining a previous package is not proof of automatic rollback. | +| Dependency terms/source | Scoped technical terms review and exact source availability are recorded with delivery conditions; component terms and provenance limits remain in force. | + +Earlier RC7 and Community Preview results remain historical records. They must not be substituted for the build, signature, installation or acceptance evidence of a later artifact. The [operations roadmap](OPERATIONS-ROADMAP.md) distinguishes proposed work from delivered capabilities. ## Retained control components -The retained development broker defaults to its existing 127.0.0.1 peer only. Repeatable --control-peer IP_LITERAL arguments add explicit peers. Invalid entries fail before Store creation/binding; checks use the real client address, not proxy headers. Retained RPC UID, host, client/socket and administrator identity guards remain unchanged. Normal Expanded handling uses the generic terminal entrypoint; FRAMEBRIDGE_ENABLE_GERAMEG_CUA_TTY=0 is not an effective guard. +The retained development broker defaults to its existing 127.0.0.1 peer only. Repeatable `--control-peer IP_LITERAL` arguments add explicit peers. Invalid entries fail before Store creation/binding; checks use the real client address, not proxy headers. Retained RPC UID, host, client/socket and administrator identity guards remain unchanged. Normal Expanded handling uses the generic terminal entrypoint; `FRAMEBRIDGE_ENABLE_GERAMEG_CUA_TTY=0` is not an effective guard. -The classic lab-target map is empty, while the compatibility route remains available for explicit integrator configuration. Do not describe retained code as entirely inactive. Further executable edits require identifiable source and candidate-specific validation. +The classic lab-target map is empty, while the compatibility route remains available for explicit integrator configuration. Do not describe retained code as entirely inactive. Executable changes require an identifiable source revision and validation of the changed behavior. diff --git a/docs/COMMAND-REFERENCE.md b/docs/COMMAND-REFERENCE.md index b7b26a6..4c521cf 100644 --- a/docs/COMMAND-REFERENCE.md +++ b/docs/COMMAND-REFERENCE.md @@ -1,6 +1,6 @@ # FrameBridge — command guide -Version: 0.3.3 | Guide schema: 1 +Version: 0.3.4 | Guide schema: 1 This reference is generated from the shared CLI/MCP guide. Use the matching installed version. @@ -36,7 +36,6 @@ fb --json status Install the package built for your exact Debian 13, Ubuntu 24.04 or Linux Mint 22.x amd64 profile. These are the three release profiles; other distributions and architectures are outside this release. Keep the distribution system Python: 3.13 on Debian 13, 3.12 on Ubuntu 24.04 and Mint 22.x. Use the matching signed APT suite: trixie, noble or mint22. Normal first connection does not require a separate setup, doctor or start sequence. Run connect in a normal interactive terminal; advanced setup is optional for central/node roles and public settings. ~~~sh -sudo apt install ./framebridge-expanded_0.3.3_amd64.deb fb connect ~~~ @@ -80,6 +79,7 @@ fb status runtime fb status boot fb status console fb doctor +fb health fb setup fb console fb help @@ -231,7 +231,7 @@ fb nodes status ### Targets -LOCAL means the actual receiving machine. Use enrolled, confirmed aliases or IDs and inspect the selected target before work. Do not guess SSH identities or use historical lab hostnames as defaults. CLI tty --node can use the authenticated node transport. The interactive console command is local; run it at the destination or use an explicitly configured SSH destination. +LOCAL means the actual receiving machine. Use enrolled, confirmed aliases or IDs and inspect the selected target before work. Fleet results include requested_target, resolved_target, node_id, hostname and transport from the confirmed descriptor. The compatibility target field can remain LOCAL; resolved_target names the actual local computer. Unknown targets never fall back to LOCAL. Do not guess SSH identities or use historical lab hostnames as defaults. CLI tty --node can use the authenticated node transport. The interactive console command is local; run it at the destination or use an explicitly configured SSH destination. ### Transport trust @@ -344,12 +344,21 @@ fb status boot ### Doctor -doctor diagnoses prerequisites and reports findings with suggested next steps. It is not an implicit repair, and it is not a substitute for checking actual live connection state. Inspect the diagnosis and relevant private application logs before considering configuration changes. Candidate eligibility checks the exact OS release, amd64 architecture, package profile and CPython ABI before starting native components. Debian 13/LMDE 7 use cp313, Ubuntu 24.04/Mint 22.x use cp312, and Ubuntu 26.04 uses cp314. A matching profile does not establish a tested installation; never repair an ABI mismatch by replacing system Python or weakening package dependencies. +doctor diagnoses prerequisites and reports findings with suggested next steps. It is not an implicit repair, and it is not a substitute for checking actual live connection state. Inspect the diagnosis and relevant private application logs before considering configuration changes. Eligibility checks the exact OS release, amd64 architecture, package profile and CPython ABI before starting native components. The published 0.3.3 scope is Debian 13 with cp313 and Ubuntu 24.04/Mint 22.x with cp312. LMDE 7 and Ubuntu 26.04 builder experiments remain outside that validated scope. A matching profile or a successful development build does not establish a tested installation; never repair an ABI mismatch by replacing system Python or weakening package dependencies. ~~~sh fb doctor ~~~ +### Local health summary + +health combines this local profile's existing status and prerequisite observations into an allowlisted report. It identifies LOCAL as this computer and reports the profile, role, connection intent, local tools, boot policy and desktop availability. It never starts, repairs, connects, changes boot policy or creates an absent profile. It does not scan the fleet or probe terminal commands, screen capture or input. States are healthy, paused, degraded, unavailable and unconfigured. An intentional connection pause can be healthy; missing information cannot. JSON result.ok describes observation completeness; result.health.healthy describes health. Health exits 0 for a complete healthy or intentionally paused observation and 1 for degraded, unavailable or unconfigured state. The CLI version is not proof of the running version. Detailed troubleshooting remains local in status and doctor. + +~~~sh +fb health +fb --json health +~~~ + ### Logs and progress Normal connection presents concise progress and a final result, then returns to the prompt. Detailed diagnostics belong in protected application logs. Share only the minimum relevant redacted result; never expose credentials, private keys, browser session data or unrelated personal paths. Avoid rerunning setup, enrollment or broad lifecycle tests merely because a view is confusing. diff --git a/docs/FRAMEBRIDGE-AGENT-START.md b/docs/FRAMEBRIDGE-AGENT-START.md index 72b5b0f..5ed8e7d 100644 --- a/docs/FRAMEBRIDGE-AGENT-START.md +++ b/docs/FRAMEBRIDGE-AGENT-START.md @@ -4,12 +4,14 @@ This is a setup and verification workflow for an assistant that **already has au The currently tested connection is the manual ChatGPT workflow. A coding assistant can help prepare it through the CLI; this document does not claim drop-in integration with every AI client. -Source preparation: **0.3.2, unsigned release candidate**, promoted from compatibility revision `0.3.2~compat.20261027.3` after its Debian 13, Ubuntu 24.04 and Mint 22.3 VM checks passed. Check the exact artifact release record before installation; this prior behavior evidence does not verify the final package bytes or claim public availability. +Latest signed release: **0.3.3**, published for Debian 13, Ubuntu 24.04 and Linux Mint 22.x on amd64. Use the exact profile, signatures and source record from the [official download page](https://framebridge.betancourt.one/download/). A later source checkout can contain unreleased changes; it is not evidence that a new package has been built, signed or published. Historical compatibility-candidate checks remain separate from validation of the actual release artifacts. ## Before handing over Give the assistant the computer you want to work on, the intended task, and the local path of the reviewed matching package. Use the [README's package matrix](../README.md#1-install-the-matching-package). If this is a pilot beside an existing installation, identify a new isolated `--state-dir` and the existing services that must remain running. +Name both the computer receiving the assistant's tool call and the intended execution target. `LOCAL` means the current FrameBridge receiver, not a fixed computer called ASUS or the computer displaying the chat. Discover enrolled nodes and use their confirmed aliases or IDs. A legacy connector can have a different default target; changing endpoints does not preserve that default automatically. + Do not put passwords, tunnel IDs, server access keys or browser codes in the prompt. The owner will enter them in the local hidden prompts. An MCP server access key is not an OpenAI API key. ## Copy this into your assistant @@ -20,12 +22,16 @@ Help me install and verify FrameBridge on this Linux computer. Target computer: [the computer to which you already have authorized shell access] Reviewed package: [absolute local path to the matching FrameBridge .deb] Scope: [new installation, or the specifically authorized pilot/profile] +Receiver and execution target: [identify both; do not assume LOCAL means ASUS] First establish where you are running. Inspect the distribution/version, architecture and system Python. Match them to the package's release profile. Read the project's README and the installed versioned help where available. Reuse FrameBridge's existing CLI and package; do not invent a replacement transport or rewrite the application to complete setup. +Confirm the receiver and selected target before the first remote operation. +Use an enrolled alias or ID for another computer and inspect the effective +target in the response. Do not fall back to a different computer on error. Use my existing task authorization. Complete the authorized preparatory work before asking for information or a decision that is actually missing. @@ -70,6 +76,8 @@ If desktop access is in scope, first identify the actual X11 session and use a new test window. A virtual display must be described as virtual. Terminal text is not a screen capture, and a VM's desktop is not the owner's physical display. Do not claim a screenshot or successful input unless it was observed. +Coordinate one controller at a time for a shared desktop. Preserve existing +console windows and their work unless changing them is part of the task. Check actual command exit codes and operation results. An outer tool wrapper's success flag is not proof that its inner command succeeded. Distinguish local diff --git a/docs/OPERATIONS-ROADMAP.md b/docs/OPERATIONS-ROADMAP.md new file mode 100644 index 0000000..16892f7 --- /dev/null +++ b/docs/OPERATIONS-ROADMAP.md @@ -0,0 +1,29 @@ +# Small Linux fleet operations: development scope + +This document records proposed follow-up work after the signed 0.3.3 release. It is not a feature-availability statement, a release announcement or a promise of unattended administration. The initial scope remains owner-controlled Debian 13, Ubuntu 24.04 and Mint 22.x amd64 computers administered by one responsible person. + +The `0.3.4` release source implements an initial **local-only** `health` summary and additive confirmed destination identity in Fleet results. It also consolidates the installed documentation, permits secure administrator-owned ancestors of the user's service directory and reduces durable idle status writes. These changes do not complete the wider acceptance items below. See [release notes](RELEASE-NOTES-0.3.4.md). + +## Acceptance order + +| Priority | Intended improvement | Acceptance evidence required | +| --- | --- | --- | +| 1 | Coherent installation and diagnosis | The package, CLI, MCP guide and website agree on version and supported profiles. An administrator outside the project completes installation, connection and a first harmless query using the documentation; record time and assistance needed. | +| 2 | Unambiguous execution destination | Show receiver and effective target separately. An explicit target never falls back to another machine. Cover unknown and duplicate names and differences from legacy defaults. | +| 3 | Unified read-only health | Reuse existing status, doctor and inventory tools. Report observation time, source and unavailable/error states. Distinguish disk capacity from free space, package presence from readiness and screenshot availability from input control. | +| 4 | Five versioned procedures | Begin with general diagnosis, a failed service, DNS diagnosis, APT diagnosis and a FrameBridge update. Record prerequisites, exact procedure version, proposed change, applicable authorization, post-check and a recovery route where supported. Read-only checks must not repair implicitly. | +| 5 | Jobs and technical history | A later conversation can identify the target, operation, result and remaining work without reconstructing the entire chat. Test disconnect/reconnect behavior and duplicate submissions. Export useful records without secrets. | +| 6 | Small external pilot | Use a separate installation per customer initially. Record installation effort, incidents, completed tasks, support time and willingness to renew before extending the platform. | + +## Preserve the current operating model + +- Keep `fb` / `framebridge` as the shared interface for the administrator and authorized agents. +- Retain the indicative console and existing work. A health view complements it. +- Use terminal and command tools for routine administration, with graphical CUA available for tasks that need a real desktop. Describe actual session and display limits. +- Keep normal OS/browser authentication in the owner's normal interface. Reuse task authorization; do not add repetitive approvals to harmless observations. +- Retain explicit connection intent and boot policy. An update must not start a profile that was intentionally stopped. +- Treat transport as a component. Another MCP client or platform is supported only after a documented interoperability test. + +## Deferred expansion + +Shared multi-customer hosting, broad Windows/macOS/DSM support, native Wayland control, a new tunnel/VPN and an autonomous always-on technician are separate projects. They need their own requirements and acceptance evidence. The initial commercial experiment is a scoped installation and maintenance service, not a claim that FrameBridge already supplies a full enterprise RMM platform. diff --git a/docs/RELEASE-NOTES-0.3.4-dev.md b/docs/RELEASE-NOTES-0.3.4-dev.md new file mode 100644 index 0000000..8c96980 --- /dev/null +++ b/docs/RELEASE-NOTES-0.3.4-dev.md @@ -0,0 +1,34 @@ +# Historical FrameBridge 0.3.4 development notes + +This record describes the earlier private I/O candidate. The final 0.3.4 source and current scope are documented in [the release notes](RELEASE-NOTES-0.3.4.md). + +Version: `0.3.4~dev.20261029.3`. + +This is a development candidate following the signed 0.3.3 release. It does not replace the signed tag, public downloads or stable APT channels. Build success is not installation, reboot, independent-user or multi-customer acceptance. + +This I/O candidate is based on `0.3.4~dev.20261029.1`. The separate `.2` node-initialization experiment is not included. + +## Changes + +- `fb health` and `fb --json health` combine existing local status and prerequisite observations. The report names the receiving computer and profile, separates query success from health and connection intent, and leaves unknown values unknown. It does not create an absent profile, start a service, repair configuration or probe terminal/desktop control. +- Health exits 0 for a complete healthy or intentionally paused observation and 1 for degraded, unavailable or unconfigured state. JSON `result.ok` describes observation completeness; `result.health.healthy` describes health. A requested pause is not healthy when the observed tunnel still runs. +- Fleet read, EXEC, screen and CUA results include `requested_target`, `resolved_target`, `node_id`, `hostname` and `transport` from the confirmed route. Existing `target` semantics remain compatible. Upstream result fields cannot replace the confirmed identity. Errors after resolution retain that identity; unresolved targets never acquire an invented local identity. +- Installed documentation is assembled from the reviewed current source, including the corrected signed 0.3.3 publication record, support scope, first-connection instructions and canonical homepage. Shared CLI/MCP/manpage references are regenerated for this development version. +- Managed connection setup accepts administrator-owned `.config` and `.config/systemd` ancestors when they are directories without group/other write permission. The final `systemd/user` directory must still belong to the connecting user. Symbolic links, other-account ownership and unsafe permissions are rejected. Setup does not change existing directory ownership or permissions. +- Managed-worker state is persisted on transitions instead of repeatedly syncing the same state while idle. Connection liveness heartbeats use the account's validated `/run/user/` runtime directory when available; profiles without it retain a bounded durable fallback. Status still requires a fresh heartbeat bound to the same worker identity. Existing permissions and credentials are unchanged. These changes do not claim that all application or system disk activity stops. + +## Native startup and the GERANAS deployment gap + +Normal `fb connect` already installs the managed user service, protects the connection credential, enables startup without an interactive login and records connection intent. Installation of a package without connection setup cannot establish a new authenticated tunnel. APT preserves a manual profile's existing policy rather than silently enabling it. + +The affected GERANAS deployment had been left as a manually started central profile without the native managed connection setup. External recovery services did not complete that setup and did not share the CLI's lifecycle controls. This candidate fixes the service-account directory compatibility issue needed for native setup; it does not add another external startup mechanism or fabricate managed installation records. + +Source tests and package builds do not prove reconnection after a reboot. Acceptance must separately record a real changed boot ID, native service startup, connection availability and execution through the intended route. No reboot acceptance is claimed by these release notes. + +## Scope + +The three build profiles remain Debian 13 amd64/cp313, Ubuntu 24.04 amd64/cp312 and Linux Mint 22.x amd64/cp312. The new health command observes one selected **local profile**, not the fleet. Invoke it on the intended host and pass `--state-dir` when using a nondefault profile. Runtime readiness and capability availability are not fresh proofs that a command, screenshot or input operation succeeded. + +The existing indicative console, connection intent, boot policy, user profiles, privileged-helper lifecycle and authentication workflow are preserved. Normal browser and operating-system authentication stay in the owner's normal interface. No new enrollment, transport, GUI or recurring approval mechanism is introduced. + +The next product acceptance items are documented in [the operations roadmap](OPERATIONS-ROADMAP.md). Versioned repair workflows, a multi-host health dashboard, customer isolation and other MCP clients remain proposed work until separately implemented and tested. diff --git a/docs/RELEASE-NOTES-0.3.4.md b/docs/RELEASE-NOTES-0.3.4.md new file mode 100644 index 0000000..8d6f9d4 --- /dev/null +++ b/docs/RELEASE-NOTES-0.3.4.md @@ -0,0 +1,29 @@ +# FrameBridge 0.3.4 + +Release profiles: Debian 13 amd64/Python 3.13, Ubuntu 24.04 amd64/Python 3.12 and Linux Mint 22.x amd64/Python 3.12. + +## Network administration and observation + +- `fb health` and `fb --json health` summarize the selected local profile without starting, repairing or connecting it. Query completeness, health, connection intent and unknown observations remain distinct. +- Fleet command, terminal, screen and CUA results identify the confirmed receiving computer, node and transport. Result fields cannot replace the destination identity established by routing. +- Transient broker failures recover with bounded backoff and fresh signed nonces. Acknowledgement retries retain the computed result and do not reexecute commands. Authentication, TLS and malformed-response failures remain terminal. + +## Persistent startup and idle writes + +- Native managed startup accepts secure administrator-owned service-directory ancestors without changing their ownership or permissions. The final user-service directory remains private to its account. +- Node startup avoids redundant privileged setup when the required account settings already exist. Existing credential grants still follow the privileged cleanup lifecycle. +- The managed supervisor persists worker-state transitions instead of repeatedly syncing identical state. Connection heartbeats use the validated account runtime directory when available, with the same process identity and 15-second freshness checks. A bounded durable fallback remains for environments without a user runtime directory. +- APT upgrades preserve profiles and existing startup policy. Normal connection setup provisions the native managed service; installing a package alone cannot establish an authenticated ChatGPT connection. + +## Console, compatibility and delivery + +- The indicative console and persistent terminal remain available. Real-screen capture, terminal transcripts and explicitly selected virtual displays are identified separately. +- The same English command guide serves the CLI, MCP help, installed manual and tutorial. Other distributions, Wayland and CPU architectures are outside this release's validated scope. +- One corresponding-source archive includes the reviewed cp313 and cp312 vendor inputs for all three packages. Public-release assembly requires its matching manifest and checks the archive, current source and selected vendor hashes. +- Original material remains GPL-3.0-only. Preserve both immutable dependency-source assets, third-party notices and stated provenance limits. No new independent native-wheel or tunnel-client rebuild is claimed. + +## Validation and release identity + +Artifact hashes, signatures, signed APT metadata and actual distribution lifecycle results belong to the accompanying release record. Build assembly does not assert that those external checks passed or that a download has been published. Retain the source archive and its sidecar manifest together when rebuilding official release profiles. + +Programming: ChatGPT. Original concept and project coordination: Gerardo Betancourt. diff --git a/docs/THIRD-PARTY-RELEASE-REVIEW.md b/docs/THIRD-PARTY-RELEASE-REVIEW.md index 8e8c236..1949e93 100644 --- a/docs/THIRD-PARTY-RELEASE-REVIEW.md +++ b/docs/THIRD-PARTY-RELEASE-REVIEW.md @@ -1,4 +1,4 @@ -# Third-party terms and source delivery — FrameBridge 0.3.1 +# Third-party terms and source delivery — FrameBridge 0.3.4 This scoped release record combines the retained Python/Rust review with the 0.3.1 OpenAI tunnel-client notice supplement. It does not relabel incorporated components as FrameBridge-owned GPL material. @@ -20,7 +20,7 @@ The executable's own module is marked (devel) and has no embedded VCS revision. ## Delivery conditions and limits -Provide FrameBridge-0.3.1.deb, its exact corresponding FrameBridge source archive, both dependency-source assets, R2 supplement and retained notices through actual working download paths. Verify file hashes at staging and public delivery. Private collection or a future URL is not public availability. +Provide all three FrameBridge 0.3.4 distribution-profile packages, their exact corresponding FrameBridge-0.3.4-source.tar.gz and SOURCE-ARCHIVE-MANIFEST.json, both dependency-source assets, R2 supplement and retained notices through actual working download paths. Verify file hashes at staging and public delivery. Private collection or a future URL is not public availability. The unchanged Certifi arrangement retains its exact source assets, original notice, full MPL-2.0 text and recipient access instructions. The original historical Mozilla certdata revision remains unidentified. No independent native-wheel rebuild is claimed. These provenance/reproducibility limits remain explicit. diff --git a/licensing/SOURCE-LICENSE-SCOPE.json b/licensing/SOURCE-LICENSE-SCOPE.json index a3f3663..d537440 100644 --- a/licensing/SOURCE-LICENSE-SCOPE.json +++ b/licensing/SOURCE-LICENSE-SCOPE.json @@ -1,5 +1,6 @@ { "schema_version": 4, + "historical_snapshot_notice": "This classification snapshot retains its original counts, hashes and version. The matching release SOURCE-ARCHIVE-MANIFEST.json identifies the complete current source set; historical path classifications do not prove ownership.", "project_license": "GPL-3.0-only", "basis": "Owner decision for original FrameBridge material the rightsholders are entitled to license.", "scope_notice": "../LICENSING.md", diff --git a/licensing/third-party/SOURCE-AVAILABILITY.md b/licensing/third-party/SOURCE-AVAILABILITY.md index 0db7213..edf938e 100644 --- a/licensing/third-party/SOURCE-AVAILABILITY.md +++ b/licensing/third-party/SOURCE-AVAILABILITY.md @@ -1,9 +1,9 @@ -# Sources accompanying FrameBridge 0.3.1 +# Sources accompanying FrameBridge 0.3.4 The matching release set is intended to be delivered together through the project download area. This record identifies the required assets; it does not assert that external access has already been opened. -- FrameBridge-0.3.1.deb, Debian version 0.3.1. -- FrameBridge-0.3.1-source.tar.gz: the corresponding FrameBridge implementation, build scripts, tests and legal records. +- FrameBridge-0.3.4-debian13-amd64-py313.deb, FrameBridge-0.3.4-ubuntu24.04-amd64-py312.deb and FrameBridge-0.3.4-mint22-amd64-py312.deb; Debian package version 0.3.4. +- FrameBridge-0.3.4-source.tar.gz and its matching SOURCE-ARCHIVE-MANIFEST.json: the corresponding FrameBridge implementation, reviewed cp313 and cp312 vendor inputs, build scripts, tests and legal records. - FrameBridge-0.3.1-Community-Preview1-dependency-sources.tar.gz: immutable shared Python/Rust dependency source asset, 82,606,998 bytes, SHA-256 64540a85bd65eac3f394d8c0a967cacbc00e112676d57d644dcd1bb691c1926a. The historical filename is deliberately retained; verify bytes rather than inferring the current project version from that name. - FrameBridge-0.3.1-tunnel-client-dependency-sources.tar.gz: exact source module archives for the 47 Go dependencies embedded in the retained OpenAI tunnel executable, plus retained notices and provenance records. Its release hash is recorded in the adjacent release checksum manifest and TUNNEL-DEPENDENCY-SOURCES.json. diff --git a/source/docs/COMMAND-GUIDE.json b/source/docs/COMMAND-GUIDE.json index 178c94f..3d5cbd8 100644 --- a/source/docs/COMMAND-GUIDE.json +++ b/source/docs/COMMAND-GUIDE.json @@ -1,12 +1,12 @@ { "product": "FrameBridge", - "version": "0.3.3", + "version": "0.3.4", "guide_schema": 1, "topics": { "start": { "ok": true, "product": "FrameBridge", - "version": "0.3.3", + "version": "0.3.4", "guide_schema": 1, "topic": "start", "title": "Start with FrameBridge", @@ -24,7 +24,6 @@ "heading": "Install and connect", "body": "Install the package built for your exact Debian 13, Ubuntu 24.04 or Linux Mint 22.x amd64 profile. These are the three release profiles; other distributions and architectures are outside this release. Keep the distribution system Python: 3.13 on Debian 13, 3.12 on Ubuntu 24.04 and Mint 22.x. Use the matching signed APT suite: trixie, noble or mint22. Normal first connection does not require a separate setup, doctor or start sequence. Run connect in a normal interactive terminal; advanced setup is optional for central/node roles and public settings.", "commands": [ - "sudo apt install ./framebridge-expanded_0.3.3_amd64.deb", "fb connect" ] }, @@ -64,12 +63,12 @@ ], "read_only": true, "capabilities_verified": false, - "guide_sha256": "23abd7eec3efb77ca160c97fec9b8bacc7040419946e75584ed563a509577a57" + "guide_sha256": "e2e7a61e9ba53021c155f3fc4d8ffad95ffbdb6e5ac253b171b6ce21a5dd42b4" }, "commands": { "ok": true, "product": "FrameBridge", - "version": "0.3.3", + "version": "0.3.4", "guide_schema": 1, "topic": "commands", "title": "Command reference", @@ -86,6 +85,7 @@ "fb status boot", "fb status console", "fb doctor", + "fb health", "fb setup", "fb console", "fb help" @@ -136,12 +136,12 @@ ], "read_only": true, "capabilities_verified": false, - "guide_sha256": "432ab37e7249793cbe72e42d27582d70c2d2c1394ac1f5418b5d3e211e6a7c06" + "guide_sha256": "d218df5cdc9e9053f461e4a3807e5e9fa45b6194e4b45bf05b6fc2e6258d5d0d" }, "connection": { "ok": true, "product": "FrameBridge", - "version": "0.3.3", + "version": "0.3.4", "guide_schema": 1, "topic": "connection", "title": "Connect and disconnect ChatGPT", @@ -190,12 +190,12 @@ ], "read_only": true, "capabilities_verified": false, - "guide_sha256": "c41d5c436a7039cfacd9cde4933e24c9f7d6235d43e2e9f4fe106f1a4c631764" + "guide_sha256": "418bad3914a250d4c734e0d0a60fcbfcd7e52cd6c5d4dfacc9da236cc4d45e53" }, "lifecycle": { "ok": true, "product": "FrameBridge", - "version": "0.3.3", + "version": "0.3.4", "guide_schema": 1, "topic": "lifecycle", "title": "Services, connection intent and boot policy", @@ -256,12 +256,12 @@ ], "read_only": true, "capabilities_verified": false, - "guide_sha256": "084358c9d1d41e0639b49d01c7e40b613127e3ead77e156cc2624850c7ebdb16" + "guide_sha256": "943322bb685d2dd37fd306bff8b6aed6c85fb54f95af95b986c3448a45dd96b7" }, "nodes": { "ok": true, "product": "FrameBridge", - "version": "0.3.3", + "version": "0.3.4", "guide_schema": 1, "topic": "nodes", "title": "Central and node operation", @@ -285,7 +285,7 @@ }, { "heading": "Targets", - "body": "LOCAL means the actual receiving machine. Use enrolled, confirmed aliases or IDs and inspect the selected target before work. Do not guess SSH identities or use historical lab hostnames as defaults. CLI tty --node can use the authenticated node transport. The interactive console command is local; run it at the destination or use an explicitly configured SSH destination.", + "body": "LOCAL means the actual receiving machine. Use enrolled, confirmed aliases or IDs and inspect the selected target before work. Fleet results include requested_target, resolved_target, node_id, hostname and transport from the confirmed descriptor. The compatibility target field can remain LOCAL; resolved_target names the actual local computer. Unknown targets never fall back to LOCAL. Do not guess SSH identities or use historical lab hostnames as defaults. CLI tty --node can use the authenticated node transport. The interactive console command is local; run it at the destination or use an explicitly configured SSH destination.", "commands": [] }, { @@ -310,12 +310,12 @@ ], "read_only": true, "capabilities_verified": false, - "guide_sha256": "ec2adb6ba2cb36e3747bcb4975a6b777ad88d4fe2b9b1cfb1a2f6ec1e8dab43a" + "guide_sha256": "79f6875d2d2486557092cbc5e5a4888f8661f00eddb13d93437737893eb343a5" }, "terminals": { "ok": true, "product": "FrameBridge", - "version": "0.3.3", + "version": "0.3.4", "guide_schema": 1, "topic": "terminals", "title": "Persistent Bash terminals", @@ -376,12 +376,12 @@ ], "read_only": true, "capabilities_verified": false, - "guide_sha256": "0aba231493d9ecae071ecdf5197440e0714393364948079fc4d163ff70cc67e3" + "guide_sha256": "41e9e1c93bb6c8cc2d84c55d33d70d71693ddf835b5d4c199810db59af932139" }, "screens": { "ok": true, "product": "FrameBridge", - "version": "0.3.3", + "version": "0.3.4", "guide_schema": 1, "topic": "screens", "title": "Authentic screen observation and input", @@ -431,12 +431,12 @@ ], "read_only": true, "capabilities_verified": false, - "guide_sha256": "eeaf9cfed9e7a84d59a727c4388b0869b02a6ed870647e87b333d318a3584c9a" + "guide_sha256": "506c7fcba592ccc473774a809871871285a199e8a8f72b9e230eb23481fd8a85" }, "diagnostics": { "ok": true, "product": "FrameBridge", - "version": "0.3.3", + "version": "0.3.4", "guide_schema": 1, "topic": "diagnostics", "title": "Status and diagnosis", @@ -453,11 +453,19 @@ }, { "heading": "Doctor", - "body": "doctor diagnoses prerequisites and reports findings with suggested next steps. It is not an implicit repair, and it is not a substitute for checking actual live connection state. Inspect the diagnosis and relevant private application logs before considering configuration changes. Candidate eligibility checks the exact OS release, amd64 architecture, package profile and CPython ABI before starting native components. Debian 13/LMDE 7 use cp313, Ubuntu 24.04/Mint 22.x use cp312, and Ubuntu 26.04 uses cp314. A matching profile does not establish a tested installation; never repair an ABI mismatch by replacing system Python or weakening package dependencies.", + "body": "doctor diagnoses prerequisites and reports findings with suggested next steps. It is not an implicit repair, and it is not a substitute for checking actual live connection state. Inspect the diagnosis and relevant private application logs before considering configuration changes. Eligibility checks the exact OS release, amd64 architecture, package profile and CPython ABI before starting native components. The published 0.3.3 scope is Debian 13 with cp313 and Ubuntu 24.04/Mint 22.x with cp312. LMDE 7 and Ubuntu 26.04 builder experiments remain outside that validated scope. A matching profile or a successful development build does not establish a tested installation; never repair an ABI mismatch by replacing system Python or weakening package dependencies.", "commands": [ "fb doctor" ] }, + { + "heading": "Local health summary", + "body": "health combines this local profile's existing status and prerequisite observations into an allowlisted report. It identifies LOCAL as this computer and reports the profile, role, connection intent, local tools, boot policy and desktop availability. It never starts, repairs, connects, changes boot policy or creates an absent profile. It does not scan the fleet or probe terminal commands, screen capture or input. States are healthy, paused, degraded, unavailable and unconfigured. An intentional connection pause can be healthy; missing information cannot. JSON result.ok describes observation completeness; result.health.healthy describes health. Health exits 0 for a complete healthy or intentionally paused observation and 1 for degraded, unavailable or unconfigured state. The CLI version is not proof of the running version. Detailed troubleshooting remains local in status and doctor.", + "commands": [ + "fb health", + "fb --json health" + ] + }, { "heading": "Logs and progress", "body": "Normal connection presents concise progress and a final result, then returns to the prompt. Detailed diagnostics belong in protected application logs. Share only the minimum relevant redacted result; never expose credentials, private keys, browser session data or unrelated personal paths. Avoid rerunning setup, enrollment or broad lifecycle tests merely because a view is confusing.", @@ -481,12 +489,12 @@ ], "read_only": true, "capabilities_verified": false, - "guide_sha256": "77a65897aacfeaa2b06dd184ac78d366d28980847b0cbdd1429c889e0e2898a6" + "guide_sha256": "6f91c71b82e945767d98f113ae1b958f2222939bf94c0657d1ada047b43d6932" }, "security": { "ok": true, "product": "FrameBridge", - "version": "0.3.3", + "version": "0.3.4", "guide_schema": 1, "topic": "security", "title": "Authorization, distribution and safe operation", @@ -521,12 +529,12 @@ ], "read_only": true, "capabilities_verified": false, - "guide_sha256": "f257fcbbdec39395a15655b0a5123fb385ce7eaba2a256de07a5da65eab3ec8e" + "guide_sha256": "0b7c6e55958911306a71f0d0ea5a504df7b1447967b424b5e8a03b4b53275211" }, "compatibility": { "ok": true, "product": "FrameBridge", - "version": "0.3.3", + "version": "0.3.4", "guide_schema": 1, "topic": "compatibility", "title": "Compatibility with earlier commands", @@ -569,8 +577,8 @@ ], "read_only": true, "capabilities_verified": false, - "guide_sha256": "9056f77b27f326bebfdbb54dd10b748d6315797b07afe1aee29f42d6210dae36" + "guide_sha256": "970ed8c5f509e78adc6fd9d99fd14a3edc93fc597184e9de54723763d7807544" } }, - "guide_sha256": "5dc6ed2744e53d7d2ce1f471e2ab94d50f9c52f83ae85abe495bacad34b1b96c" + "guide_sha256": "aecf94c39b17d53be24aecd67089ed5cfa52e3b0499e6a615b3a4e04cfb6e319" } diff --git a/source/docs/COMMAND-REFERENCE.md b/source/docs/COMMAND-REFERENCE.md index b7b26a6..4c521cf 100644 --- a/source/docs/COMMAND-REFERENCE.md +++ b/source/docs/COMMAND-REFERENCE.md @@ -1,6 +1,6 @@ # FrameBridge — command guide -Version: 0.3.3 | Guide schema: 1 +Version: 0.3.4 | Guide schema: 1 This reference is generated from the shared CLI/MCP guide. Use the matching installed version. @@ -36,7 +36,6 @@ fb --json status Install the package built for your exact Debian 13, Ubuntu 24.04 or Linux Mint 22.x amd64 profile. These are the three release profiles; other distributions and architectures are outside this release. Keep the distribution system Python: 3.13 on Debian 13, 3.12 on Ubuntu 24.04 and Mint 22.x. Use the matching signed APT suite: trixie, noble or mint22. Normal first connection does not require a separate setup, doctor or start sequence. Run connect in a normal interactive terminal; advanced setup is optional for central/node roles and public settings. ~~~sh -sudo apt install ./framebridge-expanded_0.3.3_amd64.deb fb connect ~~~ @@ -80,6 +79,7 @@ fb status runtime fb status boot fb status console fb doctor +fb health fb setup fb console fb help @@ -231,7 +231,7 @@ fb nodes status ### Targets -LOCAL means the actual receiving machine. Use enrolled, confirmed aliases or IDs and inspect the selected target before work. Do not guess SSH identities or use historical lab hostnames as defaults. CLI tty --node can use the authenticated node transport. The interactive console command is local; run it at the destination or use an explicitly configured SSH destination. +LOCAL means the actual receiving machine. Use enrolled, confirmed aliases or IDs and inspect the selected target before work. Fleet results include requested_target, resolved_target, node_id, hostname and transport from the confirmed descriptor. The compatibility target field can remain LOCAL; resolved_target names the actual local computer. Unknown targets never fall back to LOCAL. Do not guess SSH identities or use historical lab hostnames as defaults. CLI tty --node can use the authenticated node transport. The interactive console command is local; run it at the destination or use an explicitly configured SSH destination. ### Transport trust @@ -344,12 +344,21 @@ fb status boot ### Doctor -doctor diagnoses prerequisites and reports findings with suggested next steps. It is not an implicit repair, and it is not a substitute for checking actual live connection state. Inspect the diagnosis and relevant private application logs before considering configuration changes. Candidate eligibility checks the exact OS release, amd64 architecture, package profile and CPython ABI before starting native components. Debian 13/LMDE 7 use cp313, Ubuntu 24.04/Mint 22.x use cp312, and Ubuntu 26.04 uses cp314. A matching profile does not establish a tested installation; never repair an ABI mismatch by replacing system Python or weakening package dependencies. +doctor diagnoses prerequisites and reports findings with suggested next steps. It is not an implicit repair, and it is not a substitute for checking actual live connection state. Inspect the diagnosis and relevant private application logs before considering configuration changes. Eligibility checks the exact OS release, amd64 architecture, package profile and CPython ABI before starting native components. The published 0.3.3 scope is Debian 13 with cp313 and Ubuntu 24.04/Mint 22.x with cp312. LMDE 7 and Ubuntu 26.04 builder experiments remain outside that validated scope. A matching profile or a successful development build does not establish a tested installation; never repair an ABI mismatch by replacing system Python or weakening package dependencies. ~~~sh fb doctor ~~~ +### Local health summary + +health combines this local profile's existing status and prerequisite observations into an allowlisted report. It identifies LOCAL as this computer and reports the profile, role, connection intent, local tools, boot policy and desktop availability. It never starts, repairs, connects, changes boot policy or creates an absent profile. It does not scan the fleet or probe terminal commands, screen capture or input. States are healthy, paused, degraded, unavailable and unconfigured. An intentional connection pause can be healthy; missing information cannot. JSON result.ok describes observation completeness; result.health.healthy describes health. Health exits 0 for a complete healthy or intentionally paused observation and 1 for degraded, unavailable or unconfigured state. The CLI version is not proof of the running version. Detailed troubleshooting remains local in status and doctor. + +~~~sh +fb health +fb --json health +~~~ + ### Logs and progress Normal connection presents concise progress and a final result, then returns to the prompt. Detailed diagnostics belong in protected application logs. Share only the minimum relevant redacted result; never expose credentials, private keys, browser session data or unrelated personal paths. Avoid rerunning setup, enrollment or broad lifecycle tests merely because a view is confusing. diff --git a/source/docs/DEBIAN-TUTORIAL.md b/source/docs/DEBIAN-TUTORIAL.md index f6c0f9d..8d1c998 100644 --- a/source/docs/DEBIAN-TUTORIAL.md +++ b/source/docs/DEBIAN-TUTORIAL.md @@ -1,6 +1,6 @@ # FrameBridge — Debian, Ubuntu and Linux Mint installation and first use -Version: 0.3.3 | Guide schema: 1 +Version: 0.3.4 | Guide schema: 1 This reference is generated from the shared CLI/MCP guide. Use the matching installed version. @@ -36,7 +36,6 @@ fb --json status Install the package built for your exact Debian 13, Ubuntu 24.04 or Linux Mint 22.x amd64 profile. These are the three release profiles; other distributions and architectures are outside this release. Keep the distribution system Python: 3.13 on Debian 13, 3.12 on Ubuntu 24.04 and Mint 22.x. Use the matching signed APT suite: trixie, noble or mint22. Normal first connection does not require a separate setup, doctor or start sequence. Run connect in a normal interactive terminal; advanced setup is optional for central/node roles and public settings. ~~~sh -sudo apt install ./framebridge-expanded_0.3.3_amd64.deb fb connect ~~~ @@ -80,6 +79,7 @@ fb status runtime fb status boot fb status console fb doctor +fb health fb setup fb console fb help @@ -231,7 +231,7 @@ fb nodes status ### Targets -LOCAL means the actual receiving machine. Use enrolled, confirmed aliases or IDs and inspect the selected target before work. Do not guess SSH identities or use historical lab hostnames as defaults. CLI tty --node can use the authenticated node transport. The interactive console command is local; run it at the destination or use an explicitly configured SSH destination. +LOCAL means the actual receiving machine. Use enrolled, confirmed aliases or IDs and inspect the selected target before work. Fleet results include requested_target, resolved_target, node_id, hostname and transport from the confirmed descriptor. The compatibility target field can remain LOCAL; resolved_target names the actual local computer. Unknown targets never fall back to LOCAL. Do not guess SSH identities or use historical lab hostnames as defaults. CLI tty --node can use the authenticated node transport. The interactive console command is local; run it at the destination or use an explicitly configured SSH destination. ### Transport trust @@ -344,12 +344,21 @@ fb status boot ### Doctor -doctor diagnoses prerequisites and reports findings with suggested next steps. It is not an implicit repair, and it is not a substitute for checking actual live connection state. Inspect the diagnosis and relevant private application logs before considering configuration changes. Candidate eligibility checks the exact OS release, amd64 architecture, package profile and CPython ABI before starting native components. Debian 13/LMDE 7 use cp313, Ubuntu 24.04/Mint 22.x use cp312, and Ubuntu 26.04 uses cp314. A matching profile does not establish a tested installation; never repair an ABI mismatch by replacing system Python or weakening package dependencies. +doctor diagnoses prerequisites and reports findings with suggested next steps. It is not an implicit repair, and it is not a substitute for checking actual live connection state. Inspect the diagnosis and relevant private application logs before considering configuration changes. Eligibility checks the exact OS release, amd64 architecture, package profile and CPython ABI before starting native components. The published 0.3.3 scope is Debian 13 with cp313 and Ubuntu 24.04/Mint 22.x with cp312. LMDE 7 and Ubuntu 26.04 builder experiments remain outside that validated scope. A matching profile or a successful development build does not establish a tested installation; never repair an ABI mismatch by replacing system Python or weakening package dependencies. ~~~sh fb doctor ~~~ +### Local health summary + +health combines this local profile's existing status and prerequisite observations into an allowlisted report. It identifies LOCAL as this computer and reports the profile, role, connection intent, local tools, boot policy and desktop availability. It never starts, repairs, connects, changes boot policy or creates an absent profile. It does not scan the fleet or probe terminal commands, screen capture or input. States are healthy, paused, degraded, unavailable and unconfigured. An intentional connection pause can be healthy; missing information cannot. JSON result.ok describes observation completeness; result.health.healthy describes health. Health exits 0 for a complete healthy or intentionally paused observation and 1 for degraded, unavailable or unconfigured state. The CLI version is not proof of the running version. Detailed troubleshooting remains local in status and doctor. + +~~~sh +fb health +fb --json health +~~~ + ### Logs and progress Normal connection presents concise progress and a final result, then returns to the prompt. Detailed diagnostics belong in protected application logs. Share only the minimum relevant redacted result; never expose credentials, private keys, browser session data or unrelated personal paths. Avoid rerunning setup, enrollment or broad lifecycle tests merely because a view is confusing. diff --git a/source/docs/framebridge.1 b/source/docs/framebridge.1 index 0b50fca..b39b29f 100644 --- a/source/docs/framebridge.1 +++ b/source/docs/framebridge.1 @@ -1,4 +1,4 @@ -.TH FRAMEBRIDGE 1 "2026-10-28" "FrameBridge 0.3.3" "User Commands" +.TH FRAMEBRIDGE 1 "2026-10-29" "FrameBridge 0.3.4" "User Commands" .SH NAME framebridge, fb \- use local Linux tools and enrolled nodes with ChatGPT .SH SYNOPSIS @@ -21,7 +21,6 @@ fb \-\-json status Install the package built for your exact Debian 13, Ubuntu 24.04 or Linux Mint 22.x amd64 profile. These are the three release profiles; other distributions and architectures are outside this release. Keep the distribution system Python: 3.13 on Debian 13, 3.12 on Ubuntu 24.04 and Mint 22.x. Use the matching signed APT suite: trixie, noble or mint22. Normal first connection does not require a separate setup, doctor or start sequence. Run connect in a normal interactive terminal; advanced setup is optional for central/node roles and public settings. .PP .nf -sudo apt install ./framebridge\-expanded_0.3.3_amd64.deb fb connect .fi .SS Updating and removal @@ -53,6 +52,7 @@ fb status runtime fb status boot fb status console fb doctor +fb health fb setup fb console fb help @@ -162,7 +162,7 @@ fb nodes pending fb nodes status .fi .SS Targets -LOCAL means the actual receiving machine. Use enrolled, confirmed aliases or IDs and inspect the selected target before work. Do not guess SSH identities or use historical lab hostnames as defaults. CLI tty \-\-node can use the authenticated node transport. The interactive console command is local; run it at the destination or use an explicitly configured SSH destination. +LOCAL means the actual receiving machine. Use enrolled, confirmed aliases or IDs and inspect the selected target before work. Fleet results include requested_target, resolved_target, node_id, hostname and transport from the confirmed descriptor. The compatibility target field can remain LOCAL; resolved_target names the actual local computer. Unknown targets never fall back to LOCAL. Do not guess SSH identities or use historical lab hostnames as defaults. CLI tty \-\-node can use the authenticated node transport. The interactive console command is local; run it at the destination or use an explicitly configured SSH destination. .SS Transport trust certificates create manages a private central/node transport CA, not a public website certificate or ChatGPT account authentication. Private keys stay protected on the central host; distribute only the required public trust material. Off\-loopback broker use requires correctly configured verified TLS. Do not recreate trust merely to work around a connection symptom. .PP @@ -237,11 +237,18 @@ fb status runtime fb status boot .fi .SS Doctor -doctor diagnoses prerequisites and reports findings with suggested next steps. It is not an implicit repair, and it is not a substitute for checking actual live connection state. Inspect the diagnosis and relevant private application logs before considering configuration changes. Candidate eligibility checks the exact OS release, amd64 architecture, package profile and CPython ABI before starting native components. Debian 13/LMDE 7 use cp313, Ubuntu 24.04/Mint 22.x use cp312, and Ubuntu 26.04 uses cp314. A matching profile does not establish a tested installation; never repair an ABI mismatch by replacing system Python or weakening package dependencies. +doctor diagnoses prerequisites and reports findings with suggested next steps. It is not an implicit repair, and it is not a substitute for checking actual live connection state. Inspect the diagnosis and relevant private application logs before considering configuration changes. Eligibility checks the exact OS release, amd64 architecture, package profile and CPython ABI before starting native components. The published 0.3.3 scope is Debian 13 with cp313 and Ubuntu 24.04/Mint 22.x with cp312. LMDE 7 and Ubuntu 26.04 builder experiments remain outside that validated scope. A matching profile or a successful development build does not establish a tested installation; never repair an ABI mismatch by replacing system Python or weakening package dependencies. .PP .nf fb doctor .fi +.SS Local health summary +health combines this local profile's existing status and prerequisite observations into an allowlisted report. It identifies LOCAL as this computer and reports the profile, role, connection intent, local tools, boot policy and desktop availability. It never starts, repairs, connects, changes boot policy or creates an absent profile. It does not scan the fleet or probe terminal commands, screen capture or input. States are healthy, paused, degraded, unavailable and unconfigured. An intentional connection pause can be healthy; missing information cannot. JSON result.ok describes observation completeness; result.health.healthy describes health. Health exits 0 for a complete healthy or intentionally paused observation and 1 for degraded, unavailable or unconfigured state. The CLI version is not proof of the running version. Detailed troubleshooting remains local in status and doctor. +.PP +.nf +fb health +fb \-\-json health +.fi .SS Logs and progress Normal connection presents concise progress and a final result, then returns to the prompt. Detailed diagnostics belong in protected application logs. Share only the minimum relevant redacted result; never expose credentials, private keys, browser session data or unrelated personal paths. Avoid rerunning setup, enrollment or broad lifecycle tests merely because a view is confusing. .PP diff --git a/source/expanded/framebridge_expanded/__init__.py b/source/expanded/framebridge_expanded/__init__.py index e19434e..334b899 100644 --- a/source/expanded/framebridge_expanded/__init__.py +++ b/source/expanded/framebridge_expanded/__init__.py @@ -1 +1 @@ -__version__ = "0.3.3" +__version__ = "0.3.4" diff --git a/source/expanded/framebridge_expanded/autostart.py b/source/expanded/framebridge_expanded/autostart.py index add27b0..a5eed99 100644 --- a/source/expanded/framebridge_expanded/autostart.py +++ b/source/expanded/framebridge_expanded/autostart.py @@ -132,6 +132,39 @@ def _prune_helpers(state, keep=()): _command(["sudo", "-n", "--", str(HELPER), "prune", _profile_id(state), ",".join(values) or "-"]) +def _node_helper_cleanup_required(state): + """Only omit privileged cleanup when no managed grant can authorize it. + + The encrypted registry is deliberately private to root. Its orphaned + ciphertext does not grant access without a matching sudo policy. Inspect + only policy names in the secured, root-owned policy directory; never read + rule contents or treat a failed/incomplete inspection as an empty registry. + """ + from .credential_helper import SUDOERS, _root_path + _helper_ready() + uid, profile = str(os.getuid()), _profile_id(state) + prefixes = ("framebridge-credential-" + uid + "-" + profile + "-", + "framebridge-credential-authorize-" + uid + "-" + profile + "-") + patterns = (re.escape(prefixes[0]) + r"[0-9a-f]{64}", + re.escape(prefixes[1]) + r"(?:host|user)") + try: + directory = _root_path(SUDOERS, directory=True) + names = [path.name for path in directory.iterdir()] + except (OSError, RuntimeError): + return True + cleanup_required = False + for name in names: + if any(re.fullmatch(pattern, name) for pattern in patterns): + cleanup_required = True + continue + # A malformed entry in this exact owner's/profile's managed namespace + # is ambiguous. The helper's normal cleanup does not recognize every + # malformed authorization name, so require explicit review instead. + if name.startswith(prefixes): + raise RuntimeError("An ambiguous managed credential policy needs administrator review before node startup can be configured.") + return cleanup_required + + @contextmanager def _credential_transaction(state, encrypted, backend): transaction = {"committed": False} @@ -270,10 +303,17 @@ def _check_unit_directory(path): for directory in [Path.home() / ".config", Path.home() / ".config/systemd", path.parent]: if directory.is_symlink(): raise RuntimeError("User service directories must not be symbolic links.") - directory.mkdir(mode=0o700, exist_ok=True) - info = directory.stat() - if info.st_uid != os.getuid() or info.st_mode & 0o022: - raise RuntimeError("User service directories must be owned by this user and not writable by others.") + try: + directory.mkdir(mode=0o700, exist_ok=True) + except PermissionError: + raise RuntimeError("This account cannot create its user service directory; existing ownership and permissions were left unchanged.") from None + info = directory.lstat() + # Service accounts can have administrator-owned .config ancestors. + # Those ancestors are safe when immutable to other accounts; the + # directory containing the unit must still belong to this owner. + allowed_owner = info.st_uid == os.getuid() or (directory != path.parent and info.st_uid == 0) + if not stat.S_ISDIR(info.st_mode) or not allowed_owner or info.st_mode & 0o022: + raise RuntimeError("User service directories must have a safe owner and not be writable by others; the final directory must belong to this user.") def _manifest(directory): @@ -653,9 +693,15 @@ def _enable(state, *, start_now=True, confirm=True): _check_unit_directory(unit) if unit.exists() or unit.is_symlink() or (directory / "credential.cred").exists() or (directory / "credential.cred").is_symlink(): raise RuntimeError("Existing unowned service or encrypted credential; nothing was replaced.") - _command(["sudo", "-v"], timeout=180) user = pwd.getpwuid(os.getuid()).pw_name linger_before = _linger(user) + helper_cleanup_required = (node and _helper_present() and + (not linger_before or _node_helper_cleanup_required(state))) + # An administrator may already have enabled this node owner's user + # services at boot. Installing its own unit then needs no new privilege. + # Central profiles still authenticate before protecting connection input. + if not node or not linger_before or helper_cleanup_required: + _command(["sudo", "-v"], timeout=180) units_before = _enabled_units() encrypted = b"" print("[2/3] Preparing this node..." if node else "[2/3] Protecting the connection key...", flush=True) @@ -672,7 +718,7 @@ def _enable(state, *, start_now=True, confirm=True): runtime_key = "" if len(encrypted) < 80 or len(encrypted) > 131072: raise RuntimeError("Credential encryption returned an invalid result.") - elif _helper_present(): + elif helper_cleanup_required: _prune_helpers(state) with _credential_transaction(state, encrypted, backend) as transaction: blob = directory / "credential.cred" @@ -693,7 +739,8 @@ def _enable(state, *, start_now=True, confirm=True): record["changed_config_sha256"] = hashlib.sha256(_read(state / "config.json")).hexdigest() _json(directory / "installation.json", record); created.append(directory / "installation.json") print("[3/3] Enabling automatic startup...", flush=True) - _command(["sudo", "-n", "--", "loginctl", "enable-linger", user]) + if not node or not linger_before: + _command(["sudo", "-n", "--", "loginctl", "enable-linger", user]) _systemctl("daemon-reload") if start_now: _systemctl("enable", "--now", unit.name) @@ -946,7 +993,17 @@ def _connection_action(state, action): def _worker_status(directory, name): - _json(directory / "worker-status.json", {"state": name, "updated_at": time.time()}, replace=True) + path = directory / "worker-status.json" + try: + previous = json.loads(_read(path)) + except (FileNotFoundError, ValueError): + previous = None + # This is a transition timestamp, not a liveness heartbeat. systemd and the + # owned process identity report liveness separately. Avoid a durable fsync + # every two seconds while the worker remains in the same state. + if isinstance(previous, dict) and previous.get("state") == name: + return + _json(path, {"state": name, "updated_at": time.time()}, replace=True) def worker(state): diff --git a/source/expanded/framebridge_expanded/cli.py b/source/expanded/framebridge_expanded/cli.py index 8bb3d35..826d9f1 100644 --- a/source/expanded/framebridge_expanded/cli.py +++ b/source/expanded/framebridge_expanded/cli.py @@ -451,6 +451,18 @@ def _doctor_command(args, state): return _result_code(value) +def _health_command(args, state): + health = _module("health") + value = health.collect(state) + if args.json: + _show(value) + else: + print(health.render(value)) + # Query success and operational health are deliberately separate. An + # intentional pause can be healthy; missing/unknown state cannot pass. + return 0 if value.get("ok") is True and value.get("health", {}).get("healthy") is True else 1 + + def _service_command(args, state): action = args.service_action lifecycle = _module("lifecycle") @@ -540,6 +552,7 @@ def build_parser(): status = sub.add_parser("status", help="Observe local tools, ChatGPT connection and startup policy without changing them.") status.add_argument("scope", nargs="?", choices=("connection", "runtime", "boot", "console")) sub.add_parser("doctor", help="Diagnose prerequisites and problems; does not repair or connect.") + sub.add_parser("health", help="Read one local profile's health, identity and prerequisites; no repair or connection.") sub.add_parser("setup", help="Optional advanced role/profile setup; does not start services or connect.") help_parser = sub.add_parser("help", help="Read the installed, versioned English guide.") help_parser.add_argument("topic", nargs="?", default="start", help="Guide topic; use all for the complete reference.") @@ -665,14 +678,14 @@ def _dispatch(parser, args): return _help_command(argparse.Namespace(command="help", topic="start", json=True, non_interactive=True)) parser.print_help() return 0 - print("FrameBridge\n1. Connect\n2. Status\n3. Doctor\n4. Open console\n5. Help\n6. Advanced setup\n0. Exit") + print("FrameBridge\n1. Connect\n2. Status\n3. Doctor\n4. Open console\n5. Help\n6. Advanced setup\n7. Health summary\n0. Exit") try: choice = input("Choose an action: ").strip() except (EOFError, KeyboardInterrupt): print("\nCancelled.", file=sys.stderr) return 130 action = {"1":["connect"], "2":["status"], "3":["doctor"], "4":["console"], - "5":["help"], "6":["setup"]}.get(choice) + "5":["help"], "6":["setup"], "7":["health"]}.get(choice) if choice == "0": return 0 if action is None: @@ -712,6 +725,8 @@ def _dispatch(parser, args): return _status_command(args, state) if args.command == "doctor": return _doctor_command(args, state) + if args.command == "health": + return _health_command(args, state) if args.command == "service": return _service_command(args, state) if args.command == "connection": diff --git a/source/expanded/framebridge_expanded/compatibility.py b/source/expanded/framebridge_expanded/compatibility.py index 2c0f0c6..f701d14 100644 --- a/source/expanded/framebridge_expanded/compatibility.py +++ b/source/expanded/framebridge_expanded/compatibility.py @@ -111,7 +111,7 @@ def assess(prefix, *, os_release=None, machine=None, python_version=None, libc=N expected = PROFILES[manifest["profile_id"]] if (manifest.get("architecture"), manifest.get("python_minor"), manifest.get("python_abi")) != ("amd64", expected[2], expected[3]): raise ValueError("inconsistent profile ABI") - if manifest.get("validation_status") not in {"candidate", "validated"}: + if manifest.get("validation_status") not in {"candidate", "release_build", "validated"}: raise ValueError("missing validation status") if manifest["profile_id"] != selected or manifest["python_minor"] != minor or manifest["python_abi"] != abi: issues.append("Installed artifact profile/ABI does not match this operating system and interpreter.") @@ -126,7 +126,9 @@ def assess(prefix, *, os_release=None, machine=None, python_version=None, libc=N warnings.append("Source checkout has no artifact manifest; only the legacy Debian 13/cp313 profile is eligible.") issues.extend(_vendor_issues(prefix, abi)) validation = manifest.get("validation_status") if manifest else "source-unvalidated" - if validation != "validated": + if validation == "release_build": + warnings.append("Release assembly does not establish a tested installation on this target; consult the external release acceptance record.") + elif validation != "validated": warnings.append("Candidate compatibility checks do not establish a tested installation on this target.") return {"ok": not issues, "profile_id": selected, "artifact_profile_id": manifest.get("profile_id") if manifest else None, "architecture": machine, "python_minor": minor, "python_abi": abi, "python_implementation": implementation, "python_abi_flags": abi_flags, diff --git a/source/expanded/framebridge_expanded/connection_background.py b/source/expanded/framebridge_expanded/connection_background.py index 7c4a498..4fcc07b 100644 --- a/source/expanded/framebridge_expanded/connection_background.py +++ b/source/expanded/framebridge_expanded/connection_background.py @@ -1,6 +1,7 @@ """Profile-scoped detached ChatGPT supervision without a persistent credential.""" from __future__ import annotations import fcntl +import hashlib import json import os import resource @@ -17,6 +18,10 @@ from .config import private_dir, state_path _BOOTSTRAP = "import sys;sys.path.insert(0,sys.argv[1]);from framebridge_expanded.connection_background import worker_main;worker_main(sys.argv[2],int(sys.argv[3]))" _PACKAGE_PARENT = str(Path(__file__).resolve().parents[1]) _STATES = {"starting", "checking", "access", "ready", "retrying", "rejected", "unverified", "error", "stopped", "closed"} +_RUNTIME_PARENT = Path("/run/user") +_HEARTBEAT_BACKEND = "user-runtime-v1" +_IDENTITY_FIELDS = ("pid", "uid", "start_ticks", "boot_id", "session_id", "process_group", "state_dir", "lock_fd") +_DURABLE_HEARTBEAT_INTERVAL = 5.0 _MESSAGES = { "starting": "[1/3] Starting the background tunnel client.", "checking": "[2/3] Verifying the local MCP service.", @@ -59,6 +64,40 @@ def _checked_fd(path, flags, create=False): raise +def _heartbeat_directory(state, create=False): + """Keep disposable liveness updates out of the durable profile filesystem. + + Do not create a login runtime directory, follow symlinks, change existing + permissions, or trust an arbitrary XDG_RUNTIME_DIR supplied by a caller. + Hosts without a user runtime directory retain bounded durable heartbeats. + """ + root = _RUNTIME_PARENT / str(os.getuid()) + for parent in (_RUNTIME_PARENT.parent, _RUNTIME_PARENT): + try: + parent_info = parent.lstat() + except FileNotFoundError: + return None + if not stat.S_ISDIR(parent_info.st_mode) or parent_info.st_uid not in (0, os.getuid()) or stat.S_IMODE(parent_info.st_mode) & 0o022: + raise RuntimeError("The user runtime parent directory is unsafe.") + try: + info = root.lstat() + except FileNotFoundError: + return None + if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) & 0o077: + raise RuntimeError("The user runtime directory is not private and owned by this user.") + name = "framebridge-connection-" + hashlib.sha256(str(state_path(state)).encode()).hexdigest() + directory = root / name + if create: + return private_dir(directory) + try: + info = directory.lstat() + except FileNotFoundError: + return None + if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) & 0o077: + raise RuntimeError("The connection heartbeat directory is not private and owned by this user.") + return directory + + def _read(path): try: fd = _checked_fd(path, os.O_RDONLY) @@ -146,6 +185,11 @@ def status(state): if not alive and phase not in {"error", "closed", "stopped"}: phase = "stopped" checked = record.get("verified_at") + if record.get("heartbeat_backend") == _HEARTBEAT_BACKEND: + heartbeat_directory = _heartbeat_directory(state) + heartbeat = _read(heartbeat_directory / "status.json") if heartbeat_directory else {} + same_worker = all(heartbeat.get(key) == record.get(key) for key in _IDENTITY_FIELDS) + checked = heartbeat.get("verified_at") if same_worker and heartbeat.get("state") == phase else None verified = alive and phase == "ready" and type(checked) in (int, float) and 0 <= time.time() - checked <= 15 if alive and phase == "ready" and not verified: phase = "retrying" @@ -185,9 +229,14 @@ def disconnect(state, *, timeout=16): class _StatusOutput: """Progress destination owned by the worker, with no parent pipe or terminal.""" - def __init__(self, path, identity): + def __init__(self, path, identity, heartbeat_path=None): self.path = path self.record = dict(identity) + self.heartbeat_path = heartbeat_path + if heartbeat_path is not None: + self.record["heartbeat_backend"] = _HEARTBEAT_BACKEND + self.last_observed = None + self.last_durable_write = 0.0 self.pending = "" self.change("starting", "[1/3] Starting the background tunnel client.") @@ -198,13 +247,25 @@ class _StatusOutput: pass def change(self, state, message): + if self.record.get("state") == state and self.record.get("message") == message: + return self.record.update(state=state, message=message, updated_at=time.time()) _write(self.path, self.record) + self.last_durable_write = time.monotonic() def observe(self, state): self.record["checked_at"] = time.time() self.record["verified_at"] = time.time() if state == "ready" else None - _write(self.path, self.record) + if self.heartbeat_path is not None: + heartbeat = {key: self.record[key] for key in _IDENTITY_FIELDS} + heartbeat.update(state=state, checked_at=self.record["checked_at"], verified_at=self.record["verified_at"]) + _write(self.heartbeat_path, heartbeat) + elif state != self.last_observed or time.monotonic() - self.last_durable_write >= _DURABLE_HEARTBEAT_INTERVAL: + # Legacy/manual sessions can run without systemd's user runtime + # directory. Preserve the 15-second freshness contract there too. + _write(self.path, self.record) + self.last_durable_write = time.monotonic() + self.last_observed = state def write(self, text): self.pending += text @@ -243,7 +304,9 @@ def worker_main(state, lock_fd): return if not isinstance(command, list) or not command or any(not isinstance(v, str) or "\0" in v for v in command): return - sink = _StatusOutput(directory / "status.json", _identity(os.getpid(), state, lock_fd)) + heartbeat_directory = _heartbeat_directory(state, create=True) + heartbeat_path = heartbeat_directory / "status.json" if heartbeat_directory else None + sink = _StatusOutput(directory / "status.json", _identity(os.getpid(), state, lock_fd), heartbeat_path) from .tunnel import run_foreground rc = run_foreground(command, credential, state=state, output=sink, observe=sink.observe) except BaseException: diff --git a/source/expanded/framebridge_expanded/fleet.py b/source/expanded/framebridge_expanded/fleet.py index 393aa11..f05677b 100644 --- a/source/expanded/framebridge_expanded/fleet.py +++ b/source/expanded/framebridge_expanded/fleet.py @@ -164,6 +164,19 @@ def resolve_node(alias: str) -> dict[str, Any]: return matches[0] +def _target_identity(requested: str, node: dict[str, Any]) -> dict[str, Any]: + """Describe the confirmed route, never identity claimed by a node response.""" + return { + "requested_target": requested, + "resolved_target": node["alias"], + "node": node["alias"], + "target": "LOCAL" if node["transport"] == "local" else node["alias"], + "node_id": node["node_id"], + "hostname": node.get("hostname"), + "transport": node["transport"], + } + + def _normalize_read(capability: str, args: dict[str, Any]) -> dict[str, Any]: bootstrap_original() from framebridge_linux_chatgpt.fleet_routing import ReadCapability, normalize_read_args @@ -185,6 +198,7 @@ def read_local(capability: str, args: dict[str, Any]) -> dict[str, Any]: def node_read(alias: str, capability: str, args: dict[str, Any]) -> dict[str, Any]: + node = None try: normalized = _normalize_read(capability, args) node = resolve_node(alias) @@ -196,10 +210,13 @@ def node_read(alias: str, capability: str, args: dict[str, Any]) -> dict[str, An result = broker.request(node["node_id"], capability, normalized, timeout_s=30) _require(type(result) is dict, "NODE_READ_RESPONSE_INVALID") response = dict(result) - response.update(node=node["alias"], hostname=node["hostname"], transport=node["transport"]) + response.update(_target_identity(alias, node)) return response except Exception as exc: - return {"ok": False, "error": str(exc) if isinstance(exc, FleetError) else "NODE_READ_FAILED"} + response = {"ok": False, "error": str(exc) if isinstance(exc, FleetError) else "NODE_READ_FAILED"} + if node is not None: + response.update(_target_identity(alias, node)) + return response def _exec_payload(script: str, authorized: bool, timeout_s: int, job_id: str, @@ -224,6 +241,7 @@ def _exec_payload(script: str, authorized: bool, timeout_s: int, job_id: str, def execute_node(alias: str, script: str, authorized: bool, timeout_s: int = 120, job_id: str = "", sha256: str = "", include_stdout: bool = False) -> dict[str, Any]: + node = None try: payload = _exec_payload(script, authorized, timeout_s, job_id, sha256, include_stdout) node = resolve_node(alias) @@ -241,20 +259,24 @@ def execute_node(alias: str, script: str, authorized: bool, timeout_s: int = 120 "NODE_EXEC_JOB_BINDING_INVALID") _require(response.get("mcp_script_sha256", response.get("command_sha256")) == payload["sha256"], "NODE_EXEC_HASH_BINDING_INVALID") - response.update(target="LOCAL" if node["transport"] == "local" else node["alias"], - node_id=node["node_id"], mcp_job_id=payload["job_id"], + response.update(_target_identity(alias, node)) + response.update(mcp_job_id=payload["job_id"], mcp_script_sha256=payload["sha256"]) if include_stdout is not True: response.pop("stdout", None) return response except Exception as exc: - return {"ok": False, "error": str(exc) if isinstance(exc, FleetError) else "NODE_EXEC_FAILED"} + response = {"ok": False, "error": str(exc) if isinstance(exc, FleetError) else "NODE_EXEC_FAILED"} + if node is not None: + response.update(_target_identity(alias, node)) + return response def execute_screen_node(alias: str, script: str, authorized: bool, timeout_s: int = 30, job_id: str = "", sha256: str = "", include_stdout: bool = False) -> dict[str, Any]: """Use the same confirmed-node authority with an explicit non-shell screen operation.""" from .real_screen.screen_transport import REQUEST_TYPE, validate_payload, ScreenTransportError + node = None try: _require(type(timeout_s) is int and 1 <= timeout_s <= 30, "INVALID_SCREEN_TIMEOUT") payload = _exec_payload(script, authorized, timeout_s, job_id, sha256, include_stdout) @@ -280,16 +302,20 @@ def execute_screen_node(alias: str, script: str, authorized: bool, timeout_s: in from .real_screen.screen_rpc import encode response.update(ok=False, exit_code="1", stdout_truncated=False, stdout=encode({"ok": False, "error": {"code": "SCREEN_RPC_UNSUPPORTED_ON_NODE"}}).decode("ascii") + "\n") - response.update(target="LOCAL" if node["transport"] == "local" else node["alias"], - node_id=node["node_id"], mcp_job_id=payload["job_id"], + response.update(_target_identity(alias, node)) + response.update(mcp_job_id=payload["job_id"], mcp_script_sha256=payload["sha256"]) return response except Exception as exc: - return {"ok": False, "error": str(exc) if isinstance(exc, (FleetError, ScreenTransportError)) else "SCREEN_RPC_FAILED"} + response = {"ok": False, "error": str(exc) if isinstance(exc, (FleetError, ScreenTransportError)) else "SCREEN_RPC_FAILED"} + if node is not None: + response.update(_target_identity(alias, node)) + return response def cua_node(alias: str, payload: dict[str, Any], authorized: bool, timeout_s: int = 60, job_id: str = "") -> dict[str, Any]: + node = None try: _require(authorized is True, "AUTHORIZED_TRUE_REQUIRED") _require(type(payload) is dict, "CUA_PAYLOAD_REQUIRED") @@ -316,10 +342,13 @@ def cua_node(alias: str, payload: dict[str, Any], authorized: bool, response = dict(result) if response.get("ok") is True: _require(response.get("job_id") == job, "NODE_CUA_JOB_BINDING_INVALID") - response.update(target="LOCAL" if node["transport"] == "local" else node["alias"], node_id=node["node_id"]) + response.update(_target_identity(alias, node)) return response except Exception as exc: - return {"ok": False, "error": str(exc) if isinstance(exc, FleetError) else "NODE_CUA_FAILED"} + response = {"ok": False, "error": str(exc) if isinstance(exc, FleetError) else "NODE_CUA_FAILED"} + if node is not None: + response.update(_target_identity(alias, node)) + return response def main() -> None: diff --git a/source/expanded/framebridge_expanded/guide_data.py b/source/expanded/framebridge_expanded/guide_data.py index 55eb0f4..0b7b65c 100644 --- a/source/expanded/framebridge_expanded/guide_data.py +++ b/source/expanded/framebridge_expanded/guide_data.py @@ -8,7 +8,7 @@ GUIDE = {'start': {'title': 'Start with FrameBridge', 'commands': ['fb help', 'fb status', 'fb --json status']}, {'heading': 'Install and connect', 'body': 'Install the package built for your exact Debian 13, Ubuntu 24.04 or Linux Mint 22.x amd64 profile. These are the three release profiles; other distributions and architectures are outside this release. Keep the distribution system Python: 3.13 on Debian 13, 3.12 on Ubuntu 24.04 and Mint 22.x. Use the matching signed APT suite: trixie, noble or mint22. Normal first connection does not require a separate setup, doctor or start sequence. Run connect in a normal interactive terminal; advanced setup is optional for central/node roles and public settings.', - 'commands': ["sudo apt install ./framebridge-expanded_" + __version__ + "_amd64.deb", 'fb connect']}, + 'commands': ['fb connect']}, {'heading': 'Updating and removal', 'body': 'Install an official update through the normal package manager when that update has been published. Normal removal stops package-owned managed processes and can retain the user profile and encrypted connection material; it is not a credential-forgetting operation. On systemd 255, package removal also revokes the administrative boot-credential grant. After reinstalling, run fb connect interactively to provision that grant again; retained files alone do not restore startup. Use the explicit credentials forget action first when that is your intended outcome. Do not assume purge erases user-profile data. An explicit downgrade also revokes administrative helper grants before the older package files replace the helper. Helper-backed profiles require a new interactive connect with a compatible package before managed startup can be restored.', 'commands': ['sudo dpkg --remove framebridge-original-expanded']}, @@ -50,6 +50,7 @@ GUIDE = {'start': {'title': 'Start with FrameBridge', 'fb status boot', 'fb status console', 'fb doctor', + 'fb health', 'fb setup', 'fb console', 'fb help']}, @@ -210,7 +211,11 @@ GUIDE = {'start': {'title': 'Start with FrameBridge', {'heading': 'Targets', 'body': 'LOCAL means the actual receiving machine. Use enrolled, ' 'confirmed aliases or IDs and inspect the selected target before ' - 'work. Do not guess SSH identities or use historical lab ' + 'work. Fleet results include requested_target, resolved_target, ' + 'node_id, hostname and transport from the confirmed descriptor. ' + 'The compatibility target field can remain LOCAL; resolved_target ' + 'names the actual local computer. Unknown targets never fall back ' + 'to LOCAL. Do not guess SSH identities or use historical lab ' 'hostnames as defaults. CLI tty --node can use the authenticated ' 'node transport. The interactive console command is local; run it ' 'at the destination or use an explicitly configured SSH ' @@ -327,8 +332,11 @@ GUIDE = {'start': {'title': 'Start with FrameBridge', 'fb status runtime', 'fb status boot']}, {'heading': 'Doctor', - 'body': 'doctor diagnoses prerequisites and reports findings with suggested next steps. It is not an implicit repair, and it is not a substitute for checking actual live connection state. Inspect the diagnosis and relevant private application logs before considering configuration changes. Candidate eligibility checks the exact OS release, amd64 architecture, package profile and CPython ABI before starting native components. Debian 13/LMDE 7 use cp313, Ubuntu 24.04/Mint 22.x use cp312, and Ubuntu 26.04 uses cp314. A matching profile does not establish a tested installation; never repair an ABI mismatch by replacing system Python or weakening package dependencies.', + 'body': 'doctor diagnoses prerequisites and reports findings with suggested next steps. It is not an implicit repair, and it is not a substitute for checking actual live connection state. Inspect the diagnosis and relevant private application logs before considering configuration changes. Eligibility checks the exact OS release, amd64 architecture, package profile and CPython ABI before starting native components. The published 0.3.3 scope is Debian 13 with cp313 and Ubuntu 24.04/Mint 22.x with cp312. LMDE 7 and Ubuntu 26.04 builder experiments remain outside that validated scope. A matching profile or a successful development build does not establish a tested installation; never repair an ABI mismatch by replacing system Python or weakening package dependencies.', 'commands': ['fb doctor']}, + {'heading': 'Local health summary', + 'body': 'health combines this local profile\'s existing status and prerequisite observations into an allowlisted report. It identifies LOCAL as this computer and reports the profile, role, connection intent, local tools, boot policy and desktop availability. It never starts, repairs, connects, changes boot policy or creates an absent profile. It does not scan the fleet or probe terminal commands, screen capture or input. States are healthy, paused, degraded, unavailable and unconfigured. An intentional connection pause can be healthy; missing information cannot. JSON result.ok describes observation completeness; result.health.healthy describes health. Health exits 0 for a complete healthy or intentionally paused observation and 1 for degraded, unavailable or unconfigured state. The CLI version is not proof of the running version. Detailed troubleshooting remains local in status and doctor.', + 'commands': ['fb health', 'fb --json health']}, {'heading': 'Logs and progress', 'body': 'Normal connection presents concise progress and a final ' 'result, then returns to the prompt. Detailed diagnostics ' diff --git a/source/expanded/framebridge_expanded/health.py b/source/expanded/framebridge_expanded/health.py new file mode 100644 index 0000000..da09c90 --- /dev/null +++ b/source/expanded/framebridge_expanded/health.py @@ -0,0 +1,234 @@ +"""Read-only, allowlisted health observations for one local FrameBridge profile. + +This combines existing status and prerequisite checks. It does not execute a +terminal/CUA probe, contact another node, repair a profile or change boot intent. +Snapshot success, observed readiness and compatibility are separate facts. +""" +from __future__ import annotations + +from datetime import datetime, timezone +import re +import socket +import uuid + +from . import __version__, lifecycle, runtime +from .config import state_path + + +_KINDS = frozenset({"physical-x11", "virtual-x11", "text", "not_started", "unavailable"}) +_PHASES = frozenset({"starting", "checking", "access", "unverified", "ready", + "retrying", "error", "rejected", "stopped", "closed"}) +_SERVICES = frozenset({"active", "activating", "deactivating", "inactive", "failed", + "reloading", "maintenance", "refreshing", "not_configured"}) +_CAPABILITIES = ("fbexec", "fbterm", "cua_tty_module", "graphical_cua") + + +def _mapping(value): + return value if isinstance(value, dict) else {} + + +def _boolean(value): + return value if type(value) is bool else None + + +def _name(value): + return value if isinstance(value, str) and re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,252}", value) else None + + +def _node_id(value): + try: + return str(uuid.UUID(value)) if isinstance(value, str) else None + except (ValueError, AttributeError): + return None + + +def _count(value): + return len(value) if isinstance(value, list) else 0 + + +def _choice(value, options, fallback="unavailable"): + return value if isinstance(value, str) and value in options else fallback + + +def _runtime_state(value): + if value.get("ok") is False or value.get("status_unavailable") or type(value.get("running")) is not bool: + return "unavailable" + if value.get("ready") is True and value.get("running") is True: + return "ready" + return "running" if value["running"] else "stopped" + + +def _boot_state(value): + if value.get("configured") is False: + return "not_configured" + if value.get("configured") is not True or type(value.get("enabled")) is not bool: + return "unavailable" + return "enabled" if value["enabled"] else "disabled" + + +def build_report(status_snapshot=None, doctor_snapshot=None, *, observed_at=None, + host=None, status_available=True, doctor_available=True): + """Project snapshots onto a fixed public schema; never copy raw dictionaries. + + Errors deliberately omit exception text and diagnostic free text. Existing + local ``fb doctor`` remains the detailed troubleshooting interface; this + summary can be shown to an assistant without connection metadata or paths. + """ + status = _mapping(status_snapshot) + diagnosis = _mapping(doctor_snapshot) + local = _mapping(status.get("runtime")) + connection = _mapping(status.get("connection")) + boot = _mapping(status.get("boot")) + platform = _mapping(diagnosis.get("platform_compatibility")) + capabilities = _mapping(local.get("capabilities")) + prerequisite_capabilities = _mapping(diagnosis.get("capabilities")) + context = _mapping(local.get("display_context")) + status_available = bool(status_available and isinstance(status_snapshot, dict) and status.get("ok") is not False) + configured = _boolean(status.get("configured")) if status_available else None + role = status.get("role") or local.get("role") + role = _choice(role, {"standalone", "central", "node"}, None) + local_state = _runtime_state(local) if configured is not False else "not_configured" + phase = _choice(connection.get("state"), _PHASES) + intent = _choice(status.get("desired_connection"), {"connected", "disconnected", "not_configured", "not_applicable"}) + kind = _choice(context.get("kind"), _KINDS) + doctor_available = bool(doctor_available and isinstance(doctor_snapshot, dict) and type(diagnosis.get("ok")) is bool) if configured else False + diagnostics_ok = _boolean(diagnosis.get("ok")) if doctor_available else None + connection_available = role == "node" or ( + bool(connection) and connection.get("ok") is not False + and phase != "unavailable" and type(connection.get("verified")) is bool) + boot_available = _boot_state(boot) != "unavailable" + issues = [] + if not status_available: + issues.append("STATUS_UNAVAILABLE") + if configured and not doctor_available: + issues.append("DIAGNOSTICS_UNAVAILABLE") + if configured and local_state == "unavailable": + issues.append("RUNTIME_STATUS_UNAVAILABLE") + if configured and not connection_available: + issues.append("CONNECTION_STATUS_UNAVAILABLE") + if configured and not boot_available: + issues.append("BOOT_STATUS_UNAVAILABLE") + if diagnostics_ok is False: + issues.append("PREREQUISITES_NEED_ATTENTION") + if configured is False: + health_state, healthy = "unconfigured", False + elif (not status_available or configured is None or role is None or not doctor_available + or local_state == "unavailable" or not connection_available or not boot_available): + health_state, healthy = "unavailable", None + elif diagnostics_ok is not True or local_state != "ready": + health_state, healthy = "degraded", False + elif role == "node": + health_state, healthy = "healthy", True + elif intent == "disconnected": + if connection.get("verified") is False and connection.get("alive") is not True and phase in {"stopped", "closed"}: + health_state, healthy = "paused", True + else: + health_state, healthy = "degraded", False + issues.append("CONNECTION_INTENT_MISMATCH") + elif connection.get("verified") is True: + health_state, healthy = "healthy", True + else: + health_state, healthy = "degraded", False + issues.append("CONNECTION_NOT_VERIFIED") + if configured and local_state in {"running", "stopped"}: + issues.append("LOCAL_TOOLS_NOT_READY") + observation_ok = (status_available and configured is not None and local_state != "unavailable" + and (configured is False or (doctor_available and connection_available + and boot_available and role is not None))) + build_profile = platform.get("artifact_profile_id") or platform.get("profile_id") + missing = diagnosis.get("missing_commands") + missing = sorted({v for v in missing if isinstance(v, str) and re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.+-]{0,63}", v)}) if isinstance(missing, list) else [] + notes = ["This is a local profile observation, not a fleet scan.", + "Status and prerequisites are sampled sequentially; this is not an atomic snapshot.", + "No FBEXEC/FBTERM command, screen capture or input probe was performed."] + if role == "node": + notes.append("The central computer provides the ChatGPT connection; this report does not verify that central connection.") + if configured is False: + notes.append("This profile is not configured. No profile or connection was created.") + if kind == "text": + notes.append("Text mode does not require a graphical session.") + return { + "schema_version": 1, "ok": observation_ok, "read_only": True, + "observed_at": observed_at or datetime.now(timezone.utc).isoformat(), + "effective_target": {"selector": "LOCAL", "host": _name(host), + "profile_name": _name(status.get("name") or local.get("name")), + "role": role, "node_id": _node_id(local.get("node_id"))}, + "software": {"cli_version": __version__, "runtime_version": None, + "runtime_version_status": "not_reported"}, + "health": {"state": health_state, "healthy": healthy, "issues": issues}, + "runtime": {"state": local_state, "running": _boolean(local.get("running")) if configured else None, + "ready": _boolean(local.get("ready")) if configured else None, + "availability": {k: _boolean(capabilities.get(k)) for k in _CAPABILITIES}, + "operational_probe_performed": False}, + "connection": {"state": "managed_by_central" if role == "node" else phase, + "verified": None if role == "node" else _boolean(connection.get("verified")), + "desired": intent}, + "boot": {"policy": _boot_state(boot), + "service_state": _choice(boot.get("service_state"), _SERVICES)}, + "prerequisites": {"state": "not_run" if configured is False else ("passed" if diagnostics_ok is True else "needs_attention" if diagnostics_ok is False else "unavailable"), + "compatible": _boolean(platform.get("ok")) if doctor_available else None, + "profile_id": _name(build_profile), + "validation_status": _choice(platform.get("validation_status"), {"candidate", "release_build", "validated", "source-unvalidated"}, "not_reported"), + "missing_commands": missing, + "issue_count": _count(diagnosis.get("issues")), + "compatibility_issue_count": _count(platform.get("issues")), + "availability": {k: _boolean(prerequisite_capabilities.get(k)) for k in _CAPABILITIES}}, + "desktop": {"kind": kind, "graphical_available": _boolean(capabilities.get("graphical_cua")), + "capture_verified_by_this_report": False, "input_verified_by_this_report": False}, + "notes": notes, + } + + +def collect(state): + """Observe only the selected local profile without creating or repairing it.""" + state = state_path(state) + status_snapshot, diagnosis = None, None + status_available = doctor_available = True + try: + status_snapshot = lifecycle.status(state) + except Exception: + status_available = False + if _mapping(status_snapshot).get("configured") is True: + try: + diagnosis = runtime.doctor(state) + except Exception: + doctor_available = False + else: + doctor_available = False + try: + host = socket.gethostname() + except OSError: + host = None + return build_report(status_snapshot, diagnosis, host=host, + status_available=status_available, doctor_available=doctor_available) + + +def render(report): + """Render the same allowlisted report for a human terminal.""" + target = report["effective_target"] + software = report["software"] + state = report["health"]["state"] + lines = ["FrameBridge " + software["cli_version"] + " — health: " + state, + "Target: LOCAL = this computer (" + (target["host"] or "hostname unavailable") + ")", + "Profile: " + (target["profile_name"] or "not available") + " | role: " + (target["role"] or "not available")] + if target["node_id"]: + lines.append("Node ID: " + target["node_id"]) + lines.extend(["Observed: " + report["observed_at"], + "Local tools: " + report["runtime"]["state"], + "Connection: " + ("connected (verified)" if report["connection"]["verified"] is True else report["connection"]["state"]), + "Connection intent: " + report["connection"]["desired"], + "Automatic startup: " + report["boot"]["policy"], + "Current service: " + report["boot"]["service_state"], + "Prerequisites: " + report["prerequisites"]["state"]]) + if report["prerequisites"]["profile_id"]: + lines.append("Build profile: " + report["prerequisites"]["profile_id"]) + lines.append("Desktop: " + report["desktop"]["kind"] + " (capture/input not tested)") + for issue in report["health"]["issues"]: + lines.append("Finding: " + issue) + if report["prerequisites"]["missing_commands"]: + lines.append("Missing commands: " + ", ".join(report["prerequisites"]["missing_commands"])) + if state in {"degraded", "unavailable"}: + lines.append("Inspect details locally: fb status; fb doctor") + lines.append("Runtime version: not reported; the CLI version is not proof of the running version.") + lines.extend(report["notes"]) + return "\n".join(lines) diff --git a/source/packaging/archive_sources.py b/source/packaging/archive_sources.py index dec5aff..d90b5ee 100755 --- a/source/packaging/archive_sources.py +++ b/source/packaging/archive_sources.py @@ -6,11 +6,13 @@ import gzip import hashlib import io import json +import re from pathlib import Path import tarfile import sys from build_profiles import CANDIDATE_VERSION, SOURCE_VENDOR, target_profile, validate_vendor, vendor_versions +from release_metadata import PUBLIC_RELEASE_PROFILES BASE = Path(__file__).resolve().parents[1] ROOT = BASE.parent @@ -25,6 +27,45 @@ BLOG_POLICY_FILES = ("README.md", "LICENSING.md", "POST-TEMPLATE.md", "LICENSE-C ARCHIVE_README_NAME = "README-SOURCE-ARCHIVE.md" DEPENDENCY_SOURCE_ASSETS = [{'filename': 'FrameBridge-0.3.1-Community-Preview1-dependency-sources.tar.gz', 'bytes': 82606998, 'sha256': '64540a85bd65eac3f394d8c0a967cacbc00e112676d57d644dcd1bb691c1926a'}, {'filename': 'FrameBridge-0.3.1-tunnel-client-dependency-sources.tar.gz', 'bytes': 26941745, 'sha256': '6445d4f0e773aba833fe5c610fbae54102b92de2ce636438f769f21e890c2bd9'}] ARCHIVE_README = f"# FrameBridge {CANDIDATE_VERSION} compatibility candidate source\n\nThis source archive is a QA candidate, not a signed public release or evidence\nthat an OS/desktop lifecycle has been tested. Original signed 0.3.1 assets retain\ntheir separate immutable identity. Check TARGET-VENDOR-PROVENANCE.json for the\nnative ABI inputs actually included in this archive.\n\nThe offline builder requires Python 3, Debian dpkg/dpkg-deb and documented host\nbuild prerequisites. It writes source/build/ and source/release/;\nit never installs a package, fetches dependencies, changes system Python or starts\nFrameBridge services. Run from this archive's top-level directory:\n\n python3 source/packaging/build_expanded.py --target debian13-amd64-py313\n python3 source/packaging/build_expanded.py --target lmde7-amd64-py313\n\nIf the cp312 input is recorded as included:\n\n python3 source/packaging/build_expanded.py --target ubuntu24.04-amd64-py312 --vendor-dir source/target-vendors/cp312\n python3 source/packaging/build_expanded.py --target mint22-amd64-py312 --vendor-dir source/target-vendors/cp312\n\nIf the cp314 input is recorded as included:\n\n python3 source/packaging/build_expanded.py --target ubuntu26.04-amd64-py314 --vendor-dir source/target-vendors/cp314\n\nThe default archive includes only the reviewed original cp313 vendor tree.\nThe archive producer can include reviewed alternate ABI trees using:\n\n python3 source/packaging/archive_sources.py --vendor-cp312 /reviewed/vendor-cp312 --vendor-cp314 /reviewed/vendor-cp314\n\nThose arguments must identify complete reviewed trees with the same 29 pinned\ndistribution names/versions as the original. The producer validates WHEEL/RECORD,\nCPython ABI, amd64 ELF and glibc baseline before including them. Local preparation\nmarkers and executable entry-point scripts are omitted. All included files and\ntheir hashes appear in SOURCE-ARCHIVE-MANIFEST.json alongside the finished archive;\nthe embedded provenance records input ABI identity without workstation paths.\n--check-vendor on build_expanded.py validates inputs without building a package.\nAn absent alternate tree must be supplied separately with equivalent provenance;\nchanging Depends cannot convert the original cp313 extensions into cp312/cp314.\n\nPackage output names contain the candidate version and a separate profile\nfolder. release-profile.json inside the installed prefix declares the exact\ncandidate profile. Build success and profile eligibility do not establish a\nvalidated installation, graphical control, real TTY or reboot lifecycle.\n\nOriginal FrameBridge material is GNU GPL version 3 only. Read LICENSE,\nLICENSING.md, COPYRIGHT.md, LICENSE-STATUS.md and OFFICIAL-RELEASES.md for scope,\nattribution, third-party terms and the official-release trust model.\n\nThe project source asset is accompanied by these separate unchanged dependency\nsource assets; they are not embedded or renamed by this helper:\n\n- FrameBridge-0.3.1-Community-Preview1-dependency-sources.tar.gz\n 82,606,998 bytes\n SHA-256 64540a85bd65eac3f394d8c0a967cacbc00e112676d57d644dcd1bb691c1926a\n- FrameBridge-0.3.1-tunnel-client-dependency-sources.tar.gz\n 26,941,745 bytes\n SHA-256 6445d4f0e773aba833fe5c610fbae54102b92de2ce636438f769f21e890c2bd9\n\nThe Preview1 name preserves an immutable shared asset's identity. Retain the R2\nsource-notice supplements and both dependency indices under licensing/third-party.\nMatching package versions do not constitute an independent source rebuild of the\nretained tunnel executable; its exact own-source revision is not asserted.\n\nThe blog directory contains licensing policies and the post template. Draft\narticles, private runtime profiles, QA logs, build outputs and Git history are\noutside the source allowlist. Only source/evidence/BASELINE.json is included from\ntechnical evidence. Release signatures and publication status are separate\nrecords; this candidate archiver does not sign or publish anything.\n".encode("utf-8") +PUBLIC_ARCHIVE_README = f"""# FrameBridge {CANDIDATE_VERSION} corresponding source + +This archive supplies the project source and the reviewed cp313 and cp312 vendor +inputs for Debian 13, Ubuntu 24.04 and Linux Mint 22.x on amd64. Keep its matching +SOURCE-ARCHIVE-MANIFEST.json sidecar beside the archive. Signatures, publication +and actual OS lifecycle acceptance are recorded separately; assembly alone does +not certify them. Historical signed assets retain their immutable identities. + +The offline builder needs Python 3, dpkg/dpkg-deb and the documented host build +prerequisites. It never fetches dependencies, installs packages or starts services. +Extract the archive beside its matching sidecar and run from the extracted root: + + python3 source/packaging/build_expanded.py --target debian13-amd64-py313 --public-release --source-manifest ../SOURCE-ARCHIVE-MANIFEST.json + python3 source/packaging/build_expanded.py --target ubuntu24.04-amd64-py312 --vendor-dir source/target-vendors/cp312 --public-release --source-manifest ../SOURCE-ARCHIVE-MANIFEST.json + python3 source/packaging/build_expanded.py --target mint22-amd64-py312 --vendor-dir source/target-vendors/cp312 --public-release --source-manifest ../SOURCE-ARCHIVE-MANIFEST.json + +Public-release mode verifies the archive hash, every current project source file +and the selected reviewed vendor identity before assembling a package. It does +not assert an independent rebuild of third-party native wheels or tunnel-client. +--check-vendor validates inputs without staging a package. + +Original FrameBridge material is GNU GPL version 3 only. Third-party material +retains its applicable terms. Read LICENSE, LICENSING.md, COPYRIGHT.md and the +source-delivery records. Distribute these separate, unchanged dependency assets +with the release; their historical names do not identify the installer version: + +- FrameBridge-0.3.1-Community-Preview1-dependency-sources.tar.gz + Bytes: 82606998 + SHA-256: 64540a85bd65eac3f394d8c0a967cacbc00e112676d57d644dcd1bb691c1926a +- FrameBridge-0.3.1-tunnel-client-dependency-sources.tar.gz + Bytes: 26941745 + SHA-256: 6445d4f0e773aba833fe5c610fbae54102b92de2ce636438f769f21e890c2bd9 + +The exact own-source revision of the retained tunnel executable is not asserted. +Retain the R2 source-notice supplements, both dependency indices and original +notices. Private profiles, credentials, QA logs, build outputs, Git history and +unpublished blog drafts are excluded from the archive allowlist. +""".encode("utf-8") + GPL_SHA256 = "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986" def sha(path): @@ -81,12 +122,18 @@ def source_files(): def main(argv=None): parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--public-release", action="store_true", help="Assemble corresponding source for all three official release profiles") parser.add_argument("--vendor-cp312", type=Path, help="Reviewed cp312 vendor input to include") parser.add_argument("--vendor-cp314", type=Path, help="Reviewed cp314 vendor input to include") args = parser.parse_args(argv) + if args.public_release and not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", CANDIDATE_VERSION): + parser.error("--public-release requires a stable project version") + if args.public_release and (args.vendor_cp312 is None or args.vendor_cp314 is not None): + parser.error("--public-release requires --vendor-cp312 and excludes experimental cp314 input") names = source_files() expected_versions = vendor_versions(BASE / SOURCE_VENDOR) - provenance = {"schema": 1, "version": CANDIDATE_VERSION, "validation_status": "candidate", + provenance = {"schema": 1, "version": CANDIDATE_VERSION, "validation_status": "release_source" if args.public_release else "candidate", + "release_profiles": list(PUBLIC_RELEASE_PROFILES) if args.public_release else [], "vendors": {"cp313": {"path": "source/" + SOURCE_VENDOR.as_posix(), **validate_vendor(BASE / SOURCE_VENDOR, target_profile("debian13-amd64-py313"), expected_versions)}}} for abi, directory, profile_id in (("cp312", args.vendor_cp312, "ubuntu24.04-amd64-py312"), @@ -108,11 +155,11 @@ def main(argv=None): raise RuntimeError("Archive input path collision: " + name) names[name] = path provenance["omitted_vendor_abis"] = sorted({"cp312", "cp314"} - set(provenance["vendors"])) - if provenance["omitted_vendor_abis"]: + if provenance["omitted_vendor_abis"] and not args.public_release: print("Candidate source excludes alternate vendor ABIs: " + ", ".join(provenance["omitted_vendor_abis"]), file=sys.stderr) - generated = {ARCHIVE_README_NAME: ARCHIVE_README, + generated = {ARCHIVE_README_NAME: PUBLIC_ARCHIVE_README if args.public_release else ARCHIVE_README, "TARGET-VENDOR-PROVENANCE.json": (json.dumps(provenance, indent=2, sort_keys=True) + "\n").encode()} - release = BASE / "release" / "candidate-source" + release = BASE / "release" / ("public-source" if args.public_release else "candidate-source") release.mkdir(parents=True, exist_ok=True) output = release / ARCHIVE_NAME with output.open("wb") as raw: @@ -135,7 +182,7 @@ def main(argv=None): info.mode = 0o755 if path.stat().st_mode & 0o111 else 0o644 with path.open("rb") as stream: archive.addfile(info, stream) - manifest = {"status": "COMPATIBILITY_CANDIDATE_BUILD", "version": CANDIDATE_VERSION, "vendor_inputs": provenance, "archive": output.name, + manifest = {"status": "RELEASE_SOURCE_ASSEMBLED" if args.public_release else "COMPATIBILITY_CANDIDATE_BUILD", "version": CANDIDATE_VERSION, "vendor_inputs": provenance, "archive": output.name, "bytes": output.stat().st_size, "sha256": sha(output), "files": [{"path": name, "sha256": hashlib.sha256(data).hexdigest(), "generated": True} for name, data in sorted(generated.items())] diff --git a/source/packaging/build_expanded.py b/source/packaging/build_expanded.py index 717a2ab..dac5f4c 100755 --- a/source/packaging/build_expanded.py +++ b/source/packaging/build_expanded.py @@ -14,10 +14,13 @@ import shlex import subprocess from build_profiles import CANDIDATE_VERSION, PROFILES, SOURCE_VENDOR, VENDOR_RELATIVE, target_profile, validate_vendor, vendor_versions +from release_metadata import release_source BASE = Path(__file__).resolve().parents[1] ROOT = BASE.parent VERSION = CANDIDATE_VERSION +PUBLIC_RELEASE = False +SOURCE_ARCHIVE = None # Oldest package retaining the privileged helper lifecycle. Candidate/release # version bumps must not move this downgrade revocation boundary. PRIVILEGED_HELPER_FLOOR = "0.3.2" @@ -35,6 +38,14 @@ def sha(path): return hashlib.sha256(path.read_bytes()).hexdigest() +def contains_private_path(data, private_root): + """Match an actual path component, not e.g. /root in root_model.py.""" + prefix = str(private_root).rstrip("/").encode() + if not prefix: + return False + return re.search(re.escape(prefix) + rb"(?=$|[/\s\"'<>`])", data) is not None + + # These are release inputs, not files inferred from installed runtime profiles. LEGAL_FILES = ("LICENSE", "LICENSING.md", "COPYRIGHT.md", "LICENSE-STATUS.md", "OFFICIAL-RELEASES.md") THIRD_PARTY_REQUIRED = ("THIRD-PARTY-NOTICES.md", "DEPENDENCY-SOURCES.json", "SOURCE-AVAILABILITY.md", "R2-SUPPLEMENTS.json", "NATIVE-DEPENDENCY-TERMS.json", "NATIVE-DEPENDENCY-TERMS.md", "TUNNEL-DEPENDENCY-SOURCES.json") @@ -83,7 +94,7 @@ def legal_preflight(): for path in inputs: data = path.read_bytes() for private_root in private_roots: - if private_root.encode() in data: + if contains_private_path(data, private_root): raise RuntimeError("Local path found in legal input: " + str(path.relative_to(ROOT))) return index @@ -101,7 +112,7 @@ def install_legal_material(docs): shutil.copy2(ROOT / "blog" / name, docs / "blog" / name) shutil.copy2(ROOT / "RELEASE-NOTES.md", docs / "RELEASE-NOTES.md") (docs / "copyright").write_text( - "FrameBridge Original Expanded — compatibility candidate " + VERSION + "\n\n" + "FrameBridge Original Expanded — " + ("release " if PUBLIC_RELEASE else "compatibility candidate ") + VERSION + "\n\n" "Original FrameBridge material: GNU GPL version 3 only (GPL-3.0-only).\n" "The complete license is included in LICENSE. COPYRIGHT.md and LICENSING.md\n" "identify attribution, scope and third-party exceptions.\n\n" @@ -115,7 +126,8 @@ def install_legal_material(docs): "[licensing/third-party](licensing/third-party/THIRD-PARTY-NOTICES.md).\n" "Original vendored notices are preserved with their components.\n") lines = ["# Source availability for FrameBridge " + VERSION, "", - "Candidate source must be retained with its build identity; no public archive is asserted.", "", + ("Matching corresponding source: " + SOURCE_ARCHIVE["filename"] + " (SHA-256 " + SOURCE_ARCHIVE["sha256"] + ")." + if PUBLIC_RELEASE else "Candidate source must be retained with its build identity; no public archive is asserted."), "", "The release source set also includes these separate assets:", ""] for item in DEPENDENCY_SOURCE_ASSETS: lines.extend(["- " + item["filename"], @@ -131,12 +143,13 @@ def install_legal_material(docs): "signature status are external release records, not builder claims.", ""]) (docs / "SOURCE-AVAILABILITY.md").write_text("\n".join(lines)) (docs / "COMMUNITY-RELEASE-STATUS.json").write_text(json.dumps({ - "schema_version": 1, "version": VERSION, "status": "COMPATIBILITY_CANDIDATE_BUILD", + "schema_version": 1, "version": VERSION, + "status": "PUBLIC_RELEASE_BUILD_ASSEMBLED" if PUBLIC_RELEASE else "COMPATIBILITY_CANDIDATE_BUILD", "certified_as_RC7": False, "public_readiness": "EXTERNAL_RELEASE_RECORD", "signature_status": "EXTERNAL_RELEASE_RECORD", "vm_lifecycle_validation": "NOT_ASSERTED_BY_BUILDER", - "source_archive": "CANDIDATE_SOURCE_REQUIRED_BEFORE_PUBLICATION", + "source_archive": SOURCE_ARCHIVE if PUBLIC_RELEASE else "CANDIDATE_SOURCE_REQUIRED_BEFORE_PUBLICATION", "dependency_source_archives": DEPENDENCY_SOURCE_ASSETS }, indent=2) + "\n") @@ -175,7 +188,7 @@ def maintainer_prerm(version): def maintainer_preinst(version=None): - """Snapshot active profiles without stopping the process carrying APT.""" + """Snapshot running profiles for upgrades and same-version reinstalls.""" version = version or VERSION if not re.fullmatch(r"[0-9][0-9A-Za-z.+:~\\-]*", version): raise ValueError("Invalid package upgrade version") @@ -183,7 +196,7 @@ def maintainer_preinst(version=None): module = module.rsplit("if __name__ == '__main__':", 1)[0] return ("#!/bin/sh\nset -eu\ncase \"$1\" in\n upgrade)\n" '/usr/bin/dpkg --validate-version "$2"\n' - 'if /usr/bin/dpkg --compare-versions "$2" lt ' + shlex.quote(version) + "; then\n" + 'if /usr/bin/dpkg --compare-versions "$2" le ' + shlex.quote(version) + "; then\n" "/usr/bin/python3 -I -S - <<'FRAMEBRIDGE_UPGRADE_SNAPSHOT'\n" + module + "\nraise SystemExit(main(['snapshot']))\n" + "FRAMEBRIDGE_UPGRADE_SNAPSHOT\nfi\n ;;\nesac\n") @@ -204,17 +217,25 @@ def package_relationships(profile): def main(argv=None): - global VERSION, PUBLIC_FILENAME, RELEASE, STAGE, PREFIX + global VERSION, PUBLIC_FILENAME, RELEASE, STAGE, PREFIX, PUBLIC_RELEASE, SOURCE_ARCHIVE parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--target", required=True, choices=sorted(PROFILES)) parser.add_argument("--version", default=CANDIDATE_VERSION) parser.add_argument("--vendor-dir", type=Path, help="Reviewed target-ABI Fleet Hub vendor tree; never installed globally") + parser.add_argument("--public-release", action="store_true", help="Bind an official release profile to its assembled corresponding-source archive") + parser.add_argument("--source-manifest", type=Path, help="Matching SOURCE-ARCHIVE-MANIFEST.json kept beside its source archive") parser.add_argument("--check-vendor", action="store_true", help="Validate inputs without staging or building a package") args = parser.parse_args(argv) if not re.fullmatch(r"[0-9][0-9A-Za-z.+:~\-]*", args.version) or args.version == "0.3.1": parser.error("A distinct valid Debian candidate version is required") subprocess.run(["dpkg", "--validate-version", args.version], check=True) VERSION = args.version + PUBLIC_RELEASE = args.public_release + SOURCE_ARCHIVE = None + if args.public_release and args.source_manifest is None: + parser.error("--public-release requires --source-manifest") + if args.source_manifest and not args.public_release: + parser.error("--source-manifest requires --public-release") version_assignments = [node for node in ast.parse((BASE / "expanded/framebridge_expanded/__init__.py").read_text()).body if isinstance(node, ast.Assign) and any(isinstance(item, ast.Name) and item.id == "__version__" for item in node.targets)] if len(version_assignments) != 1 or ast.literal_eval(version_assignments[0].value) != VERSION: @@ -227,6 +248,11 @@ def main(argv=None): parser.error("This target requires --vendor-dir with reviewed " + profile["python_abi"] + " wheels") vendor_evidence = validate_vendor(vendor, profile, vendor_versions(original_vendor)) profile["vendor"] = vendor_evidence + if args.public_release: + from archive_sources import source_files + SOURCE_ARCHIVE = release_source(args.source_manifest, VERSION, args.target, source_files(), vendor_evidence) + profile["validation_status"] = "release_build" + profile["source_archive"] = SOURCE_ARCHIVE PUBLIC_FILENAME = "FrameBridge-" + VERSION + "-" + args.target + ".deb" RELEASE = BASE / "release" / args.target STAGE = BASE / "build" / args.target / "package-root" @@ -352,7 +378,7 @@ def main(argv=None): env = dict(os.environ, SOURCE_DATE_EPOCH=str(EPOCH)) subprocess.run(["dpkg-deb", "--root-owner-group", "-Zxz", "-z6", "--build", str(STAGE), str(output)], check=True, env=env) shutil.copy2(output, RELEASE / PUBLIC_FILENAME) - print(json.dumps({"package": str(output), "bytes": output.stat().st_size, "sha256": sha(output), "payload_entries": len(manifest), "profile_id": args.target, "validation_status": "candidate", "public_filename": PUBLIC_FILENAME, "status": "COMPATIBILITY_CANDIDATE_BUILD"})) + print(json.dumps({"package": str(output), "bytes": output.stat().st_size, "sha256": sha(output), "payload_entries": len(manifest), "profile_id": args.target, "validation_status": "release_build" if PUBLIC_RELEASE else "candidate", "public_filename": PUBLIC_FILENAME, "status": "PUBLIC_RELEASE_BUILD_ASSEMBLED" if PUBLIC_RELEASE else "COMPATIBILITY_CANDIDATE_BUILD"})) if __name__ == "__main__": main() diff --git a/source/packaging/build_profiles.py b/source/packaging/build_profiles.py index 2124307..346eaca 100644 --- a/source/packaging/build_profiles.py +++ b/source/packaging/build_profiles.py @@ -11,7 +11,7 @@ from pathlib import Path import re -CANDIDATE_VERSION = "0.3.3" +CANDIDATE_VERSION = "0.3.4" VENDOR_RELATIVE = Path("original/geranas/FrameBridge-Fleet-Hub/vendor") SOURCE_VENDOR = Path("sources/original-geranas/FrameBridge-Fleet-Hub/vendor") GLIBC_BASELINE = (2, 34) diff --git a/source/packaging/generate_command_docs.py b/source/packaging/generate_command_docs.py index 83ca95e..9b5df38 100644 --- a/source/packaging/generate_command_docs.py +++ b/source/packaging/generate_command_docs.py @@ -26,7 +26,7 @@ def markdown(data, title): def roff(text): return text.replace('\\','\\e').replace('-','\\-') def man(data): - lines=['.TH FRAMEBRIDGE 1 "2026-10-28" "FrameBridge '+__version__+'" "User Commands"', + lines=['.TH FRAMEBRIDGE 1 "2026-10-29" "FrameBridge '+__version__+'" "User Commands"', '.SH NAME','framebridge, fb \\- use local Linux tools and enrolled nodes with ChatGPT', '.SH SYNOPSIS','.B framebridge', '[\\-\\-json] [\\-\\-state\\-dir DIRECTORY] COMMAND [ARGUMENTS]', diff --git a/source/packaging/release_metadata.py b/source/packaging/release_metadata.py new file mode 100644 index 0000000..0e47817 --- /dev/null +++ b/source/packaging/release_metadata.py @@ -0,0 +1,87 @@ +"""Bind release packages to their corresponding source; never certify runtime QA.""" +import hashlib +import json +from pathlib import Path, PurePosixPath +import re +import tarfile + + +PUBLIC_RELEASE_PROFILES = ( + "debian13-amd64-py313", "ubuntu24.04-amd64-py312", "mint22-amd64-py312", +) + + +def release_source(manifest_path, version, profile_id, current_files, vendor_evidence): + """Require the same reviewed source and ABI input used by this package. + + The manifest and archive are release inputs kept beside one another. Their + signatures and the OS acceptance record are verified outside the builder. + """ + if profile_id not in PUBLIC_RELEASE_PROFILES or not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", version): + raise RuntimeError("Public release requires a stable version and an official release profile") + manifest_path = Path(manifest_path) + data = json.loads(manifest_path.read_text()) + expected_name = "FrameBridge-" + version + "-source.tar.gz" + if (data.get("version"), data.get("status"), data.get("archive")) != (version, "RELEASE_SOURCE_ASSEMBLED", expected_name): + raise RuntimeError("Matching release source manifest is required") + archive = manifest_path.parent / expected_name + if archive.is_symlink() or not archive.is_file(): + raise RuntimeError("Matching release source archive is missing") + digest = hashlib.sha256(archive.read_bytes()).hexdigest() + if archive.stat().st_size != data.get("bytes") or digest != data.get("sha256"): + raise RuntimeError("Release source archive hash or size differs from its manifest") + vendors = data.get("vendor_inputs", {}).get("vendors", {}) + if set(vendors) != {"cp312", "cp313"}: + raise RuntimeError("Release source must contain the reviewed cp312 and cp313 inputs") + abi = "cp313" if profile_id == "debian13-amd64-py313" else "cp312" + if vendors[abi].get("vendor_sha256") != vendor_evidence.get("vendor_sha256"): + raise RuntimeError("Package vendor input differs from corresponding source") + rows = data.get("files", []) + if not isinstance(rows, list) or any(not isinstance(row, dict) for row in rows): + raise RuntimeError("Invalid release source file manifest") + recorded = {row.get("path"): row.get("sha256") for row in rows} + if len(recorded) != len(rows): + raise RuntimeError("Duplicate release source manifest path") + seen = set() + prefix = "FrameBridge-" + version + "/" + with tarfile.open(archive, "r:gz") as source: + for member in source: + if not member.isfile() or not member.name.startswith(prefix): + raise RuntimeError("Unexpected release source archive member") + relative = member.name[len(prefix):] + if not relative or PurePosixPath(relative).is_absolute() or ".." in PurePosixPath(relative).parts: + raise RuntimeError("Unsafe release source archive path") + if relative in seen or relative not in recorded: + raise RuntimeError("Duplicate or unrecorded release source archive member") + seen.add(relative) + with source.extractfile(member) as stream: + if hashlib.sha256(stream.read()).hexdigest() != recorded[relative]: + raise RuntimeError("Release source archive content differs from its manifest") + if seen != set(recorded): + raise RuntimeError("Release source manifest contains files absent from the archive") + # Bind the ABI claims to the archived bytes, including the ABI not selected + # by this particular package. A manifest field alone is not vendor evidence. + for vendor_abi, expected_prefix in ( + ("cp313", "source/sources/original-geranas/FrameBridge-Fleet-Hub/vendor"), + ("cp312", "source/target-vendors/cp312"), + ): + if vendors[vendor_abi].get("path") != expected_prefix: + raise RuntimeError("Unexpected corresponding-source vendor path") + vendor_digest = hashlib.sha256() + # validate_vendor sorts Path objects, whose component ordering differs + # from slash-delimited strings for package/ versus package-1.dist-info/. + for path, file_digest in sorted(recorded.items(), key=lambda row: PurePosixPath(row[0]).parts): + if not path.startswith(expected_prefix + "/"): + continue + relative = path[len(expected_prefix) + 1:] + parts = PurePosixPath(relative).parts + if "__pycache__" in parts or "bin" in parts or relative.endswith(".pyc") or parts[-1] == "FB-ABI-PREPARED.json": + continue + vendor_digest.update(relative.encode() + b"\0") + vendor_digest.update(bytes.fromhex(file_digest)) + if vendor_digest.hexdigest() != vendors[vendor_abi].get("vendor_sha256"): + raise RuntimeError("Archived vendor bytes differ from their recorded ABI identity") + for relative, path in current_files.items(): + if recorded.get(relative) != hashlib.sha256(path.read_bytes()).hexdigest(): + raise RuntimeError("Package source differs from corresponding archive: " + relative) + return {"filename": expected_name, "bytes": data["bytes"], "sha256": digest} diff --git a/source/portable/front_door_mcp.py b/source/portable/front_door_mcp.py index 29ca06f..6f2bf77 100644 --- a/source/portable/front_door_mcp.py +++ b/source/portable/front_door_mcp.py @@ -50,7 +50,7 @@ mcp = FastMCP( "Use the installed CLI and tools for normal work; use a separate source clone when the owner requests software development. " "The six Fleet inspection tools are read-only. Guide instructions do not grant authorization." ), - website_url="https://betancourt.one/framebridge/", + website_url="https://framebridge.betancourt.one/", host=HOST, port=PORT, streamable_http_path="/mcp", diff --git a/source/target-vendors/cp312/_cffi_backend.cpython-312-x86_64-linux-gnu.so b/source/target-vendors/cp312/_cffi_backend.cpython-312-x86_64-linux-gnu.so new file mode 100644 index 0000000..c10f359 Binary files /dev/null and b/source/target-vendors/cp312/_cffi_backend.cpython-312-x86_64-linux-gnu.so differ diff --git a/source/target-vendors/cp312/annotated_types-0.8.0.dist-info/METADATA b/source/target-vendors/cp312/annotated_types-0.8.0.dist-info/METADATA new file mode 100644 index 0000000..eb5a088 --- /dev/null +++ b/source/target-vendors/cp312/annotated_types-0.8.0.dist-info/METADATA @@ -0,0 +1,295 @@ +Metadata-Version: 2.4 +Name: annotated-types +Version: 0.8.0 +Summary: Reusable constraint types to use with typing.Annotated +Project-URL: Homepage, https://github.com/annotated-types/annotated-types +Project-URL: Source, https://github.com/annotated-types/annotated-types +Project-URL: Changelog, https://github.com/annotated-types/annotated-types/releases +Author-email: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>, Samuel Colvin , Zac Hatfield-Dodds +License-Expression: MIT +License-File: LICENSE +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Console +Classifier: Environment :: MacOS X +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Information Technology +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: POSIX :: Linux +Classifier: Operating System :: Unix +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Typing :: Typed +Requires-Python: >=3.10 +Description-Content-Type: text/markdown + +# annotated-types + +[![CI](https://github.com/annotated-types/annotated-types/workflows/CI/badge.svg?event=push)](https://github.com/annotated-types/annotated-types/actions?query=event%3Apush+branch%3Amain+workflow%3ACI) +[![pypi](https://img.shields.io/pypi/v/annotated-types.svg)](https://pypi.python.org/pypi/annotated-types) +[![versions](https://img.shields.io/pypi/pyversions/annotated-types.svg)](https://github.com/annotated-types/annotated-types) +[![license](https://img.shields.io/github/license/annotated-types/annotated-types.svg)](https://github.com/annotated-types/annotated-types/blob/main/LICENSE) + +[PEP-593](https://peps.python.org/pep-0593/) added `typing.Annotated` as a way of +adding context-specific metadata to existing types, and specifies that +`Annotated[T, x]` _should_ be treated as `T` by any tool or library without special +logic for `x`. + +This package provides metadata objects which can be used to represent common +constraints such as upper and lower bounds on scalar values and collection sizes, +a `Predicate` marker for runtime checks, and +descriptions of how we intend these metadata to be interpreted. In some cases, +we also note alternative representations which do not require this package. + +## Install + +```bash +pip install annotated-types +``` + +## Examples + +```python +from typing import Annotated +from annotated_types import Gt, Len, Predicate + +class MyClass: + age: Annotated[int, Gt(18)] # Valid: 19, 20, ... + # Invalid: 17, 18, "19", 19.0, ... + factors: list[Annotated[int, Predicate(is_prime)]] # Valid: 2, 3, 5, 7, 11, ... + # Invalid: 4, 8, -2, 5.0, "prime", ... + + my_list: Annotated[list[int], Len(0, 10)] # Valid: [], [10, 20, 30, 40, 50] + # Invalid: (1, 2), ["abc"], [0] * 20 +``` + +## Documentation + +_While `annotated-types` avoids runtime checks for performance, users should not +construct invalid combinations such as `MultipleOf("non-numeric")` or `Annotated[int, Len(3)]`. +Downstream implementors may choose to raise an error, emit a warning, silently ignore +a metadata item, etc., if the metadata objects described below are used with an +incompatible type - or for any other reason!_ + +### Gt, Ge, Lt, Le + +Express inclusive and/or exclusive bounds on orderable values - which may be numbers, +dates, times, strings, sets, etc. Note that the boundary value need not be of the +same type that was annotated, so long as they can be compared: `Annotated[int, Gt(1.5)]` +is fine, for example, and implies that the value is an integer x such that `x > 1.5`. + +We suggest that implementors may also interpret `functools.partial(operator.le, 1.5)` +as being equivalent to `Gt(1.5)`, for users who wish to avoid a runtime dependency on +the `annotated-types` package. + +To be explicit, these types have the following meanings: + +* `Gt(x)` - value must be "Greater Than" `x` - equivalent to exclusive minimum +* `Ge(x)` - value must be "Greater than or Equal" to `x` - equivalent to inclusive minimum +* `Lt(x)` - value must be "Less Than" `x` - equivalent to exclusive maximum +* `Le(x)` - value must be "Less than or Equal" to `x` - equivalent to inclusive maximum + +### Interval + +`Interval(gt, ge, lt, le)` allows you to specify an upper and lower bound with a single +metadata object. `None` attributes should be ignored, and non-`None` attributes +treated as per the single bounds above. + +### MultipleOf + +`MultipleOf(multiple_of=x)` might be interpreted in two ways: + +1. Python semantics, implying `value % multiple_of == 0`, or +2. [JSONschema semantics](https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.6.2.1), + where `int(value / multiple_of) == value / multiple_of`. + +We encourage users to be aware of these two common interpretations and their +distinct behaviours, especially since very large or non-integer numbers make +it easy to cause silent data corruption due to floating-point imprecision. + +We encourage libraries to carefully document which interpretation they implement. + +### MinLen, MaxLen, Len + +`Len()` implies that `min_length <= len(value) <= max_length` - lower and upper bounds are inclusive. + +As well as `Len()` which can optionally include upper and lower bounds, we also +provide `MinLen(x)` and `MaxLen(y)` which are equivalent to `Len(min_length=x)` +and `Len(max_length=y)` respectively. + +`Len`, `MinLen`, and `MaxLen` may be used with any type which supports `len(value)`. + +Examples of usage: + +* `Annotated[list, MaxLen(10)]` (or `Annotated[list, Len(max_length=10)]`) - list must have a length of 10 or less +* `Annotated[str, MaxLen(10)]` - string must have a length of 10 or less +* `Annotated[list, MinLen(3)]` (or `Annotated[list, Len(min_length=3)]`) - list must have a length of 3 or more +* `Annotated[list, Len(4, 6)]` - list must have a length of 4, 5, or 6 +* `Annotated[list, Len(8, 8)]` - list must have a length of exactly 8 + +#### Changed in v0.4.0 + +* `min_inclusive` has been renamed to `min_length`, no change in meaning +* `max_exclusive` has been renamed to `max_length`, upper bound is now **inclusive** instead of **exclusive** +* The recommendation that slices are interpreted as `Len` has been removed due to ambiguity and different semantic + meaning of the upper bound in slices vs. `Len` + +See [issue #23](https://github.com/annotated-types/annotated-types/issues/23) for discussion. + +### Timezone + +`Timezone` can be used with a `datetime` or a `time` to express which timezones +are allowed. `Annotated[datetime, Timezone(None)]` must be a naive datetime. +`Timezone[...]` ([literal ellipsis](https://docs.python.org/3/library/constants.html#Ellipsis)) +expresses that any timezone-aware datetime is allowed. You may also pass a specific +timezone string or [`tzinfo`](https://docs.python.org/3/library/datetime.html#tzinfo-objects) +object such as `Timezone(timezone.utc)` or `Timezone("Africa/Abidjan")` to express that you only +allow a specific timezone, though we note that this is often a symptom of fragile design. + +#### Changed in v0.x.x + +* `Timezone` accepts [`tzinfo`](https://docs.python.org/3/library/datetime.html#tzinfo-objects) objects instead of + `timezone`, extending compatibility to [`zoneinfo`](https://docs.python.org/3/library/zoneinfo.html) and third party libraries. + +### Unit + +`Unit(unit: str)` expresses that the annotated numeric value is the magnitude of +a quantity with the specified unit. For example, `Annotated[float, Unit("m/s")]` +would be a float representing a velocity in meters per second. + +Please note that `annotated_types` itself makes no attempt to parse or validate +the unit string in any way. That is left entirely to downstream libraries, +such as [`pint`](https://pint.readthedocs.io) or +[`astropy.units`](https://docs.astropy.org/en/stable/units/). + +An example of how a library might use this metadata: + +```python +from annotated_types import Unit +from typing import Annotated, TypeVar, Callable, Any, get_origin, get_args + +# given a type annotated with a unit: +Meters = Annotated[float, Unit("m")] + + +# you can cast the annotation to a specific unit type with any +# callable that accepts a string and returns the desired type +T = TypeVar("T") +def cast_unit(tp: Any, unit_cls: Callable[[str], T]) -> T | None: + if get_origin(tp) is Annotated: + for arg in get_args(tp): + if isinstance(arg, Unit): + return unit_cls(arg.unit) + return None + + +# using `pint` +import pint +pint_unit = cast_unit(Meters, pint.Unit) + + +# using `astropy.units` +import astropy.units as u +astropy_unit = cast_unit(Meters, u.Unit) +``` + +### Predicate + +`Predicate(func: Callable)` expresses that `func(value)` is truthy for valid values. +Users should prefer the statically inspectable metadata above, but if you need +the full power and flexibility of arbitrary runtime predicates... here it is. + +For some common constraints, we provide generic types: + +* `LowerCase = Annotated[T, Predicate(str.islower)]` +* `UpperCase = Annotated[T, Predicate(str.isupper)]` +* `IsDigit = Annotated[T, Predicate(str.isdigit)]` +* `IsFinite = Annotated[T, Predicate(math.isfinite)]` +* `IsNotFinite = Annotated[T, Predicate(Not(math.isfinite))]` +* `IsNan = Annotated[T, Predicate(math.isnan)]` +* `IsNotNan = Annotated[T, Predicate(Not(math.isnan))]` +* `IsInfinite = Annotated[T, Predicate(math.isinf)]` +* `IsNotInfinite = Annotated[T, Predicate(Not(math.isinf))]` + +so that you can write e.g. `x: IsFinite[float] = 2.0` instead of the longer +(but exactly equivalent) `x: Annotated[float, Predicate(math.isfinite)] = 2.0`. + +Some libraries might have special logic to handle known or understandable predicates, +for example by checking for `str.isdigit` and using its presence to both call custom +logic to enforce digit-only strings, and customise some generated external schema. +Users are therefore encouraged to avoid indirection like `lambda s: s.lower()`, in +favor of introspectable methods such as `str.lower` or `re.compile("pattern").search`. + +To enable basic negation of commonly used predicates like `math.isnan` without introducing introspection that makes it impossible for implementers to introspect the predicate we provide a `Not` wrapper that simply negates the predicate in an introspectable manner. Several of the predicates listed above are created in this manner. + +We do not specify what behaviour should be expected for predicates that raise +an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently +skip invalid constraints, or statically raise an error; or it might try calling it +and then propagate or discard the resulting +`TypeError: descriptor 'isdigit' for 'str' objects doesn't apply to a 'int' object` +exception. We encourage libraries to document the behaviour they choose. + +### Doc + +`doc()` can be used to add documentation information in `Annotated`, for function and method parameters, variables, class attributes, return types, and any place where `Annotated` can be used. + +It expects a value that can be statically analyzed, as the main use case is for static analysis, editors, documentation generators, and similar tools. + +It returns a `DocInfo` class with a single attribute `documentation` containing the value passed to `doc()`. + +This is the early adopter's alternative form of the [`typing-doc` proposal](https://github.com/tiangolo/fastapi/blob/typing-doc/typing_doc.md). + +### Integrating downstream types with `GroupedMetadata` + +Implementers may choose to provide a convenience wrapper that groups multiple pieces of metadata. +This can help reduce verbosity and cognitive overhead for users. +For example, an implementer like Pydantic might provide a `Field` or `Meta` type that accepts keyword arguments and transforms these into low-level metadata: + +```python +from dataclasses import dataclass +from typing import Iterator +from annotated_types import GroupedMetadata, Ge + +@dataclass +class Field(GroupedMetadata): + ge: int | None = None + description: str | None = None + + def __iter__(self) -> Iterator[object]: + # Iterating over a GroupedMetadata object should yield annotated-types + # constraint metadata objects which describe it as fully as possible, + # and may include other unknown objects too. + if self.ge is not None: + yield Ge(self.ge) + if self.description is not None: + yield Description(self.description) +``` + +Libraries consuming annotated-types constraints should check for `GroupedMetadata` and unpack it by iterating over the object and treating the results as if they had been "unpacked" in the `Annotated` type. The same logic should be applied to the [PEP 646 `Unpack` type](https://peps.python.org/pep-0646/), so that `Annotated[T, Field(...)]`, `Annotated[T, Unpack[Field(...)]]` and `Annotated[T, *Field(...)]` are all treated consistently. + +Libraries consuming annotated-types should also ignore any metadata they do not recongize that came from unpacking a `GroupedMetadata`, just like they ignore unrecognized metadata in `Annotated` itself. + +Our own `annotated_types.Interval` class is a `GroupedMetadata` which unpacks itself into `Gt`, `Lt`, etc., so this is not an abstract concern. Similarly, `annotated_types.Len` is a `GroupedMetadata` which unpacks itself into `MinLen` (optionally) and `MaxLen`. + +### Consuming metadata + +We intend to not be prescriptive as to _how_ the metadata and constraints are used, but as an example of how one might parse constraints from types annotations see our [implementation in `test_main.py`](https://github.com/annotated-types/annotated-types/blob/f59cf6d1b5255a0fe359b93896759a180bec30ae/tests/test_main.py#L94-L103). + +It is up to the implementer to determine how this metadata is used. +You could use the metadata for runtime type checking, for generating schemas or to generate example data, amongst other use cases. + +## Design & History + +This package was designed at the PyCon 2022 sprints by the maintainers of Pydantic +and Hypothesis, with the goal of making it as easy as possible for end-users to +provide more informative annotations for use by runtime libraries. + +It is deliberately minimal, and following PEP-593 allows considerable downstream +discretion in what (if anything!) they choose to support. Nonetheless, we expect +that staying simple and covering _only_ the most common use-cases will give users +and maintainers the best experience we can. If you'd like more constraints for your +types - follow our lead, by defining them and documenting them downstream! diff --git a/source/target-vendors/cp312/annotated_types-0.8.0.dist-info/RECORD b/source/target-vendors/cp312/annotated_types-0.8.0.dist-info/RECORD new file mode 100644 index 0000000..646f789 --- /dev/null +++ b/source/target-vendors/cp312/annotated_types-0.8.0.dist-info/RECORD @@ -0,0 +1,7 @@ +annotated_types/__init__.py,sha256=pxBKTUObJ6n3T8C-I2ubobeDHmBEAmgCogWrwSmKm8g,13273 +annotated_types/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +annotated_types/test_cases.py,sha256=2GdHKstXuBzpf3iXKCjmvFZpyk2zP9Hm9kXGGDhGBo4,6310 +annotated_types-0.8.0.dist-info/METADATA,sha256=YUmFsnj2Abjvhj-CeDuZHMOtpLifWS63HHxIxfLvIpg,15009 +annotated_types-0.8.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87 +annotated_types-0.8.0.dist-info/licenses/LICENSE,sha256=_hBJiEsaDZNCkB6I4H8ykl0ksxIdmXK2poBfuYJLCV0,1083 +annotated_types-0.8.0.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/annotated_types-0.8.0.dist-info/WHEEL b/source/target-vendors/cp312/annotated_types-0.8.0.dist-info/WHEEL new file mode 100644 index 0000000..7401812 --- /dev/null +++ b/source/target-vendors/cp312/annotated_types-0.8.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.31.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/source/target-vendors/cp312/annotated_types-0.8.0.dist-info/licenses/LICENSE b/source/target-vendors/cp312/annotated_types-0.8.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000..d99323a --- /dev/null +++ b/source/target-vendors/cp312/annotated_types-0.8.0.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2022 the contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/source/target-vendors/cp312/annotated_types/__init__.py b/source/target-vendors/cp312/annotated_types/__init__.py new file mode 100644 index 0000000..dcb35c5 --- /dev/null +++ b/source/target-vendors/cp312/annotated_types/__init__.py @@ -0,0 +1,416 @@ +import math +import types +from collections.abc import Callable, Iterator +from dataclasses import dataclass +from datetime import tzinfo +from types import EllipsisType +from typing import ( + TYPE_CHECKING, + Annotated, + Any, + Literal, + Protocol, + SupportsFloat, + SupportsIndex, + TypeVar, + Union, + runtime_checkable, +) + +__all__ = ( + 'BaseMetadata', + 'GroupedMetadata', + 'Gt', + 'Ge', + 'Lt', + 'Le', + 'Interval', + 'MultipleOf', + 'MinLen', + 'MaxLen', + 'Len', + 'Timezone', + 'Predicate', + 'LowerCase', + 'UpperCase', + 'IsDigits', + 'IsFinite', + 'IsNotFinite', + 'IsNan', + 'IsNotNan', + 'IsInfinite', + 'IsNotInfinite', + 'doc', + 'DocInfo', + '__version__', +) + +__version__ = '0.8.0' + + +T = TypeVar('T') + + +# arguments that start with __ are considered +# positional only +# see https://peps.python.org/pep-0484/#positional-only-arguments + + +class SupportsGt(Protocol): + def __gt__(self: T, __other: T) -> bool: + ... + + +class SupportsGe(Protocol): + def __ge__(self: T, __other: T) -> bool: + ... + + +class SupportsLt(Protocol): + def __lt__(self: T, __other: T) -> bool: + ... + + +class SupportsLe(Protocol): + def __le__(self: T, __other: T) -> bool: + ... + + +class SupportsMod(Protocol): + def __mod__(self: T, __other: T) -> T: + ... + + +class SupportsDiv(Protocol): + def __div__(self: T, __other: T) -> T: + ... + + +class BaseMetadata: + """Base class for all metadata. + + This exists mainly so that implementers + can do `isinstance(..., BaseMetadata)` while traversing field annotations. + """ + + __slots__ = () + + +@dataclass(frozen=True, slots=True) +class Gt(BaseMetadata): + """Gt(gt=x) implies that the value must be greater than x. + + It can be used with any type that supports the ``>`` operator, + including numbers, dates and times, strings, sets, and so on. + """ + + gt: SupportsGt + + +@dataclass(frozen=True, slots=True) +class Ge(BaseMetadata): + """Ge(ge=x) implies that the value must be greater than or equal to x. + + It can be used with any type that supports the ``>=`` operator, + including numbers, dates and times, strings, sets, and so on. + """ + + ge: SupportsGe + + +@dataclass(frozen=True, slots=True) +class Lt(BaseMetadata): + """Lt(lt=x) implies that the value must be less than x. + + It can be used with any type that supports the ``<`` operator, + including numbers, dates and times, strings, sets, and so on. + """ + + lt: SupportsLt + + +@dataclass(frozen=True, slots=True) +class Le(BaseMetadata): + """Le(le=x) implies that the value must be less than or equal to x. + + It can be used with any type that supports the ``<=`` operator, + including numbers, dates and times, strings, sets, and so on. + """ + + le: SupportsLe + + +@runtime_checkable +class GroupedMetadata(Protocol): + """A grouping of multiple objects, like typing.Unpack. + + `GroupedMetadata` on its own is not metadata and has no meaning. + All of the constraints and metadata should be fully expressable + in terms of the `BaseMetadata`'s returned by `GroupedMetadata.__iter__()`. + + Concrete implementations should override `GroupedMetadata.__iter__()` + to add their own metadata. + For example: + + >>> @dataclass + >>> class Field(GroupedMetadata): + >>> gt: float | None = None + >>> description: str | None = None + ... + >>> def __iter__(self) -> Iterable[object]: + >>> if self.gt is not None: + >>> yield Gt(self.gt) + >>> if self.description is not None: + >>> yield Description(self.gt) + + Also see the implementation of `Interval` below for an example. + + Parsers should recognize this and unpack it so that it can be used + both with and without unpacking: + + - `Annotated[int, Field(...)]` (parser must unpack Field) + - `Annotated[int, *Field(...)]` (PEP-646) + """ # noqa: trailing-whitespace + + @property + def __is_annotated_types_grouped_metadata__(self) -> Literal[True]: + return True + + def __iter__(self) -> Iterator[object]: + ... + + if not TYPE_CHECKING: + __slots__ = () # allow subclasses to use slots + + def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None: + # Basic ABC like functionality without the complexity of an ABC + super().__init_subclass__(*args, **kwargs) + if cls.__iter__ is GroupedMetadata.__iter__: + raise TypeError("Can't subclass GroupedMetadata without implementing __iter__") + + def __iter__(self) -> Iterator[object]: # noqa: F811 + raise NotImplementedError # more helpful than "None has no attribute..." type errors + + +@dataclass(frozen=True, kw_only=True, slots=True) +class Interval(GroupedMetadata): + """Interval can express inclusive or exclusive bounds with a single object. + + It accepts keyword arguments ``gt``, ``ge``, ``lt``, and/or ``le``, which + are interpreted the same way as the single-bound constraints. + """ + + gt: SupportsGt | None = None + ge: SupportsGe | None = None + lt: SupportsLt | None = None + le: SupportsLe | None = None + + def __iter__(self) -> Iterator[BaseMetadata]: + """Unpack an Interval into zero or more single-bounds.""" + if self.gt is not None: + yield Gt(self.gt) + if self.ge is not None: + yield Ge(self.ge) + if self.lt is not None: + yield Lt(self.lt) + if self.le is not None: + yield Le(self.le) + + +@dataclass(frozen=True, slots=True) +class MultipleOf(BaseMetadata): + """MultipleOf(multiple_of=x) might be interpreted in two ways: + + 1. Python semantics, implying ``value % multiple_of == 0``, or + 2. JSONschema semantics, where ``int(value / multiple_of) == value / multiple_of`` + + We encourage users to be aware of these two common interpretations, + and libraries to carefully document which they implement. + """ + + multiple_of: SupportsDiv | SupportsMod + + +@dataclass(frozen=True, slots=True) +class MinLen(BaseMetadata): + """ + MinLen() implies minimum inclusive length, + e.g. ``len(value) >= min_length``. + """ + + min_length: Annotated[int, Ge(0)] + + +@dataclass(frozen=True, slots=True) +class MaxLen(BaseMetadata): + """ + MaxLen() implies maximum inclusive length, + e.g. ``len(value) <= max_length``. + """ + + max_length: Annotated[int, Ge(0)] + + +@dataclass(frozen=True, slots=True) +class Len(GroupedMetadata): + """ + Len() implies that ``min_length <= len(value) <= max_length``. + + Upper bound may be omitted or ``None`` to indicate no upper length bound. + """ + + min_length: Annotated[int, Ge(0)] = 0 + max_length: Annotated[int, Ge(0)] | None = None + + def __iter__(self) -> Iterator[BaseMetadata]: + """Unpack a Len into zero or more single-bounds.""" + if self.min_length > 0: + yield MinLen(self.min_length) + if self.max_length is not None: + yield MaxLen(self.max_length) + + +@dataclass(frozen=True, slots=True) +class Timezone(BaseMetadata): + """Timezone(tz=...) requires a datetime to be aware (or ``tz=None``, naive). + + ``Annotated[datetime, Timezone(None)]`` must be a naive datetime. + ``Timezone(...)`` (the ellipsis literal) expresses that the datetime must be + tz-aware but any timezone is allowed. + + You may also pass a specific timezone string or tzinfo object such as + ``Timezone(timezone.utc)`` or ``Timezone("Africa/Abidjan")`` to express that + you only allow a specific timezone, though we note that this is often + a symptom of poor design. + """ + + tz: str | tzinfo | EllipsisType | None + + +@dataclass(frozen=True, slots=True) +class Unit(BaseMetadata): + """Indicates that the value is a physical quantity with the specified unit. + + It is intended for usage with numeric types, where the value represents the + magnitude of the quantity. For example, ``distance: Annotated[float, Unit('m')]`` + or ``speed: Annotated[float, Unit('m/s')]``. + + Interpretation of the unit string is left to the discretion of the consumer. + It is suggested to follow conventions established by python libraries that work + with physical quantities, such as + + - ``pint`` : + - ``astropy.units``: + + For indicating a quantity with a certain dimensionality but without a specific unit + it is recommended to use square brackets, e.g. `Annotated[float, Unit('[time]')]`. + Note, however, ``annotated_types`` itself makes no use of the unit string. + """ + + unit: str + + +@dataclass(frozen=True, slots=True) +class Predicate(BaseMetadata): + """``Predicate(func: Callable)`` implies `func(value)` is truthy for valid values. + + Users should prefer statically inspectable metadata, but if you need the full + power and flexibility of arbitrary runtime predicates... here it is. + + We provide a few predefined predicates for common string constraints: + ``LowerCase = Predicate(str.islower)``, ``UpperCase = Predicate(str.isupper)``, and + ``IsDigits = Predicate(str.isdigit)``. Users are encouraged to use methods which + can be given special handling, and avoid indirection like ``lambda s: s.lower()``. + + Some libraries might have special logic to handle certain predicates, e.g. by + checking for `str.isdigit` and using its presence to both call custom logic to + enforce digit-only strings, and customise some generated external schema. + + We do not specify what behaviour should be expected for predicates that raise + an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently + skip invalid constraints, or statically raise an error; or it might try calling it + and then propagate or discard the resulting exception. + """ + + func: Callable[[Any], bool] + + def __repr__(self) -> str: + if getattr(self.func, "__name__", "") == "": + return f"{self.__class__.__name__}({self.func!r})" + if isinstance(self.func, (types.MethodType, types.BuiltinMethodType)) and ( + namespace := getattr(self.func.__self__, "__name__", None) + ): + return f"{self.__class__.__name__}({namespace}.{self.func.__name__})" + if isinstance(self.func, type(str.isascii)): # method descriptor + return f"{self.__class__.__name__}({self.func.__qualname__})" + return f"{self.__class__.__name__}({self.func.__name__})" + + +@dataclass +class Not: + func: Callable[[Any], bool] + + def __call__(self, __v: Any) -> bool: + return not self.func(__v) + + +_StrType = TypeVar("_StrType", bound=str) + +LowerCase = Annotated[_StrType, Predicate(str.islower)] +""" +Return True if the string is a lowercase string, False otherwise. + +A string is lowercase if all cased characters in the string are lowercase and there is at least one cased character in the string. +""" # noqa: E501 +UpperCase = Annotated[_StrType, Predicate(str.isupper)] +""" +Return True if the string is an uppercase string, False otherwise. + +A string is uppercase if all cased characters in the string are uppercase and there is at least one cased character in the string. +""" # noqa: E501 +IsDigit = Annotated[_StrType, Predicate(str.isdigit)] +IsDigits = IsDigit # type: ignore # plural for backwards compatibility, see #63 +""" +Return True if the string is a digit string, False otherwise. + +A string is a digit string if all characters in the string are digits and there is at least one character in the string. +""" # noqa: E501 +IsAscii = Annotated[_StrType, Predicate(str.isascii)] +""" +Return True if all characters in the string are ASCII, False otherwise. + +ASCII characters have code points in the range U+0000-U+007F. Empty string is ASCII too. +""" + +_NumericType = TypeVar('_NumericType', bound=Union[SupportsFloat, SupportsIndex]) +IsFinite = Annotated[_NumericType, Predicate(math.isfinite)] +"""Return True if x is neither an infinity nor a NaN, and False otherwise.""" +IsNotFinite = Annotated[_NumericType, Predicate(Not(math.isfinite))] +"""Return True if x is one of infinity or NaN, and False otherwise""" +IsNan = Annotated[_NumericType, Predicate(math.isnan)] +"""Return True if x is a NaN (not a number), and False otherwise.""" +IsNotNan = Annotated[_NumericType, Predicate(Not(math.isnan))] +"""Return True if x is anything but NaN (not a number), and False otherwise.""" +IsInfinite = Annotated[_NumericType, Predicate(math.isinf)] +"""Return True if x is a positive or negative infinity, and False otherwise.""" +IsNotInfinite = Annotated[_NumericType, Predicate(Not(math.isinf))] +"""Return True if x is neither a positive or negative infinity, and False otherwise.""" + +try: + # PEP 727 – Documentation in Annotated Metadata + from typing_extensions import Doc # type: ignore[attr-defined] +except ImportError: + + @dataclass(frozen=True, slots=True) + class Doc: # type: ignore [no-redef] + """ " + The return value of doc(), mainly to be used by tools that want to extract the + Annotated documentation at runtime. + """ + + documentation: str + """The documentation string passed to doc().""" + + +DocInfo = Doc # backwards compatibility +doc = Doc diff --git a/source/target-vendors/cp312/annotated_types/py.typed b/source/target-vendors/cp312/annotated_types/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/annotated_types/test_cases.py b/source/target-vendors/cp312/annotated_types/test_cases.py new file mode 100644 index 0000000..fba3c25 --- /dev/null +++ b/source/target-vendors/cp312/annotated_types/test_cases.py @@ -0,0 +1,146 @@ +import math +from collections.abc import Iterable, Iterator +from datetime import date, datetime, timedelta, timezone +from decimal import Decimal +from typing import Annotated, Any, NamedTuple + +import annotated_types as at + + +class Case(NamedTuple): + """ + A test case for `annotated_types`. + """ + + annotation: Any + valid_cases: Iterable[Any] + invalid_cases: Iterable[Any] + + +def cases() -> Iterable[Case]: + # Gt, Ge, Lt, Le + yield Case(Annotated[int, at.Gt(4)], (5, 6, 1000), (4, 0, -1)) + yield Case(Annotated[float, at.Gt(0.5)], (0.6, 0.7, 0.8, 0.9), (0.5, 0.0, -0.1)) + yield Case( + Annotated[datetime, at.Gt(datetime(2000, 1, 1))], + [datetime(2000, 1, 2), datetime(2000, 1, 3)], + [datetime(2000, 1, 1), datetime(1999, 12, 31)], + ) + yield Case( + Annotated[datetime, at.Gt(date(2000, 1, 1))], + [date(2000, 1, 2), date(2000, 1, 3)], + [date(2000, 1, 1), date(1999, 12, 31)], + ) + yield Case( + Annotated[datetime, at.Gt(Decimal('1.123'))], + [Decimal('1.1231'), Decimal('123')], + [Decimal('1.123'), Decimal('0')], + ) + + yield Case(Annotated[int, at.Ge(4)], (4, 5, 6, 1000, 4), (0, -1)) + yield Case(Annotated[float, at.Ge(0.5)], (0.5, 0.6, 0.7, 0.8, 0.9), (0.4, 0.0, -0.1)) + yield Case( + Annotated[datetime, at.Ge(datetime(2000, 1, 1))], + [datetime(2000, 1, 2), datetime(2000, 1, 3)], + [datetime(1998, 1, 1), datetime(1999, 12, 31)], + ) + + yield Case(Annotated[int, at.Lt(4)], (0, -1), (4, 5, 6, 1000, 4)) + yield Case(Annotated[float, at.Lt(0.5)], (0.4, 0.0, -0.1), (0.5, 0.6, 0.7, 0.8, 0.9)) + yield Case( + Annotated[datetime, at.Lt(datetime(2000, 1, 1))], + [datetime(1999, 12, 31), datetime(1999, 12, 31)], + [datetime(2000, 1, 2), datetime(2000, 1, 3)], + ) + + yield Case(Annotated[int, at.Le(4)], (4, 0, -1), (5, 6, 1000)) + yield Case(Annotated[float, at.Le(0.5)], (0.5, 0.0, -0.1), (0.6, 0.7, 0.8, 0.9)) + yield Case( + Annotated[datetime, at.Le(datetime(2000, 1, 1))], + [datetime(2000, 1, 1), datetime(1999, 12, 31)], + [datetime(2000, 1, 2), datetime(2000, 1, 3)], + ) + + # Interval + yield Case(Annotated[int, at.Interval(gt=4)], (5, 6, 1000), (4, 0, -1)) + yield Case(Annotated[int, at.Interval(gt=4, lt=10)], (5, 6), (4, 10, 1000, 0, -1)) + yield Case(Annotated[float, at.Interval(ge=0.5, le=1)], (0.5, 0.9, 1), (0.49, 1.1)) + yield Case( + Annotated[datetime, at.Interval(gt=datetime(2000, 1, 1), le=datetime(2000, 1, 3))], + [datetime(2000, 1, 2), datetime(2000, 1, 3)], + [datetime(2000, 1, 1), datetime(2000, 1, 4)], + ) + + yield Case(Annotated[int, at.MultipleOf(multiple_of=3)], (0, 3, 9), (1, 2, 4)) + yield Case(Annotated[float, at.MultipleOf(multiple_of=0.5)], (0, 0.5, 1, 1.5), (0.4, 1.1)) + + # lengths + + yield Case(Annotated[str, at.MinLen(3)], ('123', '1234', 'x' * 10), ('', '1', '12')) + yield Case(Annotated[str, at.Len(3)], ('123', '1234', 'x' * 10), ('', '1', '12')) + yield Case(Annotated[list[int], at.MinLen(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2])) + yield Case(Annotated[list[int], at.Len(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2])) + + yield Case(Annotated[str, at.MaxLen(4)], ('', '1234'), ('12345', 'x' * 10)) + yield Case(Annotated[str, at.Len(0, 4)], ('', '1234'), ('12345', 'x' * 10)) + yield Case(Annotated[list[str], at.MaxLen(4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10)) + yield Case(Annotated[list[str], at.Len(0, 4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10)) + + yield Case(Annotated[str, at.Len(3, 5)], ('123', '12345'), ('', '1', '12', '123456', 'x' * 10)) + yield Case(Annotated[str, at.Len(3, 3)], ('123',), ('12', '1234')) + + yield Case(Annotated[dict[int, int], at.Len(2, 3)], [{1: 1, 2: 2}], [{}, {1: 1}, {1: 1, 2: 2, 3: 3, 4: 4}]) + yield Case(Annotated[set[int], at.Len(2, 3)], ({1, 2}, {1, 2, 3}), (set(), {1}, {1, 2, 3, 4})) + yield Case(Annotated[tuple[int, ...], at.Len(2, 3)], ((1, 2), (1, 2, 3)), ((), (1,), (1, 2, 3, 4))) + + # Timezone + + yield Case( + Annotated[datetime, at.Timezone(None)], [datetime(2000, 1, 1)], [datetime(2000, 1, 1, tzinfo=timezone.utc)] + ) + yield Case( + Annotated[datetime, at.Timezone(...)], [datetime(2000, 1, 1, tzinfo=timezone.utc)], [datetime(2000, 1, 1)] + ) + yield Case( + Annotated[datetime, at.Timezone(timezone.utc)], + [datetime(2000, 1, 1, tzinfo=timezone.utc)], + [datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))], + ) + yield Case( + Annotated[datetime, at.Timezone('Europe/London')], + [datetime(2000, 1, 1, tzinfo=timezone(timedelta(0), name='Europe/London'))], + [datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))], + ) + + # Quantity + + yield Case(Annotated[float, at.Unit(unit='m')], (5, 4.2), ('5m', '4.2m')) + + # predicate types + + yield Case(at.LowerCase[str], ['abc', 'foobar'], ['', 'A', 'Boom']) + yield Case(at.UpperCase[str], ['ABC', 'DEFO'], ['', 'a', 'abc', 'AbC']) + yield Case(at.IsDigit[str], ['123'], ['', 'ab', 'a1b2']) + yield Case(at.IsAscii[str], ['123', 'foo bar'], ['£100', '😊', 'whatever 👀']) + + yield Case(Annotated[int, at.Predicate(lambda x: x % 2 == 0)], [0, 2, 4], [1, 3, 5]) + + yield Case(at.IsFinite[float], [1.23], [math.nan, math.inf, -math.inf]) + yield Case(at.IsNotFinite[float], [math.nan, math.inf], [1.23]) + yield Case(at.IsNan[float], [math.nan], [1.23, math.inf]) + yield Case(at.IsNotNan[float], [1.23, math.inf], [math.nan]) + yield Case(at.IsInfinite[float], [math.inf], [math.nan, 1.23]) + yield Case(at.IsNotInfinite[float], [math.nan, 1.23], [math.inf]) + + # check stacked predicates + yield Case(at.IsInfinite[Annotated[float, at.Predicate(lambda x: x > 0)]], [math.inf], [-math.inf, 1.23, math.nan]) + + # doc + yield Case(Annotated[int, at.doc("A number")], [1, 2], []) + + # custom GroupedMetadata + class MyCustomGroupedMetadata(at.GroupedMetadata): + def __iter__(self) -> Iterator[at.Predicate]: + yield at.Predicate(lambda x: float(x).is_integer()) + + yield Case(Annotated[float, MyCustomGroupedMetadata()], [0, 2.0], [0.01, 1.5]) diff --git a/source/target-vendors/cp312/anyio-4.15.1.dist-info/METADATA b/source/target-vendors/cp312/anyio-4.15.1.dist-info/METADATA new file mode 100644 index 0000000..13734ec --- /dev/null +++ b/source/target-vendors/cp312/anyio-4.15.1.dist-info/METADATA @@ -0,0 +1,108 @@ +Metadata-Version: 2.4 +Name: anyio +Version: 4.15.1 +Summary: High-level concurrency and networking framework on top of asyncio or Trio +Author-email: Alex Grönholm +License-Expression: MIT +Project-URL: Documentation, https://anyio.readthedocs.io/en/latest/ +Project-URL: Changelog, https://anyio.readthedocs.io/en/stable/versionhistory.html +Project-URL: Source code, https://github.com/agronholm/anyio +Project-URL: Issue tracker, https://github.com/agronholm/anyio/issues +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Framework :: AnyIO +Classifier: Typing :: Typed +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: 3.15 +Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta +Requires-Python: >=3.10 +Description-Content-Type: text/x-rst +License-File: LICENSE +Requires-Dist: exceptiongroup>=1.0.2; python_version < "3.11" +Requires-Dist: idna>=2.8 +Requires-Dist: typing_extensions>=4.16.0; python_version < "3.15" +Provides-Extra: trio +Requires-Dist: trio>=0.32.0; extra == "trio" +Dynamic: license-file + +.. image:: https://github.com/agronholm/anyio/actions/workflows/test.yml/badge.svg + :target: https://github.com/agronholm/anyio/actions/workflows/test.yml + :alt: Build Status +.. image:: https://coveralls.io/repos/github/agronholm/anyio/badge.svg?branch=master + :target: https://coveralls.io/github/agronholm/anyio?branch=master + :alt: Code Coverage +.. image:: https://readthedocs.org/projects/anyio/badge/?version=latest + :target: https://anyio.readthedocs.io/en/latest/?badge=latest + :alt: Documentation +.. image:: https://badges.gitter.im/gitterHQ/gitter.svg + :target: https://gitter.im/python-trio/AnyIO + :alt: Gitter chat +.. image:: https://tidelift.com/badges/package/pypi/anyio + :target: https://tidelift.com/subscription/pkg/pypi-anyio + :alt: Tidelift + +AnyIO is an asynchronous networking and concurrency library that works on top of either asyncio_ or +Trio_. It implements Trio-like `structured concurrency`_ (SC) on top of asyncio and works in harmony +with the native SC of Trio itself. + +Applications and libraries written against AnyIO's API will run unmodified on either asyncio_ or +Trio_. AnyIO can also be adopted into a library or application incrementally – bit by bit, no full +refactoring necessary. It will blend in with the native libraries of your chosen backend. + +To find out why you might want to use AnyIO's APIs instead of asyncio's, you can read about it +`here `_. + +Documentation +------------- + +View full documentation at: https://anyio.readthedocs.io/ + +Features +-------- + +AnyIO offers the following functionality: + +* Task groups (nurseries_ in trio terminology) +* High-level networking (TCP, UDP and UNIX sockets) + + * `Happy eyeballs`_ algorithm for TCP connections (more robust than that of asyncio on Python + 3.8) + * async/await style UDP sockets (unlike asyncio where you still have to use Transports and + Protocols) + +* A versatile API for byte streams and object streams +* Inter-task synchronization and communication (locks, conditions, events, semaphores, object + streams and futures) +* Worker threads +* Subprocesses +* Subinterpreter support for code parallelization (on Python 3.13 and later) +* Asynchronous file I/O (using worker threads) +* Signal handling +* Asynchronous versions of the functools_ and itertools_ modules + +AnyIO also comes with its own pytest_ plugin which also supports asynchronous fixtures. +It even works with the popular Hypothesis_ library. + +.. _asyncio: https://docs.python.org/3/library/asyncio.html +.. _Trio: https://github.com/python-trio/trio +.. _structured concurrency: https://en.wikipedia.org/wiki/Structured_concurrency +.. _nurseries: https://trio.readthedocs.io/en/stable/reference-core.html#nurseries-and-spawning +.. _Happy eyeballs: https://en.wikipedia.org/wiki/Happy_Eyeballs +.. _pytest: https://docs.pytest.org/en/latest/ +.. _functools: https://docs.python.org/3/library/functools.html +.. _itertools: https://docs.python.org/3/library/itertools.html +.. _Hypothesis: https://hypothesis.works/ + +Security contact information +---------------------------- + +To report a security vulnerability, please use the `Tidelift security contact`_. +Tidelift will coordinate the fix and disclosure. + +.. _Tidelift security contact: https://tidelift.com/security diff --git a/source/target-vendors/cp312/anyio-4.15.1.dist-info/RECORD b/source/target-vendors/cp312/anyio-4.15.1.dist-info/RECORD new file mode 100644 index 0000000..97a70cf --- /dev/null +++ b/source/target-vendors/cp312/anyio-4.15.1.dist-info/RECORD @@ -0,0 +1,53 @@ +anyio/__init__.py,sha256=OfroYRjIUw_JFj0Zwh1JAaajMQg4VqtoAt0XoOHDRwE,7602 +anyio/_lazyimport.py,sha256=LCjnYanIfYE9G-Dv_EsqNLDuVRppNicWZjCTfS9-sJU,6082 +anyio/from_thread.py,sha256=iZZbfcNySi4wAcx1_cxAQE4J0YZC88qRMIYqQ6SF9sc,19415 +anyio/functools.py,sha256=T4JS8IXq-x1S0Lbo2owF8l9fza2KypO147QLeyz4cjs,11797 +anyio/itertools.py,sha256=TS7_UCa2Y3cggtaLlWHbjOyf6QhPfQUdPduNW-ddXTE,16255 +anyio/lowlevel.py,sha256=QyWkvfetbCH0mHkqv1yxGwOaiZ7fyxhpZkHWOXPV5JU,6280 +anyio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +anyio/pytest_plugin.py,sha256=oLXyOstvRZW3VVbqIODhZsPt4jSz2UGh44YeG2nJXx0,14516 +anyio/to_interpreter.py,sha256=ATnoT6kei9wDmQZtXTA3SsY4CK7XKr8Ta4_x5i6pbTw,7100 +anyio/to_process.py,sha256=p6n8j0nreJUl1DEkR_9PL6KRoKtNqFxORTn8qZBvn94,9850 +anyio/to_thread.py,sha256=8MPVrrQq8ttGvV21cSd68rMto5bhxJ0-i9CupXag5Gs,2750 +anyio/_backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +anyio/_backends/_asyncio.py,sha256=Vqy7O1ymmEpxTiPzehSuBCVTziGALNV-BbsIRaKXsg4,107767 +anyio/_backends/_trio.py,sha256=Gs8XzX0IeBvje8IEZtJmpNylKCUAChKv7I_YZi3wlmg,45715 +anyio/_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +anyio/_core/_asyncio_selector_thread.py,sha256=2PdxFM3cs02Kp6BSppbvmRT7q7asreTW5FgBxEsflBo,5626 +anyio/_core/_concurrency_utils.py,sha256=kvRXMrnDqgQTBkZc-f279-lBnZM4YbSToXqhnAUCfSc,4522 +anyio/_core/_contextmanagers.py,sha256=YInBCabiEeS-UaP_Jdxa1CaFC71ETPW8HZTHIM8Rsc8,7215 +anyio/_core/_eventloop.py,sha256=ByZUeJD9alMfcyTseRo5IzTO0IltEul_Gyq9iqSjqDk,6658 +anyio/_core/_exceptions.py,sha256=B4ZhLkBQT4tI6vShv4-GJui492IJsoIDTw1FQJEkmAI,5618 +anyio/_core/_fileio.py,sha256=q--ft9h5Y_FRLzhAnoYrXf3tOuZ8ZyIN2A9mCfMbZRc,34163 +anyio/_core/_futures.py,sha256=YQq5uhnCnKQMQD62tOLfzEm7dUXY3GQorJdH6ghfgIQ,6177 +anyio/_core/_resources.py,sha256=Ld5duCe9NwgPfI7EIOh3CMIBYQ7rq6ZQEa7CFS0u9Ec,450 +anyio/_core/_signals.py,sha256=mjTBB2hTKNPRlU0IhnijeQedpWOGERDiMjSlJQsFrug,1016 +anyio/_core/_sockets.py,sha256=C8b-3w5H9n0Fo4g7X8WJvk0m6CGh6NVAAFtieBGHK3g,35289 +anyio/_core/_streams.py,sha256=FczFwIgDpnkK0bODWJXMpsUJYdvAD04kaUaGzJU8DK0,1806 +anyio/_core/_subprocesses.py,sha256=TMBuGNILz-47w5t9vayJ1GpCpDT6LxsMohJDFCewIRc,7938 +anyio/_core/_synchronization.py,sha256=ggsd74QPd846faA2HQeT07ONQ5HQ1S78gy8mvB3qCrw,21658 +anyio/_core/_tasks.py,sha256=J2dhM_Jq3Grsfg_enNWpRz2SxfM15uLt-pEfvoQq3uA,15391 +anyio/_core/_tempfile.py,sha256=IgoBCFaPhrFvsF9KHcCcrnCRn-UqMfw0WRdSDWgxAiI,19726 +anyio/_core/_testing.py,sha256=ZRucLh5kqhiFWAJMMbd8yZ6uIvz0rq3WwL72ldosxAU,2340 +anyio/_core/_typedattr.py,sha256=gt9Dzn-IL0QW14aHPDD8PAtq95FYKNNupHyf37xZGPQ,2607 +anyio/abc/__init__.py,sha256=XIVvCI2c6oyaOcqXe94k2rzU5_s7-ZNnlxGTk34H4T0,3050 +anyio/abc/_eventloop.py,sha256=0-UwgB351rkocCt8m3fjTmcNx2Fxn52nUoujG5DzMvU,11127 +anyio/abc/_resources.py,sha256=hrb0LXUgHSp3i8G6W8JJchdzKXgg1m0T-L7udXmv2y0,900 +anyio/abc/_sockets.py,sha256=jQWnPX7iDIDyvyvVkbuFhjezw4oI2WggzbW6Vl2-eqs,13162 +anyio/abc/_streams.py,sha256=IMeOPPutFXRbyeAutxV1HJe7FUQZYp2qKrJJpP1DCv0,7620 +anyio/abc/_subprocesses.py,sha256=CDqU1p0HuBS0pXUzBW_hbfhihTJRM67n__7qZmoPU84,2123 +anyio/abc/_tasks.py,sha256=HBPpT72aOy0bwMAxBCKmnXsR328wCYRjYm3iaVw2fQo,7070 +anyio/abc/_testing.py,sha256=A9Y8RAClIPn1A0aNHBN8YoaRy_MKOpbFGEbTHAXgB6U,2145 +anyio/streams/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +anyio/streams/buffered.py,sha256=u7hCD8SNrYHcutG6K5wEiy1F88-pizgjvEFM22Kq2Cw,6746 +anyio/streams/file.py,sha256=6jujI2m-QJITqqKFamrupX_DNsU7y2Fz3omLZxOLuY0,4524 +anyio/streams/memory.py,sha256=rpxNzDMag9PCOynBqN64KyuAfAPB41vFyRP-Npo-lE0,10793 +anyio/streams/stapled.py,sha256=UJ2Rgt0NUmM1VovRNOoZoic960lcbDjb3vhfZg3b_Z0,4845 +anyio/streams/text.py,sha256=BcVAGJw1VRvtIqnv-o0Rb0pwH7p8vwlvl21xHq522ag,5765 +anyio/streams/tls.py,sha256=fqLmxvULFO9rvxGyJ33VPrOIN7E3eiU1q90BkmKigQ8,15818 +anyio-4.15.1.dist-info/licenses/LICENSE,sha256=U2GsncWPLvX9LpsJxoKXwX8ElQkJu8gCO9uC6s8iwrA,1081 +anyio-4.15.1.dist-info/METADATA,sha256=BDrstXGvcYnopsUo9djjJUXxss36r3YdUebVtn6jyoE,4733 +anyio-4.15.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91 +anyio-4.15.1.dist-info/entry_points.txt,sha256=_d6Yu6uiaZmNe0CydowirE9Cmg7zUL2g08tQpoS3Qvc,39 +anyio-4.15.1.dist-info/top_level.txt,sha256=QglSMiWX8_5dpoVAEIHdEYzvqFMdSYWmCj6tYw2ITkQ,6 +anyio-4.15.1.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/anyio-4.15.1.dist-info/WHEEL b/source/target-vendors/cp312/anyio-4.15.1.dist-info/WHEEL new file mode 100644 index 0000000..13013d7 --- /dev/null +++ b/source/target-vendors/cp312/anyio-4.15.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (84.0.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/source/target-vendors/cp312/anyio-4.15.1.dist-info/entry_points.txt b/source/target-vendors/cp312/anyio-4.15.1.dist-info/entry_points.txt new file mode 100644 index 0000000..44dd9bd --- /dev/null +++ b/source/target-vendors/cp312/anyio-4.15.1.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[pytest11] +anyio = anyio.pytest_plugin diff --git a/source/target-vendors/cp312/anyio-4.15.1.dist-info/licenses/LICENSE b/source/target-vendors/cp312/anyio-4.15.1.dist-info/licenses/LICENSE new file mode 100644 index 0000000..104eebf --- /dev/null +++ b/source/target-vendors/cp312/anyio-4.15.1.dist-info/licenses/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2018 Alex Grönholm + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/source/target-vendors/cp312/anyio-4.15.1.dist-info/top_level.txt b/source/target-vendors/cp312/anyio-4.15.1.dist-info/top_level.txt new file mode 100644 index 0000000..c77c069 --- /dev/null +++ b/source/target-vendors/cp312/anyio-4.15.1.dist-info/top_level.txt @@ -0,0 +1 @@ +anyio diff --git a/source/target-vendors/cp312/anyio/__init__.py b/source/target-vendors/cp312/anyio/__init__.py new file mode 100644 index 0000000..30a3a16 --- /dev/null +++ b/source/target-vendors/cp312/anyio/__init__.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ._lazyimport import ( + fix_package_names, + install_lazy_importer, + set_deprecated_aliases, +) + +if TYPE_CHECKING or not install_lazy_importer(): + from ._core._concurrency_utils import amap as amap + from ._core._concurrency_utils import as_completed as as_completed + from ._core._concurrency_utils import gather as gather + from ._core._contextmanagers import ( + AsyncContextManagerMixin as AsyncContextManagerMixin, + ) + from ._core._contextmanagers import ContextManagerMixin as ContextManagerMixin + from ._core._eventloop import current_time as current_time + from ._core._eventloop import get_all_backends as get_all_backends + from ._core._eventloop import get_available_backends as get_available_backends + from ._core._eventloop import get_cancelled_exc_class as get_cancelled_exc_class + from ._core._eventloop import run as run + from ._core._eventloop import sleep as sleep + from ._core._eventloop import sleep_forever as sleep_forever + from ._core._eventloop import sleep_until as sleep_until + from ._core._exceptions import BrokenResourceError as BrokenResourceError + from ._core._exceptions import BrokenWorkerInterpreter as BrokenWorkerInterpreter + from ._core._exceptions import BrokenWorkerProcess as BrokenWorkerProcess + from ._core._exceptions import BusyResourceError as BusyResourceError + from ._core._exceptions import ClosedResourceError as ClosedResourceError + from ._core._exceptions import ConnectionFailed as ConnectionFailed + from ._core._exceptions import DelimiterNotFound as DelimiterNotFound + from ._core._exceptions import EndOfStream as EndOfStream + from ._core._exceptions import FutureAlreadyFinished as FutureAlreadyFinished + from ._core._exceptions import FutureCancelled as FutureCancelled + from ._core._exceptions import FutureFailed as FutureFailed + from ._core._exceptions import FutureNotFinished as FutureNotFinished + from ._core._exceptions import IncompleteRead as IncompleteRead + from ._core._exceptions import NoEventLoopError as NoEventLoopError + from ._core._exceptions import RunFinishedError as RunFinishedError + from ._core._exceptions import TaskCancelled as TaskCancelled + from ._core._exceptions import TaskFailed as TaskFailed + from ._core._exceptions import TaskNotFinished as TaskNotFinished + from ._core._exceptions import ( + TypedAttributeLookupError as TypedAttributeLookupError, + ) + from ._core._exceptions import WouldBlock as WouldBlock + from ._core._fileio import AsyncFile as AsyncFile + from ._core._fileio import Path as Path + from ._core._fileio import open_file as open_file + from ._core._fileio import wrap_file as wrap_file + from ._core._futures import Future as Future + from ._core._resources import aclose_forcefully as aclose_forcefully + from ._core._signals import open_signal_receiver as open_signal_receiver + from ._core._sockets import TCPConnectable as TCPConnectable + from ._core._sockets import UNIXConnectable as UNIXConnectable + from ._core._sockets import as_connectable as as_connectable + from ._core._sockets import connect_tcp as connect_tcp + from ._core._sockets import connect_unix as connect_unix + from ._core._sockets import ( + create_connected_udp_socket as create_connected_udp_socket, + ) + from ._core._sockets import ( + create_connected_unix_datagram_socket as create_connected_unix_datagram_socket, + ) + from ._core._sockets import create_tcp_listener as create_tcp_listener + from ._core._sockets import create_udp_socket as create_udp_socket + from ._core._sockets import ( + create_unix_datagram_socket as create_unix_datagram_socket, + ) + from ._core._sockets import create_unix_listener as create_unix_listener + from ._core._sockets import getaddrinfo as getaddrinfo + from ._core._sockets import getnameinfo as getnameinfo + from ._core._sockets import notify_closing as notify_closing + from ._core._sockets import wait_readable as wait_readable + from ._core._sockets import wait_socket_readable as wait_socket_readable + from ._core._sockets import wait_socket_writable as wait_socket_writable + from ._core._sockets import wait_writable as wait_writable + from ._core._streams import ( + create_memory_object_stream as create_memory_object_stream, + ) + from ._core._subprocesses import open_process as open_process + from ._core._subprocesses import run_process as run_process + from ._core._synchronization import CapacityLimiter as CapacityLimiter + from ._core._synchronization import ( + CapacityLimiterStatistics as CapacityLimiterStatistics, + ) + from ._core._synchronization import Condition as Condition + from ._core._synchronization import ConditionStatistics as ConditionStatistics + from ._core._synchronization import Event as Event + from ._core._synchronization import EventStatistics as EventStatistics + from ._core._synchronization import Lock as Lock + from ._core._synchronization import LockStatistics as LockStatistics + from ._core._synchronization import ResourceGuard as ResourceGuard + from ._core._synchronization import Semaphore as Semaphore + from ._core._synchronization import SemaphoreStatistics as SemaphoreStatistics + from ._core._tasks import TASK_STATUS_IGNORED as TASK_STATUS_IGNORED + from ._core._tasks import CancelScope as CancelScope + from ._core._tasks import TaskHandle as TaskHandle + from ._core._tasks import create_task_group as create_task_group + from ._core._tasks import current_effective_deadline as current_effective_deadline + from ._core._tasks import fail_after as fail_after + from ._core._tasks import fail_at as fail_at + from ._core._tasks import move_on_after as move_on_after + from ._core._tasks import move_on_at as move_on_at + from ._core._tempfile import NamedTemporaryFile as NamedTemporaryFile + from ._core._tempfile import SpooledTemporaryFile as SpooledTemporaryFile + from ._core._tempfile import TemporaryDirectory as TemporaryDirectory + from ._core._tempfile import TemporaryFile as TemporaryFile + from ._core._tempfile import gettempdir as gettempdir + from ._core._tempfile import gettempdirb as gettempdirb + from ._core._tempfile import mkdtemp as mkdtemp + from ._core._tempfile import mkstemp as mkstemp + from ._core._testing import TaskInfo as TaskInfo + from ._core._testing import get_current_task as get_current_task + from ._core._testing import get_running_tasks as get_running_tasks + from ._core._testing import wait_all_tasks_blocked as wait_all_tasks_blocked + from ._core._typedattr import TypedAttributeProvider as TypedAttributeProvider + from ._core._typedattr import TypedAttributeSet as TypedAttributeSet + from ._core._typedattr import typed_attribute as typed_attribute + + # ruff: isort: off + from . import ( + abc as abc, + ) + from . import ( + from_thread as from_thread, + ) + from . import ( + functools as functools, + ) + from . import ( + itertools as itertools, + ) + from . import ( + lowlevel as lowlevel, + ) + from . import ( + to_interpreter as to_interpreter, + ) + from . import ( + to_process as to_process, + ) + from . import ( + to_thread as to_thread, + ) + + fix_package_names() + set_deprecated_aliases( + { + "BrokenWorkerIntepreter": "anyio.BrokenWorkerInterpreter", + } + ) diff --git a/source/target-vendors/cp312/anyio/_backends/__init__.py b/source/target-vendors/cp312/anyio/_backends/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/anyio/_backends/_asyncio.py b/source/target-vendors/cp312/anyio/_backends/_asyncio.py new file mode 100644 index 0000000..7eba257 --- /dev/null +++ b/source/target-vendors/cp312/anyio/_backends/_asyncio.py @@ -0,0 +1,3201 @@ +from __future__ import annotations + +import array +import asyncio +import concurrent.futures +import math +import os +import socket +import sys +import threading +import weakref +from asyncio import ( + AbstractEventLoop, + CancelledError, + all_tasks, + create_task, + current_task, + get_running_loop, + sleep, +) +from asyncio.base_events import _run_until_complete_cb # type: ignore[attr-defined] +from collections import OrderedDict, deque +from collections.abc import ( + AsyncGenerator, + AsyncIterator, + Awaitable, + Callable, + Collection, + Coroutine, + Iterable, + Sequence, +) +from concurrent.futures import Future +from contextlib import AbstractContextManager, suppress +from contextvars import Context, copy_context +from dataclasses import dataclass, field +from functools import partial, wraps +from inspect import ( + CORO_RUNNING, + CORO_SUSPENDED, + getcoroutinestate, +) +from io import IOBase +from os import PathLike +from queue import Queue +from signal import Signals +from socket import AddressFamily, SocketKind +from threading import Thread +from types import CodeType, TracebackType +from typing import ( + IO, + TYPE_CHECKING, + Any, + Literal, + ParamSpec, + TypeVar, + cast, +) +from weakref import WeakKeyDictionary + +from .. import ( + CapacityLimiterStatistics, + EventStatistics, + LockStatistics, + TaskInfo, + abc, +) +from .._core._eventloop import ( + claim_worker_thread, + set_current_async_library, + threadlocals, +) +from .._core._exceptions import ( + BrokenResourceError, + BusyResourceError, + ClosedResourceError, + EndOfStream, + RunFinishedError, + WouldBlock, +) +from .._core._sockets import convert_ipv6_sockaddr +from .._core._streams import create_memory_object_stream +from .._core._synchronization import ( + CapacityLimiter as BaseCapacityLimiter, +) +from .._core._synchronization import Event as BaseEvent +from .._core._synchronization import Lock as BaseLock +from .._core._synchronization import ( + ResourceGuard, + SemaphoreStatistics, +) +from .._core._synchronization import Semaphore as BaseSemaphore +from .._core._tasks import CancelScope as BaseCancelScope +from .._core._tasks import TaskHandle +from ..abc import ( + AsyncBackend, + IPSockAddrType, + SocketListener, + UDPPacketType, + UNIXDatagramPacketType, +) +from ..abc._tasks import call_for_coroutine, get_callable_name, get_coro_name +from ..lowlevel import RunVar, _run_vars + +if TYPE_CHECKING: + from _typeshed import FileDescriptorLike + + from ..abc._eventloop import StrOrBytesPath + from ..streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream +else: + FileDescriptorLike = object + +if sys.version_info >= (3, 11): + from asyncio import Runner + from typing import Self, TypeVarTuple, Unpack +else: + import contextvars + import enum + import signal + from asyncio import coroutines, events, exceptions, tasks + + from exceptiongroup import BaseExceptionGroup + from typing_extensions import Self, TypeVarTuple, Unpack + + class _State(enum.Enum): + CREATED = "created" + INITIALIZED = "initialized" + CLOSED = "closed" + + class Runner: + # Copied from CPython 3.11 + def __init__( + self, + *, + debug: bool | None = None, + loop_factory: Callable[[], AbstractEventLoop] | None = None, + ): + self._state = _State.CREATED + self._debug = debug + self._loop_factory = loop_factory + self._loop: AbstractEventLoop | None = None + self._context = None + self._interrupt_count = 0 + self._set_event_loop = False + + def __enter__(self) -> Self: + self._lazy_init() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def close(self) -> None: + """Shutdown and close event loop.""" + loop = self._loop + if self._state is not _State.INITIALIZED or loop is None: + return + try: + _cancel_all_tasks(loop) + loop.run_until_complete(loop.shutdown_asyncgens()) + if hasattr(loop, "shutdown_default_executor"): + loop.run_until_complete(loop.shutdown_default_executor()) + else: + loop.run_until_complete(_shutdown_default_executor(loop)) + finally: + if self._set_event_loop: + events.set_event_loop(None) + loop.close() + self._loop = None + self._state = _State.CLOSED + + def get_loop(self) -> AbstractEventLoop: + """Return embedded event loop.""" + self._lazy_init() + return self._loop + + def run(self, coro: Coroutine[T_Retval], *, context=None) -> T_Retval: + """Run a coroutine inside the embedded event loop.""" + if not coroutines.iscoroutine(coro): + raise ValueError(f"a coroutine was expected, got {coro!r}") + + if events._get_running_loop() is not None: + # fail fast with short traceback + raise RuntimeError( + "Runner.run() cannot be called from a running event loop" + ) + + self._lazy_init() + + if context is None: + context = self._context + task = context.run(self._loop.create_task, coro) + + if ( + threading.current_thread() is threading.main_thread() + and signal.getsignal(signal.SIGINT) is signal.default_int_handler + ): + sigint_handler = partial(self._on_sigint, main_task=task) + try: + signal.signal(signal.SIGINT, sigint_handler) + except ValueError: + # `signal.signal` may throw if `threading.main_thread` does + # not support signals (e.g. embedded interpreter with signals + # not registered - see gh-91880) + sigint_handler = None + else: + sigint_handler = None + + self._interrupt_count = 0 + try: + return self._loop.run_until_complete(task) + except exceptions.CancelledError: + if self._interrupt_count > 0: + uncancel = getattr(task, "uncancel", None) + if uncancel is not None and uncancel() == 0: + raise KeyboardInterrupt # noqa: B904 + raise # CancelledError + finally: + if ( + sigint_handler is not None + and signal.getsignal(signal.SIGINT) is sigint_handler + ): + signal.signal(signal.SIGINT, signal.default_int_handler) + + def _lazy_init(self) -> None: + if self._state is _State.CLOSED: + raise RuntimeError("Runner is closed") + if self._state is _State.INITIALIZED: + return + if self._loop_factory is None: + self._loop = events.new_event_loop() + if not self._set_event_loop: + # Call set_event_loop only once to avoid calling + # attach_loop multiple times on child watchers + events.set_event_loop(self._loop) + self._set_event_loop = True + else: + self._loop = self._loop_factory() + if self._debug is not None: + self._loop.set_debug(self._debug) + self._context = contextvars.copy_context() + self._state = _State.INITIALIZED + + def _on_sigint(self, signum, frame, main_task: asyncio.Task) -> None: + self._interrupt_count += 1 + if self._interrupt_count == 1 and not main_task.done(): + main_task.cancel() + # wakeup loop if it is blocked by select() with long timeout + self._loop.call_soon_threadsafe(lambda: None) + return + raise KeyboardInterrupt() + + def _cancel_all_tasks(loop: AbstractEventLoop) -> None: + to_cancel = tasks.all_tasks(loop) + if not to_cancel: + return + + for task in to_cancel: + task.cancel() + + loop.run_until_complete(tasks.gather(*to_cancel, return_exceptions=True)) + + for task in to_cancel: + if task.cancelled(): + continue + if task.exception() is not None: + loop.call_exception_handler( + { + "message": "unhandled exception during asyncio.run() shutdown", + "exception": task.exception(), + "task": task, + } + ) + + async def _shutdown_default_executor(loop: AbstractEventLoop) -> None: + """Schedule the shutdown of the default executor.""" + + def _do_shutdown(future: asyncio.futures.Future) -> None: + try: + loop._default_executor.shutdown(wait=True) # type: ignore[attr-defined] + loop.call_soon_threadsafe(future.set_result, None) + except Exception as ex: + loop.call_soon_threadsafe(future.set_exception, ex) + + loop._executor_shutdown_called = True + if loop._default_executor is None: + return + future = loop.create_future() + thread = threading.Thread(target=_do_shutdown, args=(future,)) + thread.start() + try: + await future + finally: + thread.join() + + +T_Retval = TypeVar("T_Retval") +T_co = TypeVar("T_co", covariant=True) +T_contra = TypeVar("T_contra", contravariant=True) +PosArgsT = TypeVarTuple("PosArgsT") +P = ParamSpec("P") + +_root_task: RunVar[asyncio.Task[Any] | None] = RunVar("_root_task") + + +def find_root_task() -> asyncio.Task: + root_task = _root_task.get(None) + if root_task is not None and not root_task.done(): + return root_task + + # Look for a task that has been started via run_until_complete() + for task in all_tasks(): + if task._callbacks and not task.done(): + for cb, context in task._callbacks: + if ( + cb is _run_until_complete_cb + or getattr(cb, "__module__", None) == "uvloop.loop" + ): + _root_task.set(task) + + def _unset(t: asyncio.Task[Any]) -> None: + if vars := _run_vars.get(t.get_loop()): + vars.pop(_root_task, None) + + # Register a callback to break the task -> loop -> _run_var[loop][_root_task] -> task cycle + # Also run it in its own context to not create another reference. + # We can't use RunVar.reset() here since these are called synchronously + # and thus lowlevel.current_token() (which RunVar.reset() depends on) fails. + task.add_done_callback(_unset, context=context) + return task + + # Look up the topmost task in the AnyIO task tree, if possible + task = cast(asyncio.Task, current_task()) + state = _task_states.get(task) + if state: + cancel_scope = state.cancel_scope + while cancel_scope and cancel_scope._parent_scope is not None: + cancel_scope = cancel_scope._parent_scope + + if cancel_scope is not None: + return cast(asyncio.Task, cancel_scope._host_task) + + return task + + +def _task_started(task: asyncio.Task) -> bool: + """Return ``True`` if the task has been started and has not finished.""" + # The task coro should never be None here, as we never add finished tasks to the + # task list + coro = task.get_coro() + assert coro is not None + return getcoroutinestate(coro) in (CORO_RUNNING, CORO_SUSPENDED) + + +# +# Timeouts and cancellation +# + + +def is_anyio_cancellation(exc: CancelledError) -> bool: + # Sometimes third party frameworks catch a CancelledError and raise a new one, so as + # a workaround we have to look at the previous ones in __context__ too for a + # matching cancel message + while True: + if ( + exc.args + and isinstance(exc.args[0], str) + and exc.args[0].startswith("Cancelled via cancel scope ") + ): + return True + + if isinstance(exc.__context__, CancelledError): + exc = exc.__context__ + continue + + return False + + +class CancelScope(BaseCancelScope): + __slots__ = ( + "_active", + "_cancel_called", + "_cancel_handle", + "_cancel_reason", + "_cancelled_caught", + "_child_scopes", + "_deadline", + "_host_task", + "_parent_scope", + "_pending_uncancellations", + "_shield", + "_tasks", + "_timeout_handle", + ) + + def __new__(cls, *, deadline: float = math.inf, shield: bool = False) -> Self: + return object.__new__(cls) + + def __init__(self, deadline: float = math.inf, shield: bool = False): + self._deadline = deadline + self._shield = shield + self._parent_scope: CancelScope | None = None + self._child_scopes: set[CancelScope] = set() + self._cancel_called = False + self._cancel_reason: str | None = None + self._cancelled_caught = False + self._active = False + self._timeout_handle: asyncio.TimerHandle | None = None + self._cancel_handle: asyncio.Handle | None = None + self._tasks: set[asyncio.Task] = set() + self._host_task: asyncio.Task | None = None + if sys.version_info >= (3, 11): + self._pending_uncancellations: int | None = 0 + else: + self._pending_uncancellations = None + + def __enter__(self) -> Self: + if self._active: + raise RuntimeError( + "Each CancelScope may only be used for a single 'with' block" + ) + + self._host_task = host_task = cast(asyncio.Task, current_task()) + self._tasks.add(host_task) + try: + task_state = _task_states[host_task] + except KeyError: + task_state = TaskState(None, self) + _task_states[host_task] = task_state + else: + self._parent_scope = task_state.cancel_scope + task_state.cancel_scope = self + if self._parent_scope is not None: + # If using an eager task factory, the parent scope may not even contain + # the host task + self._parent_scope._child_scopes.add(self) + self._parent_scope._tasks.discard(host_task) + + self._timeout() + self._active = True + + # Start cancelling the host task if the scope was cancelled before entering + if self._cancel_called: + self._deliver_cancellation(self) + + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + del exc_tb + + if not self._active: + raise RuntimeError("This cancel scope is not active") + if current_task() is not self._host_task: + raise RuntimeError( + "Attempted to exit cancel scope in a different task than it was " + "entered in" + ) + + assert self._host_task is not None + host_task_state = _task_states.get(self._host_task) + if host_task_state is None or host_task_state.cancel_scope is not self: + raise RuntimeError( + "Attempted to exit a cancel scope that isn't the current tasks's " + "current cancel scope" + ) + + try: + self._active = False + if self._timeout_handle: + self._timeout_handle.cancel() + self._timeout_handle = None + + self._tasks.remove(self._host_task) + if self._parent_scope is not None: + self._parent_scope._child_scopes.remove(self) + self._parent_scope._tasks.add(self._host_task) + + host_task_state.cancel_scope = self._parent_scope + + # Restart the cancellation effort in the closest visible, cancelled parent + # scope if necessary + self._restart_cancellation_in_parent() + + # We only swallow the exception iff it was an AnyIO CancelledError, either + # directly as exc_val or inside an exception group and there are no cancelled + # parent cancel scopes visible to us here + if self._cancel_called and not self._parent_cancellation_is_visible_to_us: + # For each level-cancel() call made on the host task, call uncancel() + while self._pending_uncancellations: + self._host_task.uncancel() + self._pending_uncancellations -= 1 + + # Update cancelled_caught and check for exceptions we must not swallow + if isinstance(exc_val, BaseExceptionGroup): + cancelleds_caught, remaining = exc_val.split( + lambda exc: ( + isinstance(exc, CancelledError) + and is_anyio_cancellation(exc) + ) + ) + + if cancelleds_caught is None: + return False + + self._cancelled_caught = True + + if remaining is None: + return True + + context = remaining.__context__ + try: + # Preserve __cause__ and __suppress_context__ by avoiding `raise + # ... from ...` + raise remaining + finally: + # Preserve __context__ + remaining.__context__ = context + del context + else: + if isinstance(exc_val, CancelledError) and is_anyio_cancellation( + exc_val + ): + self._cancelled_caught = True + return True + else: + return False + else: + if self._pending_uncancellations: + assert self._parent_scope is not None + assert self._parent_scope._pending_uncancellations is not None + self._parent_scope._pending_uncancellations += ( + self._pending_uncancellations + ) + self._pending_uncancellations = 0 + + return False + finally: + self._host_task = None + del exc_val + + @property + def _effectively_cancelled(self) -> bool: + cancel_scope: CancelScope | None = self + while cancel_scope is not None: + if cancel_scope._cancel_called: + return True + + if cancel_scope.shield: + return False + + cancel_scope = cancel_scope._parent_scope + + return False + + @property + def _parent_cancellation_is_visible_to_us(self) -> bool: + return ( + self._parent_scope is not None + and not self.shield + and self._parent_scope._effectively_cancelled + ) + + def _timeout(self) -> None: + if self._deadline != math.inf: + loop = get_running_loop() + if loop.time() >= self._deadline: + self.cancel("deadline exceeded") + else: + self._timeout_handle = loop.call_at(self._deadline, self._timeout) + + def _deliver_cancellation(self, origin: CancelScope) -> bool: + """ + Deliver cancellation to directly contained tasks and nested cancel scopes. + + Schedule another run at the end if we still have tasks eligible for + cancellation. + + :param origin: the cancel scope that originated the cancellation + :return: ``True`` if the delivery needs to be retried on the next cycle + + """ + should_retry = False + current = current_task() + for task in self._tasks: + # Always skip tasks that are already done (see issue #1111) + if task.done(): + continue + + should_retry = True + if task._must_cancel: # type: ignore[attr-defined] + continue + + # The task is eligible for cancellation if it has started + if task is not current and (task is self._host_task or _task_started(task)): + waiter = task._fut_waiter # type: ignore[attr-defined] + if not isinstance(waiter, asyncio.Future) or not waiter.done(): + task.cancel(origin._cancel_reason) + if ( + task is origin._host_task + and origin._pending_uncancellations is not None + ): + origin._pending_uncancellations += 1 + + # Deliver cancellation to child scopes that aren't shielded or running their own + # cancellation callbacks + for scope in self._child_scopes: + if not scope._shield and not scope.cancel_called: + should_retry = scope._deliver_cancellation(origin) or should_retry + + # Schedule another callback if there are still tasks left + if origin is self: + if should_retry: + self._cancel_handle = get_running_loop().call_soon( + self._deliver_cancellation, origin + ) + else: + self._cancel_handle = None + + return should_retry + + def _restart_cancellation_in_parent(self) -> None: + """ + Restart the cancellation effort in the closest directly cancelled parent scope. + + """ + scope = self._parent_scope + while scope is not None: + if scope._cancel_called: + if scope._cancel_handle is None: + scope._deliver_cancellation(scope) + + break + + # No point in looking beyond any shielded scope + if scope._shield: + break + + scope = scope._parent_scope + + def _reparent(self, new_parent: CancelScope) -> None: + """ + Move this active scope from its current parent to ``new_parent``. + + Used by :meth:`TaskGroup.start` to move a task that has just called + ``task_status.started()`` into the target task group's cancel scope. + """ + if self._parent_scope is new_parent: + return + + if self._parent_scope is not None: + self._parent_scope._child_scopes.discard(self) + + self._parent_scope = new_parent + new_parent._child_scopes.add(self) + + # If the new parent (or an ancestor) is already cancelled, (re)start the + # delivery loop to ensure we're cancelled at next checkpoint like Trio. + self._restart_cancellation_in_parent() + + def cancel(self, reason: str | None = None) -> None: + if not self._cancel_called: + if self._timeout_handle: + self._timeout_handle.cancel() + self._timeout_handle = None + + self._cancel_called = True + self._cancel_reason = f"Cancelled via cancel scope {id(self):x}" + if task := current_task(): + self._cancel_reason += f" by {task}" + + if reason: + self._cancel_reason += f"; reason: {reason}" + + if self._host_task is not None: + self._deliver_cancellation(self) + + @property + def deadline(self) -> float: + return self._deadline + + @deadline.setter + def deadline(self, value: float) -> None: + self._deadline = float(value) + if self._timeout_handle is not None: + self._timeout_handle.cancel() + self._timeout_handle = None + + if self._active and not self._cancel_called: + self._timeout() + + @property + def cancel_called(self) -> bool: + return self._cancel_called + + @property + def cancelled_caught(self) -> bool: + return self._cancelled_caught + + @property + def shield(self) -> bool: + return self._shield + + @shield.setter + def shield(self, value: bool) -> None: + if self._shield != value: + self._shield = value + if not value: + self._restart_cancellation_in_parent() + + +# +# Task states +# + + +class TaskState: + """ + Encapsulates auxiliary task information that cannot be added to the Task instance + itself because there are no guarantees about its implementation. + """ + + __slots__ = "__weakref__", "cancel_scope", "parent_id" + + def __init__(self, parent_id: int | None, cancel_scope: CancelScope | None): + self.parent_id = parent_id + self.cancel_scope = cancel_scope + + +_task_states: WeakKeyDictionary[asyncio.Task, TaskState] = WeakKeyDictionary() + + +# +# Task groups +# + + +class _AsyncioTaskStatus(abc.TaskStatus[T_contra]): + def __init__( + self, + future: asyncio.Future, + parent_id: int, + target_scope: CancelScope, + spawn_scope: CancelScope, + ): + self._future = future + self._parent_id = parent_id + # The eventual parent scope for this spawn_scope + # (after task_status.started() has been called) + self._target_scope = target_scope + # The task's own cancel scope, also held by its TaskHandle + self._spawn_scope = spawn_scope + + def started(self, value: T_contra | None = None) -> None: + task = cast(asyncio.Task, current_task()) + _task_states[task].parent_id = self._parent_id + if self._future.done(): + if not self._future.cancelled(): + raise RuntimeError("called 'started' twice on the same task status") + else: + # Caller of start() was cancelled, nothing to reparent + return + + self._future.set_result(value) + self._spawn_scope._reparent(self._target_scope) + + +if sys.version_info >= (3, 12): + _eager_task_factory_code: CodeType | None = asyncio.eager_task_factory.__code__ +else: + _eager_task_factory_code = None + + +class TaskGroup(abc.TaskGroup): + def __init__(self) -> None: + self.cancel_scope: CancelScope = CancelScope() + self._entered = False + self._exceptions: list[BaseException] = [] + self._tasks: set[asyncio.Task] = set() + self._on_completed_fut: asyncio.Future[None] | None = None + + async def __aenter__(self) -> Self: + if self._entered: + raise RuntimeError("TaskGroup cannot be entered more than once") + + self._entered = True + + self.cancel_scope.__enter__() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + try: + if exc_val is not None: + self.cancel_scope.cancel() + if not isinstance(exc_val, CancelledError): + self._exceptions.append(exc_val) + + loop = get_running_loop() + try: + if self._tasks: + with CancelScope() as wait_scope: + while self._tasks: + self._on_completed_fut = loop.create_future() + + try: + await self._on_completed_fut + except CancelledError as exc: + # Shield the scope against further cancellation attempts, + # as they're not productive (#695) + wait_scope.shield = True + self.cancel_scope.cancel() + + # Set exc_val from the cancellation exception if it was + # previously unset. However, we should not replace a native + # cancellation exception with one raise by a cancel scope. + if exc_val is None or ( + isinstance(exc_val, CancelledError) + and not is_anyio_cancellation(exc) + ): + exc_val = exc + + self._on_completed_fut = None + else: + # If there are no child tasks to wait on, run at least one checkpoint + # anyway + await AsyncIOBackend.cancel_shielded_checkpoint() + + if self._exceptions: + # The exception that got us here should already have been + # added to self._exceptions so it's ok to break exception + # chaining and avoid adding a "During handling of above..." + # for each nesting level. + raise BaseExceptionGroup( + "unhandled errors in a TaskGroup", self._exceptions + ) from None + elif exc_val: + raise exc_val + except BaseException as exc: + if self.cancel_scope.__exit__(type(exc), exc, exc.__traceback__): + return True + + raise + + return self.cancel_scope.__exit__(exc_type, exc_val, exc_tb) + finally: + del exc_val, exc_tb, self._exceptions + + def _spawn( + self, + coro: Coroutine[Any, Any, T_co], + name: object, + task_status: _AsyncioTaskStatus | None = None, + ) -> TaskHandle[T_co]: + task_status_future = task_status._future if task_status is not None else None + + def task_done(_task: asyncio.Task) -> None: + if sys.version_info >= (3, 14) and self.cancel_scope._host_task is not None: + asyncio.future_discard_from_awaited_by( + _task, self.cancel_scope._host_task + ) + + task_state = _task_states[_task] + assert task_state.cancel_scope is not None + assert _task in task_state.cancel_scope._tasks + task_state.cancel_scope._tasks.remove(_task) + self._tasks.remove(task) + del _task_states[_task] + + if self._on_completed_fut is not None and not self._tasks: + try: + self._on_completed_fut.set_result(None) + except asyncio.InvalidStateError: + pass + + try: + exc = _task.exception() + except CancelledError as e: + while isinstance(e.__context__, CancelledError): + e = e.__context__ + + exc = e + + if exc is not None: + # The future can only be in the cancelled state if the host task was + # cancelled, so return immediately instead of adding one more + # CancelledError to the exceptions list + if task_status_future is not None and task_status_future.cancelled(): + return + + if task_status_future is None or task_status_future.done(): + if not isinstance(exc, CancelledError): + self._exceptions.append(exc) + + if not self.cancel_scope._effectively_cancelled: + self.cancel_scope.cancel() + else: + task_status_future.set_exception(exc) + elif task_status_future is not None and not task_status_future.done(): + task_status_future.set_exception( + RuntimeError("Child exited without calling task_status.started()") + ) + + if task_status_future is not None: + parent_id = id(current_task()) + caller_state = _task_states.get(cast(asyncio.Task, current_task())) + if caller_state is not None and caller_state.cancel_scope is not None: + initial_scope = caller_state.cancel_scope + else: + # The caller is an unmanaged task (no task state) + initial_scope = self.cancel_scope + else: + parent_id = id(self.cancel_scope._host_task) + initial_scope = self.cancel_scope + + spawn_scope = task_status._spawn_scope if task_status is not None else None + handle = TaskHandle(coro, name, cancel_scope=spawn_scope) + loop = asyncio.get_running_loop() + wrapper_coro = handle._run_coro() + try: + if ( + (factory := loop.get_task_factory()) + and getattr(factory, "__code__", None) is _eager_task_factory_code + and (closure := getattr(factory, "__closure__", None)) + ): + custom_task_constructor = closure[0].cell_contents + task = custom_task_constructor( + wrapper_coro, loop=loop, name=handle.name + ) + else: + task = loop.create_task(wrapper_coro, name=handle.name) + except BaseException: + with suppress(BaseException): + wrapper_coro.close() + + with suppress(BaseException): + coro.close() + + raise + + # Make the spawned task inherit the initial cancel scope + _task_states[task] = TaskState(parent_id=parent_id, cancel_scope=initial_scope) + initial_scope._tasks.add(task) + self._tasks.add(task) + if sys.version_info >= (3, 14) and self.cancel_scope._host_task is not None: + asyncio.future_add_to_awaited_by(task, self.cancel_scope._host_task) + + task.add_done_callback(task_done) + return handle + + def create_task( + self, + coro: Coroutine[Any, Any, T_co], + *, + name: object = None, + context: Context | None = None, + ) -> TaskHandle[T_co]: + if not isinstance(coro, Coroutine): + raise TypeError(f"expected a coroutine, got {coro.__class__.__qualname__}") + + if not self._entered or not self.cancel_scope._active: + coro.close() + raise RuntimeError( + "This task group is not active; no new tasks can be started." + ) + + final_name = get_coro_name(coro, name) + + if context is not None: + return context.run(self._spawn, coro, name=final_name) + else: + return self._spawn(coro, name=final_name) + + async def start( + self, + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + *args: Unpack[PosArgsT], + name: object = None, + return_handle: Literal[False, True] = False, + ) -> Any: + if not self._entered or not self.cancel_scope._active: + raise RuntimeError( + "This task group is not active; no new tasks can be started." + ) + + # Until task_status.started() is called, a task spawned via start() belongs to + # the *caller's* cancel scope, not the target group's, so cancelling the group + # does not cancel a task that hasn't reported startup yet. The + # task_status.started() call moves the task's cancel scope to this task group's + # cancel scope. + # + # The caller may be an unmanaged task (no _task_states entry), in which case + # fall back to the group's own scope. + future: asyncio.Future = asyncio.Future() + final_name = get_callable_name(func, name) + task_status: _AsyncioTaskStatus[Any] = _AsyncioTaskStatus( + future, id(self.cancel_scope._host_task), self.cancel_scope, CancelScope() + ) + coro = call_for_coroutine(func, args, task_status=task_status) + handle = self._spawn(coro, final_name, task_status) + + # If the task raises an exception after sending a start value without a switch + # point between, the task group is cancelled and this method never proceeds to + # process the completed future. That's why we have to have a shielded cancel + # scope here. + try: + await future + except BaseException: + if handle.status is TaskHandle.Status.PENDING: + # Cancel the task and wait for it to exit before returning + handle.cancel() + with CancelScope(shield=True): + await handle.wait() + + raise + + if return_handle: + handle._start_value = future.result() + return handle + else: + return future.result() + + +# +# Threads +# + +_Retval_Queue_Type = tuple[T_Retval | None, BaseException | None] + + +class WorkerThread(Thread): + MAX_IDLE_TIME = 10 # seconds + + def __init__( + self, + root_task: asyncio.Task, + workers: set[WorkerThread], + idle_workers: deque[WorkerThread], + ): + kwargs: dict[str, Any] = {} + if sys.version_info >= (3, 14): + kwargs["context"] = Context() + + super().__init__(name="AnyIO worker thread", **kwargs) + self.root_task = root_task + self.workers = workers + self.idle_workers = idle_workers + self.loop = root_task._loop + self.queue: Queue[ + tuple[Context, Callable, tuple, asyncio.Future, CancelScope] | None + ] = Queue(2) + self.idle_since = AsyncIOBackend.current_time() + self.stopping = False + + def _report_result( + self, future: asyncio.Future, result: Any, exc: BaseException | None + ) -> None: + self.idle_since = AsyncIOBackend.current_time() + if not self.stopping: + self.idle_workers.append(self) + + if not future.cancelled(): + if exc is not None: + if isinstance(exc, StopIteration): + new_exc = RuntimeError("coroutine raised StopIteration") + new_exc.__cause__ = exc + exc = new_exc + + future.set_exception(exc) + else: + future.set_result(result) + + def run(self) -> None: + with claim_worker_thread(AsyncIOBackend, self.loop): + while True: + item = self.queue.get() + if item is None: + # Shutdown command received + return + + context, func, args, future, cancel_scope = item + if not future.cancelled(): + result = None + exception: BaseException | None = None + threadlocals.current_cancel_scope = cancel_scope + try: + result = context.run(func, *args) + except BaseException as exc: + exception = exc + finally: + del threadlocals.current_cancel_scope + + try: + self.loop.call_soon_threadsafe( + self._report_result, future, result, exception + ) + except RuntimeError: + if not self.loop.is_closed(): + raise + + del result, exception + + self.queue.task_done() + del item, context, func, args, future, cancel_scope + + def stop(self, f: asyncio.Task | None = None) -> None: + self.stopping = True + self.queue.put_nowait(None) + self.workers.discard(self) + try: + self.idle_workers.remove(self) + except ValueError: + pass + + +_threadpool_idle_workers: RunVar[deque[WorkerThread]] = RunVar( + "_threadpool_idle_workers" +) +_threadpool_workers: RunVar[set[WorkerThread]] = RunVar("_threadpool_workers") + + +# +# Subprocesses +# + + +@dataclass(eq=False) +class StreamReaderWrapper(abc.ByteReceiveStream): + _stream: asyncio.StreamReader + + async def receive(self, max_bytes: int = 65536) -> bytes: + if max_bytes < 1: + raise ValueError("max_bytes must be a positive integer") + + data = await self._stream.read(max_bytes) + if data: + return data + else: + raise EndOfStream + + async def aclose(self) -> None: + self._stream.set_exception(ClosedResourceError()) + await AsyncIOBackend.checkpoint() + + +@dataclass(eq=False) +class StreamWriterWrapper(abc.ByteSendStream): + _stream: asyncio.StreamWriter + _closed: bool = field(init=False, default=False) + + async def send(self, item: bytes) -> None: + await AsyncIOBackend.checkpoint_if_cancelled() + stream_paused = self._stream._protocol._paused # type: ignore[attr-defined] + try: + self._stream.write(item) + await self._stream.drain() + except (ConnectionResetError, BrokenPipeError, RuntimeError) as exc: + # If closed by us and/or the peer: + # * on stdlib, drain() raises ConnectionResetError or BrokenPipeError + # * on uvloop and Winloop, write() eventually starts raising RuntimeError + if self._closed: + raise ClosedResourceError from exc + elif self._stream.is_closing(): + raise BrokenResourceError from exc + + raise + + if not stream_paused: + await AsyncIOBackend.cancel_shielded_checkpoint() + + async def aclose(self) -> None: + self._closed = True + self._stream.close() + await AsyncIOBackend.checkpoint() + + +@dataclass(eq=False) +class Process(abc.Process): + _process: asyncio.subprocess.Process + _stdin: StreamWriterWrapper | None + _stdout: StreamReaderWrapper | None + _stderr: StreamReaderWrapper | None + _exited: asyncio.Event + _transport: asyncio.SubprocessTransport + + async def aclose(self) -> None: + with CancelScope(shield=True) as scope: + # We need to close the underlying pipe_transports as well to allow a + # process blocking on full buffers to receive SIGPIPE and exit. + if self._stdin: + await self._stdin.aclose() + if pipe := self._transport.get_pipe_transport(0): + pipe.close() + if self._stdout: + await self._stdout.aclose() + if pipe := self._transport.get_pipe_transport(1): + pipe.close() + if self._stderr: + await self._stderr.aclose() + if pipe := self._transport.get_pipe_transport(2): + pipe.close() + + scope.shield = False + try: + await self.wait() + except BaseException: + scope.shield = True + # Closing the transport on asyncio also handles sending kill + self._transport.close() + await self.wait() + raise + + async def wait(self) -> int: + await self._exited.wait() + assert self._process.returncode is not None + return self._process.returncode + + def terminate(self) -> None: + self._process.terminate() + + def kill(self) -> None: + self._process.kill() + + def send_signal(self, signal: int) -> None: + self._process.send_signal(signal) + + @property + def pid(self) -> int: + return self._process.pid + + @property + def returncode(self) -> int | None: + return self._process.returncode + + @property + def stdin(self) -> abc.ByteSendStream | None: + return self._stdin + + @property + def stdout(self) -> abc.ByteReceiveStream | None: + return self._stdout + + @property + def stderr(self) -> abc.ByteReceiveStream | None: + return self._stderr + + +def _forcibly_shutdown_process_pool_on_exit( + workers: set[Process], _task: object +) -> None: + """ + Forcibly shuts down worker processes belonging to this event loop.""" + child_watcher: asyncio.AbstractChildWatcher | None = None # type: ignore[name-defined] + if sys.version_info < (3, 12): + try: + child_watcher = asyncio.get_event_loop_policy().get_child_watcher() + except NotImplementedError: + pass + + # Close as much as possible (w/o async/await) to avoid warnings + for process in workers.copy(): + if process.returncode is not None: + continue + + process._stdin._stream._transport.close() # type: ignore[union-attr] + process._stdout._stream._transport.close() # type: ignore[union-attr] + process._stderr._stream._transport.close() # type: ignore[union-attr] + process.kill() + if child_watcher: + child_watcher.remove_child_handler(process.pid) + + +async def _shutdown_process_pool_on_exit(workers: set[abc.Process]) -> None: + """ + Shuts down worker processes belonging to this event loop. + + NOTE: this only works when the event loop was started using asyncio.run() or + anyio.run(). + + """ + process: abc.Process + try: + await sleep(math.inf) + except asyncio.CancelledError: + workers = workers.copy() + for process in workers: + if process.returncode is None: + process.kill() + + for process in workers: + await process.aclose() + + +# +# Sockets and networking +# + + +class StreamProtocol(asyncio.Protocol): + read_queue: deque[bytes] + read_event: asyncio.Event + write_event: asyncio.Event + exception: Exception | None = None + is_at_eof: bool = False + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + self.read_queue = deque() + self.read_event = asyncio.Event() + self.write_event = asyncio.Event() + self.write_event.set() + cast(asyncio.Transport, transport).set_write_buffer_limits(0) + + def connection_lost(self, exc: Exception | None) -> None: + if exc: + self.exception = exc + + self.read_event.set() + self.write_event.set() + + def data_received(self, data: bytes) -> None: + # ProactorEventloop sometimes sends bytearray instead of bytes + self.read_queue.append(bytes(data)) + self.read_event.set() + + def eof_received(self) -> bool | None: + self.is_at_eof = True + self.read_event.set() + return True + + def pause_writing(self) -> None: + self.write_event = asyncio.Event() + + def resume_writing(self) -> None: + self.write_event.set() + + +class DatagramProtocol(asyncio.DatagramProtocol): + read_queue: deque[tuple[bytes, IPSockAddrType]] + read_event: asyncio.Event + write_event: asyncio.Event + closed_event: asyncio.Event + exception: Exception | None = None + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + self.read_queue = deque(maxlen=100) # arbitrary value + self.read_event = asyncio.Event() + self.write_event = asyncio.Event() + self.closed_event = asyncio.Event() + self.write_event.set() + + def connection_lost(self, exc: Exception | None) -> None: + self.read_event.set() + self.write_event.set() + self.closed_event.set() + + def datagram_received(self, data: bytes, addr: IPSockAddrType) -> None: + addr = convert_ipv6_sockaddr(addr) + self.read_queue.append((data, addr)) + self.read_event.set() + + def error_received(self, exc: Exception) -> None: + self.exception = exc + + def pause_writing(self) -> None: + self.write_event.clear() + + def resume_writing(self) -> None: + self.write_event.set() + + +class SocketStream(abc.SocketStream): + def __init__(self, transport: asyncio.Transport, protocol: StreamProtocol): + self._transport = transport + self._protocol = protocol + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + self._closed = False + + @property + def _raw_socket(self) -> socket.socket: + return self._transport.get_extra_info("socket") + + async def receive(self, max_bytes: int = 65536) -> bytes: + if max_bytes < 1: + raise ValueError("max_bytes must be a positive integer") + + with self._receive_guard: + if ( + not self._protocol.read_event.is_set() + and not self._transport.is_closing() + and not self._protocol.is_at_eof + ): + self._transport.resume_reading() + await self._protocol.read_event.wait() + self._transport.pause_reading() + else: + await AsyncIOBackend.checkpoint() + + try: + chunk = self._protocol.read_queue.popleft() + except IndexError: + if self._closed: + raise ClosedResourceError from None + elif self._protocol.exception: + raise BrokenResourceError from self._protocol.exception + else: + raise EndOfStream from None + + if len(chunk) > max_bytes: + # Split the oversized chunk + chunk, leftover = chunk[:max_bytes], chunk[max_bytes:] + self._protocol.read_queue.appendleft(leftover) + + # If the read queue is empty, clear the flag so that the next call will + # block until data is available + if not self._protocol.read_queue: + self._protocol.read_event.clear() + + return chunk + + async def send(self, item: bytes) -> None: + with self._send_guard: + await AsyncIOBackend.checkpoint() + + if self._closed: + raise ClosedResourceError + elif self._protocol.exception is not None: + raise BrokenResourceError from self._protocol.exception + + try: + self._transport.write(item) + except RuntimeError as exc: + if self._transport.is_closing(): + raise BrokenResourceError from exc + else: + raise + + await self._protocol.write_event.wait() + + async def send_eof(self) -> None: + try: + self._transport.write_eof() + except OSError: + pass + + async def aclose(self) -> None: + self._closed = True + if not self._transport.is_closing(): + try: + self._transport.write_eof() + except OSError: + pass + + self._transport.close() + await sleep(0) + self._transport.abort() + + +class _RawSocketMixin: + _receive_future: asyncio.Future | None = None + _send_future: asyncio.Future | None = None + _closing = False + + def __init__(self, raw_socket: socket.socket): + self.__raw_socket = raw_socket + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + + @property + def _raw_socket(self) -> socket.socket: + return self.__raw_socket + + def _wait_until_readable(self, loop: asyncio.AbstractEventLoop) -> asyncio.Future: + def callback(f: object) -> None: + del self._receive_future + loop.remove_reader(self.__raw_socket) + + f = self._receive_future = asyncio.Future() + loop.add_reader(self.__raw_socket, f.set_result, None) + f.add_done_callback(callback) + return f + + def _wait_until_writable(self, loop: asyncio.AbstractEventLoop) -> asyncio.Future: + def callback(f: object) -> None: + del self._send_future + loop.remove_writer(self.__raw_socket) + + f = self._send_future = asyncio.Future() + loop.add_writer(self.__raw_socket, f.set_result, None) + f.add_done_callback(callback) + return f + + async def aclose(self) -> None: + if not self._closing: + self._closing = True + if self.__raw_socket.fileno() != -1: + self.__raw_socket.close() + + if self._receive_future and not self._receive_future.done(): + self._receive_future.set_result(None) + if self._send_future and not self._send_future.done(): + self._send_future.set_result(None) + + +class UNIXSocketStream(_RawSocketMixin, abc.UNIXSocketStream): + async def send_eof(self) -> None: + with self._send_guard: + self._raw_socket.shutdown(socket.SHUT_WR) + + async def receive(self, max_bytes: int = 65536) -> bytes: + if max_bytes < 1: + raise ValueError("max_bytes must be a positive integer") + + loop = get_running_loop() + await AsyncIOBackend.checkpoint() + with self._receive_guard: + while True: + try: + data = self._raw_socket.recv(max_bytes) + except BlockingIOError: + await self._wait_until_readable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + else: + if not data: + raise EndOfStream + + return data + + async def send(self, item: bytes) -> None: + loop = get_running_loop() + await AsyncIOBackend.checkpoint() + with self._send_guard: + view = memoryview(item) + while view: + try: + bytes_sent = self._raw_socket.send(view) + except BlockingIOError: + await self._wait_until_writable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + else: + view = view[bytes_sent:] + + async def receive_fds(self, msglen: int, maxfds: int) -> tuple[bytes, list[int]]: + if not isinstance(msglen, int) or msglen < 0: + raise ValueError("msglen must be a non-negative integer") + if not isinstance(maxfds, int) or maxfds < 1: + raise ValueError("maxfds must be a positive integer") + + loop = get_running_loop() + fds = array.array("i") + await AsyncIOBackend.checkpoint() + with self._receive_guard: + while True: + try: + message, ancdata, _flags, _addr = self._raw_socket.recvmsg( + msglen, socket.CMSG_LEN(maxfds * fds.itemsize) + ) + except BlockingIOError: + await self._wait_until_readable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + else: + if not message and not ancdata: + raise EndOfStream + + break + + for cmsg_level, cmsg_type, cmsg_data in ancdata: + if cmsg_level != socket.SOL_SOCKET or cmsg_type != socket.SCM_RIGHTS: + raise RuntimeError( + f"Received unexpected ancillary data; message = {message!r}, " + f"cmsg_level = {cmsg_level}, cmsg_type = {cmsg_type}" + ) + + fds.frombytes(cmsg_data[: len(cmsg_data) - (len(cmsg_data) % fds.itemsize)]) + + return message, list(fds) + + async def send_fds(self, message: bytes, fds: Collection[int | IOBase]) -> None: + if not message: + raise ValueError("message must not be empty") + if not fds: + raise ValueError("fds must not be empty") + + loop = get_running_loop() + filenos: list[int] = [] + for fd in fds: + if isinstance(fd, int): + filenos.append(fd) + elif isinstance(fd, IOBase): + filenos.append(fd.fileno()) + + fdarray = array.array("i", filenos) + await AsyncIOBackend.checkpoint() + with self._send_guard: + while True: + try: + # The ignore can be removed after mypy picks up + # https://github.com/python/typeshed/pull/5545 + self._raw_socket.sendmsg( + [message], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fdarray)] + ) + break + except BlockingIOError: + await self._wait_until_writable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + + +class TCPSocketListener(abc.SocketListener): + _accept_scope: CancelScope | None = None + _closed = False + + def __init__(self, raw_socket: socket.socket): + self.__raw_socket = raw_socket + self._loop = cast(asyncio.BaseEventLoop, get_running_loop()) + self._accept_guard = ResourceGuard("accepting connections from") + + @property + def _raw_socket(self) -> socket.socket: + return self.__raw_socket + + async def accept(self) -> abc.SocketStream: + if self._closed: + raise ClosedResourceError + + with self._accept_guard: + await AsyncIOBackend.checkpoint() + with CancelScope() as self._accept_scope: + try: + client_sock, _addr = await self._loop.sock_accept(self._raw_socket) + except asyncio.CancelledError: + # Workaround for https://bugs.python.org/issue41317 + try: + self._loop.remove_reader(self._raw_socket) + except (ValueError, NotImplementedError): + pass + + if self._closed: + raise ClosedResourceError from None + + raise + finally: + self._accept_scope = None + + client_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + transport, protocol = await self._loop.connect_accepted_socket( + StreamProtocol, client_sock + ) + return SocketStream(transport, protocol) + + async def aclose(self) -> None: + if self._closed: + return + + self._closed = True + if self._accept_scope: + # Workaround for https://bugs.python.org/issue41317 + try: + self._loop.remove_reader(self._raw_socket) + except (ValueError, NotImplementedError): + pass + + self._accept_scope.cancel() + await sleep(0) + + self._raw_socket.close() + + +class UNIXSocketListener(abc.SocketListener): + def __init__(self, raw_socket: socket.socket): + self.__raw_socket = raw_socket + self._loop = get_running_loop() + self._accept_guard = ResourceGuard("accepting connections from") + self._closed = False + + async def accept(self) -> abc.SocketStream: + await AsyncIOBackend.checkpoint() + with self._accept_guard: + while True: + try: + client_sock, _ = self.__raw_socket.accept() + client_sock.setblocking(False) + return UNIXSocketStream(client_sock) + except BlockingIOError: + f: asyncio.Future = asyncio.Future() + self._loop.add_reader(self.__raw_socket, f.set_result, None) + f.add_done_callback( + lambda _: self._loop.remove_reader(self.__raw_socket) + ) + await f + except OSError as exc: + if self._closed: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + + async def aclose(self) -> None: + self._closed = True + self.__raw_socket.close() + + @property + def _raw_socket(self) -> socket.socket: + return self.__raw_socket + + +class UDPSocket(abc.UDPSocket): + def __init__( + self, transport: asyncio.DatagramTransport, protocol: DatagramProtocol + ): + self._transport = transport + self._protocol = protocol + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + self._closed = False + + @property + def _raw_socket(self) -> socket.socket: + return self._transport.get_extra_info("socket") + + async def aclose(self) -> None: + self._closed = True + if not self._transport.is_closing(): + self._transport.close() + + await self._protocol.closed_event.wait() + + async def receive(self) -> tuple[bytes, IPSockAddrType]: + with self._receive_guard: + await AsyncIOBackend.checkpoint() + + # If the buffer is empty, ask for more data + if not self._protocol.read_queue and not self._transport.is_closing(): + self._protocol.read_event.clear() + await self._protocol.read_event.wait() + + try: + return self._protocol.read_queue.popleft() + except IndexError: + if self._closed: + raise ClosedResourceError from None + else: + raise BrokenResourceError from None + + async def send(self, item: UDPPacketType) -> None: + with self._send_guard: + await AsyncIOBackend.checkpoint() + await self._protocol.write_event.wait() + if self._closed: + raise ClosedResourceError + elif self._transport.is_closing(): + raise BrokenResourceError + else: + self._transport.sendto(*item) + + +class ConnectedUDPSocket(abc.ConnectedUDPSocket): + def __init__( + self, transport: asyncio.DatagramTransport, protocol: DatagramProtocol + ): + self._transport = transport + self._protocol = protocol + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + self._closed = False + + @property + def _raw_socket(self) -> socket.socket: + return self._transport.get_extra_info("socket") + + async def aclose(self) -> None: + self._closed = True + if not self._transport.is_closing(): + self._transport.close() + + await self._protocol.closed_event.wait() + + async def receive(self) -> bytes: + with self._receive_guard: + await AsyncIOBackend.checkpoint() + + # If the buffer is empty, ask for more data + if not self._protocol.read_queue and not self._transport.is_closing(): + self._protocol.read_event.clear() + await self._protocol.read_event.wait() + + try: + packet = self._protocol.read_queue.popleft() + except IndexError: + if self._closed: + raise ClosedResourceError from None + else: + raise BrokenResourceError from None + + return packet[0] + + async def send(self, item: bytes) -> None: + with self._send_guard: + await AsyncIOBackend.checkpoint() + await self._protocol.write_event.wait() + if self._closed: + raise ClosedResourceError + elif self._transport.is_closing(): + raise BrokenResourceError + else: + self._transport.sendto(item) + + +class UNIXDatagramSocket(_RawSocketMixin, abc.UNIXDatagramSocket): + async def receive(self) -> UNIXDatagramPacketType: + loop = get_running_loop() + await AsyncIOBackend.checkpoint() + with self._receive_guard: + while True: + try: + data = self._raw_socket.recvfrom(65536) + except BlockingIOError: + await self._wait_until_readable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + else: + return data + + async def send(self, item: UNIXDatagramPacketType) -> None: + loop = get_running_loop() + await AsyncIOBackend.checkpoint() + with self._send_guard: + while True: + try: + self._raw_socket.sendto(*item) + except BlockingIOError: + await self._wait_until_writable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + else: + return + + +class ConnectedUNIXDatagramSocket(_RawSocketMixin, abc.ConnectedUNIXDatagramSocket): + async def receive(self) -> bytes: + loop = get_running_loop() + await AsyncIOBackend.checkpoint() + with self._receive_guard: + while True: + try: + data = self._raw_socket.recv(65536) + except BlockingIOError: + await self._wait_until_readable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + else: + return data + + async def send(self, item: bytes) -> None: + loop = get_running_loop() + await AsyncIOBackend.checkpoint() + with self._send_guard: + while True: + try: + self._raw_socket.send(item) + except BlockingIOError: + await self._wait_until_writable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + else: + return + + +_read_events: RunVar[dict[int, asyncio.Future[bool]]] = RunVar("read_events") +_write_events: RunVar[dict[int, asyncio.Future[bool]]] = RunVar("write_events") + + +# +# Synchronization +# + + +class Event(BaseEvent): + __slots__ = ("_event",) + + def __new__(cls) -> Self: + return object.__new__(cls) + + def __init__(self) -> None: + self._event = asyncio.Event() + + def set(self) -> None: + self._event.set() + + def is_set(self) -> bool: + return self._event.is_set() + + async def wait(self) -> None: + if self.is_set(): + await AsyncIOBackend.checkpoint() + else: + await self._event.wait() + + def statistics(self) -> EventStatistics: + return EventStatistics(len(self._event._waiters)) + + +class Lock(BaseLock): + __slots__ = "_fast_acquire", "_owner_task", "_waiters" + + def __new__(cls, *, fast_acquire: bool = False) -> Self: + return object.__new__(cls) + + def __init__(self, *, fast_acquire: bool = False) -> None: + self._fast_acquire = fast_acquire + self._owner_task: asyncio.Task | None = None + self._waiters: deque[tuple[asyncio.Task, asyncio.Future]] = deque() + + async def acquire(self) -> None: + task = cast(asyncio.Task, current_task()) + if self._owner_task is None and not self._waiters: + await AsyncIOBackend.checkpoint_if_cancelled() + self._owner_task = task + + # Unless on the "fast path", yield control of the event loop so that other + # tasks can run too + if not self._fast_acquire: + try: + await AsyncIOBackend.cancel_shielded_checkpoint() + except CancelledError: + self.release() + raise + + return + + if self._owner_task == task: + raise RuntimeError("Attempted to acquire an already held Lock") + + fut: asyncio.Future[None] = asyncio.Future() + item = task, fut + self._waiters.append(item) + try: + await fut + except CancelledError: + if fut.cancelled(): + try: + self._waiters.remove(item) + except ValueError: + pass + else: + self.release() + + raise + + def acquire_nowait(self) -> None: + task = cast(asyncio.Task, current_task()) + if self._owner_task is None and not self._waiters: + self._owner_task = task + return + + if self._owner_task is task: + raise RuntimeError("Attempted to acquire an already held Lock") + + raise WouldBlock + + def locked(self) -> bool: + return self._owner_task is not None + + def release(self) -> None: + if self._owner_task != current_task(): + raise RuntimeError("The current task is not holding this lock") + + # A cancelled waiter that already received ownership removes itself from + # _waiters before calling release(); any cancelled waiter still queued here + # was cancelled before being woken, so drop it. + while self._waiters: + task, fut = self._waiters.popleft() + if fut.cancelled(): + continue + + self._owner_task = task + fut.set_result(None) + return + + self._owner_task = None + + def statistics(self) -> LockStatistics: + task_info = AsyncIOTaskInfo(self._owner_task) if self._owner_task else None + return LockStatistics(self.locked(), task_info, len(self._waiters)) + + +class Semaphore(BaseSemaphore): + __slots__ = "_fast_acquire", "_max_value", "_value", "_waiters" + + def __new__( + cls, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> Self: + return object.__new__(cls) + + def __init__( + self, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ): + super().__init__(initial_value, max_value=max_value) + self._value = initial_value + self._max_value = max_value + self._fast_acquire = fast_acquire + self._waiters: deque[asyncio.Future[None]] = deque() + + async def acquire(self) -> None: + if self._value > 0 and not self._waiters: + await AsyncIOBackend.checkpoint_if_cancelled() + self._value -= 1 + + # Unless on the "fast path", yield control of the event loop so that other + # tasks can run too + if not self._fast_acquire: + try: + await AsyncIOBackend.cancel_shielded_checkpoint() + except CancelledError: + self.release() + raise + + return + + fut: asyncio.Future[None] = asyncio.Future() + self._waiters.append(fut) + try: + await fut + except CancelledError: + if fut.cancelled(): + try: + self._waiters.remove(fut) + except ValueError: + pass + else: + self.release() + + raise + + def acquire_nowait(self) -> None: + if self._value == 0: + raise WouldBlock + + self._value -= 1 + + def release(self) -> None: + if self._max_value is not None and self._value == self._max_value: + raise ValueError("semaphore released too many times") + + while self._waiters: + fut = self._waiters.popleft() + if fut.cancelled(): + continue + + fut.set_result(None) + return + + self._value += 1 + + @property + def value(self) -> int: + return self._value + + @property + def max_value(self) -> int | None: + return self._max_value + + def statistics(self) -> SemaphoreStatistics: + return SemaphoreStatistics(len(self._waiters)) + + +class CapacityLimiter(BaseCapacityLimiter): + __slots__ = "_borrowers", "_total_tokens", "_wait_queue" + + def __new__(cls, total_tokens: float) -> Self: + return object.__new__(cls) + + def __init__(self, total_tokens: float): + self._total_tokens: float = 0 + self._borrowers: set[Any] = set() + self._wait_queue: OrderedDict[Any, asyncio.Event] = OrderedDict() + self.total_tokens = total_tokens + + async def __aenter__(self) -> None: + await self.acquire() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.release() + + @property + def total_tokens(self) -> float: + return self._total_tokens + + @total_tokens.setter + def total_tokens(self, value: float) -> None: + if not isinstance(value, int) and not math.isinf(value): + raise TypeError("total_tokens must be an int or math.inf") + + if value < 0: + raise ValueError("total_tokens must be >= 0") + + self._total_tokens = value + + # Notify waiting tasks that they have acquired the limiter while + # there is spare capacity. + while self._wait_queue and len(self._borrowers) < self._total_tokens: + borrower, event = self._wait_queue.popitem(last=False) + self._borrowers.add(borrower) + event.set() + + @property + def borrowed_tokens(self) -> int: + return len(self._borrowers) + + @property + def available_tokens(self) -> float: + return self._total_tokens - len(self._borrowers) + + def _notify_next_waiter(self) -> None: + """Hand a free token to the next task in line, if any.""" + if self._wait_queue and len(self._borrowers) < self._total_tokens: + borrower, event = self._wait_queue.popitem(last=False) + self._borrowers.add(borrower) + event.set() + + def acquire_nowait(self) -> None: + self.acquire_on_behalf_of_nowait(current_task()) + + def acquire_on_behalf_of_nowait(self, borrower: object) -> None: + if borrower in self._borrowers: + raise RuntimeError( + "this borrower is already holding one of this CapacityLimiter's tokens" + ) + + if self._wait_queue or len(self._borrowers) >= self._total_tokens: + raise WouldBlock + + self._borrowers.add(borrower) + + async def acquire(self) -> None: + return await self.acquire_on_behalf_of(current_task()) + + async def acquire_on_behalf_of(self, borrower: object) -> None: + await AsyncIOBackend.checkpoint_if_cancelled() + try: + self.acquire_on_behalf_of_nowait(borrower) + except WouldBlock: + event = asyncio.Event() + self._wait_queue[borrower] = event + try: + await event.wait() + except BaseException: + self._wait_queue.pop(borrower, None) + if event.is_set(): + self._borrowers.discard(borrower) + self._notify_next_waiter() + + raise + else: + try: + await AsyncIOBackend.cancel_shielded_checkpoint() + except BaseException: + self.release() + raise + + def release(self) -> None: + self.release_on_behalf_of(current_task()) + + def release_on_behalf_of(self, borrower: object) -> None: + try: + self._borrowers.remove(borrower) + except KeyError: + raise RuntimeError( + "this borrower isn't holding any of this CapacityLimiter's tokens" + ) from None + + self._notify_next_waiter() + + def statistics(self) -> CapacityLimiterStatistics: + return CapacityLimiterStatistics( + self.borrowed_tokens, + self.total_tokens, + tuple(self._borrowers), + len(self._wait_queue), + ) + + +_default_thread_limiter: RunVar[CapacityLimiter] = RunVar("_default_thread_limiter") + + +# +# Operating system signals +# + + +class _SignalReceiver: + def __init__(self, signals: tuple[Signals, ...]): + self._signals = signals + self._loop = get_running_loop() + self._signal_queue: deque[Signals] = deque() + self._future: asyncio.Future = asyncio.Future() + self._handled_signals: set[Signals] = set() + + def _deliver(self, signum: Signals) -> None: + self._signal_queue.append(signum) + if not self._future.done(): + self._future.set_result(None) + + def __enter__(self) -> Self: + for sig in set(self._signals): + self._loop.add_signal_handler(sig, self._deliver, sig) + self._handled_signals.add(sig) + + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + for sig in self._handled_signals: + self._loop.remove_signal_handler(sig) + + def __aiter__(self) -> _SignalReceiver: + return self + + async def __anext__(self) -> Signals: + await AsyncIOBackend.checkpoint() + if not self._signal_queue: + self._future = asyncio.Future() + await self._future + + return self._signal_queue.popleft() + + +# +# Testing and debugging +# + + +class AsyncIOTaskInfo(TaskInfo): + def __init__(self, task: asyncio.Task): + task_state = _task_states.get(task) + if task_state is None: + parent_id = None + else: + parent_id = task_state.parent_id + + coro = task.get_coro() + assert coro is not None, "created TaskInfo from a completed Task" + super().__init__(id(task), parent_id, task.get_name(), coro) + self._task = weakref.ref(task) + + def has_pending_cancellation(self) -> bool: + if not (task := self._task()): + # If the task isn't around anymore, it won't have a pending cancellation + return False + + if task._must_cancel or ( # type: ignore[attr-defined] + isinstance(task._fut_waiter, asyncio.Future) # type: ignore[attr-defined] + and task._fut_waiter.cancelled() # type: ignore[attr-defined] + ): + return True + + if task_state := _task_states.get(task): + if cancel_scope := task_state.cancel_scope: + return cancel_scope._effectively_cancelled + + return False + + +class TestRunner(abc.TestRunner): + _send_stream: MemoryObjectSendStream[tuple[Awaitable[Any], asyncio.Future[Any]]] + + def __init__( + self, + *, + debug: bool | None = None, + use_uvloop: bool = False, + loop_factory: Callable[[], AbstractEventLoop] | None = None, + ) -> None: + if use_uvloop and loop_factory is None: + if sys.platform != "win32": + import uvloop + + loop_factory = uvloop.new_event_loop + else: + import winloop + + loop_factory = winloop.new_event_loop + + self._runner = Runner(debug=debug, loop_factory=loop_factory) + self._exceptions: list[BaseException] = [] + self._runner_task: asyncio.Task | None = None + + def __enter__(self) -> Self: + self._runner.__enter__() + self.get_loop().set_exception_handler(self._exception_handler) + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self._runner.__exit__(exc_type, exc_val, exc_tb) + + def get_loop(self) -> AbstractEventLoop: + return self._runner.get_loop() + + def is_running(self) -> bool: + try: + asyncio.get_running_loop() + return True + except RuntimeError: + return False + + def _exception_handler( + self, loop: asyncio.AbstractEventLoop, context: dict[str, Any] + ) -> None: + if isinstance(context.get("exception"), Exception): + self._exceptions.append(context["exception"]) + else: + loop.default_exception_handler(context) + + def _raise_async_exceptions(self) -> None: + # Re-raise any exceptions raised in asynchronous callbacks + if self._exceptions: + exceptions, self._exceptions = self._exceptions, [] + if len(exceptions) == 1: + raise exceptions[0] + elif exceptions: + raise BaseExceptionGroup( + "Multiple exceptions occurred in asynchronous callbacks", exceptions + ) + + async def _run_tests_and_fixtures( + self, + receive_stream: MemoryObjectReceiveStream[ + tuple[Awaitable[T_Retval], asyncio.Future[T_Retval]] + ], + ) -> None: + from _pytest.outcomes import OutcomeException + + with receive_stream, self._send_stream: + async for coro, future in receive_stream: + try: + retval = await coro + except CancelledError as exc: + if not future.cancelled(): + future.cancel(*exc.args) + + raise + except BaseException as exc: + if not future.cancelled(): + future.set_exception(exc) + + if not isinstance(exc, (Exception, OutcomeException)): + raise + else: + if not future.cancelled(): + future.set_result(retval) + + async def _call_in_runner_task( + self, + func: Callable[P, Awaitable[T_Retval]], + /, + *args: P.args, + **kwargs: P.kwargs, + ) -> T_Retval: + if not self._runner_task: + self._send_stream, receive_stream = create_memory_object_stream[ + tuple[Awaitable[Any], asyncio.Future] + ](1) + self._runner_task = self.get_loop().create_task( + self._run_tests_and_fixtures(receive_stream) + ) + + coro = func(*args, **kwargs) + future: asyncio.Future[T_Retval] = self.get_loop().create_future() + self._send_stream.send_nowait((coro, future)) + return await future + + def run_asyncgen_fixture( + self, + fixture_func: Callable[..., AsyncGenerator[T_Retval, Any]], + kwargs: dict[str, Any], + ) -> Iterable[T_Retval]: + asyncgen = fixture_func(**kwargs) + fixturevalue: T_Retval = self.get_loop().run_until_complete( + self._call_in_runner_task(asyncgen.asend, None) + ) + self._raise_async_exceptions() + + yield fixturevalue + + try: + self.get_loop().run_until_complete( + self._call_in_runner_task(asyncgen.asend, None) + ) + except StopAsyncIteration: + self._raise_async_exceptions() + else: + self.get_loop().run_until_complete(asyncgen.aclose()) + raise RuntimeError("Async generator fixture did not stop") + + def run_fixture( + self, + fixture_func: Callable[..., Coroutine[Any, Any, T_Retval]], + kwargs: dict[str, Any], + ) -> T_Retval: + retval = self.get_loop().run_until_complete( + self._call_in_runner_task(fixture_func, **kwargs) + ) + self._raise_async_exceptions() + return retval + + def run_test( + self, test_func: Callable[..., Coroutine[Any, Any, Any]], kwargs: dict[str, Any] + ) -> None: + from _pytest.outcomes import OutcomeException + + try: + self.get_loop().run_until_complete( + self._call_in_runner_task(test_func, **kwargs) + ) + except Exception as exc: + self._exceptions.append(exc) + except OutcomeException: + raise + except BaseException: + # A BaseException (e.g. KeyboardInterrupt, SystemExit) interrupted the event loop before + # the test completed. Cancel _runner_task so it does not resume when the event + # loop is re-entered during async generator fixture teardown. + if self._runner_task is not None and not self._runner_task.done(): + self._runner_task.cancel() + self._send_stream.close() + try: + self.get_loop().run_until_complete(self._runner_task) + except CancelledError: + pass + finally: + self._runner_task = None + raise + self._raise_async_exceptions() + + +class _ProcessStreamProtocol(asyncio.subprocess.SubprocessStreamProtocol): + """ + A subprocess protocol that allows us to be notified of ``process_exited`` + + asyncio's own ``Process.wait()`` only resolves once every pipe transport has + disconnected so to get same semantics as on trio and uvloop we need this. + """ + + def __init__(self) -> None: + # Match the standard factory for asyncio.create_process + super().__init__(limit=2**16, loop=asyncio.get_running_loop()) + self.exited = asyncio.Event() + + def process_exited(self) -> None: + super().process_exited() + self.exited.set() + + +class AsyncIOBackend(AsyncBackend): + @classmethod + def run( + cls, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], + args: tuple[Unpack[PosArgsT]], + kwargs: dict[str, Any], + options: dict[str, Any], + ) -> T_Retval: + @wraps(func) + async def wrapper() -> T_Retval: + task = cast(asyncio.Task, current_task()) + task.set_name(get_callable_name(func)) + _task_states[task] = TaskState(None, None) + + try: + return await func(*args) + finally: + del _task_states[task] + + debug = options.get("debug", None) + loop_factory = options.get("loop_factory", None) + if loop_factory is None and options.get("use_uvloop", False): + if sys.platform != "win32": + import uvloop + + loop_factory = uvloop.new_event_loop + else: + import winloop + + loop_factory = winloop.new_event_loop + + with Runner(debug=debug, loop_factory=loop_factory) as runner: + return runner.run(wrapper()) + + @classmethod + def current_token(cls) -> object: + return get_running_loop() + + @classmethod + def current_time(cls) -> float: + return get_running_loop().time() + + @classmethod + def cancelled_exception_class(cls) -> type[BaseException]: + return CancelledError + + @classmethod + async def checkpoint(cls) -> None: + await sleep(0) + + @classmethod + async def checkpoint_if_cancelled(cls) -> None: + task = current_task() + if task is None: + return + + try: + cancel_scope = _task_states[task].cancel_scope + except KeyError: + return + + while cancel_scope: + if cancel_scope.cancel_called: + await sleep(0) + elif cancel_scope.shield: + break + else: + cancel_scope = cancel_scope._parent_scope + + @classmethod + async def cancel_shielded_checkpoint(cls) -> None: + with CancelScope(shield=True): + await sleep(0) + + @classmethod + async def sleep(cls, delay: float) -> None: + await sleep(delay) + + @classmethod + def create_cancel_scope( + cls, *, deadline: float = math.inf, shield: bool = False + ) -> CancelScope: + return CancelScope(deadline=deadline, shield=shield) + + @classmethod + def current_effective_deadline(cls) -> float: + if (task := current_task()) is None: + return math.inf + + try: + cancel_scope = _task_states[task].cancel_scope + except KeyError: + return math.inf + + deadline = math.inf + while cancel_scope: + deadline = min(deadline, cancel_scope.deadline) + if cancel_scope._cancel_called: + deadline = -math.inf + break + elif cancel_scope.shield: + break + else: + cancel_scope = cancel_scope._parent_scope + + return deadline + + @classmethod + def create_task_group(cls) -> abc.TaskGroup: + return TaskGroup() + + @classmethod + def create_event(cls) -> BaseEvent: + return Event() + + @classmethod + def create_lock(cls, *, fast_acquire: bool) -> BaseLock: + return Lock(fast_acquire=fast_acquire) + + @classmethod + def create_semaphore( + cls, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> BaseSemaphore: + return Semaphore(initial_value, max_value=max_value, fast_acquire=fast_acquire) + + @classmethod + def create_capacity_limiter(cls, total_tokens: float) -> BaseCapacityLimiter: + return CapacityLimiter(total_tokens) + + @classmethod + async def run_sync_in_worker_thread( # type: ignore[return] + cls, + func: Callable[[Unpack[PosArgsT]], T_Retval], + args: tuple[Unpack[PosArgsT]], + abandon_on_cancel: bool = False, + limiter: BaseCapacityLimiter | None = None, + ) -> T_Retval: + await cls.checkpoint() + + # If this is the first run in this event loop thread, set up the necessary + # variables + try: + idle_workers = _threadpool_idle_workers.get() + workers = _threadpool_workers.get() + except LookupError: + idle_workers = deque() + workers = set() + _threadpool_idle_workers.set(idle_workers) + _threadpool_workers.set(workers) + + async with limiter or cls.current_default_thread_limiter(): + with CancelScope(shield=not abandon_on_cancel) as scope: + future = asyncio.Future[T_Retval]() + root_task = find_root_task() + if not idle_workers: + worker = WorkerThread(root_task, workers, idle_workers) + worker.start() + workers.add(worker) + root_task.add_done_callback(worker.stop, context=Context()) + else: + worker = idle_workers.pop() + + # Prune any other workers that have been idle for MAX_IDLE_TIME + # seconds or longer + now = cls.current_time() + while idle_workers: + if ( + now - idle_workers[0].idle_since + < WorkerThread.MAX_IDLE_TIME + ): + break + + expired_worker = idle_workers.popleft() + expired_worker.root_task.remove_done_callback( + expired_worker.stop + ) + expired_worker.stop() + + context = copy_context() + context.run(set_current_async_library, None) + if abandon_on_cancel or scope._parent_scope is None: + worker_scope = scope + else: + worker_scope = scope._parent_scope + + worker.queue.put_nowait((context, func, args, future, worker_scope)) + return await future + + @classmethod + def check_cancelled(cls) -> None: + scope: CancelScope | None = threadlocals.current_cancel_scope + while scope is not None: + if scope.cancel_called: + raise CancelledError(f"Cancelled via cancel scope {id(scope):x}") + + if scope.shield: + return + + scope = scope._parent_scope + + @classmethod + def run_async_from_thread( + cls, + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + args: tuple[Unpack[PosArgsT]], + token: object, + ) -> T_co: + async def task_wrapper() -> T_co: + __tracebackhide__ = True + if scope is not None: + task = cast(asyncio.Task, current_task()) + _task_states[task] = TaskState(None, scope) + scope._tasks.add(task) + try: + return await func(*args) + except CancelledError as exc: + raise concurrent.futures.CancelledError(str(exc)) from None + finally: + if scope is not None: + scope._tasks.discard(task) + + loop = cast( + "AbstractEventLoop", token or threadlocals.current_token.native_token + ) + if loop.is_closed(): + raise RunFinishedError + + context = copy_context() + context.run(set_current_async_library, "asyncio") + scope = getattr(threadlocals, "current_cancel_scope", None) + f: concurrent.futures.Future[T_co] = context.run( + asyncio.run_coroutine_threadsafe, task_wrapper(), loop=loop + ) + return f.result() + + @classmethod + def run_sync_from_thread( + cls, + func: Callable[[Unpack[PosArgsT]], T_Retval], + args: tuple[Unpack[PosArgsT]], + token: object, + ) -> T_Retval: + @wraps(func) + def wrapper() -> None: + try: + set_current_async_library("asyncio") + f.set_result(func(*args)) + except BaseException as exc: + f.set_exception(exc) + if not isinstance(exc, Exception): + raise + + loop = cast( + "AbstractEventLoop", token or threadlocals.current_token.native_token + ) + if loop.is_closed(): + raise RunFinishedError + + f: concurrent.futures.Future[T_Retval] = Future() + loop.call_soon_threadsafe(wrapper) + return f.result() + + @classmethod + async def open_process( + cls, + command: StrOrBytesPath | Sequence[StrOrBytesPath], + *, + stdin: int | IO[Any] | None, + stdout: int | IO[Any] | None, + stderr: int | IO[Any] | None, + **kwargs: Any, + ) -> Process: + await cls.checkpoint() + if isinstance(command, PathLike): + command = os.fspath(command) + + # Use loop.subprocess_shell()/subprocess_exec() rather than their + # asyncio.create_subprocess_*() counterparts to get access to + # transport/protocol. + loop = asyncio.get_running_loop() + if isinstance(command, (str, bytes)): + transport, protocol = await loop.subprocess_shell( + _ProcessStreamProtocol, + command, + stdin=stdin, + stdout=stdout, + stderr=stderr, + **kwargs, + ) + else: + transport, protocol = await loop.subprocess_exec( + _ProcessStreamProtocol, + *command, + stdin=stdin, + stdout=stdout, + stderr=stderr, + **kwargs, + ) + + process = asyncio.subprocess.Process(transport, protocol, loop) + stdin_stream = StreamWriterWrapper(process.stdin) if process.stdin else None + stdout_stream = StreamReaderWrapper(process.stdout) if process.stdout else None + stderr_stream = StreamReaderWrapper(process.stderr) if process.stderr else None + return Process( + process, + stdin_stream, + stdout_stream, + stderr_stream, + protocol.exited, + transport, + ) + + @classmethod + def setup_process_pool_exit_at_shutdown(cls, workers: set[abc.Process]) -> None: + create_task( + _shutdown_process_pool_on_exit(workers), + name="AnyIO process pool shutdown task", + ) + find_root_task().add_done_callback( + partial(_forcibly_shutdown_process_pool_on_exit, workers) # type:ignore[arg-type] + ) + + @classmethod + async def connect_tcp( + cls, host: str, port: int, local_address: IPSockAddrType | None = None + ) -> abc.SocketStream: + transport, protocol = cast( + tuple[asyncio.Transport, StreamProtocol], + await get_running_loop().create_connection( + StreamProtocol, host, port, local_addr=local_address + ), + ) + transport.pause_reading() + return SocketStream(transport, protocol) + + @classmethod + async def connect_unix(cls, path: str | bytes) -> abc.UNIXSocketStream: + await cls.checkpoint() + loop = get_running_loop() + raw_socket = socket.socket(socket.AF_UNIX) + raw_socket.setblocking(False) + while True: + try: + raw_socket.connect(path) + except BlockingIOError: + f: asyncio.Future = asyncio.Future() + loop.add_writer(raw_socket, f.set_result, None) + f.add_done_callback(lambda _: loop.remove_writer(raw_socket)) + await f + except BaseException: + raw_socket.close() + raise + else: + return UNIXSocketStream(raw_socket) + + @classmethod + def create_tcp_listener(cls, sock: socket.socket) -> SocketListener: + return TCPSocketListener(sock) + + @classmethod + def create_unix_listener(cls, sock: socket.socket) -> SocketListener: + return UNIXSocketListener(sock) + + @classmethod + async def create_udp_socket( + cls, + family: AddressFamily, + local_address: IPSockAddrType | None, + remote_address: IPSockAddrType | None, + reuse_port: bool, + ) -> UDPSocket | ConnectedUDPSocket: + transport, protocol = await get_running_loop().create_datagram_endpoint( + DatagramProtocol, + local_addr=local_address, + remote_addr=remote_address, + family=family, + reuse_port=reuse_port, + ) + if protocol.exception: + transport.close() + raise protocol.exception + + if not remote_address: + return UDPSocket(transport, protocol) + else: + return ConnectedUDPSocket(transport, protocol) + + @classmethod + async def create_unix_datagram_socket( # type: ignore[override] + cls, raw_socket: socket.socket, remote_path: str | bytes | None + ) -> abc.UNIXDatagramSocket | abc.ConnectedUNIXDatagramSocket: + await cls.checkpoint() + loop = get_running_loop() + + if remote_path: + while True: + try: + raw_socket.connect(remote_path) + except BlockingIOError: + f: asyncio.Future = asyncio.Future() + loop.add_writer(raw_socket, f.set_result, None) + f.add_done_callback(lambda _: loop.remove_writer(raw_socket)) + await f + except BaseException: + raw_socket.close() + raise + else: + return ConnectedUNIXDatagramSocket(raw_socket) + else: + return UNIXDatagramSocket(raw_socket) + + @classmethod + async def getaddrinfo( + cls, + host: bytes | str | None, + port: str | int | None, + *, + family: int | AddressFamily = 0, + type: int | SocketKind = 0, + proto: int = 0, + flags: int = 0, + ) -> Sequence[ + tuple[ + AddressFamily, + SocketKind, + int, + str, + tuple[str, int] | tuple[str, int, int, int] | tuple[int, bytes], + ] + ]: + return await get_running_loop().getaddrinfo( + host, port, family=family, type=type, proto=proto, flags=flags + ) + + @classmethod + async def getnameinfo( + cls, sockaddr: IPSockAddrType, flags: int = 0 + ) -> tuple[str, str]: + return await get_running_loop().getnameinfo(sockaddr, flags) + + @classmethod + async def wait_readable(cls, obj: FileDescriptorLike) -> None: + try: + read_events = _read_events.get() + except LookupError: + read_events = {} + _read_events.set(read_events) + + fd = obj if isinstance(obj, int) else obj.fileno() + if read_events.get(fd): + raise BusyResourceError("reading from") + + loop = get_running_loop() + fut: asyncio.Future[bool] = loop.create_future() + + def cb() -> None: + try: + del read_events[fd] + except KeyError: + pass + else: + remove_reader(fd) + + try: + fut.set_result(True) + except asyncio.InvalidStateError: + pass + + try: + loop.add_reader(fd, cb) + except NotImplementedError: + from anyio._core._asyncio_selector_thread import get_selector + + selector = get_selector() + selector.add_reader(fd, cb) + remove_reader = selector.remove_reader + else: + remove_reader = loop.remove_reader + + read_events[fd] = fut + try: + success = await fut + finally: + try: + del read_events[fd] + except KeyError: + pass + else: + remove_reader(fd) + + if not success: + raise ClosedResourceError + + @classmethod + async def wait_writable(cls, obj: FileDescriptorLike) -> None: + try: + write_events = _write_events.get() + except LookupError: + write_events = {} + _write_events.set(write_events) + + fd = obj if isinstance(obj, int) else obj.fileno() + if write_events.get(fd): + raise BusyResourceError("writing to") + + loop = get_running_loop() + fut: asyncio.Future[bool] = loop.create_future() + + def cb() -> None: + try: + del write_events[fd] + except KeyError: + pass + else: + remove_writer(fd) + + try: + fut.set_result(True) + except asyncio.InvalidStateError: + pass + + try: + loop.add_writer(fd, cb) + except NotImplementedError: + from anyio._core._asyncio_selector_thread import get_selector + + selector = get_selector() + selector.add_writer(fd, cb) + remove_writer = selector.remove_writer + else: + remove_writer = loop.remove_writer + + write_events[fd] = fut + try: + success = await fut + finally: + try: + del write_events[fd] + except KeyError: + pass + else: + remove_writer(fd) + + if not success: + raise ClosedResourceError + + @classmethod + def notify_closing(cls, obj: FileDescriptorLike) -> None: + fd = obj if isinstance(obj, int) else obj.fileno() + loop = get_running_loop() + + try: + write_events = _write_events.get() + except LookupError: + pass + else: + try: + fut = write_events.pop(fd) + except KeyError: + pass + else: + try: + fut.set_result(False) + except asyncio.InvalidStateError: + pass + + try: + loop.remove_writer(fd) + except NotImplementedError: + from anyio._core._asyncio_selector_thread import get_selector + + get_selector().remove_writer(fd) + + try: + read_events = _read_events.get() + except LookupError: + pass + else: + try: + fut = read_events.pop(fd) + except KeyError: + pass + else: + try: + fut.set_result(False) + except asyncio.InvalidStateError: + pass + + try: + loop.remove_reader(fd) + except NotImplementedError: + from anyio._core._asyncio_selector_thread import get_selector + + get_selector().remove_reader(fd) + + @classmethod + async def wrap_listener_socket(cls, sock: socket.socket) -> SocketListener: + if hasattr(socket, "AF_UNIX") and sock.family == socket.AF_UNIX: + return UNIXSocketListener(sock) + + return TCPSocketListener(sock) + + @classmethod + async def wrap_stream_socket(cls, sock: socket.socket) -> SocketStream: + transport, protocol = await get_running_loop().create_connection( + StreamProtocol, sock=sock + ) + return SocketStream(transport, protocol) + + @classmethod + async def wrap_unix_stream_socket(cls, sock: socket.socket) -> UNIXSocketStream: + return UNIXSocketStream(sock) + + @classmethod + async def wrap_udp_socket(cls, sock: socket.socket) -> UDPSocket: + transport, protocol = await get_running_loop().create_datagram_endpoint( + DatagramProtocol, sock=sock + ) + return UDPSocket(transport, protocol) + + @classmethod + async def wrap_connected_udp_socket(cls, sock: socket.socket) -> ConnectedUDPSocket: + transport, protocol = await get_running_loop().create_datagram_endpoint( + DatagramProtocol, sock=sock + ) + return ConnectedUDPSocket(transport, protocol) + + @classmethod + async def wrap_unix_datagram_socket(cls, sock: socket.socket) -> UNIXDatagramSocket: + return UNIXDatagramSocket(sock) + + @classmethod + async def wrap_connected_unix_datagram_socket( + cls, sock: socket.socket + ) -> ConnectedUNIXDatagramSocket: + return ConnectedUNIXDatagramSocket(sock) + + @classmethod + def current_default_thread_limiter(cls) -> CapacityLimiter: + try: + return _default_thread_limiter.get() + except LookupError: + limiter = CapacityLimiter(40) + _default_thread_limiter.set(limiter) + return limiter + + @classmethod + def open_signal_receiver( + cls, *signals: Signals + ) -> AbstractContextManager[AsyncIterator[Signals]]: + return _SignalReceiver(signals) + + @classmethod + def get_current_task(cls) -> TaskInfo: + return AsyncIOTaskInfo(current_task()) # type: ignore[arg-type] + + @classmethod + def get_running_tasks(cls) -> Sequence[TaskInfo]: + return [AsyncIOTaskInfo(task) for task in all_tasks() if not task.done()] + + @classmethod + async def wait_all_tasks_blocked(cls) -> None: + await cls.checkpoint() + this_task = current_task() + while True: + for task in all_tasks(): + if task is this_task: + continue + + waiter = task._fut_waiter # type: ignore[attr-defined] + if waiter is None or waiter.done(): + await sleep(0.1) + break + else: + return + + @classmethod + def create_test_runner(cls, options: dict[str, Any]) -> TestRunner: + return TestRunner(**options) + + +backend_class = AsyncIOBackend diff --git a/source/target-vendors/cp312/anyio/_backends/_trio.py b/source/target-vendors/cp312/anyio/_backends/_trio.py new file mode 100644 index 0000000..2e67786 --- /dev/null +++ b/source/target-vendors/cp312/anyio/_backends/_trio.py @@ -0,0 +1,1473 @@ +from __future__ import annotations + +import array +import math +import os +import socket +import sys +import types +import weakref +from collections.abc import ( + AsyncGenerator, + AsyncIterator, + Awaitable, + Callable, + Collection, + Coroutine, + Iterable, + Sequence, +) +from contextlib import AbstractContextManager +from contextvars import Context +from dataclasses import dataclass +from functools import partial, wraps +from io import IOBase +from os import PathLike +from signal import Signals +from socket import AddressFamily, SocketKind +from types import TracebackType +from typing import ( + IO, + TYPE_CHECKING, + Any, + Generic, + Literal, + NoReturn, + ParamSpec, + TypeVar, + cast, + overload, +) + +import trio.from_thread +import trio.lowlevel +from outcome import Error, Outcome, Value +from trio.lowlevel import ( + current_root_task, + current_task, + notify_closing, + wait_readable, + wait_writable, +) +from trio.socket import SocketType as TrioSocketType +from trio.to_thread import run_sync + +from .. import ( + CapacityLimiterStatistics, + EventStatistics, + LockStatistics, + RunFinishedError, + TaskInfo, + WouldBlock, + abc, +) +from .._core._eventloop import claim_worker_thread +from .._core._exceptions import ( + BrokenResourceError, + BusyResourceError, + ClosedResourceError, + EndOfStream, +) +from .._core._sockets import convert_ipv6_sockaddr +from .._core._streams import create_memory_object_stream +from .._core._synchronization import ( + CapacityLimiter as BaseCapacityLimiter, +) +from .._core._synchronization import Event as BaseEvent +from .._core._synchronization import Lock as BaseLock +from .._core._synchronization import ( + ResourceGuard, + SemaphoreStatistics, +) +from .._core._synchronization import Semaphore as BaseSemaphore +from .._core._tasks import CancelScope as BaseCancelScope +from .._core._tasks import TaskHandle +from ..abc import IPSockAddrType, UDPPacketType, UNIXDatagramPacketType +from ..abc._eventloop import AsyncBackend, StrOrBytesPath +from ..abc._tasks import T_contra, call_for_coroutine, get_callable_name, get_coro_name + +if TYPE_CHECKING: + from _typeshed import FileDescriptorLike + + from ..streams.memory import MemoryObjectSendStream + +if sys.version_info < (3, 15): + from typing_extensions import sentinel + +if sys.version_info >= (3, 11): + from typing import Self, TypeVarTuple, Unpack +else: + from exceptiongroup import BaseExceptionGroup + from typing_extensions import Self, TypeVarTuple, Unpack + +T = TypeVar("T") +T_Retval = TypeVar("T_Retval") +T_co = TypeVar("T_co", covariant=True) +T_SockAddr = TypeVar("T_SockAddr", str, IPSockAddrType) +PosArgsT = TypeVarTuple("PosArgsT") +P = ParamSpec("P") + + +def ensure_returns_coro( + func: Callable[P, Awaitable[T_Retval]], +) -> Callable[P, Coroutine[Any, Any, T_Retval]]: + @wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> Coroutine[Any, Any, T_Retval]: + awaitable = func(*args, **kwargs) + # Check the common case first. + if isinstance(awaitable, Coroutine): + return awaitable + elif not isinstance(awaitable, Awaitable): + # The user violated the type annotations. Still, we should pass this on to + # Trio so it can raise with an appropriate message. + return awaitable + else: + + @wraps(func) + async def inner_wrapper() -> T_Retval: + return await awaitable + + return inner_wrapper() + + return wrapper + + +# +# Event loop +# + +RunVar = trio.lowlevel.RunVar + + +# +# Timeouts and cancellation +# + + +class CancelScope(BaseCancelScope): + __slots__ = ("__original",) + + def __new__( + cls, original: trio.CancelScope | None = None, **kwargs: object + ) -> Self: + return object.__new__(cls) + + def __init__(self, original: trio.CancelScope | None = None, **kwargs: Any) -> None: + self.__original = original or trio.CancelScope(**kwargs) + + def __enter__(self) -> Self: + self.__original.__enter__() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + return self.__original.__exit__(exc_type, exc_val, exc_tb) + + def cancel(self, reason: str | None = None) -> None: + self.__original.cancel(reason) + + @property + def deadline(self) -> float: + return self.__original.deadline + + @deadline.setter + def deadline(self, value: float) -> None: + self.__original.deadline = value + + @property + def cancel_called(self) -> bool: + return self.__original.cancel_called + + @property + def cancelled_caught(self) -> bool: + return self.__original.cancelled_caught + + @property + def shield(self) -> bool: + return self.__original.shield + + @shield.setter + def shield(self, value: bool) -> None: + self.__original.shield = value + + +# +# Task groups +# + +empty_start_value = sentinel("empty_start_value") + + +class _TrioTaskStatus(abc.TaskStatus[T_contra], Generic[T_contra]): + early_start_value: T_contra | object = empty_start_value + real_task_status: trio.TaskStatus[T_contra | None] | None = None + + def started(self, value: T_contra | None = None) -> None: + if self.real_task_status is None: + if self.early_start_value is not empty_start_value: + raise RuntimeError("called 'started' twice on the same task status") + + self.early_start_value = value + else: + self.real_task_status.started(value) + + +class TaskGroup(abc.TaskGroup): + def __init__(self) -> None: + self._entered = False + self._active = False + self._nursery_manager = trio.open_nursery(strict_exception_groups=True) + self.cancel_scope = None # type: ignore[assignment] + + async def __aenter__(self) -> Self: + if self._entered: + raise RuntimeError("TaskGroup cannot be entered more than once") + + self._entered = True + self._active = True + self._nursery = await self._nursery_manager.__aenter__() + self.cancel_scope = CancelScope(self._nursery.cancel_scope) + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + try: + # trio.Nursery.__exit__ returns bool; .open_nursery has wrong type + return await self._nursery_manager.__aexit__(exc_type, exc_val, exc_tb) # type: ignore[return-value] + except BaseExceptionGroup as exc: + if not exc.split(trio.Cancelled)[1]: + raise trio.Cancelled._create() from exc + + raise + finally: + del exc_val, exc_tb + self._active = False + + def _check_active(self, coro: Coroutine | None = None) -> None: + if not self._active: + if coro is not None: + coro.close() + + raise RuntimeError( + "This task group is not active; no new tasks can be started." + ) + + def create_task( + self, + coro: Coroutine[Any, Any, T_co], + *, + name: object = None, + context: Context | None = None, + ) -> TaskHandle[T_co]: + if not isinstance(coro, Coroutine): + raise TypeError(f"expected a coroutine, got {coro.__class__.__qualname__}") + + self._check_active(coro) + final_name = get_coro_name(coro, name) + handle = TaskHandle(coro, final_name) + if context is not None: + context.run( + partial(self._nursery.start_soon, handle._run_coro, name=handle.name) + ) + else: + self._nursery.start_soon(handle._run_coro, name=handle.name) + + return handle + + async def start( + self, + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + *args: Unpack[PosArgsT], + name: object = None, + return_handle: Literal[False, True] = False, + ) -> Any: + handle: TaskHandle[T_co] + + async def run_coro_with_task_status( + *, task_status: trio.TaskStatus[Any] + ) -> None: + nonlocal handle + wrapper_task_status = _TrioTaskStatus() + coro = call_for_coroutine(func, args, task_status=wrapper_task_status) + if wrapper_task_status.early_start_value is not empty_start_value: + task_status.started(wrapper_task_status.early_start_value) + else: + wrapper_task_status.real_task_status = task_status + + handle = TaskHandle(coro, final_name) + await handle._run_coro() + + self._check_active() + final_name = get_callable_name(func, name) + start_value = await self._nursery.start( + run_coro_with_task_status, name=final_name + ) + if return_handle: + handle._start_value = start_value + return handle + else: + return start_value + + +# +# Subprocesses +# + + +@dataclass(eq=False) +class ReceiveStreamWrapper(abc.ByteReceiveStream): + _stream: trio.abc.ReceiveStream + + async def receive(self, max_bytes: int | None = None) -> bytes: + if max_bytes is not None and max_bytes < 1: + raise ValueError("max_bytes must be a positive integer") + + try: + data = await self._stream.receive_some(max_bytes) + except trio.ClosedResourceError as exc: + raise ClosedResourceError from exc.__cause__ + except trio.BrokenResourceError as exc: + raise BrokenResourceError from exc.__cause__ + + if data: + return bytes(data) + else: + raise EndOfStream + + async def aclose(self) -> None: + await self._stream.aclose() + + +@dataclass(eq=False) +class SendStreamWrapper(abc.ByteSendStream): + _stream: trio.abc.SendStream + + async def send(self, item: bytes) -> None: + try: + await self._stream.send_all(item) + except trio.ClosedResourceError as exc: + raise ClosedResourceError from exc.__cause__ + except trio.BrokenResourceError as exc: + raise BrokenResourceError from exc.__cause__ + + async def aclose(self) -> None: + await self._stream.aclose() + + +@dataclass(eq=False) +class Process(abc.Process): + _process: trio.Process + _stdin: abc.ByteSendStream | None + _stdout: abc.ByteReceiveStream | None + _stderr: abc.ByteReceiveStream | None + + async def aclose(self) -> None: + with CancelScope(shield=True): + if self._stdin: + await self._stdin.aclose() + if self._stdout: + await self._stdout.aclose() + if self._stderr: + await self._stderr.aclose() + + try: + await self.wait() + except BaseException: + self.kill() + with CancelScope(shield=True): + await self.wait() + raise + + async def wait(self) -> int: + return await self._process.wait() + + def terminate(self) -> None: + self._process.terminate() + + def kill(self) -> None: + self._process.kill() + + def send_signal(self, signal: Signals) -> None: + self._process.send_signal(signal) + + @property + def pid(self) -> int: + return self._process.pid + + @property + def returncode(self) -> int | None: + return self._process.returncode + + @property + def stdin(self) -> abc.ByteSendStream | None: + return self._stdin + + @property + def stdout(self) -> abc.ByteReceiveStream | None: + return self._stdout + + @property + def stderr(self) -> abc.ByteReceiveStream | None: + return self._stderr + + +class _ProcessPoolShutdownInstrument(trio.abc.Instrument): + def after_run(self) -> None: + super().after_run() + + +current_default_worker_process_limiter: trio.lowlevel.RunVar = RunVar( + "current_default_worker_process_limiter" +) + + +async def _shutdown_process_pool(workers: set[abc.Process]) -> None: + try: + await trio.sleep(math.inf) + except trio.Cancelled: + for process in workers: + if process.returncode is None: + process.kill() + + with CancelScope(shield=True): + for process in workers: + await process.aclose() + + +# +# Sockets and networking +# + + +class _TrioSocketMixin(Generic[T_SockAddr]): + def __init__(self, trio_socket: TrioSocketType) -> None: + self._trio_socket = trio_socket + self._closed = False + + def _check_closed(self) -> None: + if self._closed: + raise ClosedResourceError + if self._trio_socket.fileno() < 0: + raise BrokenResourceError + + @property + def _raw_socket(self) -> socket.socket: + return self._trio_socket._sock # type: ignore[attr-defined] + + async def aclose(self) -> None: + if self._trio_socket.fileno() >= 0: + self._closed = True + self._trio_socket.close() + + def _convert_socket_error(self, exc: BaseException) -> NoReturn: + if isinstance(exc, trio.ClosedResourceError): + raise ClosedResourceError from exc + elif self._trio_socket.fileno() < 0 and self._closed: + raise ClosedResourceError from None + elif isinstance(exc, OSError): + raise BrokenResourceError from exc + else: + raise exc + + +class SocketStream(_TrioSocketMixin, abc.SocketStream): + def __init__(self, trio_socket: TrioSocketType) -> None: + super().__init__(trio_socket) + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + + async def receive(self, max_bytes: int = 65536) -> bytes: + if max_bytes < 1: + raise ValueError("max_bytes must be a positive integer") + + with self._receive_guard: + try: + data = await self._trio_socket.recv(max_bytes) + except BaseException as exc: + self._convert_socket_error(exc) + + if data: + return data + else: + raise EndOfStream + + async def send(self, item: bytes) -> None: + with self._send_guard: + view = memoryview(item) + while view: + try: + bytes_sent = await self._trio_socket.send(view) + except BaseException as exc: + self._convert_socket_error(exc) + + view = view[bytes_sent:] + + async def send_eof(self) -> None: + self._trio_socket.shutdown(socket.SHUT_WR) + + +class UNIXSocketStream(SocketStream, abc.UNIXSocketStream): + async def receive_fds(self, msglen: int, maxfds: int) -> tuple[bytes, list[int]]: + if not isinstance(msglen, int) or msglen < 0: + raise ValueError("msglen must be a non-negative integer") + if not isinstance(maxfds, int) or maxfds < 1: + raise ValueError("maxfds must be a positive integer") + + fds = array.array("i") + await trio.lowlevel.checkpoint() + with self._receive_guard: + while True: + try: + message, ancdata, _flags, _addr = await self._trio_socket.recvmsg( + msglen, socket.CMSG_LEN(maxfds * fds.itemsize) + ) + except BaseException as exc: + self._convert_socket_error(exc) + else: + if not message and not ancdata: + raise EndOfStream + + break + + for cmsg_level, cmsg_type, cmsg_data in ancdata: + if cmsg_level != socket.SOL_SOCKET or cmsg_type != socket.SCM_RIGHTS: + raise RuntimeError( + f"Received unexpected ancillary data; message = {message!r}, " + f"cmsg_level = {cmsg_level}, cmsg_type = {cmsg_type}" + ) + + fds.frombytes(cmsg_data[: len(cmsg_data) - (len(cmsg_data) % fds.itemsize)]) + + return message, list(fds) + + async def send_fds(self, message: bytes, fds: Collection[int | IOBase]) -> None: + if not message: + raise ValueError("message must not be empty") + if not fds: + raise ValueError("fds must not be empty") + + filenos: list[int] = [] + for fd in fds: + if isinstance(fd, int): + filenos.append(fd) + elif isinstance(fd, IOBase): + filenos.append(fd.fileno()) + + fdarray = array.array("i", filenos) + await trio.lowlevel.checkpoint() + with self._send_guard: + while True: + try: + await self._trio_socket.sendmsg( + [message], + [ + ( + socket.SOL_SOCKET, + socket.SCM_RIGHTS, + fdarray, + ) + ], + ) + break + except BaseException as exc: + self._convert_socket_error(exc) + + +class TCPSocketListener(_TrioSocketMixin, abc.SocketListener): + def __init__(self, raw_socket: socket.socket): + super().__init__(trio.socket.from_stdlib_socket(raw_socket)) + self._accept_guard = ResourceGuard("accepting connections from") + + async def accept(self) -> SocketStream: + with self._accept_guard: + try: + trio_socket, _addr = await self._trio_socket.accept() + except BaseException as exc: + self._convert_socket_error(exc) + + trio_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + return SocketStream(trio_socket) + + +class UNIXSocketListener(_TrioSocketMixin, abc.SocketListener): + def __init__(self, raw_socket: socket.socket): + super().__init__(trio.socket.from_stdlib_socket(raw_socket)) + self._accept_guard = ResourceGuard("accepting connections from") + + async def accept(self) -> UNIXSocketStream: + with self._accept_guard: + try: + trio_socket, _addr = await self._trio_socket.accept() + except BaseException as exc: + self._convert_socket_error(exc) + + return UNIXSocketStream(trio_socket) + + +class UDPSocket(_TrioSocketMixin[IPSockAddrType], abc.UDPSocket): + def __init__(self, trio_socket: TrioSocketType) -> None: + super().__init__(trio_socket) + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + + async def receive(self) -> tuple[bytes, IPSockAddrType]: + with self._receive_guard: + try: + data, addr = await self._trio_socket.recvfrom(65536) + return data, convert_ipv6_sockaddr(addr) + except BaseException as exc: + self._convert_socket_error(exc) + + async def send(self, item: UDPPacketType) -> None: + with self._send_guard: + try: + await self._trio_socket.sendto(*item) + except BaseException as exc: + self._convert_socket_error(exc) + + +class ConnectedUDPSocket(_TrioSocketMixin[IPSockAddrType], abc.ConnectedUDPSocket): + def __init__(self, trio_socket: TrioSocketType) -> None: + super().__init__(trio_socket) + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + + async def receive(self) -> bytes: + with self._receive_guard: + try: + return await self._trio_socket.recv(65536) + except BaseException as exc: + self._convert_socket_error(exc) + + async def send(self, item: bytes) -> None: + with self._send_guard: + try: + await self._trio_socket.send(item) + except BaseException as exc: + self._convert_socket_error(exc) + + +class UNIXDatagramSocket(_TrioSocketMixin[str], abc.UNIXDatagramSocket): + def __init__(self, trio_socket: TrioSocketType) -> None: + super().__init__(trio_socket) + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + + async def receive(self) -> UNIXDatagramPacketType: + with self._receive_guard: + try: + data, addr = await self._trio_socket.recvfrom(65536) + return data, addr + except BaseException as exc: + self._convert_socket_error(exc) + + async def send(self, item: UNIXDatagramPacketType) -> None: + with self._send_guard: + try: + await self._trio_socket.sendto(*item) + except BaseException as exc: + self._convert_socket_error(exc) + + +class ConnectedUNIXDatagramSocket( + _TrioSocketMixin[str], abc.ConnectedUNIXDatagramSocket +): + def __init__(self, trio_socket: TrioSocketType) -> None: + super().__init__(trio_socket) + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + + async def receive(self) -> bytes: + with self._receive_guard: + try: + return await self._trio_socket.recv(65536) + except BaseException as exc: + self._convert_socket_error(exc) + + async def send(self, item: bytes) -> None: + with self._send_guard: + try: + await self._trio_socket.send(item) + except BaseException as exc: + self._convert_socket_error(exc) + + +# +# Synchronization +# + + +class Event(BaseEvent): + __slots__ = ("__original",) + + def __new__(cls) -> Self: + return object.__new__(cls) + + def __init__(self) -> None: + self.__original = trio.Event() + + def is_set(self) -> bool: + return self.__original.is_set() + + async def wait(self) -> None: + return await self.__original.wait() + + def statistics(self) -> EventStatistics: + orig_statistics = self.__original.statistics() + return EventStatistics(tasks_waiting=orig_statistics.tasks_waiting) + + def set(self) -> None: + self.__original.set() + + +class Lock(BaseLock): + __slots__ = "__original", "_fast_acquire" + + def __new__(cls, *, fast_acquire: bool = False) -> Self: + return object.__new__(cls) + + def __init__(self, *, fast_acquire: bool = False) -> None: + self._fast_acquire = fast_acquire + self.__original = trio.Lock() + + @staticmethod + def _convert_runtime_error_msg(exc: RuntimeError) -> None: + if exc.args == ("attempt to re-acquire an already held Lock",): + exc.args = ("Attempted to acquire an already held Lock",) + + async def acquire(self) -> None: + if not self._fast_acquire: + try: + await self.__original.acquire() + except RuntimeError as exc: + self._convert_runtime_error_msg(exc) + raise + + return + + # This is the "fast path" where we don't let other tasks run + await trio.lowlevel.checkpoint_if_cancelled() + try: + self.__original.acquire_nowait() + except trio.WouldBlock: + await self.__original._lot.park() + except RuntimeError as exc: + self._convert_runtime_error_msg(exc) + raise + + def acquire_nowait(self) -> None: + try: + self.__original.acquire_nowait() + except trio.WouldBlock: + raise WouldBlock from None + except RuntimeError as exc: + self._convert_runtime_error_msg(exc) + raise + + def locked(self) -> bool: + return self.__original.locked() + + def release(self) -> None: + self.__original.release() + + def statistics(self) -> LockStatistics: + orig_statistics = self.__original.statistics() + owner = TrioTaskInfo(orig_statistics.owner) if orig_statistics.owner else None + return LockStatistics( + orig_statistics.locked, owner, orig_statistics.tasks_waiting + ) + + +class Semaphore(BaseSemaphore): + __slots__ = ("__original",) + + def __new__( + cls, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> Self: + return object.__new__(cls) + + def __init__( + self, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> None: + super().__init__(initial_value, max_value=max_value, fast_acquire=fast_acquire) + self.__original = trio.Semaphore(initial_value, max_value=max_value) + + async def acquire(self) -> None: + if not self._fast_acquire: + await self.__original.acquire() + return + + # This is the "fast path" where we don't let other tasks run + await trio.lowlevel.checkpoint_if_cancelled() + try: + self.__original.acquire_nowait() + except trio.WouldBlock: + await self.__original._lot.park() + + def acquire_nowait(self) -> None: + try: + self.__original.acquire_nowait() + except trio.WouldBlock: + raise WouldBlock from None + + @property + def max_value(self) -> int | None: + return self.__original.max_value + + @property + def value(self) -> int: + return self.__original.value + + def release(self) -> None: + self.__original.release() + + def statistics(self) -> SemaphoreStatistics: + orig_statistics = self.__original.statistics() + return SemaphoreStatistics(orig_statistics.tasks_waiting) + + +class CapacityLimiter(BaseCapacityLimiter): + __slots__ = ("__original",) + + def __new__( + cls, + total_tokens: float | None = None, + *, + original: trio.CapacityLimiter | None = None, + ) -> Self: + return object.__new__(cls) + + def __init__( + self, + total_tokens: float | None = None, + *, + original: trio.CapacityLimiter | None = None, + ) -> None: + if original is not None: + self.__original = original + else: + assert total_tokens is not None + self.__original = trio.CapacityLimiter(total_tokens) + + async def __aenter__(self) -> None: + return await self.__original.__aenter__() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.__original.__aexit__(exc_type, exc_val, exc_tb) + + @property + def total_tokens(self) -> float: + return self.__original.total_tokens + + @total_tokens.setter + def total_tokens(self, value: float) -> None: + self.__original.total_tokens = value + + @property + def borrowed_tokens(self) -> int: + return self.__original.borrowed_tokens + + @property + def available_tokens(self) -> float: + return self.__original.available_tokens + + def acquire_nowait(self) -> None: + try: + self.__original.acquire_nowait() + except trio.WouldBlock: + raise WouldBlock from None + + def acquire_on_behalf_of_nowait(self, borrower: object) -> None: + try: + self.__original.acquire_on_behalf_of_nowait(borrower) + except trio.WouldBlock: + raise WouldBlock from None + + async def acquire(self) -> None: + await self.__original.acquire() + + async def acquire_on_behalf_of(self, borrower: object) -> None: + await self.__original.acquire_on_behalf_of(borrower) + + def release(self) -> None: + return self.__original.release() + + def release_on_behalf_of(self, borrower: object) -> None: + return self.__original.release_on_behalf_of(borrower) + + def statistics(self) -> CapacityLimiterStatistics: + orig = self.__original.statistics() + return CapacityLimiterStatistics( + borrowed_tokens=orig.borrowed_tokens, + total_tokens=orig.total_tokens, + borrowers=tuple(orig.borrowers), + tasks_waiting=orig.tasks_waiting, + ) + + +_capacity_limiter_wrapper: trio.lowlevel.RunVar = RunVar("_capacity_limiter_wrapper") + + +# +# Signal handling +# + + +class _SignalReceiver: + _iterator: AsyncIterator[int] + + def __init__(self, signals: tuple[Signals, ...]): + self._signals = signals + + def __enter__(self) -> Self: + self._cm = trio.open_signal_receiver(*self._signals) + self._iterator = self._cm.__enter__() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + return self._cm.__exit__(exc_type, exc_val, exc_tb) + + def __aiter__(self) -> _SignalReceiver: + return self + + async def __anext__(self) -> Signals: + signum = await self._iterator.__anext__() + return Signals(signum) + + +# +# Testing and debugging +# + + +class TestRunner(abc.TestRunner): + def __init__(self, **options: Any) -> None: + from queue import Queue + + self._call_queue: Queue[Callable[[], object]] = Queue() + self._send_stream: ( + MemoryObjectSendStream[tuple[Awaitable[Any], list[Outcome]]] | None + ) = None + self._options = options + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: types.TracebackType | None, + ) -> None: + if self._send_stream: + self._send_stream.close() + while self._send_stream is not None: + self._call_queue.get()() + + def is_running(self) -> bool: + return trio.lowlevel.in_trio_task() + + async def _run_tests_and_fixtures(self) -> None: + self._send_stream, receive_stream = create_memory_object_stream[ + tuple[Awaitable[Any], list[Outcome]] + ](1) + with receive_stream: + async for awaitable, outcome_holder in receive_stream: + try: + retval = await awaitable + except BaseException as exc: + outcome_holder.append(Error(exc)) + else: + outcome_holder.append(Value(retval)) + + def _main_task_finished(self, outcome: object) -> None: + self._send_stream = None + + def _call_in_runner_task( + self, + func: Callable[P, Awaitable[T_Retval]], + /, + *args: P.args, + **kwargs: P.kwargs, + ) -> T_Retval: + if self._send_stream is None: + trio.lowlevel.start_guest_run( + self._run_tests_and_fixtures, + run_sync_soon_threadsafe=self._call_queue.put, + done_callback=self._main_task_finished, + **self._options, + ) + while self._send_stream is None: + self._call_queue.get()() + + outcome_holder: list[Outcome] = [] + self._send_stream.send_nowait((func(*args, **kwargs), outcome_holder)) + while not outcome_holder: + self._call_queue.get()() + + return outcome_holder[0].unwrap() + + def run_asyncgen_fixture( + self, + fixture_func: Callable[..., AsyncGenerator[T_Retval, Any]], + kwargs: dict[str, Any], + ) -> Iterable[T_Retval]: + asyncgen = fixture_func(**kwargs) + fixturevalue: T_Retval = self._call_in_runner_task(asyncgen.asend, None) + + yield fixturevalue + + try: + self._call_in_runner_task(asyncgen.asend, None) + except StopAsyncIteration: + pass + else: + self._call_in_runner_task(asyncgen.aclose) + raise RuntimeError("Async generator fixture did not stop") + + def run_fixture( + self, + fixture_func: Callable[..., Coroutine[Any, Any, T_Retval]], + kwargs: dict[str, Any], + ) -> T_Retval: + return self._call_in_runner_task(fixture_func, **kwargs) + + def run_test( + self, test_func: Callable[..., Coroutine[Any, Any, Any]], kwargs: dict[str, Any] + ) -> None: + self._call_in_runner_task(test_func, **kwargs) + + +class TrioTaskInfo(TaskInfo): + def __init__(self, task: trio.lowlevel.Task): + parent_id = None + if task.parent_nursery and task.parent_nursery.parent_task: + parent_id = id(task.parent_nursery.parent_task) + + super().__init__(id(task), parent_id, task.name, task.coro) + self._task = weakref.proxy(task) + + def has_pending_cancellation(self) -> bool: + try: + return self._task._cancel_status.effectively_cancelled + except ReferenceError: + # If the task is no longer around, it surely doesn't have a cancellation + # pending + return False + + +class TrioBackend(AsyncBackend): + @classmethod + def run( + cls, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], + args: tuple[Unpack[PosArgsT]], + kwargs: dict[str, Any], + options: dict[str, Any], + ) -> T_Retval: + assert not kwargs, "unreachable, and not supported by Trio" + return trio.run(ensure_returns_coro(func), *args, **options) + + @classmethod + def current_token(cls) -> object: + return trio.lowlevel.current_trio_token() + + @classmethod + def current_time(cls) -> float: + return trio.current_time() + + @classmethod + def cancelled_exception_class(cls) -> type[BaseException]: + return trio.Cancelled + + @classmethod + async def checkpoint(cls) -> None: + await trio.lowlevel.checkpoint() + + @classmethod + async def checkpoint_if_cancelled(cls) -> None: + await trio.lowlevel.checkpoint_if_cancelled() + + @classmethod + async def cancel_shielded_checkpoint(cls) -> None: + await trio.lowlevel.cancel_shielded_checkpoint() + + @classmethod + async def sleep(cls, delay: float) -> None: + await trio.sleep(delay) + + @classmethod + def create_cancel_scope( + cls, *, deadline: float = math.inf, shield: bool = False + ) -> BaseCancelScope: + return CancelScope(deadline=deadline, shield=shield) + + @classmethod + def current_effective_deadline(cls) -> float: + return trio.current_effective_deadline() + + @classmethod + def create_task_group(cls) -> abc.TaskGroup: + return TaskGroup() + + @classmethod + def create_event(cls) -> BaseEvent: + return Event() + + @classmethod + def create_lock(cls, *, fast_acquire: bool) -> Lock: + return Lock(fast_acquire=fast_acquire) + + @classmethod + def create_semaphore( + cls, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> BaseSemaphore: + return Semaphore(initial_value, max_value=max_value, fast_acquire=fast_acquire) + + @classmethod + def create_capacity_limiter(cls, total_tokens: float) -> CapacityLimiter: + return CapacityLimiter(total_tokens) + + @classmethod + async def run_sync_in_worker_thread( + cls, + func: Callable[[Unpack[PosArgsT]], T_Retval], + args: tuple[Unpack[PosArgsT]], + abandon_on_cancel: bool = False, + limiter: BaseCapacityLimiter | None = None, + ) -> T_Retval: + def wrapper() -> T_Retval: + with claim_worker_thread(TrioBackend, token): + return func(*args) + + token = TrioBackend.current_token() + return await run_sync( + wrapper, + abandon_on_cancel=abandon_on_cancel, + limiter=cast(trio.CapacityLimiter, limiter), + ) + + @classmethod + def check_cancelled(cls) -> None: + trio.from_thread.check_cancelled() + + @classmethod + def run_async_from_thread( + cls, + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + args: tuple[Unpack[PosArgsT]], + token: object, + ) -> T_co: + trio_token = cast("trio.lowlevel.TrioToken | None", token) + try: + return trio.from_thread.run(func, *args, trio_token=trio_token) + except trio.RunFinishedError: + raise RunFinishedError from None + + @classmethod + def run_sync_from_thread( + cls, + func: Callable[[Unpack[PosArgsT]], T_Retval], + args: tuple[Unpack[PosArgsT]], + token: object, + ) -> T_Retval: + trio_token = cast("trio.lowlevel.TrioToken | None", token) + try: + return trio.from_thread.run_sync(func, *args, trio_token=trio_token) + except trio.RunFinishedError: + raise RunFinishedError from None + + @classmethod + async def open_process( + cls, + command: StrOrBytesPath | Sequence[StrOrBytesPath], + *, + stdin: int | IO[Any] | None, + stdout: int | IO[Any] | None, + stderr: int | IO[Any] | None, + **kwargs: Any, + ) -> Process: + def convert_item(item: StrOrBytesPath) -> str: + str_or_bytes = os.fspath(item) + if isinstance(str_or_bytes, str): + return str_or_bytes + else: + return os.fsdecode(str_or_bytes) + + if isinstance(command, (str, bytes, PathLike)): + process = await trio.lowlevel.open_process( + convert_item(command), + stdin=stdin, + stdout=stdout, + stderr=stderr, + shell=True, + **kwargs, + ) + else: + process = await trio.lowlevel.open_process( + [convert_item(item) for item in command], + stdin=stdin, + stdout=stdout, + stderr=stderr, + shell=False, + **kwargs, + ) + + stdin_stream = SendStreamWrapper(process.stdin) if process.stdin else None + stdout_stream = ReceiveStreamWrapper(process.stdout) if process.stdout else None + stderr_stream = ReceiveStreamWrapper(process.stderr) if process.stderr else None + return Process(process, stdin_stream, stdout_stream, stderr_stream) + + @classmethod + def setup_process_pool_exit_at_shutdown(cls, workers: set[abc.Process]) -> None: + trio.lowlevel.spawn_system_task(_shutdown_process_pool, workers) + + @classmethod + async def connect_tcp( + cls, host: str, port: int, local_address: IPSockAddrType | None = None + ) -> SocketStream: + family = socket.AF_INET6 if ":" in host else socket.AF_INET + trio_socket = trio.socket.socket(family) + trio_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + if local_address: + await trio_socket.bind(local_address) + + try: + await trio_socket.connect((host, port)) + except BaseException: + trio_socket.close() + raise + + return SocketStream(trio_socket) + + @classmethod + async def connect_unix(cls, path: str | bytes) -> abc.UNIXSocketStream: + trio_socket = trio.socket.socket(socket.AF_UNIX) + try: + await trio_socket.connect(path) + except BaseException: + trio_socket.close() + raise + + return UNIXSocketStream(trio_socket) + + @classmethod + def create_tcp_listener(cls, sock: socket.socket) -> abc.SocketListener: + return TCPSocketListener(sock) + + @classmethod + def create_unix_listener(cls, sock: socket.socket) -> abc.SocketListener: + return UNIXSocketListener(sock) + + @classmethod + async def create_udp_socket( + cls, + family: socket.AddressFamily, + local_address: IPSockAddrType | None, + remote_address: IPSockAddrType | None, + reuse_port: bool, + ) -> UDPSocket | ConnectedUDPSocket: + trio_socket = trio.socket.socket(family=family, type=socket.SOCK_DGRAM) + + if reuse_port: + trio_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + + if local_address: + await trio_socket.bind(local_address) + + if remote_address: + await trio_socket.connect(remote_address) + return ConnectedUDPSocket(trio_socket) + else: + return UDPSocket(trio_socket) + + @classmethod + @overload + async def create_unix_datagram_socket( + cls, raw_socket: socket.socket, remote_path: None + ) -> abc.UNIXDatagramSocket: ... + + @classmethod + @overload + async def create_unix_datagram_socket( + cls, raw_socket: socket.socket, remote_path: str | bytes + ) -> abc.ConnectedUNIXDatagramSocket: ... + + @classmethod + async def create_unix_datagram_socket( + cls, raw_socket: socket.socket, remote_path: str | bytes | None + ) -> abc.UNIXDatagramSocket | abc.ConnectedUNIXDatagramSocket: + trio_socket = trio.socket.from_stdlib_socket(raw_socket) + + if remote_path: + await trio_socket.connect(remote_path) + return ConnectedUNIXDatagramSocket(trio_socket) + else: + return UNIXDatagramSocket(trio_socket) + + @classmethod + async def getaddrinfo( + cls, + host: bytes | str | None, + port: str | int | None, + *, + family: int | AddressFamily = 0, + type: int | SocketKind = 0, + proto: int = 0, + flags: int = 0, + ) -> Sequence[ + tuple[ + AddressFamily, + SocketKind, + int, + str, + tuple[str, int] | tuple[str, int, int, int] | tuple[int, bytes], + ] + ]: + return await trio.socket.getaddrinfo(host, port, family, type, proto, flags) + + @classmethod + async def getnameinfo( + cls, sockaddr: IPSockAddrType, flags: int = 0 + ) -> tuple[str, str]: + return await trio.socket.getnameinfo(sockaddr, flags) + + @classmethod + async def wait_readable(cls, obj: FileDescriptorLike) -> None: + try: + await wait_readable(obj) + except trio.ClosedResourceError as exc: + raise ClosedResourceError().with_traceback(exc.__traceback__) from None + except trio.BusyResourceError: + raise BusyResourceError("reading from") from None + + @classmethod + async def wait_writable(cls, obj: FileDescriptorLike) -> None: + try: + await wait_writable(obj) + except trio.ClosedResourceError as exc: + raise ClosedResourceError().with_traceback(exc.__traceback__) from None + except trio.BusyResourceError: + raise BusyResourceError("writing to") from None + + @classmethod + def notify_closing(cls, obj: FileDescriptorLike) -> None: + notify_closing(obj) + + @classmethod + async def wrap_listener_socket(cls, sock: socket.socket) -> abc.SocketListener: + if hasattr(socket, "AF_UNIX") and sock.family == socket.AF_UNIX: + return UNIXSocketListener(sock) + + return TCPSocketListener(sock) + + @classmethod + async def wrap_stream_socket(cls, sock: socket.socket) -> SocketStream: + trio_sock = trio.socket.from_stdlib_socket(sock) + return SocketStream(trio_sock) + + @classmethod + async def wrap_unix_stream_socket(cls, sock: socket.socket) -> UNIXSocketStream: + trio_sock = trio.socket.from_stdlib_socket(sock) + return UNIXSocketStream(trio_sock) + + @classmethod + async def wrap_udp_socket(cls, sock: socket.socket) -> UDPSocket: + trio_sock = trio.socket.from_stdlib_socket(sock) + return UDPSocket(trio_sock) + + @classmethod + async def wrap_connected_udp_socket(cls, sock: socket.socket) -> ConnectedUDPSocket: + trio_sock = trio.socket.from_stdlib_socket(sock) + return ConnectedUDPSocket(trio_sock) + + @classmethod + async def wrap_unix_datagram_socket(cls, sock: socket.socket) -> UNIXDatagramSocket: + trio_sock = trio.socket.from_stdlib_socket(sock) + return UNIXDatagramSocket(trio_sock) + + @classmethod + async def wrap_connected_unix_datagram_socket( + cls, sock: socket.socket + ) -> ConnectedUNIXDatagramSocket: + trio_sock = trio.socket.from_stdlib_socket(sock) + return ConnectedUNIXDatagramSocket(trio_sock) + + @classmethod + def current_default_thread_limiter(cls) -> CapacityLimiter: + try: + return _capacity_limiter_wrapper.get() + except LookupError: + limiter = CapacityLimiter( + original=trio.to_thread.current_default_thread_limiter() + ) + _capacity_limiter_wrapper.set(limiter) + return limiter + + @classmethod + def open_signal_receiver( + cls, *signals: Signals + ) -> AbstractContextManager[AsyncIterator[Signals]]: + return _SignalReceiver(signals) + + @classmethod + def get_current_task(cls) -> TaskInfo: + task = current_task() + return TrioTaskInfo(task) + + @classmethod + def get_running_tasks(cls) -> Sequence[TaskInfo]: + root_task = current_root_task() + assert root_task + task_infos = [TrioTaskInfo(root_task)] + nurseries = root_task.child_nurseries + while nurseries: + new_nurseries: list[trio.Nursery] = [] + for nursery in nurseries: + for task in nursery.child_tasks: + task_infos.append(TrioTaskInfo(task)) + new_nurseries.extend(task.child_nurseries) + + nurseries = new_nurseries + + return task_infos + + @classmethod + async def wait_all_tasks_blocked(cls) -> None: + from trio.testing import wait_all_tasks_blocked + + await wait_all_tasks_blocked() + + @classmethod + def create_test_runner(cls, options: dict[str, Any]) -> TestRunner: + return TestRunner(**options) + + +backend_class = TrioBackend diff --git a/source/target-vendors/cp312/anyio/_core/__init__.py b/source/target-vendors/cp312/anyio/_core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/anyio/_core/_asyncio_selector_thread.py b/source/target-vendors/cp312/anyio/_core/_asyncio_selector_thread.py new file mode 100644 index 0000000..9f35bae --- /dev/null +++ b/source/target-vendors/cp312/anyio/_core/_asyncio_selector_thread.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +import asyncio +import socket +import threading +from collections.abc import Callable +from selectors import EVENT_READ, EVENT_WRITE, DefaultSelector +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from _typeshed import FileDescriptorLike + +_selector_lock = threading.Lock() +_selector: Selector | None = None + + +class Selector: + def __init__(self) -> None: + self._thread = threading.Thread(target=self.run, name="AnyIO socket selector") + self._selector = DefaultSelector() + self._send, self._receive = socket.socketpair() + self._send.setblocking(False) + self._receive.setblocking(False) + # This somewhat reduces the amount of memory wasted queueing up data + # for wakeups. With these settings, maximum number of 1-byte sends + # before getting BlockingIOError: + # Linux 4.8: 6 + # macOS (darwin 15.5): 1 + # Windows 10: 525347 + # Windows you're weird. (And on Windows setting SNDBUF to 0 makes send + # blocking, even on non-blocking sockets, so don't do that.) + self._receive.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1) + self._send.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1) + # On Windows this is a TCP socket so this might matter. On other + # platforms this fails b/c AF_UNIX sockets aren't actually TCP. + try: + self._send.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + except OSError: + pass + + self._selector.register(self._receive, EVENT_READ) + self._closed = False + + def start(self) -> None: + self._thread.start() + threading._register_atexit(self._stop) # type: ignore[attr-defined] + + def _stop(self) -> None: + global _selector + self._closed = True + self._notify_self() + self._send.close() + self._thread.join() + self._selector.unregister(self._receive) + self._receive.close() + self._selector.close() + _selector = None + assert not self._selector.get_map(), ( + "selector still has registered file descriptors after shutdown" + ) + + def _notify_self(self) -> None: + try: + self._send.send(b"\x00") + except BlockingIOError: + pass + + def add_reader(self, fd: FileDescriptorLike, callback: Callable[[], Any]) -> None: + loop = asyncio.get_running_loop() + try: + key = self._selector.get_key(fd) + except KeyError: + self._selector.register(fd, EVENT_READ, {EVENT_READ: (loop, callback)}) + else: + if EVENT_READ in key.data: + raise ValueError( + "this file descriptor is already registered for reading" + ) + + key.data[EVENT_READ] = loop, callback + self._selector.modify(fd, key.events | EVENT_READ, key.data) + + self._notify_self() + + def add_writer(self, fd: FileDescriptorLike, callback: Callable[[], Any]) -> None: + loop = asyncio.get_running_loop() + try: + key = self._selector.get_key(fd) + except KeyError: + self._selector.register(fd, EVENT_WRITE, {EVENT_WRITE: (loop, callback)}) + else: + if EVENT_WRITE in key.data: + raise ValueError( + "this file descriptor is already registered for writing" + ) + + key.data[EVENT_WRITE] = loop, callback + self._selector.modify(fd, key.events | EVENT_WRITE, key.data) + + self._notify_self() + + def remove_reader(self, fd: FileDescriptorLike) -> bool: + try: + key = self._selector.get_key(fd) + except KeyError: + return False + + if new_events := key.events ^ EVENT_READ: + del key.data[EVENT_READ] + self._selector.modify(fd, new_events, key.data) + else: + self._selector.unregister(fd) + + return True + + def remove_writer(self, fd: FileDescriptorLike) -> bool: + try: + key = self._selector.get_key(fd) + except KeyError: + return False + + if new_events := key.events ^ EVENT_WRITE: + del key.data[EVENT_WRITE] + self._selector.modify(fd, new_events, key.data) + else: + self._selector.unregister(fd) + + return True + + def run(self) -> None: + while not self._closed: + for key, events in self._selector.select(): + if key.fileobj is self._receive: + try: + while self._receive.recv(4096): + pass + except BlockingIOError: + pass + + continue + + if events & EVENT_READ: + loop, callback = key.data[EVENT_READ] + self.remove_reader(key.fd) + try: + loop.call_soon_threadsafe(callback) + except RuntimeError: + pass # the loop was already closed + + if events & EVENT_WRITE: + loop, callback = key.data[EVENT_WRITE] + self.remove_writer(key.fd) + try: + loop.call_soon_threadsafe(callback) + except RuntimeError: + pass # the loop was already closed + + +def get_selector() -> Selector: + global _selector + + with _selector_lock: + if _selector is None: + _selector = Selector() + _selector.start() + + return _selector diff --git a/source/target-vendors/cp312/anyio/_core/_concurrency_utils.py b/source/target-vendors/cp312/anyio/_core/_concurrency_utils.py new file mode 100644 index 0000000..f66229a --- /dev/null +++ b/source/target-vendors/cp312/anyio/_core/_concurrency_utils.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +from inspect import iscoroutine + +__all__ = ("amap", "as_completed", "gather") + +from collections.abc import AsyncGenerator, Awaitable, Callable, Coroutine, Iterable +from contextlib import asynccontextmanager +from typing import TYPE_CHECKING, Any, TypeVar, overload + +from ..abc._tasks import get_coro_name +from ._exceptions import BrokenResourceError +from ._streams import create_memory_object_stream +from ._tasks import TaskHandle, create_task_group + +if TYPE_CHECKING: + from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream + +R = TypeVar("R") +S = TypeVar("S") +T = TypeVar("T") +U = TypeVar("U") +V = TypeVar("V") +W = TypeVar("W") + + +@overload +async def gather( + coro1: Coroutine[Any, Any, R], coro2: Coroutine[Any, Any, S], / +) -> tuple[R, S]: ... + + +@overload +async def gather( + coro1: Coroutine[Any, Any, R], + coro2: Coroutine[Any, Any, S], + coro3: Coroutine[Any, Any, T], + /, +) -> tuple[R, S, T]: ... + + +@overload +async def gather( + coro1: Coroutine[Any, Any, R], + coro2: Coroutine[Any, Any, S], + coro3: Coroutine[Any, Any, T], + coro4: Coroutine[Any, Any, U], + /, +) -> tuple[R, S, T, U]: ... + + +@overload +async def gather( + coro1: Coroutine[Any, Any, R], + coro2: Coroutine[Any, Any, S], + coro3: Coroutine[Any, Any, T], + coro4: Coroutine[Any, Any, U], + coro5: Coroutine[Any, Any, V], + /, +) -> tuple[R, S, T, U, V]: ... + + +@overload +async def gather( + coro1: Coroutine[Any, Any, R], + coro2: Coroutine[Any, Any, S], + coro3: Coroutine[Any, Any, T], + coro4: Coroutine[Any, Any, U], + coro5: Coroutine[Any, Any, V], + coro6: Coroutine[Any, Any, W], + /, +) -> tuple[R, S, T, U, V, W]: ... + + +# handle arbitrary length if awaitables are all of the same type +@overload +async def gather(*coros: Coroutine[Any, Any, R]) -> tuple[R, ...]: ... + + +async def gather(*coros: Coroutine[Any, Any, Any]) -> tuple[Any, ...]: + """ + Run coroutines concurrently in a task group. The order of result values corresponds + to the order of coroutines passed. + + :param coros: coroutine objects to run as tasks + :return: task results for each argument in the same order as they were passed + + """ + handles: list[TaskHandle[Any]] = [] + async with create_task_group() as tg: + handles.extend(tg.create_task(coro) for coro in coros) + + return tuple(h.return_value for h in handles) + + +@asynccontextmanager +async def as_completed( + *awaitables: Awaitable[R], +) -> AsyncGenerator[MemoryObjectReceiveStream[TaskHandle[R]]]: + """ + Run awaitable objects concurrently in a task group, returning an iterator which can + be used to get finished task handles in the order they complete. + + :param awaitables: awaitable objects to run as tasks + :return: MemoryObjectReceiveStream for iterating over task handles as tasks complete. + + """ + if not awaitables: + raise ValueError("as_completed() takes at least one awaitable") + + send, recv = create_memory_object_stream[TaskHandle[R]](len(awaitables)) + task_handles: list[TaskHandle[R]] = [] + + async def runner( + awaitable: Awaitable[R], + index: int, + _send: MemoryObjectSendStream[TaskHandle[R]], + ) -> R: + async with _send: + try: + return await awaitable + finally: + try: + _send.send_nowait(task_handles[index]) + except BrokenResourceError: + pass + + async with recv, create_task_group() as tg: + async with send: + for i, awaitable in enumerate(awaitables): + name = get_coro_name(awaitable) if iscoroutine(awaitable) else None + task_handles.append( + tg.start_soon(runner, awaitable, i, send.clone(), name=name) + ) + try: + yield recv + finally: + tg.cancel_scope.cancel() + + +async def amap( + func: Callable[[T], Coroutine[Any, Any, R]], args: Iterable[T] +) -> list[R]: + """ + Run the given coroutine function concurrently for multiple argument values. + + :param func: a coroutine function that takes a single argument + :param args: a sequence of argument values to pass to ``func`` + :return: task results for each argument in the same order as they were passed + + """ + handles: list[TaskHandle[R]] = [] + async with create_task_group() as tg: + handles.extend(tg.start_soon(func, arg) for arg in args) + + return [h.return_value for h in handles] diff --git a/source/target-vendors/cp312/anyio/_core/_contextmanagers.py b/source/target-vendors/cp312/anyio/_core/_contextmanagers.py new file mode 100644 index 0000000..302f32b --- /dev/null +++ b/source/target-vendors/cp312/anyio/_core/_contextmanagers.py @@ -0,0 +1,200 @@ +from __future__ import annotations + +from abc import abstractmethod +from contextlib import AbstractAsyncContextManager, AbstractContextManager +from inspect import isasyncgen, iscoroutine, isgenerator +from types import TracebackType +from typing import Protocol, TypeVar, cast, final + +_T_co = TypeVar("_T_co", covariant=True) +_ExitT_co = TypeVar("_ExitT_co", covariant=True, bound="bool | None") + + +class _SupportsCtxMgr(Protocol[_T_co, _ExitT_co]): + def __contextmanager__(self) -> AbstractContextManager[_T_co, _ExitT_co]: ... + + +class _SupportsAsyncCtxMgr(Protocol[_T_co, _ExitT_co]): + def __asynccontextmanager__( + self, + ) -> AbstractAsyncContextManager[_T_co, _ExitT_co]: ... + + +class ContextManagerMixin: + """ + Mixin class providing context manager functionality via a generator-based + implementation. + + This class allows you to implement a context manager via :meth:`__contextmanager__` + which should return a generator. The mechanics are meant to mirror those of + :func:`@contextmanager `. + + .. note:: Classes using this mix-in are not reentrant as context managers, meaning + that once you enter it, you can't re-enter before first exiting it. + + .. seealso:: :doc:`contextmanagers` + """ + + __cm: AbstractContextManager[object, bool | None] | None = None + + @final + def __enter__(self: _SupportsCtxMgr[_T_co, bool | None]) -> _T_co: + # Needed for mypy to assume self still has the __cm member + assert isinstance(self, ContextManagerMixin) + if self.__cm is not None: + raise RuntimeError( + f"this {self.__class__.__qualname__} has already been entered" + ) + + cm = self.__contextmanager__() + if not isinstance(cm, AbstractContextManager): + if isgenerator(cm): + raise TypeError( + "__contextmanager__() returned a generator object instead of " + "a context manager. Did you forget to add the @contextmanager " + "decorator?" + ) + + raise TypeError( + f"__contextmanager__() did not return a context manager object, " + f"but {cm.__class__!r}" + ) + + if cm is self: + raise TypeError( + f"{self.__class__.__qualname__}.__contextmanager__() returned " + f"self. Did you forget to add the @contextmanager decorator and a " + f"'yield' statement?" + ) + + value = cm.__enter__() + self.__cm = cm + return value + + @final + def __exit__( + self: _SupportsCtxMgr[object, _ExitT_co], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> _ExitT_co: + # Needed for mypy to assume self still has the __cm member + assert isinstance(self, ContextManagerMixin) + if self.__cm is None: + raise RuntimeError( + f"this {self.__class__.__qualname__} has not been entered yet" + ) + + # Prevent circular references + cm = self.__cm + del self.__cm + + return cast(_ExitT_co, cm.__exit__(exc_type, exc_val, exc_tb)) + + @abstractmethod + def __contextmanager__(self) -> AbstractContextManager[object, bool | None]: + """ + Implement your context manager logic here. + + This method **must** be decorated with + :func:`@contextmanager `. + + .. note:: Remember that the ``yield`` will raise any exception raised in the + enclosed context block, so use a ``finally:`` block to clean up resources! + + :return: a context manager object + """ + + +class AsyncContextManagerMixin: + """ + Mixin class providing async context manager functionality via a generator-based + implementation. + + This class allows you to implement a context manager via + :meth:`__asynccontextmanager__`. The mechanics are meant to mirror those of + :func:`@asynccontextmanager `. + + .. note:: Classes using this mix-in are not reentrant as context managers, meaning + that once you enter it, you can't re-enter before first exiting it. + + .. seealso:: :doc:`contextmanagers` + """ + + __cm: AbstractAsyncContextManager[object, bool | None] | None = None + + @final + async def __aenter__(self: _SupportsAsyncCtxMgr[_T_co, bool | None]) -> _T_co: + # Needed for mypy to assume self still has the __cm member + assert isinstance(self, AsyncContextManagerMixin) + if self.__cm is not None: + raise RuntimeError( + f"this {self.__class__.__qualname__} has already been entered" + ) + + cm = self.__asynccontextmanager__() + if not isinstance(cm, AbstractAsyncContextManager): + if isasyncgen(cm): + raise TypeError( + "__asynccontextmanager__() returned an async generator instead of " + "an async context manager. Did you forget to add the " + "@asynccontextmanager decorator?" + ) + elif iscoroutine(cm): + cm.close() + raise TypeError( + "__asynccontextmanager__() returned a coroutine object instead of " + "an async context manager. Did you forget to add the " + "@asynccontextmanager decorator and a 'yield' statement?" + ) + + raise TypeError( + f"__asynccontextmanager__() did not return an async context manager, " + f"but {cm.__class__!r}" + ) + + if cm is self: + raise TypeError( + f"{self.__class__.__qualname__}.__asynccontextmanager__() returned " + f"self. Did you forget to add the @asynccontextmanager decorator and a " + f"'yield' statement?" + ) + + value = await cm.__aenter__() + self.__cm = cm + return value + + @final + async def __aexit__( + self: _SupportsAsyncCtxMgr[object, _ExitT_co], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> _ExitT_co: + assert isinstance(self, AsyncContextManagerMixin) + if self.__cm is None: + raise RuntimeError( + f"this {self.__class__.__qualname__} has not been entered yet" + ) + + # Prevent circular references + cm = self.__cm + del self.__cm + + return cast(_ExitT_co, await cm.__aexit__(exc_type, exc_val, exc_tb)) + + @abstractmethod + def __asynccontextmanager__( + self, + ) -> AbstractAsyncContextManager[object, bool | None]: + """ + Implement your async context manager logic here. + + This method **must** be decorated with + :func:`@asynccontextmanager `. + + .. note:: Remember that the ``yield`` will raise any exception raised in the + enclosed context block, so use a ``finally:`` block to clean up resources! + + :return: an async context manager object + """ diff --git a/source/target-vendors/cp312/anyio/_core/_eventloop.py b/source/target-vendors/cp312/anyio/_core/_eventloop.py new file mode 100644 index 0000000..a3e2ab1 --- /dev/null +++ b/source/target-vendors/cp312/anyio/_core/_eventloop.py @@ -0,0 +1,240 @@ +from __future__ import annotations + +import math +import sys +import threading +from collections.abc import Awaitable, Callable, Generator +from contextlib import contextmanager +from contextvars import Token +from importlib import import_module +from typing import TYPE_CHECKING, Any, TypeVar + +from ._exceptions import NoEventLoopError + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +sniffio: Any +try: + import sniffio +except ModuleNotFoundError: + sniffio = None + +if TYPE_CHECKING: + from ..abc import AsyncBackend + +# This must be updated when new backends are introduced +BACKENDS = "asyncio", "trio" + +T_Retval = TypeVar("T_Retval") +PosArgsT = TypeVarTuple("PosArgsT") + +threadlocals = threading.local() +loaded_backends: dict[str, type[AsyncBackend]] = {} + + +def run( + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], + *args: Unpack[PosArgsT], + backend: str = "asyncio", + backend_options: dict[str, Any] | None = None, +) -> T_Retval: + """ + Run the given coroutine function in an asynchronous event loop. + + The current thread must not be already running an event loop. + + :param func: a coroutine function + :param args: positional arguments to ``func`` + :param backend: name of the asynchronous event loop implementation – currently + either ``asyncio`` or ``trio`` + :param backend_options: keyword arguments to call the backend ``run()`` + implementation with (documented :ref:`here `) + :return: the return value of the coroutine function + :raises RuntimeError: if an asynchronous event loop is already running in this + thread + :raises LookupError: if the named backend is not found + + """ + if asynclib_name := current_async_library(): + raise RuntimeError(f"Already running {asynclib_name} in this thread") + + try: + async_backend = get_async_backend(backend) + except ImportError as exc: + if backend in BACKENDS: + raise LookupError( + f"Backend {backend!r} is not available. " + f"Install it with: pip install anyio[{backend}]" + ) from exc + + raise LookupError(f"No such backend: {backend}") from exc + + token = None + if asynclib_name is None: + # Since we're in control of the event loop, we can cache the name of the async + # library + token = set_current_async_library(backend) + + try: + backend_options = backend_options or {} + return async_backend.run(func, args, {}, backend_options) + finally: + reset_current_async_library(token) + + +async def sleep(delay: float) -> None: + """ + Pause the current task for the specified duration. + + :param delay: the duration, in seconds + + """ + return await get_async_backend().sleep(delay) + + +async def sleep_forever() -> None: + """ + Pause the current task until it's cancelled. + + This is a shortcut for ``sleep(math.inf)``. + + .. versionadded:: 3.1 + + """ + await sleep(math.inf) + + +async def sleep_until(deadline: float) -> None: + """ + Pause the current task until the given time. + + :param deadline: the absolute time to wake up at (according to the internal + monotonic clock of the event loop) + + .. versionadded:: 3.1 + + """ + now = current_time() + await sleep(max(deadline - now, 0)) + + +def current_time() -> float: + """ + Return the current value of the event loop's internal clock. + + :return: the clock value (seconds) + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().current_time() + + +def get_all_backends() -> tuple[str, ...]: + """Return a tuple of the names of all built-in backends.""" + return BACKENDS + + +def get_available_backends() -> tuple[str, ...]: + """ + Test for the availability of built-in backends. + + :return a tuple of the built-in backend names that were successfully imported + + .. versionadded:: 4.12 + + """ + available_backends: list[str] = [] + for backend_name in get_all_backends(): + try: + get_async_backend(backend_name) + except ImportError: + continue + + available_backends.append(backend_name) + + return tuple(available_backends) + + +def get_cancelled_exc_class() -> type[BaseException]: + """ + Return the current async library's cancellation exception class. + + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().cancelled_exception_class() + + +# +# Private API +# + + +@contextmanager +def claim_worker_thread( + backend_class: type[AsyncBackend], token: object +) -> Generator[Any, None, None]: + from ..lowlevel import EventLoopToken + + threadlocals.current_token = EventLoopToken(backend_class, token) + try: + yield + finally: + del threadlocals.current_token + + +def get_async_backend(asynclib_name: str | None = None) -> type[AsyncBackend]: + if asynclib_name is None: + asynclib_name = current_async_library() + if not asynclib_name: + raise NoEventLoopError( + f"Not currently running on any asynchronous event loop. " + f"Available async backends: {', '.join(get_all_backends())}" + ) + + # We use our own dict instead of sys.modules to get the already imported back-end + # class because the appropriate modules in sys.modules could potentially be only + # partially initialized + try: + return loaded_backends[asynclib_name] + except KeyError: + module = import_module(f"anyio._backends._{asynclib_name}") + loaded_backends[asynclib_name] = module.backend_class + return module.backend_class + + +def current_async_library() -> str | None: + if sniffio is None: + # If sniffio is not installed, we assume we're either running asyncio or nothing + import asyncio + + try: + asyncio.get_running_loop() + return "asyncio" + except RuntimeError: + pass + else: + try: + return sniffio.current_async_library() + except sniffio.AsyncLibraryNotFoundError: + pass + + return None + + +def set_current_async_library(asynclib_name: str | None) -> Token | None: + # no-op if sniffio is not installed + if sniffio is None: + return None + + return sniffio.current_async_library_cvar.set(asynclib_name) + + +def reset_current_async_library(token: Token | None) -> None: + if token is not None: + sniffio.current_async_library_cvar.reset(token) diff --git a/source/target-vendors/cp312/anyio/_core/_exceptions.py b/source/target-vendors/cp312/anyio/_core/_exceptions.py new file mode 100644 index 0000000..7d9165f --- /dev/null +++ b/source/target-vendors/cp312/anyio/_core/_exceptions.py @@ -0,0 +1,205 @@ +from __future__ import annotations + +import sys +from collections.abc import Generator +from textwrap import dedent +from typing import Any + +if sys.version_info < (3, 11): + from exceptiongroup import BaseExceptionGroup + + +class BrokenResourceError(Exception): + """ + Raised when trying to use a resource that has been rendered unusable due to external + causes (e.g. a send stream whose peer has disconnected). + """ + + +class BrokenWorkerProcess(Exception): + """ + Raised by :meth:`~anyio.to_process.run_sync` if the worker process terminates abruptly or + otherwise misbehaves. + """ + + +class BrokenWorkerInterpreter(Exception): + """ + Raised by :meth:`~anyio.to_interpreter.run_sync` if an unexpected exception is + raised in the subinterpreter. + """ + + def __init__(self, excinfo: Any): + # This was adapted from concurrent.futures.interpreter.ExecutionFailed + msg = excinfo.formatted + if not msg: + if excinfo.type and excinfo.msg: + msg = f"{excinfo.type.__name__}: {excinfo.msg}" + else: + msg = excinfo.type.__name__ or excinfo.msg + + super().__init__(msg) + self.excinfo = excinfo + + def __str__(self) -> str: + try: + formatted = self.excinfo.errdisplay + except Exception: + return super().__str__() + else: + return dedent( + f""" + {super().__str__()} + + Uncaught in the interpreter: + + {formatted} + """.strip() + ) + + +class BusyResourceError(Exception): + """ + Raised when two tasks are trying to read from or write to the same resource + concurrently. + """ + + def __init__(self, action: str): + super().__init__(f"Another task is already {action} this resource") + + +class ClosedResourceError(Exception): + """Raised when trying to use a resource that has been closed.""" + + +class ConnectionFailed(OSError): + """ + Raised when a connection attempt fails. + + .. note:: This class inherits from :exc:`OSError` for backwards compatibility. + """ + + +def iterate_exceptions( + exception: BaseException, +) -> Generator[BaseException, None, None]: + if isinstance(exception, BaseExceptionGroup): + for exc in exception.exceptions: + yield from iterate_exceptions(exc) + else: + yield exception + + +class DelimiterNotFound(Exception): + """ + Raised during + :meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_until` if the + maximum number of bytes has been read without the delimiter being found. + """ + + def __init__(self, max_bytes: int) -> None: + super().__init__( + f"The delimiter was not found among the first {max_bytes} bytes" + ) + + +class EndOfStream(Exception): + """ + Raised when trying to read from a stream that has been closed from the other end. + """ + + +class IncompleteRead(Exception): + """ + Raised during + :meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_exactly` or + :meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_until` if the + connection is closed before the requested amount of bytes has been read. + """ + + def __init__(self) -> None: + super().__init__( + "The stream was closed before the read operation could be completed" + ) + + +class TypedAttributeLookupError(LookupError): + """ + Raised by :meth:`~anyio.TypedAttributeProvider.extra` when the given typed attribute + is not found and no default value has been given. + """ + + +class WouldBlock(Exception): + """Raised by ``X_nowait`` functions if ``X()`` would block.""" + + +class NoEventLoopError(RuntimeError): + """ + Raised by several functions that require an event loop to be running in the current + thread when there is no running event loop. + + This is also raised by :func:`.from_thread.run` and :func:`.from_thread.run_sync` + if not calling from an AnyIO worker thread, and no ``token`` was passed. + """ + + +class RunFinishedError(RuntimeError): + """ + Raised by :func:`.from_thread.run` and :func:`.from_thread.run_sync` if the event + loop associated with the explicitly passed token has already finished. + """ + + def __init__(self) -> None: + super().__init__( + "The event loop associated with the given token has already finished" + ) + + +class TaskFailed(Exception): + """ + Raised when awaiting on, or attempting to access the return value of, a + :class:`.TaskHandle` that raised an exception. + """ + + +class TaskCancelled(TaskFailed): + """ + Raised when awaiting on, or attempting to access the return value of, a + :class:`.TaskHandle` that was cancelled. + """ + + +class TaskNotFinished(Exception): + """ + Raised when attempting to access the return value or exception of a + :class:`.TaskHandle` that is still pending completion. + """ + + +class FutureFailed(Exception): + """ + Raised when awaiting on, or attempting to access the return value of, a + :class:`.Future` that raised an exception. + """ + + +class FutureCancelled(FutureFailed): + """ + Raised when attempting to access the return value or exception of a + :class:`.Future` that was cancelled. + """ + + +class FutureNotFinished(Exception): + """ + Raised when attempting to access the return value or exception of a + :class:`.Future` that is still pending completion. + """ + + +class FutureAlreadyFinished(Exception): + """ + Raised when attempting set a result of or await a + :class:`.Future` that has already completed. + """ diff --git a/source/target-vendors/cp312/anyio/_core/_fileio.py b/source/target-vendors/cp312/anyio/_core/_fileio.py new file mode 100644 index 0000000..498eef4 --- /dev/null +++ b/source/target-vendors/cp312/anyio/_core/_fileio.py @@ -0,0 +1,1045 @@ +from __future__ import annotations + +import os +import pathlib +import sys +from collections.abc import ( + AsyncIterator, + Callable, + Iterable, + Iterator, + Sequence, +) +from dataclasses import dataclass +from functools import partial +from os import PathLike +from typing import ( + IO, + TYPE_CHECKING, + Any, + AnyStr, + ClassVar, + Final, + Generic, + TypeVar, + overload, +) + +from .. import to_thread +from ..abc import AsyncResource +from ._synchronization import CapacityLimiter + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +if sys.version_info >= (3, 14): + from pathlib.types import PathInfo + +if TYPE_CHECKING: + from types import ModuleType + + from _typeshed import OpenBinaryMode, OpenTextMode, ReadableBuffer, WriteableBuffer +else: + ReadableBuffer = OpenBinaryMode = OpenTextMode = WriteableBuffer = object + + +T = TypeVar("T", bound="Path") + + +class AsyncFile(AsyncResource, Generic[AnyStr]): + """ + An asynchronous file object. + + This class wraps a standard file object and provides async friendly versions of the + following blocking methods (where available on the original file object): + + * read + * read1 + * readline + * readlines + * readinto + * readinto1 + * write + * writelines + * truncate + * seek + * tell + * flush + + All other methods are directly passed through. + + This class supports the asynchronous context manager protocol which closes the + underlying file at the end of the context block. + + This class also supports asynchronous iteration:: + + async with await open_file(...) as f: + async for line in f: + print(line) + """ + + def __init__( + self, fp: IO[AnyStr], *, limiter: CapacityLimiter | None = None + ) -> None: + if limiter is not None and not isinstance(limiter, CapacityLimiter): + raise TypeError( + f"limiter must be a CapacityLimiter or None, not " + f"{limiter.__class__.__name__}" + ) + + self._fp: Any = fp + self._limiter = limiter + + def __getattr__(self, name: str) -> object: + return getattr(self._fp, name) + + @property + def limiter(self) -> CapacityLimiter | None: + """The capacity limiter used by this file object, if not the global limiter.""" + return self._limiter + + @property + def wrapped(self) -> IO[AnyStr]: + """The wrapped file object.""" + return self._fp + + async def __aiter__(self) -> AsyncIterator[AnyStr]: + while True: + line = await self.readline() + if line: + yield line + else: + break + + async def aclose(self) -> None: + return await to_thread.run_sync(self._fp.close, limiter=self._limiter) + + async def read(self, size: int = -1) -> AnyStr: + return await to_thread.run_sync(self._fp.read, size, limiter=self._limiter) + + async def read1(self: AsyncFile[bytes], size: int = -1) -> bytes: + return await to_thread.run_sync(self._fp.read1, size, limiter=self._limiter) + + async def readline(self) -> AnyStr: + return await to_thread.run_sync(self._fp.readline, limiter=self._limiter) + + async def readlines(self) -> list[AnyStr]: + return await to_thread.run_sync(self._fp.readlines, limiter=self._limiter) + + async def readinto(self: AsyncFile[bytes], b: WriteableBuffer) -> int: + return await to_thread.run_sync(self._fp.readinto, b, limiter=self._limiter) + + async def readinto1(self: AsyncFile[bytes], b: WriteableBuffer) -> int: + return await to_thread.run_sync(self._fp.readinto1, b, limiter=self._limiter) + + @overload + async def write(self: AsyncFile[bytes], b: ReadableBuffer) -> int: ... + + @overload + async def write(self: AsyncFile[str], b: str) -> int: ... + + async def write(self, b: ReadableBuffer | str) -> int: + return await to_thread.run_sync(self._fp.write, b, limiter=self._limiter) + + @overload + async def writelines( + self: AsyncFile[bytes], lines: Iterable[ReadableBuffer] + ) -> None: ... + + @overload + async def writelines(self: AsyncFile[str], lines: Iterable[str]) -> None: ... + + async def writelines(self, lines: Iterable[ReadableBuffer] | Iterable[str]) -> None: + return await to_thread.run_sync( + self._fp.writelines, lines, limiter=self._limiter + ) + + async def truncate(self, size: int | None = None) -> int: + return await to_thread.run_sync(self._fp.truncate, size, limiter=self._limiter) + + async def seek(self, offset: int, whence: int | None = os.SEEK_SET) -> int: + return await to_thread.run_sync( + self._fp.seek, offset, whence, limiter=self._limiter + ) + + async def tell(self) -> int: + return await to_thread.run_sync(self._fp.tell, limiter=self._limiter) + + async def flush(self) -> None: + return await to_thread.run_sync(self._fp.flush, limiter=self._limiter) + + +@overload +async def open_file( + file: str | PathLike[str] | int, + mode: OpenBinaryMode, + buffering: int = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + closefd: bool = ..., + opener: Callable[[str, int], int] | None = ..., + *, + limiter: CapacityLimiter | None = ..., +) -> AsyncFile[bytes]: ... + + +@overload +async def open_file( + file: str | PathLike[str] | int, + mode: OpenTextMode = ..., + buffering: int = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + closefd: bool = ..., + opener: Callable[[str, int], int] | None = ..., + *, + limiter: CapacityLimiter | None = ..., +) -> AsyncFile[str]: ... + + +async def open_file( + file: str | PathLike[str] | int, + mode: str = "r", + buffering: int = -1, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, + closefd: bool = True, + opener: Callable[[str, int], int] | None = None, + *, + limiter: CapacityLimiter | None = None, +) -> AsyncFile[Any]: + """ + Open a file asynchronously. + + Except for ``limiter``, the arguments are exactly the same as for the builtin :func:`open`. + + :param limiter: an optional capacity limiter to use with the file + instead of the default one + :return: an asynchronous file object + + .. versionchanged:: 4.14.0 + Added the ``limiter`` keyword argument. + + """ + fp = await to_thread.run_sync( + open, + file, + mode, + buffering, + encoding, + errors, + newline, + closefd, + opener, + limiter=limiter, + ) + return AsyncFile(fp, limiter=limiter) + + +def wrap_file( + file: IO[AnyStr], *, limiter: CapacityLimiter | None = None +) -> AsyncFile[AnyStr]: + """ + Wrap an existing file as an asynchronous file. + + :param file: an existing file-like object + :param limiter: an optional capacity limiter to use with the file + instead of the default one + :return: an asynchronous file object + + .. versionchanged:: 4.14.0 + Added the ``limiter`` keyword argument. + + """ + return AsyncFile(file, limiter=limiter) + + +@dataclass(eq=False) +class _PathIterator(AsyncIterator[T]): + iterator: Iterator[PathLike[str]] + limiter: CapacityLimiter | None + # This was added to ensure that iterating over a subclass of Path yields instances + # of that subclass rather than the base Path class. + path_cls: type[T] + + async def __anext__(self) -> T: + nextval = await to_thread.run_sync( + next, self.iterator, None, abandon_on_cancel=True, limiter=self.limiter + ) + if nextval is None: + raise StopAsyncIteration from None + + return self.path_cls(nextval, limiter=self.limiter) + + +class Path: + """ + An asynchronous version of :class:`pathlib.Path`. + + This class cannot be substituted for :class:`pathlib.Path` or + :class:`pathlib.PurePath`, but it is compatible with the :class:`os.PathLike` + interface. + + It implements the Python 3.10 version of :class:`pathlib.Path` interface, except for + the deprecated :meth:`~pathlib.Path.link_to` method. + + Some methods may be unavailable or have limited functionality, based on the Python + version: + + * :meth:`~pathlib.Path.copy` (available on Python 3.14 or later) + * :meth:`~pathlib.Path.copy_into` (available on Python 3.14 or later) + * :meth:`~pathlib.Path.from_uri` (available on Python 3.13 or later) + * :meth:`~pathlib.PurePath.full_match` (available on Python 3.13 or later) + * :attr:`~pathlib.Path.info` (available on Python 3.14 or later) + * :meth:`~pathlib.Path.is_junction` (available on Python 3.12 or later) + * :meth:`~pathlib.PurePath.match` (the ``case_sensitive`` parameter is only + available on Python 3.13 or later) + * :meth:`~pathlib.Path.move` (available on Python 3.14 or later) + * :meth:`~pathlib.Path.move_into` (available on Python 3.14 or later) + * :meth:`~pathlib.PurePath.relative_to` (the ``walk_up`` parameter is only available + on Python 3.12 or later) + * :meth:`~pathlib.Path.walk` (available on Python 3.12 or later) + + Any methods that do disk I/O need to be awaited on. These methods are: + + * :meth:`~pathlib.Path.absolute` + * :meth:`~pathlib.Path.chmod` + * :meth:`~pathlib.Path.cwd` + * :meth:`~pathlib.Path.exists` + * :meth:`~pathlib.Path.expanduser` + * :meth:`~pathlib.Path.group` + * :meth:`~pathlib.Path.hardlink_to` + * :meth:`~pathlib.Path.home` + * :meth:`~pathlib.Path.is_block_device` + * :meth:`~pathlib.Path.is_char_device` + * :meth:`~pathlib.Path.is_dir` + * :meth:`~pathlib.Path.is_fifo` + * :meth:`~pathlib.Path.is_file` + * :meth:`~pathlib.Path.is_junction` + * :meth:`~pathlib.Path.is_mount` + * :meth:`~pathlib.Path.is_socket` + * :meth:`~pathlib.Path.is_symlink` + * :meth:`~pathlib.Path.lchmod` + * :meth:`~pathlib.Path.lstat` + * :meth:`~pathlib.Path.mkdir` + * :meth:`~pathlib.Path.open` + * :meth:`~pathlib.Path.owner` + * :meth:`~pathlib.Path.read_bytes` + * :meth:`~pathlib.Path.read_text` + * :meth:`~pathlib.Path.readlink` + * :meth:`~pathlib.Path.rename` + * :meth:`~pathlib.Path.replace` + * :meth:`~pathlib.Path.resolve` + * :meth:`~pathlib.Path.rmdir` + * :meth:`~pathlib.Path.samefile` + * :meth:`~pathlib.Path.stat` + * :meth:`~pathlib.Path.symlink_to` + * :meth:`~pathlib.Path.touch` + * :meth:`~pathlib.Path.unlink` + * :meth:`~pathlib.Path.walk` + * :meth:`~pathlib.Path.write_bytes` + * :meth:`~pathlib.Path.write_text` + + Additionally, the following methods return an async iterator yielding + :class:`~.Path` objects: + + * :meth:`~pathlib.Path.glob` + * :meth:`~pathlib.Path.iterdir` + * :meth:`~pathlib.Path.rglob` + + .. versionchanged:: 4.14.0 + Added the ``limiter`` keyword argument. + """ + + __slots__ = "__weakref__", "_limiter", "_path" + + __weakref__: Any + + def __init__( + self, *args: str | PathLike[str], limiter: CapacityLimiter | None = None + ) -> None: + if limiter is not None and not isinstance(limiter, CapacityLimiter): + raise TypeError( + f"limiter must be a CapacityLimiter or None, not " + f"{limiter.__class__.__name__}" + ) + + self._path: Final[pathlib.Path] = pathlib.Path(*args) + self._limiter = limiter + + def __fspath__(self) -> str: + return self._path.__fspath__() + + if sys.version_info >= (3, 15): + + def __vfspath__(self) -> str: + return self._path.__vfspath__() + + def __str__(self) -> str: + return self._path.__str__() + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.as_posix()!r})" + + def __bytes__(self) -> bytes: + return self._path.__bytes__() + + def __hash__(self) -> int: + return self._path.__hash__() + + def __eq__(self, other: object) -> bool: + target = other._path if isinstance(other, Path) else other + return self._path.__eq__(target) + + def __lt__(self, other: pathlib.PurePath | Path) -> bool: + target = other._path if isinstance(other, Path) else other + return self._path.__lt__(target) + + def __le__(self, other: pathlib.PurePath | Path) -> bool: + target = other._path if isinstance(other, Path) else other + return self._path.__le__(target) + + def __gt__(self, other: pathlib.PurePath | Path) -> bool: + target = other._path if isinstance(other, Path) else other + return self._path.__gt__(target) + + def __ge__(self, other: pathlib.PurePath | Path) -> bool: + target = other._path if isinstance(other, Path) else other + return self._path.__ge__(target) + + def __truediv__(self, other: str | PathLike[str]) -> Self: + return type(self)(self._path / other, limiter=self._limiter) + + def __rtruediv__(self, other: str | PathLike[str]) -> Self: + return type(self)(other, limiter=self._limiter) / self + + @property + def limiter(self) -> CapacityLimiter | None: + """The capacity limiter used by this path, if not the global limiter.""" + return self._limiter + + @property + def parts(self) -> tuple[str, ...]: + return self._path.parts + + @property + def drive(self) -> str: + return self._path.drive + + @property + def root(self) -> str: + return self._path.root + + @property + def anchor(self) -> str: + return self._path.anchor + + @property + def parents(self) -> Sequence[Self]: + return tuple(type(self)(p, limiter=self._limiter) for p in self._path.parents) + + @property + def parent(self) -> Self: + return type(self)(self._path.parent, limiter=self._limiter) + + @property + def name(self) -> str: + return self._path.name + + @property + def suffix(self) -> str: + return self._path.suffix + + @property + def suffixes(self) -> list[str]: + return self._path.suffixes + + @property + def stem(self) -> str: + return self._path.stem + + async def absolute(self) -> Self: + path = await to_thread.run_sync(self._path.absolute, limiter=self._limiter) + return type(self)(path, limiter=self._limiter) + + def as_posix(self) -> str: + return self._path.as_posix() + + def as_uri(self) -> str: + return self._path.as_uri() + + if sys.version_info >= (3, 13): + parser: ClassVar[ModuleType] = pathlib.Path.parser + + @classmethod + def from_uri(cls, uri: str, *, limiter: CapacityLimiter | None = None) -> Self: + return cls(pathlib.Path.from_uri(uri), limiter=limiter) + + def full_match( + self, path_pattern: str, *, case_sensitive: bool | None = None + ) -> bool: + return self._path.full_match(path_pattern, case_sensitive=case_sensitive) + + def match( + self, path_pattern: str, *, case_sensitive: bool | None = None + ) -> bool: + return self._path.match(path_pattern, case_sensitive=case_sensitive) + else: + + def match(self, path_pattern: str) -> bool: + return self._path.match(path_pattern) + + if sys.version_info >= (3, 14): + + @property + def info(self) -> PathInfo: + return self._path.info + + async def copy( + self, + target: str | os.PathLike[str], + *, + follow_symlinks: bool = True, + preserve_metadata: bool = False, + ) -> Self: + func = partial( + self._path.copy, + follow_symlinks=follow_symlinks, + preserve_metadata=preserve_metadata, + ) + return type(self)( + await to_thread.run_sync( + func, pathlib.Path(target), limiter=self._limiter + ), + limiter=self._limiter, + ) + + async def copy_into( + self, + target_dir: str | os.PathLike[str], + *, + follow_symlinks: bool = True, + preserve_metadata: bool = False, + ) -> Self: + func = partial( + self._path.copy_into, + follow_symlinks=follow_symlinks, + preserve_metadata=preserve_metadata, + ) + return type(self)( + await to_thread.run_sync( + func, pathlib.Path(target_dir), limiter=self._limiter + ), + limiter=self._limiter, + ) + + async def move(self, target: str | os.PathLike[str]) -> Self: + # Upstream does not handle anyio.Path properly as a PathLike + target = pathlib.Path(target) + return type(self)( + await to_thread.run_sync( + self._path.move, target, limiter=self._limiter + ), + limiter=self._limiter, + ) + + async def move_into( + self, + target_dir: str | os.PathLike[str], + ) -> Self: + return type(self)( + await to_thread.run_sync( + self._path.move_into, target_dir, limiter=self._limiter + ), + limiter=self._limiter, + ) + + def is_relative_to(self, other: str | PathLike[str]) -> bool: + try: + self.relative_to(other) + return True + except ValueError: + return False + + async def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None: + func = partial(os.chmod, follow_symlinks=follow_symlinks) + return await to_thread.run_sync(func, self._path, mode, limiter=self._limiter) + + @classmethod + async def cwd(cls, *, limiter: CapacityLimiter | None = None) -> Self: + path = await to_thread.run_sync(pathlib.Path.cwd, limiter=limiter) + return cls(path, limiter=limiter) + + if sys.version_info >= (3, 12): + + async def exists(self, *, follow_symlinks: bool = True) -> bool: + return await to_thread.run_sync( + partial(self._path.exists, follow_symlinks=follow_symlinks), + abandon_on_cancel=True, + limiter=self._limiter, + ) + + else: + + async def exists(self) -> bool: + return await to_thread.run_sync( + self._path.exists, abandon_on_cancel=True, limiter=self._limiter + ) + + async def expanduser(self) -> Self: + return type(self)( + await to_thread.run_sync( + self._path.expanduser, abandon_on_cancel=True, limiter=self._limiter + ), + limiter=self._limiter, + ) + + if sys.version_info < (3, 12): + # Python 3.11 and earlier + def glob(self, pattern: str) -> AsyncIterator[Self]: + gen = self._path.glob(pattern) + return _PathIterator(gen, self._limiter, type(self)) + elif (3, 12) <= sys.version_info < (3, 13): + # changed in Python 3.12: + # - The case_sensitive parameter was added. + def glob( + self, + pattern: str, + *, + case_sensitive: bool | None = None, + ) -> AsyncIterator[Self]: + gen = self._path.glob(pattern, case_sensitive=case_sensitive) + return _PathIterator(gen, self._limiter, type(self)) + elif sys.version_info >= (3, 13): + # Changed in Python 3.13: + # - The recurse_symlinks parameter was added. + # - The pattern parameter accepts a path-like object. + def glob( # type: ignore[misc] # mypy doesn't allow for differing signatures in a conditional block + self, + pattern: str | PathLike[str], + *, + case_sensitive: bool | None = None, + recurse_symlinks: bool = False, + ) -> AsyncIterator[Self]: + gen = self._path.glob( + pattern, # type: ignore[arg-type] + case_sensitive=case_sensitive, + recurse_symlinks=recurse_symlinks, + ) + return _PathIterator(gen, self._limiter, type(self)) + + if sys.version_info >= (3, 13): + + async def group(self, *, follow_symlinks: bool = True) -> str: + return await to_thread.run_sync( + partial(self._path.group, follow_symlinks=follow_symlinks), + abandon_on_cancel=True, + limiter=self._limiter, + ) + + else: + + async def group(self) -> str: + return await to_thread.run_sync( + self._path.group, abandon_on_cancel=True, limiter=self._limiter + ) + + async def hardlink_to( + self, target: str | bytes | PathLike[str] | PathLike[bytes] + ) -> None: + if isinstance(target, Path): + target = target._path + + await to_thread.run_sync(os.link, target, self, limiter=self._limiter) + + @classmethod + async def home(cls, *, limiter: CapacityLimiter | None = None) -> Self: + home_path = await to_thread.run_sync(pathlib.Path.home, limiter=limiter) + return cls(home_path, limiter=limiter) + + def is_absolute(self) -> bool: + return self._path.is_absolute() + + async def is_block_device(self) -> bool: + return await to_thread.run_sync( + self._path.is_block_device, abandon_on_cancel=True, limiter=self._limiter + ) + + async def is_char_device(self) -> bool: + return await to_thread.run_sync( + self._path.is_char_device, abandon_on_cancel=True, limiter=self._limiter + ) + + if sys.version_info >= (3, 13): + + async def is_dir(self, *, follow_symlinks: bool = True) -> bool: + return await to_thread.run_sync( + partial(self._path.is_dir, follow_symlinks=follow_symlinks), + abandon_on_cancel=True, + limiter=self._limiter, + ) + + else: + + async def is_dir(self) -> bool: + return await to_thread.run_sync( + self._path.is_dir, abandon_on_cancel=True, limiter=self._limiter + ) + + async def is_fifo(self) -> bool: + return await to_thread.run_sync( + self._path.is_fifo, abandon_on_cancel=True, limiter=self._limiter + ) + + if sys.version_info >= (3, 13): + + async def is_file(self, *, follow_symlinks: bool = True) -> bool: + return await to_thread.run_sync( + partial(self._path.is_file, follow_symlinks=follow_symlinks), + abandon_on_cancel=True, + limiter=self._limiter, + ) + + else: + + async def is_file(self) -> bool: + return await to_thread.run_sync( + self._path.is_file, abandon_on_cancel=True, limiter=self._limiter + ) + + if sys.version_info >= (3, 12): + + async def is_junction(self) -> bool: + return await to_thread.run_sync( + self._path.is_junction, limiter=self._limiter + ) + + async def is_mount(self) -> bool: + return await to_thread.run_sync( + os.path.ismount, self._path, abandon_on_cancel=True, limiter=self._limiter + ) + + if sys.version_info < (3, 15): + + def is_reserved(self) -> bool: + return self._path.is_reserved() + + async def is_socket(self) -> bool: + return await to_thread.run_sync( + self._path.is_socket, abandon_on_cancel=True, limiter=self._limiter + ) + + async def is_symlink(self) -> bool: + return await to_thread.run_sync( + self._path.is_symlink, abandon_on_cancel=True, limiter=self._limiter + ) + + async def iterdir(self) -> AsyncIterator[Self]: + gen = ( + self._path.iterdir() + if sys.version_info < (3, 13) + else await to_thread.run_sync( + self._path.iterdir, abandon_on_cancel=True, limiter=self._limiter + ) + ) + async for path in _PathIterator(gen, self._limiter, type(self)): + yield path + + def joinpath(self, *args: str | PathLike[str]) -> Self: + return type(self)(self._path.joinpath(*args), limiter=self._limiter) + + async def lchmod(self, mode: int) -> None: + await to_thread.run_sync(self._path.lchmod, mode, limiter=self._limiter) + + async def lstat(self) -> os.stat_result: + return await to_thread.run_sync( + self._path.lstat, abandon_on_cancel=True, limiter=self._limiter + ) + + async def mkdir( + self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False + ) -> None: + await to_thread.run_sync( + self._path.mkdir, mode, parents, exist_ok, limiter=self._limiter + ) + + @overload + async def open( + self, + mode: OpenBinaryMode, + buffering: int = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + ) -> AsyncFile[bytes]: ... + + @overload + async def open( + self, + mode: OpenTextMode = ..., + buffering: int = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + ) -> AsyncFile[str]: ... + + async def open( + self, + mode: str = "r", + buffering: int = -1, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, + ) -> AsyncFile[Any]: + fp = await to_thread.run_sync( + self._path.open, + mode, + buffering, + encoding, + errors, + newline, + limiter=self._limiter, + ) + return AsyncFile(fp, limiter=self._limiter) + + if sys.version_info >= (3, 13): + + async def owner(self, *, follow_symlinks: bool = True) -> str: + return await to_thread.run_sync( + partial(self._path.owner, follow_symlinks=follow_symlinks), + abandon_on_cancel=True, + limiter=self._limiter, + ) + + else: + + async def owner(self) -> str: + return await to_thread.run_sync( + self._path.owner, abandon_on_cancel=True, limiter=self._limiter + ) + + async def read_bytes(self) -> bytes: + return await to_thread.run_sync(self._path.read_bytes, limiter=self._limiter) + + if sys.version_info >= (3, 13): + + async def read_text( + self, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, + ) -> str: + return await to_thread.run_sync( + self._path.read_text, + encoding, + errors, + newline, + limiter=self._limiter, + ) + + else: + + async def read_text( + self, encoding: str | None = None, errors: str | None = None + ) -> str: + return await to_thread.run_sync( + self._path.read_text, encoding, errors, limiter=self._limiter + ) + + if sys.version_info >= (3, 12): + + def relative_to( + self, *other: str | PathLike[str], walk_up: bool = False + ) -> Self: + # relative_to() should work with any PathLike but it doesn't + others = [pathlib.Path(other) for other in other] + return type(self)( + self._path.relative_to(*others, walk_up=walk_up), limiter=self._limiter + ) + + else: + + def relative_to(self, *other: str | PathLike[str]) -> Self: + return type(self)(self._path.relative_to(*other), limiter=self._limiter) + + async def readlink(self) -> Self: + target = await to_thread.run_sync( + os.readlink, self._path, limiter=self._limiter + ) + return type(self)(target, limiter=self._limiter) + + async def rename(self, target: str | pathlib.PurePath | Path) -> Self: + if isinstance(target, Path): + target = target._path + + await to_thread.run_sync(self._path.rename, target, limiter=self._limiter) + return type(self)(target, limiter=self._limiter) + + async def replace(self, target: str | pathlib.PurePath | Path) -> Self: + if isinstance(target, Path): + target = target._path + + await to_thread.run_sync(self._path.replace, target, limiter=self._limiter) + return type(self)(target, limiter=self._limiter) + + async def resolve(self, strict: bool = False) -> Self: + func = partial(self._path.resolve, strict=strict) + return type(self)( + await to_thread.run_sync( + func, abandon_on_cancel=True, limiter=self._limiter + ), + limiter=self._limiter, + ) + + if sys.version_info < (3, 12): + # Pre Python 3.12 + def rglob(self, pattern: str) -> AsyncIterator[Self]: + gen = self._path.rglob(pattern) + return _PathIterator(gen, self._limiter, type(self)) + elif (3, 12) <= sys.version_info < (3, 13): + # Changed in Python 3.12: + # - The case_sensitive parameter was added. + def rglob( + self, pattern: str, *, case_sensitive: bool | None = None + ) -> AsyncIterator[Self]: + gen = self._path.rglob(pattern, case_sensitive=case_sensitive) + return _PathIterator(gen, self._limiter, type(self)) + elif sys.version_info >= (3, 13): + # Changed in Python 3.13: + # - The recurse_symlinks parameter was added. + # - The pattern parameter accepts a path-like object. + def rglob( # type: ignore[misc] # mypy doesn't allow for differing signatures in a conditional block + self, + pattern: str | PathLike[str], + *, + case_sensitive: bool | None = None, + recurse_symlinks: bool = False, + ) -> AsyncIterator[Self]: + gen = self._path.rglob( + pattern, # type: ignore[arg-type] + case_sensitive=case_sensitive, + recurse_symlinks=recurse_symlinks, + ) + return _PathIterator(gen, self._limiter, type(self)) + + async def rmdir(self) -> None: + await to_thread.run_sync(self._path.rmdir, limiter=self._limiter) + + async def samefile(self, other_path: str | PathLike[str]) -> bool: + if isinstance(other_path, Path): + other_path = other_path._path + + return await to_thread.run_sync( + self._path.samefile, + other_path, + abandon_on_cancel=True, + limiter=self._limiter, + ) + + async def stat(self, *, follow_symlinks: bool = True) -> os.stat_result: + func = partial(os.stat, follow_symlinks=follow_symlinks) + return await to_thread.run_sync( + func, self._path, abandon_on_cancel=True, limiter=self._limiter + ) + + async def symlink_to( + self, + target: str | bytes | PathLike[str] | PathLike[bytes], + target_is_directory: bool = False, + ) -> None: + if isinstance(target, Path): + target = target._path + + await to_thread.run_sync( + self._path.symlink_to, target, target_is_directory, limiter=self._limiter + ) + + async def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None: + await to_thread.run_sync( + self._path.touch, mode, exist_ok, limiter=self._limiter + ) + + async def unlink(self, missing_ok: bool = False) -> None: + try: + await to_thread.run_sync(self._path.unlink, limiter=self._limiter) + except FileNotFoundError: + if not missing_ok: + raise + + if sys.version_info >= (3, 12): + + async def walk( + self, + top_down: bool = True, + on_error: Callable[[OSError], object] | None = None, + follow_symlinks: bool = False, + ) -> AsyncIterator[tuple[Self, list[str], list[str]]]: + def get_next_value() -> tuple[pathlib.Path, list[str], list[str]] | None: + try: + return next(gen) + except StopIteration: + return None + + gen = self._path.walk(top_down, on_error, follow_symlinks) + while True: + value = await to_thread.run_sync(get_next_value, limiter=self._limiter) + if value is None: + return + + root, dirs, paths = value + yield type(self)(root, limiter=self._limiter), dirs, paths + + def with_name(self, name: str) -> Self: + return type(self)(self._path.with_name(name), limiter=self._limiter) + + if sys.version_info < (3, 13): + # Backport pathlib's Python>=3.13 behavior for empty stems on paths with non-empty suffixes. + # See: https://github.com/python/cpython/pull/114612 + def with_stem(self, stem: str) -> Self: + suffix = self._path.suffix + if not suffix: + return self.with_name(stem) + elif not stem: + # If the suffix is non-empty, we can't make the stem empty. + raise ValueError(f"{self!r} has a non-empty suffix") + else: + return self.with_name(stem + suffix) + else: + + def with_stem(self, stem: str) -> Self: + return type(self)(self._path.with_stem(stem), limiter=self._limiter) + + def with_suffix(self, suffix: str) -> Self: + return type(self)(self._path.with_suffix(suffix), limiter=self._limiter) + + def with_segments(self, *pathsegments: str | PathLike[str]) -> Self: + return type(self)(*pathsegments, limiter=self._limiter) + + async def write_bytes(self, data: ReadableBuffer) -> int: + return await to_thread.run_sync( + self._path.write_bytes, data, limiter=self._limiter + ) + + async def write_text( + self, + data: str, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, + ) -> int: + return await to_thread.run_sync( + self._path.write_text, + data, + encoding, + errors, + newline, + limiter=self._limiter, + ) + + +PathLike.register(Path) diff --git a/source/target-vendors/cp312/anyio/_core/_futures.py b/source/target-vendors/cp312/anyio/_core/_futures.py new file mode 100644 index 0000000..7730c53 --- /dev/null +++ b/source/target-vendors/cp312/anyio/_core/_futures.py @@ -0,0 +1,191 @@ +from __future__ import annotations + +from collections.abc import Generator +from enum import Enum, auto +from typing import Any, Generic, TypeVar + +from ._exceptions import ( + FutureAlreadyFinished, + FutureCancelled, + FutureFailed, + FutureNotFinished, +) +from ._synchronization import Event + +T = TypeVar("T") + + +class Future(Generic[T]): + """ + An awaitable object that works similar to a :class:`asyncio.Future` but + with similar characteristics to a :class:`.TaskHandle`. + """ + + class Status(Enum): + """ + The status of a future handle. + + .. attribute:: PENDING + + The future has not finished yet. + .. attribute:: FINISHED + + The future has finished with a return value. + .. attribute:: CANCELLED + + The future was cancelled and has finished since. + .. attribute:: FAILED + + The future raised an exception. + """ + + PENDING = auto() + FINISHED = auto() + CANCELLED = auto() + FAILED = auto() + + __slots__ = ( + "_cancelled", + "_exception", + "_finished_event", + "_name", + "_return_value", + ) + _return_value: T + + def __init__(self, *, name: str | None = None) -> None: + self._finished_event = Event() + self._exception: BaseException | None = None + self._cancelled: bool = False + self._name = name + + def _check_pending(self) -> None: + """Shortcut for checking if a Future is pending + + :raises FutureAlreadyFinished: if future was already given a result or exception. + :raises FutureCancelled: if future has been cancelled previously. + """ + match self.status: + case Future.Status.PENDING: + return + case Future.Status.FINISHED: + raise FutureAlreadyFinished("future has already finished") + case Future.Status.FAILED: + raise FutureAlreadyFinished("future already failed") + case Future.Status.CANCELLED: + raise FutureCancelled("future was cancelled") + + async def wait(self) -> None: + """ + Waits for the future to finish. + + This method will attempt to wait for a result or exception + """ + await self._finished_event.wait() + + def cancel(self) -> None: + """Cancels a pending `.Future` object + + Does nothing if the Future was already finished. + """ + if self.status is Future.Status.PENDING: + self._cancelled = True + self._finished_event.set() + + @property + def exception(self) -> BaseException | None: + """ + The exception value of a `.Future` + + :raises FutureNotFinished: if future is still pending + :raises FutureCancelled: if future was cancelled + :returns: None if future succeeds with a result sent instead + otherwise this will be an exception + """ + + match self.status: + case Future.Status.PENDING: + raise FutureNotFinished("the future has not finished yet") + case Future.Status.FINISHED: + return None + case Future.Status.CANCELLED: + raise FutureCancelled("the future was cancelled") + case Future.Status.FAILED: + return self._exception + + @exception.setter + def exception(self, exception: BaseException) -> None: + """Send exception for a `.Future` object + + :raises FutureAlreadyFinished: if future was already given a result or exception. + :raises FutureCancelled: if future has been cancelled previously. + """ + self._check_pending() + self._exception = exception + self._finished_event.set() + + @property + def return_value(self) -> T: + """ + The return value of the future. + + :raises FutureNotFinished: if the future has not finished yet + :raises FutureCancelled: if the future was cancelled + :raises FutureFailed: if the future raised an exception + + """ + match self.status: + case Future.Status.PENDING: + raise FutureNotFinished("the future has not finished yet") + case Future.Status.FINISHED: + return self._return_value + case Future.Status.CANCELLED: + raise FutureCancelled("the future was cancelled") + case Future.Status.FAILED: + raise FutureFailed( + "the future raised an exception" + ) from self._exception + + @return_value.setter + def return_value(self, value: T) -> None: + """ + Send pending result for a `.Future` object + + :raises FutureAlreadyFinished: if future was already given a result or exception. + :raises FutureCancelled: if future has been cancelled previously. + """ + self._check_pending() + self._return_value = value + self._finished_event.set() + + @property + def status(self) -> Future.Status: + """ + The current status of a future. + + Every future starts in the :attr:`~Future.Status.PENDING` state. + If a future is cancelled while in this state, it will transition to the + :attr:`Future.Status.CANCELLED` state. When the task finishes, it will + transition to one of the three final states ( + :attr:`Future.Status.FINISHED`, :attr:`Future.Status.FAILED`, or + :attr:`Future.Status.CANCELLED`) depending on the exception the task + raised, if any. No other status transitions will happen. + """ + if not self._finished_event.is_set(): + return Future.Status.PENDING + elif self._cancelled: + return Future.Status.CANCELLED + elif self._exception is not None: + return Future.Status.FAILED + else: + return Future.Status.FINISHED + + def __await__(self) -> Generator[Any, Any, T]: + yield from self.wait().__await__() + return self.return_value + + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__} {self.status.name.lower()} " + f"name={self._name!r}>" + ) diff --git a/source/target-vendors/cp312/anyio/_core/_resources.py b/source/target-vendors/cp312/anyio/_core/_resources.py new file mode 100644 index 0000000..6ca35d9 --- /dev/null +++ b/source/target-vendors/cp312/anyio/_core/_resources.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from ..abc import AsyncResource # noqa: TC001 +from ._tasks import CancelScope + + +async def aclose_forcefully(resource: AsyncResource) -> None: + """ + Close an asynchronous resource in a cancelled scope. + + Doing this closes the resource without waiting on anything. + + :param resource: the resource to close + + """ + with CancelScope() as scope: + scope.cancel() + await resource.aclose() diff --git a/source/target-vendors/cp312/anyio/_core/_signals.py b/source/target-vendors/cp312/anyio/_core/_signals.py new file mode 100644 index 0000000..e24c79e --- /dev/null +++ b/source/target-vendors/cp312/anyio/_core/_signals.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from collections.abc import AsyncIterator +from contextlib import AbstractContextManager +from signal import Signals + +from ._eventloop import get_async_backend + + +def open_signal_receiver( + *signals: Signals, +) -> AbstractContextManager[AsyncIterator[Signals]]: + """ + Start receiving operating system signals. + + :param signals: signals to receive (e.g. ``signal.SIGINT``) + :return: an asynchronous context manager for an asynchronous iterator which yields + signal numbers + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + .. warning:: Windows does not support signals natively so it is best to avoid + relying on this in cross-platform applications. + + .. warning:: On asyncio, this permanently replaces any previous signal handler for + the given signals, as set via :meth:`~asyncio.loop.add_signal_handler`. + + """ + return get_async_backend().open_signal_receiver(*signals) diff --git a/source/target-vendors/cp312/anyio/_core/_sockets.py b/source/target-vendors/cp312/anyio/_core/_sockets.py new file mode 100644 index 0000000..14ee6e4 --- /dev/null +++ b/source/target-vendors/cp312/anyio/_core/_sockets.py @@ -0,0 +1,1011 @@ +from __future__ import annotations + +import errno +import os +import socket +import ssl +import stat +import sys +from collections.abc import Awaitable +from dataclasses import dataclass +from ipaddress import IPv4Address, IPv6Address, ip_address +from os import PathLike, chmod +from socket import AddressFamily, SocketKind +from typing import TYPE_CHECKING, Any, Literal, cast, overload + +from .. import ConnectionFailed, to_thread +from ..abc import ( + ByteStreamConnectable, + ConnectedUDPSocket, + ConnectedUNIXDatagramSocket, + IPAddressType, + IPSockAddrType, + SocketListener, + SocketStream, + UDPSocket, + UNIXDatagramSocket, + UNIXSocketStream, +) +from ..streams.stapled import MultiListener +from ..streams.tls import TLSConnectable, TLSStream +from ._eventloop import get_async_backend +from ._resources import aclose_forcefully +from ._synchronization import Event +from ._tasks import create_task_group, move_on_after + +if TYPE_CHECKING: + from _typeshed import FileDescriptorLike +else: + FileDescriptorLike = object + +if sys.version_info < (3, 11): + from exceptiongroup import ExceptionGroup + +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + +if sys.version_info < (3, 13): + from typing_extensions import deprecated +else: + from warnings import deprecated + +IPPROTO_IPV6 = getattr(socket, "IPPROTO_IPV6", 41) # https://bugs.python.org/issue29515 + +AnyIPAddressFamily = Literal[ + AddressFamily.AF_UNSPEC, AddressFamily.AF_INET, AddressFamily.AF_INET6 +] +IPAddressFamily = Literal[AddressFamily.AF_INET, AddressFamily.AF_INET6] + + +def idna2008_resolve(host: str) -> bytes: + try: + return host.encode("ascii") + except UnicodeEncodeError: + import idna + + return idna.encode(host, uts46=True) + + +# tls_hostname given +@overload +async def connect_tcp( + remote_host: IPAddressType, + remote_port: int, + *, + local_host: IPAddressType | None = ..., + local_port: int | None = ..., + ssl_context: ssl.SSLContext | None = ..., + tls_standard_compatible: bool = ..., + tls_hostname: str, + happy_eyeballs_delay: float = ..., +) -> TLSStream: ... + + +# ssl_context given +@overload +async def connect_tcp( + remote_host: IPAddressType, + remote_port: int, + *, + local_host: IPAddressType | None = ..., + local_port: int | None = ..., + ssl_context: ssl.SSLContext, + tls_standard_compatible: bool = ..., + tls_hostname: str | None = ..., + happy_eyeballs_delay: float = ..., +) -> TLSStream: ... + + +# tls=True +@overload +async def connect_tcp( + remote_host: IPAddressType, + remote_port: int, + *, + local_host: IPAddressType | None = ..., + local_port: int | None = ..., + tls: Literal[True], + ssl_context: ssl.SSLContext | None = ..., + tls_standard_compatible: bool = ..., + tls_hostname: str | None = ..., + happy_eyeballs_delay: float = ..., +) -> TLSStream: ... + + +# tls=False +@overload +async def connect_tcp( + remote_host: IPAddressType, + remote_port: int, + *, + local_host: IPAddressType | None = ..., + local_port: int | None = ..., + tls: Literal[False], + ssl_context: ssl.SSLContext | None = ..., + tls_standard_compatible: bool = ..., + tls_hostname: str | None = ..., + happy_eyeballs_delay: float = ..., +) -> SocketStream: ... + + +# No TLS arguments +@overload +async def connect_tcp( + remote_host: IPAddressType, + remote_port: int, + *, + local_host: IPAddressType | None = ..., + local_port: int | None = ..., + happy_eyeballs_delay: float = ..., +) -> SocketStream: ... + + +async def connect_tcp( + remote_host: IPAddressType, + remote_port: int, + *, + local_host: IPAddressType | None = None, + local_port: int | None = None, + tls: bool = False, + ssl_context: ssl.SSLContext | None = None, + tls_standard_compatible: bool = True, + tls_hostname: str | None = None, + happy_eyeballs_delay: float = 0.25, +) -> SocketStream | TLSStream: + """ + Connect to a host using the TCP protocol. + + This function implements the stateless version of the Happy Eyeballs algorithm (RFC + 6555). If ``remote_host`` is a host name that resolves to multiple IP addresses, + each one is tried until one connection attempt succeeds. If the first attempt does + not connected within 250 milliseconds, a second attempt is started using the next + address in the list, and so on. On IPv6 enabled systems, an IPv6 address (if + available) is tried first. + + When the connection has been established, a TLS handshake will be done if either + ``ssl_context`` or ``tls_hostname`` is not ``None``, or if ``tls`` is ``True``. + + :param remote_host: the IP address or host name to connect to + :param remote_port: port on the target host to connect to + :param local_host: the interface address or name to bind the socket to before + connecting + :param local_port: the local port to bind to (requires ``local_host`` to also be + set) + :param tls: ``True`` to do a TLS handshake with the connected stream and return a + :class:`~anyio.streams.tls.TLSStream` instead + :param ssl_context: the SSL context object to use (if omitted, a default context is + created) + :param tls_standard_compatible: If ``True``, performs the TLS shutdown handshake + before closing the stream and requires that the server does this as well. + Otherwise, :exc:`~ssl.SSLEOFError` may be raised during reads from the stream. + Some protocols, such as HTTP, require this option to be ``False``. + See :meth:`~ssl.SSLContext.wrap_socket` for details. + :param tls_hostname: host name to check the server certificate against (defaults to + the value of ``remote_host``) + :param happy_eyeballs_delay: delay (in seconds) before starting the next connection + attempt + :return: a socket stream object if no TLS handshake was done, otherwise a TLS stream + :raises ConnectionFailed: if the connection fails + + """ + # Placed here due to https://github.com/python/mypy/issues/7057 + connected_stream: SocketStream | None = None + + async def try_connect(remote_host: str, event: Event) -> None: + nonlocal connected_stream + try: + stream = await asynclib.connect_tcp(remote_host, remote_port, local_address) + except OSError as exc: + oserrors.append(exc) + return + else: + if connected_stream is None: + connected_stream = stream + tg.cancel_scope.cancel() + else: + await stream.aclose() + finally: + event.set() + + asynclib = get_async_backend() + local_address: IPSockAddrType | None = None + family = socket.AF_UNSPEC + if local_host: + gai_res = await getaddrinfo(str(local_host), local_port) + family, *_, local_address = gai_res[0] + + target_host = str(remote_host) + try: + addr_obj = ip_address(remote_host) + except ValueError: + addr_obj = None + + if addr_obj is not None: + if isinstance(addr_obj, IPv6Address): + target_addrs = [(socket.AF_INET6, addr_obj.compressed)] + else: + target_addrs = [(socket.AF_INET, addr_obj.compressed)] + else: + # getaddrinfo() will raise an exception if name resolution fails + gai_res = await getaddrinfo( + target_host, remote_port, family=family, type=socket.SOCK_STREAM + ) + + # Organize the list so that the first address is an IPv6 address (if available) + # and the second one is an IPv4 addresses. The rest can be in whatever order. + v6_found = v4_found = False + target_addrs = [] + for af, *_, sa in gai_res: + if af == socket.AF_INET6 and not v6_found: + v6_found = True + target_addrs.insert(0, (af, sa[0])) + elif af == socket.AF_INET and not v4_found and v6_found: + v4_found = True + target_addrs.insert(1, (af, sa[0])) + else: + target_addrs.append((af, sa[0])) + + oserrors: list[OSError] = [] + try: + async with create_task_group() as tg: + for _af, addr in target_addrs: + event = Event() + tg.start_soon(try_connect, addr, event) + with move_on_after(happy_eyeballs_delay): + await event.wait() + + if connected_stream is None: + cause = ( + oserrors[0] + if len(oserrors) == 1 + else ExceptionGroup("multiple connection attempts failed", oserrors) + ) + raise OSError("All connection attempts failed") from cause + finally: + oserrors.clear() + + if tls or tls_hostname or ssl_context: + try: + return await TLSStream.wrap( + connected_stream, + server_side=False, + hostname=tls_hostname or str(remote_host), + ssl_context=ssl_context, + standard_compatible=tls_standard_compatible, + ) + except BaseException: + await aclose_forcefully(connected_stream) + raise + + return connected_stream + + +async def connect_unix(path: str | bytes | PathLike[Any]) -> UNIXSocketStream: + """ + Connect to the given UNIX socket. + + Not available on Windows. + + :param path: path to the socket + :return: a socket stream object + :raises ConnectionFailed: if the connection fails + + """ + path = os.fspath(path) + return await get_async_backend().connect_unix(path) + + +async def create_tcp_listener( + *, + local_host: IPAddressType | None = None, + local_port: int = 0, + family: AnyIPAddressFamily = socket.AddressFamily.AF_UNSPEC, + backlog: int = 65536, + reuse_port: bool = False, +) -> MultiListener[SocketStream]: + """ + Create a TCP socket listener. + + :param local_port: port number to listen on + :param local_host: IP address of the interface to listen on. If omitted, listen on + all IPv4 and IPv6 interfaces. To listen on all interfaces on a specific address + family, use ``0.0.0.0`` for IPv4 or ``::`` for IPv6. + :param family: address family (used if ``local_host`` was omitted) + :param backlog: maximum number of queued incoming connections (up to a maximum of + 2**16, or 65536) + :param reuse_port: ``True`` to allow multiple sockets to bind to the same + address/port (not supported on Windows) + :return: a multi-listener object containing one or more socket listeners + :raises OSError: if there's an error creating a socket, or binding to one or more + interfaces failed + + """ + asynclib = get_async_backend() + backlog = min(backlog, 65536) + local_host = str(local_host) if local_host is not None else None + + def setup_raw_socket( + fam: AddressFamily, + bind_addr: tuple[str, int] | tuple[str, int, int, int], + *, + v6only: bool = True, + ) -> socket.socket: + sock = socket.socket(fam) + try: + sock.setblocking(False) + + if fam == AddressFamily.AF_INET6: + sock.setsockopt(IPPROTO_IPV6, socket.IPV6_V6ONLY, v6only) + + # For Windows, enable exclusive address use. For others, enable address + # reuse. + if sys.platform == "win32": + sock.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1) + else: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + if reuse_port: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + + # Workaround for #554 + if fam == socket.AF_INET6 and "%" in bind_addr[0]: + addr, scope_id = bind_addr[0].split("%", 1) + bind_addr = (addr, bind_addr[1], 0, int(scope_id)) + + sock.bind(bind_addr) + sock.listen(backlog) + except BaseException: + sock.close() + raise + + return sock + + # We passing type=0 on non-Windows platforms as a workaround for a uvloop bug + # where we don't get the correct scope ID for IPv6 link-local addresses when passing + # type=socket.SOCK_STREAM to getaddrinfo(): + # https://github.com/MagicStack/uvloop/issues/539 + gai_res = await getaddrinfo( + local_host, + local_port, + family=family, + type=socket.SOCK_STREAM if sys.platform == "win32" else 0, + flags=socket.AI_PASSIVE | socket.AI_ADDRCONFIG, + ) + + # The set comprehension is here to work around a glibc bug: + # https://sourceware.org/bugzilla/show_bug.cgi?id=14969 + sockaddrs = sorted({res for res in gai_res if res[1] == SocketKind.SOCK_STREAM}) + + # Special case for dual-stack binding on the "any" interface + if ( + local_host is None + and family == AddressFamily.AF_UNSPEC + and socket.has_dualstack_ipv6() + and any(fam == AddressFamily.AF_INET6 for fam, *_ in gai_res) + ): + raw_socket = setup_raw_socket( + AddressFamily.AF_INET6, ("::", local_port), v6only=False + ) + listener = asynclib.create_tcp_listener(raw_socket) + return MultiListener([listener]) + + errors: list[OSError] = [] + try: + for _ in range(len(sockaddrs)): + listeners: list[SocketListener] = [] + bound_ephemeral_port = local_port + try: + for fam, *_, sockaddr in sockaddrs: + sockaddr = sockaddr[0], bound_ephemeral_port, *sockaddr[2:] + raw_socket = setup_raw_socket(fam, sockaddr) + + # Store the assigned port if an ephemeral port was requested, so + # we'll bind to the same port on all interfaces + if local_port == 0 and len(gai_res) > 1: + bound_ephemeral_port = raw_socket.getsockname()[1] + + listeners.append(asynclib.create_tcp_listener(raw_socket)) + except BaseException as exc: + for listener in listeners: + await listener.aclose() + + # If an ephemeral port was requested but binding the assigned port + # failed for another interface, rotate the address list and try again + if ( + isinstance(exc, OSError) + and exc.errno == errno.EADDRINUSE + and local_port == 0 + and bound_ephemeral_port + ): + errors.append(exc) + sockaddrs.append(sockaddrs.pop(0)) + continue + + raise + + return MultiListener(listeners) + + raise OSError( + f"Could not create {len(sockaddrs)} listeners with a consistent port" + ) from ExceptionGroup("Several bind attempts failed", errors) + finally: + del errors # Prevent reference cycles + + +async def create_unix_listener( + path: str | bytes | PathLike[Any], + *, + mode: int | None = None, + backlog: int = 65536, +) -> SocketListener: + """ + Create a UNIX socket listener. + + Not available on Windows. + + :param path: path of the socket + :param mode: permissions to set on the socket + :param backlog: maximum number of queued incoming connections (up to a maximum of + 2**16, or 65536) + :return: a listener object + + .. versionchanged:: 3.0 + If a socket already exists on the file system in the given path, it will be + removed first. + + """ + backlog = min(backlog, 65536) + raw_socket = await setup_unix_local_socket(path, mode, socket.SOCK_STREAM) + try: + raw_socket.listen(backlog) + return get_async_backend().create_unix_listener(raw_socket) + except BaseException: + raw_socket.close() + raise + + +async def create_udp_socket( + family: AnyIPAddressFamily = AddressFamily.AF_UNSPEC, + *, + local_host: IPAddressType | None = None, + local_port: int = 0, + reuse_port: bool = False, +) -> UDPSocket: + """ + Create a UDP socket. + + If ``port`` has been given, the socket will be bound to this port on the local + machine, making this socket suitable for providing UDP based services. + + :param family: address family (``AF_INET`` or ``AF_INET6``) – automatically + determined from ``local_host`` if omitted + :param local_host: IP address or host name of the local interface to bind to + :param local_port: local port to bind to + :param reuse_port: ``True`` to allow multiple sockets to bind to the same + address/port (not supported on Windows) + :return: a UDP socket + + """ + if family is AddressFamily.AF_UNSPEC and not local_host: + raise ValueError('Either "family" or "local_host" must be given') + + if local_host: + gai_res = await getaddrinfo( + str(local_host), + local_port, + family=family, + type=socket.SOCK_DGRAM, + flags=socket.AI_PASSIVE | socket.AI_ADDRCONFIG, + ) + family = cast(AnyIPAddressFamily, gai_res[0][0]) + local_address = gai_res[0][-1] + elif family is AddressFamily.AF_INET6: + local_address = ("::", 0) + else: + local_address = ("0.0.0.0", 0) + + sock = await get_async_backend().create_udp_socket( + family, local_address, None, reuse_port + ) + return cast(UDPSocket, sock) + + +async def create_connected_udp_socket( + remote_host: IPAddressType, + remote_port: int, + *, + family: AnyIPAddressFamily = AddressFamily.AF_UNSPEC, + local_host: IPAddressType | None = None, + local_port: int = 0, + reuse_port: bool = False, +) -> ConnectedUDPSocket: + """ + Create a connected UDP socket. + + Connected UDP sockets can only communicate with the specified remote host/port, an + any packets sent from other sources are dropped. + + :param remote_host: remote host to set as the default target + :param remote_port: port on the remote host to set as the default target + :param family: address family (``AF_INET`` or ``AF_INET6``) – automatically + determined from ``local_host`` or ``remote_host`` if omitted + :param local_host: IP address or host name of the local interface to bind to + :param local_port: local port to bind to + :param reuse_port: ``True`` to allow multiple sockets to bind to the same + address/port (not supported on Windows) + :return: a connected UDP socket + + """ + local_address = None + if local_host: + gai_res = await getaddrinfo( + str(local_host), + local_port, + family=family, + type=socket.SOCK_DGRAM, + flags=socket.AI_PASSIVE | socket.AI_ADDRCONFIG, + ) + family = cast(AnyIPAddressFamily, gai_res[0][0]) + local_address = gai_res[0][-1] + + gai_res = await getaddrinfo( + str(remote_host), remote_port, family=family, type=socket.SOCK_DGRAM + ) + family = cast(AnyIPAddressFamily, gai_res[0][0]) + remote_address = gai_res[0][-1] + + sock = await get_async_backend().create_udp_socket( + family, local_address, remote_address, reuse_port + ) + return cast(ConnectedUDPSocket, sock) + + +async def create_unix_datagram_socket( + *, + local_path: None | str | bytes | PathLike[Any] = None, + local_mode: int | None = None, +) -> UNIXDatagramSocket: + """ + Create a UNIX datagram socket. + + Not available on Windows. + + If ``local_path`` has been given, the socket will be bound to this path, making this + socket suitable for receiving datagrams from other processes. Other processes can + send datagrams to this socket only if ``local_path`` is set. + + If a socket already exists on the file system in the ``local_path``, it will be + removed first. + + :param local_path: the path on which to bind to + :param local_mode: permissions to set on the local socket + :return: a UNIX datagram socket + + """ + raw_socket = await setup_unix_local_socket( + local_path, local_mode, socket.SOCK_DGRAM + ) + return await get_async_backend().create_unix_datagram_socket(raw_socket, None) + + +async def create_connected_unix_datagram_socket( + remote_path: str | bytes | PathLike[Any], + *, + local_path: None | str | bytes | PathLike[Any] = None, + local_mode: int | None = None, +) -> ConnectedUNIXDatagramSocket: + """ + Create a connected UNIX datagram socket. + + Connected datagram sockets can only communicate with the specified remote path. + + If ``local_path`` has been given, the socket will be bound to this path, making + this socket suitable for receiving datagrams from other processes. Other processes + can send datagrams to this socket only if ``local_path`` is set. + + If a socket already exists on the file system in the ``local_path``, it will be + removed first. + + :param remote_path: the path to set as the default target + :param local_path: the path on which to bind to + :param local_mode: permissions to set on the local socket + :return: a connected UNIX datagram socket + + """ + remote_path = os.fspath(remote_path) + raw_socket = await setup_unix_local_socket( + local_path, local_mode, socket.SOCK_DGRAM + ) + return await get_async_backend().create_unix_datagram_socket( + raw_socket, remote_path + ) + + +async def getaddrinfo( + host: bytes | str | None, + port: str | int | None, + *, + family: int | AddressFamily = 0, + type: int | SocketKind = 0, + proto: int = 0, + flags: int = 0, +) -> list[tuple[AddressFamily, SocketKind, int, str, tuple[str, int]]]: + """ + Look up a numeric IP address given a host name. + + Internationalized domain names are translated according to the (non-transitional) + IDNA 2008 standard. + + .. note:: 4-tuple IPv6 socket addresses are automatically converted to 2-tuples of + (host, port), unlike what :func:`socket.getaddrinfo` does. + + :param host: host name + :param port: port number + :param family: socket family (`'AF_INET``, ...) + :param type: socket type (``SOCK_STREAM``, ...) + :param proto: protocol number + :param flags: flags to pass to upstream ``getaddrinfo()`` + :return: list of tuples containing (family, type, proto, canonname, sockaddr) + + .. seealso:: :func:`socket.getaddrinfo` + + """ + # Handle unicode hostnames + encoded_host = idna2008_resolve(host) if isinstance(host, str) else host + gai_res = await get_async_backend().getaddrinfo( + encoded_host, port, family=family, type=type, proto=proto, flags=flags + ) + return [ + (family, type, proto, canonname, convert_ipv6_sockaddr(sockaddr)) + for family, type, proto, canonname, sockaddr in gai_res + # filter out IPv6 results when IPv6 is disabled + if not isinstance(sockaddr[0], int) + ] + + +def getnameinfo(sockaddr: IPSockAddrType, flags: int = 0) -> Awaitable[tuple[str, str]]: + """ + Look up the host name of an IP address. + + :param sockaddr: socket address (e.g. (ipaddress, port) for IPv4) + :param flags: flags to pass to upstream ``getnameinfo()`` + :return: a tuple of (host name, service name) + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + .. seealso:: :func:`socket.getnameinfo` + + """ + return get_async_backend().getnameinfo(sockaddr, flags) + + +@deprecated("This function is deprecated; use `wait_readable` instead") +def wait_socket_readable(sock: socket.socket) -> Awaitable[None]: + """ + .. deprecated:: 4.7.0 + Use :func:`wait_readable` instead. + + Wait until the given socket has data to be read. + + .. warning:: Only use this on raw sockets that have not been wrapped by any higher + level constructs like socket streams! + + :param sock: a socket object + :raises ~anyio.ClosedResourceError: if the socket was closed while waiting for the + socket to become readable + :raises ~anyio.BusyResourceError: if another task is already waiting for the socket + to become readable + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().wait_readable(sock.fileno()) + + +@deprecated("This function is deprecated; use `wait_writable` instead") +def wait_socket_writable(sock: socket.socket) -> Awaitable[None]: + """ + .. deprecated:: 4.7.0 + Use :func:`wait_writable` instead. + + Wait until the given socket can be written to. + + This does **NOT** work on Windows when using the asyncio backend with a proactor + event loop (default on py3.8+). + + .. warning:: Only use this on raw sockets that have not been wrapped by any higher + level constructs like socket streams! + + :param sock: a socket object + :raises ~anyio.ClosedResourceError: if the socket was closed while waiting for the + socket to become writable + :raises ~anyio.BusyResourceError: if another task is already waiting for the socket + to become writable + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().wait_writable(sock.fileno()) + + +def wait_readable(obj: FileDescriptorLike) -> Awaitable[None]: + """ + Wait until the given object has data to be read. + + On Unix systems, ``obj`` must either be an integer file descriptor, or else an + object with a ``.fileno()`` method which returns an integer file descriptor. Any + kind of file descriptor can be passed, though the exact semantics will depend on + your kernel. For example, this probably won't do anything useful for on-disk files. + + On Windows systems, ``obj`` must either be an integer ``SOCKET`` handle, or else an + object with a ``.fileno()`` method which returns an integer ``SOCKET`` handle. File + descriptors aren't supported, and neither are handles that refer to anything besides + a ``SOCKET``. + + On backends where this functionality is not natively provided (asyncio + ``ProactorEventLoop`` on Windows), it is provided using a separate selector thread + which is set to shut down when the interpreter shuts down. + + .. warning:: Don't use this on raw sockets that have been wrapped by any higher + level constructs like socket streams! + + :param obj: an object with a ``.fileno()`` method or an integer handle + :raises ~anyio.ClosedResourceError: if the object was closed while waiting for the + object to become readable + :raises ~anyio.BusyResourceError: if another task is already waiting for the object + to become readable + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().wait_readable(obj) + + +def wait_writable(obj: FileDescriptorLike) -> Awaitable[None]: + """ + Wait until the given object can be written to. + + :param obj: an object with a ``.fileno()`` method or an integer handle + :raises ~anyio.ClosedResourceError: if the object was closed while waiting for the + object to become writable + :raises ~anyio.BusyResourceError: if another task is already waiting for the object + to become writable + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + .. seealso:: See the documentation of :func:`wait_readable` for the definition of + ``obj`` and notes on backend compatibility. + + .. warning:: Don't use this on raw sockets that have been wrapped by any higher + level constructs like socket streams! + + """ + return get_async_backend().wait_writable(obj) + + +def notify_closing(obj: FileDescriptorLike) -> None: + """ + Call this before closing a file descriptor (on Unix) or socket (on + Windows). This will cause any `wait_readable` or `wait_writable` + calls on the given object to immediately wake up and raise + `~anyio.ClosedResourceError`. + + This doesn't actually close the object – you still have to do that + yourself afterwards. Also, you want to be careful to make sure no + new tasks start waiting on the object in between when you call this + and when it's actually closed. So to close something properly, you + usually want to do these steps in order: + + 1. Explicitly mark the object as closed, so that any new attempts + to use it will abort before they start. + 2. Call `notify_closing` to wake up any already-existing users. + 3. Actually close the object. + + It's also possible to do them in a different order if that's more + convenient, *but only if* you make sure not to have any checkpoints in + between the steps. This way they all happen in a single atomic + step, so other tasks won't be able to tell what order they happened + in anyway. + + :param obj: an object with a ``.fileno()`` method or an integer handle + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + get_async_backend().notify_closing(obj) + + +# +# Private API +# + + +def convert_ipv6_sockaddr( + sockaddr: tuple[str, int, int, int] | tuple[str, int], +) -> tuple[str, int]: + """ + Convert a 4-tuple IPv6 socket address to a 2-tuple (address, port) format. + + If the scope ID is nonzero, it is added to the address, separated with ``%``. + Otherwise the flow id and scope id are simply cut off from the tuple. + Any other kinds of socket addresses are returned as-is. + + :param sockaddr: the result of :meth:`~socket.socket.getsockname` + :return: the converted socket address + + """ + # This is more complicated than it should be because of MyPy + if isinstance(sockaddr, tuple) and len(sockaddr) == 4: + host, port, _flowinfo, scope_id = sockaddr + if scope_id: + # PyPy (as of v7.3.11) leaves the interface name in the result, so + # we discard it and only get the scope ID from the end + # (https://foss.heptapod.net/pypy/pypy/-/issues/3938) + host = host.split("%")[0] + + # Add scope_id to the address + return f"{host}%{scope_id}", port + else: + return host, port + else: + return sockaddr + + +async def setup_unix_local_socket( + path: None | str | bytes | PathLike[Any], + mode: int | None, + socktype: int, +) -> socket.socket: + """ + Create a UNIX local socket object, deleting the socket at the given path if it + exists. + + Not available on Windows. + + :param path: path of the socket + :param mode: permissions to set on the socket + :param socktype: socket.SOCK_STREAM or socket.SOCK_DGRAM + + """ + path_str: str | None + if path is not None: + path_str = os.fsdecode(path) + + # Linux abstract namespace sockets aren't backed by a concrete file so skip stat call + if not path_str.startswith("\0"): + # Copied from pathlib... + try: + stat_result = os.stat(path) + except OSError as e: + if e.errno not in ( + errno.ENOENT, + errno.ENOTDIR, + errno.EBADF, + errno.ELOOP, + ): + raise + else: + if stat.S_ISSOCK(stat_result.st_mode): + os.unlink(path) + else: + path_str = None + + raw_socket = socket.socket(socket.AF_UNIX, socktype) + raw_socket.setblocking(False) + + if path_str is not None: + try: + await to_thread.run_sync(raw_socket.bind, path_str, abandon_on_cancel=True) + if mode is not None: + await to_thread.run_sync(chmod, path_str, mode, abandon_on_cancel=True) + except BaseException: + raw_socket.close() + raise + + return raw_socket + + +@dataclass +class TCPConnectable(ByteStreamConnectable): + """ + Connects to a TCP server at the given host and port. + + :param host: host name or IP address of the server + :param port: TCP port number of the server + """ + + host: str | IPv4Address | IPv6Address + port: int + + def __post_init__(self) -> None: + if self.port < 1 or self.port > 65535: + raise ValueError("TCP port number out of range") + + @override + async def connect(self) -> SocketStream: + try: + return await connect_tcp(self.host, self.port) + except OSError as exc: + raise ConnectionFailed( + f"error connecting to {self.host}:{self.port}: {exc}" + ) from exc + + +@dataclass +class UNIXConnectable(ByteStreamConnectable): + """ + Connects to a UNIX domain socket at the given path. + + :param path: the file system path of the socket + """ + + path: str | bytes | PathLike[str] | PathLike[bytes] + + @override + async def connect(self) -> UNIXSocketStream: + try: + return await connect_unix(self.path) + except OSError as exc: + raise ConnectionFailed(f"error connecting to {self.path!r}: {exc}") from exc + + +def as_connectable( + remote: ByteStreamConnectable + | tuple[str | IPv4Address | IPv6Address, int] + | str + | bytes + | PathLike[str], + /, + *, + tls: bool = False, + ssl_context: ssl.SSLContext | None = None, + tls_hostname: str | None = None, + tls_standard_compatible: bool = True, +) -> ByteStreamConnectable: + """ + Return a byte stream connectable from the given object. + + If a bytestream connectable is given, it is returned unchanged. + If a tuple of (host, port) is given, a TCP connectable is returned. + If a string or bytes path is given, a UNIX connectable is returned. + + If ``tls=True``, the connectable will be wrapped in a + :class:`~.streams.tls.TLSConnectable`. + + :param remote: a connectable, a tuple of (host, port) or a path to a UNIX socket + :param tls: if ``True``, wrap the plaintext connectable in a + :class:`~.streams.tls.TLSConnectable`, using the provided TLS settings) + :param ssl_context: if ``tls=True``, the SSLContext object to use (if not provided, + a secure default will be created) + :param tls_hostname: if ``tls=True``, host name of the server to use for checking + the server certificate (defaults to the host portion of the address for TCP + connectables) + :param tls_standard_compatible: if ``False`` and ``tls=True``, makes the TLS stream + skip the closing handshake when closing the connection, so it won't raise an + exception if the server does the same + + """ + connectable: TCPConnectable | UNIXConnectable | TLSConnectable + if isinstance(remote, ByteStreamConnectable): + return remote + elif isinstance(remote, tuple) and len(remote) == 2: + connectable = TCPConnectable(*remote) + elif isinstance(remote, (str, bytes, PathLike)): + connectable = UNIXConnectable(remote) + else: + raise TypeError(f"cannot convert {remote!r} to a connectable") + + if tls: + if not tls_hostname and isinstance(connectable, TCPConnectable): + tls_hostname = str(connectable.host) + + connectable = TLSConnectable( + connectable, + ssl_context=ssl_context, + hostname=tls_hostname, + standard_compatible=tls_standard_compatible, + ) + + return connectable diff --git a/source/target-vendors/cp312/anyio/_core/_streams.py b/source/target-vendors/cp312/anyio/_core/_streams.py new file mode 100644 index 0000000..2b9c7df --- /dev/null +++ b/source/target-vendors/cp312/anyio/_core/_streams.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import math +from typing import TypeVar +from warnings import warn + +from ..streams.memory import ( + MemoryObjectReceiveStream, + MemoryObjectSendStream, + _MemoryObjectStreamState, +) + +T_Item = TypeVar("T_Item") + + +class create_memory_object_stream( + tuple[MemoryObjectSendStream[T_Item], MemoryObjectReceiveStream[T_Item]], +): + """ + Create a memory object stream. + + The stream's item type can be annotated like + :func:`create_memory_object_stream[T_Item]`. + + :param max_buffer_size: number of items held in the buffer until ``send()`` starts + blocking + :param item_type: old way of marking the streams with the right generic type for + static typing (does nothing on AnyIO 4) + + .. deprecated:: 4.0 + Use ``create_memory_object_stream[YourItemType](...)`` instead. + :return: a tuple of (send stream, receive stream) + + """ + + def __new__( # type: ignore[misc] + cls, max_buffer_size: float = 0, item_type: object = None + ) -> tuple[MemoryObjectSendStream[T_Item], MemoryObjectReceiveStream[T_Item]]: + if max_buffer_size != math.inf and not isinstance(max_buffer_size, int): + raise ValueError("max_buffer_size must be either an integer or math.inf") + if max_buffer_size < 0: + raise ValueError("max_buffer_size cannot be negative") + if item_type is not None: + warn( + "The item_type argument has been deprecated in AnyIO 4.0. " + "Use create_memory_object_stream[YourItemType](...) instead.", + DeprecationWarning, + stacklevel=2, + ) + + state = _MemoryObjectStreamState[T_Item](max_buffer_size) + return (MemoryObjectSendStream(state), MemoryObjectReceiveStream(state)) diff --git a/source/target-vendors/cp312/anyio/_core/_subprocesses.py b/source/target-vendors/cp312/anyio/_core/_subprocesses.py new file mode 100644 index 0000000..53184a2 --- /dev/null +++ b/source/target-vendors/cp312/anyio/_core/_subprocesses.py @@ -0,0 +1,196 @@ +from __future__ import annotations + +from collections.abc import AsyncIterable, Iterable, Mapping, Sequence +from io import BytesIO +from os import PathLike +from subprocess import PIPE, CalledProcessError, CompletedProcess +from typing import IO, Any, TypeAlias, cast + +from ..abc import Process # noqa: TC001 +from ._eventloop import get_async_backend +from ._tasks import create_task_group + +StrOrBytesPath: TypeAlias = str | bytes | PathLike[str] | PathLike[bytes] + + +async def run_process( + command: StrOrBytesPath | Sequence[StrOrBytesPath], + *, + input: bytes | None = None, + stdin: int | IO[Any] | None = None, + stdout: int | IO[Any] | None = PIPE, + stderr: int | IO[Any] | None = PIPE, + check: bool = True, + cwd: StrOrBytesPath | None = None, + env: Mapping[str, str] | None = None, + startupinfo: Any = None, + creationflags: int = 0, + start_new_session: bool = False, + pass_fds: Sequence[int] = (), + user: str | int | None = None, + group: str | int | None = None, + extra_groups: Iterable[str | int] | None = None, + umask: int = -1, +) -> CompletedProcess[bytes]: + """ + Run an external command in a subprocess and wait until it completes. + + .. seealso:: :func:`subprocess.run` + + :param command: either a string to pass to the shell, or an iterable of strings + containing the executable name or path and its arguments + :param input: bytes passed to the standard input of the subprocess + :param stdin: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, + a file-like object, or `None`; ``input`` overrides this + :param stdout: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, + a file-like object, or `None` + :param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, + :data:`subprocess.STDOUT`, a file-like object, or `None` + :param check: if ``True``, raise :exc:`~subprocess.CalledProcessError` if the + process terminates with a return code other than 0 + :param cwd: If not ``None``, change the working directory to this before running the + command + :param env: if not ``None``, this mapping replaces the inherited environment + variables from the parent process + :param startupinfo: an instance of :class:`subprocess.STARTUPINFO` that can be used + to specify process startup parameters (Windows only) + :param creationflags: flags that can be used to control the creation of the + subprocess (see :class:`subprocess.Popen` for the specifics) + :param start_new_session: if ``true`` the setsid() system call will be made in the + child process prior to the execution of the subprocess. (POSIX only) + :param pass_fds: sequence of file descriptors to keep open between the parent and + child processes. (POSIX only) + :param user: effective user to run the process as (Python >= 3.9, POSIX only) + :param group: effective group to run the process as (Python >= 3.9, POSIX only) + :param extra_groups: supplementary groups to set in the subprocess (Python >= 3.9, + POSIX only) + :param umask: if not negative, this umask is applied in the child process before + running the given command (Python >= 3.9, POSIX only) + :return: an object representing the completed process + :raises ~subprocess.CalledProcessError: if ``check`` is ``True`` and the process + exits with a nonzero return code + + """ + + async def drain_stream(stream: AsyncIterable[bytes], index: int) -> None: + buffer = BytesIO() + async for chunk in stream: + buffer.write(chunk) + + stream_contents[index] = buffer.getvalue() + + if stdin is not None and input is not None: + raise ValueError("only one of stdin and input is allowed") + + async with await open_process( + command, + stdin=PIPE if input else stdin, + stdout=stdout, + stderr=stderr, + cwd=cwd, + env=env, + startupinfo=startupinfo, + creationflags=creationflags, + start_new_session=start_new_session, + pass_fds=pass_fds, + user=user, + group=group, + extra_groups=extra_groups, + umask=umask, + ) as process: + stream_contents: list[bytes | None] = [None, None] + async with create_task_group() as tg: + if process.stdout: + tg.start_soon(drain_stream, process.stdout, 0) + + if process.stderr: + tg.start_soon(drain_stream, process.stderr, 1) + + if process.stdin and input: + await process.stdin.send(input) + await process.stdin.aclose() + + await process.wait() + + output, errors = stream_contents + if check and process.returncode != 0: + raise CalledProcessError(cast(int, process.returncode), command, output, errors) + + return CompletedProcess(command, cast(int, process.returncode), output, errors) + + +async def open_process( + command: StrOrBytesPath | Sequence[StrOrBytesPath], + *, + stdin: int | IO[Any] | None = PIPE, + stdout: int | IO[Any] | None = PIPE, + stderr: int | IO[Any] | None = PIPE, + cwd: StrOrBytesPath | None = None, + env: Mapping[str, str] | None = None, + startupinfo: Any = None, + creationflags: int = 0, + start_new_session: bool = False, + pass_fds: Sequence[int] = (), + user: str | int | None = None, + group: str | int | None = None, + extra_groups: Iterable[str | int] | None = None, + umask: int = -1, +) -> Process: + """ + Start an external command in a subprocess. + + .. seealso:: :class:`subprocess.Popen` + + :param command: either a string to pass to the shell, or an iterable of strings + containing the executable name or path and its arguments + :param stdin: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, a + file-like object, or ``None`` + :param stdout: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, + a file-like object, or ``None`` + :param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, + :data:`subprocess.STDOUT`, a file-like object, or ``None`` + :param cwd: If not ``None``, the working directory is changed before executing + :param env: If env is not ``None``, it must be a mapping that defines the + environment variables for the new process + :param creationflags: flags that can be used to control the creation of the + subprocess (see :class:`subprocess.Popen` for the specifics) + :param startupinfo: an instance of :class:`subprocess.STARTUPINFO` that can be used + to specify process startup parameters (Windows only) + :param start_new_session: if ``true`` the setsid() system call will be made in the + child process prior to the execution of the subprocess. (POSIX only) + :param pass_fds: sequence of file descriptors to keep open between the parent and + child processes. (POSIX only) + :param user: effective user to run the process as (POSIX only) + :param group: effective group to run the process as (POSIX only) + :param extra_groups: supplementary groups to set in the subprocess (POSIX only) + :param umask: if not negative, this umask is applied in the child process before + running the given command (POSIX only) + :return: an asynchronous process object + + """ + kwargs: dict[str, Any] = {} + if user is not None: + kwargs["user"] = user + + if group is not None: + kwargs["group"] = group + + if extra_groups is not None: + kwargs["extra_groups"] = extra_groups + + if umask >= 0: + kwargs["umask"] = umask + + return await get_async_backend().open_process( + command, + stdin=stdin, + stdout=stdout, + stderr=stderr, + cwd=cwd, + env=env, + startupinfo=startupinfo, + creationflags=creationflags, + start_new_session=start_new_session, + pass_fds=pass_fds, + **kwargs, + ) diff --git a/source/target-vendors/cp312/anyio/_core/_synchronization.py b/source/target-vendors/cp312/anyio/_core/_synchronization.py new file mode 100644 index 0000000..cb988c6 --- /dev/null +++ b/source/target-vendors/cp312/anyio/_core/_synchronization.py @@ -0,0 +1,778 @@ +from __future__ import annotations + +import math +import sys +from collections import deque +from collections.abc import Callable +from dataclasses import dataclass +from types import TracebackType +from typing import TypeVar + +from ..lowlevel import checkpoint_if_cancelled +from ._eventloop import get_async_backend +from ._exceptions import BusyResourceError, NoEventLoopError +from ._tasks import CancelScope +from ._testing import TaskInfo, get_current_task + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +T = TypeVar("T") + + +@dataclass(frozen=True) +class EventStatistics: + """ + :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Event.wait` + """ + + tasks_waiting: int + + +@dataclass(frozen=True) +class CapacityLimiterStatistics: + """ + :ivar int borrowed_tokens: number of tokens currently borrowed by tasks + :ivar float total_tokens: total number of available tokens + :ivar tuple borrowers: tasks or other objects currently holding tokens borrowed from + this limiter + :ivar int tasks_waiting: number of tasks waiting on + :meth:`~.CapacityLimiter.acquire` or + :meth:`~.CapacityLimiter.acquire_on_behalf_of` + """ + + borrowed_tokens: int + total_tokens: float + borrowers: tuple[object, ...] + tasks_waiting: int + + +@dataclass(frozen=True) +class LockStatistics: + """ + :ivar bool locked: flag indicating if this lock is locked or not + :ivar ~anyio.TaskInfo owner: task currently holding the lock (or ``None`` if the + lock is not held by any task) + :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Lock.acquire` + """ + + locked: bool + owner: TaskInfo | None + tasks_waiting: int + + +@dataclass(frozen=True) +class ConditionStatistics: + """ + :ivar int tasks_waiting: number of tasks blocked on :meth:`~.Condition.wait` + :ivar ~anyio.LockStatistics lock_statistics: statistics of the underlying + :class:`~.Lock` + """ + + tasks_waiting: int + lock_statistics: LockStatistics + + +@dataclass(frozen=True) +class SemaphoreStatistics: + """ + :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Semaphore.acquire` + + """ + + tasks_waiting: int + + +class Event: + __slots__ = ("__weakref__",) + + def __new__(cls) -> Event: + try: + return get_async_backend().create_event() + except NoEventLoopError: + return EventAdapter() + + def set(self) -> None: + """Set the flag, notifying all listeners.""" + raise NotImplementedError + + def is_set(self) -> bool: + """Return ``True`` if the flag is set, ``False`` if not.""" + raise NotImplementedError + + async def wait(self) -> None: + """ + Wait until the flag has been set. + + If the flag has already been set when this method is called, it returns + immediately. + + """ + raise NotImplementedError + + def statistics(self) -> EventStatistics: + """Return statistics about the current state of this event.""" + raise NotImplementedError + + +class EventAdapter(Event): + __slots__ = "_internal_event", "_is_set" + + def __new__(cls) -> Self: + return object.__new__(cls) + + def __init__(self) -> None: + self._internal_event: Event | None = None + self._is_set = False + + @property + def _event(self) -> Event: + if self._internal_event is None: + self._internal_event = get_async_backend().create_event() + if self._is_set: + self._internal_event.set() + + return self._internal_event + + def set(self) -> None: + if self._internal_event is None: + self._is_set = True + else: + self._event.set() + + def is_set(self) -> bool: + if self._internal_event is None: + return self._is_set + + return self._internal_event.is_set() + + async def wait(self) -> None: + await self._event.wait() + + def statistics(self) -> EventStatistics: + if self._internal_event is None: + return EventStatistics(tasks_waiting=0) + + return self._internal_event.statistics() + + +class Lock: + __slots__ = ("__weakref__",) + + def __new__(cls, *, fast_acquire: bool = False) -> Lock: + try: + return get_async_backend().create_lock(fast_acquire=fast_acquire) + except NoEventLoopError: + return LockAdapter(fast_acquire=fast_acquire) + + async def __aenter__(self) -> None: + await self.acquire() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.release() + + async def acquire(self) -> None: + """Acquire the lock.""" + raise NotImplementedError + + def acquire_nowait(self) -> None: + """ + Acquire the lock, without blocking. + + :raises ~anyio.WouldBlock: if the operation would block + + """ + raise NotImplementedError + + def release(self) -> None: + """Release the lock.""" + raise NotImplementedError + + def locked(self) -> bool: + """Return True if the lock is currently held.""" + raise NotImplementedError + + def statistics(self) -> LockStatistics: + """ + Return statistics about the current state of this lock. + + .. versionadded:: 3.0 + """ + raise NotImplementedError + + +class LockAdapter(Lock): + __slots__ = "_fast_acquire", "_internal_lock" + + def __new__(cls, *, fast_acquire: bool = False) -> Self: + return object.__new__(cls) + + def __init__(self, *, fast_acquire: bool = False): + self._internal_lock: Lock | None = None + self._fast_acquire = fast_acquire + + @property + def _lock(self) -> Lock: + if self._internal_lock is None: + self._internal_lock = get_async_backend().create_lock( + fast_acquire=self._fast_acquire + ) + + return self._internal_lock + + async def __aenter__(self) -> None: + await self._lock.acquire() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + if self._internal_lock is not None: + self._internal_lock.release() + + async def acquire(self) -> None: + """Acquire the lock.""" + await self._lock.acquire() + + def acquire_nowait(self) -> None: + """ + Acquire the lock, without blocking. + + :raises ~anyio.WouldBlock: if the operation would block + + """ + self._lock.acquire_nowait() + + def release(self) -> None: + """Release the lock.""" + self._lock.release() + + def locked(self) -> bool: + """Return True if the lock is currently held.""" + return self._lock.locked() + + def statistics(self) -> LockStatistics: + """ + Return statistics about the current state of this lock. + + .. versionadded:: 3.0 + + """ + if self._internal_lock is None: + return LockStatistics(False, None, 0) + + return self._internal_lock.statistics() + + +class Condition: + __slots__ = "__weakref__", "_lock", "_owner_task", "_waiters" + + def __init__(self, lock: Lock | None = None): + self._owner_task: TaskInfo | None = None + self._lock = lock or Lock() + self._waiters: deque[Event] = deque() + + async def __aenter__(self) -> None: + await self.acquire() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.release() + + def _check_acquired(self) -> None: + if self._owner_task != get_current_task(): + raise RuntimeError("The current task is not holding the underlying lock") + + async def acquire(self) -> None: + """Acquire the underlying lock.""" + await self._lock.acquire() + self._owner_task = get_current_task() + + def acquire_nowait(self) -> None: + """ + Acquire the underlying lock, without blocking. + + :raises ~anyio.WouldBlock: if the operation would block + + """ + self._lock.acquire_nowait() + self._owner_task = get_current_task() + + def release(self) -> None: + """Release the underlying lock.""" + self._lock.release() + + def locked(self) -> bool: + """Return True if the lock is set.""" + return self._lock.locked() + + def notify(self, n: int = 1) -> None: + """Notify exactly n listeners.""" + self._check_acquired() + for _ in range(n): + try: + event = self._waiters.popleft() + except IndexError: + break + + event.set() + + def notify_all(self) -> None: + """Notify all the listeners.""" + self._check_acquired() + for event in self._waiters: + event.set() + + self._waiters.clear() + + async def wait(self) -> None: + """Wait for a notification.""" + await checkpoint_if_cancelled() + self._check_acquired() + event = Event() + self._waiters.append(event) + self.release() + try: + await event.wait() + except BaseException: + if not event.is_set(): + self._waiters.remove(event) + elif self._waiters: + # This task was notified by could not act on it, so pass + # it on to the next task + self._waiters.popleft().set() + + raise + finally: + with CancelScope(shield=True): + await self.acquire() + + async def wait_for(self, predicate: Callable[[], T]) -> T: + """ + Wait until a predicate becomes true. + + :param predicate: a callable that returns a truthy value when the condition is + met + :return: the result of the predicate + + .. versionadded:: 4.11.0 + + """ + while not (result := predicate()): + await self.wait() + + return result + + def statistics(self) -> ConditionStatistics: + """ + Return statistics about the current state of this condition. + + .. versionadded:: 3.0 + """ + return ConditionStatistics(len(self._waiters), self._lock.statistics()) + + +class Semaphore: + __slots__ = "__weakref__", "_fast_acquire" + + def __new__( + cls, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> Semaphore: + try: + return get_async_backend().create_semaphore( + initial_value, max_value=max_value, fast_acquire=fast_acquire + ) + except NoEventLoopError: + return SemaphoreAdapter(initial_value, max_value=max_value) + + def __init__( + self, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ): + if not isinstance(initial_value, int): + raise TypeError("initial_value must be an integer") + if initial_value < 0: + raise ValueError("initial_value must be >= 0") + if max_value is not None: + if not isinstance(max_value, int): + raise TypeError("max_value must be an integer or None") + if max_value < initial_value: + raise ValueError( + "max_value must be equal to or higher than initial_value" + ) + + self._fast_acquire = fast_acquire + + async def __aenter__(self) -> Self: + await self.acquire() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.release() + + async def acquire(self) -> None: + """Decrement the semaphore value, blocking if necessary.""" + raise NotImplementedError + + def acquire_nowait(self) -> None: + """ + Acquire the underlying lock, without blocking. + + :raises ~anyio.WouldBlock: if the operation would block + + """ + raise NotImplementedError + + def release(self) -> None: + """Increment the semaphore value.""" + raise NotImplementedError + + @property + def value(self) -> int: + """The current value of the semaphore.""" + raise NotImplementedError + + @property + def max_value(self) -> int | None: + """The maximum value of the semaphore.""" + raise NotImplementedError + + def statistics(self) -> SemaphoreStatistics: + """ + Return statistics about the current state of this semaphore. + + .. versionadded:: 3.0 + """ + raise NotImplementedError + + +class SemaphoreAdapter(Semaphore): + __slots__ = "_initial_value", "_internal_semaphore", "_max_value" + + def __new__( + cls, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> Self: + return object.__new__(cls) + + def __init__( + self, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> None: + super().__init__(initial_value, max_value=max_value, fast_acquire=fast_acquire) + self._internal_semaphore: Semaphore | None = None + self._initial_value = initial_value + self._max_value = max_value + + @property + def _semaphore(self) -> Semaphore: + if self._internal_semaphore is None: + self._internal_semaphore = get_async_backend().create_semaphore( + self._initial_value, max_value=self._max_value + ) + + return self._internal_semaphore + + async def acquire(self) -> None: + await self._semaphore.acquire() + + def acquire_nowait(self) -> None: + self._semaphore.acquire_nowait() + + def release(self) -> None: + self._semaphore.release() + + @property + def value(self) -> int: + if self._internal_semaphore is None: + return self._initial_value + + return self._semaphore.value + + @property + def max_value(self) -> int | None: + return self._max_value + + def statistics(self) -> SemaphoreStatistics: + if self._internal_semaphore is None: + return SemaphoreStatistics(tasks_waiting=0) + + return self._semaphore.statistics() + + +class CapacityLimiter: + __slots__ = ("__weakref__",) + + def __new__(cls, total_tokens: float) -> CapacityLimiter: + try: + return get_async_backend().create_capacity_limiter(total_tokens) + except NoEventLoopError: + return CapacityLimiterAdapter(total_tokens) + + async def __aenter__(self) -> None: + raise NotImplementedError + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + raise NotImplementedError + + @property + def total_tokens(self) -> float: + """ + The total number of tokens available for borrowing. + + This is a read-write property. If the total number of tokens is increased, the + proportionate number of tasks waiting on this limiter will be granted their + tokens. + + .. versionchanged:: 3.0 + The property is now writable. + .. versionchanged:: 4.12 + The value can now be set to 0. + + """ + raise NotImplementedError + + @total_tokens.setter + def total_tokens(self, value: float) -> None: + raise NotImplementedError + + @property + def borrowed_tokens(self) -> int: + """The number of tokens that have currently been borrowed.""" + raise NotImplementedError + + @property + def available_tokens(self) -> float: + """The number of tokens currently available to be borrowed""" + raise NotImplementedError + + def acquire_nowait(self) -> None: + """ + Acquire a token for the current task without waiting for one to become + available. + + :raises ~anyio.WouldBlock: if there are no tokens available for borrowing + + """ + raise NotImplementedError + + def acquire_on_behalf_of_nowait(self, borrower: object) -> None: + """ + Acquire a token without waiting for one to become available. + + :param borrower: the entity borrowing a token + :raises ~anyio.WouldBlock: if there are no tokens available for borrowing + + """ + raise NotImplementedError + + async def acquire(self) -> None: + """ + Acquire a token for the current task, waiting if necessary for one to become + available. + + """ + raise NotImplementedError + + async def acquire_on_behalf_of(self, borrower: object) -> None: + """ + Acquire a token, waiting if necessary for one to become available. + + :param borrower: the entity borrowing a token + + """ + raise NotImplementedError + + def release(self) -> None: + """ + Release the token held by the current task. + + :raises RuntimeError: if the current task has not borrowed a token from this + limiter. + + """ + raise NotImplementedError + + def release_on_behalf_of(self, borrower: object) -> None: + """ + Release the token held by the given borrower. + + :raises RuntimeError: if the borrower has not borrowed a token from this + limiter. + + """ + raise NotImplementedError + + def statistics(self) -> CapacityLimiterStatistics: + """ + Return statistics about the current state of this limiter. + + .. versionadded:: 3.0 + + """ + raise NotImplementedError + + +class CapacityLimiterAdapter(CapacityLimiter): + __slots__ = "_internal_limiter", "_total_tokens" + + def __new__(cls, total_tokens: float) -> Self: + return object.__new__(cls) + + def __init__(self, total_tokens: float) -> None: + self._internal_limiter: CapacityLimiter | None = None + self.total_tokens = total_tokens + + @property + def _limiter(self) -> CapacityLimiter: + if self._internal_limiter is None: + self._internal_limiter = get_async_backend().create_capacity_limiter( + self._total_tokens + ) + + return self._internal_limiter + + async def __aenter__(self) -> None: + await self._limiter.__aenter__() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + return await self._limiter.__aexit__(exc_type, exc_val, exc_tb) + + @property + def total_tokens(self) -> float: + if self._internal_limiter is None: + return self._total_tokens + + return self._internal_limiter.total_tokens + + @total_tokens.setter + def total_tokens(self, value: float) -> None: + if not isinstance(value, int) and not math.isinf(value): + raise TypeError("total_tokens must be an int or math.inf") + elif value < 0: + raise ValueError("total_tokens must be >= 0") + + if self._internal_limiter is None: + self._total_tokens = value + return + + self._limiter.total_tokens = value + + @property + def borrowed_tokens(self) -> int: + if self._internal_limiter is None: + return 0 + + return self._internal_limiter.borrowed_tokens + + @property + def available_tokens(self) -> float: + if self._internal_limiter is None: + return self._total_tokens + + return self._internal_limiter.available_tokens + + def acquire_nowait(self) -> None: + self._limiter.acquire_nowait() + + def acquire_on_behalf_of_nowait(self, borrower: object) -> None: + self._limiter.acquire_on_behalf_of_nowait(borrower) + + async def acquire(self) -> None: + await self._limiter.acquire() + + async def acquire_on_behalf_of(self, borrower: object) -> None: + await self._limiter.acquire_on_behalf_of(borrower) + + def release(self) -> None: + self._limiter.release() + + def release_on_behalf_of(self, borrower: object) -> None: + self._limiter.release_on_behalf_of(borrower) + + def statistics(self) -> CapacityLimiterStatistics: + if self._internal_limiter is None: + return CapacityLimiterStatistics( + borrowed_tokens=0, + total_tokens=self.total_tokens, + borrowers=(), + tasks_waiting=0, + ) + + return self._internal_limiter.statistics() + + +class ResourceGuard: + """ + A context manager for ensuring that a resource is only used by a single task at a + time. + + Entering this context manager while the previous has not exited it yet will trigger + :exc:`BusyResourceError`. + + :param action: the action to guard against (visible in the :exc:`BusyResourceError` + when triggered, e.g. "Another task is already {action} this resource") + + .. versionadded:: 4.1 + """ + + __slots__ = "__weakref__", "_guarded", "action" + + def __init__(self, action: str = "using"): + self.action: str = action + self._guarded = False + + def __enter__(self) -> None: + if self._guarded: + raise BusyResourceError(self.action) + + self._guarded = True + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self._guarded = False diff --git a/source/target-vendors/cp312/anyio/_core/_tasks.py b/source/target-vendors/cp312/anyio/_core/_tasks.py new file mode 100644 index 0000000..044eda6 --- /dev/null +++ b/source/target-vendors/cp312/anyio/_core/_tasks.py @@ -0,0 +1,464 @@ +from __future__ import annotations + +import math +import sys +from collections.abc import ( + Coroutine, + Generator, +) +from contextlib import contextmanager +from enum import Enum, auto +from inspect import iscoroutine +from types import TracebackType +from typing import Any, Generic, final + +from ..abc import TaskGroup, TaskStatus +from ._eventloop import get_async_backend, get_cancelled_exc_class +from ._exceptions import TaskCancelled, TaskFailed, TaskNotFinished + +if sys.version_info >= (3, 13): + from typing import TypeVar +else: + from typing_extensions import TypeVar + +if sys.version_info >= (3, 11): + from typing import Never, Self, TypeVarTuple +else: + from typing_extensions import Never, Self, TypeVarTuple + +T = TypeVar("T") +T_co = TypeVar("T_co", covariant=True) +T_startval_co = TypeVar("T_startval_co", covariant=True, default=Never) +PosArgsT = TypeVarTuple("PosArgsT") + + +class _IgnoredTaskStatus(TaskStatus[object]): + def started(self, value: object = None) -> None: + pass + + +TASK_STATUS_IGNORED = _IgnoredTaskStatus() + + +class CancelScope: + """ + Wraps a unit of work that can be made separately cancellable. + + :param deadline: The time (clock value) when this scope is cancelled automatically + :param shield: ``True`` to shield the cancel scope from external cancellation + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + """ + + __slots__ = ("__weakref__",) + + def __new__( + cls, *, deadline: float = math.inf, shield: bool = False + ) -> CancelScope: + return get_async_backend().create_cancel_scope(shield=shield, deadline=deadline) + + def cancel(self, reason: str | None = None) -> None: + """ + Cancel this scope immediately. + + :param reason: a message describing the reason for the cancellation + + """ + raise NotImplementedError + + @property + def deadline(self) -> float: + """ + The time (clock value) when this scope is cancelled automatically. + + Will be ``float('inf')`` if no timeout has been set. + + """ + raise NotImplementedError + + @deadline.setter + def deadline(self, value: float) -> None: + raise NotImplementedError + + @property + def cancel_called(self) -> bool: + """``True`` if :meth:`cancel` has been called.""" + raise NotImplementedError + + @property + def cancelled_caught(self) -> bool: + """ + ``True`` if this scope suppressed a cancellation exception it itself raised. + + This is typically used to check if any work was interrupted, or to see if the + scope was cancelled due to its deadline being reached. The value will, however, + only be ``True`` if the cancellation was triggered by the scope itself (and not + an outer scope). + + """ + raise NotImplementedError + + @property + def shield(self) -> bool: + """ + ``True`` if this scope is shielded from external cancellation. + + While a scope is shielded, it will not receive cancellations from outside. + + """ + raise NotImplementedError + + @shield.setter + def shield(self, value: bool) -> None: + raise NotImplementedError + + def __enter__(self) -> Self: + raise NotImplementedError + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + raise NotImplementedError + + +@contextmanager +def fail_at( + deadline: float | None, shield: bool = False, reason: str | None = None +) -> Generator[CancelScope, None, None]: + """ + Create a context manager which raises a :class:`TimeoutError` if the code in the + enclosing context does not finish by the given deadline. + + :param deadline: the deadline before raising the exception, or + ``None`` to disable the timeout + :param shield: ``True`` to shield the cancel scope from external cancellation + :param reason: explanation for timeout to add to the message of a raised `TimeoutError` + :return: a context manager that yields a cancel scope + :rtype: :class:`~typing.ContextManager`\\[:class:`~anyio.CancelScope`\\] + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + .. versionadded:: 4.15.0 + + """ + current_time = get_async_backend().current_time + effective_deadline = math.inf if deadline is None else deadline + with get_async_backend().create_cancel_scope( + deadline=effective_deadline, shield=shield + ) as cancel_scope: + yield cancel_scope + + if cancel_scope.cancelled_caught and current_time() >= cancel_scope.deadline: + raise TimeoutError(reason) if reason else TimeoutError + + +@contextmanager +def fail_after( + delay: float | None, shield: bool = False, reason: str | None = None +) -> Generator[CancelScope, None, None]: + """ + Create a context manager which raises a :class:`TimeoutError` if the code in the + enclosing context block does not finish in time. + + :param delay: maximum allowed time (in seconds) before raising the exception, or + ``None`` to disable the timeout + :param shield: ``True`` to shield the cancel scope from external cancellation + :param reason: explanation for timeout to add to the message of a raised `TimeoutError` + :return: a context manager that yields a cancel scope + :rtype: :class:`~typing.ContextManager`\\[:class:`~anyio.CancelScope`\\] + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + current_time = get_async_backend().current_time + deadline = (current_time() + delay) if delay is not None else math.inf + with fail_at(deadline, shield=shield, reason=reason) as scope: + yield scope + + +def move_on_at(deadline: float | None, shield: bool = False) -> CancelScope: + """ + Create a cancel scope with a deadline that expires after the given delay. + + :param deadline: the deadline before exiting the context block, or + ``None`` to disable the timeout + :param shield: ``True`` to shield the cancel scope from external cancellation + :return: a cancel scope + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + .. versionadded:: 4.15.0 + + """ + return get_async_backend().create_cancel_scope( + deadline=deadline if deadline is not None else math.inf, shield=shield + ) + + +def move_on_after(delay: float | None, shield: bool = False) -> CancelScope: + """ + Create a cancel scope with a deadline that expires after the given delay. + + :param delay: maximum allowed time (in seconds) before exiting the context block, or + ``None`` to disable the timeout + :param shield: ``True`` to shield the cancel scope from external cancellation + :return: a cancel scope + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + .. note:: Unlike with :func:`fail_after`, the timer starts when this function is + called, not when the context manager is entered. This will be changed in v5.0. + + """ + deadline = ( + (get_async_backend().current_time() + delay) if delay is not None else math.inf + ) + return get_async_backend().create_cancel_scope(deadline=deadline, shield=shield) + + +def current_effective_deadline() -> float: + """ + Return the nearest deadline among all the cancel scopes effective for the current + task. + + :return: a clock value from the event loop's internal clock (or ``float('inf')`` if + there is no deadline in effect, or ``float('-inf')`` if the current scope has + been cancelled) + :rtype: float + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().current_effective_deadline() + + +def create_task_group() -> TaskGroup: + """ + Create a task group. + + :return: a task group + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().create_task_group() + + +@final +class TaskHandle(Generic[T_co, T_startval_co]): + """ + Returned from the task-spawning methods of :class:`TaskGroup`. Can be awaited on to + get the return value of the task (or the raised exception). If the task was + terminated by a :exc:`BaseException`, :exc:`TaskFailed` will be raised (or its + subclass :exc:`TaskCancelled` if the task was cancelled). + + .. versionadded:: 4.14.0 + """ + + class Status(Enum): + """ + The status of a task handle. + + .. attribute:: PENDING + + The task has not finished yet. + .. attribute:: FINISHED + + The task has finished with a return value. + .. attribute:: CANCELLING + + The task has been cancelled but has not finished yet. + .. attribute:: CANCELLED + + The task was cancelled and has finished since. + .. attribute:: FAILED + + The task raised an exception. + """ + + PENDING = auto() + FINISHED = auto() + CANCELLING = auto() + CANCELLED = auto() + FAILED = auto() + + __slots__ = ( + "__weakref__", + "_cancel_scope", + "_coro", + "_exception", + "_finished_event", + "_name", + "_return_value", + "_start_value", + ) + + _return_value: T_co + _start_value: T_startval_co + + def __init__( + self, + coro: Coroutine[Any, Any, T_co], + name: object, + cancel_scope: CancelScope | None = None, + ) -> None: + from ._synchronization import Event + + self._coro = coro + self._cancel_scope = cancel_scope if cancel_scope is not None else CancelScope() + self._finished_event = Event() + self._exception: BaseException | None = None + + if name is not None: + self._name = str(name) + elif iscoroutine(coro): + self._name = coro.__qualname__ + else: + self._name = str(coro) # coroutine-like object (e.g. asend() objects) + + async def _run_coro(self) -> None: + __tracebackhide__ = True + + with self._cancel_scope: + try: + retval = await self._coro + except BaseException as exc: + self._exception = exc + raise + else: + self._return_value = retval + finally: + self._finished_event.set() + del self # Break the reference cycle + + def cancel(self) -> None: + """ + Set the task to a cancelled state. + + This will interrupt any interruptible asynchronous operation, and will cause + any further awaits on this task to get immediately cancelled, unless done in + a shielded cancel scope. + + If the task has already finished, this method has no effect. + """ + if not self._finished_event.is_set(): + self._cancel_scope.cancel() + + @property + def coro(self) -> Coroutine[Any, Any, T_co]: + """ + The coroutine object that was passed to one of the task-spawning methods in + :class:`TaskGroup`. + """ + return self._coro + + @property + def status(self) -> TaskHandle.Status: + """ + The current status of the task. + + Every task starts in the :attr:`~TaskHandle.Status.PENDING` state. + If a task is cancelled while in this state, it will transition to the + :attr:`~TaskHandle.Status.CANCELLING` state. When the task finishes, it will + transition to one of the three final states ( + :attr:`~TaskHandle.Status.FINISHED`, :attr:`~TaskHandle.Status.FAILED`, or + :attr:`~TaskHandle.Status.CANCELLING`) depending on the exception the task + raised, if any. No other status transitions will happen. + """ + if not self._finished_event.is_set(): + if self._cancel_scope.cancel_called: + return TaskHandle.Status.CANCELLING + else: + return TaskHandle.Status.PENDING + elif self._exception is not None: + if isinstance(self._exception, get_cancelled_exc_class()): + return TaskHandle.Status.CANCELLED + else: + return TaskHandle.Status.FAILED + else: + return TaskHandle.Status.FINISHED + + @property + def name(self) -> str: + """The name of the task.""" + return self._name + + @property + def exception(self) -> BaseException | None: + """ + The exception raised by the task, or ``None`` if it finished without raising. + + :raises TaskNotFinished: if the task has not finished yet + :raises TaskCancelled: if the task was cancelled + + """ + match self.status: + case TaskHandle.Status.PENDING: + raise TaskNotFinished("the task has not finished yet") + case TaskHandle.Status.FINISHED: + return None + case TaskHandle.Status.CANCELLING: + raise TaskCancelled("the task was cancelled") + case TaskHandle.Status.CANCELLED: + raise TaskCancelled("the task was cancelled") from self._exception + case TaskHandle.Status.FAILED: + return self._exception + + @property + def return_value(self) -> T_co: + """ + The return value of the task. + + :raises TaskNotFinished: if the task has not finished yet + :raises TaskCancelled: if the task was cancelled + :raises TaskFailed: if the task raised an exception + + """ + match self.status: + case TaskHandle.Status.PENDING: + raise TaskNotFinished("the task has not finished yet") + case TaskHandle.Status.FINISHED: + return self._return_value + case TaskHandle.Status.CANCELLING: + raise TaskCancelled("the task was cancelled") + case TaskHandle.Status.CANCELLED: + raise TaskCancelled("the task was cancelled") from self._exception + case TaskHandle.Status.FAILED: + raise TaskFailed("the task raised an exception") from self._exception + + @property + def start_value(self) -> T_startval_co: + """ + The value passed to :meth:`task_status.started() <.abc.TaskStatus.started>`, + + :raises RuntimeError: if the task was not started with :meth:`TaskGroup.start() + <.abc.TaskGroup.start>` + """ + try: + return self._start_value + except AttributeError: + raise RuntimeError( + "the task was not started with TaskGroup.start()" + ) from None + + async def wait(self) -> None: + """ + Wait for the task to finish. + + This method will return as soon as the task has finished, no matter how it + happened. + """ + await self._finished_event.wait() + + def __await__(self) -> Generator[Any, Any, T_co]: + yield from self._finished_event.wait().__await__() + return self.return_value + + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__} {self.status.name.lower()} " + f"name={self._name!r} coro={self._coro!r}>" + ) diff --git a/source/target-vendors/cp312/anyio/_core/_tempfile.py b/source/target-vendors/cp312/anyio/_core/_tempfile.py new file mode 100644 index 0000000..9603086 --- /dev/null +++ b/source/target-vendors/cp312/anyio/_core/_tempfile.py @@ -0,0 +1,615 @@ +from __future__ import annotations + +import os +import sys +import tempfile +from collections.abc import Iterable +from io import BytesIO, TextIOWrapper +from types import TracebackType +from typing import ( + TYPE_CHECKING, + Any, + AnyStr, + Generic, + overload, +) + +from .. import to_thread +from .._core._fileio import AsyncFile +from ..lowlevel import checkpoint_if_cancelled +from ._tasks import CancelScope + +if TYPE_CHECKING: + from _typeshed import OpenBinaryMode, OpenTextMode, ReadableBuffer, WriteableBuffer + + +class TemporaryFile(Generic[AnyStr]): + """ + An asynchronous temporary file that is automatically created and cleaned up. + + This class provides an asynchronous context manager interface to a temporary file. + The file is created using Python's standard `tempfile.TemporaryFile` function in a + background thread, and is wrapped as an asynchronous file using `AsyncFile`. + + :param mode: The mode in which the file is opened. Defaults to "w+b". + :param buffering: The buffering policy (-1 means the default buffering). + :param encoding: The encoding used to decode or encode the file. Only applicable in + text mode. + :param newline: Controls how universal newlines mode works (only applicable in text + mode). + :param suffix: The suffix for the temporary file name. + :param prefix: The prefix for the temporary file name. + :param dir: The directory in which the temporary file is created. + :param errors: The error handling scheme used for encoding/decoding errors. + """ + + _async_file: AsyncFile[AnyStr] + + @overload + def __init__( + self: TemporaryFile[bytes], + mode: OpenBinaryMode = ..., + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + *, + errors: str | None = ..., + ): ... + @overload + def __init__( + self: TemporaryFile[str], + mode: OpenTextMode, + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + *, + errors: str | None = ..., + ): ... + + def __init__( + self, + mode: OpenTextMode | OpenBinaryMode = "w+b", + buffering: int = -1, + encoding: str | None = None, + newline: str | None = None, + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, + *, + errors: str | None = None, + ) -> None: + self.mode = mode + self.buffering = buffering + self.encoding = encoding + self.newline = newline + self.suffix: str | None = suffix + self.prefix: str | None = prefix + self.dir: str | None = dir + self.errors = errors + + async def __aenter__(self) -> AsyncFile[AnyStr]: + fp = await to_thread.run_sync( + lambda: tempfile.TemporaryFile( + self.mode, + self.buffering, + self.encoding, + self.newline, + self.suffix, + self.prefix, + self.dir, + errors=self.errors, + ) + ) + self._async_file = AsyncFile(fp) + return self._async_file + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + await self._async_file.aclose() + + +class NamedTemporaryFile(Generic[AnyStr]): + """ + An asynchronous named temporary file that is automatically created and cleaned up. + + This class provides an asynchronous context manager for a temporary file with a + visible name in the file system. It uses Python's standard + :func:`~tempfile.NamedTemporaryFile` function and wraps the file object with + :class:`AsyncFile` for asynchronous operations. + + :param mode: The mode in which the file is opened. Defaults to "w+b". + :param buffering: The buffering policy (-1 means the default buffering). + :param encoding: The encoding used to decode or encode the file. Only applicable in + text mode. + :param newline: Controls how universal newlines mode works (only applicable in text + mode). + :param suffix: The suffix for the temporary file name. + :param prefix: The prefix for the temporary file name. + :param dir: The directory in which the temporary file is created. + :param delete: Whether to delete the file when it is closed. + :param errors: The error handling scheme used for encoding/decoding errors. + :param delete_on_close: (Python 3.12+) Whether to delete the file on close. + """ + + _async_file: AsyncFile[AnyStr] + + @overload + def __init__( + self: NamedTemporaryFile[bytes], + mode: OpenBinaryMode = ..., + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + delete: bool = ..., + *, + errors: str | None = ..., + delete_on_close: bool = ..., + ): ... + @overload + def __init__( + self: NamedTemporaryFile[str], + mode: OpenTextMode, + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + delete: bool = ..., + *, + errors: str | None = ..., + delete_on_close: bool = ..., + ): ... + + def __init__( + self, + mode: OpenBinaryMode | OpenTextMode = "w+b", + buffering: int = -1, + encoding: str | None = None, + newline: str | None = None, + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, + delete: bool = True, + *, + errors: str | None = None, + delete_on_close: bool = True, + ) -> None: + self._params: dict[str, Any] = { + "mode": mode, + "buffering": buffering, + "encoding": encoding, + "newline": newline, + "suffix": suffix, + "prefix": prefix, + "dir": dir, + "delete": delete, + "errors": errors, + } + if sys.version_info >= (3, 12): + self._params["delete_on_close"] = delete_on_close + + async def __aenter__(self) -> AsyncFile[AnyStr]: + fp = await to_thread.run_sync( + lambda: tempfile.NamedTemporaryFile(**self._params) + ) + self._async_file = AsyncFile(fp) + return self._async_file + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + await self._async_file.aclose() + + +class SpooledTemporaryFile(AsyncFile[AnyStr]): + """ + An asynchronous spooled temporary file that starts in memory and is spooled to disk. + + This class provides an asynchronous interface to a spooled temporary file, much like + Python's standard :class:`~tempfile.SpooledTemporaryFile`. It supports asynchronous + write operations and provides a method to force a rollover to disk. + + :param max_size: Maximum size in bytes before the file is rolled over to disk. + :param mode: The mode in which the file is opened. Defaults to "w+b". + :param buffering: The buffering policy (-1 means the default buffering). + :param encoding: The encoding used to decode or encode the file (text mode only). + :param newline: Controls how universal newlines mode works (text mode only). + :param suffix: The suffix for the temporary file name. + :param prefix: The prefix for the temporary file name. + :param dir: The directory in which the temporary file is created. + :param errors: The error handling scheme used for encoding/decoding errors. + """ + + _rolled: bool = False + + @overload + def __init__( + self: SpooledTemporaryFile[bytes], + max_size: int = ..., + mode: OpenBinaryMode = ..., + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + *, + errors: str | None = ..., + ): ... + @overload + def __init__( + self: SpooledTemporaryFile[str], + max_size: int = ..., + mode: OpenTextMode = ..., + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + *, + errors: str | None = ..., + ): ... + + def __init__( + self, + max_size: int = 0, + mode: OpenBinaryMode | OpenTextMode = "w+b", + buffering: int = -1, + encoding: str | None = None, + newline: str | None = None, + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, + *, + errors: str | None = None, + ) -> None: + self._tempfile_params: dict[str, Any] = { + "mode": mode, + "buffering": buffering, + "encoding": encoding, + "newline": newline, + "suffix": suffix, + "prefix": prefix, + "dir": dir, + "errors": errors, + } + self._max_size = max_size + if "b" in mode: + super().__init__(BytesIO()) # type: ignore[arg-type] + else: + super().__init__( + TextIOWrapper( # type: ignore[arg-type] + BytesIO(), + encoding=encoding, + errors=errors, + newline=newline, + write_through=True, + ) + ) + + async def aclose(self) -> None: + if not self._rolled: + self._fp.close() + return + + await super().aclose() + + async def _check(self) -> None: + if self._rolled or self._fp.tell() <= self._max_size: + return + + await self.rollover() + + async def rollover(self) -> None: + if self._rolled: + return + + self._rolled = True + buffer = self._fp + buffer.seek(0) + self._fp = await to_thread.run_sync( + lambda: tempfile.TemporaryFile(**self._tempfile_params) + ) + await self.write(buffer.read()) + buffer.close() + + @property + def closed(self) -> bool: + return self._fp.closed + + async def read(self, size: int = -1) -> AnyStr: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.read(size) + + return await super().read(size) # type: ignore[return-value] + + async def read1(self: SpooledTemporaryFile[bytes], size: int = -1) -> bytes: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.read1(size) + + return await super().read1(size) + + async def readline(self) -> AnyStr: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.readline() + + return await super().readline() # type: ignore[return-value] + + async def readlines(self) -> list[AnyStr]: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.readlines() + + return await super().readlines() # type: ignore[return-value] + + async def readinto(self: SpooledTemporaryFile[bytes], b: WriteableBuffer) -> int: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.readinto(b) + + return await super().readinto(b) + + async def readinto1(self: SpooledTemporaryFile[bytes], b: WriteableBuffer) -> int: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.readinto1(b) + + return await super().readinto1(b) + + async def seek(self, offset: int, whence: int | None = os.SEEK_SET) -> int: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.seek(offset, whence) + + return await super().seek(offset, whence) + + async def tell(self) -> int: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.tell() + + return await super().tell() + + async def truncate(self, size: int | None = None) -> int: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.truncate(size) + + return await super().truncate(size) + + @overload + async def write(self: SpooledTemporaryFile[bytes], b: ReadableBuffer) -> int: ... + @overload + async def write(self: SpooledTemporaryFile[str], b: str) -> int: ... + + async def write(self, b: ReadableBuffer | str) -> int: + """ + Asynchronously write data to the spooled temporary file. + + If the file has not yet been rolled over, the data is written synchronously, + and a rollover is triggered if the size exceeds the maximum size. + + :param s: The data to write. + :return: The number of bytes written. + :raises RuntimeError: If the underlying file is not initialized. + + """ + if not self._rolled: + await checkpoint_if_cancelled() + result = self._fp.write(b) + await self._check() + return result + + return await super().write(b) # type: ignore[misc] + + @overload + async def writelines( + self: SpooledTemporaryFile[bytes], lines: Iterable[ReadableBuffer] + ) -> None: ... + @overload + async def writelines( + self: SpooledTemporaryFile[str], lines: Iterable[str] + ) -> None: ... + + async def writelines(self, lines: Iterable[str] | Iterable[ReadableBuffer]) -> None: + """ + Asynchronously write a list of lines to the spooled temporary file. + + If the file has not yet been rolled over, the lines are written synchronously, + and a rollover is triggered if the size exceeds the maximum size. + + :param lines: An iterable of lines to write. + :raises RuntimeError: If the underlying file is not initialized. + + """ + if not self._rolled: + await checkpoint_if_cancelled() + result = self._fp.writelines(lines) + await self._check() + return result + + return await super().writelines(lines) # type: ignore[misc] + + +class TemporaryDirectory(Generic[AnyStr]): + """ + An asynchronous temporary directory that is created and cleaned up automatically. + + This class provides an asynchronous context manager for creating a temporary + directory. It wraps Python's standard :class:`~tempfile.TemporaryDirectory` to + perform directory creation and cleanup operations in a background thread. + + :param suffix: Suffix to be added to the temporary directory name. + :param prefix: Prefix to be added to the temporary directory name. + :param dir: The parent directory where the temporary directory is created. + :param ignore_cleanup_errors: Whether to ignore errors during cleanup + :param delete: Whether to delete the directory upon closing (Python 3.12+). + """ + + def __init__( + self, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: AnyStr | None = None, + *, + ignore_cleanup_errors: bool = False, + delete: bool = True, + ) -> None: + self.suffix: AnyStr | None = suffix + self.prefix: AnyStr | None = prefix + self.dir: AnyStr | None = dir + self.ignore_cleanup_errors = ignore_cleanup_errors + self.delete = delete + + self._tempdir: tempfile.TemporaryDirectory | None = None + + async def __aenter__(self) -> str: + params: dict[str, Any] = { + "suffix": self.suffix, + "prefix": self.prefix, + "dir": self.dir, + "ignore_cleanup_errors": self.ignore_cleanup_errors, + } + if sys.version_info >= (3, 12): + params["delete"] = self.delete + + self._tempdir = await to_thread.run_sync( + lambda: tempfile.TemporaryDirectory(**params) + ) + return await to_thread.run_sync(self._tempdir.__enter__) + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + if self._tempdir is not None: + with CancelScope(shield=True): + await to_thread.run_sync( + self._tempdir.__exit__, exc_type, exc_value, traceback + ) + + async def cleanup(self) -> None: + if self._tempdir is not None: + await to_thread.run_sync(self._tempdir.cleanup) + + +@overload +async def mkstemp( + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, + text: bool = False, +) -> tuple[int, str]: ... + + +@overload +async def mkstemp( + suffix: bytes | None = None, + prefix: bytes | None = None, + dir: bytes | None = None, + text: bool = False, +) -> tuple[int, bytes]: ... + + +async def mkstemp( + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: AnyStr | None = None, + text: bool = False, +) -> tuple[int, str | bytes]: + """ + Asynchronously create a temporary file and return an OS-level handle and the file + name. + + This function wraps `tempfile.mkstemp` and executes it in a background thread. + + :param suffix: Suffix to be added to the file name. + :param prefix: Prefix to be added to the file name. + :param dir: Directory in which the temporary file is created. + :param text: Whether the file is opened in text mode. + :return: A tuple containing the file descriptor and the file name. + + """ + return await to_thread.run_sync(tempfile.mkstemp, suffix, prefix, dir, text) + + +@overload +async def mkdtemp( + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, +) -> str: ... + + +@overload +async def mkdtemp( + suffix: bytes | None = None, + prefix: bytes | None = None, + dir: bytes | None = None, +) -> bytes: ... + + +async def mkdtemp( + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: AnyStr | None = None, +) -> str | bytes: + """ + Asynchronously create a temporary directory and return its path. + + This function wraps `tempfile.mkdtemp` and executes it in a background thread. + + :param suffix: Suffix to be added to the directory name. + :param prefix: Prefix to be added to the directory name. + :param dir: Parent directory where the temporary directory is created. + :return: The path of the created temporary directory. + + """ + return await to_thread.run_sync(tempfile.mkdtemp, suffix, prefix, dir) + + +async def gettempdir() -> str: + """ + Asynchronously return the name of the directory used for temporary files. + + This function wraps `tempfile.gettempdir` and executes it in a background thread. + + :return: The path of the temporary directory as a string. + + """ + return await to_thread.run_sync(tempfile.gettempdir) + + +async def gettempdirb() -> bytes: + """ + Asynchronously return the name of the directory used for temporary files in bytes. + + This function wraps `tempfile.gettempdirb` and executes it in a background thread. + + :return: The path of the temporary directory as bytes. + + """ + return await to_thread.run_sync(tempfile.gettempdirb) diff --git a/source/target-vendors/cp312/anyio/_core/_testing.py b/source/target-vendors/cp312/anyio/_core/_testing.py new file mode 100644 index 0000000..fa2c391 --- /dev/null +++ b/source/target-vendors/cp312/anyio/_core/_testing.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from collections.abc import Awaitable, Generator +from typing import Any, cast + +from ._eventloop import get_async_backend + + +class TaskInfo: + """ + Represents an asynchronous task. + + :ivar int id: the unique identifier of the task + :ivar parent_id: the identifier of the parent task, if any + :vartype parent_id: Optional[int] + :ivar str name: the description of the task (if any) + :ivar ~collections.abc.Coroutine coro: the coroutine object of the task + """ + + __slots__ = "_name", "coro", "id", "name", "parent_id" + + def __init__( + self, + id: int, + parent_id: int | None, + name: str | None, + coro: Generator[Any, Any, Any] | Awaitable[Any], + ): + func = get_current_task + self._name = f"{func.__module__}.{func.__qualname__}" + self.id: int = id + self.parent_id: int | None = parent_id + self.name: str | None = name + self.coro: Generator[Any, Any, Any] | Awaitable[Any] = coro + + def __eq__(self, other: object) -> bool: + if isinstance(other, TaskInfo): + return self.id == other.id + + return NotImplemented + + def __hash__(self) -> int: + return hash(self.id) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(id={self.id!r}, name={self.name!r})" + + def has_pending_cancellation(self) -> bool: + """ + Return ``True`` if the task has a cancellation pending, ``False`` otherwise. + + """ + return False + + +def get_current_task() -> TaskInfo: + """ + Return the current task. + + :return: a representation of the current task + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().get_current_task() + + +def get_running_tasks() -> list[TaskInfo]: + """ + Return a list of running tasks in the current event loop. + + :return: a list of task info objects + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return cast("list[TaskInfo]", get_async_backend().get_running_tasks()) + + +async def wait_all_tasks_blocked() -> None: + """Wait until all other tasks are waiting for something.""" + await get_async_backend().wait_all_tasks_blocked() diff --git a/source/target-vendors/cp312/anyio/_core/_typedattr.py b/source/target-vendors/cp312/anyio/_core/_typedattr.py new file mode 100644 index 0000000..59d3da9 --- /dev/null +++ b/source/target-vendors/cp312/anyio/_core/_typedattr.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +import sys +from collections.abc import Callable, Mapping +from typing import Any, TypeVar, final, overload + +from ._exceptions import TypedAttributeLookupError + +if sys.version_info < (3, 15): + from typing_extensions import sentinel + +T_Attr = TypeVar("T_Attr") +T_Default = TypeVar("T_Default") +undefined = sentinel("undefined") + + +def typed_attribute() -> Any: + """Return a unique object, used to mark typed attributes.""" + return object() + + +class TypedAttributeSet: + """ + Superclass for typed attribute collections. + + Checks that every public attribute of every subclass has a type annotation. + """ + + def __init_subclass__(cls) -> None: + annotations: dict[str, Any] = getattr(cls, "__annotations__", {}) + for attrname in dir(cls): + if not attrname.startswith("_") and attrname not in annotations: + raise TypeError( + f"Attribute {attrname!r} is missing its type annotation" + ) + + super().__init_subclass__() + + +class TypedAttributeProvider: + """Base class for classes that wish to provide typed extra attributes.""" + + @property + def extra_attributes(self) -> Mapping[T_Attr, Callable[[], T_Attr]]: + """ + A mapping of the extra attributes to callables that return the corresponding + values. + + If the provider wraps another provider, the attributes from that wrapper should + also be included in the returned mapping (but the wrapper may override the + callables from the wrapped instance). + + """ + return {} + + @overload + def extra(self, attribute: T_Attr) -> T_Attr: ... + + @overload + def extra(self, attribute: T_Attr, default: T_Default) -> T_Attr | T_Default: ... + + @final + def extra(self, attribute: Any, default: object = undefined) -> object: + """ + extra(attribute, default=undefined) + + Return the value of the given typed extra attribute. + + :param attribute: the attribute (member of a :class:`~TypedAttributeSet`) to + look for + :param default: the value that should be returned if no value is found for the + attribute + :raises ~anyio.TypedAttributeLookupError: if the search failed and no default + value was given + + """ + try: + getter = self.extra_attributes[attribute] + except KeyError: + if default is undefined: + raise TypedAttributeLookupError("Attribute not found") from None + else: + return default + + return getter() diff --git a/source/target-vendors/cp312/anyio/_lazyimport.py b/source/target-vendors/cp312/anyio/_lazyimport.py new file mode 100644 index 0000000..dc0a4fa --- /dev/null +++ b/source/target-vendors/cp312/anyio/_lazyimport.py @@ -0,0 +1,176 @@ +from __future__ import annotations + +__all__ = ( + "fix_package_names", + "install_lazy_importer", + "set_deprecated_aliases", +) + +import ast +import inspect +import sys +import warnings +from importlib import import_module +from types import ModuleType +from typing import Any + + +def install_lazy_importer() -> bool: + module_globals = sys._getframe(1).f_globals + module_name = module_globals["__name__"] + module_prefix = module_name + "." + module = sys.modules[module_name] + lazy_map, deprecated_aliases, submodule_names = _build_lazy_map(module) + names = sorted(lazy_map) + + # Delete symbols that are not part of the API + del module_globals["TYPE_CHECKING"] + del module_globals["install_lazy_importer"] + + if not lazy_map and not deprecated_aliases: + return False + + def __getattr__(name: str) -> Any: + if new_name := deprecated_aliases.get(name): + emit_deprecation_warning(module_name, name, new_name) + target_mod, target_attr = new_name.rsplit(".", 1) + elif name in submodule_names: + target_mod, target_attr = "." + name, "" + else: + try: + target_mod, target_attr = lazy_map[name] + except KeyError: + raise AttributeError( + f"module {module_name!r} has no attribute {name!r}" + ) from None + + imported = import_module(target_mod, module_name) + value = getattr(imported, target_attr) if target_attr else imported + + # patch the module name to match + if ( + getattr(value, "__module__", "").startswith(module_prefix) + and name not in deprecated_aliases + ): + value.__module__ = module_name + + module_globals[name] = value + return value + + def __dir__() -> list[str]: + return names + + module_globals["__dir__"] = __dir__ + module_globals["__getattr__"] = __getattr__ + module_globals.pop("fix_package_names", None) + module_globals.pop("set_deprecated_aliases", None) + return True + + +def fix_package_names() -> None: + module_globals = sys._getframe(1).f_globals + module_prefix = module_globals["__name__"] + "." + del module_globals[fix_package_names.__name__] + for value in module_globals.values(): + if modname := getattr(value, "__module__", ""): + if modname.startswith(module_prefix): + parts = modname.split(".") + value.__module__ = ".".join( + part for part in parts if not part.startswith("_") + ) + + +def emit_deprecation_warning(module_name: str, name: str, target: str) -> None: + warnings.warn( + f"The {module_name}.{name} alias is deprecated, use {target} instead.", + DeprecationWarning, + stacklevel=3, + ) + + +def set_deprecated_aliases(aliases: dict[str, str]) -> None: + module_globals = sys._getframe(1).f_globals + module_name = module_globals["__name__"] + del module_globals[set_deprecated_aliases.__name__] + + def __getattr__(name: str) -> Any: + try: + target = aliases[name] + except KeyError: + raise AttributeError( + f"module {module_name!r} has no attribute {name!r}" + ) from None + + emit_deprecation_warning(module_name, name, target) + target_modname, attrname = target.rsplit(".", 1) + module = import_module(target_modname) + return getattr(module, attrname) + + sys.modules[module_name].__dict__["__getattr__"] = __getattr__ + + +def _build_lazy_map( + module: ModuleType, +) -> tuple[dict[str, tuple[str, str]], dict[str, str], list[str]]: + try: + source = inspect.getsource(module) + except OSError: + return {}, {}, [] + + tree = compile(source, module.__file__ or "", "exec", ast.PyCF_ONLY_AST) + assert isinstance(tree, ast.Module) + out: dict[str, tuple[str, str]] = {} + deprecated_aliases: dict[str, str] = {} + submodule_names: list[str] = [] + + for node in tree.body: + if not isinstance(node, ast.If) or not _is_type_checking_block(node.test): + continue + + for stmt in node.body: + match stmt: + case ast.ImportFrom(): + if stmt.module is None: + submodule_names.extend(alias.name for alias in stmt.names) + else: + base = "." * stmt.level + (stmt.module or "") + for alias in stmt.names: + if alias.name == "*": + raise RuntimeError("star imports not supported") + + exported = alias.asname or alias.name + out[exported] = (base, alias.name) + case ast.Expr() if isinstance(stmt.value, ast.Call): + call = stmt.value + if ( + isinstance(call.func, ast.Name) + and call.func.id == "set_deprecated_aliases" + ): + arg0 = call.args[0] + assert isinstance(arg0, ast.Dict) + for key, value in zip(arg0.keys, arg0.values, strict=True): + assert isinstance(key, ast.Constant) + assert isinstance(key.value, str) + assert isinstance(value, ast.Constant) + assert isinstance(value.value, str) + deprecated_aliases[key.value] = value.value + + return out, deprecated_aliases, submodule_names + + +def _is_type_checking_block(test: ast.AST) -> bool: + if not isinstance(test, ast.BoolOp): + return False + + subtest = test.values[0] + match subtest: + case ast.Name(): + return subtest.id == "TYPE_CHECKING" + case ast.Attribute(): + return ( + isinstance(subtest.value, ast.Name) + and subtest.value.id == "typing" + and subtest.attr == "TYPE_CHECKING" + ) + case _: + return False diff --git a/source/target-vendors/cp312/anyio/abc/__init__.py b/source/target-vendors/cp312/anyio/abc/__init__.py new file mode 100644 index 0000000..ba738f8 --- /dev/null +++ b/source/target-vendors/cp312/anyio/abc/__init__.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .._lazyimport import ( + fix_package_names, + install_lazy_importer, + set_deprecated_aliases, +) + +if TYPE_CHECKING or not install_lazy_importer(): + from ._eventloop import AsyncBackend as AsyncBackend + from ._resources import AsyncResource as AsyncResource + from ._sockets import ConnectedUDPSocket as ConnectedUDPSocket + from ._sockets import ConnectedUNIXDatagramSocket as ConnectedUNIXDatagramSocket + from ._sockets import IPAddressType as IPAddressType + from ._sockets import IPSockAddrType as IPSockAddrType + from ._sockets import SocketAttribute as SocketAttribute + from ._sockets import SocketListener as SocketListener + from ._sockets import SocketStream as SocketStream + from ._sockets import UDPPacketType as UDPPacketType + from ._sockets import UDPSocket as UDPSocket + from ._sockets import UNIXDatagramPacketType as UNIXDatagramPacketType + from ._sockets import UNIXDatagramSocket as UNIXDatagramSocket + from ._sockets import UNIXSocketStream as UNIXSocketStream + from ._streams import AnyByteReceiveStream as AnyByteReceiveStream + from ._streams import AnyByteSendStream as AnyByteSendStream + from ._streams import AnyByteStream as AnyByteStream + from ._streams import AnyByteStreamConnectable as AnyByteStreamConnectable + from ._streams import ( + AnyUnreliableByteReceiveStream as AnyUnreliableByteReceiveStream, + ) + from ._streams import AnyUnreliableByteSendStream as AnyUnreliableByteSendStream + from ._streams import AnyUnreliableByteStream as AnyUnreliableByteStream + from ._streams import ByteReceiveStream as ByteReceiveStream + from ._streams import ByteSendStream as ByteSendStream + from ._streams import ByteStream as ByteStream + from ._streams import ByteStreamConnectable as ByteStreamConnectable + from ._streams import Listener as Listener + from ._streams import ObjectReceiveStream as ObjectReceiveStream + from ._streams import ObjectSendStream as ObjectSendStream + from ._streams import ObjectStream as ObjectStream + from ._streams import ObjectStreamConnectable as ObjectStreamConnectable + from ._streams import UnreliableObjectReceiveStream as UnreliableObjectReceiveStream + from ._streams import UnreliableObjectSendStream as UnreliableObjectSendStream + from ._streams import UnreliableObjectStream as UnreliableObjectStream + from ._subprocesses import Process as Process + from ._tasks import TaskGroup as TaskGroup + from ._tasks import TaskStatus as TaskStatus + from ._testing import TestRunner as TestRunner + + fix_package_names() + set_deprecated_aliases( + { + "CapacityLimiter": "anyio.CapacityLimiter", + "Condition": "anyio.Condition", + "Event": "anyio.Event", + "Lock": "anyio.Lock", + "Semaphore": "anyio.Semaphore", + "CancelScope": "anyio.CancelScope", + "BlockingPortal": "anyio.from_thread.BlockingPortal", + } + ) diff --git a/source/target-vendors/cp312/anyio/abc/_eventloop.py b/source/target-vendors/cp312/anyio/abc/_eventloop.py new file mode 100644 index 0000000..a3cbed2 --- /dev/null +++ b/source/target-vendors/cp312/anyio/abc/_eventloop.py @@ -0,0 +1,428 @@ +from __future__ import annotations + +import math +import sys +from abc import ABCMeta, abstractmethod +from collections.abc import ( + AsyncIterator, + Awaitable, + Callable, + Coroutine, + Iterable, + Mapping, + Sequence, +) +from contextlib import AbstractContextManager +from os import PathLike +from signal import Signals +from socket import AddressFamily, SocketKind, socket +from typing import ( + IO, + TYPE_CHECKING, + Any, + TypeAlias, + TypeVar, + overload, +) + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +if TYPE_CHECKING: + from _typeshed import FileDescriptorLike + + from .._core._synchronization import CapacityLimiter, Event, Lock, Semaphore + from .._core._tasks import CancelScope + from .._core._testing import TaskInfo + from ._sockets import ( + ConnectedUDPSocket, + ConnectedUNIXDatagramSocket, + IPSockAddrType, + SocketListener, + SocketStream, + UDPSocket, + UNIXDatagramSocket, + UNIXSocketStream, + ) + from ._subprocesses import Process + from ._tasks import TaskGroup + from ._testing import TestRunner + +T_Retval = TypeVar("T_Retval") +T_co = TypeVar("T_co", covariant=True) +PosArgsT = TypeVarTuple("PosArgsT") +StrOrBytesPath: TypeAlias = str | bytes | PathLike[str] | PathLike[bytes] + + +class AsyncBackend(metaclass=ABCMeta): + @classmethod + @abstractmethod + def run( + cls, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], + args: tuple[Unpack[PosArgsT]], + kwargs: dict[str, Any], + options: dict[str, Any], + ) -> T_Retval: + """ + Run the given coroutine function in an asynchronous event loop. + + The current thread must not be already running an event loop. + + :param func: a coroutine function + :param args: positional arguments to ``func`` + :param kwargs: positional arguments to ``func`` + :param options: keyword arguments to call the backend ``run()`` implementation + with + :return: the return value of the coroutine function + """ + + @classmethod + @abstractmethod + def current_token(cls) -> object: + """ + Return an object that allows other threads to run code inside the event loop. + + :return: a token object, specific to the event loop running in the current + thread + """ + + @classmethod + @abstractmethod + def current_time(cls) -> float: + """ + Return the current value of the event loop's internal clock. + + :return: the clock value (seconds) + """ + + @classmethod + @abstractmethod + def cancelled_exception_class(cls) -> type[BaseException]: + """Return the exception class that is raised in a task if it's cancelled.""" + + @classmethod + @abstractmethod + async def checkpoint(cls) -> None: + """ + Check if the task has been cancelled, and allow rescheduling of other tasks. + + This is effectively the same as running :meth:`checkpoint_if_cancelled` and then + :meth:`cancel_shielded_checkpoint`. + """ + + @classmethod + async def checkpoint_if_cancelled(cls) -> None: + """ + Check if the current task group has been cancelled. + + This will check if the task has been cancelled, but will not allow other tasks + to be scheduled if not. + + """ + if cls.current_effective_deadline() == -math.inf: + await cls.checkpoint() + + @classmethod + async def cancel_shielded_checkpoint(cls) -> None: + """ + Allow the rescheduling of other tasks. + + This will give other tasks the opportunity to run, but without checking if the + current task group has been cancelled, unlike with :meth:`checkpoint`. + + """ + with cls.create_cancel_scope(shield=True): + await cls.sleep(0) + + @classmethod + @abstractmethod + async def sleep(cls, delay: float) -> None: + """ + Pause the current task for the specified duration. + + :param delay: the duration, in seconds + """ + + @classmethod + @abstractmethod + def create_cancel_scope( + cls, *, deadline: float = math.inf, shield: bool = False + ) -> CancelScope: + pass + + @classmethod + @abstractmethod + def current_effective_deadline(cls) -> float: + """ + Return the nearest deadline among all the cancel scopes effective for the + current task. + + :return: + - a clock value from the event loop's internal clock + - ``inf`` if there is no deadline in effect + - ``-inf`` if the current scope has been cancelled + :rtype: float + """ + + @classmethod + @abstractmethod + def create_task_group(cls) -> TaskGroup: + pass + + @classmethod + @abstractmethod + def create_event(cls) -> Event: + pass + + @classmethod + @abstractmethod + def create_lock(cls, *, fast_acquire: bool) -> Lock: + pass + + @classmethod + @abstractmethod + def create_semaphore( + cls, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> Semaphore: + pass + + @classmethod + @abstractmethod + def create_capacity_limiter(cls, total_tokens: float) -> CapacityLimiter: + pass + + @classmethod + @abstractmethod + async def run_sync_in_worker_thread( + cls, + func: Callable[[Unpack[PosArgsT]], T_Retval], + args: tuple[Unpack[PosArgsT]], + abandon_on_cancel: bool = False, + limiter: CapacityLimiter | None = None, + ) -> T_Retval: + pass + + @classmethod + @abstractmethod + def check_cancelled(cls) -> None: + pass + + @classmethod + @abstractmethod + def run_async_from_thread( + cls, + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + args: tuple[Unpack[PosArgsT]], + token: object, + ) -> T_co: + pass + + @classmethod + @abstractmethod + def run_sync_from_thread( + cls, + func: Callable[[Unpack[PosArgsT]], T_Retval], + args: tuple[Unpack[PosArgsT]], + token: object, + ) -> T_Retval: + pass + + @classmethod + @abstractmethod + async def open_process( + cls, + command: StrOrBytesPath | Sequence[StrOrBytesPath], + *, + stdin: int | IO[Any] | None, + stdout: int | IO[Any] | None, + stderr: int | IO[Any] | None, + cwd: StrOrBytesPath | None = None, + env: Mapping[str, str] | None = None, + startupinfo: Any = None, + creationflags: int = 0, + start_new_session: bool = False, + pass_fds: Sequence[int] = (), + user: str | int | None = None, + group: str | int | None = None, + extra_groups: Iterable[str | int] | None = None, + umask: int = -1, + **kwargs: Any, + ) -> Process: + pass + + @classmethod + @abstractmethod + def setup_process_pool_exit_at_shutdown(cls, workers: set[Process]) -> None: + pass + + @classmethod + @abstractmethod + async def connect_tcp( + cls, host: str, port: int, local_address: IPSockAddrType | None = None + ) -> SocketStream: + pass + + @classmethod + @abstractmethod + async def connect_unix(cls, path: str | bytes) -> UNIXSocketStream: + pass + + @classmethod + @abstractmethod + def create_tcp_listener(cls, sock: socket) -> SocketListener: + pass + + @classmethod + @abstractmethod + def create_unix_listener(cls, sock: socket) -> SocketListener: + pass + + @classmethod + @abstractmethod + async def create_udp_socket( + cls, + family: AddressFamily, + local_address: IPSockAddrType | None, + remote_address: IPSockAddrType | None, + reuse_port: bool, + ) -> UDPSocket | ConnectedUDPSocket: + pass + + @classmethod + @overload + async def create_unix_datagram_socket( + cls, raw_socket: socket, remote_path: None + ) -> UNIXDatagramSocket: ... + + @classmethod + @overload + async def create_unix_datagram_socket( + cls, raw_socket: socket, remote_path: str | bytes + ) -> ConnectedUNIXDatagramSocket: ... + + @classmethod + @abstractmethod + async def create_unix_datagram_socket( + cls, raw_socket: socket, remote_path: str | bytes | None + ) -> UNIXDatagramSocket | ConnectedUNIXDatagramSocket: + pass + + @classmethod + @abstractmethod + async def getaddrinfo( + cls, + host: bytes | str | None, + port: str | int | None, + *, + family: int | AddressFamily = 0, + type: int | SocketKind = 0, + proto: int = 0, + flags: int = 0, + ) -> Sequence[ + tuple[ + AddressFamily, + SocketKind, + int, + str, + tuple[str, int] | tuple[str, int, int, int] | tuple[int, bytes], + ] + ]: + pass + + @classmethod + @abstractmethod + async def getnameinfo( + cls, sockaddr: IPSockAddrType, flags: int = 0 + ) -> tuple[str, str]: + pass + + @classmethod + @abstractmethod + async def wait_readable(cls, obj: FileDescriptorLike) -> None: + pass + + @classmethod + @abstractmethod + async def wait_writable(cls, obj: FileDescriptorLike) -> None: + pass + + @classmethod + @abstractmethod + def notify_closing(cls, obj: FileDescriptorLike) -> None: + pass + + @classmethod + @abstractmethod + async def wrap_listener_socket(cls, sock: socket) -> SocketListener: + pass + + @classmethod + @abstractmethod + async def wrap_stream_socket(cls, sock: socket) -> SocketStream: + pass + + @classmethod + @abstractmethod + async def wrap_unix_stream_socket(cls, sock: socket) -> UNIXSocketStream: + pass + + @classmethod + @abstractmethod + async def wrap_udp_socket(cls, sock: socket) -> UDPSocket: + pass + + @classmethod + @abstractmethod + async def wrap_connected_udp_socket(cls, sock: socket) -> ConnectedUDPSocket: + pass + + @classmethod + @abstractmethod + async def wrap_unix_datagram_socket(cls, sock: socket) -> UNIXDatagramSocket: + pass + + @classmethod + @abstractmethod + async def wrap_connected_unix_datagram_socket( + cls, sock: socket + ) -> ConnectedUNIXDatagramSocket: + pass + + @classmethod + @abstractmethod + def current_default_thread_limiter(cls) -> CapacityLimiter: + pass + + @classmethod + @abstractmethod + def open_signal_receiver( + cls, *signals: Signals + ) -> AbstractContextManager[AsyncIterator[Signals]]: + pass + + @classmethod + @abstractmethod + def get_current_task(cls) -> TaskInfo: + pass + + @classmethod + @abstractmethod + def get_running_tasks(cls) -> Sequence[TaskInfo]: + pass + + @classmethod + @abstractmethod + async def wait_all_tasks_blocked(cls) -> None: + pass + + @classmethod + @abstractmethod + def create_test_runner(cls, options: dict[str, Any]) -> TestRunner: + pass diff --git a/source/target-vendors/cp312/anyio/abc/_resources.py b/source/target-vendors/cp312/anyio/abc/_resources.py new file mode 100644 index 0000000..9373f71 --- /dev/null +++ b/source/target-vendors/cp312/anyio/abc/_resources.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import sys +from abc import ABCMeta, abstractmethod +from types import TracebackType +from typing import TypeVar + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +T = TypeVar("T") + + +class AsyncResource(metaclass=ABCMeta): + """ + Abstract base class for all closeable asynchronous resources. + + Works as an asynchronous context manager which returns the instance itself on enter, + and calls :meth:`aclose` on exit. + """ + + __slots__ = () + + async def __aenter__(self) -> Self: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.aclose() + + @abstractmethod + async def aclose(self) -> None: + """Close the resource.""" diff --git a/source/target-vendors/cp312/anyio/abc/_sockets.py b/source/target-vendors/cp312/anyio/abc/_sockets.py new file mode 100644 index 0000000..d6a1139 --- /dev/null +++ b/source/target-vendors/cp312/anyio/abc/_sockets.py @@ -0,0 +1,401 @@ +from __future__ import annotations + +import errno +import socket +from abc import abstractmethod +from collections.abc import Callable, Collection, Mapping +from contextlib import AsyncExitStack +from io import IOBase +from ipaddress import IPv4Address, IPv6Address +from socket import AddressFamily +from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar + +from .._core._eventloop import get_async_backend +from .._core._typedattr import ( + TypedAttributeProvider, + TypedAttributeSet, + typed_attribute, +) +from ._streams import ByteStream, Listener, UnreliableObjectStream + +if TYPE_CHECKING: + from ._tasks import TaskGroup + +IPAddressType: TypeAlias = str | IPv4Address | IPv6Address +IPSockAddrType: TypeAlias = tuple[str, int] +SockAddrType: TypeAlias = IPSockAddrType | str +UDPPacketType: TypeAlias = tuple[bytes, IPSockAddrType] +UNIXDatagramPacketType: TypeAlias = tuple[bytes, str] +T_Retval = TypeVar("T_Retval") + + +def _validate_socket( + sock_or_fd: socket.socket | int, + sock_type: socket.SocketKind, + addr_family: socket.AddressFamily = socket.AF_UNSPEC, + *, + require_connected: bool = False, + require_bound: bool = False, +) -> socket.socket: + if isinstance(sock_or_fd, int): + try: + sock = socket.socket(fileno=sock_or_fd) + except OSError as exc: + if exc.errno == errno.ENOTSOCK: + raise ValueError( + "the file descriptor does not refer to a socket" + ) from exc + elif require_connected: + raise ValueError("the socket must be connected") from exc + elif require_bound: + raise ValueError("the socket must be bound to a local address") from exc + else: + raise + elif isinstance(sock_or_fd, socket.socket): + sock = sock_or_fd + else: + raise TypeError( + f"expected an int or socket, got {type(sock_or_fd).__qualname__} instead" + ) + + try: + if require_connected: + try: + sock.getpeername() + except OSError as exc: + raise ValueError("the socket must be connected") from exc + + if require_bound: + try: + if sock.family in (socket.AF_INET, socket.AF_INET6): + bound_addr = sock.getsockname()[1] + else: + bound_addr = sock.getsockname() + except OSError: + bound_addr = None + + if not bound_addr: + raise ValueError("the socket must be bound to a local address") + + if addr_family != socket.AF_UNSPEC and sock.family != addr_family: + raise ValueError( + f"address family mismatch: expected {addr_family.name}, got " + f"{sock.family.name}" + ) + + if sock.type != sock_type: + raise ValueError( + f"socket type mismatch: expected {sock_type.name}, got {sock.type.name}" + ) + except BaseException: + # Avoid ResourceWarning from the locally constructed socket object + if isinstance(sock_or_fd, int): + sock.detach() + + raise + + sock.setblocking(False) + return sock + + +class SocketAttribute(TypedAttributeSet): + """ + .. attribute:: family + :type: socket.AddressFamily + + the address family of the underlying socket + + .. attribute:: local_address + :type: tuple[str, int] | str + + the local address the underlying socket is connected to + + .. attribute:: local_port + :type: int + + for IP based sockets, the local port the underlying socket is bound to + + .. attribute:: raw_socket + :type: socket.socket + + the underlying stdlib socket object + + .. attribute:: remote_address + :type: tuple[str, int] | str + + the remote address the underlying socket is connected to + + .. attribute:: remote_port + :type: int + + for IP based sockets, the remote port the underlying socket is connected to + """ + + family: AddressFamily = typed_attribute() + local_address: SockAddrType = typed_attribute() + local_port: int = typed_attribute() + raw_socket: socket.socket = typed_attribute() + remote_address: SockAddrType = typed_attribute() + remote_port: int = typed_attribute() + + +class _SocketProvider(TypedAttributeProvider): + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + from .._core._sockets import convert_ipv6_sockaddr as convert + + attributes: dict[Any, Callable[[], Any]] = { + SocketAttribute.family: lambda: self._raw_socket.family, + SocketAttribute.local_address: lambda: convert( + self._raw_socket.getsockname() + ), + SocketAttribute.raw_socket: lambda: self._raw_socket, + } + try: + peername: tuple[str, int] | None = convert(self._raw_socket.getpeername()) + except OSError: + peername = None + + # Provide the remote address for connected sockets + if peername is not None: + attributes[SocketAttribute.remote_address] = lambda: peername + + # Provide local and remote ports for IP based sockets + if self._raw_socket.family in (AddressFamily.AF_INET, AddressFamily.AF_INET6): + attributes[SocketAttribute.local_port] = lambda: ( + self._raw_socket.getsockname()[1] + ) + if peername is not None: + remote_port = peername[1] + attributes[SocketAttribute.remote_port] = lambda: remote_port + + return attributes + + @property + @abstractmethod + def _raw_socket(self) -> socket.socket: + pass + + +class SocketStream(ByteStream, _SocketProvider): + """ + Transports bytes over a socket. + + Supports all relevant extra attributes from :class:`~SocketAttribute`. + """ + + @classmethod + async def from_socket(cls, sock_or_fd: socket.socket | int) -> SocketStream: + """ + Wrap an existing socket object or file descriptor as a socket stream. + + The newly created socket wrapper takes ownership of the socket being passed in. + The existing socket must already be connected. + + :param sock_or_fd: a socket object or file descriptor + :return: a socket stream + + """ + sock = _validate_socket(sock_or_fd, socket.SOCK_STREAM, require_connected=True) + return await get_async_backend().wrap_stream_socket(sock) + + +class UNIXSocketStream(SocketStream): + @classmethod + async def from_socket(cls, sock_or_fd: socket.socket | int) -> UNIXSocketStream: + """ + Wrap an existing socket object or file descriptor as a UNIX socket stream. + + The newly created socket wrapper takes ownership of the socket being passed in. + The existing socket must already be connected. + + :param sock_or_fd: a socket object or file descriptor + :return: a UNIX socket stream + + """ + sock = _validate_socket( + sock_or_fd, socket.SOCK_STREAM, socket.AF_UNIX, require_connected=True + ) + return await get_async_backend().wrap_unix_stream_socket(sock) + + @abstractmethod + async def send_fds(self, message: bytes, fds: Collection[int | IOBase]) -> None: + """ + Send file descriptors along with a message to the peer. + + :param message: a non-empty bytestring + :param fds: a collection of files (either numeric file descriptors or open file + or socket objects) + """ + + @abstractmethod + async def receive_fds(self, msglen: int, maxfds: int) -> tuple[bytes, list[int]]: + """ + Receive file descriptors along with a message from the peer. + + :param msglen: length of the message to expect from the peer + :param maxfds: maximum number of file descriptors to expect from the peer + :return: a tuple of (message, file descriptors) + """ + + +class SocketListener(Listener[SocketStream], _SocketProvider): + """ + Listens to incoming socket connections. + + Supports all relevant extra attributes from :class:`~SocketAttribute`. + """ + + @classmethod + async def from_socket( + cls, + sock_or_fd: socket.socket | int, + ) -> SocketListener: + """ + Wrap an existing socket object or file descriptor as a socket listener. + + The newly created listener takes ownership of the socket being passed in. + + :param sock_or_fd: a socket object or file descriptor + :return: a socket listener + + """ + sock = _validate_socket(sock_or_fd, socket.SOCK_STREAM, require_bound=True) + return await get_async_backend().wrap_listener_socket(sock) + + @abstractmethod + async def accept(self) -> SocketStream: + """Accept an incoming connection.""" + + async def serve( + self, + handler: Callable[[SocketStream], Any], + task_group: TaskGroup | None = None, + ) -> None: + from .. import create_task_group + + async with AsyncExitStack() as stack: + if task_group is None: + task_group = await stack.enter_async_context(create_task_group()) + + while True: + stream = await self.accept() + task_group.start_soon(handler, stream) + + +class UDPSocket(UnreliableObjectStream[UDPPacketType], _SocketProvider): + """ + Represents an unconnected UDP socket. + + Supports all relevant extra attributes from :class:`~SocketAttribute`. + """ + + @classmethod + async def from_socket(cls, sock_or_fd: socket.socket | int) -> UDPSocket: + """ + Wrap an existing socket object or file descriptor as a UDP socket. + + The newly created socket wrapper takes ownership of the socket being passed in. + The existing socket must be bound to a local address. + + :param sock_or_fd: a socket object or file descriptor + :return: a UDP socket + + """ + sock = _validate_socket(sock_or_fd, socket.SOCK_DGRAM, require_bound=True) + return await get_async_backend().wrap_udp_socket(sock) + + async def sendto(self, data: bytes, host: str, port: int) -> None: + """ + Alias for :meth:`~.UnreliableObjectSendStream.send` ((data, (host, port))). + + """ + return await self.send((data, (host, port))) + + +class ConnectedUDPSocket(UnreliableObjectStream[bytes], _SocketProvider): + """ + Represents an connected UDP socket. + + Supports all relevant extra attributes from :class:`~SocketAttribute`. + """ + + @classmethod + async def from_socket(cls, sock_or_fd: socket.socket | int) -> ConnectedUDPSocket: + """ + Wrap an existing socket object or file descriptor as a connected UDP socket. + + The newly created socket wrapper takes ownership of the socket being passed in. + The existing socket must already be connected. + + :param sock_or_fd: a socket object or file descriptor + :return: a connected UDP socket + + """ + sock = _validate_socket( + sock_or_fd, + socket.SOCK_DGRAM, + require_connected=True, + ) + return await get_async_backend().wrap_connected_udp_socket(sock) + + +class UNIXDatagramSocket( + UnreliableObjectStream[UNIXDatagramPacketType], _SocketProvider +): + """ + Represents an unconnected Unix datagram socket. + + Supports all relevant extra attributes from :class:`~SocketAttribute`. + """ + + @classmethod + async def from_socket( + cls, + sock_or_fd: socket.socket | int, + ) -> UNIXDatagramSocket: + """ + Wrap an existing socket object or file descriptor as a UNIX datagram + socket. + + The newly created socket wrapper takes ownership of the socket being passed in. + + :param sock_or_fd: a socket object or file descriptor + :return: a UNIX datagram socket + + """ + sock = _validate_socket(sock_or_fd, socket.SOCK_DGRAM, socket.AF_UNIX) + return await get_async_backend().wrap_unix_datagram_socket(sock) + + async def sendto(self, data: bytes, path: str) -> None: + """Alias for :meth:`~.UnreliableObjectSendStream.send` ((data, path)).""" + return await self.send((data, path)) + + +class ConnectedUNIXDatagramSocket(UnreliableObjectStream[bytes], _SocketProvider): + """ + Represents a connected Unix datagram socket. + + Supports all relevant extra attributes from :class:`~SocketAttribute`. + """ + + @classmethod + async def from_socket( + cls, + sock_or_fd: socket.socket | int, + ) -> ConnectedUNIXDatagramSocket: + """ + Wrap an existing socket object or file descriptor as a connected UNIX datagram + socket. + + The newly created socket wrapper takes ownership of the socket being passed in. + The existing socket must already be connected. + + :param sock_or_fd: a socket object or file descriptor + :return: a connected UNIX datagram socket + + """ + sock = _validate_socket( + sock_or_fd, socket.SOCK_DGRAM, socket.AF_UNIX, require_connected=True + ) + return await get_async_backend().wrap_connected_unix_datagram_socket(sock) diff --git a/source/target-vendors/cp312/anyio/abc/_streams.py b/source/target-vendors/cp312/anyio/abc/_streams.py new file mode 100644 index 0000000..daf335d --- /dev/null +++ b/source/target-vendors/cp312/anyio/abc/_streams.py @@ -0,0 +1,237 @@ +from __future__ import annotations + +from abc import ABCMeta, abstractmethod +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, Generic, TypeAlias, TypeVar + +from .._core._exceptions import EndOfStream +from .._core._typedattr import TypedAttributeProvider +from ._resources import AsyncResource + +if TYPE_CHECKING: + from ._tasks import TaskGroup + +T_Item = TypeVar("T_Item") +T_co = TypeVar("T_co", covariant=True) +T_contra = TypeVar("T_contra", contravariant=True) + + +class UnreliableObjectReceiveStream( + AsyncResource, TypedAttributeProvider, Generic[T_co] +): + """ + An interface for receiving objects. + + This interface makes no guarantees that the received messages arrive in the order in + which they were sent, or that no messages are missed. + + Asynchronously iterating over objects of this type will yield objects matching the + given type parameter. + """ + + def __aiter__(self) -> UnreliableObjectReceiveStream[T_co]: + return self + + async def __anext__(self) -> T_co: + try: + return await self.receive() + except EndOfStream: + raise StopAsyncIteration from None + + @abstractmethod + async def receive(self) -> T_co: + """ + Receive the next item. + + :raises ~anyio.ClosedResourceError: if the receive stream has been explicitly + closed + :raises ~anyio.EndOfStream: if this stream has been closed from the other end + :raises ~anyio.BrokenResourceError: if this stream has been rendered unusable + due to external causes + """ + + +class UnreliableObjectSendStream( + AsyncResource, TypedAttributeProvider, Generic[T_contra] +): + """ + An interface for sending objects. + + This interface makes no guarantees that the messages sent will reach the + recipient(s) in the same order in which they were sent, or at all. + """ + + @abstractmethod + async def send(self, item: T_contra) -> None: + """ + Send an item to the peer(s). + + :param item: the item to send + :raises ~anyio.ClosedResourceError: if the send stream has been explicitly + closed + :raises ~anyio.BrokenResourceError: if this stream has been rendered unusable + due to external causes + """ + + +class UnreliableObjectStream( + UnreliableObjectReceiveStream[T_Item], UnreliableObjectSendStream[T_Item] +): + """ + A bidirectional message stream which does not guarantee the order or reliability of + message delivery. + """ + + +class ObjectReceiveStream(UnreliableObjectReceiveStream[T_co]): + """ + A receive message stream which guarantees that messages are received in the same + order in which they were sent, and that no messages are missed. + """ + + +class ObjectSendStream(UnreliableObjectSendStream[T_contra]): + """ + A send message stream which guarantees that messages are delivered in the same order + in which they were sent, without missing any messages in the middle. + """ + + +class ObjectStream( + ObjectReceiveStream[T_Item], + ObjectSendStream[T_Item], + UnreliableObjectStream[T_Item], +): + """ + A bidirectional message stream which guarantees the order and reliability of message + delivery. + """ + + @abstractmethod + async def send_eof(self) -> None: + """ + Send an end-of-file indication to the peer. + + You should not try to send any further data to this stream after calling this + method. This method is idempotent (does nothing on successive calls). + """ + + +class ByteReceiveStream(AsyncResource, TypedAttributeProvider): + """ + An interface for receiving bytes from a single peer. + + Iterating this byte stream will yield a byte string of arbitrary length, but no more + than 65536 bytes. + """ + + def __aiter__(self) -> ByteReceiveStream: + return self + + async def __anext__(self) -> bytes: + try: + return await self.receive() + except EndOfStream: + raise StopAsyncIteration from None + + @abstractmethod + async def receive(self, max_bytes: int = 65536) -> bytes: + """ + Receive at most ``max_bytes`` bytes from the peer. + + .. note:: Implementers of this interface should not return an empty + :class:`bytes` object, and users should ignore them. + + :param max_bytes: maximum number of bytes to receive (must be a positive + integer) + :return: the received bytes + :raises ValueError: if ``max_bytes`` is less than 1 + :raises ~anyio.EndOfStream: if this stream has been closed from the other end + """ + + +class ByteSendStream(AsyncResource, TypedAttributeProvider): + """An interface for sending bytes to a single peer.""" + + @abstractmethod + async def send(self, item: bytes) -> None: + """ + Send the given bytes to the peer. + + :param item: the bytes to send + """ + + +class ByteStream(ByteReceiveStream, ByteSendStream): + """A bidirectional byte stream.""" + + @abstractmethod + async def send_eof(self) -> None: + """ + Send an end-of-file indication to the peer. + + You should not try to send any further data to this stream after calling this + method. This method is idempotent (does nothing on successive calls). + """ + + +#: Type alias for all unreliable bytes-oriented receive streams. +AnyUnreliableByteReceiveStream: TypeAlias = ( + UnreliableObjectReceiveStream[bytes] | ByteReceiveStream +) +#: Type alias for all unreliable bytes-oriented send streams. +AnyUnreliableByteSendStream: TypeAlias = ( + UnreliableObjectSendStream[bytes] | ByteSendStream +) +#: Type alias for all unreliable bytes-oriented streams. +AnyUnreliableByteStream: TypeAlias = UnreliableObjectStream[bytes] | ByteStream +#: Type alias for all bytes-oriented receive streams. +AnyByteReceiveStream: TypeAlias = ObjectReceiveStream[bytes] | ByteReceiveStream +#: Type alias for all bytes-oriented send streams. +AnyByteSendStream: TypeAlias = ObjectSendStream[bytes] | ByteSendStream +#: Type alias for all bytes-oriented streams. +AnyByteStream: TypeAlias = ObjectStream[bytes] | ByteStream + + +class Listener(AsyncResource, TypedAttributeProvider, Generic[T_co]): + """An interface for objects that let you accept incoming connections.""" + + @abstractmethod + async def serve( + self, handler: Callable[[T_co], Any], task_group: TaskGroup | None = None + ) -> None: + """ + Accept incoming connections as they come in and start tasks to handle them. + + :param handler: a callable that will be used to handle each accepted connection + :param task_group: the task group that will be used to start tasks for handling + each accepted connection (if omitted, an ad-hoc task group will be created) + """ + + +class ObjectStreamConnectable(Generic[T_co], metaclass=ABCMeta): + @abstractmethod + async def connect(self) -> ObjectStream[T_co]: + """ + Connect to the remote endpoint. + + :return: an object stream connected to the remote end + :raises ConnectionFailed: if the connection fails + """ + + +class ByteStreamConnectable(metaclass=ABCMeta): + @abstractmethod + async def connect(self) -> ByteStream: + """ + Connect to the remote endpoint. + + :return: a bytestream connected to the remote end + :raises ConnectionFailed: if the connection fails + """ + + +#: Type alias for all connectables returning bytestreams or bytes-oriented object streams +AnyByteStreamConnectable: TypeAlias = ( + ObjectStreamConnectable[bytes] | ByteStreamConnectable +) diff --git a/source/target-vendors/cp312/anyio/abc/_subprocesses.py b/source/target-vendors/cp312/anyio/abc/_subprocesses.py new file mode 100644 index 0000000..555f15f --- /dev/null +++ b/source/target-vendors/cp312/anyio/abc/_subprocesses.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from abc import abstractmethod +from signal import Signals +from typing import TYPE_CHECKING + +from ._resources import AsyncResource + +if TYPE_CHECKING: + from ._streams import ByteReceiveStream, ByteSendStream + + +class Process(AsyncResource): + """An asynchronous version of :class:`subprocess.Popen`.""" + + @abstractmethod + async def wait(self) -> int: + """ + Wait until the process exits. + + :return: the exit code of the process + """ + + @abstractmethod + def terminate(self) -> None: + """ + Terminates the process, gracefully if possible. + + On Windows, this calls ``TerminateProcess()``. + On POSIX systems, this sends ``SIGTERM`` to the process. + + .. seealso:: :meth:`subprocess.Popen.terminate` + """ + + @abstractmethod + def kill(self) -> None: + """ + Kills the process. + + On Windows, this calls ``TerminateProcess()``. + On POSIX systems, this sends ``SIGKILL`` to the process. + + .. seealso:: :meth:`subprocess.Popen.kill` + """ + + @abstractmethod + def send_signal(self, signal: Signals) -> None: + """ + Send a signal to the subprocess. + + .. seealso:: :meth:`subprocess.Popen.send_signal` + + :param signal: the signal number (e.g. :data:`signal.SIGHUP`) + """ + + @property + @abstractmethod + def pid(self) -> int: + """The process ID of the process.""" + + @property + @abstractmethod + def returncode(self) -> int | None: + """ + The return code of the process. If the process has not yet terminated, this will + be ``None``. + """ + + @property + @abstractmethod + def stdin(self) -> ByteSendStream | None: + """The stream for the standard input of the process.""" + + @property + @abstractmethod + def stdout(self) -> ByteReceiveStream | None: + """The stream for the standard output of the process.""" + + @property + @abstractmethod + def stderr(self) -> ByteReceiveStream | None: + """The stream for the standard error output of the process.""" diff --git a/source/target-vendors/cp312/anyio/abc/_tasks.py b/source/target-vendors/cp312/anyio/abc/_tasks.py new file mode 100644 index 0000000..0a019e2 --- /dev/null +++ b/source/target-vendors/cp312/anyio/abc/_tasks.py @@ -0,0 +1,223 @@ +from __future__ import annotations + +import sys +from abc import ABCMeta, abstractmethod +from collections.abc import Callable, Coroutine +from contextvars import Context +from types import TracebackType +from typing import TYPE_CHECKING, Any, Literal, Protocol, final, overload + +if sys.version_info >= (3, 13): + from typing import TypeVar +else: + from typing_extensions import TypeVar + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +if TYPE_CHECKING: + from .._core._tasks import CancelScope, TaskHandle + +T_co = TypeVar("T_co", covariant=True) +T_contra = TypeVar("T_contra", contravariant=True, default=None) +PosArgsT = TypeVarTuple("PosArgsT") + + +def get_coro_name(coro: Coroutine[Any, Any, object], override: object = None) -> str: + if override is not None: + return str(override) + + try: + cr_frame = coro.cr_frame # type: ignore[attr-defined] + module = cr_frame.f_globals["__name__"] + except (AttributeError, KeyError): + module = None + + qualname = getattr(coro, "__qualname__", None) + return ".".join([x for x in (module, qualname) if x]) + + +def get_callable_name(func: Callable, override: object = None) -> str: + if override is not None: + return str(override) + + module = getattr(func, "__module__", None) + qualname = getattr(func, "__qualname__", None) + return ".".join([x for x in (module, qualname) if x]) + + +def call_for_coroutine( + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + args: tuple[Unpack[PosArgsT]], + **kwargs: Any, +) -> Coroutine[Any, Any, T_co]: + """ + Call the given function with the given positional and keyword arguments. + + :return: the resulting coroutine + :raises TypeError: if the return value was not a coroutine object + + """ + coro = func(*args, **kwargs) + if not isinstance(coro, Coroutine): + prefix = f"{func.__module__}." if hasattr(func, "__module__") else "" + raise TypeError( + f"Expected {prefix}{func.__qualname__}() to return a coroutine, but " + f"the return value ({coro!r}) is not a coroutine object" + ) + + return coro + + +class TaskStatus(Protocol[T_contra]): + @overload + def started(self: TaskStatus[None]) -> None: ... + + @overload + def started(self, value: T_contra) -> None: ... + + def started(self, value: T_contra | None = None) -> None: + """ + Signal that the task has started. + + :param value: object passed back to the starter of the task + """ + + +class TaskGroup(metaclass=ABCMeta): + """ + Groups several asynchronous tasks together. + + :ivar cancel_scope: the cancel scope inherited by all child tasks + :vartype cancel_scope: CancelScope + + .. note:: On asyncio, support for eager task factories is considered to be + **experimental**. In particular, they don't follow the usual semantics of new + tasks being scheduled on the next iteration of the event loop, and may thus + cause unexpected behavior in code that wasn't written with such semantics in + mind. + """ + + cancel_scope: CancelScope + + def cancel(self, reason: str | None = None) -> None: + """ + Cancel this task group's cancel scope immediately. + + This is a shortcut for calling ``.cancel_scope.cancel()`` on the task group. + + :param reason: a message describing the reason for the cancellation + + .. versionadded:: 4.14.0 + + """ + self.cancel_scope.cancel(reason) + + @abstractmethod + def create_task( + self, + coro: Coroutine[Any, Any, T_co], + *, + name: object = None, + context: Context | None = None, + ) -> TaskHandle[T_co]: + """ + Create a new task from a coroutine object and schedule it to run. + + :param coro: a coroutine object + :param name: optional name to give the task + :param context: optional context to run the task in + :return: a task handle + + .. versionadded:: 4.14.0 + """ + + @final + def start_soon( + self, + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + *args: Unpack[PosArgsT], + name: object = None, + ) -> TaskHandle[T_co]: + """ + Start a new task in this task group. + + :param func: a coroutine function + :param args: positional arguments to call the function with + :param name: name of the task, for the purposes of introspection and debugging + :return: a task handle + + .. versionadded:: 3.0 + .. versionchanged:: 4.14.0 + This method now returns a task handle. + + """ + final_name = get_callable_name(func, name) + return self.create_task(call_for_coroutine(func, args), name=final_name) + + @overload + async def start( + self, + func: Callable[..., Coroutine[Any, Any, T_co]], + *args: object, + name: object = None, + return_handle: Literal[False] = ..., + ) -> Any: ... + + @overload + async def start( + self, + func: Callable[..., Coroutine[Any, Any, T_co]], + *args: object, + name: object = None, + return_handle: Literal[True], + ) -> TaskHandle[T_co, Any]: ... + + @abstractmethod + async def start( + self, + func: Callable[..., Coroutine[Any, Any, T_co]], + *args: object, + name: object = None, + return_handle: Literal[False, True] = False, + ) -> Any: + """ + Start a new task and wait until it signals for readiness. + + The target callable must accept a keyword argument ``task_status`` (of type + :class:`TaskStatus`). Awaiting on this method will return whatever was passed to + ``task_status.started()`` (``None`` by default). + + .. note:: The :class:`TaskStatus` class is generic, and the type argument should + indicate the type of the value that will be passed to + ``task_status.started()``. + + :param func: a coroutine function that accepts the ``task_status`` keyword + argument + :param args: positional arguments to call the function with + :param name: an optional name for the task, for introspection and debugging + :param return_handle: if ``True``, return a :class:`TaskHandle` which also + contains the start value in ``start_value`` + :return: the value passed to ``task_status.started()`` + :raises RuntimeError: if the task finishes without calling + ``task_status.started()`` + + .. seealso:: :ref:`start_initialize` + + .. versionadded:: 3.0 + """ + + @abstractmethod + async def __aenter__(self) -> TaskGroup: + """Enter the task group context and allow starting new tasks.""" + + @abstractmethod + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + """Exit the task group context waiting for all tasks to finish.""" diff --git a/source/target-vendors/cp312/anyio/abc/_testing.py b/source/target-vendors/cp312/anyio/abc/_testing.py new file mode 100644 index 0000000..b18d835 --- /dev/null +++ b/source/target-vendors/cp312/anyio/abc/_testing.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +import sys +import types +from abc import ABCMeta, abstractmethod +from collections.abc import AsyncGenerator, Callable, Coroutine, Iterable +from typing import Any, TypeVar + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +_T = TypeVar("_T") + + +class TestRunner(metaclass=ABCMeta): + """ + Encapsulates a running event loop. Every call made through this object will use the + same event loop. + """ + + def __enter__(self) -> Self: + return self + + @abstractmethod + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: types.TracebackType | None, + ) -> bool | None: ... + + @abstractmethod + def run_asyncgen_fixture( + self, + fixture_func: Callable[..., AsyncGenerator[_T, Any]], + kwargs: dict[str, Any], + ) -> Iterable[_T]: + """ + Run an async generator fixture. + + :param fixture_func: the fixture function + :param kwargs: keyword arguments to call the fixture function with + :return: an iterator yielding the value yielded from the async generator + """ + + @abstractmethod + def run_fixture( + self, + fixture_func: Callable[..., Coroutine[Any, Any, _T]], + kwargs: dict[str, Any], + ) -> _T: + """ + Run an async fixture. + + :param fixture_func: the fixture function + :param kwargs: keyword arguments to call the fixture function with + :return: the return value of the fixture function + """ + + @abstractmethod + def run_test( + self, test_func: Callable[..., Coroutine[Any, Any, Any]], kwargs: dict[str, Any] + ) -> None: + """ + Run an async test function. + + :param test_func: the test function + :param kwargs: keyword arguments to call the test function with + """ + + @abstractmethod + def is_running(self) -> bool: + """ + Check if the test runner is running. + + :return: ``True`` if the coroutine is currently being run, ``False`` otherwise. + """ diff --git a/source/target-vendors/cp312/anyio/from_thread.py b/source/target-vendors/cp312/anyio/from_thread.py new file mode 100644 index 0000000..f163d81 --- /dev/null +++ b/source/target-vendors/cp312/anyio/from_thread.py @@ -0,0 +1,588 @@ +from __future__ import annotations + +__all__ = ( + "BlockingPortal", + "BlockingPortalProvider", + "check_cancelled", + "run", + "run_sync", + "start_blocking_portal", +) + +import sys +from collections.abc import Awaitable, Callable, Coroutine, Generator +from concurrent.futures import Future +from contextlib import ( + AbstractAsyncContextManager, + AbstractContextManager, + contextmanager, +) +from contextvars import Context +from dataclasses import dataclass, field +from functools import partial +from inspect import isawaitable +from threading import Lock, Thread, current_thread, get_ident +from types import TracebackType +from typing import ( + Any, + Generic, + TypeVar, + cast, + overload, +) + +from ._core._eventloop import ( + get_cancelled_exc_class, + threadlocals, +) +from ._core._eventloop import run as run_eventloop +from ._core._exceptions import NoEventLoopError +from ._core._synchronization import Event +from ._core._tasks import CancelScope, create_task_group +from .abc._tasks import TaskStatus +from .lowlevel import EventLoopToken, current_token + +if sys.version_info >= (3, 11): + from typing import Self, TypeVarTuple, Unpack +else: + from typing_extensions import Self, TypeVarTuple, Unpack + +T_Retval = TypeVar("T_Retval") +T_co = TypeVar("T_co", covariant=True) +PosArgsT = TypeVarTuple("PosArgsT") + + +def _token_or_error(token: EventLoopToken | None) -> EventLoopToken: + if token is not None: + return token + + try: + return threadlocals.current_token + except AttributeError: + raise NoEventLoopError( + "Not running inside an AnyIO worker thread, and no event loop token was " + "provided" + ) from None + + +def run( + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + *args: Unpack[PosArgsT], + token: EventLoopToken | None = None, +) -> T_co: + """ + Call a coroutine function from a worker thread. + + :param func: a coroutine function + :param args: positional arguments for the callable + :param token: an event loop token to use to get back to the event loop thread + (required if calling this function from outside an AnyIO worker thread) + :return: the return value of the coroutine function + :raises MissingTokenError: if no token was provided and called from outside an + AnyIO worker thread + :raises RunFinishedError: if the event loop tied to ``token`` is no longer running + + .. versionchanged:: 4.11.0 + Added the ``token`` parameter. + + """ + explicit_token = token is not None + token = _token_or_error(token) + return token.backend_class.run_async_from_thread( + func, args, token=token.native_token if explicit_token else None + ) + + +def run_sync( + func: Callable[[Unpack[PosArgsT]], T_Retval], + *args: Unpack[PosArgsT], + token: EventLoopToken | None = None, +) -> T_Retval: + """ + Call a function in the event loop thread from a worker thread. + + :param func: a callable + :param args: positional arguments for the callable + :param token: an event loop token to use to get back to the event loop thread + (required if calling this function from outside an AnyIO worker thread) + :return: the return value of the callable + :raises MissingTokenError: if no token was provided and called from outside an + AnyIO worker thread + :raises RunFinishedError: if the event loop tied to ``token`` is no longer running + + .. versionchanged:: 4.11.0 + Added the ``token`` parameter. + + """ + explicit_token = token is not None + token = _token_or_error(token) + return token.backend_class.run_sync_from_thread( + func, args, token=token.native_token if explicit_token else None + ) + + +class _BlockingAsyncContextManager(AbstractContextManager, Generic[T_co]): + _enter_future: Future[T_co] + _exit_future: Future[bool | None] + _exit_event: Event + _exit_exc_info: tuple[ + type[BaseException] | None, BaseException | None, TracebackType | None + ] = (None, None, None) + + def __init__( + self, async_cm: AbstractAsyncContextManager[T_co], portal: BlockingPortal + ): + self._async_cm = async_cm + self._portal = portal + + async def run_async_cm(self) -> bool | None: + try: + self._exit_event = Event() + value = await self._async_cm.__aenter__() + except BaseException as exc: + self._enter_future.set_exception(exc) + raise + else: + self._enter_future.set_result(value) + + try: + # Wait for the sync context manager to exit. + # This next statement can raise `get_cancelled_exc_class()` if + # something went wrong in a task group in this async context + # manager. + await self._exit_event.wait() + finally: + # In case of cancellation, it could be that we end up here before + # `_BlockingAsyncContextManager.__exit__` is called, and an + # `_exit_exc_info` has been set. + result = await self._async_cm.__aexit__(*self._exit_exc_info) + + return result + + def __enter__(self) -> T_co: + self._enter_future = Future() + self._exit_future = self._portal.start_task_soon(self.run_async_cm) + return self._enter_future.result() + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + /, + ) -> bool | None: + self._exit_exc_info = exc_type, exc_value, traceback + self._portal.call(self._exit_event.set) + return self._exit_future.result() + + +class _BlockingPortalTaskStatus(TaskStatus): + def __init__(self, future: Future): + self._future = future + + def started(self, value: object = None) -> None: + self._future.set_result(value) + + +class BlockingPortal: + """ + An object that lets external threads run code in an asynchronous event loop. + + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + """ + + def __init__(self) -> None: + self._token = current_token() + self._event_loop_thread_id: int | None = get_ident() + self._stop_event = Event() + self._task_group = create_task_group() + + async def __aenter__(self) -> Self: + await self._task_group.__aenter__() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + await self.stop() + return await self._task_group.__aexit__(exc_type, exc_val, exc_tb) + + def _check_running(self) -> None: + if self._event_loop_thread_id is None: + raise RuntimeError("This portal is not running") + if self._event_loop_thread_id == get_ident(): + raise RuntimeError( + "This method cannot be called from the event loop thread" + ) + + async def sleep_until_stopped(self) -> None: + """Sleep until :meth:`stop` is called.""" + await self._stop_event.wait() + + async def stop(self, cancel_remaining: bool = False) -> None: + """ + Signal the portal to shut down. + + This marks the portal as no longer accepting new calls and exits from + :meth:`sleep_until_stopped`. + + :param cancel_remaining: ``True`` to cancel all the remaining tasks, ``False`` + to let them finish before returning + + """ + self._event_loop_thread_id = None + self._stop_event.set() + if cancel_remaining: + self._task_group.cancel_scope.cancel("the blocking portal is shutting down") + + async def _call_func( + self, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], + args: tuple[Unpack[PosArgsT]], + kwargs: dict[str, Any], + future: Future[T_Retval], + ) -> None: + event_loop_thread_id = self._event_loop_thread_id + + def callback(f: Future[T_Retval]) -> None: + if f.cancelled(): + if event_loop_thread_id == get_ident(): + scope.cancel("the future was cancelled") + elif event_loop_thread_id is not None: + run_sync( + scope.cancel, "the future was cancelled", token=self._token + ) + + try: + retval_or_awaitable = func(*args, **kwargs) + if isawaitable(retval_or_awaitable): + with CancelScope() as scope: + future.add_done_callback(callback) + retval = await retval_or_awaitable + else: + retval = retval_or_awaitable + except get_cancelled_exc_class(): + future.cancel() + future.set_running_or_notify_cancel() + except BaseException as exc: + if not future.cancelled(): + future.set_exception(exc) + + # Let base exceptions fall through + if not isinstance(exc, Exception): + raise + else: + if not future.cancelled(): + future.set_result(retval) + finally: + scope = None # type: ignore[assignment] + + def _spawn_task_from_thread( + self, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], + args: tuple[Unpack[PosArgsT]], + kwargs: dict[str, Any], + name: object, + future: Future[T_Retval], + ) -> None: + """ + Spawn a new task using the given callable. + + :param func: a callable + :param args: positional arguments to be passed to the callable + :param kwargs: keyword arguments to be passed to the callable + :param name: name of the task (will be coerced to a string if not ``None``) + :param future: a future that will resolve to the return value of the callable, + or the exception raised during its execution + + """ + run_sync( + partial(self._task_group.start_soon, name=name), + self._call_func, + func, + args, + kwargs, + future, + token=self._token, + ) + + @overload + def call( + self, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], + *args: Unpack[PosArgsT], + ) -> T_Retval: ... + + @overload + def call( + self, func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT] + ) -> T_Retval: ... + + def call( + self, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], + *args: Unpack[PosArgsT], + ) -> T_Retval: + """ + Call the given function in the event loop thread. + + If the callable returns a coroutine object, it is awaited on. + + :param func: any callable + :raises RuntimeError: if the portal is not running or if this method is called + from within the event loop thread + + """ + return cast(T_Retval, self.start_task_soon(func, *args).result()) + + @overload + def start_task_soon( + self, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], + *args: Unpack[PosArgsT], + name: object = None, + ) -> Future[T_Retval]: ... + + @overload + def start_task_soon( + self, + func: Callable[[Unpack[PosArgsT]], T_Retval], + *args: Unpack[PosArgsT], + name: object = None, + ) -> Future[T_Retval]: ... + + def start_task_soon( + self, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], + *args: Unpack[PosArgsT], + name: object = None, + ) -> Future[T_Retval]: + """ + Start a task in the portal's task group. + + The task will be run inside a cancel scope which can be cancelled by cancelling + the returned future. + + :param func: the target function + :param args: positional arguments passed to ``func`` + :param name: name of the task (will be coerced to a string if not ``None``) + :return: a future that resolves with the return value of the callable if the + task completes successfully, or with the exception raised in the task + :raises RuntimeError: if the portal is not running or if this method is called + from within the event loop thread + :rtype: concurrent.futures.Future[T_Retval] + + .. versionadded:: 3.0 + + """ + self._check_running() + f: Future[T_Retval] = Future() + self._spawn_task_from_thread(func, args, {}, name, f) + return f + + def start_task( + self, + func: Callable[..., Awaitable[T_Retval]], + *args: object, + name: object = None, + ) -> tuple[Future[T_Retval], Any]: + """ + Start a task in the portal's task group and wait until it signals for readiness. + + This method works the same way as :meth:`.abc.TaskGroup.start`. + + :param func: the target function + :param args: positional arguments passed to ``func`` + :param name: name of the task (will be coerced to a string if not ``None``) + :return: a tuple of (future, task_status_value) where the ``task_status_value`` + is the value passed to ``task_status.started()`` from within the target + function + :rtype: tuple[concurrent.futures.Future[T_Retval], Any] + + .. versionadded:: 3.0 + + """ + + def task_done(future: Future[T_Retval]) -> None: + if not task_status_future.done(): + if future.cancelled(): + task_status_future.cancel() + elif future.exception(): + task_status_future.set_exception(future.exception()) + else: + exc = RuntimeError( + "Task exited without calling task_status.started()" + ) + task_status_future.set_exception(exc) + + self._check_running() + task_status_future: Future = Future() + task_status = _BlockingPortalTaskStatus(task_status_future) + f: Future = Future() + f.add_done_callback(task_done) + self._spawn_task_from_thread(func, args, {"task_status": task_status}, name, f) + return f, task_status_future.result() + + def wrap_async_context_manager( + self, cm: AbstractAsyncContextManager[T_co] + ) -> AbstractContextManager[T_co]: + """ + Wrap an async context manager as a synchronous context manager via this portal. + + Spawns a task that will call both ``__aenter__()`` and ``__aexit__()``, stopping + in the middle until the synchronous context manager exits. + + :param cm: an asynchronous context manager + :return: a synchronous context manager + + .. versionadded:: 2.1 + + """ + return _BlockingAsyncContextManager(cm, self) + + +@dataclass +class BlockingPortalProvider: + """ + A manager for a blocking portal. Used as a context manager. The first thread to + enter this context manager causes a blocking portal to be started with the specific + parameters, and the last thread to exit causes the portal to be shut down. Thus, + there will be exactly one blocking portal running in this context as long as at + least one thread has entered this context manager. + + The parameters are the same as for :func:`~anyio.run`. + + :param backend: name of the backend + :param backend_options: backend options + + .. versionadded:: 4.4 + """ + + backend: str = "asyncio" + backend_options: dict[str, Any] | None = None + _lock: Lock = field(init=False, default_factory=Lock) + _leases: int = field(init=False, default=0) + _portal: BlockingPortal = field(init=False) + _portal_cm: AbstractContextManager[BlockingPortal] | None = field( + init=False, default=None + ) + + def __enter__(self) -> BlockingPortal: + with self._lock: + if self._portal_cm is None: + self._portal_cm = start_blocking_portal( + self.backend, self.backend_options + ) + self._portal = self._portal_cm.__enter__() + + self._leases += 1 + return self._portal + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + portal_cm: AbstractContextManager[BlockingPortal] | None = None + with self._lock: + assert self._portal_cm + assert self._leases > 0 + self._leases -= 1 + if not self._leases: + portal_cm = self._portal_cm + self._portal_cm = None + del self._portal + + if portal_cm: + portal_cm.__exit__(None, None, None) + + +@contextmanager +def start_blocking_portal( + backend: str = "asyncio", + backend_options: dict[str, Any] | None = None, + *, + name: str | None = None, +) -> Generator[BlockingPortal, Any, None]: + """ + Start a new event loop in a new thread and run a blocking portal in its main task. + + The parameters are the same as for :func:`~anyio.run`. + + :param backend: name of the backend + :param backend_options: backend options + :param name: name of the thread + :return: a context manager that yields a blocking portal + + .. versionchanged:: 3.0 + Usage as a context manager is now required. + + """ + + async def run_portal() -> None: + async with BlockingPortal() as portal_: + if name is None: + current_thread().name = f"{backend}-portal-{id(portal_):x}" + + future.set_result(portal_) + await portal_.sleep_until_stopped() + + def run_blocking_portal() -> None: + if future.set_running_or_notify_cancel(): + try: + run_eventloop( + run_portal, backend=backend, backend_options=backend_options + ) + except BaseException as exc: + if not future.done(): + future.set_exception(exc) + + future: Future[BlockingPortal] = Future() + kwargs: dict[str, Any] = {} + if sys.version_info >= (3, 14): + kwargs["context"] = Context() + + thread = Thread(target=run_blocking_portal, daemon=True, name=name, **kwargs) + thread.start() + try: + cancel_remaining_tasks = False + portal = future.result() + try: + yield portal + except BaseException: + cancel_remaining_tasks = True + raise + finally: + try: + portal.call(portal.stop, cancel_remaining_tasks) + except RuntimeError: + pass + finally: + thread.join() + + +def check_cancelled() -> None: + """ + Check if the cancel scope of the host task's running the current worker thread has + been cancelled. + + If the host task's current cancel scope has indeed been cancelled, the + backend-specific cancellation exception will be raised. + + :raises RuntimeError: if the current thread was not spawned by + :func:`.to_thread.run_sync` + + """ + try: + token: EventLoopToken = threadlocals.current_token + except AttributeError: + raise NoEventLoopError( + "This function can only be called inside an AnyIO worker thread" + ) from None + + token.backend_class.check_cancelled() diff --git a/source/target-vendors/cp312/anyio/functools.py b/source/target-vendors/cp312/anyio/functools.py new file mode 100644 index 0000000..b0bdfb4 --- /dev/null +++ b/source/target-vendors/cp312/anyio/functools.py @@ -0,0 +1,400 @@ +from __future__ import annotations + +__all__ = ( + "AsyncCacheInfo", + "AsyncCacheParameters", + "AsyncLRUCacheWrapper", + "cache", + "lru_cache", + "reduce", +) + +import functools +from collections import OrderedDict +from collections.abc import ( + AsyncIterable, + Awaitable, + Callable, + Coroutine, + Hashable, + Iterable, +) +from functools import update_wrapper +from inspect import iscoroutinefunction +from typing import ( + Any, + Generic, + NamedTuple, + ParamSpec, + TypedDict, + TypeVar, + cast, + final, + overload, +) +from weakref import WeakKeyDictionary + +from ._core._eventloop import current_time +from ._core._synchronization import Lock +from .lowlevel import RunVar, checkpoint + +T = TypeVar("T") +S = TypeVar("S") +P = ParamSpec("P") +lru_cache_items: RunVar[ + WeakKeyDictionary[ + AsyncLRUCacheWrapper[Any, Any], + OrderedDict[ + Hashable, + tuple[_InitialMissingType, Lock, float | None] + | tuple[Any, None, float | None], + ], + ] +] = RunVar("lru_cache_items") + + +class _InitialMissingType: + pass + + +initial_missing: _InitialMissingType = _InitialMissingType() + + +class AsyncCacheInfo(NamedTuple): + hits: int + misses: int + maxsize: int | None + currsize: int + ttl: int | None + + +class AsyncCacheParameters(TypedDict): + maxsize: int | None + typed: bool + always_checkpoint: bool + ttl: int | None + + +class _LRUMethodWrapper(Generic[T]): + def __init__(self, wrapper: AsyncLRUCacheWrapper[..., T], instance: object): + self.__wrapper = wrapper + self.__instance = instance + + def cache_info(self) -> AsyncCacheInfo: + return self.__wrapper.cache_info() + + def cache_parameters(self) -> AsyncCacheParameters: + return self.__wrapper.cache_parameters() + + def cache_clear(self) -> None: + self.__wrapper.cache_clear() + + async def __call__(self, *args: Any, **kwargs: Any) -> T: + if self.__instance is None: + return await self.__wrapper(*args, **kwargs) + + return await self.__wrapper(self.__instance, *args, **kwargs) + + +@final +class AsyncLRUCacheWrapper(Generic[P, T]): + def __init__( + self, + func: Callable[P, Awaitable[T]], + maxsize: int | None, + typed: bool, + always_checkpoint: bool, + ttl: int | None, + ): + self.__wrapped__ = func + self._hits: int = 0 + self._misses: int = 0 + self._maxsize = max(maxsize, 0) if maxsize is not None else None + self._currsize: int = 0 + self._typed = typed + self._always_checkpoint = always_checkpoint + self._ttl = ttl + update_wrapper(self, func) + + def cache_info(self) -> AsyncCacheInfo: + return AsyncCacheInfo( + self._hits, self._misses, self._maxsize, self._currsize, self._ttl + ) + + def cache_parameters(self) -> AsyncCacheParameters: + return { + "maxsize": self._maxsize, + "typed": self._typed, + "always_checkpoint": self._always_checkpoint, + "ttl": self._ttl, + } + + def cache_clear(self) -> None: + if cache := lru_cache_items.get(None): + cache.pop(self, None) + self._hits = self._misses = self._currsize = 0 + + async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: + # Easy case first: if maxsize == 0, no caching is done + if self._maxsize == 0: + value = await self.__wrapped__(*args, **kwargs) + self._misses += 1 + return value + + # The key is constructed as a flat tuple to avoid memory overhead + key: tuple[Any, ...] = args + if kwargs: + # initial_missing is used as a separator + key += (initial_missing,) + sum(kwargs.items(), ()) + + if self._typed: + key += tuple(type(arg) for arg in args) + if kwargs: + key += (initial_missing,) + tuple(type(val) for val in kwargs.values()) + + try: + cache = lru_cache_items.get() + except LookupError: + cache = WeakKeyDictionary() + lru_cache_items.set(cache) + + try: + cache_entry = cache[self] + except KeyError: + cache_entry = cache[self] = OrderedDict() + + cached_value: T | _InitialMissingType + try: + cached_value, lock, expires_at = cache_entry[key] + except KeyError: + # We're the first task to call this function + cached_value, lock, expires_at = ( + initial_missing, + Lock(fast_acquire=not self._always_checkpoint), + None, + ) + cache_entry[key] = cached_value, lock, expires_at + + if lock is None: + if expires_at is not None and current_time() >= expires_at: + self._currsize -= 1 + cached_value, lock, expires_at = ( + initial_missing, + Lock(fast_acquire=not self._always_checkpoint), + None, + ) + cache_entry[key] = cached_value, lock, expires_at + else: + # The value was already cached + self._hits += 1 + cache_entry.move_to_end(key) + if self._always_checkpoint: + await checkpoint() + + return cast(T, cached_value) + + async with lock: + # Check if another task filled the cache while we acquired the lock + if (cached_value := cache_entry[key][0]) is initial_missing: + self._misses += 1 + if self._maxsize is not None and self._currsize >= self._maxsize: + cache_entry.popitem(last=False) + else: + self._currsize += 1 + + value = await self.__wrapped__(*args, **kwargs) + expires_at = ( + current_time() + self._ttl if self._ttl is not None else None + ) + cache_entry[key] = value, None, expires_at + else: + # Another task filled the cache while we were waiting for the lock + self._hits += 1 + cache_entry.move_to_end(key) + value = cast(T, cached_value) + + return value + + def __get__( + self, instance: object, owner: type | None = None + ) -> _LRUMethodWrapper[T]: + wrapper = _LRUMethodWrapper(self, instance) + update_wrapper(wrapper, self.__wrapped__) + return wrapper + + +class _LRUCacheWrapper: + def __init__( + self, maxsize: int | None, typed: bool, always_checkpoint: bool, ttl: int | None + ): + self._maxsize = maxsize + self._typed = typed + self._always_checkpoint = always_checkpoint + self._ttl = ttl + + @overload + def __call__( # type: ignore[overload-overlap] + self, func: Callable[P, Coroutine[Any, Any, T]], / + ) -> AsyncLRUCacheWrapper[P, T]: ... + + @overload + def __call__( + self, func: Callable[..., T], / + ) -> functools._lru_cache_wrapper[T]: ... + + def __call__( + self, f: Callable[P, Coroutine[Any, Any, T]] | Callable[..., T], / + ) -> AsyncLRUCacheWrapper[P, T] | functools._lru_cache_wrapper[T]: + if iscoroutinefunction(f): + return AsyncLRUCacheWrapper( + f, self._maxsize, self._typed, self._always_checkpoint, self._ttl + ) + + return functools.lru_cache(maxsize=self._maxsize, typed=self._typed)(f) # type: ignore[arg-type] + + +@overload +def cache( # type: ignore[overload-overlap] + func: Callable[P, Coroutine[Any, Any, T]], / +) -> AsyncLRUCacheWrapper[P, T]: ... + + +@overload +def cache(func: Callable[..., T], /) -> functools._lru_cache_wrapper[T]: ... + + +def cache(func: Callable[..., Any] | Callable[P, Coroutine[Any, Any, Any]], /) -> Any: + """ + A convenient shortcut for :func:`lru_cache` with ``maxsize=None``. + + This is the asynchronous equivalent to :func:`functools.cache`. + + """ + return lru_cache(maxsize=None)(func) + + +@overload +def lru_cache( + *, + maxsize: int | None = ..., + typed: bool = ..., + always_checkpoint: bool = ..., + ttl: int | None = ..., +) -> _LRUCacheWrapper: ... + + +@overload +def lru_cache( # type: ignore[overload-overlap] + func: Callable[P, Coroutine[Any, Any, T]], / +) -> AsyncLRUCacheWrapper[P, T]: ... + + +@overload +def lru_cache(func: Callable[..., T], /) -> functools._lru_cache_wrapper[T]: ... + + +def lru_cache( + func: Callable[..., Coroutine[Any, Any, Any]] | Callable[..., Any] | None = None, + /, + *, + maxsize: int | None = 128, + typed: bool = False, + always_checkpoint: bool = False, + ttl: int | None = None, +) -> Any: + """ + An asynchronous version of :func:`functools.lru_cache`. + + If a synchronous function is passed, the standard library + :func:`functools.lru_cache` is applied instead. + + :param always_checkpoint: if ``True``, every call to the cached function will be + guaranteed to yield control to the event loop at least once + :param ttl: time in seconds after which to invalidate cache entries + + .. note:: Caches and locks are managed on a per-event loop basis. + + """ + if func is None: + return _LRUCacheWrapper(maxsize, typed, always_checkpoint, ttl) + + if not callable(func): + raise TypeError("the first argument must be callable") + + return _LRUCacheWrapper(maxsize, typed, always_checkpoint, ttl)(func) + + +@overload +async def reduce( + function: Callable[[T, S], Awaitable[T]], + iterable: Iterable[S] | AsyncIterable[S], + /, + initial: T, +) -> T: ... + + +@overload +async def reduce( + function: Callable[[T, T], Awaitable[T]], + iterable: Iterable[T] | AsyncIterable[T], + /, +) -> T: ... + + +async def reduce( # type: ignore[misc] + function: Callable[[T, T], Awaitable[T]] | Callable[[T, S], Awaitable[T]], + iterable: Iterable[T] | Iterable[S] | AsyncIterable[T] | AsyncIterable[S], + /, + initial: T | _InitialMissingType = initial_missing, +) -> T: + """ + Asynchronous version of :func:`functools.reduce`. + + :param function: a coroutine function that takes two arguments: the accumulated + value and the next element from the iterable + :param iterable: an iterable or async iterable + :param initial: the initial value (if missing, the first element of the iterable is + used as the initial value) + + """ + element: Any + function_called = False + if isinstance(iterable, AsyncIterable): + async_it = iterable.__aiter__() + if initial is initial_missing: + try: + value = cast(T, await async_it.__anext__()) + except StopAsyncIteration: + raise TypeError( + "reduce() of empty sequence with no initial value" + ) from None + else: + value = cast(T, initial) + + async for element in async_it: + value = await function(value, element) + function_called = True + elif isinstance(iterable, Iterable): + it = iter(iterable) + if initial is initial_missing: + try: + value = cast(T, next(it)) + except StopIteration: + raise TypeError( + "reduce() of empty sequence with no initial value" + ) from None + else: + value = cast(T, initial) + + for element in it: + value = await function(value, element) + function_called = True + else: + raise TypeError("reduce() argument 2 must be an iterable or async iterable") + + # Make sure there is at least one checkpoint, even if an empty iterable and an + # initial value were given + if not function_called: + await checkpoint() + + return value diff --git a/source/target-vendors/cp312/anyio/itertools.py b/source/target-vendors/cp312/anyio/itertools.py new file mode 100644 index 0000000..2a10cfd --- /dev/null +++ b/source/target-vendors/cp312/anyio/itertools.py @@ -0,0 +1,629 @@ +from __future__ import annotations + +__all__ = ( + "Chain", + "accumulate", + "batched", + "combinations", + "combinations_with_replacement", + "compress", + "count", + "cycle", + "dropwhile", + "filterfalse", + "groupby", + "islice", + "pairwise", + "permutations", + "product", + "repeat", + "starmap", + "takewhile", + "tee", + "zip_longest", +) + +import itertools +import operator +import sys +from collections.abc import ( + AsyncGenerator, + AsyncIterable, + AsyncIterator, + Awaitable, + Callable, + Iterable, + Iterator, +) +from dataclasses import dataclass, field +from typing import Any, Generic, TypeVar, cast, overload + +from ._core._synchronization import Lock +from ._core._tasks import CancelScope +from .lowlevel import cancel_shielded_checkpoint, checkpoint, checkpoint_if_cancelled + +if sys.version_info < (3, 15): + from typing_extensions import sentinel + +T = TypeVar("T") +R = TypeVar("R") +_tee_end = sentinel("_tee_end") + + +@dataclass(eq=False) +class _IterableAsyncIterator(AsyncIterator[T]): + iterator: Iterator[T] + + async def __anext__(self) -> T: + await checkpoint_if_cancelled() + try: + result = next(self.iterator) + except StopIteration: + await cancel_shielded_checkpoint() + raise StopAsyncIteration from None + + await cancel_shielded_checkpoint() + return result + + +def _iterate(iterable: Iterable[T] | AsyncIterable[T]) -> AsyncIterator[T]: + if isinstance(iterable, AsyncIterator): + return iterable + + if isinstance(iterable, AsyncIterable): + return iterable.__aiter__() + + return _IterableAsyncIterator(iter(iterable)) + + +@dataclass(eq=False) +class _TeeLink(Generic[T]): + value: object | None = None + next: _TeeLink[T] | None = None + filled: bool = False + + +@dataclass(eq=False) +class _TeeState(Generic[T]): + iterator: AsyncIterator[T] + lock: Lock = field(default_factory=Lock) + + async def fill(self, link: _TeeLink[T]) -> bool: + if link.filled: + return False + + async with self.lock: + if link.filled: + return True + + link.value = await anext(self.iterator, _tee_end) + if link.value is not _tee_end: + link.next = _TeeLink() + + link.filled = True + return True + + +class _TeeAsyncIterator(AsyncIterator[T]): + _state: _TeeState[T] + _link: _TeeLink[T] + _element_yielded: bool + + def __init__( + self, iterable: Iterable[T] | AsyncIterable[T] | _TeeAsyncIterator[T] + ) -> None: + if isinstance(iterable, _TeeAsyncIterator): + self._state = iterable._state + self._link = iterable._link + else: + self._state = _TeeState(_iterate(iterable)) + self._link = _TeeLink() + + self._element_yielded = False + + async def __anext__(self) -> T: + had_yieldpoint = await self._state.fill(self._link) + if self._link.value is _tee_end: + if not self._element_yielded: + await checkpoint() + + raise StopAsyncIteration + + if not had_yieldpoint: + await checkpoint_if_cancelled() + + self._element_yielded = True + value = cast(T, self._link.value) + next_link = self._link.next + assert next_link is not None + self._link = next_link + if not had_yieldpoint: + await cancel_shielded_checkpoint() + + return value + + +async def _operator_add(x: T, y: T) -> T: + return operator.add(x, y) + + +async def accumulate( + iterable: Iterable[T] | AsyncIterable[T], + function: Callable[[T, T], Awaitable[T]] = _operator_add, + *, + initial: T | None = None, +) -> AsyncGenerator[T, None]: + iterator = _iterate(iterable) + if initial is None: + try: + total = await anext(iterator) + except StopAsyncIteration: + await checkpoint() + return + else: + await checkpoint_if_cancelled() + total = initial + await cancel_shielded_checkpoint() + + yield total + + async for element in iterator: + total = await function(total, element) + yield total + + +async def batched( + iterable: Iterable[T] | AsyncIterable[T], n: int, *, strict: bool = False +) -> AsyncGenerator[tuple[T, ...], None]: + if n < 1: + raise ValueError("n must be at least one") + + iterator = _iterate(iterable) + + while True: + batch: list[T] = [] + for _ in range(n): + try: + batch.append(await anext(iterator)) + except StopAsyncIteration: + if not batch: + await checkpoint() + return + if strict: + raise ValueError("batched(): incomplete batch") from None + + yield tuple(batch) + return + + yield tuple(batch) + + +class Chain: + def __call__( + self, *iterables: Iterable[T] | AsyncIterable[T] + ) -> AsyncGenerator[T, None]: + return self.from_iterable(iterables) + + async def from_iterable( + self, + iterables: ( + Iterable[Iterable[T] | AsyncIterable[T]] + | AsyncIterable[Iterable[T] | AsyncIterable[T]] + ), + ) -> AsyncGenerator[T, None]: + element_yielded = False + outer_iter = _iterate(iterables) + + try: + async for iterable in outer_iter: + async for element in _iterate(iterable): + element_yielded = True + yield element + finally: + aclose = getattr(outer_iter, "aclose", None) + if aclose is not None: + with CancelScope(shield=True): + await aclose() + + if not element_yielded: + await checkpoint() + + +chain: Chain = Chain() + + +async def combinations( + iterable: Iterable[T] | AsyncIterable[T], r: int +) -> AsyncGenerator[tuple[T, ...], None]: + pool: list[T] = [element async for element in _iterate(iterable)] + async for combination in _iterate(itertools.combinations(pool, r)): + yield combination + + +async def combinations_with_replacement( + iterable: Iterable[T] | AsyncIterable[T], r: int +) -> AsyncGenerator[tuple[T, ...], None]: + pool: list[T] = [element async for element in _iterate(iterable)] + async for combination in _iterate(itertools.combinations_with_replacement(pool, r)): + yield combination + + +async def compress( + data: Iterable[T] | AsyncIterable[T], + selectors: Iterable[object] | AsyncIterable[object], +) -> AsyncGenerator[T, None]: + data_iterator = _iterate(data) + selector_iterator = _iterate(selectors) + element_yielded = False + + while True: + try: + datum = await anext(data_iterator) + selector = await anext(selector_iterator) + except StopAsyncIteration: + if not element_yielded: + await checkpoint() + + return + + if selector: + element_yielded = True + yield datum + + +async def count(start: int = 0, step: int = 1) -> AsyncGenerator[int, None]: + n = start + while True: + await checkpoint_if_cancelled() + value = n + n += step + await cancel_shielded_checkpoint() + yield value + + +async def cycle( + iterable: Iterable[T] | AsyncIterable[T], +) -> AsyncGenerator[T, None]: + saved: list[T] = [] + async for element in _iterate(iterable): + saved.append(element) + yield element + + if not saved: + await checkpoint() + return + + while True: + for element in saved: + await checkpoint() + yield element + + +async def dropwhile( + predicate: Callable[[T], Awaitable[object]], + iterable: Iterable[T] | AsyncIterable[T], +) -> AsyncGenerator[T, None]: + element_yielded = False + dropping = True + + async for element in _iterate(iterable): + if dropping and await predicate(element): + continue + + dropping = False + element_yielded = True + yield element + + if not element_yielded: + await checkpoint() + + +async def filterfalse( + predicate: Callable[[T], Awaitable[object]], + iterable: Iterable[T] | AsyncIterable[T], +) -> AsyncGenerator[T, None]: + element_yielded = False + + async for element in _iterate(iterable): + if not await predicate(element): + element_yielded = True + yield element + + if not element_yielded: + await checkpoint() + + +@overload +def groupby( + iterable: Iterable[T] | AsyncIterable[T], +) -> AsyncGenerator[tuple[T, list[T]], None]: ... + + +@overload +def groupby( + iterable: Iterable[T] | AsyncIterable[T], + key: Callable[[T], Awaitable[R]], +) -> AsyncGenerator[tuple[R, list[T]], None]: ... + + +async def groupby( + iterable: Iterable[T] | AsyncIterable[T], + key: Callable[[T], Awaitable[object]] | None = None, +) -> AsyncGenerator[tuple[object, list[T]], None]: + iterator = _iterate(iterable) + try: + element = await anext(iterator) + except StopAsyncIteration: + await checkpoint() + return + + group_key = element if key is None else await key(element) + values = [element] + + async for element in iterator: + next_key = element if key is None else await key(element) + if next_key != group_key: + completed_group = group_key, values + group_key = next_key + values = [element] + yield completed_group + else: + values.append(element) + + yield group_key, values + + +@overload +def islice( + iterable: Iterable[T] | AsyncIterable[T], + stop: int | None, + /, +) -> AsyncGenerator[T, None]: ... + + +@overload +def islice( + iterable: Iterable[T] | AsyncIterable[T], + start: int | None, + stop: int | None, + step: int | None = 1, + /, +) -> AsyncGenerator[T, None]: ... + + +async def islice( + iterable: Iterable[T] | AsyncIterable[T], + *args: int | None, +) -> AsyncGenerator[T, None]: + if not args: + raise TypeError("islice expected at least 2 arguments, got 1") + if len(args) > 3: + raise TypeError(f"islice expected at most 4 arguments, got {len(args) + 1}") + + slice_args = slice(*args) + + start_message = ( + "Indices for islice() must be None or an integer: 0 <= x <= sys.maxsize." + ) + stop_message = ( + "Stop argument for islice() must be None or an integer: 0 <= x <= sys.maxsize." + ) + step_message = "Step for islice() must be a positive integer or None." + + def normalize_index(value: object, message: str) -> int: + try: + index = operator.index(cast(Any, value)) + except TypeError: + raise ValueError(message) from None + + if index < 0 or index > sys.maxsize: + raise ValueError(message) + + return index + + start = ( + 0 + if slice_args.start is None + else normalize_index(slice_args.start, start_message) + ) + stop = ( + None + if slice_args.stop is None + else normalize_index(slice_args.stop, stop_message) + ) + step = ( + 1 if slice_args.step is None else normalize_index(slice_args.step, step_message) + ) + + if step <= 0: + raise ValueError(step_message) + + if stop == 0 or start == stop: + await checkpoint() + return + + iterator = _iterate(iterable) + index = 0 + element_yielded = False + + while stop is None or index < stop: + try: + element = await anext(iterator) + except StopAsyncIteration: + if not element_yielded: + await checkpoint() + + return + + if index >= start and (index - start) % step == 0: + index += 1 + element_yielded = True + yield element + else: + index += 1 + + if not element_yielded: + await checkpoint() + + +async def pairwise( + iterable: Iterable[T] | AsyncIterable[T], +) -> AsyncGenerator[tuple[T, T], None]: + iterator = _iterate(iterable) + try: + previous = await anext(iterator) + except StopAsyncIteration: + await checkpoint() + return + + element_yielded = False + async for element in iterator: + element_yielded = True + pair = (previous, element) + previous = element + yield pair + + if not element_yielded: + await checkpoint() + + +async def permutations( + iterable: Iterable[T] | AsyncIterable[T], r: int | None = None +) -> AsyncGenerator[tuple[T, ...], None]: + pool: list[T] = [element async for element in _iterate(iterable)] + n = len(pool) + if r is None: + r = n + elif not isinstance(r, int): + raise TypeError("Expected int as r") + elif r < 0: + raise ValueError("r must be non-negative") + + async for permutation in _iterate(itertools.permutations(pool, r)): + yield permutation + + +async def product( + *iterables: Iterable[T] | AsyncIterable[T], repeat: int = 1 +) -> AsyncGenerator[tuple[T, ...], None]: + repeat = operator.index(repeat) + if repeat < 0: + raise ValueError("repeat argument cannot be negative") + + pools: list[tuple[T, ...]] = [] + for iterable in iterables: + pool: list[T] = [element async for element in _iterate(iterable)] + pools.append(tuple(pool)) + + async for value in _iterate(itertools.product(*pools, repeat=repeat)): + yield value + + +async def repeat(element: T, times: int | None = None) -> AsyncGenerator[T, None]: + if times is None: + while True: + await checkpoint() + yield element + + remaining = operator.index(cast(Any, times)) + if remaining <= 0: + await checkpoint() + return + + while remaining > 0: + await checkpoint_if_cancelled() + remaining -= 1 + await cancel_shielded_checkpoint() + yield element + + +async def starmap( + function: Callable[..., Awaitable[R]], + iterable: ( + Iterable[Iterable[object] | AsyncIterable[object]] + | AsyncIterable[Iterable[object] | AsyncIterable[object]] + ), +) -> AsyncGenerator[R, None]: + result_yielded = False + + async for args_iterable in _iterate(iterable): + args = [element async for element in _iterate(args_iterable)] + result_yielded = True + yield await function(*args) + + if not result_yielded: + await checkpoint() + + +def tee( + iterable: Iterable[T] | AsyncIterable[T], n: int = 2 +) -> tuple[AsyncIterator[T], ...]: + n = operator.index(cast(Any, n)) + if n < 0: + raise ValueError("n must be >= 0") + if n == 0: + return () + + iterator = _TeeAsyncIterator(iterable) + iterators: list[AsyncIterator[T]] = [iterator] + iterators.extend(_TeeAsyncIterator(iterator) for _ in range(n - 1)) + return tuple(iterators) + + +async def takewhile( + predicate: Callable[[T], Awaitable[object]], + iterable: Iterable[T] | AsyncIterable[T], +) -> AsyncGenerator[T, None]: + element_yielded = False + + async for element in _iterate(iterable): + if not await predicate(element): + if not element_yielded: + await checkpoint() + + return + + element_yielded = True + yield element + + if not element_yielded: + await checkpoint() + + +async def zip_longest( + *iterables: Iterable[object] | AsyncIterable[object], + fillvalue: object = None, +) -> AsyncGenerator[tuple[object, ...], None]: + iterators = [_iterate(iterable) for iterable in iterables] + num_active = len(iterators) + if not num_active: + await checkpoint() + return + + active = [True] * num_active + tuple_yielded = False + + while True: + values: list[object] = [] + for index, iterator in enumerate(iterators): + if not active[index]: + values.append(fillvalue) + continue + + try: + value = await anext(iterator) + except StopAsyncIteration: + active[index] = False + num_active -= 1 + if not num_active: + if not tuple_yielded: + await checkpoint() + + return + + value = fillvalue + + values.append(value) + + tuple_yielded = True + yield tuple(values) diff --git a/source/target-vendors/cp312/anyio/lowlevel.py b/source/target-vendors/cp312/anyio/lowlevel.py new file mode 100644 index 0000000..361b2eb --- /dev/null +++ b/source/target-vendors/cp312/anyio/lowlevel.py @@ -0,0 +1,228 @@ +from __future__ import annotations + +__all__ = ( + "EventLoopToken", + "RunVar", + "RunvarToken", + "cancel_shielded_checkpoint", + "checkpoint", + "checkpoint_if_cancelled", + "current_token", +) + +import enum +from dataclasses import dataclass +from types import TracebackType +from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, final, overload +from weakref import WeakKeyDictionary + +from ._core._eventloop import get_async_backend + +if TYPE_CHECKING: + from .abc import AsyncBackend + +T = TypeVar("T") +D = TypeVar("D") + + +async def checkpoint() -> None: + """ + Check for cancellation and allow the scheduler to switch to another task. + + Equivalent to (but more efficient than):: + + await checkpoint_if_cancelled() + await cancel_shielded_checkpoint() + + .. versionadded:: 3.0 + + """ + await get_async_backend().checkpoint() + + +async def checkpoint_if_cancelled() -> None: + """ + Enter a checkpoint if the enclosing cancel scope has been cancelled. + + This does not allow the scheduler to switch to a different task. + + .. versionadded:: 3.0 + + """ + await get_async_backend().checkpoint_if_cancelled() + + +async def cancel_shielded_checkpoint() -> None: + """ + Allow the scheduler to switch to another task but without checking for cancellation. + + Equivalent to (but potentially more efficient than):: + + with CancelScope(shield=True): + await checkpoint() + + .. versionadded:: 3.0 + + """ + await get_async_backend().cancel_shielded_checkpoint() + + +@final +@dataclass(frozen=True, repr=False) +class EventLoopToken: + """ + An opaque object that holds a reference to an event loop. + + .. versionadded:: 4.11.0 + """ + + backend_class: type[AsyncBackend] + native_token: object + + +def current_token() -> EventLoopToken: + """ + Return a token object that can be used to call code in the current event loop from + another thread. + + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + .. versionadded:: 4.11.0 + + """ + backend_class = get_async_backend() + raw_token = backend_class.current_token() + return EventLoopToken(backend_class, raw_token) + + +_run_vars: WeakKeyDictionary[object, dict[RunVar[Any], Any]] = WeakKeyDictionary() + + +class _NoValueSet(enum.Enum): + NO_VALUE_SET = enum.auto() + + +class RunvarToken(Generic[T]): + """ + A token that can be used to restore a :class:`RunVar` to its previous value. + + Returned by :meth:`RunVar.set`. Can be used as a context manager to automatically + reset the variable on exit, or passed directly to :meth:`RunVar.reset`. + """ + + __slots__ = "_redeemed", "_value", "_var" + + def __init__(self, var: RunVar[T], value: T | Literal[_NoValueSet.NO_VALUE_SET]): + self._var = var + self._value: T | Literal[_NoValueSet.NO_VALUE_SET] = value + self._redeemed = False + + def __enter__(self) -> RunvarToken[T]: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self._var.reset(self) + + +class RunVar(Generic[T]): + """ + Like a :class:`~contextvars.ContextVar`, except scoped to the running event loop. + + Can be used as a context manager, Just like :class:`~contextvars.ContextVar`, that + will reset the variable to its previous value when the context block is exited. + """ + + __slots__ = "_default", "_name" + + NO_VALUE_SET: Literal[_NoValueSet.NO_VALUE_SET] = _NoValueSet.NO_VALUE_SET + + def __init__( + self, name: str, default: T | Literal[_NoValueSet.NO_VALUE_SET] = NO_VALUE_SET + ): + self._name = name + self._default = default + + @property + def _current_vars(self) -> dict[RunVar[T], T]: + native_token = current_token().native_token + try: + return _run_vars[native_token] + except KeyError: + run_vars = _run_vars[native_token] = {} + return run_vars + + @overload + def get(self, default: D) -> T | D: ... + + @overload + def get(self) -> T: ... + + def get( + self, default: D | Literal[_NoValueSet.NO_VALUE_SET] = NO_VALUE_SET + ) -> T | D: + """ + Return the current value of this run variable. + + :param default: a fallback value to return if no value has been set + :return: the current value, the provided default, or the variable's own default + :raises LookupError: if no value is set and no default is available + + """ + try: + return self._current_vars[self] + except KeyError: + if default is not RunVar.NO_VALUE_SET: + return default + elif self._default is not RunVar.NO_VALUE_SET: + return self._default + + raise LookupError( + f'Run variable "{self._name}" has no value and no default set' + ) + + def set(self, value: T) -> RunvarToken[T]: + """ + Set the value of this run variable for the current event loop. + + :param value: the new value + :return: a token that can be used to restore the previous value + + """ + current_vars = self._current_vars + token = RunvarToken(self, current_vars.get(self, RunVar.NO_VALUE_SET)) + current_vars[self] = value + return token + + def reset(self, token: RunvarToken[T]) -> None: + """ + Restore this run variable to the value it held before the matching :meth:`set`. + + :param token: the token returned by :meth:`set` + :raises ValueError: if the token belongs to a different :class:`RunVar` or the token + has already been used + + """ + if token._var is not self: + raise ValueError("This token does not belong to this RunVar") + + if token._redeemed: + raise ValueError("This token has already been used") + + if token._value is _NoValueSet.NO_VALUE_SET: + try: + del self._current_vars[self] + except KeyError: + pass + else: + self._current_vars[self] = token._value + + token._redeemed = True + + def __repr__(self) -> str: + return f"" diff --git a/source/target-vendors/cp312/anyio/py.typed b/source/target-vendors/cp312/anyio/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/anyio/pytest_plugin.py b/source/target-vendors/cp312/anyio/pytest_plugin.py new file mode 100644 index 0000000..d6238cd --- /dev/null +++ b/source/target-vendors/cp312/anyio/pytest_plugin.py @@ -0,0 +1,404 @@ +from __future__ import annotations + +import dataclasses +import socket +import sys +from collections.abc import Callable, Generator, Iterator +from contextlib import ExitStack, contextmanager +from inspect import isasyncgenfunction, iscoroutinefunction, ismethod +from typing import TYPE_CHECKING, Any, cast + +import pytest +from _pytest.fixtures import FuncFixtureInfo, SubRequest +from _pytest.outcomes import Exit +from _pytest.scope import Scope + +from . import get_available_backends +from ._core._eventloop import ( + current_async_library, + get_async_backend, + reset_current_async_library, + set_current_async_library, +) +from ._core._exceptions import iterate_exceptions + +if TYPE_CHECKING: + from .abc import TestRunner + +if sys.version_info < (3, 11): + from exceptiongroup import ExceptionGroup + +if TYPE_CHECKING: + # pytest >= 9.2 keeps CallSpec2 as a TYPE_CHECKING-only alias of CallSpec + from _pytest.python import CallSpec2 as CallSpec +else: + try: + # c.f. https://github.com/pytest-dev/pytest/pull/14742 + # pytest >= 9.2 + from _pytest.python import CallSpec + except ImportError: + from _pytest.python import CallSpec2 as CallSpec + +_current_runner: TestRunner | None = None +_runner_stack: ExitStack | None = None +_runner_leases = 0 + + +def extract_backend_and_options(backend: object) -> tuple[str, dict[str, Any]]: + if isinstance(backend, str): + return backend, {} + elif isinstance(backend, tuple) and len(backend) == 2: + if isinstance(backend[0], str) and isinstance(backend[1], dict): + return cast(tuple[str, dict[str, Any]], backend) + + raise TypeError("anyio_backend must be either a string or tuple of (string, dict)") + + +@contextmanager +def get_runner( + backend_name: str, backend_options: dict[str, Any] +) -> Iterator[TestRunner]: + global _current_runner, _runner_leases, _runner_stack + if _current_runner is None: + asynclib = get_async_backend(backend_name) + _runner_stack = ExitStack() + if current_async_library() is None: + # Since we're in control of the event loop, we can cache the name of the + # async library + token = set_current_async_library(backend_name) + _runner_stack.callback(reset_current_async_library, token) + + backend_options = backend_options or {} + _current_runner = _runner_stack.enter_context( + asynclib.create_test_runner(backend_options) + ) + + _runner_leases += 1 + try: + yield _current_runner + finally: + _runner_leases -= 1 + if not _runner_leases: + assert _runner_stack is not None + _runner_stack.close() + _runner_stack = _current_runner = None + + +def pytest_addoption(parser: pytest.Parser) -> None: + group = parser.getgroup("anyio") + parser.addini( + "anyio_mode", + default="strict", + help='AnyIO plugin mode (either "strict" or "auto")', + ) + group.addoption( + "--anyio-mode", + default=None, + dest="anyio_mode", + help=""" + 'auto' - All async test functions will be handled by AnyIO pytest plugin + 'strict' - Disabling autoprocessing(useful when anyio tests + need to coexist with other async test plugins) + """, + ) + + +def _is_auto_mode(config: pytest.Config, mode: str) -> bool: + return ( + config.getoption(mode, default=None) == "auto" or config.getini(mode) == "auto" + ) + + +def pytest_configure(config: pytest.Config) -> None: + config.addinivalue_line( + "markers", + "anyio: mark the (coroutine function) test to be run asynchronously via anyio.", + ) + if ( + _is_auto_mode(config, "anyio_mode") + and config.pluginmanager.has_plugin("asyncio") + and _is_auto_mode(config, "asyncio_mode") + ): + config.issue_config_time_warning( + pytest.PytestConfigWarning( + "AnyIO auto mode has been enabled together with pytest-asyncio auto " + "mode. This may cause unexpected behavior." + ), + 1, + ) + + +@pytest.hookimpl(hookwrapper=True) +def pytest_fixture_setup(fixturedef: Any, request: Any) -> Generator[Any]: + def wrapper(anyio_backend: Any, request: SubRequest, **kwargs: Any) -> Any: + # Rebind any fixture methods to the request instance + if ( + request.instance + and ismethod(func) + and type(func.__self__) is type(request.instance) + ): + local_func = func.__func__.__get__(request.instance) + else: + local_func = func + + backend_name, backend_options = extract_backend_and_options(anyio_backend) + if has_backend_arg: + kwargs["anyio_backend"] = anyio_backend + + if has_request_arg: + kwargs["request"] = request + + with get_runner(backend_name, backend_options) as runner: + # re-entrant call into the test runner detected. this happens when an async fixture + # is dynamically requested via request.getfixturevalue() from inside a running async + # test or fixture. on asyncio this raises RuntimeError: This event loop is already + # running, on trio the runner deadlocks - the host loop blocks waiting for the + # coroutine to return, but the coroutine is waiting for the host loop. raising here + # prevents the hang and gives a consistent error across backends. + if runner.is_running(): + raise RuntimeError( + "Cannot schedule a coroutine in the test runner while another is already running; " + "likely caused by request.getfixturevalue() on an async fixture." + ) + + if isasyncgenfunction(local_func): + yield from runner.run_asyncgen_fixture(local_func, kwargs) + else: + yield runner.run_fixture(local_func, kwargs) + + # Only apply this to coroutine functions and async generator functions in requests + # that involve the anyio_backend fixture + func = fixturedef.func + if isasyncgenfunction(func) or iscoroutinefunction(func): + if "anyio_backend" in request.fixturenames: + fixturedef.func = wrapper + original_argname = fixturedef.argnames + + if not (has_backend_arg := "anyio_backend" in fixturedef.argnames): + fixturedef.argnames += ("anyio_backend",) + + if not (has_request_arg := "request" in fixturedef.argnames): + fixturedef.argnames += ("request",) + + try: + return (yield) + finally: + fixturedef.func = func + fixturedef.argnames = original_argname + + return (yield) + + +@pytest.hookimpl(tryfirst=True) +def pytest_pycollect_makeitem( + collector: pytest.Module | pytest.Class, name: str, obj: object +) -> None: + if collector.istestfunction(obj, name): + inner_func = obj.hypothesis.inner_test if hasattr(obj, "hypothesis") else obj + if iscoroutinefunction(inner_func): + anyio_auto_mode = _is_auto_mode(collector.config, "anyio_mode") + marker = collector.get_closest_marker("anyio") + own_markers = getattr(obj, "pytestmark", ()) + if ( + anyio_auto_mode + or marker + or any(marker.name == "anyio" for marker in own_markers) + ): + pytest.mark.usefixtures("anyio_backend")(obj) + + +def pytest_collection_finish(session: pytest.Session) -> None: + for i, item in reversed(list(enumerate(session.items))): + if ( + isinstance(item, pytest.Function) + and iscoroutinefunction(item.function) + and item.get_closest_marker("anyio") is not None + and "anyio_backend" not in item.fixturenames + ): + new_items = [] + try: + cs_fields = {f.name for f in dataclasses.fields(CallSpec)} + except TypeError: + cs_fields = set() + + for param_index, backend in enumerate(get_available_backends()): + if "_arg2scope" in cs_fields: # pytest >= 8 + callspec = CallSpec( + params={"anyio_backend": backend}, + indices={"anyio_backend": param_index}, + _arg2scope={"anyio_backend": Scope.Module}, + _idlist=[backend], + marks=[], + ) + else: # pytest 7.x + callspec = CallSpec( # type: ignore[call-arg] + funcargs={}, + params={"anyio_backend": backend}, + indices={"anyio_backend": param_index}, + arg2scope={"anyio_backend": Scope.Module}, + idlist=[backend], + marks=[], + ) + + fi = item._fixtureinfo + new_names_closure = list(fi.names_closure) + if "anyio_backend" not in new_names_closure: + new_names_closure.append("anyio_backend") + + new_fixtureinfo = FuncFixtureInfo( + argnames=fi.argnames, + initialnames=fi.initialnames, + names_closure=new_names_closure, + name2fixturedefs=fi.name2fixturedefs, + ) + new_item = pytest.Function.from_parent( + item.parent, + name=f"{item.originalname}[{backend}]", + callspec=callspec, + callobj=item.obj, + fixtureinfo=new_fixtureinfo, + keywords=item.keywords, + originalname=item.originalname, + ) + new_items.append(new_item) + + session.items[i : i + 1] = new_items + + +@pytest.hookimpl(tryfirst=True) +def pytest_pyfunc_call(pyfuncitem: Any) -> bool | None: + def run_with_hypothesis(**kwargs: Any) -> None: + with get_runner(backend_name, backend_options) as runner: + runner.run_test(original_func, kwargs) + + backend = pyfuncitem.funcargs.get("anyio_backend") + if backend: + backend_name, backend_options = extract_backend_and_options(backend) + + if hasattr(pyfuncitem.obj, "hypothesis"): + # Wrap the inner test function unless it's already wrapped + original_func = pyfuncitem.obj.hypothesis.inner_test + if original_func.__qualname__ != run_with_hypothesis.__qualname__: + if iscoroutinefunction(original_func): + pyfuncitem.obj.hypothesis.inner_test = run_with_hypothesis + + return None + + if iscoroutinefunction(pyfuncitem.obj): + funcargs = pyfuncitem.funcargs + testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} + with get_runner(backend_name, backend_options) as runner: + try: + runner.run_test(pyfuncitem.obj, testargs) + except ExceptionGroup as excgrp: + for exc in iterate_exceptions(excgrp): + if isinstance(exc, (Exit, KeyboardInterrupt, SystemExit)): + raise exc from excgrp + + raise + + return True + + return None + + +@pytest.fixture(scope="module", params=get_available_backends()) +def anyio_backend(request: Any) -> Any: + return request.param + + +@pytest.fixture +def anyio_backend_name(anyio_backend: Any) -> str: + if isinstance(anyio_backend, str): + return anyio_backend + else: + return anyio_backend[0] + + +@pytest.fixture +def anyio_backend_options(anyio_backend: Any) -> dict[str, Any]: + if isinstance(anyio_backend, str): + return {} + else: + return anyio_backend[1] + + +class FreePortFactory: + """ + Manages port generation based on specified socket kind, ensuring no duplicate + ports are generated. + + This class provides functionality for generating available free ports on the + system. It is initialized with a specific socket kind and can generate ports + for given address families while avoiding reuse of previously generated ports. + + Users should not instantiate this class directly, but use the + ``free_tcp_port_factory`` and ``free_udp_port_factory`` fixtures instead. For simple + uses cases, ``free_tcp_port`` and ``free_udp_port`` can be used instead. + """ + + def __init__(self, kind: socket.SocketKind) -> None: + self._kind = kind + self._generated = set[int]() + + @property + def kind(self) -> socket.SocketKind: + """ + The type of socket connection (e.g., :data:`~socket.SOCK_STREAM` or + :data:`~socket.SOCK_DGRAM`) used to bind for checking port availability + + """ + return self._kind + + def __call__(self, family: socket.AddressFamily | None = None) -> int: + """ + Return an unbound port for the given address family. + + :param family: if omitted, both IPv4 and IPv6 addresses will be tried + :return: a port number + + """ + if family is not None: + families = [family] + else: + families = [socket.AF_INET] + if socket.has_ipv6: + families.append(socket.AF_INET6) + + while True: + port = 0 + with ExitStack() as stack: + for fam in families: + sock = stack.enter_context(socket.socket(fam, self._kind)) + addr = "::1" if fam == socket.AF_INET6 else "127.0.0.1" + try: + sock.bind((addr, port)) + except OSError: + break + + if not port: + port = sock.getsockname()[1] + else: + if port not in self._generated: + self._generated.add(port) + return port + + +@pytest.fixture(scope="session") +def free_tcp_port_factory() -> FreePortFactory: + return FreePortFactory(socket.SOCK_STREAM) + + +@pytest.fixture(scope="session") +def free_udp_port_factory() -> FreePortFactory: + return FreePortFactory(socket.SOCK_DGRAM) + + +@pytest.fixture +def free_tcp_port(free_tcp_port_factory: Callable[[], int]) -> int: + return free_tcp_port_factory() + + +@pytest.fixture +def free_udp_port(free_udp_port_factory: Callable[[], int]) -> int: + return free_udp_port_factory() diff --git a/source/target-vendors/cp312/anyio/streams/__init__.py b/source/target-vendors/cp312/anyio/streams/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/anyio/streams/buffered.py b/source/target-vendors/cp312/anyio/streams/buffered.py new file mode 100644 index 0000000..a3b07f7 --- /dev/null +++ b/source/target-vendors/cp312/anyio/streams/buffered.py @@ -0,0 +1,201 @@ +from __future__ import annotations + +__all__ = ( + "BufferedByteReceiveStream", + "BufferedByteStream", + "BufferedConnectable", +) + +import sys +from collections.abc import Callable, Iterable, Mapping +from dataclasses import dataclass, field +from typing import Any, SupportsIndex + +from .. import ClosedResourceError, DelimiterNotFound, EndOfStream, IncompleteRead +from ..abc import ( + AnyByteReceiveStream, + AnyByteStream, + AnyByteStreamConnectable, + ByteReceiveStream, + ByteStream, + ByteStreamConnectable, +) + +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + + +@dataclass(eq=False) +class BufferedByteReceiveStream(ByteReceiveStream): + """ + Wraps any bytes-based receive stream and uses a buffer to provide sophisticated + receiving capabilities in the form of a byte stream. + """ + + receive_stream: AnyByteReceiveStream + _buffer: bytearray = field(init=False, default_factory=bytearray) + _closed: bool = field(init=False, default=False) + + async def aclose(self) -> None: + await self.receive_stream.aclose() + self._closed = True + + @property + def buffer(self) -> bytes: + """The bytes currently in the buffer.""" + return bytes(self._buffer) + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return self.receive_stream.extra_attributes + + def feed_data(self, data: Iterable[SupportsIndex], /) -> None: + """ + Append data directly into the buffer. + + Any data in the buffer will be consumed by receive operations before receiving + anything from the wrapped stream. + + :param data: the data to append to the buffer (can be bytes or anything else + that supports ``__index__()``) + + """ + self._buffer.extend(data) + + async def receive(self, max_bytes: int = 65536) -> bytes: + if max_bytes < 1: + raise ValueError("max_bytes must be a positive integer") + + if self._closed: + raise ClosedResourceError + + if self._buffer: + chunk = bytes(self._buffer[:max_bytes]) + del self._buffer[:max_bytes] + return chunk + elif isinstance(self.receive_stream, ByteReceiveStream): + return await self.receive_stream.receive(max_bytes) + else: + # With a bytes-oriented object stream, we need to handle any surplus bytes + # we get from the receive() call + chunk = await self.receive_stream.receive() + if len(chunk) > max_bytes: + # Save the surplus bytes in the buffer + self._buffer.extend(chunk[max_bytes:]) + return chunk[:max_bytes] + else: + return chunk + + async def receive_exactly(self, nbytes: int) -> bytes: + """ + Read exactly the given amount of bytes from the stream. + + :param nbytes: the number of bytes to read + :return: the bytes read + :raises ~anyio.IncompleteRead: if the stream was closed before the requested + amount of bytes could be read from the stream + + """ + while True: + remaining = nbytes - len(self._buffer) + if remaining <= 0: + retval = self._buffer[:nbytes] + del self._buffer[:nbytes] + return bytes(retval) + + try: + if isinstance(self.receive_stream, ByteReceiveStream): + chunk = await self.receive_stream.receive(remaining) + else: + chunk = await self.receive_stream.receive() + except EndOfStream as exc: + raise IncompleteRead from exc + + self._buffer.extend(chunk) + + async def receive_until(self, delimiter: bytes, max_bytes: int) -> bytes: + """ + Read from the stream until the delimiter is found or max_bytes have been read. + + :param delimiter: the marker to look for in the stream + :param max_bytes: maximum number of bytes that will be read before raising + :exc:`~anyio.DelimiterNotFound` + :return: the bytes read (not including the delimiter) + :raises ~anyio.IncompleteRead: if the stream was closed before the delimiter + was found + :raises ~anyio.DelimiterNotFound: if the delimiter is not found within the + bytes read up to the maximum allowed + + """ + delimiter_size = len(delimiter) + offset = 0 + while True: + # Check if the delimiter can be found in the current buffer + index = self._buffer.find(delimiter, offset) + if index >= 0: + found = self._buffer[:index] + del self._buffer[: index + len(delimiter) :] + return bytes(found) + + # Check if the buffer is already at or over the limit + if len(self._buffer) >= max_bytes: + raise DelimiterNotFound(max_bytes) + + # Read more data into the buffer from the socket + try: + data = await self.receive_stream.receive() + except EndOfStream as exc: + raise IncompleteRead from exc + + # Move the offset forward and add the new data to the buffer + offset = max(len(self._buffer) - delimiter_size + 1, 0) + self._buffer.extend(data) + + +class BufferedByteStream(BufferedByteReceiveStream, ByteStream): + """ + A full-duplex variant of :class:`BufferedByteReceiveStream`. All writes are passed + through to the wrapped stream as-is. + """ + + def __init__(self, stream: AnyByteStream): + """ + :param stream: the stream to be wrapped + + """ + super().__init__(stream) + self._stream = stream + + @override + async def send_eof(self) -> None: + await self._stream.send_eof() + + @override + async def send(self, item: bytes) -> None: + await self._stream.send(item) + + +class BufferedConnectable(ByteStreamConnectable): + """ + Wraps a byte stream connectable to produce :class:`BufferedByteStream` connections. + + Use this when you want the streams returned by :meth:`connect` to have the buffered + receive API (e.g. :meth:`~BufferedByteReceiveStream.receive_exactly` and + :meth:`~BufferedByteReceiveStream.receive_until`). + + :param connectable: the byte stream connectable to wrap + """ + + def __init__(self, connectable: AnyByteStreamConnectable): + """ + :param connectable: the connectable to wrap + + """ + self.connectable = connectable + + @override + async def connect(self) -> BufferedByteStream: + stream = await self.connectable.connect() + return BufferedByteStream(stream) diff --git a/source/target-vendors/cp312/anyio/streams/file.py b/source/target-vendors/cp312/anyio/streams/file.py new file mode 100644 index 0000000..b9d7ca4 --- /dev/null +++ b/source/target-vendors/cp312/anyio/streams/file.py @@ -0,0 +1,157 @@ +from __future__ import annotations + +__all__ = ( + "FileReadStream", + "FileStreamAttribute", + "FileWriteStream", +) + +from collections.abc import Callable, Mapping +from io import SEEK_SET, UnsupportedOperation +from os import PathLike +from pathlib import Path +from typing import IO, Any + +from .. import ( + BrokenResourceError, + ClosedResourceError, + EndOfStream, + TypedAttributeSet, + to_thread, + typed_attribute, +) +from ..abc import ByteReceiveStream, ByteSendStream + + +class FileStreamAttribute(TypedAttributeSet): + #: the open file descriptor + file: IO[bytes] = typed_attribute() + #: the path of the file on the file system, if available (file must be a real file) + path: Path = typed_attribute() + #: the file number, if available (file must be a real file or a TTY) + fileno: int = typed_attribute() + + +class _BaseFileStream: + def __init__(self, file: IO[bytes]): + self._file = file + + async def aclose(self) -> None: + await to_thread.run_sync(self._file.close) + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + attributes: dict[Any, Callable[[], Any]] = { + FileStreamAttribute.file: lambda: self._file, + } + + if hasattr(self._file, "name"): + attributes[FileStreamAttribute.path] = lambda: Path(self._file.name) + + try: + self._file.fileno() + except UnsupportedOperation: + pass + else: + attributes[FileStreamAttribute.fileno] = lambda: self._file.fileno() + + return attributes + + +class FileReadStream(_BaseFileStream, ByteReceiveStream): + """ + A byte stream that reads from a file in the file system. + + :param file: a file that has been opened for reading in binary mode + + .. versionadded:: 3.0 + """ + + @classmethod + async def from_path(cls, path: str | PathLike[str]) -> FileReadStream: + """ + Create a file read stream by opening the given file. + + :param path: path of the file to read from + + """ + file = await to_thread.run_sync(Path(path).open, "rb") + return cls(file) + + async def receive(self, max_bytes: int = 65536) -> bytes: + if max_bytes < 1: + raise ValueError("max_bytes must be a positive integer") + + try: + data = await to_thread.run_sync(self._file.read, max_bytes) + except ValueError: + raise ClosedResourceError from None + except OSError as exc: + raise BrokenResourceError from exc + + if data: + return data + else: + raise EndOfStream + + async def seek(self, position: int, whence: int = SEEK_SET) -> int: + """ + Seek the file to the given position. + + .. seealso:: :meth:`io.IOBase.seek` + + .. note:: Not all file descriptors are seekable. + + :param position: position to seek the file to + :param whence: controls how ``position`` is interpreted + :return: the new absolute position + :raises OSError: if the file is not seekable + + """ + return await to_thread.run_sync(self._file.seek, position, whence) + + async def tell(self) -> int: + """ + Return the current stream position. + + .. note:: Not all file descriptors are seekable. + + :return: the current absolute position + :raises OSError: if the file is not seekable + + """ + return await to_thread.run_sync(self._file.tell) + + +class FileWriteStream(_BaseFileStream, ByteSendStream): + """ + A byte stream that writes to a file in the file system. + + :param file: a file that has been opened for writing in binary mode + + .. versionadded:: 3.0 + """ + + @classmethod + async def from_path( + cls, path: str | PathLike[str], append: bool = False + ) -> FileWriteStream: + """ + Create a file write stream by opening the given file for writing. + + :param path: path of the file to write to + :param append: if ``True``, open the file for appending; if ``False``, any + existing file at the given path will be truncated + + """ + mode = "ab" if append else "wb" + file = await to_thread.run_sync(Path(path).open, mode) + return cls(file) + + async def send(self, item: bytes) -> None: + try: + await to_thread.run_sync(self._file.write, item) + except ValueError: + raise ClosedResourceError from None + except OSError as exc: + raise BrokenResourceError from exc diff --git a/source/target-vendors/cp312/anyio/streams/memory.py b/source/target-vendors/cp312/anyio/streams/memory.py new file mode 100644 index 0000000..329a75e --- /dev/null +++ b/source/target-vendors/cp312/anyio/streams/memory.py @@ -0,0 +1,326 @@ +from __future__ import annotations + +__all__ = ( + "MemoryObjectReceiveStream", + "MemoryObjectSendStream", + "MemoryObjectStreamStatistics", +) + +import warnings +from collections import OrderedDict, deque +from dataclasses import dataclass, field +from types import TracebackType +from typing import Generic, NamedTuple, TypeVar + +from .._core._exceptions import ( + BrokenResourceError, + ClosedResourceError, + EndOfStream, + WouldBlock, +) +from .._core._synchronization import Event +from .._core._testing import TaskInfo, get_current_task +from ..abc import ObjectReceiveStream, ObjectSendStream +from ..lowlevel import checkpoint + +T_Item = TypeVar("T_Item") +T_co = TypeVar("T_co", covariant=True) +T_contra = TypeVar("T_contra", contravariant=True) + + +class MemoryObjectStreamStatistics(NamedTuple): + current_buffer_used: int #: number of items stored in the buffer + #: maximum number of items that can be stored on this stream (or :data:`math.inf`) + max_buffer_size: float + open_send_streams: int #: number of unclosed clones of the send stream + open_receive_streams: int #: number of unclosed clones of the receive stream + #: number of tasks blocked on :meth:`MemoryObjectSendStream.send` + tasks_waiting_send: int + #: number of tasks blocked on :meth:`MemoryObjectReceiveStream.receive` + tasks_waiting_receive: int + + +@dataclass(eq=False) +class _MemoryObjectItemReceiver(Generic[T_Item]): + task_info: TaskInfo = field(init=False, default_factory=get_current_task) + item: T_Item = field(init=False) + + def __repr__(self) -> str: + # When item is not defined, we get following error with default __repr__: + # AttributeError: 'MemoryObjectItemReceiver' object has no attribute 'item' + item = getattr(self, "item", None) + return f"{self.__class__.__name__}(task_info={self.task_info}, item={item!r})" + + +@dataclass(eq=False) +class _MemoryObjectStreamState(Generic[T_Item]): + max_buffer_size: float = field() + buffer: deque[T_Item] = field(init=False, default_factory=deque) + open_send_channels: int = field(init=False, default=0) + open_receive_channels: int = field(init=False, default=0) + waiting_receivers: OrderedDict[Event, _MemoryObjectItemReceiver[T_Item]] = field( + init=False, default_factory=OrderedDict + ) + waiting_senders: OrderedDict[Event, T_Item] = field( + init=False, default_factory=OrderedDict + ) + + def statistics(self) -> MemoryObjectStreamStatistics: + return MemoryObjectStreamStatistics( + len(self.buffer), + self.max_buffer_size, + self.open_send_channels, + self.open_receive_channels, + len(self.waiting_senders), + len(self.waiting_receivers), + ) + + +@dataclass(eq=False) +class MemoryObjectReceiveStream(ObjectReceiveStream[T_co], Generic[T_co]): + _state: _MemoryObjectStreamState[T_co] + _closed: bool = field(init=False, default=False) + + def __post_init__(self) -> None: + self._state.open_receive_channels += 1 + + def receive_nowait(self) -> T_co: + """ + Receive the next item if it can be done without waiting. + + :return: the received item + :raises ~anyio.ClosedResourceError: if this send stream has been closed + :raises ~anyio.EndOfStream: if the buffer is empty and this stream has been + closed from the sending end + :raises ~anyio.WouldBlock: if there are no items in the buffer and no tasks + waiting to send + + """ + if self._closed: + raise ClosedResourceError + + if self._state.waiting_senders: + # Get the item from the next sender + send_event, item = self._state.waiting_senders.popitem(last=False) + self._state.buffer.append(item) + send_event.set() + + if self._state.buffer: + return self._state.buffer.popleft() + elif not self._state.open_send_channels: + raise EndOfStream + + raise WouldBlock + + async def receive(self) -> T_co: + await checkpoint() + try: + return self.receive_nowait() + except WouldBlock: + # Add ourselves in the queue + receive_event = Event() + receiver = _MemoryObjectItemReceiver[T_co]() + self._state.waiting_receivers[receive_event] = receiver + + try: + await receive_event.wait() + finally: + self._state.waiting_receivers.pop(receive_event, None) + + try: + return receiver.item + except AttributeError: + raise EndOfStream from None + + def clone(self) -> MemoryObjectReceiveStream[T_co]: + """ + Create a clone of this receive stream. + + Each clone can be closed separately. Only when all clones have been closed will + the receiving end of the memory stream be considered closed by the sending ends. + + :return: the cloned stream + + """ + if self._closed: + raise ClosedResourceError + + return MemoryObjectReceiveStream(_state=self._state) + + def close(self) -> None: + """ + Close the stream. + + This works the exact same way as :meth:`aclose`, but is provided as a special + case for the benefit of synchronous callbacks. + + """ + if not self._closed: + self._closed = True + self._state.open_receive_channels -= 1 + if self._state.open_receive_channels == 0: + send_events = list(self._state.waiting_senders.keys()) + for event in send_events: + event.set() + + async def aclose(self) -> None: + self.close() + + def statistics(self) -> MemoryObjectStreamStatistics: + """ + Return statistics about the current state of this stream. + + .. versionadded:: 3.0 + """ + return self._state.statistics() + + def __enter__(self) -> MemoryObjectReceiveStream[T_co]: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def __del__(self) -> None: + if not self._closed: + warnings.warn( + f"Unclosed <{self.__class__.__name__} at {id(self):x}>", + ResourceWarning, + stacklevel=1, + source=self, + ) + + +@dataclass(eq=False) +class MemoryObjectSendStream(ObjectSendStream[T_contra], Generic[T_contra]): + _state: _MemoryObjectStreamState[T_contra] + _closed: bool = field(init=False, default=False) + + def __post_init__(self) -> None: + self._state.open_send_channels += 1 + + def send_nowait(self, item: T_contra) -> None: + """ + Send an item immediately if it can be done without waiting. + + :param item: the item to send + :raises ~anyio.ClosedResourceError: if this send stream has been closed + :raises ~anyio.BrokenResourceError: if the stream has been closed from the + receiving end + :raises ~anyio.WouldBlock: if the buffer is full and there are no tasks waiting + to receive + + """ + if self._closed: + raise ClosedResourceError + if not self._state.open_receive_channels: + raise BrokenResourceError + + while self._state.waiting_receivers: + receive_event, receiver = self._state.waiting_receivers.popitem(last=False) + if not receiver.task_info.has_pending_cancellation(): + receiver.item = item + receive_event.set() + return + + if len(self._state.buffer) < self._state.max_buffer_size: + self._state.buffer.append(item) + else: + raise WouldBlock + + async def send(self, item: T_contra) -> None: + """ + Send an item to the stream. + + If the buffer is full, this method blocks until there is again room in the + buffer or the item can be sent directly to a receiver. + + :param item: the item to send + :raises ~anyio.ClosedResourceError: if this send stream has been closed + :raises ~anyio.BrokenResourceError: if the stream has been closed from the + receiving end + + """ + await checkpoint() + try: + self.send_nowait(item) + except WouldBlock: + # Wait until there's someone on the receiving end + send_event = Event() + self._state.waiting_senders[send_event] = item + try: + await send_event.wait() + except BaseException: + self._state.waiting_senders.pop(send_event, None) + raise + + if send_event in self._state.waiting_senders: + del self._state.waiting_senders[send_event] + raise BrokenResourceError from None + + def clone(self) -> MemoryObjectSendStream[T_contra]: + """ + Create a clone of this send stream. + + Each clone can be closed separately. Only when all clones have been closed will + the sending end of the memory stream be considered closed by the receiving ends. + + :return: the cloned stream + + """ + if self._closed: + raise ClosedResourceError + + return MemoryObjectSendStream(_state=self._state) + + def close(self) -> None: + """ + Close the stream. + + This works the exact same way as :meth:`aclose`, but is provided as a special + case for the benefit of synchronous callbacks. + + """ + if not self._closed: + self._closed = True + self._state.open_send_channels -= 1 + if self._state.open_send_channels == 0: + receive_events = list(self._state.waiting_receivers.keys()) + self._state.waiting_receivers.clear() + for event in receive_events: + event.set() + + async def aclose(self) -> None: + self.close() + + def statistics(self) -> MemoryObjectStreamStatistics: + """ + Return statistics about the current state of this stream. + + .. versionadded:: 3.0 + """ + return self._state.statistics() + + def __enter__(self) -> MemoryObjectSendStream[T_contra]: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def __del__(self) -> None: + if not self._closed: + warnings.warn( + f"Unclosed <{self.__class__.__name__} at {id(self):x}>", + ResourceWarning, + stacklevel=1, + source=self, + ) diff --git a/source/target-vendors/cp312/anyio/streams/stapled.py b/source/target-vendors/cp312/anyio/streams/stapled.py new file mode 100644 index 0000000..9a75345 --- /dev/null +++ b/source/target-vendors/cp312/anyio/streams/stapled.py @@ -0,0 +1,160 @@ +from __future__ import annotations + +__all__ = ( + "MultiListener", + "StapledByteStream", + "StapledObjectStream", +) + +from collections.abc import Callable, Mapping, Sequence +from dataclasses import dataclass +from typing import Any, Generic, TypeVar + +from ..abc import ( + ByteReceiveStream, + ByteSendStream, + ByteStream, + Listener, + ObjectReceiveStream, + ObjectSendStream, + ObjectStream, + TaskGroup, +) + +T_Item = TypeVar("T_Item") +T_Stream = TypeVar("T_Stream") + + +@dataclass(eq=False) +class StapledByteStream(ByteStream): + """ + Combines two byte streams into a single, bidirectional byte stream. + + Extra attributes will be provided from both streams, with the receive stream + providing the values in case of a conflict. + + :param ByteSendStream send_stream: the sending byte stream + :param ByteReceiveStream receive_stream: the receiving byte stream + """ + + send_stream: ByteSendStream + receive_stream: ByteReceiveStream + + async def receive(self, max_bytes: int = 65536) -> bytes: + if max_bytes < 1: + raise ValueError("max_bytes must be a positive integer") + + return await self.receive_stream.receive(max_bytes) + + async def send(self, item: bytes) -> None: + await self.send_stream.send(item) + + async def send_eof(self) -> None: + await self.send_stream.aclose() + + async def aclose(self) -> None: + await self.send_stream.aclose() + await self.receive_stream.aclose() + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return { + **self.send_stream.extra_attributes, + **self.receive_stream.extra_attributes, + } + + +@dataclass(eq=False) +class StapledObjectStream(ObjectStream[T_Item], Generic[T_Item]): + """ + Combines two object streams into a single, bidirectional object stream. + + Extra attributes will be provided from both streams, with the receive stream + providing the values in case of a conflict. + + :param ObjectSendStream send_stream: the sending object stream + :param ObjectReceiveStream receive_stream: the receiving object stream + """ + + send_stream: ObjectSendStream[T_Item] + receive_stream: ObjectReceiveStream[T_Item] + + async def receive(self) -> T_Item: + return await self.receive_stream.receive() + + async def send(self, item: T_Item) -> None: + await self.send_stream.send(item) + + def send_nowait(self, item: T_Item) -> None: + try: + send_nowait = self.send_stream.send_nowait # type: ignore[attr-defined] + except AttributeError as exc: + raise NotImplementedError( + f"'send_nowait' method not implemented in {type(self.send_stream)}" + ) from exc + + send_nowait(item) + + async def send_eof(self) -> None: + await self.send_stream.aclose() + + async def aclose(self) -> None: + await self.send_stream.aclose() + await self.receive_stream.aclose() + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return { + **self.send_stream.extra_attributes, + **self.receive_stream.extra_attributes, + } + + +@dataclass(eq=False) +class MultiListener(Listener[T_Stream], Generic[T_Stream]): + """ + Combines multiple listeners into one, serving connections from all of them at once. + + Any MultiListeners in the given collection of listeners will have their listeners + moved into this one. + + Extra attributes are provided from each listener, with each successive listener + overriding any conflicting attributes from the previous one. + + :param listeners: listeners to serve + :type listeners: Sequence[Listener[T_Stream]] + """ + + listeners: Sequence[Listener[T_Stream]] + + def __post_init__(self) -> None: + listeners: list[Listener[T_Stream]] = [] + for listener in self.listeners: + if isinstance(listener, MultiListener): + listeners.extend(listener.listeners) + del listener.listeners[:] # type: ignore[attr-defined] + else: + listeners.append(listener) + + self.listeners = listeners + + async def serve( + self, handler: Callable[[T_Stream], Any], task_group: TaskGroup | None = None + ) -> None: + from .. import create_task_group + + async with create_task_group() as tg: + for listener in self.listeners: + tg.start_soon(listener.serve, handler, task_group) + + async def aclose(self) -> None: + for listener in self.listeners: + await listener.aclose() + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + attributes: dict = {} + for listener in self.listeners: + attributes.update(listener.extra_attributes) + + return attributes diff --git a/source/target-vendors/cp312/anyio/streams/text.py b/source/target-vendors/cp312/anyio/streams/text.py new file mode 100644 index 0000000..296cd25 --- /dev/null +++ b/source/target-vendors/cp312/anyio/streams/text.py @@ -0,0 +1,176 @@ +from __future__ import annotations + +__all__ = ( + "TextConnectable", + "TextReceiveStream", + "TextSendStream", + "TextStream", +) + +import codecs +import sys +from collections.abc import Callable, Mapping +from dataclasses import InitVar, dataclass, field +from typing import Any + +from ..abc import ( + AnyByteReceiveStream, + AnyByteSendStream, + AnyByteStream, + AnyByteStreamConnectable, + ObjectReceiveStream, + ObjectSendStream, + ObjectStream, + ObjectStreamConnectable, +) + +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + + +@dataclass(eq=False) +class TextReceiveStream(ObjectReceiveStream[str]): + """ + Stream wrapper that decodes bytes to strings using the given encoding. + + Decoding is done using :class:`~codecs.IncrementalDecoder` which returns any + completely received unicode characters as soon as they come in. + + :param transport_stream: any bytes-based receive stream + :param encoding: character encoding to use for decoding bytes to strings (defaults + to ``utf-8``) + :param errors: handling scheme for decoding errors (defaults to ``strict``; see the + `codecs module documentation`_ for a comprehensive list of options) + + .. _codecs module documentation: + https://docs.python.org/3/library/codecs.html#codec-objects + """ + + transport_stream: AnyByteReceiveStream + encoding: InitVar[str] = "utf-8" + errors: InitVar[str] = "strict" + _decoder: codecs.IncrementalDecoder = field(init=False) + + def __post_init__(self, encoding: str, errors: str) -> None: + decoder_class = codecs.getincrementaldecoder(encoding) + self._decoder = decoder_class(errors=errors) + + async def receive(self) -> str: + while True: + chunk = await self.transport_stream.receive() + decoded = self._decoder.decode(chunk) + if decoded: + return decoded + + async def aclose(self) -> None: + await self.transport_stream.aclose() + self._decoder.reset() + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return self.transport_stream.extra_attributes + + +@dataclass(eq=False) +class TextSendStream(ObjectSendStream[str]): + """ + Sends strings to the wrapped stream as bytes using the given encoding. + + :param AnyByteSendStream transport_stream: any bytes-based send stream + :param str encoding: character encoding to use for encoding strings to bytes + (defaults to ``utf-8``) + :param str errors: handling scheme for encoding errors (defaults to ``strict``; see + the `codecs module documentation`_ for a comprehensive list of options) + + .. _codecs module documentation: + https://docs.python.org/3/library/codecs.html#codec-objects + """ + + transport_stream: AnyByteSendStream + encoding: InitVar[str] = "utf-8" + errors: str = "strict" + _encoder: Callable[..., tuple[bytes, int]] = field(init=False) + + def __post_init__(self, encoding: str) -> None: + self._encoder = codecs.getencoder(encoding) + + async def send(self, item: str) -> None: + encoded = self._encoder(item, self.errors)[0] + await self.transport_stream.send(encoded) + + async def aclose(self) -> None: + await self.transport_stream.aclose() + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return self.transport_stream.extra_attributes + + +@dataclass(eq=False) +class TextStream(ObjectStream[str]): + """ + A bidirectional stream that decodes bytes to strings on receive and encodes strings + to bytes on send. + + Extra attributes will be provided from both streams, with the receive stream + providing the values in case of a conflict. + + :param AnyByteStream transport_stream: any bytes-based stream + :param str encoding: character encoding to use for encoding/decoding strings to/from + bytes (defaults to ``utf-8``) + :param str errors: handling scheme for encoding errors (defaults to ``strict``; see + the `codecs module documentation`_ for a comprehensive list of options) + + .. _codecs module documentation: + https://docs.python.org/3/library/codecs.html#codec-objects + """ + + transport_stream: AnyByteStream + encoding: InitVar[str] = "utf-8" + errors: InitVar[str] = "strict" + _receive_stream: TextReceiveStream = field(init=False) + _send_stream: TextSendStream = field(init=False) + + def __post_init__(self, encoding: str, errors: str) -> None: + self._receive_stream = TextReceiveStream( + self.transport_stream, encoding=encoding, errors=errors + ) + self._send_stream = TextSendStream( + self.transport_stream, encoding=encoding, errors=errors + ) + + async def receive(self) -> str: + return await self._receive_stream.receive() + + async def send(self, item: str) -> None: + await self._send_stream.send(item) + + async def send_eof(self) -> None: + await self.transport_stream.send_eof() + + async def aclose(self) -> None: + await self._send_stream.aclose() + await self._receive_stream.aclose() + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return { + **self._send_stream.extra_attributes, + **self._receive_stream.extra_attributes, + } + + +class TextConnectable(ObjectStreamConnectable[str]): + def __init__(self, connectable: AnyByteStreamConnectable): + """ + :param connectable: the bytestream endpoint to wrap + + """ + self.connectable = connectable + + @override + async def connect(self) -> TextStream: + stream = await self.connectable.connect() + return TextStream(stream) diff --git a/source/target-vendors/cp312/anyio/streams/tls.py b/source/target-vendors/cp312/anyio/streams/tls.py new file mode 100644 index 0000000..b9ddf1d --- /dev/null +++ b/source/target-vendors/cp312/anyio/streams/tls.py @@ -0,0 +1,436 @@ +from __future__ import annotations + +__all__ = ( + "TLSAttribute", + "TLSConnectable", + "TLSListener", + "TLSStream", +) + +import logging +import re +import ssl +import sys +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from functools import wraps +from ssl import SSLContext +from typing import Any, TypeAlias, TypeVar + +from .. import ( + BrokenResourceError, + EndOfStream, + aclose_forcefully, + get_cancelled_exc_class, + to_thread, +) +from .._core._typedattr import TypedAttributeSet, typed_attribute +from ..abc import ( + AnyByteStream, + AnyByteStreamConnectable, + ByteStream, + ByteStreamConnectable, + Listener, + TaskGroup, +) + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + +T_Retval = TypeVar("T_Retval") +PosArgsT = TypeVarTuple("PosArgsT") +_PCTRTT: TypeAlias = tuple[tuple[str, str], ...] +_PCTRTTT: TypeAlias = tuple[_PCTRTT, ...] + + +class TLSAttribute(TypedAttributeSet): + """Contains Transport Layer Security related attributes.""" + + #: the selected ALPN protocol + alpn_protocol: str | None = typed_attribute() + #: the channel binding for type ``tls-unique`` + channel_binding_tls_unique: bytes = typed_attribute() + #: the selected cipher + cipher: tuple[str, str, int] = typed_attribute() + #: the peer certificate in dictionary form (see :meth:`ssl.SSLSocket.getpeercert` + # for more information) + peer_certificate: None | (dict[str, str | _PCTRTTT | _PCTRTT]) = typed_attribute() + #: the peer certificate in binary form + peer_certificate_binary: bytes | None = typed_attribute() + #: ``True`` if this is the server side of the connection + server_side: bool = typed_attribute() + #: ciphers shared by the client during the TLS handshake (``None`` if this is the + #: client side) + shared_ciphers: list[tuple[str, str, int]] | None = typed_attribute() + #: the :class:`~ssl.SSLObject` used for encryption + ssl_object: ssl.SSLObject = typed_attribute() + #: ``True`` if this stream does (and expects) a closing TLS handshake when the + #: stream is being closed + standard_compatible: bool = typed_attribute() + #: the TLS protocol version (e.g. ``TLSv1.2``) + tls_version: str = typed_attribute() + + +@dataclass(eq=False) +class TLSStream(ByteStream): + """ + A stream wrapper that encrypts all sent data and decrypts received data. + + This class has no public initializer; use :meth:`wrap` instead. + All extra attributes from :class:`~TLSAttribute` are supported. + + :var AnyByteStream transport_stream: the wrapped stream + + """ + + transport_stream: AnyByteStream + standard_compatible: bool + _ssl_object: ssl.SSLObject + _read_bio: ssl.MemoryBIO + _write_bio: ssl.MemoryBIO + + @classmethod + async def wrap( + cls, + transport_stream: AnyByteStream, + *, + server_side: bool | None = None, + hostname: str | None = None, + ssl_context: ssl.SSLContext | None = None, + standard_compatible: bool = True, + ) -> TLSStream: + """ + Wrap an existing stream with Transport Layer Security. + + This performs a TLS handshake with the peer. + + :param transport_stream: a bytes-transporting stream to wrap + :param server_side: ``True`` if this is the server side of the connection, + ``False`` if this is the client side (if omitted, will be set to ``False`` + if ``hostname`` has been provided, ``False`` otherwise). Used only to create + a default context when an explicit context has not been provided. + :param hostname: host name of the peer (if host name checking is desired) + :param ssl_context: the SSLContext object to use (if not provided, a secure + default will be created) + :param standard_compatible: if ``False``, skip the closing handshake when + closing the connection, and don't raise an exception if the peer does the + same + :raises ~ssl.SSLError: if the TLS handshake fails + + """ + if server_side is None: + server_side = not hostname + + if not ssl_context: + purpose = ( + ssl.Purpose.CLIENT_AUTH if server_side else ssl.Purpose.SERVER_AUTH + ) + ssl_context = ssl.create_default_context(purpose) + + # Re-enable detection of unexpected EOFs if it was disabled by Python + if hasattr(ssl, "OP_IGNORE_UNEXPECTED_EOF"): + ssl_context.options &= ~ssl.OP_IGNORE_UNEXPECTED_EOF + + bio_in = ssl.MemoryBIO() + bio_out = ssl.MemoryBIO() + + # Resolve international host names using IDNA 2008. + # Otherwise wrap_bio() would resolve them with IDNA 2003. + if hostname is not None: + from .._core._sockets import idna2008_resolve + + server_hostname: bytes | None = idna2008_resolve(hostname) + else: + server_hostname = None + + # External SSLContext implementations may do blocking I/O in wrap_bio(), + # but the standard library implementation won't + if type(ssl_context) is ssl.SSLContext: + ssl_object = ssl_context.wrap_bio( + bio_in, + bio_out, + server_side=server_side, + server_hostname=server_hostname, + ) + else: + ssl_object = await to_thread.run_sync( + ssl_context.wrap_bio, + bio_in, + bio_out, + server_side, + server_hostname, + None, + ) + + wrapper = cls( + transport_stream=transport_stream, + standard_compatible=standard_compatible, + _ssl_object=ssl_object, + _read_bio=bio_in, + _write_bio=bio_out, + ) + await wrapper._call_sslobject_method(ssl_object.do_handshake) + return wrapper + + async def _call_sslobject_method( + self, func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT] + ) -> T_Retval: + while True: + try: + result = func(*args) + except ssl.SSLWantReadError: + try: + # Flush any pending writes first + if self._write_bio.pending: + await self.transport_stream.send(self._write_bio.read()) + + data = await self.transport_stream.receive() + except EndOfStream: + self._read_bio.write_eof() + except OSError as exc: + self._read_bio.write_eof() + self._write_bio.write_eof() + raise BrokenResourceError from exc + else: + self._read_bio.write(data) + except ssl.SSLWantWriteError: + await self.transport_stream.send(self._write_bio.read()) + except ssl.SSLSyscallError as exc: + self._read_bio.write_eof() + self._write_bio.write_eof() + raise BrokenResourceError from exc + except ssl.SSLError as exc: + self._read_bio.write_eof() + self._write_bio.write_eof() + if isinstance(exc, ssl.SSLEOFError) or ( + exc.strerror and "UNEXPECTED_EOF_WHILE_READING" in exc.strerror + ): + if self.standard_compatible: + raise BrokenResourceError from exc + else: + raise EndOfStream from None + + raise + else: + # Flush any pending writes first + if self._write_bio.pending: + await self.transport_stream.send(self._write_bio.read()) + + return result + + async def unwrap(self) -> tuple[AnyByteStream, bytes]: + """ + Does the TLS closing handshake. + + :return: a tuple of (wrapped byte stream, bytes left in the read buffer) + + """ + await self._call_sslobject_method(self._ssl_object.unwrap) + self._read_bio.write_eof() + self._write_bio.write_eof() + return self.transport_stream, self._read_bio.read() + + async def aclose(self) -> None: + if self.standard_compatible: + try: + await self.unwrap() + except BaseException: + await aclose_forcefully(self.transport_stream) + raise + + await self.transport_stream.aclose() + + async def receive(self, max_bytes: int = 65536) -> bytes: + if max_bytes < 1: + raise ValueError("max_bytes must be a positive integer") + + data = await self._call_sslobject_method(self._ssl_object.read, max_bytes) + if not data: + raise EndOfStream + + return data + + async def send(self, item: bytes) -> None: + await self._call_sslobject_method(self._ssl_object.write, item) + + async def send_eof(self) -> None: + tls_version = self.extra(TLSAttribute.tls_version) + match = re.match(r"TLSv(\d+)(?:\.(\d+))?", tls_version) + if match: + major, minor = int(match.group(1)), int(match.group(2) or 0) + if (major, minor) < (1, 3): + raise NotImplementedError( + f"send_eof() requires at least TLSv1.3; current " + f"session uses {tls_version}" + ) + + raise NotImplementedError( + "send_eof() has not yet been implemented for TLS streams" + ) + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return { + **self.transport_stream.extra_attributes, + TLSAttribute.alpn_protocol: self._ssl_object.selected_alpn_protocol, + TLSAttribute.channel_binding_tls_unique: ( + self._ssl_object.get_channel_binding + ), + TLSAttribute.cipher: self._ssl_object.cipher, + TLSAttribute.peer_certificate: lambda: self._ssl_object.getpeercert(False), + TLSAttribute.peer_certificate_binary: lambda: self._ssl_object.getpeercert( + True + ), + TLSAttribute.server_side: lambda: self._ssl_object.server_side, + TLSAttribute.shared_ciphers: lambda: ( + self._ssl_object.shared_ciphers() + if self._ssl_object.server_side + else None + ), + TLSAttribute.standard_compatible: lambda: self.standard_compatible, + TLSAttribute.ssl_object: lambda: self._ssl_object, + TLSAttribute.tls_version: self._ssl_object.version, + } + + +@dataclass(eq=False) +class TLSListener(Listener[TLSStream]): + """ + A convenience listener that wraps another listener and auto-negotiates a TLS session + on every accepted connection. + + If the TLS handshake times out or raises an exception, + :meth:`handle_handshake_error` is called to do whatever post-mortem processing is + deemed necessary. + + Supports only the :attr:`~TLSAttribute.standard_compatible` extra attribute. + + :param Listener listener: the listener to wrap + :param ssl_context: the SSL context object + :param standard_compatible: a flag passed through to :meth:`TLSStream.wrap` + :param handshake_timeout: time limit for the TLS handshake + (passed to :func:`~anyio.fail_after`) + """ + + listener: Listener[Any] + ssl_context: ssl.SSLContext + standard_compatible: bool = True + handshake_timeout: float = 30 + + @staticmethod + async def handle_handshake_error(exc: BaseException, stream: AnyByteStream) -> None: + """ + Handle an exception raised during the TLS handshake. + + This method does 3 things: + + #. Forcefully closes the original stream + #. Logs the exception (unless it was a cancellation exception) using the + ``anyio.streams.tls`` logger + #. Reraises the exception if it was a base exception or a cancellation exception + + :param exc: the exception + :param stream: the original stream + + """ + await aclose_forcefully(stream) + + # Log all except cancellation exceptions + if not isinstance(exc, get_cancelled_exc_class()): + # CPython (as of 3.11.5) returns incorrect `sys.exc_info()` here when using + # any asyncio implementation, so we explicitly pass the exception to log + # (https://github.com/python/cpython/issues/108668). Trio does not have this + # issue because it works around the CPython bug. + logging.getLogger(__name__).exception( + "Error during TLS handshake", exc_info=exc + ) + + # Only reraise base exceptions and cancellation exceptions + if not isinstance(exc, Exception) or isinstance(exc, get_cancelled_exc_class()): + raise # noqa: PLE0704 + + async def serve( + self, + handler: Callable[[TLSStream], Any], + task_group: TaskGroup | None = None, + ) -> None: + @wraps(handler) + async def handler_wrapper(stream: AnyByteStream) -> None: + from .. import fail_after + + try: + with fail_after(self.handshake_timeout): + wrapped_stream = await TLSStream.wrap( + stream, + ssl_context=self.ssl_context, + standard_compatible=self.standard_compatible, + ) + except BaseException as exc: + await self.handle_handshake_error(exc, stream) + else: + await handler(wrapped_stream) + + await self.listener.serve(handler_wrapper, task_group) + + async def aclose(self) -> None: + await self.listener.aclose() + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return { + TLSAttribute.standard_compatible: lambda: self.standard_compatible, + } + + +class TLSConnectable(ByteStreamConnectable): + """ + Wraps another connectable and does TLS negotiation after a successful connection. + + :param connectable: the connectable to wrap + :param hostname: host name of the server (if host name checking is desired) + :param ssl_context: the SSLContext object to use (if not provided, a secure default + will be created) + :param standard_compatible: if ``False``, skip the closing handshake when closing + the connection, and don't raise an exception if the server does the same + """ + + def __init__( + self, + connectable: AnyByteStreamConnectable, + *, + hostname: str | None = None, + ssl_context: ssl.SSLContext | None = None, + standard_compatible: bool = True, + ) -> None: + self.connectable = connectable + self.ssl_context: SSLContext = ssl_context or ssl.create_default_context( + ssl.Purpose.SERVER_AUTH + ) + if not isinstance(self.ssl_context, ssl.SSLContext): + raise TypeError( + "ssl_context must be an instance of ssl.SSLContext, not " + f"{type(self.ssl_context).__name__}" + ) + self.hostname = hostname + self.standard_compatible = standard_compatible + + @override + async def connect(self) -> TLSStream: + stream = await self.connectable.connect() + try: + return await TLSStream.wrap( + stream, + hostname=self.hostname, + ssl_context=self.ssl_context, + standard_compatible=self.standard_compatible, + ) + except BaseException: + await aclose_forcefully(stream) + raise diff --git a/source/target-vendors/cp312/anyio/to_interpreter.py b/source/target-vendors/cp312/anyio/to_interpreter.py new file mode 100644 index 0000000..54ef4a8 --- /dev/null +++ b/source/target-vendors/cp312/anyio/to_interpreter.py @@ -0,0 +1,246 @@ +from __future__ import annotations + +__all__ = ( + "current_default_interpreter_limiter", + "run_sync", +) + +import atexit +import os +import sys +from collections import deque +from collections.abc import Callable +from typing import Any, Final, TypeVar + +from . import current_time, to_thread +from ._core._exceptions import BrokenWorkerInterpreter +from ._core._synchronization import CapacityLimiter +from .lowlevel import RunVar + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +if sys.version_info >= (3, 14): + from concurrent.interpreters import ExecutionFailed, create + + def _interp_call( + func: Callable[..., Any], args: tuple[Any, ...] + ) -> tuple[Any, bool]: + try: + retval = func(*args) + except BaseException as exc: + return exc, True + else: + return retval, False + + class _Worker: + last_used: float = 0 + + def __init__(self) -> None: + self._interpreter = create() + + def destroy(self) -> None: + self._interpreter.close() + + def call( + self, + func: Callable[..., T_Retval], + args: tuple[Any, ...], + ) -> T_Retval: + try: + res, is_exception = self._interpreter.call(_interp_call, func, args) + except ExecutionFailed as exc: + raise BrokenWorkerInterpreter(exc.excinfo) from exc + + if is_exception: + raise res + + return res +elif sys.version_info >= (3, 13): + import _interpqueues + import _interpreters + + UNBOUND: Final = 2 # I have no clue how this works, but it was used in the stdlib + FMT_UNPICKLED: Final = 0 + FMT_PICKLED: Final = 1 + QUEUE_PICKLE_ARGS: Final = (FMT_PICKLED, UNBOUND) + QUEUE_UNPICKLE_ARGS: Final = (FMT_UNPICKLED, UNBOUND) + + _run_func = compile( + """ +import _interpqueues +from _interpreters import NotShareableError +from pickle import loads, dumps, HIGHEST_PROTOCOL + +QUEUE_PICKLE_ARGS = (1, 2) +QUEUE_UNPICKLE_ARGS = (0, 2) + +item = _interpqueues.get(queue_id)[0] +try: + func, args = loads(item) + retval = func(*args) +except BaseException as exc: + is_exception = True + retval = exc +else: + is_exception = False + +try: + _interpqueues.put(queue_id, (retval, is_exception), *QUEUE_UNPICKLE_ARGS) +except NotShareableError: + retval = dumps(retval, HIGHEST_PROTOCOL) + _interpqueues.put(queue_id, (retval, is_exception), *QUEUE_PICKLE_ARGS) + """, + "", + "exec", + ) + + class _Worker: + last_used: float = 0 + + def __init__(self) -> None: + self._interpreter_id = _interpreters.create() + self._queue_id = _interpqueues.create(1, *QUEUE_UNPICKLE_ARGS) + _interpreters.set___main___attrs( + self._interpreter_id, {"queue_id": self._queue_id} + ) + + def destroy(self) -> None: + _interpqueues.destroy(self._queue_id) + _interpreters.destroy(self._interpreter_id) + + def call( + self, + func: Callable[..., T_Retval], + args: tuple[Any, ...], + ) -> T_Retval: + import pickle + + item = pickle.dumps((func, args), pickle.HIGHEST_PROTOCOL) + _interpqueues.put(self._queue_id, item, *QUEUE_PICKLE_ARGS) + exc_info = _interpreters.exec(self._interpreter_id, _run_func) + if exc_info: + raise BrokenWorkerInterpreter(exc_info) + + res = _interpqueues.get(self._queue_id) + (res, is_exception), fmt = res[:2] + if fmt == FMT_PICKLED: + res = pickle.loads(res) + + if is_exception: + raise res + + return res +else: + + class _Worker: + last_used: float = 0 + + def __init__(self) -> None: + raise RuntimeError("subinterpreters require at least Python 3.13") + + def call( + self, + func: Callable[..., T_Retval], + args: tuple[Any, ...], + ) -> T_Retval: + raise NotImplementedError + + def destroy(self) -> None: + pass + + +DEFAULT_CPU_COUNT: Final = 8 # this is just an arbitrarily selected value +MAX_WORKER_IDLE_TIME = ( + 30 # seconds a subinterpreter can be idle before becoming eligible for pruning +) + +T_Retval = TypeVar("T_Retval") +PosArgsT = TypeVarTuple("PosArgsT") + +_idle_workers = RunVar[deque[_Worker]]("_available_workers") +_default_interpreter_limiter = RunVar[CapacityLimiter]("_default_interpreter_limiter") + + +def _stop_workers(workers: deque[_Worker]) -> None: + for worker in workers: + worker.destroy() + + workers.clear() + + +async def run_sync( + func: Callable[[Unpack[PosArgsT]], T_Retval], + *args: Unpack[PosArgsT], + limiter: CapacityLimiter | None = None, +) -> T_Retval: + """ + Call the given function with the given arguments in a subinterpreter. + + .. warning:: On Python 3.13, the :mod:`concurrent.interpreters` module was not yet + available, so the code path for that Python version relies on an undocumented, + private API. As such, it is recommended to not rely on this function for anything + mission-critical on Python 3.13. + + :param func: a callable + :param args: the positional arguments for the callable + :param limiter: capacity limiter to use to limit the total number of subinterpreters + running (if omitted, the default limiter is used) + :return: the result of the call + :raises BrokenWorkerInterpreter: if there's an internal error in a subinterpreter + + """ + if limiter is None: + limiter = current_default_interpreter_limiter() + + try: + idle_workers = _idle_workers.get() + except LookupError: + idle_workers = deque() + _idle_workers.set(idle_workers) + atexit.register(_stop_workers, idle_workers) + + async with limiter: + try: + worker = idle_workers.pop() + except IndexError: + worker = _Worker() + + try: + return await to_thread.run_sync( + worker.call, + func, + args, + limiter=limiter, + ) + finally: + # Prune workers that have been idle for too long + now = current_time() + while idle_workers: + if now - idle_workers[0].last_used <= MAX_WORKER_IDLE_TIME: + break + + await to_thread.run_sync(idle_workers.popleft().destroy, limiter=limiter) + + worker.last_used = current_time() + idle_workers.append(worker) + + +def current_default_interpreter_limiter() -> CapacityLimiter: + """ + Return the capacity limiter used by default to limit the number of concurrently + running subinterpreters. + + Defaults to the number of CPU cores. + + :return: a capacity limiter object + + """ + try: + return _default_interpreter_limiter.get() + except LookupError: + limiter = CapacityLimiter(os.cpu_count() or DEFAULT_CPU_COUNT) + _default_interpreter_limiter.set(limiter) + return limiter diff --git a/source/target-vendors/cp312/anyio/to_process.py b/source/target-vendors/cp312/anyio/to_process.py new file mode 100644 index 0000000..584128e --- /dev/null +++ b/source/target-vendors/cp312/anyio/to_process.py @@ -0,0 +1,269 @@ +from __future__ import annotations + +__all__ = ( + "current_default_process_limiter", + "process_worker", + "run_sync", +) + +import os +import pickle +import runpy +import subprocess +import sys +from collections import deque +from collections.abc import Callable +from types import ModuleType +from typing import TypeVar, cast + +from ._core._eventloop import current_time, get_async_backend, get_cancelled_exc_class +from ._core._exceptions import BrokenWorkerProcess +from ._core._subprocesses import open_process +from ._core._synchronization import CapacityLimiter +from ._core._tasks import CancelScope, fail_after +from .abc import ByteReceiveStream, ByteSendStream, Process +from .lowlevel import RunVar, checkpoint_if_cancelled +from .streams.buffered import BufferedByteReceiveStream + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +WORKER_MAX_IDLE_TIME = 300 # 5 minutes + +T_Retval = TypeVar("T_Retval") +PosArgsT = TypeVarTuple("PosArgsT") + +_process_pool_workers: RunVar[set[Process]] = RunVar("_process_pool_workers") +_process_pool_idle_workers: RunVar[deque[tuple[Process, float]]] = RunVar( + "_process_pool_idle_workers" +) +_default_process_limiter: RunVar[CapacityLimiter] = RunVar("_default_process_limiter") + + +async def run_sync( # type: ignore[return] + func: Callable[[Unpack[PosArgsT]], T_Retval], + *args: Unpack[PosArgsT], + cancellable: bool = False, + limiter: CapacityLimiter | None = None, +) -> T_Retval: + """ + Call the given function with the given arguments in a worker process. + + If the ``cancellable`` option is enabled and the task waiting for its completion is + cancelled, the worker process running it will be abruptly terminated using SIGKILL + (or ``terminateProcess()`` on Windows). + + :param func: a callable + :param args: positional arguments for the callable + :param cancellable: ``True`` to allow cancellation of the operation while it's + running + :param limiter: capacity limiter to use to limit the total amount of processes + running (if omitted, the default limiter is used) + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + :return: an awaitable that yields the return value of the function. + + """ + + async def send_raw_command(pickled_cmd: bytes) -> object: + try: + await stdin.send(pickled_cmd) + response = await buffered.receive_until(b"\n", 50) + status, length = response.split(b" ") + if status not in (b"RETURN", b"EXCEPTION"): + raise RuntimeError( + f"Worker process returned unexpected response: {response!r}" + ) + + pickled_response = await buffered.receive_exactly(int(length)) + except BaseException as exc: + workers.discard(process) + try: + process.kill() + with CancelScope(shield=True): + await process.aclose() + except ProcessLookupError: + pass + + if isinstance(exc, get_cancelled_exc_class()): + raise + else: + raise BrokenWorkerProcess from exc + + retval = pickle.loads(pickled_response) + if status == b"EXCEPTION": + assert isinstance(retval, BaseException) + raise retval + else: + return retval + + # First pickle the request before trying to reserve a worker process + await checkpoint_if_cancelled() + request = pickle.dumps(("run", func, args), protocol=pickle.HIGHEST_PROTOCOL) + + # If this is the first run in this event loop thread, set up the necessary variables + try: + workers = _process_pool_workers.get() + idle_workers = _process_pool_idle_workers.get() + except LookupError: + workers = set() + idle_workers = deque() + _process_pool_workers.set(workers) + _process_pool_idle_workers.set(idle_workers) + get_async_backend().setup_process_pool_exit_at_shutdown(workers) + + async with limiter or current_default_process_limiter(): + # Pop processes from the pool (starting from the most recently used) until we + # find one that hasn't exited yet + process: Process + while idle_workers: + process, _idle_since = idle_workers.pop() + if process.returncode is None: + stdin = cast(ByteSendStream, process.stdin) + buffered = BufferedByteReceiveStream( + cast(ByteReceiveStream, process.stdout) + ) + + # Prune any other workers that have been idle for WORKER_MAX_IDLE_TIME + # seconds or longer + now = current_time() + killed_processes: list[Process] = [] + while idle_workers: + if now - idle_workers[0][1] < WORKER_MAX_IDLE_TIME: + break + + process_to_kill, _idle_since = idle_workers.popleft() + process_to_kill.kill() + workers.remove(process_to_kill) + killed_processes.append(process_to_kill) + + with CancelScope(shield=True): + for killed_process in killed_processes: + await killed_process.aclose() + + break + + workers.remove(process) + else: + command = [sys.executable, "-u", "-m", __name__] + process = await open_process( + command, stdin=subprocess.PIPE, stdout=subprocess.PIPE + ) + try: + stdin = cast(ByteSendStream, process.stdin) + buffered = BufferedByteReceiveStream( + cast(ByteReceiveStream, process.stdout) + ) + with fail_after(20): + message = await buffered.receive(6) + + if message != b"READY\n": + raise BrokenWorkerProcess( + f"Worker process returned unexpected response: {message!r}" + ) + + main_module_path = getattr(sys.modules["__main__"], "__file__", None) + pickled = pickle.dumps( + ("init", sys.path, main_module_path), + protocol=pickle.HIGHEST_PROTOCOL, + ) + await send_raw_command(pickled) + except (BrokenWorkerProcess, get_cancelled_exc_class()): + raise + except BaseException as exc: + process.kill() + raise BrokenWorkerProcess( + "Error during worker process initialization" + ) from exc + + workers.add(process) + + with CancelScope(shield=not cancellable): + try: + return cast(T_Retval, await send_raw_command(request)) + finally: + if process in workers: + idle_workers.append((process, current_time())) + + +def current_default_process_limiter() -> CapacityLimiter: + """ + Return the capacity limiter that is used by default to limit the number of worker + processes. + + :return: a capacity limiter object + + """ + try: + return _default_process_limiter.get() + except LookupError: + limiter = CapacityLimiter(os.cpu_count() or 2) + _default_process_limiter.set(limiter) + return limiter + + +def process_worker() -> None: + # Redirect standard streams to os.devnull so that user code won't interfere with the + # parent-worker communication + stdin = sys.stdin + stdout = sys.stdout + sys.stdin = open(os.devnull) + sys.stdout = open(os.devnull, "w") + sys.stderr = open(os.devnull, "w") + + stdout.buffer.write(b"READY\n") + while True: + retval = exception = None + try: + command, *args = pickle.load(stdin.buffer) + except EOFError: + return + except BaseException as exc: + exception = exc + else: + if command == "run": + func, args = args + try: + retval = func(*args) + except BaseException as exc: + exception = exc + elif command == "init": + main_module_path: str | None + sys.path, main_module_path = args + del sys.modules["__main__"] + if main_module_path and os.path.isfile(main_module_path): + # Load the parent's main module but as __mp_main__ instead of + # __main__ (like multiprocessing does) to avoid infinite recursion + try: + main = ModuleType("__mp_main__") + main_content = runpy.run_path( + main_module_path, run_name="__mp_main__" + ) + main.__dict__.update(main_content) + sys.modules["__main__"] = sys.modules["__mp_main__"] = main + except BaseException as exc: + exception = exc + try: + if exception is not None: + status = b"EXCEPTION" + pickled = pickle.dumps(exception, pickle.HIGHEST_PROTOCOL) + else: + status = b"RETURN" + pickled = pickle.dumps(retval, pickle.HIGHEST_PROTOCOL) + except BaseException as exc: + exception = exc + status = b"EXCEPTION" + pickled = pickle.dumps(exc, pickle.HIGHEST_PROTOCOL) + + stdout.buffer.write(b"%s %d\n" % (status, len(pickled))) + stdout.buffer.write(pickled) + + # Respect SIGTERM + if isinstance(exception, SystemExit): + raise exception + + +if __name__ == "__main__": + process_worker() diff --git a/source/target-vendors/cp312/anyio/to_thread.py b/source/target-vendors/cp312/anyio/to_thread.py new file mode 100644 index 0000000..5cdb725 --- /dev/null +++ b/source/target-vendors/cp312/anyio/to_thread.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +__all__ = ( + "current_default_thread_limiter", + "run_sync", +) + +import sys +from collections.abc import Callable +from typing import TYPE_CHECKING, TypeVar +from warnings import warn + +from ._core._eventloop import get_async_backend + +if TYPE_CHECKING: + from ._core._synchronization import CapacityLimiter + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +T_Retval = TypeVar("T_Retval") +PosArgsT = TypeVarTuple("PosArgsT") + + +async def run_sync( + func: Callable[[Unpack[PosArgsT]], T_Retval], + *args: Unpack[PosArgsT], + abandon_on_cancel: bool = False, + cancellable: bool | None = None, + limiter: CapacityLimiter | None = None, +) -> T_Retval: + """ + Call the given function with the given arguments in a worker thread. + + If the ``abandon_on_cancel`` option is enabled and the task waiting for its + completion is cancelled, the thread will still run its course but its + return value (or any raised exception) will be ignored. + + :param func: a callable + :param args: positional arguments for the callable + :param abandon_on_cancel: ``True`` to abandon the thread (leaving it to run + unchecked on own) if the host task is cancelled, ``False`` to ignore + cancellations in the host task until the operation has completed in the worker + thread + :param cancellable: deprecated alias of ``abandon_on_cancel``; will override + ``abandon_on_cancel`` if both parameters are passed + :param limiter: capacity limiter to use to limit the total amount of threads running + (if omitted, the default limiter is used) + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + :return: an awaitable that yields the return value of the function. + + """ + if cancellable is not None: + abandon_on_cancel = cancellable + warn( + "The `cancellable=` keyword argument to `anyio.to_thread.run_sync` is " + "deprecated since AnyIO 4.1.0; use `abandon_on_cancel=` instead", + DeprecationWarning, + stacklevel=2, + ) + + return await get_async_backend().run_sync_in_worker_thread( + func, args, abandon_on_cancel=abandon_on_cancel, limiter=limiter + ) + + +def current_default_thread_limiter() -> CapacityLimiter: + """ + Return the capacity limiter that is used by default to limit the number of + concurrent threads. + + :return: a capacity limiter object + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().current_default_thread_limiter() diff --git a/source/target-vendors/cp312/attr/__init__.py b/source/target-vendors/cp312/attr/__init__.py new file mode 100644 index 0000000..5c6e065 --- /dev/null +++ b/source/target-vendors/cp312/attr/__init__.py @@ -0,0 +1,104 @@ +# SPDX-License-Identifier: MIT + +""" +Classes Without Boilerplate +""" + +from functools import partial +from typing import Callable, Literal, Protocol + +from . import converters, exceptions, filters, setters, validators +from ._cmp import cmp_using +from ._config import get_run_validators, set_run_validators +from ._funcs import asdict, assoc, astuple, has, resolve_types +from ._make import ( + NOTHING, + Attribute, + Converter, + Factory, + _Nothing, + attrib, + attrs, + evolve, + fields, + fields_dict, + make_class, + validate, +) +from ._next_gen import define, field, frozen, mutable +from ._version_info import VersionInfo + + +s = attributes = attrs +ib = attr = attrib +dataclass = partial(attrs, auto_attribs=True) # happy Easter ;) + + +class AttrsInstance(Protocol): + pass + + +NothingType = Literal[_Nothing.NOTHING] + +__all__ = [ + "NOTHING", + "Attribute", + "AttrsInstance", + "Converter", + "Factory", + "NothingType", + "asdict", + "assoc", + "astuple", + "attr", + "attrib", + "attributes", + "attrs", + "cmp_using", + "converters", + "define", + "evolve", + "exceptions", + "field", + "fields", + "fields_dict", + "filters", + "frozen", + "get_run_validators", + "has", + "ib", + "make_class", + "mutable", + "resolve_types", + "s", + "set_run_validators", + "setters", + "validate", + "validators", +] + + +def _make_getattr(mod_name: str) -> Callable: + """ + Create a metadata proxy for packaging information that uses *mod_name* in + its warnings and errors. + """ + + def __getattr__(name: str) -> str: + if name not in ("__version__", "__version_info__"): + msg = f"module {mod_name} has no attribute {name}" + raise AttributeError(msg) + + from importlib.metadata import metadata + + meta = metadata("attrs") + + if name == "__version_info__": + return VersionInfo._from_version_string(meta["version"]) + + return meta["version"] + + return __getattr__ + + +__getattr__ = _make_getattr(__name__) diff --git a/source/target-vendors/cp312/attr/__init__.pyi b/source/target-vendors/cp312/attr/__init__.pyi new file mode 100644 index 0000000..758decf --- /dev/null +++ b/source/target-vendors/cp312/attr/__init__.pyi @@ -0,0 +1,389 @@ +import enum +import sys + +from typing import ( + Any, + Callable, + Generic, + Literal, + Mapping, + Protocol, + Sequence, + TypeVar, + overload, +) + +# `import X as X` is required to make these public +from . import converters as converters +from . import exceptions as exceptions +from . import filters as filters +from . import setters as setters +from . import validators as validators +from ._cmp import cmp_using as cmp_using +from ._typing_compat import AttrsInstance_ +from ._version_info import VersionInfo +from attrs import ( + define as define, + field as field, + mutable as mutable, + frozen as frozen, + _EqOrderType, + _ValidatorType, + _ConverterType, + _ReprArgType, + _OnSetAttrType, + _OnSetAttrArgType, + _FieldTransformer, + _ValidatorArgType, +) + +if sys.version_info >= (3, 10): + from typing import TypeGuard, TypeAlias +else: + from typing_extensions import TypeGuard, TypeAlias + +if sys.version_info >= (3, 11): + from typing import dataclass_transform +else: + from typing_extensions import dataclass_transform + +__version__: str +__version_info__: VersionInfo +__title__: str +__description__: str +__url__: str +__uri__: str +__author__: str +__email__: str +__license__: str +__copyright__: str + +_T = TypeVar("_T") +_C = TypeVar("_C", bound=type) + +_FilterType = Callable[["Attribute[_T]", _T], bool] + +# We subclass this here to keep the protocol's qualified name clean. +class AttrsInstance(AttrsInstance_, Protocol): + pass + +_A = TypeVar("_A", bound=type[AttrsInstance]) + +class _Nothing(enum.Enum): + NOTHING = enum.auto() + +NOTHING = _Nothing.NOTHING +NothingType: TypeAlias = Literal[_Nothing.NOTHING] + +# NOTE: Factory lies about its return type to make this possible: +# `x: List[int] # = Factory(list)` +# Work around mypy issue #4554 in the common case by using an overload. + +@overload +def Factory(factory: Callable[[], _T]) -> _T: ... +@overload +def Factory( + factory: Callable[[Any], _T], + takes_self: Literal[True], +) -> _T: ... +@overload +def Factory( + factory: Callable[[], _T], + takes_self: Literal[False], +) -> _T: ... + +In = TypeVar("In") +Out = TypeVar("Out") + +class Converter(Generic[In, Out]): + @overload + def __init__(self, converter: Callable[[In], Out]) -> None: ... + @overload + def __init__( + self, + converter: Callable[[In, AttrsInstance, Attribute], Out], + *, + takes_self: Literal[True], + takes_field: Literal[True], + ) -> None: ... + @overload + def __init__( + self, + converter: Callable[[In, Attribute], Out], + *, + takes_field: Literal[True], + ) -> None: ... + @overload + def __init__( + self, + converter: Callable[[In, AttrsInstance], Out], + *, + takes_self: Literal[True], + ) -> None: ... + +class Attribute(Generic[_T]): + name: str + default: _T | None + validator: _ValidatorType[_T] | None + repr: _ReprArgType + cmp: _EqOrderType + eq: _EqOrderType + order: _EqOrderType + hash: bool | None + init: bool + converter: Converter | None + metadata: dict[Any, Any] + type: type[_T] | None + kw_only: bool + on_setattr: _OnSetAttrType + alias: str | None + + def evolve(self, **changes: Any) -> "Attribute[Any]": ... + +# NOTE: We had several choices for the annotation to use for type arg: +# 1) Type[_T] +# - Pros: Handles simple cases correctly +# - Cons: Might produce less informative errors in the case of conflicting +# TypeVars e.g. `attr.ib(default='bad', type=int)` +# 2) Callable[..., _T] +# - Pros: Better error messages than #1 for conflicting TypeVars +# - Cons: Terrible error messages for validator checks. +# e.g. attr.ib(type=int, validator=validate_str) +# -> error: Cannot infer function type argument +# 3) type (and do all of the work in the mypy plugin) +# - Pros: Simple here, and we could customize the plugin with our own errors. +# - Cons: Would need to write mypy plugin code to handle all the cases. +# We chose option #1. + +# `attr` lies about its return type to make the following possible: +# attr() -> Any +# attr(8) -> int +# attr(validator=) -> Whatever the callable expects. +# This makes this type of assignments possible: +# x: int = attr(8) +# +# This form catches explicit None or no default but with no other arguments +# returns Any. +@overload +def attrib( + default: None = ..., + validator: None = ..., + repr: _ReprArgType = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + type: None = ..., + converter: None = ..., + factory: None = ..., + kw_only: bool | None = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., +) -> Any: ... + +# This form catches an explicit None or no default and infers the type from the +# other arguments. +@overload +def attrib( + default: None = ..., + validator: _ValidatorArgType[_T] | None = ..., + repr: _ReprArgType = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + type: type[_T] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType] + | None = ..., + factory: Callable[[], _T] | None = ..., + kw_only: bool | None = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., +) -> _T: ... + +# This form catches an explicit default argument. +@overload +def attrib( + default: _T, + validator: _ValidatorArgType[_T] | None = ..., + repr: _ReprArgType = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + type: type[_T] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType] + | None = ..., + factory: Callable[[], _T] | None = ..., + kw_only: bool | None = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., +) -> _T: ... + +# This form covers type=non-Type: e.g. forward references (str), Any +@overload +def attrib( + default: _T | None = ..., + validator: _ValidatorArgType[_T] | None = ..., + repr: _ReprArgType = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + type: object = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType] + | None = ..., + factory: Callable[[], _T] | None = ..., + kw_only: bool | None = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., +) -> Any: ... +@overload +@dataclass_transform(order_default=True, field_specifiers=(attrib, field)) +def attrs( + maybe_cls: _C, + these: dict[str, Any] | None = ..., + repr_ns: str | None = ..., + repr: bool = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + auto_detect: bool = ..., + collect_by_mro: bool = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., + match_args: bool = ..., + unsafe_hash: bool | None = ..., +) -> _C: ... +@overload +@dataclass_transform(order_default=True, field_specifiers=(attrib, field)) +def attrs( + maybe_cls: None = ..., + these: dict[str, Any] | None = ..., + repr_ns: str | None = ..., + repr: bool = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + auto_detect: bool = ..., + collect_by_mro: bool = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., + match_args: bool = ..., + unsafe_hash: bool | None = ..., +) -> Callable[[_C], _C]: ... +def fields(cls: type[AttrsInstance] | AttrsInstance) -> Any: ... +def fields_dict(cls: type[AttrsInstance]) -> dict[str, Attribute[Any]]: ... +def validate(inst: AttrsInstance) -> None: ... +def resolve_types( + cls: _A, + globalns: dict[str, Any] | None = ..., + localns: dict[str, Any] | None = ..., + attribs: list[Attribute[Any]] | None = ..., + include_extras: bool = ..., +) -> _A: ... + +# TODO: add support for returning a proper attrs class from the mypy plugin +# we use Any instead of _CountingAttr so that e.g. `make_class('Foo', +# [attr.ib()])` is valid +def make_class( + name: str, + attrs: list[str] | tuple[str, ...] | dict[str, Any], + bases: tuple[type, ...] = ..., + class_body: dict[str, Any] | None = ..., + repr_ns: str | None = ..., + repr: bool = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + collect_by_mro: bool = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., +) -> type: ... + +# _funcs -- + +# TODO: add support for returning TypedDict from the mypy plugin +# FIXME: asdict/astuple do not honor their factory args. Waiting on one of +# these: +# https://github.com/python/mypy/issues/4236 +# https://github.com/python/typing/issues/253 +# XXX: remember to fix attrs.asdict/astuple too! +def asdict( + inst: AttrsInstance, + recurse: bool = ..., + filter: _FilterType[Any] | None = ..., + dict_factory: type[Mapping[Any, Any]] = ..., + retain_collection_types: bool = ..., + value_serializer: Callable[[type, Attribute[Any], Any], Any] | None = ..., + tuple_keys: bool | None = ..., +) -> dict[str, Any]: ... + +# TODO: add support for returning NamedTuple from the mypy plugin +def astuple( + inst: AttrsInstance, + recurse: bool = ..., + filter: _FilterType[Any] | None = ..., + tuple_factory: type[Sequence[Any]] = ..., + retain_collection_types: bool = ..., +) -> tuple[Any, ...]: ... +def has(cls: type) -> TypeGuard[type[AttrsInstance]]: ... +def assoc(inst: _T, **changes: Any) -> _T: ... +def evolve(inst: _T, **changes: Any) -> _T: ... + +# _config -- + +def set_run_validators(run: bool) -> None: ... +def get_run_validators() -> bool: ... + +# aliases -- + +s = attributes = attrs +ib = attr = attrib +dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;) diff --git a/source/target-vendors/cp312/attr/_cmp.py b/source/target-vendors/cp312/attr/_cmp.py new file mode 100644 index 0000000..09bab49 --- /dev/null +++ b/source/target-vendors/cp312/attr/_cmp.py @@ -0,0 +1,160 @@ +# SPDX-License-Identifier: MIT + + +import functools +import types + +from ._make import __ne__ + + +_operation_names = {"eq": "==", "lt": "<", "le": "<=", "gt": ">", "ge": ">="} + + +def cmp_using( + eq=None, + lt=None, + le=None, + gt=None, + ge=None, + require_same_type=True, + class_name="Comparable", +): + """ + Create a class that can be passed into `attrs.field`'s ``eq``, ``order``, + and ``cmp`` arguments to customize field comparison. + + The resulting class will have a full set of ordering methods if at least + one of ``{lt, le, gt, ge}`` and ``eq`` are provided. + + Args: + eq (typing.Callable | None): + Callable used to evaluate equality of two objects. + + lt (typing.Callable | None): + Callable used to evaluate whether one object is less than another + object. + + le (typing.Callable | None): + Callable used to evaluate whether one object is less than or equal + to another object. + + gt (typing.Callable | None): + Callable used to evaluate whether one object is greater than + another object. + + ge (typing.Callable | None): + Callable used to evaluate whether one object is greater than or + equal to another object. + + require_same_type (bool): + When `True`, equality and ordering methods will return + `NotImplemented` if objects are not of the same type. + + class_name (str | None): Name of class. Defaults to "Comparable". + + See `comparison` for more details. + + .. versionadded:: 21.1.0 + """ + + body = { + "__slots__": ["value"], + "__init__": _make_init(), + "_requirements": [], + "_is_comparable_to": _is_comparable_to, + } + + # Add operations. + num_order_functions = 0 + has_eq_function = False + + if eq is not None: + has_eq_function = True + body["__eq__"] = _make_operator("eq", eq) + body["__ne__"] = __ne__ + + if lt is not None: + num_order_functions += 1 + body["__lt__"] = _make_operator("lt", lt) + + if le is not None: + num_order_functions += 1 + body["__le__"] = _make_operator("le", le) + + if gt is not None: + num_order_functions += 1 + body["__gt__"] = _make_operator("gt", gt) + + if ge is not None: + num_order_functions += 1 + body["__ge__"] = _make_operator("ge", ge) + + type_ = types.new_class( + class_name, (object,), {}, lambda ns: ns.update(body) + ) + + # Add same type requirement. + if require_same_type: + type_._requirements.append(_check_same_type) + + # Add total ordering if at least one operation was defined. + if 0 < num_order_functions < 4: + if not has_eq_function: + # functools.total_ordering requires __eq__ to be defined, + # so raise early error here to keep a nice stack. + msg = "eq must be define is order to complete ordering from lt, le, gt, ge." + raise ValueError(msg) + type_ = functools.total_ordering(type_) + + return type_ + + +def _make_init(): + """ + Create __init__ method. + """ + + def __init__(self, value): + """ + Initialize object with *value*. + """ + self.value = value + + return __init__ + + +def _make_operator(name, func): + """ + Create operator method. + """ + + def method(self, other): + if not self._is_comparable_to(other): + return NotImplemented + + result = func(self.value, other.value) + if result is NotImplemented: + return NotImplemented + + return result + + method.__name__ = f"__{name}__" + method.__doc__ = ( + f"Return a {_operation_names[name]} b. Computed by attrs." + ) + + return method + + +def _is_comparable_to(self, other): + """ + Check whether `other` is comparable to `self`. + """ + return all(func(self, other) for func in self._requirements) + + +def _check_same_type(self, other): + """ + Return True if *self* and *other* are of the same type, False otherwise. + """ + return other.value.__class__ is self.value.__class__ diff --git a/source/target-vendors/cp312/attr/_cmp.pyi b/source/target-vendors/cp312/attr/_cmp.pyi new file mode 100644 index 0000000..cc7893b --- /dev/null +++ b/source/target-vendors/cp312/attr/_cmp.pyi @@ -0,0 +1,13 @@ +from typing import Any, Callable + +_CompareWithType = Callable[[Any, Any], bool] + +def cmp_using( + eq: _CompareWithType | None = ..., + lt: _CompareWithType | None = ..., + le: _CompareWithType | None = ..., + gt: _CompareWithType | None = ..., + ge: _CompareWithType | None = ..., + require_same_type: bool = ..., + class_name: str = ..., +) -> type: ... diff --git a/source/target-vendors/cp312/attr/_compat.py b/source/target-vendors/cp312/attr/_compat.py new file mode 100644 index 0000000..bc68ed9 --- /dev/null +++ b/source/target-vendors/cp312/attr/_compat.py @@ -0,0 +1,99 @@ +# SPDX-License-Identifier: MIT + +import inspect +import platform +import sys +import threading + +from collections.abc import Mapping, Sequence # noqa: F401 +from typing import _GenericAlias + + +PYPY = platform.python_implementation() == "PyPy" +PY_3_10_PLUS = sys.version_info[:2] >= (3, 10) +PY_3_11_PLUS = sys.version_info[:2] >= (3, 11) +PY_3_12_PLUS = sys.version_info[:2] >= (3, 12) +PY_3_13_PLUS = sys.version_info[:2] >= (3, 13) +PY_3_14_PLUS = sys.version_info[:2] >= (3, 14) + + +if PY_3_14_PLUS: + import annotationlib + + # We request forward-ref annotations to not break in the presence of + # forward references. + + def _get_annotations(cls): + return annotationlib.get_annotations( + cls, format=annotationlib.Format.FORWARDREF + ) + +else: + + def _get_annotations(cls): + """ + Get annotations for *cls*. + """ + return cls.__dict__.get("__annotations__", {}) + + +class _AnnotationExtractor: + """ + Extract type annotations from a callable, returning None whenever there + is none. + """ + + __slots__ = ["sig"] + + def __init__(self, callable): + try: + self.sig = inspect.signature(callable) + except (ValueError, TypeError): # inspect failed + self.sig = None + + def get_first_param_type(self): + """ + Return the type annotation of the first argument if it's not empty. + """ + if not self.sig: + return None + + params = list(self.sig.parameters.values()) + if params and params[0].annotation is not inspect.Parameter.empty: + return params[0].annotation + + return None + + def get_return_type(self): + """ + Return the return type if it's not empty. + """ + if ( + self.sig + and self.sig.return_annotation is not inspect.Signature.empty + ): + return self.sig.return_annotation + + return None + + +# Thread-local global to track attrs instances which are already being repr'd. +# This is needed because there is no other (thread-safe) way to pass info +# about the instances that are already being repr'd through the call stack +# in order to ensure we don't perform infinite recursion. +# +# For instance, if an instance contains a dict which contains that instance, +# we need to know that we're already repr'ing the outside instance from within +# the dict's repr() call. +# +# This lives here rather than in _make.py so that the functions in _make.py +# don't have a direct reference to the thread-local in their globals dict. +# If they have such a reference, it breaks cloudpickle. +repr_context = threading.local() + + +def get_generic_base(cl): + """If this is a generic class (A[str]), return the generic base for it.""" + if cl.__class__ is _GenericAlias: + return cl.__origin__ + return None diff --git a/source/target-vendors/cp312/attr/_config.py b/source/target-vendors/cp312/attr/_config.py new file mode 100644 index 0000000..4b25772 --- /dev/null +++ b/source/target-vendors/cp312/attr/_config.py @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: MIT + +__all__ = ["get_run_validators", "set_run_validators"] + +_run_validators = True + + +def set_run_validators(run): + """ + Set whether or not validators are run. By default, they are run. + + .. deprecated:: 21.3.0 It will not be removed, but it also will not be + moved to new ``attrs`` namespace. Use `attrs.validators.set_disabled()` + instead. + """ + if not isinstance(run, bool): + msg = "'run' must be bool." + raise TypeError(msg) + global _run_validators + _run_validators = run + + +def get_run_validators(): + """ + Return whether or not validators are run. + + .. deprecated:: 21.3.0 It will not be removed, but it also will not be + moved to new ``attrs`` namespace. Use `attrs.validators.get_disabled()` + instead. + """ + return _run_validators diff --git a/source/target-vendors/cp312/attr/_funcs.py b/source/target-vendors/cp312/attr/_funcs.py new file mode 100644 index 0000000..1adb500 --- /dev/null +++ b/source/target-vendors/cp312/attr/_funcs.py @@ -0,0 +1,497 @@ +# SPDX-License-Identifier: MIT + + +import copy + +from ._compat import get_generic_base +from ._make import _OBJ_SETATTR, NOTHING, fields +from .exceptions import AttrsAttributeNotFoundError + + +_ATOMIC_TYPES = frozenset( + { + type(None), + bool, + int, + float, + str, + complex, + bytes, + type(...), + type, + range, + property, + } +) + + +def asdict( + inst, + recurse=True, + filter=None, + dict_factory=dict, + retain_collection_types=False, + value_serializer=None, +): + """ + Return the *attrs* attribute values of *inst* as a dict. + + Optionally recurse into other *attrs*-decorated classes. + + Args: + inst: Instance of an *attrs*-decorated class. + + recurse (bool): Recurse into classes that are also *attrs*-decorated. + + filter (~typing.Callable): + A callable whose return code determines whether an attribute or + element is included (`True`) or dropped (`False`). Is called with + the `attrs.Attribute` as the first argument and the value as the + second argument. + + dict_factory (~typing.Callable): + A callable to produce dictionaries from. For example, to produce + ordered dictionaries instead of normal Python dictionaries, pass in + ``collections.OrderedDict``. + + retain_collection_types (bool): + Do not convert to `list` when encountering an attribute whose type + is `tuple` or `set`. Only meaningful if *recurse* is `True`. + + value_serializer (typing.Callable | None): + A hook that is called for every attribute or dict key/value. It + receives the current instance, field and value and must return the + (updated) value. The hook is run *after* the optional *filter* has + been applied. + + Returns: + Return type of *dict_factory*. + + Raises: + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + .. versionadded:: 16.0.0 *dict_factory* + .. versionadded:: 16.1.0 *retain_collection_types* + .. versionadded:: 20.3.0 *value_serializer* + .. versionadded:: 21.3.0 + If a dict has a collection for a key, it is serialized as a tuple. + """ + attrs = fields(inst.__class__) + rv = dict_factory() + for a in attrs: + v = getattr(inst, a.name) + if filter is not None and not filter(a, v): + continue + + if value_serializer is not None: + v = value_serializer(inst, a, v) + + if recurse is True: + value_type = type(v) + if value_type in _ATOMIC_TYPES: + rv[a.name] = v + elif has(value_type): + rv[a.name] = asdict( + v, + recurse=True, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + elif issubclass(value_type, (tuple, list, set, frozenset)): + cf = value_type if retain_collection_types is True else list + items = [ + _asdict_anything( + i, + is_key=False, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + for i in v + ] + try: + rv[a.name] = cf(items) + except TypeError: + if not issubclass(cf, tuple): + raise + # Workaround for TypeError: cf.__new__() missing 1 required + # positional argument (which appears, for a namedturle) + rv[a.name] = cf(*items) + elif issubclass(value_type, dict): + df = dict_factory + rv[a.name] = df( + ( + _asdict_anything( + kk, + is_key=True, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + _asdict_anything( + vv, + is_key=False, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + ) + for kk, vv in v.items() + ) + else: + rv[a.name] = v + else: + rv[a.name] = v + return rv + + +def _asdict_anything( + val, + is_key, + filter, + dict_factory, + retain_collection_types, + value_serializer, +): + """ + ``asdict`` only works on attrs instances, this works on anything. + """ + val_type = type(val) + if val_type in _ATOMIC_TYPES: + rv = val + if value_serializer is not None: + rv = value_serializer(None, None, rv) + elif getattr(val_type, "__attrs_attrs__", None) is not None: + # Attrs class. + rv = asdict( + val, + recurse=True, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + elif issubclass(val_type, (tuple, list, set, frozenset)): + if retain_collection_types is True: + cf = val.__class__ + elif is_key: + cf = tuple + else: + cf = list + + rv = cf( + [ + _asdict_anything( + i, + is_key=False, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + for i in val + ] + ) + elif issubclass(val_type, dict): + df = dict_factory + rv = df( + ( + _asdict_anything( + kk, + is_key=True, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + _asdict_anything( + vv, + is_key=False, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + ) + for kk, vv in val.items() + ) + else: + rv = val + if value_serializer is not None: + rv = value_serializer(None, None, rv) + + return rv + + +def astuple( + inst, + recurse=True, + filter=None, + tuple_factory=tuple, + retain_collection_types=False, +): + """ + Return the *attrs* attribute values of *inst* as a tuple. + + Optionally recurse into other *attrs*-decorated classes. + + Args: + inst: Instance of an *attrs*-decorated class. + + recurse (bool): + Recurse into classes that are also *attrs*-decorated. + + filter (~typing.Callable): + A callable whose return code determines whether an attribute or + element is included (`True`) or dropped (`False`). Is called with + the `attrs.Attribute` as the first argument and the value as the + second argument. + + tuple_factory (~typing.Callable): + A callable to produce tuples from. For example, to produce lists + instead of tuples. + + retain_collection_types (bool): + Do not convert to `list` or `dict` when encountering an attribute + which type is `tuple`, `dict` or `set`. Only meaningful if + *recurse* is `True`. + + Returns: + Return type of *tuple_factory* + + Raises: + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + .. versionadded:: 16.2.0 + """ + attrs = fields(inst.__class__) + rv = [] + retain = retain_collection_types # Very long. :/ + for a in attrs: + v = getattr(inst, a.name) + if filter is not None and not filter(a, v): + continue + value_type = type(v) + if recurse is True: + if value_type in _ATOMIC_TYPES: + rv.append(v) + elif has(value_type): + rv.append( + astuple( + v, + recurse=True, + filter=filter, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + ) + elif issubclass(value_type, (tuple, list, set, frozenset)): + cf = v.__class__ if retain is True else list + items = [ + ( + astuple( + j, + recurse=True, + filter=filter, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(j.__class__) + else j + ) + for j in v + ] + try: + rv.append(cf(items)) + except TypeError: + if not issubclass(cf, tuple): + raise + # Workaround for TypeError: cf.__new__() missing 1 required + # positional argument (which appears, for a namedturle) + rv.append(cf(*items)) + elif issubclass(value_type, dict): + df = value_type if retain is True else dict + rv.append( + df( + ( + ( + astuple( + kk, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(kk.__class__) + else kk + ), + ( + astuple( + vv, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(vv.__class__) + else vv + ), + ) + for kk, vv in v.items() + ) + ) + else: + rv.append(v) + else: + rv.append(v) + + return rv if tuple_factory is list else tuple_factory(rv) + + +def has(cls): + """ + Check whether *cls* is a class with *attrs* attributes. + + Args: + cls (type): Class to introspect. + + Raises: + TypeError: If *cls* is not a class. + + Returns: + bool: + """ + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is not None: + return True + + # No attrs, maybe it's a specialized generic (A[str])? + generic_base = get_generic_base(cls) + if generic_base is not None: + generic_attrs = getattr(generic_base, "__attrs_attrs__", None) + if generic_attrs is not None: + # Stick it on here for speed next time. + cls.__attrs_attrs__ = generic_attrs + return generic_attrs is not None + return False + + +def assoc(inst, **changes): + """ + Copy *inst* and apply *changes*. + + This is different from `evolve` that applies the changes to the arguments + that create the new instance. + + `evolve`'s behavior is preferable, but there are `edge cases`_ where it + doesn't work. Therefore `assoc` is deprecated, but will not be removed. + + .. _`edge cases`: https://github.com/python-attrs/attrs/issues/251 + + Args: + inst: Instance of a class with *attrs* attributes. + + changes: Keyword changes in the new copy. + + Returns: + A copy of inst with *changes* incorporated. + + Raises: + attrs.exceptions.AttrsAttributeNotFoundError: + If *attr_name* couldn't be found on *cls*. + + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + .. deprecated:: 17.1.0 + Use `attrs.evolve` instead if you can. This function will not be + removed du to the slightly different approach compared to + `attrs.evolve`, though. + """ + new = copy.copy(inst) + attrs = fields(inst.__class__) + for k, v in changes.items(): + a = getattr(attrs, k, NOTHING) + if a is NOTHING: + msg = f"{k} is not an attrs attribute on {new.__class__}." + raise AttrsAttributeNotFoundError(msg) + _OBJ_SETATTR(new, k, v) + return new + + +def resolve_types( + cls, globalns=None, localns=None, attribs=None, include_extras=True +): + """ + Resolve any strings and forward annotations in type annotations. + + This is only required if you need concrete types in :class:`Attribute`'s + *type* field. In other words, you don't need to resolve your types if you + only use them for static type checking. + + With no arguments, names will be looked up in the module in which the class + was created. If this is not what you want, for example, if the name only + exists inside a method, you may pass *globalns* or *localns* to specify + other dictionaries in which to look up these names. See the docs of + `typing.get_type_hints` for more details. + + Args: + cls (type): Class to resolve. + + globalns (dict | None): Dictionary containing global variables. + + localns (dict | None): Dictionary containing local variables. + + attribs (list | None): + List of attribs for the given class. This is necessary when calling + from inside a ``field_transformer`` since *cls* is not an *attrs* + class yet. + + include_extras (bool): + Resolve more accurately, if possible. Pass ``include_extras`` to + ``typing.get_hints``, if supported by the typing module. On + supported Python versions (3.9+), this resolves the types more + accurately. + + Raises: + TypeError: If *cls* is not a class. + + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class and you didn't pass any attribs. + + NameError: If types cannot be resolved because of missing variables. + + Returns: + *cls* so you can use this function also as a class decorator. Please + note that you have to apply it **after** `attrs.define`. That means the + decorator has to come in the line **before** `attrs.define`. + + .. versionadded:: 20.1.0 + .. versionadded:: 21.1.0 *attribs* + .. versionadded:: 23.1.0 *include_extras* + """ + # Since calling get_type_hints is expensive we cache whether we've + # done it already. + if getattr(cls, "__attrs_types_resolved__", None) != cls: + import typing + + kwargs = { + "globalns": globalns, + "localns": localns, + "include_extras": include_extras, + } + + hints = typing.get_type_hints(cls, **kwargs) + for field in fields(cls) if attribs is None else attribs: + if field.name in hints: + # Since fields have been frozen we must work around it. + _OBJ_SETATTR(field, "type", hints[field.name]) + # We store the class we resolved so that subclasses know they haven't + # been resolved. + cls.__attrs_types_resolved__ = cls + + # Return the class so you can use it as a decorator too. + return cls diff --git a/source/target-vendors/cp312/attr/_make.py b/source/target-vendors/cp312/attr/_make.py new file mode 100644 index 0000000..4b32d6a --- /dev/null +++ b/source/target-vendors/cp312/attr/_make.py @@ -0,0 +1,3406 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import abc +import contextlib +import copy +import enum +import inspect +import itertools +import linecache +import sys +import types +import unicodedata +import weakref + +from collections.abc import Callable, Mapping +from functools import cached_property +from typing import Any, NamedTuple, TypeVar + +# We need to import _compat itself in addition to the _compat members to avoid +# having the thread-local in the globals here. +from . import _compat, _config, setters +from ._compat import ( + PY_3_10_PLUS, + PY_3_11_PLUS, + PY_3_13_PLUS, + _AnnotationExtractor, + _get_annotations, + get_generic_base, +) +from .exceptions import ( + DefaultAlreadySetError, + FrozenInstanceError, + NotAnAttrsClassError, + UnannotatedAttributeError, +) + + +# This is used at least twice, so cache it here. +_OBJ_SETATTR = object.__setattr__ +_INIT_FACTORY_PAT = "__attr_factory_%s" +_CLASSVAR_PREFIXES = ( + "typing.ClassVar", + "t.ClassVar", + "ClassVar", + "typing_extensions.ClassVar", +) +# we don't use a double-underscore prefix because that triggers +# name mangling when trying to create a slot for the field +# (when slots=True) +_HASH_CACHE_FIELD = "_attrs_cached_hash" + +_EMPTY_METADATA_SINGLETON = types.MappingProxyType({}) + +# Unique object for unequivocal getattr() defaults. +_SENTINEL = object() + +_DEFAULT_ON_SETATTR = setters.pipe(setters.convert, setters.validate) + + +class _Nothing(enum.Enum): + """ + Sentinel to indicate the lack of a value when `None` is ambiguous. + + If extending attrs, you can use ``typing.Literal[NOTHING]`` to show + that a value may be ``NOTHING``. + + .. versionchanged:: 21.1.0 ``bool(NOTHING)`` is now False. + .. versionchanged:: 22.2.0 ``NOTHING`` is now an ``enum.Enum`` variant. + """ + + NOTHING = enum.auto() + + def __repr__(self): + return "NOTHING" + + def __bool__(self): + return False + + +NOTHING = _Nothing.NOTHING +""" +Sentinel to indicate the lack of a value when `None` is ambiguous. + +When using in 3rd party code, use `attrs.NothingType` for type annotations. +""" + + +class _CacheHashWrapper(int): + """ + An integer subclass that pickles / copies as None + + This is used for non-slots classes with ``cache_hash=True``, to avoid + serializing a potentially (even likely) invalid hash value. Since `None` + is the default value for uncalculated hashes, whenever this is copied, + the copy's value for the hash should automatically reset. + + See GH #613 for more details. + """ + + def __reduce__(self, _none_constructor=type(None), _args=()): # noqa: B008 + return _none_constructor, _args + + +def attrib( + default=NOTHING, + validator=None, + repr=True, + cmp=None, + hash=None, + init=True, + metadata=None, + type=None, + converter=None, + factory=None, + kw_only=None, + eq=None, + order=None, + on_setattr=None, + alias=None, +): + """ + Create a new field / attribute on a class. + + Identical to `attrs.field`, except it's not keyword-only. + + Consider using `attrs.field` in new code (``attr.ib`` will *never* go away, + though). + + .. warning:: + + Does **nothing** unless the class is also decorated with + `attr.s` (or similar)! + + + .. versionadded:: 15.2.0 *convert* + .. versionadded:: 16.3.0 *metadata* + .. versionchanged:: 17.1.0 *validator* can be a ``list`` now. + .. versionchanged:: 17.1.0 + *hash* is `None` and therefore mirrors *eq* by default. + .. versionadded:: 17.3.0 *type* + .. deprecated:: 17.4.0 *convert* + .. versionadded:: 17.4.0 + *converter* as a replacement for the deprecated *convert* to achieve + consistency with other noun-based arguments. + .. versionadded:: 18.1.0 + ``factory=f`` is syntactic sugar for ``default=attr.Factory(f)``. + .. versionadded:: 18.2.0 *kw_only* + .. versionchanged:: 19.2.0 *convert* keyword argument removed. + .. versionchanged:: 19.2.0 *repr* also accepts a custom callable. + .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. + .. versionadded:: 19.2.0 *eq* and *order* + .. versionadded:: 20.1.0 *on_setattr* + .. versionchanged:: 20.3.0 *kw_only* backported to Python 2 + .. versionchanged:: 21.1.0 + *eq*, *order*, and *cmp* also accept a custom callable + .. versionchanged:: 21.1.0 *cmp* undeprecated + .. versionadded:: 22.2.0 *alias* + .. versionchanged:: 25.4.0 + *kw_only* can now be None, and its default is also changed from False to + None. + """ + eq, eq_key, order, order_key = _determine_attrib_eq_order( + cmp, eq, order, True + ) + + if hash is not None and hash is not True and hash is not False: + msg = "Invalid value for hash. Must be True, False, or None." + raise TypeError(msg) + + if factory is not None: + if default is not NOTHING: + msg = ( + "The `default` and `factory` arguments are mutually exclusive." + ) + raise ValueError(msg) + if not callable(factory): + msg = "The `factory` argument must be a callable." + raise ValueError(msg) + default = Factory(factory) + + if metadata is None: + metadata = {} + + # Apply syntactic sugar by auto-wrapping. + if isinstance(on_setattr, (list, tuple)): + on_setattr = setters.pipe(*on_setattr) + + if validator and isinstance(validator, (list, tuple)): + validator = and_(*validator) + + if converter and isinstance(converter, (list, tuple)): + converter = pipe(*converter) + + return _CountingAttr( + default=default, + validator=validator, + repr=repr, + cmp=None, + hash=hash, + init=init, + converter=converter, + metadata=metadata, + type=type, + kw_only=kw_only, + eq=eq, + eq_key=eq_key, + order=order, + order_key=order_key, + on_setattr=on_setattr, + alias=alias, + ) + + +def _compile_and_eval( + script: str, + globs: dict[str, Any] | None, + locs: Mapping[str, object] | None = None, + filename: str = "", +) -> None: + """ + Evaluate the script with the given global (globs) and local (locs) + variables. + """ + bytecode = compile(script, filename, "exec") + eval(bytecode, globs, locs) + + +def _linecache_and_compile( + script: str, + filename: str, + globs: dict[str, Any] | None, + locals: Mapping[str, object] | None = None, +) -> dict[str, Any]: + """ + Cache the script with _linecache_, compile it and return the _locals_. + """ + + locs = {} if locals is None else locals + + # In order of debuggers like PDB being able to step through the code, + # we add a fake linecache entry. + count = 1 + base_filename = filename + while True: + linecache_tuple = ( + len(script), + None, + script.splitlines(True), + filename, + ) + old_val = linecache.cache.setdefault(filename, linecache_tuple) + if old_val == linecache_tuple: + break + + filename = f"{base_filename[:-1]}-{count}>" + count += 1 + + _compile_and_eval(script, globs, locs, filename) + + return locs + + +def _make_attr_tuple_class(cls_name: str, attr_names: list[str]) -> type: + """ + Create a tuple subclass to hold `Attribute`s for an `attrs` class. + + The subclass is a bare tuple with properties for names. + + class MyClassAttributes(tuple): + __slots__ = () + x = property(itemgetter(0)) + """ + attr_class_name = f"{cls_name}Attributes" + body = {} + for i, attr_name in enumerate(attr_names): + + def getter(self, i=i): + return self[i] + + body[attr_name] = property(getter) + return type(attr_class_name, (tuple,), body) + + +# Tuple class for extracted attributes from a class definition. +# `base_attrs` is a subset of `attrs`. +class _Attributes(NamedTuple): + attrs: type + base_attrs: list[Attribute] + base_attrs_map: dict[str, type] + + +def _is_class_var(annot): + """ + Check whether *annot* is a typing.ClassVar. + + The string comparison hack is used to avoid evaluating all string + annotations which would put attrs-based classes at a performance + disadvantage compared to plain old classes. + """ + annot = str(annot) + + # Annotation can be quoted. + if annot.startswith(("'", '"')) and annot.endswith(("'", '"')): + annot = annot[1:-1] + + return annot.startswith(_CLASSVAR_PREFIXES) + + +def _has_own_attribute(cls, attrib_name): + """ + Check whether *cls* defines *attrib_name* (and doesn't just inherit it). + """ + return attrib_name in cls.__dict__ + + +def _collect_base_attrs( + cls, taken_attr_names +) -> tuple[list[Attribute], dict[str, type]]: + """ + Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. + """ + base_attrs = [] + base_attr_map = {} # A dictionary of base attrs to their classes. + + # Traverse the MRO and collect attributes. + for base_cls in reversed(cls.__mro__[1:-1]): + for a in getattr(base_cls, "__attrs_attrs__", []): + if a.inherited or a.name in taken_attr_names: + continue + + a = a.evolve(inherited=True) # noqa: PLW2901 + base_attrs.append(a) + base_attr_map[a.name] = base_cls + + # For each name, only keep the freshest definition i.e. the furthest at the + # back. base_attr_map is fine because it gets overwritten with every new + # instance. + filtered = [] + seen = set() + for a in reversed(base_attrs): + if a.name in seen: + continue + filtered.insert(0, a) + seen.add(a.name) + + return filtered, base_attr_map + + +def _collect_base_attrs_broken(cls, taken_attr_names): + """ + Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. + + N.B. *taken_attr_names* will be mutated. + + Adhere to the old incorrect behavior. + + Notably it collects from the front and considers inherited attributes which + leads to the buggy behavior reported in #428. + """ + base_attrs = [] + base_attr_map = {} # A dictionary of base attrs to their classes. + + # Traverse the MRO and collect attributes. + for base_cls in cls.__mro__[1:-1]: + for a in getattr(base_cls, "__attrs_attrs__", []): + if a.name in taken_attr_names: + continue + + a = a.evolve(inherited=True) # noqa: PLW2901 + taken_attr_names.add(a.name) + base_attrs.append(a) + base_attr_map[a.name] = base_cls + + return base_attrs, base_attr_map + + +def _transform_attrs( + cls, + these, + auto_attribs, + kw_only, + collect_by_mro, + field_transformer, +) -> _Attributes: + """ + Transform all `_CountingAttr`s on a class into `Attribute`s. + + If *these* is passed, use that and don't look for them on the class. + + If *collect_by_mro* is True, collect them in the correct MRO order, + otherwise use the old -- incorrect -- order. See #428. + + Return an `_Attributes`. + """ + cd = cls.__dict__ + anns = _get_annotations(cls) + + if these is not None: + ca_list = list(these.items()) + elif auto_attribs is True: + ca_names = { + name + for name, attr in cd.items() + if attr.__class__ is _CountingAttr + } + ca_list = [] + annot_names = set() + for attr_name, type in anns.items(): + if _is_class_var(type): + continue + annot_names.add(attr_name) + a = cd.get(attr_name, NOTHING) + + if a.__class__ is not _CountingAttr: + a = attrib(a) + ca_list.append((attr_name, a)) + + unannotated = ca_names - annot_names + if unannotated: + raise UnannotatedAttributeError( + "The following `attr.ib`s lack a type annotation: " + + ", ".join( + sorted(unannotated, key=lambda n: cd.get(n).counter) + ) + + "." + ) + else: + ca_list = sorted( + ( + (name, attr) + for name, attr in cd.items() + if attr.__class__ is _CountingAttr + ), + key=lambda e: e[1].counter, + ) + + fca = Attribute.from_counting_attr + no = ClassProps.KeywordOnly.NO + own_attrs = [ + fca( + attr_name, + ca, + kw_only is not no, + anns.get(attr_name), + ) + for attr_name, ca in ca_list + ] + + if collect_by_mro: + base_attrs, base_attr_map = _collect_base_attrs( + cls, {a.name for a in own_attrs} + ) + else: + base_attrs, base_attr_map = _collect_base_attrs_broken( + cls, {a.name for a in own_attrs} + ) + + if kw_only is ClassProps.KeywordOnly.FORCE: + own_attrs = [a.evolve(kw_only=True) for a in own_attrs] + base_attrs = [a.evolve(kw_only=True) for a in base_attrs] + + attrs = base_attrs + own_attrs + + # Resolve default field alias before executing field_transformer, so that + # the transformer receives fully populated Attribute objects with usable + # alias values. + for a in attrs: + if not a.alias: + # Evolve is very slow, so we hold our nose and do it dirty. + _OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name)) + _OBJ_SETATTR.__get__(a)("alias_is_default", True) + + if field_transformer is not None: + attrs = tuple(field_transformer(cls, attrs)) + + # Check attr order after executing the field_transformer. + # Mandatory vs non-mandatory attr order only matters when they are part of + # the __init__ signature and when they aren't kw_only (which are moved to + # the end and can be mandatory or non-mandatory in any order, as they will + # be specified as keyword args anyway). Check the order of those attrs: + had_default = False + for a in (a for a in attrs if a.init is not False and a.kw_only is False): + if had_default is True and a.default is NOTHING: + msg = f"No mandatory attributes allowed after an attribute with a default value or factory. Attribute in question: {a!r}" + raise ValueError(msg) + + if had_default is False and a.default is not NOTHING: + had_default = True + + # Resolve default field alias for any new attributes that the + # field_transformer may have added without setting an alias. + for a in attrs: + if not a.alias: + _OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name)) + _OBJ_SETATTR.__get__(a)("alias_is_default", True) + + # Create AttrsClass *after* applying the field_transformer since it may + # add or remove attributes! + attr_names = [a.name for a in attrs] + AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names) + + return _Attributes(AttrsClass(attrs), base_attrs, base_attr_map) + + +def _make_cached_property_getattr(cached_properties, original_getattr, cls): + lines = [ + # Wrapped to get `__class__` into closure cell for super() + # (It will be replaced with the newly constructed class after construction). + "def wrapper(_cls):", + " __class__ = _cls", + " def __getattr__(self, item, cached_properties=cached_properties, original_getattr=original_getattr, _cached_setattr_get=_cached_setattr_get):", + " func = cached_properties.get(item)", + " if func is not None:", + " result = func(self)", + " _setter = _cached_setattr_get(self)", + " _setter(item, result)", + " return result", + ] + if original_getattr is not None: + lines.append( + " return original_getattr(self, item)", + ) + else: + lines.extend( + [ + " try:", + " return super().__getattribute__(item)", + " except AttributeError:", + " if not hasattr(super(), '__getattr__'):", + " raise", + " return super().__getattr__(item)", + " original_error = f\"'{self.__class__.__name__}' object has no attribute '{item}'\"", + " raise AttributeError(original_error)", + ] + ) + + lines.extend( + [ + " return __getattr__", + "__getattr__ = wrapper(_cls)", + ] + ) + + unique_filename = _generate_unique_filename(cls, "getattr") + + glob = { + "cached_properties": cached_properties, + "_cached_setattr_get": _OBJ_SETATTR.__get__, + "original_getattr": original_getattr, + } + + return _linecache_and_compile( + "\n".join(lines), unique_filename, glob, locals={"_cls": cls} + )["__getattr__"] + + +def _frozen_setattrs(self, name, value): + """ + Attached to frozen classes as __setattr__. + """ + if isinstance(self, BaseException) and name in ( + "__cause__", + "__context__", + "__traceback__", + "__suppress_context__", + "__notes__", + ): + BaseException.__setattr__(self, name, value) + return + + raise FrozenInstanceError + + +def _frozen_delattrs(self, name): + """ + Attached to frozen classes as __delattr__. + """ + if isinstance(self, BaseException) and name == "__notes__": + BaseException.__delattr__(self, name) + return + + raise FrozenInstanceError + + +def evolve(*args, **changes): + """ + Create a new instance, based on the first positional argument with + *changes* applied. + + .. tip:: + + On Python 3.13 and later, you can also use `copy.replace` instead. + + Args: + + inst: + Instance of a class with *attrs* attributes. *inst* must be passed + as a positional argument. + + changes: + Keyword changes in the new copy. + + Returns: + A copy of inst with *changes* incorporated. + + Raises: + TypeError: + If *attr_name* couldn't be found in the class ``__init__``. + + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + .. versionadded:: 17.1.0 + .. deprecated:: 23.1.0 + It is now deprecated to pass the instance using the keyword argument + *inst*. It will raise a warning until at least April 2024, after which + it will become an error. Always pass the instance as a positional + argument. + .. versionchanged:: 24.1.0 + *inst* can't be passed as a keyword argument anymore. + """ + try: + (inst,) = args + except ValueError: + msg = ( + f"evolve() takes 1 positional argument, but {len(args)} were given" + ) + raise TypeError(msg) from None + + cls = inst.__class__ + attrs = fields(cls) + for a in attrs: + if not a.init: + continue + attr_name = a.name # To deal with private attributes. + init_name = a.alias + if init_name not in changes: + changes[init_name] = getattr(inst, attr_name) + + return cls(**changes) + + +class _ClassBuilder: + """ + Iteratively build *one* class. + """ + + __slots__ = ( + "_add_method_dunders", + "_attr_names", + "_attrs", + "_base_attr_map", + "_base_names", + "_cache_hash", + "_cls", + "_cls_dict", + "_delete_attribs", + "_frozen", + "_has_custom_setattr", + "_has_post_init", + "_has_pre_init", + "_is_exc", + "_on_setattr", + "_pre_init_has_args", + "_repr_added", + "_script_snippets", + "_slots", + "_weakref_slot", + "_wrote_own_setattr", + ) + + def __init__( + self, + cls: type, + these, + auto_attribs: bool, + props: ClassProps, + has_custom_setattr: bool, + ): + attrs, base_attrs, base_map = _transform_attrs( + cls, + these, + auto_attribs, + props.kw_only, + props.collected_fields_by_mro, + props.field_transformer, + ) + + self._cls = cls + self._cls_dict = dict(cls.__dict__) if props.is_slotted else {} + self._attrs = attrs + self._base_names = {a.name for a in base_attrs} + self._base_attr_map = base_map + self._attr_names = tuple(a.name for a in attrs) + self._slots = props.is_slotted + self._frozen = props.is_frozen + self._weakref_slot = props.has_weakref_slot + self._cache_hash = ( + props.hashability is ClassProps.Hashability.HASHABLE_CACHED + ) + self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False)) + self._pre_init_has_args = False + if self._has_pre_init: + # Check if the pre init method has more arguments than just `self` + # We want to pass arguments if pre init expects arguments + pre_init_func = cls.__attrs_pre_init__ + pre_init_signature = inspect.signature(pre_init_func) + self._pre_init_has_args = len(pre_init_signature.parameters) > 1 + self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False)) + self._delete_attribs = not bool(these) + self._is_exc = props.is_exception + self._on_setattr = props.on_setattr_hook + + self._has_custom_setattr = has_custom_setattr + self._wrote_own_setattr = False + + self._cls_dict["__attrs_attrs__"] = self._attrs + self._cls_dict["__attrs_props__"] = props + + if props.is_frozen: + self._cls_dict["__setattr__"] = _frozen_setattrs + self._cls_dict["__delattr__"] = _frozen_delattrs + + self._wrote_own_setattr = True + elif self._on_setattr in ( + _DEFAULT_ON_SETATTR, + setters.validate, + setters.convert, + ): + has_validator = has_converter = False + for a in attrs: + if a.validator is not None: + has_validator = True + if a.converter is not None: + has_converter = True + + if has_validator and has_converter: + break + if ( + ( + self._on_setattr == _DEFAULT_ON_SETATTR + and not (has_validator or has_converter) + ) + or (self._on_setattr == setters.validate and not has_validator) + or (self._on_setattr == setters.convert and not has_converter) + ): + # If class-level on_setattr is set to convert + validate, but + # there's no field to convert or validate, pretend like there's + # no on_setattr. + self._on_setattr = None + + if props.added_pickling: + ( + self._cls_dict["__getstate__"], + self._cls_dict["__setstate__"], + ) = self._make_getstate_setstate() + + # tuples of script, globs, hook + self._script_snippets: list[ + tuple[str, dict, Callable[[dict, dict], Any]] + ] = [] + self._repr_added = False + + # We want to only do this check once; in 99.9% of cases these + # exist. + if not hasattr(self._cls, "__module__") or not hasattr( + self._cls, "__qualname__" + ): + self._add_method_dunders = self._add_method_dunders_safe + else: + self._add_method_dunders = self._add_method_dunders_unsafe + + def __repr__(self): + return f"<_ClassBuilder(cls={self._cls.__name__})>" + + def _eval_snippets(self) -> None: + """ + Evaluate any registered snippets in one go. + """ + script = "\n".join([snippet[0] for snippet in self._script_snippets]) + globs = {} + for _, snippet_globs, _ in self._script_snippets: + globs.update(snippet_globs) + + locs = _linecache_and_compile( + script, + _generate_unique_filename(self._cls, "methods"), + globs, + ) + + for _, _, hook in self._script_snippets: + hook(self._cls_dict, locs) + + def build_class(self): + """ + Finalize class based on the accumulated configuration. + + Builder cannot be used after calling this method. + """ + self._eval_snippets() + if self._slots is True: + cls = self._create_slots_class() + self._cls.__attrs_base_of_slotted__ = weakref.ref(cls) + else: + cls = self._patch_original_class() + if PY_3_10_PLUS: + cls = abc.update_abstractmethods(cls) + + # The method gets only called if it's not inherited from a base class. + # _has_own_attribute does NOT work properly for classmethods. + if ( + getattr(cls, "__attrs_init_subclass__", None) + and "__attrs_init_subclass__" not in cls.__dict__ + ): + cls.__attrs_init_subclass__() + + return cls + + def _patch_original_class(self): + """ + Apply accumulated methods and return the class. + """ + cls = self._cls + base_names = self._base_names + + # Clean class of attribute definitions (`attr.ib()`s). + if self._delete_attribs: + for name in self._attr_names: + if ( + name not in base_names + and getattr(cls, name, _SENTINEL) is not _SENTINEL + ): + # An AttributeError can happen if a base class defines a + # class variable and we want to set an attribute with the + # same name by using only a type annotation. + with contextlib.suppress(AttributeError): + delattr(cls, name) + + # Attach our dunder methods. + for name, value in self._cls_dict.items(): + setattr(cls, name, value) + + # If we've inherited an attrs __setattr__ and don't write our own, + # reset it to object's. + if not self._wrote_own_setattr and getattr( + cls, "__attrs_own_setattr__", False + ): + cls.__attrs_own_setattr__ = False + + if not self._has_custom_setattr: + cls.__setattr__ = _OBJ_SETATTR + + return cls + + def _create_slots_class(self): + """ + Build and return a new class with a `__slots__` attribute. + """ + cd = { + k: v + for k, v in self._cls_dict.items() + if k not in (*tuple(self._attr_names), "__dict__", "__weakref__") + } + + # 3.14.0rc2+ + if hasattr(sys, "_clear_type_descriptors"): + sys._clear_type_descriptors(self._cls) + + # If our class doesn't have its own implementation of __setattr__ + # (either from the user or by us), check the bases, if one of them has + # an attrs-made __setattr__, that needs to be reset. We don't walk the + # MRO because we only care about our immediate base classes. + # XXX: This can be confused by subclassing a slotted attrs class with + # XXX: a non-attrs class and subclass the resulting class with an attrs + # XXX: class. See `test_slotted_confused` for details. For now that's + # XXX: OK with us. + if not self._wrote_own_setattr: + cd["__attrs_own_setattr__"] = False + + if not self._has_custom_setattr: + for base_cls in self._cls.__bases__: + if base_cls.__dict__.get("__attrs_own_setattr__", False): + cd["__setattr__"] = _OBJ_SETATTR + break + + # Traverse the MRO to collect existing slots + # and check for an existing __weakref__. + existing_slots = {} + weakref_inherited = False + for base_cls in self._cls.__mro__[1:-1]: + if base_cls.__dict__.get("__weakref__", None) is not None: + weakref_inherited = True + existing_slots.update( + { + name: getattr(base_cls, name) + for name in getattr(base_cls, "__slots__", []) + } + ) + + base_names = set(self._base_names) + + names = self._attr_names + if ( + self._weakref_slot + and "__weakref__" not in getattr(self._cls, "__slots__", ()) + and "__weakref__" not in names + and not weakref_inherited + ): + names += ("__weakref__",) + + cached_properties = { + name: cached_prop.func + for name, cached_prop in cd.items() + if isinstance(cached_prop, cached_property) + } + + # Collect methods with a `__class__` reference that are shadowed in the new class. + # To know to update them. + additional_closure_functions_to_update = [] + if cached_properties: + class_annotations = _get_annotations(self._cls) + for name, func in cached_properties.items(): + # Add cached properties to names for slotting. + names += (name,) + # Clear out function from class to avoid clashing. + del cd[name] + additional_closure_functions_to_update.append(func) + annotation = inspect.signature(func).return_annotation + if annotation is not inspect.Parameter.empty: + class_annotations[name] = annotation + + original_getattr = cd.get("__getattr__") + if original_getattr is not None: + additional_closure_functions_to_update.append(original_getattr) + + cd["__getattr__"] = _make_cached_property_getattr( + cached_properties, original_getattr, self._cls + ) + + # We only add the names of attributes that aren't inherited. + # Setting __slots__ to inherited attributes wastes memory. + slot_names = [name for name in names if name not in base_names] + + # There are slots for attributes from current class + # that are defined in parent classes. + # As their descriptors may be overridden by a child class, + # we collect them here and update the class dict + reused_slots = { + slot: slot_descriptor + for slot, slot_descriptor in existing_slots.items() + if slot in slot_names + } + slot_names = [name for name in slot_names if name not in reused_slots] + cd.update(reused_slots) + if self._cache_hash: + slot_names.append(_HASH_CACHE_FIELD) + + cd["__slots__"] = tuple(slot_names) + + cd["__qualname__"] = self._cls.__qualname__ + + # Create new class based on old class and our methods. + cls = type(self._cls)(self._cls.__name__, self._cls.__bases__, cd) + + # The following is a fix for + # . + # If a method mentions `__class__` or uses the no-arg super(), the + # compiler will bake a reference to the class in the method itself + # as `method.__closure__`. Since we replace the class with a + # clone, we rewrite these references so it keeps working. + for item in itertools.chain( + cls.__dict__.values(), additional_closure_functions_to_update + ): + if isinstance(item, (classmethod, staticmethod)): + # Class- and staticmethods hide their functions inside. + # These might need to be rewritten as well. + closure_cells = getattr(item.__func__, "__closure__", None) + elif isinstance(item, property): + # Workaround for property `super()` shortcut (PY3-only). + # There is no universal way for other descriptors. + closure_cells = getattr(item.fget, "__closure__", None) + else: + closure_cells = getattr(item, "__closure__", None) + + if not closure_cells: # Catch None or the empty list. + continue + for cell in closure_cells: + try: + match = cell.cell_contents is self._cls + except ValueError: # noqa: PERF203 + # ValueError: Cell is empty + pass + else: + if match: + cell.cell_contents = cls + return cls + + def add_repr(self, ns): + script, globs = _make_repr_script(self._attrs, ns) + + def _attach_repr(cls_dict, globs): + cls_dict["__repr__"] = self._add_method_dunders(globs["__repr__"]) + + self._script_snippets.append((script, globs, _attach_repr)) + self._repr_added = True + return self + + def add_str(self): + if not self._repr_added: + msg = "__str__ can only be generated if a __repr__ exists." + raise ValueError(msg) + + def __str__(self): + return self.__repr__() + + self._cls_dict["__str__"] = self._add_method_dunders(__str__) + return self + + def _make_getstate_setstate(self): + """ + Create custom __setstate__ and __getstate__ methods. + """ + # __weakref__ is not writable. + state_attr_names = tuple( + an for an in self._attr_names if an != "__weakref__" + ) + + def slots_getstate(self): + """ + Automatically created by attrs. + """ + return {name: getattr(self, name) for name in state_attr_names} + + hash_caching_enabled = self._cache_hash + + def slots_setstate(self, state): + """ + Automatically created by attrs. + """ + __bound_setattr = _OBJ_SETATTR.__get__(self) + if isinstance(state, tuple): + # Backward compatibility with attrs instances pickled with + # attrs versions before v22.2.0 which stored tuples. + for name, value in zip(state_attr_names, state): + __bound_setattr(name, value) + else: + for name in state_attr_names: + if name in state: + __bound_setattr(name, state[name]) + + # The hash code cache is not included when the object is + # serialized, but it still needs to be initialized to None to + # indicate that the first call to __hash__ should be a cache + # miss. + if hash_caching_enabled: + __bound_setattr(_HASH_CACHE_FIELD, None) + + return slots_getstate, slots_setstate + + def make_unhashable(self): + self._cls_dict["__hash__"] = None + return self + + def add_hash(self): + script, globs = _make_hash_script( + self._cls, + self._attrs, + frozen=self._frozen, + cache_hash=self._cache_hash, + ) + + def attach_hash(cls_dict: dict, locs: dict) -> None: + cls_dict["__hash__"] = self._add_method_dunders(locs["__hash__"]) + + self._script_snippets.append((script, globs, attach_hash)) + + return self + + def add_init(self): + script, globs, annotations = _make_init_script( + self._cls, + self._attrs, + self._has_pre_init, + self._pre_init_has_args, + self._has_post_init, + self._frozen, + self._slots, + self._cache_hash, + self._base_attr_map, + self._is_exc, + self._on_setattr, + attrs_init=False, + ) + + def _attach_init(cls_dict, globs): + init = globs["__init__"] + init.__annotations__ = annotations + cls_dict["__init__"] = self._add_method_dunders(init) + + self._script_snippets.append((script, globs, _attach_init)) + + return self + + def add_replace(self): + self._cls_dict["__replace__"] = self._add_method_dunders(evolve) + return self + + def add_match_args(self): + self._cls_dict["__match_args__"] = tuple( + field.name + for field in self._attrs + if field.init and not field.kw_only + ) + + def add_attrs_init(self): + script, globs, annotations = _make_init_script( + self._cls, + self._attrs, + self._has_pre_init, + self._pre_init_has_args, + self._has_post_init, + self._frozen, + self._slots, + self._cache_hash, + self._base_attr_map, + self._is_exc, + self._on_setattr, + attrs_init=True, + ) + + def _attach_attrs_init(cls_dict, globs): + init = globs["__attrs_init__"] + init.__annotations__ = annotations + cls_dict["__attrs_init__"] = self._add_method_dunders(init) + + self._script_snippets.append((script, globs, _attach_attrs_init)) + + return self + + def add_eq(self): + cd = self._cls_dict + + script, globs = _make_eq_script(self._attrs) + + def _attach_eq(cls_dict, globs): + cls_dict["__eq__"] = self._add_method_dunders(globs["__eq__"]) + + self._script_snippets.append((script, globs, _attach_eq)) + + cd["__ne__"] = __ne__ + + return self + + def add_order(self): + cd = self._cls_dict + + cd["__lt__"], cd["__le__"], cd["__gt__"], cd["__ge__"] = ( + self._add_method_dunders(meth) + for meth in _make_order(self._cls, self._attrs) + ) + + return self + + def add_setattr(self): + sa_attrs = {} + for a in self._attrs: + on_setattr = a.on_setattr or self._on_setattr + if on_setattr and on_setattr is not setters.NO_OP: + sa_attrs[a.name] = a, on_setattr + + if not sa_attrs: + return self + + if self._has_custom_setattr: + # We need to write a __setattr__ but there already is one! + msg = "Can't combine custom __setattr__ with on_setattr hooks." + raise ValueError(msg) + + # docstring comes from _add_method_dunders + def __setattr__(self, name, val): + try: + a, hook = sa_attrs[name] + except KeyError: + nval = val + else: + nval = hook(self, a, val) + + _OBJ_SETATTR(self, name, nval) + + self._cls_dict["__attrs_own_setattr__"] = True + self._cls_dict["__setattr__"] = self._add_method_dunders(__setattr__) + self._wrote_own_setattr = True + + return self + + def _add_method_dunders_unsafe(self, method: Callable) -> Callable: + """ + Add __module__ and __qualname__ to a *method*. + """ + method.__module__ = self._cls.__module__ + + method.__qualname__ = f"{self._cls.__qualname__}.{method.__name__}" + + method.__doc__ = ( + f"Method generated by attrs for class {self._cls.__qualname__}." + ) + + return method + + def _add_method_dunders_safe(self, method: Callable) -> Callable: + """ + Add __module__ and __qualname__ to a *method* if possible. + """ + with contextlib.suppress(AttributeError): + method.__module__ = self._cls.__module__ + + with contextlib.suppress(AttributeError): + method.__qualname__ = f"{self._cls.__qualname__}.{method.__name__}" + + with contextlib.suppress(AttributeError): + method.__doc__ = f"Method generated by attrs for class {self._cls.__qualname__}." + + return method + + +def _determine_attrs_eq_order(cmp, eq, order, default_eq): + """ + Validate the combination of *cmp*, *eq*, and *order*. Derive the effective + values of eq and order. If *eq* is None, set it to *default_eq*. + """ + if cmp is not None and any((eq is not None, order is not None)): + msg = "Don't mix `cmp` with `eq' and `order`." + raise ValueError(msg) + + # cmp takes precedence due to bw-compatibility. + if cmp is not None: + return cmp, cmp + + # If left None, equality is set to the specified default and ordering + # mirrors equality. + if eq is None: + eq = default_eq + + if order is None: + order = eq + + if eq is False and order is True: + msg = "`order` can only be True if `eq` is True too." + raise ValueError(msg) + + return eq, order + + +def _determine_attrib_eq_order(cmp, eq, order, default_eq): + """ + Validate the combination of *cmp*, *eq*, and *order*. Derive the effective + values of eq and order. If *eq* is None, set it to *default_eq*. + """ + if cmp is not None and any((eq is not None, order is not None)): + msg = "Don't mix `cmp` with `eq' and `order`." + raise ValueError(msg) + + def decide_callable_or_boolean(value): + """ + Decide whether a key function is used. + """ + if callable(value): + value, key = True, value + else: + key = None + return value, key + + # cmp takes precedence due to bw-compatibility. + if cmp is not None: + cmp, cmp_key = decide_callable_or_boolean(cmp) + return cmp, cmp_key, cmp, cmp_key + + # If left None, equality is set to the specified default and ordering + # mirrors equality. + if eq is None: + eq, eq_key = default_eq, None + else: + eq, eq_key = decide_callable_or_boolean(eq) + + if order is None: + order, order_key = eq, eq_key + else: + order, order_key = decide_callable_or_boolean(order) + + if eq is False and order is True: + msg = "`order` can only be True if `eq` is True too." + raise ValueError(msg) + + return eq, eq_key, order, order_key + + +def _determine_whether_to_implement( + cls, flag, auto_detect, dunders, default=True +): + """ + Check whether we should implement a set of methods for *cls*. + + *flag* is the argument passed into @attr.s like 'init', *auto_detect* the + same as passed into @attr.s and *dunders* is a tuple of attribute names + whose presence signal that the user has implemented it themselves. + + Return *default* if no reason for either for or against is found. + """ + if flag is True or flag is False: + return flag + + if flag is None and auto_detect is False: + return default + + # Logically, flag is None and auto_detect is True here. + for dunder in dunders: + if _has_own_attribute(cls, dunder): + return False + + return default + + +def attrs( + maybe_cls=None, + these=None, + repr_ns=None, + repr=None, + cmp=None, + hash=None, + init=None, + slots=False, + frozen=False, + weakref_slot=True, + str=False, + auto_attribs=False, + kw_only=False, + cache_hash=False, + auto_exc=False, + eq=None, + order=None, + auto_detect=False, + collect_by_mro=False, + getstate_setstate=None, + on_setattr=None, + field_transformer=None, + match_args=True, + unsafe_hash=None, + force_kw_only=True, +): + r""" + A class decorator that adds :term:`dunder methods` according to the + specified attributes using `attr.ib` or the *these* argument. + + Consider using `attrs.define` / `attrs.frozen` in new code (``attr.s`` will + *never* go away, though). + + Args: + repr_ns (str): + When using nested classes, there was no way in Python 2 to + automatically detect that. This argument allows to set a custom + name for a more meaningful ``repr`` output. This argument is + pointless in Python 3 and is therefore deprecated. + + .. caution:: + Refer to `attrs.define` for the rest of the parameters, but note that they + can have different defaults. + + Notably, leaving *on_setattr* as `None` will **not** add any hooks. + + .. versionadded:: 16.0.0 *slots* + .. versionadded:: 16.1.0 *frozen* + .. versionadded:: 16.3.0 *str* + .. versionadded:: 16.3.0 Support for ``__attrs_post_init__``. + .. versionchanged:: 17.1.0 + *hash* supports `None` as value which is also the default now. + .. versionadded:: 17.3.0 *auto_attribs* + .. versionchanged:: 18.1.0 + If *these* is passed, no attributes are deleted from the class body. + .. versionchanged:: 18.1.0 If *these* is ordered, the order is retained. + .. versionadded:: 18.2.0 *weakref_slot* + .. deprecated:: 18.2.0 + ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now raise a + `DeprecationWarning` if the classes compared are subclasses of + each other. ``__eq`` and ``__ne__`` never tried to compared subclasses + to each other. + .. versionchanged:: 19.2.0 + ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now do not consider + subclasses comparable anymore. + .. versionadded:: 18.2.0 *kw_only* + .. versionadded:: 18.2.0 *cache_hash* + .. versionadded:: 19.1.0 *auto_exc* + .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. + .. versionadded:: 19.2.0 *eq* and *order* + .. versionadded:: 20.1.0 *auto_detect* + .. versionadded:: 20.1.0 *collect_by_mro* + .. versionadded:: 20.1.0 *getstate_setstate* + .. versionadded:: 20.1.0 *on_setattr* + .. versionadded:: 20.3.0 *field_transformer* + .. versionchanged:: 21.1.0 + ``init=False`` injects ``__attrs_init__`` + .. versionchanged:: 21.1.0 Support for ``__attrs_pre_init__`` + .. versionchanged:: 21.1.0 *cmp* undeprecated + .. versionadded:: 21.3.0 *match_args* + .. versionadded:: 22.2.0 + *unsafe_hash* as an alias for *hash* (for :pep:`681` compliance). + .. deprecated:: 24.1.0 *repr_ns* + .. versionchanged:: 24.1.0 + Instances are not compared as tuples of attributes anymore, but using a + big ``and`` condition. This is faster and has more correct behavior for + uncomparable values like `math.nan`. + .. versionadded:: 24.1.0 + If a class has an *inherited* classmethod called + ``__attrs_init_subclass__``, it is executed after the class is created. + .. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*. + .. versionchanged:: 25.4.0 + *kw_only* now only applies to attributes defined in the current class, + and respects attribute-level ``kw_only=False`` settings. + .. versionadded:: 25.4.0 *force_kw_only* + """ + if repr_ns is not None: + import warnings + + warnings.warn( + DeprecationWarning( + "The `repr_ns` argument is deprecated and will be removed in or after August 2025." + ), + stacklevel=2, + ) + + eq_, order_ = _determine_attrs_eq_order(cmp, eq, order, None) + + # unsafe_hash takes precedence due to PEP 681. + if unsafe_hash is not None: + hash = unsafe_hash + + if isinstance(on_setattr, (list, tuple)): + on_setattr = setters.pipe(*on_setattr) + + def wrap(cls): + nonlocal hash + is_frozen = frozen or _has_frozen_base_class(cls) + is_exc = auto_exc is True and issubclass(cls, BaseException) + has_own_setattr = auto_detect and _has_own_attribute( + cls, "__setattr__" + ) + + if has_own_setattr and is_frozen: + msg = "Can't freeze a class with a custom __setattr__." + raise ValueError(msg) + + eq = not is_exc and _determine_whether_to_implement( + cls, eq_, auto_detect, ("__eq__", "__ne__") + ) + + Hashability = ClassProps.Hashability + + if is_exc: + hashability = Hashability.LEAVE_ALONE + elif hash is True: + hashability = ( + Hashability.HASHABLE_CACHED + if cache_hash + else Hashability.HASHABLE + ) + elif hash is False: + hashability = Hashability.LEAVE_ALONE + elif hash is None: + if auto_detect is True and _has_own_attribute(cls, "__hash__"): + hashability = Hashability.LEAVE_ALONE + elif eq is True and is_frozen is True: + hashability = ( + Hashability.HASHABLE_CACHED + if cache_hash + else Hashability.HASHABLE + ) + elif eq is False: + hashability = Hashability.LEAVE_ALONE + else: + hashability = Hashability.UNHASHABLE + else: + msg = "Invalid value for hash. Must be True, False, or None." + raise TypeError(msg) + + KeywordOnly = ClassProps.KeywordOnly + if kw_only: + kwo = KeywordOnly.FORCE if force_kw_only else KeywordOnly.YES + else: + kwo = KeywordOnly.NO + + props = ClassProps( + is_exception=is_exc, + is_frozen=is_frozen, + is_slotted=slots, + collected_fields_by_mro=collect_by_mro, + added_init=_determine_whether_to_implement( + cls, init, auto_detect, ("__init__",) + ), + added_repr=_determine_whether_to_implement( + cls, repr, auto_detect, ("__repr__",) + ), + added_eq=eq, + added_ordering=not is_exc + and _determine_whether_to_implement( + cls, + order_, + auto_detect, + ("__lt__", "__le__", "__gt__", "__ge__"), + ), + hashability=hashability, + added_match_args=match_args, + kw_only=kwo, + has_weakref_slot=weakref_slot, + added_str=str, + added_pickling=_determine_whether_to_implement( + cls, + getstate_setstate, + auto_detect, + ("__getstate__", "__setstate__"), + default=slots, + ), + on_setattr_hook=on_setattr, + field_transformer=field_transformer, + ) + + if not props.is_hashable and cache_hash: + msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled." + raise TypeError(msg) + + builder = _ClassBuilder( + cls, + these, + auto_attribs=auto_attribs, + props=props, + has_custom_setattr=has_own_setattr, + ) + + if props.added_repr: + builder.add_repr(repr_ns) + + if props.added_str: + builder.add_str() + + if props.added_eq: + builder.add_eq() + if props.added_ordering: + builder.add_order() + + if not frozen: + builder.add_setattr() + + if props.is_hashable: + builder.add_hash() + elif props.hashability is Hashability.UNHASHABLE: + builder.make_unhashable() + + if props.added_init: + builder.add_init() + else: + builder.add_attrs_init() + if cache_hash: + msg = "Invalid value for cache_hash. To use hash caching, init must be True." + raise TypeError(msg) + + if PY_3_13_PLUS and not _has_own_attribute(cls, "__replace__"): + builder.add_replace() + + if ( + PY_3_10_PLUS + and match_args + and not _has_own_attribute(cls, "__match_args__") + ): + builder.add_match_args() + + return builder.build_class() + + # maybe_cls's type depends on the usage of the decorator. It's a class + # if it's used as `@attrs` but `None` if used as `@attrs()`. + if maybe_cls is None: + return wrap + + return wrap(maybe_cls) + + +_attrs = attrs +""" +Internal alias so we can use it in functions that take an argument called +*attrs*. +""" + + +def _has_frozen_base_class(cls): + """ + Check whether *cls* has a frozen ancestor by looking at its + __setattr__. + """ + return cls.__setattr__ is _frozen_setattrs + + +def _generate_unique_filename(cls: type, func_name: str) -> str: + """ + Create a "filename" suitable for a function being generated. + """ + return ( + f"" + ) + + +def _make_hash_script( + cls: type, attrs: list[Attribute], frozen: bool, cache_hash: bool +) -> tuple[str, dict]: + attrs = tuple( + a for a in attrs if a.hash is True or (a.hash is None and a.eq is True) + ) + + tab = " " + + type_hash = hash(_generate_unique_filename(cls, "hash")) + # If eq is custom generated, we need to include the functions in globs + globs = {} + + hash_def = "def __hash__(self" + hash_func = "hash((" + closing_braces = "))" + if not cache_hash: + hash_def += "):" + else: + hash_def += ", *" + + hash_def += ", _cache_wrapper=__import__('attr._make')._make._CacheHashWrapper):" + hash_func = "_cache_wrapper(" + hash_func + closing_braces += ")" + + method_lines = [hash_def] + + def append_hash_computation_lines(prefix, indent): + """ + Generate the code for actually computing the hash code. + Below this will either be returned directly or used to compute + a value which is then cached, depending on the value of cache_hash + """ + + method_lines.extend( + [ + indent + prefix + hash_func, + indent + f" {type_hash},", + ] + ) + + for a in attrs: + if a.eq_key: + cmp_name = f"_{a.name}_key" + globs[cmp_name] = a.eq_key + method_lines.append( + indent + f" {cmp_name}(self.{a.name})," + ) + else: + method_lines.append(indent + f" self.{a.name},") + + method_lines.append(indent + " " + closing_braces) + + if cache_hash: + method_lines.append(tab + f"if self.{_HASH_CACHE_FIELD} is None:") + if frozen: + append_hash_computation_lines( + f"object.__setattr__(self, '{_HASH_CACHE_FIELD}', ", tab * 2 + ) + method_lines.append(tab * 2 + ")") # close __setattr__ + else: + append_hash_computation_lines( + f"self.{_HASH_CACHE_FIELD} = ", tab * 2 + ) + method_lines.append(tab + f"return self.{_HASH_CACHE_FIELD}") + else: + append_hash_computation_lines("return ", tab) + + script = "\n".join(method_lines) + return script, globs + + +def _add_hash(cls: type, attrs: list[Attribute]): + """ + Add a hash method to *cls*. + """ + script, globs = _make_hash_script( + cls, attrs, frozen=False, cache_hash=False + ) + _compile_and_eval( + script, globs, filename=_generate_unique_filename(cls, "__hash__") + ) + cls.__hash__ = globs["__hash__"] + return cls + + +def __ne__(self, other): + """ + Check equality and either forward a NotImplemented or + return the result negated. + """ + result = self.__eq__(other) + if result is NotImplemented: + return NotImplemented + + return not result + + +def _make_eq_script(attrs: list) -> tuple[str, dict]: + """ + Create __eq__ method for *cls* with *attrs*. + """ + attrs = [a for a in attrs if a.eq] + + lines = [ + "def __eq__(self, other):", + " if other.__class__ is not self.__class__:", + " return NotImplemented", + ] + + globs = {} + if attrs: + lines.append(" return (") + for a in attrs: + if a.eq_key: + cmp_name = f"_{a.name}_key" + # Add the key function to the global namespace + # of the evaluated function. + globs[cmp_name] = a.eq_key + lines.append( + f" {cmp_name}(self.{a.name}) == {cmp_name}(other.{a.name})" + ) + else: + lines.append(f" self.{a.name} == other.{a.name}") + if a is not attrs[-1]: + lines[-1] = f"{lines[-1]} and" + lines.append(" )") + else: + lines.append(" return True") + + script = "\n".join(lines) + + return script, globs + + +def _make_order(cls, attrs): + """ + Create ordering methods for *cls* with *attrs*. + """ + attrs = [a for a in attrs if a.order] + + def attrs_to_tuple(obj): + """ + Save us some typing. + """ + return tuple( + key(value) if key else value + for value, key in ( + (getattr(obj, a.name), a.order_key) for a in attrs + ) + ) + + def __lt__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) < attrs_to_tuple(other) + + return NotImplemented + + def __le__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) <= attrs_to_tuple(other) + + return NotImplemented + + def __gt__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) > attrs_to_tuple(other) + + return NotImplemented + + def __ge__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) >= attrs_to_tuple(other) + + return NotImplemented + + return __lt__, __le__, __gt__, __ge__ + + +def _add_eq(cls, attrs=None): + """ + Add equality methods to *cls* with *attrs*. + """ + if attrs is None: + attrs = cls.__attrs_attrs__ + + script, globs = _make_eq_script(attrs) + _compile_and_eval( + script, globs, filename=_generate_unique_filename(cls, "__eq__") + ) + cls.__eq__ = globs["__eq__"] + cls.__ne__ = __ne__ + + return cls + + +def _make_repr_script(attrs, ns) -> tuple[str, dict]: + """ + Create the source and globs for a __repr__ and return it. + """ + # Figure out which attributes to include, and which function to use to + # format them. The a.repr value can be either bool or a custom + # callable. + attr_names_with_reprs = tuple( + (a.name, (repr if a.repr is True else a.repr), a.init) + for a in attrs + if a.repr is not False + ) + globs = { + name + "_repr": r for name, r, _ in attr_names_with_reprs if r != repr + } + globs["_compat"] = _compat + globs["AttributeError"] = AttributeError + globs["NOTHING"] = NOTHING + attribute_fragments = [] + for name, r, i in attr_names_with_reprs: + accessor = ( + "self." + name if i else 'getattr(self, "' + name + '", NOTHING)' + ) + fragment = ( + "%s={%s!r}" % (name, accessor) + if r == repr + else "%s={%s_repr(%s)}" % (name, name, accessor) + ) + attribute_fragments.append(fragment) + repr_fragment = ", ".join(attribute_fragments) + + if ns is None: + cls_name_fragment = '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}' + else: + cls_name_fragment = ns + ".{self.__class__.__name__}" + + lines = [ + "def __repr__(self):", + " try:", + " already_repring = _compat.repr_context.already_repring", + " except AttributeError:", + " already_repring = {id(self),}", + " _compat.repr_context.already_repring = already_repring", + " else:", + " if id(self) in already_repring:", + " return '...'", + " else:", + " already_repring.add(id(self))", + " try:", + f" return f'{cls_name_fragment}({repr_fragment})'", + " finally:", + " already_repring.remove(id(self))", + ] + + return "\n".join(lines), globs + + +def _add_repr(cls, ns=None, attrs=None): + """ + Add a repr method to *cls*. + """ + if attrs is None: + attrs = cls.__attrs_attrs__ + + script, globs = _make_repr_script(attrs, ns) + _compile_and_eval( + script, globs, filename=_generate_unique_filename(cls, "__repr__") + ) + cls.__repr__ = globs["__repr__"] + return cls + + +def fields(cls): + """ + Return the tuple of *attrs* attributes for a class or instance. + + The tuple also allows accessing the fields by their names (see below for + examples). + + Args: + cls (type): Class or instance to introspect. + + Raises: + TypeError: If *cls* is neither a class nor an *attrs* instance. + + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + Returns: + tuple (with name accessors) of `attrs.Attribute` + + .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields + by name. + .. versionchanged:: 23.1.0 Add support for generic classes. + .. versionchanged:: 26.1.0 Add support for instances. + """ + generic_base = get_generic_base(cls) + + if generic_base is None and not isinstance(cls, type): + type_ = type(cls) + if getattr(type_, "__attrs_attrs__", None) is None: + msg = "Passed object must be a class or attrs instance." + raise TypeError(msg) + + return fields(type_) + + attrs = getattr(cls, "__attrs_attrs__", None) + + if attrs is None: + if generic_base is not None: + attrs = getattr(generic_base, "__attrs_attrs__", None) + if attrs is not None: + # Even though this is global state, stick it on here to speed + # it up. We rely on `cls` being cached for this to be + # efficient. + cls.__attrs_attrs__ = attrs + return attrs + msg = f"{cls!r} is not an attrs-decorated class." + raise NotAnAttrsClassError(msg) + + return attrs + + +def fields_dict(cls): + """ + Return an ordered dictionary of *attrs* attributes for a class, whose keys + are the attribute names. + + Args: + cls (type): Class to introspect. + + Raises: + TypeError: If *cls* is not a class. + + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + Returns: + dict[str, attrs.Attribute]: Dict of attribute name to definition + + .. versionadded:: 18.1.0 + """ + if not isinstance(cls, type): + msg = "Passed object must be a class." + raise TypeError(msg) + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is None: + msg = f"{cls!r} is not an attrs-decorated class." + raise NotAnAttrsClassError(msg) + return {a.name: a for a in attrs} + + +def validate(inst): + """ + Validate all attributes on *inst* that have a validator. + + Leaves all exceptions through. + + Args: + inst: Instance of a class with *attrs* attributes. + """ + if _config._run_validators is False: + return + + for a in fields(inst.__class__): + v = a.validator + if v is not None: + v(inst, a, getattr(inst, a.name)) + + +def _is_slot_attr(a_name, base_attr_map): + """ + Check if the attribute name comes from a slot class. + """ + cls = base_attr_map.get(a_name) + return cls and "__slots__" in cls.__dict__ + + +def _make_init_script( + cls, + attrs, + pre_init, + pre_init_has_args, + post_init, + frozen, + slots, + cache_hash, + base_attr_map, + is_exc, + cls_on_setattr, + attrs_init, +) -> tuple[str, dict, dict]: + has_cls_on_setattr = ( + cls_on_setattr is not None and cls_on_setattr is not setters.NO_OP + ) + + if frozen and has_cls_on_setattr: + msg = "Frozen classes can't use on_setattr." + raise ValueError(msg) + + needs_cached_setattr = cache_hash or frozen + filtered_attrs = [] + attr_dict = {} + for a in attrs: + if not a.init and a.default is NOTHING: + continue + + filtered_attrs.append(a) + attr_dict[a.name] = a + + if a.on_setattr is not None: + if frozen is True and a.on_setattr is not setters.NO_OP: + msg = "Frozen classes can't use on_setattr." + raise ValueError(msg) + + needs_cached_setattr = True + elif has_cls_on_setattr and a.on_setattr is not setters.NO_OP: + needs_cached_setattr = True + + script, globs, annotations = _attrs_to_init_script( + filtered_attrs, + frozen, + slots, + pre_init, + pre_init_has_args, + post_init, + cache_hash, + base_attr_map, + is_exc, + needs_cached_setattr, + has_cls_on_setattr, + "__attrs_init__" if attrs_init else "__init__", + ) + if cls.__module__ in sys.modules: + # This makes typing.get_type_hints(CLS.__init__) resolve string types. + globs.update(sys.modules[cls.__module__].__dict__) + + globs.update({"NOTHING": NOTHING, "attr_dict": attr_dict}) + + if needs_cached_setattr: + # Save the lookup overhead in __init__ if we need to circumvent + # setattr hooks. + globs["_cached_setattr_get"] = _OBJ_SETATTR.__get__ + + return script, globs, annotations + + +def _setattr(attr_name: str, value_var: str, has_on_setattr: bool) -> str: + """ + Use the cached object.setattr to set *attr_name* to *value_var*. + """ + return f"_setattr('{attr_name}', {value_var})" + + +def _setattr_with_converter( + attr_name: str, value_var: str, has_on_setattr: bool, converter: Converter +) -> str: + """ + Use the cached object.setattr to set *attr_name* to *value_var*, but run + its converter first. + """ + return f"_setattr('{attr_name}', {converter._fmt_converter_call(attr_name, value_var)})" + + +def _assign(attr_name: str, value: str, has_on_setattr: bool) -> str: + """ + Unless *attr_name* has an on_setattr hook, use normal assignment. Otherwise + relegate to _setattr. + """ + if has_on_setattr: + return _setattr(attr_name, value, True) + + return f"self.{attr_name} = {value}" + + +def _assign_with_converter( + attr_name: str, value_var: str, has_on_setattr: bool, converter: Converter +) -> str: + """ + Unless *attr_name* has an on_setattr hook, use normal assignment after + conversion. Otherwise relegate to _setattr_with_converter. + """ + if has_on_setattr: + return _setattr_with_converter(attr_name, value_var, True, converter) + + return f"self.{attr_name} = {converter._fmt_converter_call(attr_name, value_var)}" + + +def _determine_setters( + frozen: bool, slots: bool, base_attr_map: dict[str, type] +): + """ + Determine the correct setter functions based on whether a class is frozen + and/or slotted. + """ + if frozen is True: + if slots is True: + return (), _setattr, _setattr_with_converter + + # Dict frozen classes assign directly to __dict__. + # But only if the attribute doesn't come from an ancestor slot + # class. + # Note _inst_dict will be used again below if cache_hash is True + + def fmt_setter( + attr_name: str, value_var: str, has_on_setattr: bool + ) -> str: + if _is_slot_attr(attr_name, base_attr_map): + return _setattr(attr_name, value_var, has_on_setattr) + + return f"_inst_dict['{attr_name}'] = {value_var}" + + def fmt_setter_with_converter( + attr_name: str, + value_var: str, + has_on_setattr: bool, + converter: Converter, + ) -> str: + if has_on_setattr or _is_slot_attr(attr_name, base_attr_map): + return _setattr_with_converter( + attr_name, value_var, has_on_setattr, converter + ) + + return f"_inst_dict['{attr_name}'] = {converter._fmt_converter_call(attr_name, value_var)}" + + return ( + ("_inst_dict = self.__dict__",), + fmt_setter, + fmt_setter_with_converter, + ) + + # Not frozen -- we can just assign directly. + return (), _assign, _assign_with_converter + + +def _attrs_to_init_script( + attrs: list[Attribute], + is_frozen: bool, + is_slotted: bool, + call_pre_init: bool, + pre_init_has_args: bool, + call_post_init: bool, + does_cache_hash: bool, + base_attr_map: dict[str, type], + is_exc: bool, + needs_cached_setattr: bool, + has_cls_on_setattr: bool, + method_name: str, +) -> tuple[str, dict, dict]: + """ + Return a script of an initializer for *attrs*, a dict of globals, and + annotations for the initializer. + + The globals are required by the generated script. + """ + lines = ["self.__attrs_pre_init__()"] if call_pre_init else [] + + if needs_cached_setattr: + lines.append( + # Circumvent the __setattr__ descriptor to save one lookup per + # assignment. Note _setattr will be used again below if + # does_cache_hash is True. + "_setattr = _cached_setattr_get(self)" + ) + + extra_lines, fmt_setter, fmt_setter_with_converter = _determine_setters( + is_frozen, is_slotted, base_attr_map + ) + lines.extend(extra_lines) + + args = [] # Parameters in the definition of __init__ + pre_init_args = [] # Parameters in the call to __attrs_pre_init__ + kw_only_args = [] # Used for both 'args' and 'pre_init_args' above + attrs_to_validate = [] + + # This is a dictionary of names to validator and converter callables. + # Injecting this into __init__ globals lets us avoid lookups. + names_for_globals = {} + annotations = {"return": None} + + for a in attrs: + if a.validator: + attrs_to_validate.append(a) + + attr_name = a.name + has_on_setattr = a.on_setattr is not None or ( + a.on_setattr is not setters.NO_OP and has_cls_on_setattr + ) + # a.alias is set to maybe-mangled attr_name in _ClassBuilder if not + # explicitly provided + arg_name = a.alias + + has_factory = isinstance(a.default, Factory) + maybe_self = "self" if has_factory and a.default.takes_self else "" + + if a.converter is not None and not isinstance(a.converter, Converter): + converter = Converter(a.converter) + else: + converter = a.converter + + if a.init is False: + if has_factory: + init_factory_name = _INIT_FACTORY_PAT % (a.name,) + if converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, + init_factory_name + f"({maybe_self})", + has_on_setattr, + converter, + ) + ) + names_for_globals[converter._get_global_name(a.name)] = ( + converter.converter + ) + else: + lines.append( + fmt_setter( + attr_name, + init_factory_name + f"({maybe_self})", + has_on_setattr, + ) + ) + names_for_globals[init_factory_name] = a.default.factory + elif converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, + f"attr_dict['{attr_name}'].default", + has_on_setattr, + converter, + ) + ) + names_for_globals[converter._get_global_name(a.name)] = ( + converter.converter + ) + else: + lines.append( + fmt_setter( + attr_name, + f"attr_dict['{attr_name}'].default", + has_on_setattr, + ) + ) + elif a.default is not NOTHING and not has_factory: + arg = f"{arg_name}=attr_dict['{attr_name}'].default" + if a.kw_only: + kw_only_args.append(arg) + else: + args.append(arg) + pre_init_args.append(arg_name) + + if converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, arg_name, has_on_setattr, converter + ) + ) + names_for_globals[converter._get_global_name(a.name)] = ( + converter.converter + ) + else: + lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) + + elif has_factory: + arg = f"{arg_name}=NOTHING" + if a.kw_only: + kw_only_args.append(arg) + else: + args.append(arg) + pre_init_args.append(arg_name) + lines.append(f"if {arg_name} is not NOTHING:") + + init_factory_name = _INIT_FACTORY_PAT % (a.name,) + if converter is not None: + lines.append( + " " + + fmt_setter_with_converter( + attr_name, arg_name, has_on_setattr, converter + ) + ) + lines.append("else:") + lines.append( + " " + + fmt_setter_with_converter( + attr_name, + init_factory_name + "(" + maybe_self + ")", + has_on_setattr, + converter, + ) + ) + names_for_globals[converter._get_global_name(a.name)] = ( + converter.converter + ) + else: + lines.append( + " " + fmt_setter(attr_name, arg_name, has_on_setattr) + ) + lines.append("else:") + lines.append( + " " + + fmt_setter( + attr_name, + init_factory_name + "(" + maybe_self + ")", + has_on_setattr, + ) + ) + names_for_globals[init_factory_name] = a.default.factory + else: + if a.kw_only: + kw_only_args.append(arg_name) + else: + args.append(arg_name) + pre_init_args.append(arg_name) + + if converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, arg_name, has_on_setattr, converter + ) + ) + names_for_globals[converter._get_global_name(a.name)] = ( + converter.converter + ) + else: + lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) + + if a.init is True: + if a.type is not None and converter is None: + annotations[arg_name] = a.type + elif converter is not None and converter._first_param_type: + # Use the type from the converter if present. + annotations[arg_name] = converter._first_param_type + + if attrs_to_validate: # we can skip this if there are no validators. + names_for_globals["_config"] = _config + lines.append("if _config._run_validators is True:") + for a in attrs_to_validate: + val_name = "__attr_validator_" + a.name + attr_name = "__attr_" + a.name + lines.append(f" {val_name}(self, {attr_name}, self.{a.name})") + names_for_globals[val_name] = a.validator + names_for_globals[attr_name] = a + + if call_post_init: + lines.append("self.__attrs_post_init__()") + + # Because this is set only after __attrs_post_init__ is called, a crash + # will result if post-init tries to access the hash code. This seemed + # preferable to setting this beforehand, in which case alteration to field + # values during post-init combined with post-init accessing the hash code + # would result in silent bugs. + if does_cache_hash: + if is_frozen: + if is_slotted: + init_hash_cache = f"_setattr('{_HASH_CACHE_FIELD}', None)" + else: + init_hash_cache = f"_inst_dict['{_HASH_CACHE_FIELD}'] = None" + else: + init_hash_cache = f"self.{_HASH_CACHE_FIELD} = None" + lines.append(init_hash_cache) + + # For exceptions we rely on BaseException.__init__ for proper + # initialization. + if is_exc: + vals = ",".join(f"self.{a.name}" for a in attrs if a.init) + + lines.append(f"BaseException.__init__(self, {vals})") + + args = ", ".join(args) + pre_init_args = ", ".join(pre_init_args) + if kw_only_args: + # leading comma & kw_only args + args += f"{', ' if args else ''}*, {', '.join(kw_only_args)}" + pre_init_kw_only_args = ", ".join( + [ + f"{kw_arg_name}={kw_arg_name}" + # We need to remove the defaults from the kw_only_args. + for kw_arg_name in (kwa.split("=")[0] for kwa in kw_only_args) + ] + ) + pre_init_args += ", " if pre_init_args else "" + pre_init_args += pre_init_kw_only_args + + if call_pre_init and pre_init_has_args: + # If pre init method has arguments, pass the values given to __init__. + lines[0] = f"self.__attrs_pre_init__({pre_init_args})" + + # Python <3.12 doesn't allow backslashes in f-strings. + NL = "\n " + return ( + f"""def {method_name}(self, {args}): + {NL.join(lines) if lines else "pass"} +""", + names_for_globals, + annotations, + ) + + +def _default_init_alias_for(name: str) -> str: + """ + The default __init__ parameter name for a field. + + This performs private-name adjustment via leading-unscore stripping, + and is the default value of Attribute.alias if not provided. + """ + + return name.lstrip("_") + + +class Attribute: + """ + *Read-only* representation of an attribute. + + .. warning:: + + You should never instantiate this class yourself. + + The class has *all* arguments of `attr.ib` (except for ``factory`` which is + only syntactic sugar for ``default=Factory(...)`` plus the following: + + - ``name`` (`str`): The name of the attribute. + - ``alias`` (`str`): The __init__ parameter name of the attribute, after + any explicit overrides and default private-attribute-name handling. + - ``alias_is_default`` (`bool`): Whether the ``alias`` was automatically + generated (``True``) or explicitly provided by the user (``False``). + - ``inherited`` (`bool`): Whether or not that attribute has been inherited + from a base class. + - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The + callables that are used for comparing and ordering objects by this + attribute, respectively. These are set by passing a callable to + `attr.ib`'s ``eq``, ``order``, or ``cmp`` arguments. See also + :ref:`comparison customization `. + + Instances of this class are frequently used for introspection purposes + like: + + - `fields` returns a tuple of them. + - Validators get them passed as the first argument. + - The :ref:`field transformer ` hook receives a list of + them. + - The ``alias`` property exposes the __init__ parameter name of the field, + with any overrides and default private-attribute handling applied. + + + .. versionadded:: 20.1.0 *inherited* + .. versionadded:: 20.1.0 *on_setattr* + .. versionchanged:: 20.2.0 *inherited* is not taken into account for + equality checks and hashing anymore. + .. versionadded:: 21.1.0 *eq_key* and *order_key* + .. versionadded:: 22.2.0 *alias* + .. versionadded:: 26.1.0 *alias_is_default* + + For the full version history of the fields, see `attr.ib`. + """ + + # These slots must NOT be reordered because we use them later for + # instantiation. + __slots__ = ( # noqa: RUF023 + "name", + "default", + "validator", + "repr", + "eq", + "eq_key", + "order", + "order_key", + "hash", + "init", + "metadata", + "type", + "converter", + "kw_only", + "inherited", + "on_setattr", + "alias", + "alias_is_default", + ) + + def __init__( + self, + name, + default, + validator, + repr, + cmp, # XXX: unused, remove along with other cmp code. + hash, + init, + inherited, + metadata=None, + type=None, + converter=None, + kw_only=False, + eq=None, + eq_key=None, + order=None, + order_key=None, + on_setattr=None, + alias=None, + alias_is_default=None, + ): + eq, eq_key, order, order_key = _determine_attrib_eq_order( + cmp, eq_key or eq, order_key or order, True + ) + + # Cache this descriptor here to speed things up later. + bound_setattr = _OBJ_SETATTR.__get__(self) + + # Despite the big red warning, people *do* instantiate `Attribute` + # themselves. + bound_setattr("name", name) + bound_setattr("default", default) + bound_setattr("validator", validator) + bound_setattr("repr", repr) + bound_setattr("eq", eq) + bound_setattr("eq_key", eq_key) + bound_setattr("order", order) + bound_setattr("order_key", order_key) + bound_setattr("hash", hash) + bound_setattr("init", init) + bound_setattr("converter", converter) + bound_setattr( + "metadata", + ( + types.MappingProxyType(dict(metadata)) # Shallow copy + if metadata + else _EMPTY_METADATA_SINGLETON + ), + ) + bound_setattr("type", type) + bound_setattr("kw_only", kw_only) + bound_setattr("inherited", inherited) + bound_setattr("on_setattr", on_setattr) + bound_setattr("alias", alias) + bound_setattr( + "alias_is_default", + alias is None if alias_is_default is None else alias_is_default, + ) + + def __setattr__(self, name, value): + raise FrozenInstanceError + + @classmethod + def from_counting_attr( + cls, name: str, ca: _CountingAttr, kw_only: bool, type=None + ): + # The 'kw_only' argument is the class-level setting, and is used if the + # attribute itself does not explicitly set 'kw_only'. + # type holds the annotated value. deal with conflicts: + if type is None: + type = ca.type + elif ca.type is not None: + msg = f"Type annotation and type argument cannot both be present for '{name}'." + raise ValueError(msg) + return cls( + name, + ca._default, + ca._validator, + ca.repr, + None, + ca.hash, + ca.init, + False, + ca.metadata, + type, + ca.converter, + kw_only if ca.kw_only is None else ca.kw_only, + ca.eq, + ca.eq_key, + ca.order, + ca.order_key, + ca.on_setattr, + ca.alias, + ca.alias is None, + ) + + # Don't use attrs.evolve since fields(Attribute) doesn't work + def evolve(self, **changes): + """ + Copy *self* and apply *changes*. + + This works similarly to `attrs.evolve` but that function does not work + with :class:`attrs.Attribute`. + + It is mainly meant to be used for `transform-fields`. + + .. versionadded:: 20.3.0 + """ + new = copy.copy(self) + + new._setattrs(changes.items()) + + if "alias" in changes and "alias_is_default" not in changes: + # Explicit alias provided -- no longer the default. + _OBJ_SETATTR.__get__(new)("alias_is_default", False) + elif ( + "name" in changes + and "alias" not in changes + # Don't auto-generate alias if the user picked picked the old one. + and self.alias_is_default + ): + # Name changed, alias was auto-generated -- update it. + _OBJ_SETATTR.__get__(new)( + "alias", _default_init_alias_for(new.name) + ) + + return new + + # Don't use _add_pickle since fields(Attribute) doesn't work + def __getstate__(self): + """ + Play nice with pickle. + """ + return tuple( + getattr(self, name) if name != "metadata" else dict(self.metadata) + for name in self.__slots__ + ) + + def __setstate__(self, state): + """ + Play nice with pickle. + """ + if len(state) < len(self.__slots__): + # Pre-26.1.0 pickle without alias_is_default -- infer it + # heuristically. + state_dict = dict(zip(self.__slots__, state)) + alias_is_default = state_dict.get( + "alias" + ) is None or state_dict.get("alias") == _default_init_alias_for( + state_dict["name"] + ) + state = (*state, alias_is_default) + + self._setattrs(zip(self.__slots__, state)) + + def _setattrs(self, name_values_pairs): + bound_setattr = _OBJ_SETATTR.__get__(self) + for name, value in name_values_pairs: + if name != "metadata": + bound_setattr(name, value) + else: + bound_setattr( + name, + ( + types.MappingProxyType(dict(value)) + if value + else _EMPTY_METADATA_SINGLETON + ), + ) + + +_a = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=(name != "alias_is_default"), + cmp=None, + eq=True, + order=False, + hash=(name != "metadata"), + init=True, + inherited=False, + alias=_default_init_alias_for(name), + ) + for name in Attribute.__slots__ +] + +Attribute = _add_hash( + _add_eq( + _add_repr(Attribute, attrs=_a), + attrs=[a for a in _a if a.name != "inherited"], + ), + attrs=[a for a in _a if a.hash and a.name != "inherited"], +) + + +class _CountingAttr: + """ + Intermediate representation of attributes that uses a counter to preserve + the order in which the attributes have been defined. + + *Internal* data structure of the attrs library. Running into is most + likely the result of a bug like a forgotten `@attr.s` decorator. + """ + + __slots__ = ( + "_default", + "_validator", + "alias", + "converter", + "counter", + "eq", + "eq_key", + "hash", + "init", + "kw_only", + "metadata", + "on_setattr", + "order", + "order_key", + "repr", + "type", + ) + __attrs_attrs__ = ( + *tuple( + Attribute( + name=name, + alias=_default_init_alias_for(name), + default=NOTHING, + validator=None, + repr=True, + cmp=None, + hash=True, + init=True, + kw_only=False, + eq=True, + eq_key=None, + order=False, + order_key=None, + inherited=False, + on_setattr=None, + ) + for name in ( + "counter", + "_default", + "repr", + "eq", + "order", + "hash", + "init", + "on_setattr", + "alias", + ) + ), + Attribute( + name="metadata", + alias="metadata", + default=None, + validator=None, + repr=True, + cmp=None, + hash=False, + init=True, + kw_only=False, + eq=True, + eq_key=None, + order=False, + order_key=None, + inherited=False, + on_setattr=None, + ), + ) + cls_counter = 0 + + def __init__( + self, + default, + validator, + repr, + cmp, + hash, + init, + converter, + metadata, + type, + kw_only, + eq, + eq_key, + order, + order_key, + on_setattr, + alias, + ): + _CountingAttr.cls_counter += 1 + self.counter = _CountingAttr.cls_counter + self._default = default + self._validator = validator + self.converter = converter + self.repr = repr + self.eq = eq + self.eq_key = eq_key + self.order = order + self.order_key = order_key + self.hash = hash + self.init = init + self.metadata = metadata + self.type = type + self.kw_only = kw_only + self.on_setattr = on_setattr + self.alias = alias + + def validator(self, meth): + """ + Decorator that adds *meth* to the list of validators. + + Returns *meth* unchanged. + + .. versionadded:: 17.1.0 + """ + if self._validator is None: + self._validator = meth + else: + self._validator = and_(self._validator, meth) + return meth + + def default(self, meth): + """ + Decorator that allows to set the default for an attribute. + + Returns *meth* unchanged. + + Raises: + DefaultAlreadySetError: If default has been set before. + + .. versionadded:: 17.1.0 + """ + if self._default is not NOTHING: + raise DefaultAlreadySetError + + self._default = Factory(meth, takes_self=True) + + return meth + + +_CountingAttr = _add_eq(_add_repr(_CountingAttr)) + + +class ClassProps: + """ + Effective class properties as derived from parameters to `attr.s()` or + `define()` decorators. + + This is the same data structure that *attrs* uses internally to decide how + to construct the final class. + + Warning: + + This feature is currently **experimental** and is not covered by our + strict backwards-compatibility guarantees. + + + Attributes: + is_exception (bool): + Whether the class is treated as an exception class. + + is_slotted (bool): + Whether the class is `slotted `. + + has_weakref_slot (bool): + Whether the class has a slot for weak references. + + is_frozen (bool): + Whether the class is frozen. + + kw_only (KeywordOnly): + Whether / how the class enforces keyword-only arguments on the + ``__init__`` method. + + collected_fields_by_mro (bool): + Whether the class fields were collected by method resolution order. + That is, correctly but unlike `dataclasses`. + + added_init (bool): + Whether the class has an *attrs*-generated ``__init__`` method. + + added_repr (bool): + Whether the class has an *attrs*-generated ``__repr__`` method. + + added_eq (bool): + Whether the class has *attrs*-generated equality methods. + + added_ordering (bool): + Whether the class has *attrs*-generated ordering methods. + + hashability (Hashability): How `hashable ` the class is. + + added_match_args (bool): + Whether the class supports positional `match ` over its + fields. + + added_str (bool): + Whether the class has an *attrs*-generated ``__str__`` method. + + added_pickling (bool): + Whether the class has *attrs*-generated ``__getstate__`` and + ``__setstate__`` methods for `pickle`. + + on_setattr_hook (Callable[[Any, Attribute[Any], Any], Any] | None): + The class's ``__setattr__`` hook. + + field_transformer (Callable[[Attribute[Any]], Attribute[Any]] | None): + The class's `field transformers `. + + .. versionadded:: 25.4.0 + """ + + class Hashability(enum.Enum): + """ + The hashability of a class. + + .. versionadded:: 25.4.0 + """ + + HASHABLE = "hashable" + """Write a ``__hash__``.""" + HASHABLE_CACHED = "hashable_cache" + """Write a ``__hash__`` and cache the hash.""" + UNHASHABLE = "unhashable" + """Set ``__hash__`` to ``None``.""" + LEAVE_ALONE = "leave_alone" + """Don't touch ``__hash__``.""" + + class KeywordOnly(enum.Enum): + """ + How attributes should be treated regarding keyword-only parameters. + + .. versionadded:: 25.4.0 + """ + + NO = "no" + """Attributes are not keyword-only.""" + YES = "yes" + """Attributes in current class without kw_only=False are keyword-only.""" + FORCE = "force" + """All attributes are keyword-only.""" + + __slots__ = ( # noqa: RUF023 -- order matters for __init__ + "is_exception", + "is_slotted", + "has_weakref_slot", + "is_frozen", + "kw_only", + "collected_fields_by_mro", + "added_init", + "added_repr", + "added_eq", + "added_ordering", + "hashability", + "added_match_args", + "added_str", + "added_pickling", + "on_setattr_hook", + "field_transformer", + ) + + def __init__( + self, + is_exception, + is_slotted, + has_weakref_slot, + is_frozen, + kw_only, + collected_fields_by_mro, + added_init, + added_repr, + added_eq, + added_ordering, + hashability, + added_match_args, + added_str, + added_pickling, + on_setattr_hook, + field_transformer, + ): + self.is_exception = is_exception + self.is_slotted = is_slotted + self.has_weakref_slot = has_weakref_slot + self.is_frozen = is_frozen + self.kw_only = kw_only + self.collected_fields_by_mro = collected_fields_by_mro + self.added_init = added_init + self.added_repr = added_repr + self.added_eq = added_eq + self.added_ordering = added_ordering + self.hashability = hashability + self.added_match_args = added_match_args + self.added_str = added_str + self.added_pickling = added_pickling + self.on_setattr_hook = on_setattr_hook + self.field_transformer = field_transformer + + @property + def is_hashable(self): + return ( + self.hashability is ClassProps.Hashability.HASHABLE + or self.hashability is ClassProps.Hashability.HASHABLE_CACHED + ) + + +_cas = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + eq=True, + order=False, + hash=True, + init=True, + inherited=False, + alias=_default_init_alias_for(name), + ) + for name in ClassProps.__slots__ +] + +ClassProps = _add_eq(_add_repr(ClassProps, attrs=_cas), attrs=_cas) + + +class Factory: + """ + Stores a factory callable. + + If passed as the default value to `attrs.field`, the factory is used to + generate a new value. + + Args: + factory (typing.Callable): + A callable that takes either none or exactly one mandatory + positional argument depending on *takes_self*. + + takes_self (bool): + Pass the partially initialized instance that is being initialized + as a positional argument. + + .. versionadded:: 17.1.0 *takes_self* + """ + + __slots__ = ("factory", "takes_self") + + def __init__(self, factory, takes_self=False): + self.factory = factory + self.takes_self = takes_self + + def __getstate__(self): + """ + Play nice with pickle. + """ + return tuple(getattr(self, name) for name in self.__slots__) + + def __setstate__(self, state): + """ + Play nice with pickle. + """ + for name, value in zip(self.__slots__, state): + setattr(self, name, value) + + +_f = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + eq=True, + order=False, + hash=True, + init=True, + inherited=False, + ) + for name in Factory.__slots__ +] + +Factory = _add_hash(_add_eq(_add_repr(Factory, attrs=_f), attrs=_f), attrs=_f) + + +class Converter: + """ + Stores a converter callable. + + Allows for the wrapped converter to take additional arguments. The + arguments are passed in the order they are documented. + + Args: + converter (Callable): A callable that converts the passed value. + + takes_self (bool): + Pass the partially initialized instance that is being initialized + as a positional argument. (default: `False`) + + takes_field (bool): + Pass the field definition (an :class:`Attribute`) into the + converter as a positional argument. (default: `False`) + + .. versionadded:: 24.1.0 + """ + + __slots__ = ( + "__call__", + "_first_param_type", + "_global_name", + "converter", + "takes_field", + "takes_self", + ) + + def __init__(self, converter, *, takes_self=False, takes_field=False): + self.converter = converter + self.takes_self = takes_self + self.takes_field = takes_field + + ex = _AnnotationExtractor(converter) + self._first_param_type = ex.get_first_param_type() + + if not (self.takes_self or self.takes_field): + self.__call__ = lambda value, _, __: self.converter(value) + elif self.takes_self and not self.takes_field: + self.__call__ = lambda value, instance, __: self.converter( + value, instance + ) + elif not self.takes_self and self.takes_field: + self.__call__ = lambda value, __, field: self.converter( + value, field + ) + else: + self.__call__ = self.converter + + rt = ex.get_return_type() + if rt is not None: + self.__call__.__annotations__["return"] = rt + + @staticmethod + def _get_global_name(attr_name: str) -> str: + """ + Return the name that a converter for an attribute name *attr_name* + would have. + """ + return f"__attr_converter_{attr_name}" + + def _fmt_converter_call(self, attr_name: str, value_var: str) -> str: + """ + Return a string that calls the converter for an attribute name + *attr_name* and the value in variable named *value_var* according to + `self.takes_self` and `self.takes_field`. + """ + if not (self.takes_self or self.takes_field): + return f"{self._get_global_name(attr_name)}({value_var})" + + if self.takes_self and self.takes_field: + return f"{self._get_global_name(attr_name)}({value_var}, self, attr_dict['{attr_name}'])" + + if self.takes_self: + return f"{self._get_global_name(attr_name)}({value_var}, self)" + + return f"{self._get_global_name(attr_name)}({value_var}, attr_dict['{attr_name}'])" + + def __getstate__(self): + """ + Return a dict containing only converter and takes_self -- the rest gets + computed when loading. + """ + return { + "converter": self.converter, + "takes_self": self.takes_self, + "takes_field": self.takes_field, + } + + def __setstate__(self, state): + """ + Load instance from state. + """ + self.__init__(**state) + + +_f = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + eq=True, + order=False, + hash=True, + init=True, + inherited=False, + ) + for name in ("converter", "takes_self", "takes_field") +] + +Converter = _add_hash( + _add_eq(_add_repr(Converter, attrs=_f), attrs=_f), attrs=_f +) + + +def make_class( + name, attrs, bases=(object,), class_body=None, **attributes_arguments +): + r""" + A quick way to create a new class called *name* with *attrs*. + + .. note:: + + ``make_class()`` is a thin wrapper around `attr.s`, not `attrs.define` + which means that it doesn't come with some of the improved defaults. + + For example, if you want the same ``on_setattr`` behavior as in + `attrs.define`, you have to pass the hooks yourself: ``make_class(..., + on_setattr=setters.pipe(setters.convert, setters.validate)`` + + .. warning:: + + It is *your* duty to ensure that the class name and the attribute names + are valid identifiers. ``make_class()`` will *not* validate them for + you. + + Args: + name (str): The name for the new class. + + attrs (list | dict): + A list of names or a dictionary of mappings of names to `attr.ib`\ + s / `attrs.field`\ s. + + The order is deduced from the order of the names or attributes + inside *attrs*. Otherwise the order of the definition of the + attributes is used. + + bases (tuple[type, ...]): Classes that the new class will subclass. + + class_body (dict): + An optional dictionary of class attributes for the new class. + + attributes_arguments: Passed unmodified to `attr.s`. + + Returns: + type: A new class with *attrs*. + + .. versionadded:: 17.1.0 *bases* + .. versionchanged:: 18.1.0 If *attrs* is ordered, the order is retained. + .. versionchanged:: 23.2.0 *class_body* + .. versionchanged:: 25.2.0 Class names can now be unicode. + """ + # Class identifiers are converted into the normal form NFKC while parsing + name = unicodedata.normalize("NFKC", name) + + if isinstance(attrs, dict): + cls_dict = attrs + elif isinstance(attrs, (list, tuple)): + cls_dict = {a: attrib() for a in attrs} + else: + msg = "attrs argument must be a dict or a list." + raise TypeError(msg) + + pre_init = cls_dict.pop("__attrs_pre_init__", None) + post_init = cls_dict.pop("__attrs_post_init__", None) + user_init = cls_dict.pop("__init__", None) + + body = {} + if class_body is not None: + body.update(class_body) + if pre_init is not None: + body["__attrs_pre_init__"] = pre_init + if post_init is not None: + body["__attrs_post_init__"] = post_init + if user_init is not None: + body["__init__"] = user_init + + type_ = types.new_class(name, bases, {}, lambda ns: ns.update(body)) + + # For pickling to work, the __module__ variable needs to be set to the + # frame where the class is created. Bypass this step in environments where + # sys._getframe is not defined (Jython for example) or sys._getframe is not + # defined for arguments greater than 0 (IronPython). + with contextlib.suppress(AttributeError, ValueError): + type_.__module__ = sys._getframe(1).f_globals.get( + "__name__", "__main__" + ) + + # We do it here for proper warnings with meaningful stacklevel. + cmp = attributes_arguments.pop("cmp", None) + ( + attributes_arguments["eq"], + attributes_arguments["order"], + ) = _determine_attrs_eq_order( + cmp, + attributes_arguments.get("eq"), + attributes_arguments.get("order"), + True, + ) + + cls = _attrs(these=cls_dict, **attributes_arguments)(type_) + # Only add type annotations now or "_attrs()" will complain: + cls.__annotations__ = { + k: v.type for k, v in cls_dict.items() if v.type is not None + } + return cls + + +# These are required by within this module so we define them here and merely +# import into .validators / .converters. + + +@attrs(slots=True, unsafe_hash=True) +class _AndValidator: + """ + Compose many validators to a single one. + """ + + _validators = attrib() + + def __call__(self, inst, attr, value): + for v in self._validators: + v(inst, attr, value) + + +def and_(*validators): + """ + A validator that composes multiple validators into one. + + When called on a value, it runs all wrapped validators. + + Args: + validators (~collections.abc.Iterable[typing.Callable]): + Arbitrary number of validators. + + .. versionadded:: 17.1.0 + """ + vals = [] + for validator in validators: + vals.extend( + validator._validators + if isinstance(validator, _AndValidator) + else [validator] + ) + + return _AndValidator(tuple(vals)) + + +def pipe(*converters): + """ + A converter that composes multiple converters into one. + + When called on a value, it runs all wrapped converters, returning the + *last* value. + + Type annotations will be inferred from the wrapped converters', if they + have any. + + converters (~collections.abc.Iterable[typing.Callable]): + Arbitrary number of converters. + + .. versionadded:: 20.1.0 + """ + + return_instance = any(isinstance(c, Converter) for c in converters) + + if return_instance: + + def pipe_converter(val, inst, field): + for c in converters: + val = ( + c(val, inst, field) if isinstance(c, Converter) else c(val) + ) + + return val + + else: + + def pipe_converter(val): + for c in converters: + val = c(val) + + return val + + if not converters: + # If the converter list is empty, pipe_converter is the identity. + A = TypeVar("A") + pipe_converter.__annotations__.update({"val": A, "return": A}) + else: + # Get parameter type from first converter. + t = _AnnotationExtractor(converters[0]).get_first_param_type() + if t: + pipe_converter.__annotations__["val"] = t + + last = converters[-1] + if not PY_3_11_PLUS and isinstance(last, Converter): + last = last.__call__ + + # Get return type from last converter. + rt = _AnnotationExtractor(last).get_return_type() + if rt: + pipe_converter.__annotations__["return"] = rt + + if return_instance: + return Converter(pipe_converter, takes_self=True, takes_field=True) + return pipe_converter diff --git a/source/target-vendors/cp312/attr/_next_gen.py b/source/target-vendors/cp312/attr/_next_gen.py new file mode 100644 index 0000000..4ccd0da --- /dev/null +++ b/source/target-vendors/cp312/attr/_next_gen.py @@ -0,0 +1,674 @@ +# SPDX-License-Identifier: MIT + +""" +These are keyword-only APIs that call `attr.s` and `attr.ib` with different +default values. +""" + +from functools import partial + +from . import setters +from ._funcs import asdict as _asdict +from ._funcs import astuple as _astuple +from ._make import ( + _DEFAULT_ON_SETATTR, + NOTHING, + _frozen_setattrs, + attrib, + attrs, +) +from .exceptions import NotAnAttrsClassError, UnannotatedAttributeError + + +def define( + maybe_cls=None, + *, + these=None, + repr=None, + unsafe_hash=None, + hash=None, + init=None, + slots=True, + frozen=False, + weakref_slot=True, + str=False, + auto_attribs=None, + kw_only=False, + cache_hash=False, + auto_exc=True, + eq=None, + order=False, + auto_detect=True, + getstate_setstate=None, + on_setattr=None, + field_transformer=None, + match_args=True, + force_kw_only=False, +): + r""" + A class decorator that adds :term:`dunder methods` according to + :term:`fields ` specified using :doc:`type annotations `, + `field()` calls, or the *these* argument. + + Since *attrs* patches or replaces an existing class, you cannot use + `object.__init_subclass__` with *attrs* classes, because it runs too early. + As a replacement, you can define ``__attrs_init_subclass__`` on your class. + It will be called by *attrs* classes that subclass it after they're + created. See also :ref:`init-subclass`. + + Args: + slots (bool): + Create a :term:`slotted class ` that's more + memory-efficient. Slotted classes are generally superior to the + default dict classes, but have some gotchas you should know about, + so we encourage you to read the :term:`glossary entry `. + + auto_detect (bool): + Instead of setting the *init*, *repr*, *eq*, and *hash* arguments + explicitly, assume they are set to True **unless any** of the + involved methods for one of the arguments is implemented in the + *current* class (meaning, it is *not* inherited from some base + class). + + So, for example by implementing ``__eq__`` on a class yourself, + *attrs* will deduce ``eq=False`` and will create *neither* + ``__eq__`` *nor* ``__ne__`` (but Python classes come with a + sensible ``__ne__`` by default, so it *should* be enough to only + implement ``__eq__`` in most cases). + + Passing :data:`True` or :data:`False` to *init*, *repr*, *eq*, or *hash* + overrides whatever *auto_detect* would determine. + + auto_exc (bool): + If the class subclasses `BaseException` (which implicitly includes + any subclass of any exception), the following happens to behave + like a well-behaved Python exception class: + + - the values for *eq*, *order*, and *hash* are ignored and the + instances compare and hash by the instance's ids [#]_ , + - all attributes that are either passed into ``__init__`` or have a + default value are additionally available as a tuple in the + ``args`` attribute, + - the value of *str* is ignored leaving ``__str__`` to base + classes. + + .. [#] + Note that *attrs* will *not* remove existing implementations of + ``__hash__`` or the equality methods. It just won't add own + ones. + + on_setattr (~typing.Callable | list[~typing.Callable] | None | ~typing.Literal[attrs.setters.NO_OP]): + A callable that is run whenever the user attempts to set an + attribute (either by assignment like ``i.x = 42`` or by using + `setattr` like ``setattr(i, "x", 42)``). It receives the same + arguments as validators: the instance, the attribute that is being + modified, and the new value. + + If no exception is raised, the attribute is set to the return value + of the callable. + + If a list of callables is passed, they're automatically wrapped in + an `attrs.setters.pipe`. + + If left None, the default behavior is to run converters and + validators whenever an attribute is set. + + init (bool): + Create a ``__init__`` method that initializes the *attrs* + attributes. Leading underscores are stripped for the argument name, + unless an alias is set on the attribute. + + .. seealso:: + `init` shows advanced ways to customize the generated + ``__init__`` method, including executing code before and after. + + repr(bool): + Create a ``__repr__`` method with a human readable representation + of *attrs* attributes. + + str (bool): + Create a ``__str__`` method that is identical to ``__repr__``. This + is usually not necessary except for `Exception`\ s. + + eq (bool | None): + If True or None (default), add ``__eq__`` and ``__ne__`` methods + that check two instances for equality. + + .. seealso:: + `comparison` describes how to customize the comparison behavior + going as far comparing NumPy arrays. + + order (bool | None): + If True, add ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` + methods that behave like *eq* above and allow instances to be + ordered. + + They compare the instances as if they were tuples of their *attrs* + attributes if and only if the types of both classes are + *identical*. + + If `None` mirror value of *eq*. + + .. seealso:: `comparison` + + unsafe_hash (bool | None): + If None (default), the ``__hash__`` method is generated according + how *eq* and *frozen* are set. + + 1. If *both* are True, *attrs* will generate a ``__hash__`` for + you. + 2. If *eq* is True and *frozen* is False, ``__hash__`` will be set + to None, marking it unhashable (which it is). + 3. If *eq* is False, ``__hash__`` will be left untouched meaning + the ``__hash__`` method of the base class will be used. If the + base class is `object`, this means it will fall back to id-based + hashing. + + Although not recommended, you can decide for yourself and force + *attrs* to create one (for example, if the class is immutable even + though you didn't freeze it programmatically) by passing True or + not. Both of these cases are rather special and should be used + carefully. + + .. seealso:: + + - Our documentation on `hashing`, + - Python's documentation on `object.__hash__`, + - and the `GitHub issue that led to the default \ behavior + `_ for more + details. + + hash (bool | None): + Deprecated alias for *unsafe_hash*. *unsafe_hash* takes precedence. + + cache_hash (bool): + Ensure that the object's hash code is computed only once and stored + on the object. If this is set to True, hashing must be either + explicitly or implicitly enabled for this class. If the hash code + is cached, avoid any reassignments of fields involved in hash code + computation or mutations of the objects those fields point to after + object creation. If such changes occur, the behavior of the + object's hash code is undefined. + + frozen (bool): + Make instances immutable after initialization. If someone attempts + to modify a frozen instance, `attrs.exceptions.FrozenInstanceError` + is raised. + + .. note:: + + 1. This is achieved by installing a custom ``__setattr__`` + method on your class, so you can't implement your own. + + 2. True immutability is impossible in Python. + + 3. This *does* have a minor a runtime performance `impact + ` when initializing new instances. In other + words: ``__init__`` is slightly slower with ``frozen=True``. + + 4. If a class is frozen, you cannot modify ``self`` in + ``__attrs_post_init__`` or a self-written ``__init__``. You + can circumvent that limitation by using + ``object.__setattr__(self, "attribute_name", value)``. + + 5. Subclasses of a frozen class are frozen too. + + kw_only (bool): + Make attributes keyword-only in the generated ``__init__`` (if + *init* is False, this parameter is ignored). Attributes that + explicitly set ``kw_only=False`` are not affected; base class + attributes are also not affected. + + Also see *force_kw_only*. + + weakref_slot (bool): + Make instances weak-referenceable. This has no effect unless + *slots* is True. + + field_transformer (~typing.Callable | None): + A function that is called with the original class object and all + fields right before *attrs* finalizes the class. You can use this, + for example, to automatically add converters or validators to + fields based on their types. + + .. seealso:: `transform-fields` + + match_args (bool): + If True (default), set ``__match_args__`` on the class to support + :pep:`634` (*Structural Pattern Matching*). It is a tuple of all + non-keyword-only ``__init__`` parameter names on Python 3.10 and + later. Ignored on older Python versions. + + collect_by_mro (bool): + If True, *attrs* collects attributes from base classes correctly + according to the `method resolution order + `_. If False, *attrs* + will mimic the (wrong) behavior of `dataclasses` and :pep:`681`. + + See also `issue #428 + `_. + + force_kw_only (bool): + A back-compat flag for restoring pre-25.4.0 behavior. If True and + ``kw_only=True``, all attributes are made keyword-only, including + base class attributes, and those set to ``kw_only=False`` at the + attribute level. Defaults to False. + + See also `issue #980 + `_. + + getstate_setstate (bool | None): + .. note:: + + This is usually only interesting for slotted classes and you + should probably just set *auto_detect* to True. + + If True, ``__getstate__`` and ``__setstate__`` are generated and + attached to the class. This is necessary for slotted classes to be + pickleable. If left None, it's True by default for slotted classes + and False for dict classes. + + If *auto_detect* is True, and *getstate_setstate* is left None, and + **either** ``__getstate__`` or ``__setstate__`` is detected + directly on the class (meaning: not inherited), it is set to False + (this is usually what you want). + + auto_attribs (bool | None): + If True, look at type annotations to determine which attributes to + use, like `dataclasses`. If False, it will only look for explicit + :func:`field` class attributes, like classic *attrs*. + + If left None, it will guess: + + 1. If any attributes are annotated and no unannotated + `attrs.field`\ s are found, it assumes *auto_attribs=True*. + 2. Otherwise it assumes *auto_attribs=False* and tries to collect + `attrs.field`\ s. + + If *attrs* decides to look at type annotations, **all** fields + **must** be annotated. If *attrs* encounters a field that is set to + a :func:`field` / `attr.ib` but lacks a type annotation, an + `attrs.exceptions.UnannotatedAttributeError` is raised. Use + ``field_name: typing.Any = field(...)`` if you don't want to set a + type. + + .. warning:: + + For features that use the attribute name to create decorators + (for example, :ref:`validators `), you still *must* + assign :func:`field` / `attr.ib` to them. Otherwise Python will + either not find the name or try to use the default value to + call, for example, ``validator`` on it. + + Attributes annotated as `typing.ClassVar`, and attributes that are + neither annotated nor set to an `field()` are **ignored**. + + these (dict[str, object]): + A dictionary of name to the (private) return value of `field()` + mappings. This is useful to avoid the definition of your attributes + within the class body because you can't (for example, if you want + to add ``__repr__`` methods to Django models) or don't want to. + + If *these* is not `None`, *attrs* will *not* search the class body + for attributes and will *not* remove any attributes from it. + + The order is deduced from the order of the attributes inside + *these*. + + Arguably, this is a rather obscure feature. + + .. versionadded:: 20.1.0 + .. versionchanged:: 21.3.0 Converters are also run ``on_setattr``. + .. versionadded:: 22.2.0 + *unsafe_hash* as an alias for *hash* (for :pep:`681` compliance). + .. versionchanged:: 24.1.0 + Instances are not compared as tuples of attributes anymore, but using a + big ``and`` condition. This is faster and has more correct behavior for + uncomparable values like `math.nan`. + .. versionadded:: 24.1.0 + If a class has an *inherited* classmethod called + ``__attrs_init_subclass__``, it is executed after the class is created. + .. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*. + .. versionadded:: 24.3.0 + Unless already present, a ``__replace__`` method is automatically + created for `copy.replace` (Python 3.13+ only). + .. versionchanged:: 25.4.0 + *kw_only* now only applies to attributes defined in the current class, + and respects attribute-level ``kw_only=False`` settings. + .. versionadded:: 25.4.0 + Added *force_kw_only* to go back to the previous *kw_only* behavior. + + .. note:: + + The main differences to the classic `attr.s` are: + + - Automatically detect whether or not *auto_attribs* should be `True` + (c.f. *auto_attribs* parameter). + - Converters and validators run when attributes are set by default -- + if *frozen* is `False`. + - *slots=True* + + Usually, this has only upsides and few visible effects in everyday + programming. But it *can* lead to some surprising behaviors, so + please make sure to read :term:`slotted classes`. + + - *auto_exc=True* + - *auto_detect=True* + - *order=False* + - *force_kw_only=False* + - Some options that were only relevant on Python 2 or were kept around + for backwards-compatibility have been removed. + + """ + + def do_it(cls, auto_attribs): + return attrs( + maybe_cls=cls, + these=these, + repr=repr, + hash=hash, + unsafe_hash=unsafe_hash, + init=init, + slots=slots, + frozen=frozen, + weakref_slot=weakref_slot, + str=str, + auto_attribs=auto_attribs, + kw_only=kw_only, + cache_hash=cache_hash, + auto_exc=auto_exc, + eq=eq, + order=order, + auto_detect=auto_detect, + collect_by_mro=True, + getstate_setstate=getstate_setstate, + on_setattr=on_setattr, + field_transformer=field_transformer, + match_args=match_args, + force_kw_only=force_kw_only, + ) + + def wrap(cls): + """ + Making this a wrapper ensures this code runs during class creation. + + We also ensure that frozen-ness of classes is inherited. + """ + nonlocal frozen, on_setattr + + had_on_setattr = on_setattr not in (None, setters.NO_OP) + + # By default, mutable classes convert & validate on setattr. + if frozen is False and on_setattr is None: + on_setattr = _DEFAULT_ON_SETATTR + + # However, if we subclass a frozen class, we inherit the immutability + # and disable on_setattr. + for base_cls in cls.__bases__: + if base_cls.__setattr__ is _frozen_setattrs: + if had_on_setattr: + msg = "Frozen classes can't use on_setattr (frozen-ness was inherited)." + raise ValueError(msg) + + on_setattr = setters.NO_OP + break + + if auto_attribs is not None: + return do_it(cls, auto_attribs) + + try: + return do_it(cls, True) + except UnannotatedAttributeError: + return do_it(cls, False) + + # maybe_cls's type depends on the usage of the decorator. It's a class + # if it's used as `@attrs` but `None` if used as `@attrs()`. + if maybe_cls is None: + return wrap + + return wrap(maybe_cls) + + +mutable = define +frozen = partial(define, frozen=True, on_setattr=None) + + +def field( + *, + default=NOTHING, + validator=None, + repr=True, + hash=None, + init=True, + metadata=None, + type=None, + converter=None, + factory=None, + kw_only=None, + eq=None, + order=None, + on_setattr=None, + alias=None, +): + """ + Create a new :term:`field` / :term:`attribute` on a class. + + .. warning:: + + Does **nothing** unless the class is also decorated with + `attrs.define` (or similar)! + + Args: + default: + A value that is used if an *attrs*-generated ``__init__`` is used + and no value is passed while instantiating or the attribute is + excluded using ``init=False``. + + If the value is an instance of `attrs.Factory`, its callable will + be used to construct a new value (useful for mutable data types + like lists or dicts). + + If a default is not set (or set manually to `attrs.NOTHING`), a + value *must* be supplied when instantiating; otherwise a + `TypeError` will be raised. + + .. seealso:: `defaults` + + factory (~typing.Callable): + Syntactic sugar for ``default=attr.Factory(factory)``. + + validator (~typing.Callable | list[~typing.Callable]): + Callable that is called by *attrs*-generated ``__init__`` methods + after the instance has been initialized. They receive the + initialized instance, the :func:`~attrs.Attribute`, and the passed + value. + + The return value is *not* inspected so the validator has to throw + an exception itself. + + If a `list` is passed, its items are treated as validators and must + all pass. + + Validators can be globally disabled and re-enabled using + `attrs.validators.get_disabled` / `attrs.validators.set_disabled`. + + The validator can also be set using decorator notation as shown + below. + + .. seealso:: :ref:`validators` + + repr (bool | ~typing.Callable): + Include this attribute in the generated ``__repr__`` method. If + True, include the attribute; if False, omit it. By default, the + built-in ``repr()`` function is used. To override how the attribute + value is formatted, pass a ``callable`` that takes a single value + and returns a string. Note that the resulting string is used as-is, + which means it will be used directly *instead* of calling + ``repr()`` (the default). + + eq (bool | ~typing.Callable): + If True (default), include this attribute in the generated + ``__eq__`` and ``__ne__`` methods that check two instances for + equality. To override how the attribute value is compared, pass a + callable that takes a single value and returns the value to be + compared. + + .. seealso:: `comparison` + + order (bool | ~typing.Callable): + If True (default), include this attributes in the generated + ``__lt__``, ``__le__``, ``__gt__`` and ``__ge__`` methods. To + override how the attribute value is ordered, pass a callable that + takes a single value and returns the value to be ordered. + + .. seealso:: `comparison` + + hash (bool | None): + Include this attribute in the generated ``__hash__`` method. If + None (default), mirror *eq*'s value. This is the correct behavior + according the Python spec. Setting this value to anything else + than None is *discouraged*. + + .. seealso:: `hashing` + + init (bool): + Include this attribute in the generated ``__init__`` method. + + It is possible to set this to False and set a default value. In + that case this attributed is unconditionally initialized with the + specified default value or factory. + + .. seealso:: `init` + + converter (typing.Callable | Converter): + A callable that is called by *attrs*-generated ``__init__`` methods + to convert attribute's value to the desired format. + + If a vanilla callable is passed, it is given the passed-in value as + the only positional argument. It is possible to receive additional + arguments by wrapping the callable in a `Converter`. + + Either way, the returned value will be used as the new value of the + attribute. The value is converted before being passed to the + validator, if any. + + .. seealso:: :ref:`converters` + + metadata (dict | None): + An arbitrary mapping, to be used by third-party code. + + .. seealso:: `extending-metadata`. + + type (type): + The type of the attribute. Nowadays, the preferred method to + specify the type is using a variable annotation (see :pep:`526`). + This argument is provided for backwards-compatibility and for usage + with `make_class`. Regardless of the approach used, the type will + be stored on ``Attribute.type``. + + Please note that *attrs* doesn't do anything with this metadata by + itself. You can use it as part of your own code or for `static type + checking `. + + kw_only (bool | None): + Make this attribute keyword-only in the generated ``__init__`` (if + *init* is False, this parameter is ignored). If None (default), + mirror the setting from `attrs.define`. + + on_setattr (~typing.Callable | list[~typing.Callable] | None | ~typing.Literal[attrs.setters.NO_OP]): + Allows to overwrite the *on_setattr* setting from `attr.s`. If left + None, the *on_setattr* value from `attr.s` is used. Set to + `attrs.setters.NO_OP` to run **no** `setattr` hooks for this + attribute -- regardless of the setting in `define()`. + + alias (str | None): + Override this attribute's parameter name in the generated + ``__init__`` method. If left None, default to ``name`` stripped + of leading underscores. See `private-attributes`. + + .. versionadded:: 20.1.0 + .. versionchanged:: 21.1.0 + *eq*, *order*, and *cmp* also accept a custom callable + .. versionadded:: 22.2.0 *alias* + .. versionadded:: 23.1.0 + The *type* parameter has been re-added; mostly for `attrs.make_class`. + Please note that type checkers ignore this metadata. + .. versionchanged:: 25.4.0 + *kw_only* can now be None, and its default is also changed from False to + None. + + .. seealso:: + + `attr.ib` + """ + return attrib( + default=default, + validator=validator, + repr=repr, + hash=hash, + init=init, + metadata=metadata, + type=type, + converter=converter, + factory=factory, + kw_only=kw_only, + eq=eq, + order=order, + on_setattr=on_setattr, + alias=alias, + ) + + +def asdict(inst, *, recurse=True, filter=None, value_serializer=None): + """ + Same as `attr.asdict`, except that collections types are always retained + and dict is always used as *dict_factory*. + + .. versionadded:: 21.3.0 + """ + return _asdict( + inst=inst, + recurse=recurse, + filter=filter, + value_serializer=value_serializer, + retain_collection_types=True, + ) + + +def astuple(inst, *, recurse=True, filter=None): + """ + Same as `attr.astuple`, except that collections types are always retained + and `tuple` is always used as the *tuple_factory*. + + .. versionadded:: 21.3.0 + """ + return _astuple( + inst=inst, recurse=recurse, filter=filter, retain_collection_types=True + ) + + +def inspect(cls): + """ + Inspect the class and return its effective build parameters. + + Warning: + This feature is currently **experimental** and is not covered by our + strict backwards-compatibility guarantees. + + Args: + cls: The *attrs*-decorated class to inspect. + + Returns: + The effective build parameters of the class. + + Raises: + NotAnAttrsClassError: If the class is not an *attrs*-decorated class. + + .. versionadded:: 25.4.0 + """ + try: + return cls.__dict__["__attrs_props__"] + except KeyError: + msg = f"{cls!r} is not an attrs-decorated class." + raise NotAnAttrsClassError(msg) from None diff --git a/source/target-vendors/cp312/attr/_typing_compat.pyi b/source/target-vendors/cp312/attr/_typing_compat.pyi new file mode 100644 index 0000000..ca7b71e --- /dev/null +++ b/source/target-vendors/cp312/attr/_typing_compat.pyi @@ -0,0 +1,15 @@ +from typing import Any, ClassVar, Protocol + +# MYPY is a special constant in mypy which works the same way as `TYPE_CHECKING`. +MYPY = False + +if MYPY: + # A protocol to be able to statically accept an attrs class. + class AttrsInstance_(Protocol): + __attrs_attrs__: ClassVar[Any] + +else: + # For type checkers without plug-in support use an empty protocol that + # will (hopefully) be combined into a union. + class AttrsInstance_(Protocol): + pass diff --git a/source/target-vendors/cp312/attr/_version_info.py b/source/target-vendors/cp312/attr/_version_info.py new file mode 100644 index 0000000..27f1888 --- /dev/null +++ b/source/target-vendors/cp312/attr/_version_info.py @@ -0,0 +1,89 @@ +# SPDX-License-Identifier: MIT + + +from functools import total_ordering + +from ._funcs import astuple +from ._make import attrib, attrs + + +@total_ordering +@attrs(eq=False, order=False, slots=True, frozen=True) +class VersionInfo: + """ + A version object that can be compared to tuple of length 1--4: + + >>> attr.VersionInfo(19, 1, 0, "final") <= (19, 2) + True + >>> attr.VersionInfo(19, 1, 0, "final") < (19, 1, 1) + True + >>> vi = attr.VersionInfo(19, 2, 0, "final") + >>> vi < (19, 1, 1) + False + >>> vi < (19,) + False + >>> vi == (19, 2,) + True + >>> vi == (19, 2, 1) + False + + .. versionadded:: 19.2 + """ + + year = attrib(type=int) + minor = attrib(type=int) + micro = attrib(type=int) + releaselevel = attrib(type=str) + + @classmethod + def _from_version_string(cls, s): + """ + Parse *s* and return a _VersionInfo. + """ + v = s.split(".") + if len(v) == 3: + v.append("final") + + return cls( + year=int(v[0]), minor=int(v[1]), micro=int(v[2]), releaselevel=v[3] + ) + + def _ensure_tuple(self, other): + """ + Ensure *other* is a tuple of a valid length. + + Returns a possibly transformed *other* and ourselves as a tuple of + the same length as *other*. + """ + + if self.__class__ is other.__class__: + other = astuple(other) + + if not isinstance(other, tuple): + raise NotImplementedError + + if not (1 <= len(other) <= 4): + raise NotImplementedError + + return astuple(self)[: len(other)], other + + def __eq__(self, other): + try: + us, them = self._ensure_tuple(other) + except NotImplementedError: + return NotImplemented + + return us == them + + def __lt__(self, other): + try: + us, them = self._ensure_tuple(other) + except NotImplementedError: + return NotImplemented + + # Since alphabetically "dev0" < "final" < "post1" < "post2", we don't + # have to do anything special with releaselevel for now. + return us < them + + def __hash__(self): + return hash((self.year, self.minor, self.micro, self.releaselevel)) diff --git a/source/target-vendors/cp312/attr/_version_info.pyi b/source/target-vendors/cp312/attr/_version_info.pyi new file mode 100644 index 0000000..45ced08 --- /dev/null +++ b/source/target-vendors/cp312/attr/_version_info.pyi @@ -0,0 +1,9 @@ +class VersionInfo: + @property + def year(self) -> int: ... + @property + def minor(self) -> int: ... + @property + def micro(self) -> int: ... + @property + def releaselevel(self) -> str: ... diff --git a/source/target-vendors/cp312/attr/converters.py b/source/target-vendors/cp312/attr/converters.py new file mode 100644 index 0000000..0a79dee --- /dev/null +++ b/source/target-vendors/cp312/attr/converters.py @@ -0,0 +1,162 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly useful converters. +""" + +import typing + +from ._compat import _AnnotationExtractor +from ._make import NOTHING, Converter, Factory, pipe + + +__all__ = [ + "default_if_none", + "optional", + "pipe", + "to_bool", +] + + +def optional(converter): + """ + A converter that allows an attribute to be optional. An optional attribute + is one which can be set to `None`. + + Type annotations will be inferred from the wrapped converter's, if it has + any. + + Args: + converter (typing.Callable): + the converter that is used for non-`None` values. + + .. versionadded:: 17.1.0 + """ + + if isinstance(converter, Converter): + + def optional_converter(val, inst, field): + if val is None: + return None + return converter(val, inst, field) + + else: + + def optional_converter(val): + if val is None: + return None + return converter(val) + + xtr = _AnnotationExtractor(converter) + + t = xtr.get_first_param_type() + if t: + optional_converter.__annotations__["val"] = typing.Optional[t] + + rt = xtr.get_return_type() + if rt: + optional_converter.__annotations__["return"] = typing.Optional[rt] + + if isinstance(converter, Converter): + return Converter(optional_converter, takes_self=True, takes_field=True) + + return optional_converter + + +def default_if_none(default=NOTHING, factory=None): + """ + A converter that allows to replace `None` values by *default* or the result + of *factory*. + + Args: + default: + Value to be used if `None` is passed. Passing an instance of + `attrs.Factory` is supported, however the ``takes_self`` option is + *not*. + + factory (typing.Callable): + A callable that takes no parameters whose result is used if `None` + is passed. + + Raises: + TypeError: If **neither** *default* or *factory* is passed. + + TypeError: If **both** *default* and *factory* are passed. + + ValueError: + If an instance of `attrs.Factory` is passed with + ``takes_self=True``. + + .. versionadded:: 18.2.0 + """ + if default is NOTHING and factory is None: + msg = "Must pass either `default` or `factory`." + raise TypeError(msg) + + if default is not NOTHING and factory is not None: + msg = "Must pass either `default` or `factory` but not both." + raise TypeError(msg) + + if factory is not None: + default = Factory(factory) + + if isinstance(default, Factory): + if default.takes_self: + msg = "`takes_self` is not supported by default_if_none." + raise ValueError(msg) + + def default_if_none_converter(val): + if val is not None: + return val + + return default.factory() + + else: + + def default_if_none_converter(val): + if val is not None: + return val + + return default + + return default_if_none_converter + + +def to_bool(val): + """ + Convert "boolean" strings (for example, from environment variables) to real + booleans. + + Values mapping to `True`: + + - ``True`` + - ``"true"`` / ``"t"`` + - ``"yes"`` / ``"y"`` + - ``"on"`` + - ``"1"`` + - ``1`` + + Values mapping to `False`: + + - ``False`` + - ``"false"`` / ``"f"`` + - ``"no"`` / ``"n"`` + - ``"off"`` + - ``"0"`` + - ``0`` + + Raises: + ValueError: For any other value. + + .. versionadded:: 21.3.0 + """ + if isinstance(val, str): + val = val.lower() + + if val in (True, "true", "t", "yes", "y", "on", "1", 1): + return True + if val in (False, "false", "f", "no", "n", "off", "0", 0): + return False + + msg = f"Cannot convert value to bool: {val!r}" + raise ValueError(msg) diff --git a/source/target-vendors/cp312/attr/converters.pyi b/source/target-vendors/cp312/attr/converters.pyi new file mode 100644 index 0000000..12bd0c4 --- /dev/null +++ b/source/target-vendors/cp312/attr/converters.pyi @@ -0,0 +1,19 @@ +from typing import Callable, Any, overload + +from attrs import _ConverterType, _CallableConverterType + +@overload +def pipe(*validators: _CallableConverterType) -> _CallableConverterType: ... +@overload +def pipe(*validators: _ConverterType) -> _ConverterType: ... +@overload +def optional(converter: _CallableConverterType) -> _CallableConverterType: ... +@overload +def optional(converter: _ConverterType) -> _ConverterType: ... +@overload +def default_if_none(default: Any) -> _CallableConverterType: ... +@overload +def default_if_none( + *, factory: Callable[[], Any] +) -> _CallableConverterType: ... +def to_bool(val: str | int | bool) -> bool: ... diff --git a/source/target-vendors/cp312/attr/exceptions.py b/source/target-vendors/cp312/attr/exceptions.py new file mode 100644 index 0000000..a207df4 --- /dev/null +++ b/source/target-vendors/cp312/attr/exceptions.py @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + + +class FrozenError(AttributeError): + """ + A frozen/immutable instance or attribute have been attempted to be + modified. + + It mirrors the behavior of ``namedtuples`` by using the same error message + and subclassing `AttributeError`. + + .. versionadded:: 20.1.0 + """ + + def __init__(self): + msg = "can't set attribute" + super().__init__(msg) + self.msg = msg + + +class FrozenInstanceError(FrozenError): + """ + A frozen instance has been attempted to be modified. + + .. versionadded:: 16.1.0 + """ + + +class FrozenAttributeError(FrozenError): + """ + A frozen attribute has been attempted to be modified. + + .. versionadded:: 20.1.0 + """ + + +class AttrsAttributeNotFoundError(ValueError): + """ + An *attrs* function couldn't find an attribute that the user asked for. + + .. versionadded:: 16.2.0 + """ + + +class NotAnAttrsClassError(ValueError): + """ + A non-*attrs* class has been passed into an *attrs* function. + + .. versionadded:: 16.2.0 + """ + + +class DefaultAlreadySetError(RuntimeError): + """ + A default has been set when defining the field and is attempted to be reset + using the decorator. + + .. versionadded:: 17.1.0 + """ + + +class UnannotatedAttributeError(RuntimeError): + """ + A class with ``auto_attribs=True`` has a field without a type annotation. + + .. versionadded:: 17.3.0 + """ + + +class PythonTooOldError(RuntimeError): + """ + It was attempted to use an *attrs* feature that requires a newer Python + version. + + .. versionadded:: 18.2.0 + """ + + +class NotCallableError(TypeError): + """ + A field requiring a callable has been set with a value that is not + callable. + + .. versionadded:: 19.2.0 + """ + + def __init__(self, msg, value): + super(TypeError, self).__init__(msg, value) + self.msg = msg + self.value = value + + def __str__(self): + return str(self.msg) diff --git a/source/target-vendors/cp312/attr/exceptions.pyi b/source/target-vendors/cp312/attr/exceptions.pyi new file mode 100644 index 0000000..f268011 --- /dev/null +++ b/source/target-vendors/cp312/attr/exceptions.pyi @@ -0,0 +1,17 @@ +from typing import Any + +class FrozenError(AttributeError): + msg: str = ... + +class FrozenInstanceError(FrozenError): ... +class FrozenAttributeError(FrozenError): ... +class AttrsAttributeNotFoundError(ValueError): ... +class NotAnAttrsClassError(ValueError): ... +class DefaultAlreadySetError(RuntimeError): ... +class UnannotatedAttributeError(RuntimeError): ... +class PythonTooOldError(RuntimeError): ... + +class NotCallableError(TypeError): + msg: str = ... + value: Any = ... + def __init__(self, msg: str, value: Any) -> None: ... diff --git a/source/target-vendors/cp312/attr/filters.py b/source/target-vendors/cp312/attr/filters.py new file mode 100644 index 0000000..689b170 --- /dev/null +++ b/source/target-vendors/cp312/attr/filters.py @@ -0,0 +1,72 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly useful filters for `attrs.asdict` and `attrs.astuple`. +""" + +from ._make import Attribute + + +def _split_what(what): + """ + Returns a tuple of `frozenset`s of classes and attributes. + """ + return ( + frozenset(cls for cls in what if isinstance(cls, type)), + frozenset(cls for cls in what if isinstance(cls, str)), + frozenset(cls for cls in what if isinstance(cls, Attribute)), + ) + + +def include(*what): + """ + Create a filter that only allows *what*. + + Args: + what (list[type, str, attrs.Attribute]): + What to include. Can be a type, a name, or an attribute. + + Returns: + Callable: + A callable that can be passed to `attrs.asdict`'s and + `attrs.astuple`'s *filter* argument. + + .. versionchanged:: 23.1.0 Accept strings with field names. + """ + cls, names, attrs = _split_what(what) + + def include_(attribute, value): + return ( + value.__class__ in cls + or attribute.name in names + or attribute in attrs + ) + + return include_ + + +def exclude(*what): + """ + Create a filter that does **not** allow *what*. + + Args: + what (list[type, str, attrs.Attribute]): + What to exclude. Can be a type, a name, or an attribute. + + Returns: + Callable: + A callable that can be passed to `attrs.asdict`'s and + `attrs.astuple`'s *filter* argument. + + .. versionchanged:: 23.3.0 Accept field name string as input argument + """ + cls, names, attrs = _split_what(what) + + def exclude_(attribute, value): + return not ( + value.__class__ in cls + or attribute.name in names + or attribute in attrs + ) + + return exclude_ diff --git a/source/target-vendors/cp312/attr/filters.pyi b/source/target-vendors/cp312/attr/filters.pyi new file mode 100644 index 0000000..974abdc --- /dev/null +++ b/source/target-vendors/cp312/attr/filters.pyi @@ -0,0 +1,6 @@ +from typing import Any + +from . import Attribute, _FilterType + +def include(*what: type | str | Attribute[Any]) -> _FilterType[Any]: ... +def exclude(*what: type | str | Attribute[Any]) -> _FilterType[Any]: ... diff --git a/source/target-vendors/cp312/attr/py.typed b/source/target-vendors/cp312/attr/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/attr/setters.py b/source/target-vendors/cp312/attr/setters.py new file mode 100644 index 0000000..78b0839 --- /dev/null +++ b/source/target-vendors/cp312/attr/setters.py @@ -0,0 +1,79 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly used hooks for on_setattr. +""" + +from . import _config +from .exceptions import FrozenAttributeError + + +def pipe(*setters): + """ + Run all *setters* and return the return value of the last one. + + .. versionadded:: 20.1.0 + """ + + def wrapped_pipe(instance, attrib, new_value): + rv = new_value + + for setter in setters: + rv = setter(instance, attrib, rv) + + return rv + + return wrapped_pipe + + +def frozen(_, __, ___): + """ + Prevent an attribute to be modified. + + .. versionadded:: 20.1.0 + """ + raise FrozenAttributeError + + +def validate(instance, attrib, new_value): + """ + Run *attrib*'s validator on *new_value* if it has one. + + .. versionadded:: 20.1.0 + """ + if _config._run_validators is False: + return new_value + + v = attrib.validator + if not v: + return new_value + + v(instance, attrib, new_value) + + return new_value + + +def convert(instance, attrib, new_value): + """ + Run *attrib*'s converter -- if it has one -- on *new_value* and return the + result. + + .. versionadded:: 20.1.0 + """ + c = attrib.converter + if c: + # This can be removed once we drop 3.8 and use attrs.Converter instead. + from ._make import Converter + + if not isinstance(c, Converter): + return c(new_value) + + return c(new_value, instance, attrib) + + return new_value + + +# Sentinel for disabling class-wide *on_setattr* hooks for certain attributes. +# Sphinx's autodata stopped working, so the docstring is inlined in the API +# docs. +NO_OP = object() diff --git a/source/target-vendors/cp312/attr/setters.pyi b/source/target-vendors/cp312/attr/setters.pyi new file mode 100644 index 0000000..73abf36 --- /dev/null +++ b/source/target-vendors/cp312/attr/setters.pyi @@ -0,0 +1,20 @@ +from typing import Any, NewType, NoReturn, TypeVar + +from . import Attribute +from attrs import _OnSetAttrType + +_T = TypeVar("_T") + +def frozen( + instance: Any, attribute: Attribute[Any], new_value: Any +) -> NoReturn: ... +def pipe(*setters: _OnSetAttrType) -> _OnSetAttrType: ... +def validate(instance: Any, attribute: Attribute[_T], new_value: _T) -> _T: ... + +# convert is allowed to return Any, because they can be chained using pipe. +def convert( + instance: Any, attribute: Attribute[Any], new_value: Any +) -> Any: ... + +_NoOpType = NewType("_NoOpType", object) +NO_OP: _NoOpType diff --git a/source/target-vendors/cp312/attr/validators.py b/source/target-vendors/cp312/attr/validators.py new file mode 100644 index 0000000..0b1a294 --- /dev/null +++ b/source/target-vendors/cp312/attr/validators.py @@ -0,0 +1,750 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly useful validators. +""" + +import operator +import re + +from contextlib import contextmanager +from re import Pattern + +from ._config import get_run_validators, set_run_validators +from ._make import _AndValidator, and_, attrib, attrs +from .converters import default_if_none +from .exceptions import NotCallableError + + +__all__ = [ + "and_", + "deep_iterable", + "deep_mapping", + "disabled", + "ge", + "get_disabled", + "gt", + "in_", + "instance_of", + "is_callable", + "le", + "lt", + "matches_re", + "max_len", + "min_len", + "not_", + "optional", + "or_", + "set_disabled", +] + + +def set_disabled(disabled): + """ + Globally disable or enable running validators. + + By default, they are run. + + Args: + disabled (bool): If `True`, disable running all validators. + + .. warning:: + + This function is not thread-safe! + + .. versionadded:: 21.3.0 + """ + set_run_validators(not disabled) + + +def get_disabled(): + """ + Return a bool indicating whether validators are currently disabled or not. + + Returns: + bool:`True` if validators are currently disabled. + + .. versionadded:: 21.3.0 + """ + return not get_run_validators() + + +@contextmanager +def disabled(): + """ + Context manager that disables running validators within its context. + + .. warning:: + + This context manager is not thread-safe! + + .. versionadded:: 21.3.0 + .. versionchanged:: 26.1.0 The contextmanager is nestable. + """ + prev = get_run_validators() + set_run_validators(False) + try: + yield + finally: + set_run_validators(prev) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _InstanceOfValidator: + type = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not isinstance(value, self.type): + msg = f"'{attr.name}' must be {self.type!r} (got {value!r} that is a {value.__class__!r})." + raise TypeError( + msg, + attr, + self.type, + value, + ) + + def __repr__(self): + return f"" + + +def instance_of(type): + """ + A validator that raises a `TypeError` if the initializer is called with a + wrong type for this particular attribute (checks are performed using + `isinstance` therefore it's also valid to pass a tuple of types). + + Args: + type (type | tuple[type]): The type to check for. + + Raises: + TypeError: + With a human readable error message, the attribute (of type + `attrs.Attribute`), the expected type, and the value it got. + """ + return _InstanceOfValidator(type) + + +@attrs(repr=False, frozen=True, slots=True) +class _MatchesReValidator: + pattern = attrib() + match_func = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not self.match_func(value): + msg = f"'{attr.name}' must match regex {self.pattern.pattern!r} ({value!r} doesn't)" + raise ValueError( + msg, + attr, + self.pattern, + value, + ) + + def __repr__(self): + return f"" + + +def matches_re(regex, flags=0, func=None): + r""" + A validator that raises `ValueError` if the initializer is called with a + string that doesn't match *regex*. + + Args: + regex (str, re.Pattern): + A regex string or precompiled pattern to match against + + flags (int): + Flags that will be passed to the underlying re function (default 0) + + func (typing.Callable): + Which underlying `re` function to call. Valid options are + `re.fullmatch`, `re.search`, and `re.match`; the default `None` + means `re.fullmatch`. For performance reasons, the pattern is + always precompiled using `re.compile`. + + .. versionadded:: 19.2.0 + .. versionchanged:: 21.3.0 *regex* can be a pre-compiled pattern. + """ + valid_funcs = (re.fullmatch, None, re.search, re.match) + if func not in valid_funcs: + msg = "'func' must be one of {}.".format( + ", ".join( + sorted((e and e.__name__) or "None" for e in set(valid_funcs)) + ) + ) + raise ValueError(msg) + + if isinstance(regex, Pattern): + if flags: + msg = "'flags' can only be used with a string pattern; pass flags to re.compile() instead" + raise TypeError(msg) + pattern = regex + else: + pattern = re.compile(regex, flags) + + if func is re.match: + match_func = pattern.match + elif func is re.search: + match_func = pattern.search + else: + match_func = pattern.fullmatch + + return _MatchesReValidator(pattern, match_func) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _OptionalValidator: + validator = attrib() + + def __call__(self, inst, attr, value): + if value is None: + return + + self.validator(inst, attr, value) + + def __repr__(self): + return f"" + + +def optional(validator): + """ + A validator that makes an attribute optional. An optional attribute is one + which can be set to `None` in addition to satisfying the requirements of + the sub-validator. + + Args: + validator + (typing.Callable | tuple[typing.Callable] | list[typing.Callable]): + A validator (or validators) that is used for non-`None` values. + + .. versionadded:: 15.1.0 + .. versionchanged:: 17.1.0 *validator* can be a list of validators. + .. versionchanged:: 23.1.0 *validator* can also be a tuple of validators. + """ + if isinstance(validator, (list, tuple)): + return _OptionalValidator(_AndValidator(validator)) + + return _OptionalValidator(validator) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _InValidator: + options = attrib() + _original_options = attrib(hash=False) + + def __call__(self, inst, attr, value): + try: + in_options = value in self.options + except TypeError: # e.g. `1 in "abc"` + in_options = False + + if not in_options: + msg = f"'{attr.name}' must be in {self._original_options!r} (got {value!r})" + raise ValueError( + msg, + attr, + self._original_options, + value, + ) + + def __repr__(self): + return f"" + + +def in_(options): + """ + A validator that raises a `ValueError` if the initializer is called with a + value that does not belong in the *options* provided. + + The check is performed using ``value in options``, so *options* has to + support that operation. + + To keep the validator hashable, dicts, lists, and sets are transparently + transformed into a `tuple`. + + Args: + options: Allowed options. + + Raises: + ValueError: + With a human readable error message, the attribute (of type + `attrs.Attribute`), the expected options, and the value it got. + + .. versionadded:: 17.1.0 + .. versionchanged:: 22.1.0 + The ValueError was incomplete until now and only contained the human + readable error message. Now it contains all the information that has + been promised since 17.1.0. + .. versionchanged:: 24.1.0 + *options* that are a list, dict, or a set are now transformed into a + tuple to keep the validator hashable. + """ + repr_options = options + if isinstance(options, (list, dict, set)): + options = tuple(options) + + return _InValidator(options, repr_options) + + +@attrs(repr=False, slots=False, unsafe_hash=True) +class _IsCallableValidator: + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not callable(value): + message = ( + "'{name}' must be callable " + "(got {value!r} that is a {actual!r})." + ) + raise NotCallableError( + msg=message.format( + name=attr.name, value=value, actual=value.__class__ + ), + value=value, + ) + + def __repr__(self): + return "" + + +def is_callable(): + """ + A validator that raises a `attrs.exceptions.NotCallableError` if the + initializer is called with a value for this particular attribute that is + not callable. + + .. versionadded:: 19.1.0 + + Raises: + attrs.exceptions.NotCallableError: + With a human readable error message containing the attribute + (`attrs.Attribute`) name, and the value it got. + """ + return _IsCallableValidator() + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _DeepIterable: + member_validator = attrib(validator=is_callable()) + iterable_validator = attrib( + default=None, validator=optional(is_callable()) + ) + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if self.iterable_validator is not None: + self.iterable_validator(inst, attr, value) + + for member in value: + self.member_validator(inst, attr, member) + + def __repr__(self): + iterable_identifier = ( + "" + if self.iterable_validator is None + else f" {self.iterable_validator!r}" + ) + return ( + f"" + ) + + +def deep_iterable(member_validator, iterable_validator=None): + """ + A validator that performs deep validation of an iterable. + + Args: + member_validator: Validator(s) to apply to iterable members. + + iterable_validator: + Validator(s) to apply to iterable itself (optional). + + Raises + TypeError: if any sub-validators fail + + .. versionadded:: 19.1.0 + + .. versionchanged:: 25.4.0 + *member_validator* and *iterable_validator* can now be a list or tuple + of validators. + """ + if isinstance(member_validator, (list, tuple)): + member_validator = and_(*member_validator) + if isinstance(iterable_validator, (list, tuple)): + iterable_validator = and_(*iterable_validator) + return _DeepIterable(member_validator, iterable_validator) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _DeepMapping: + key_validator = attrib(validator=optional(is_callable())) + value_validator = attrib(validator=optional(is_callable())) + mapping_validator = attrib(validator=optional(is_callable())) + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if self.mapping_validator is not None: + self.mapping_validator(inst, attr, value) + + for key in value: + if self.key_validator is not None: + self.key_validator(inst, attr, key) + if self.value_validator is not None: + self.value_validator(inst, attr, value[key]) + + def __repr__(self): + return f"" + + +def deep_mapping( + key_validator=None, value_validator=None, mapping_validator=None +): + """ + A validator that performs deep validation of a dictionary. + + All validators are optional, but at least one of *key_validator* or + *value_validator* must be provided. + + Args: + key_validator: Validator(s) to apply to dictionary keys. + + value_validator: Validator(s) to apply to dictionary values. + + mapping_validator: + Validator(s) to apply to top-level mapping attribute. + + .. versionadded:: 19.1.0 + + .. versionchanged:: 25.4.0 + *key_validator* and *value_validator* are now optional, but at least one + of them must be provided. + + .. versionchanged:: 25.4.0 + *key_validator*, *value_validator*, and *mapping_validator* can now be a + list or tuple of validators. + + Raises: + TypeError: If any sub-validator fails on validation. + + ValueError: + If neither *key_validator* nor *value_validator* is provided on + instantiation. + """ + if key_validator is None and value_validator is None: + msg = ( + "At least one of key_validator or value_validator must be provided" + ) + raise ValueError(msg) + + if isinstance(key_validator, (list, tuple)): + key_validator = and_(*key_validator) + if isinstance(value_validator, (list, tuple)): + value_validator = and_(*value_validator) + if isinstance(mapping_validator, (list, tuple)): + mapping_validator = and_(*mapping_validator) + + return _DeepMapping(key_validator, value_validator, mapping_validator) + + +@attrs(repr=False, frozen=True, slots=True) +class _NumberValidator: + bound = attrib() + compare_op = attrib() + compare_func = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not self.compare_func(value, self.bound): + msg = f"'{attr.name}' must be {self.compare_op} {self.bound}: {value}" + raise ValueError(msg) + + def __repr__(self): + return f"" + + +def lt(val): + """ + A validator that raises `ValueError` if the initializer is called with a + number larger or equal to *val*. + + The validator uses `operator.lt` to compare the values. + + Args: + val: Exclusive upper bound for values. + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, "<", operator.lt) + + +def le(val): + """ + A validator that raises `ValueError` if the initializer is called with a + number greater than *val*. + + The validator uses `operator.le` to compare the values. + + Args: + val: Inclusive upper bound for values. + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, "<=", operator.le) + + +def ge(val): + """ + A validator that raises `ValueError` if the initializer is called with a + number smaller than *val*. + + The validator uses `operator.ge` to compare the values. + + Args: + val: Inclusive lower bound for values + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, ">=", operator.ge) + + +def gt(val): + """ + A validator that raises `ValueError` if the initializer is called with a + number smaller or equal to *val*. + + The validator uses `operator.gt` to compare the values. + + Args: + val: Exclusive lower bound for values + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, ">", operator.gt) + + +@attrs(repr=False, frozen=True, slots=True) +class _MaxLengthValidator: + max_length = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if len(value) > self.max_length: + msg = f"Length of '{attr.name}' must be <= {self.max_length}: {len(value)}" + raise ValueError(msg) + + def __repr__(self): + return f"" + + +def max_len(length): + """ + A validator that raises `ValueError` if the initializer is called + with a string or iterable that is longer than *length*. + + Args: + length (int): Maximum length of the string or iterable + + .. versionadded:: 21.3.0 + """ + return _MaxLengthValidator(length) + + +@attrs(repr=False, frozen=True, slots=True) +class _MinLengthValidator: + min_length = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if len(value) < self.min_length: + msg = f"Length of '{attr.name}' must be >= {self.min_length}: {len(value)}" + raise ValueError(msg) + + def __repr__(self): + return f"" + + +def min_len(length): + """ + A validator that raises `ValueError` if the initializer is called + with a string or iterable that is shorter than *length*. + + Args: + length (int): Minimum length of the string or iterable + + .. versionadded:: 22.1.0 + """ + return _MinLengthValidator(length) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _SubclassOfValidator: + type = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not issubclass(value, self.type): + msg = f"'{attr.name}' must be a subclass of {self.type!r} (got {value!r})." + raise TypeError( + msg, + attr, + self.type, + value, + ) + + def __repr__(self): + return f"" + + +def _subclass_of(type): + """ + A validator that raises a `TypeError` if the initializer is called with a + wrong type for this particular attribute (checks are performed using + `issubclass` therefore it's also valid to pass a tuple of types). + + Args: + type (type | tuple[type, ...]): The type(s) to check for. + + Raises: + TypeError: + With a human readable error message, the attribute (of type + `attrs.Attribute`), the expected type, and the value it got. + """ + return _SubclassOfValidator(type) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _NotValidator: + validator = attrib() + msg = attrib( + converter=default_if_none( + "not_ validator child '{validator!r}' " + "did not raise a captured error" + ) + ) + exc_types = attrib( + validator=deep_iterable( + member_validator=_subclass_of(Exception), + iterable_validator=instance_of(tuple), + ), + ) + + def __call__(self, inst, attr, value): + try: + self.validator(inst, attr, value) + except self.exc_types: + pass # suppress error to invert validity + else: + raise ValueError( + self.msg.format( + validator=self.validator, + exc_types=self.exc_types, + ), + attr, + self.validator, + value, + self.exc_types, + ) + + def __repr__(self): + return f"" + + +def not_(validator, *, msg=None, exc_types=(ValueError, TypeError)): + """ + A validator that wraps and logically 'inverts' the validator passed to it. + It will raise a `ValueError` if the provided validator *doesn't* raise a + `ValueError` or `TypeError` (by default), and will suppress the exception + if the provided validator *does*. + + Intended to be used with existing validators to compose logic without + needing to create inverted variants, for example, ``not_(in_(...))``. + + Args: + validator: A validator to be logically inverted. + + msg (str): + Message to raise if validator fails. Formatted with keys + ``exc_types`` and ``validator``. + + exc_types (tuple[type, ...]): + Exception type(s) to capture. Other types raised by child + validators will not be intercepted and pass through. + + Raises: + ValueError: + With a human readable error message, the attribute (of type + `attrs.Attribute`), the validator that failed to raise an + exception, the value it got, and the expected exception types. + + .. versionadded:: 22.2.0 + """ + try: + exc_types = tuple(exc_types) + except TypeError: + exc_types = (exc_types,) + return _NotValidator(validator, msg, exc_types) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _OrValidator: + validators = attrib() + + def __call__(self, inst, attr, value): + for v in self.validators: + try: + v(inst, attr, value) + except Exception: # noqa: BLE001, PERF203, S112 + continue + else: + return + + msg = f"None of {self.validators!r} satisfied for value {value!r}" + raise ValueError(msg) + + def __repr__(self): + return f"" + + +def or_(*validators): + """ + A validator that composes multiple validators into one. + + When called on a value, it runs all wrapped validators until one of them is + satisfied. + + Args: + validators (~collections.abc.Iterable[typing.Callable]): + Arbitrary number of validators. + + Raises: + ValueError: + If no validator is satisfied. Raised with a human-readable error + message listing all the wrapped validators and the value that + failed all of them. + + .. versionadded:: 24.1.0 + """ + vals = [] + for v in validators: + vals.extend(v.validators if isinstance(v, _OrValidator) else [v]) + + return _OrValidator(tuple(vals)) diff --git a/source/target-vendors/cp312/attr/validators.pyi b/source/target-vendors/cp312/attr/validators.pyi new file mode 100644 index 0000000..18fb112 --- /dev/null +++ b/source/target-vendors/cp312/attr/validators.pyi @@ -0,0 +1,140 @@ +from types import UnionType +from typing import ( + Any, + AnyStr, + Callable, + Container, + ContextManager, + Iterable, + Mapping, + Match, + Pattern, + TypeVar, + overload, +) + +from attrs import _ValidatorType +from attrs import _ValidatorArgType + +_T = TypeVar("_T") +_T1 = TypeVar("_T1") +_T2 = TypeVar("_T2") +_T3 = TypeVar("_T3") +_T4 = TypeVar("_T4") +_T5 = TypeVar("_T5") +_T6 = TypeVar("_T6") +_I = TypeVar("_I", bound=Iterable) +_K = TypeVar("_K") +_V = TypeVar("_V") +_M = TypeVar("_M", bound=Mapping) + +def set_disabled(run: bool) -> None: ... +def get_disabled() -> bool: ... +def disabled() -> ContextManager[None]: ... + +# To be more precise on instance_of use some overloads. +# If there are more than 3 items in the tuple then we fall back to Any +@overload +def instance_of(type: type[_T]) -> _ValidatorType[_T]: ... +@overload +def instance_of(type: tuple[type[_T]]) -> _ValidatorType[_T]: ... +@overload +def instance_of( + type: tuple[type[_T1], type[_T2]], +) -> _ValidatorType[_T1 | _T2]: ... +@overload +def instance_of( + type: tuple[type[_T1], type[_T2], type[_T3]], +) -> _ValidatorType[_T1 | _T2 | _T3]: ... +@overload +def instance_of(type: tuple[type, ...]) -> _ValidatorType[Any]: ... +@overload +def instance_of(type: UnionType) -> _ValidatorType[Any]: ... +def optional( + validator: ( + _ValidatorType[_T] + | list[_ValidatorType[_T]] + | tuple[_ValidatorType[_T], ...] + ), +) -> _ValidatorType[_T | None]: ... +def in_(options: Container[_T]) -> _ValidatorType[_T]: ... +def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ... +def matches_re( + regex: Pattern[AnyStr] | AnyStr, + flags: int = ..., + func: Callable[[AnyStr, AnyStr, int], Match[AnyStr] | None] | None = ..., +) -> _ValidatorType[AnyStr]: ... +def deep_iterable( + member_validator: _ValidatorArgType[_T], + iterable_validator: _ValidatorArgType[_I] | None = ..., +) -> _ValidatorType[_I]: ... +@overload +def deep_mapping( + key_validator: _ValidatorArgType[_K], + value_validator: _ValidatorArgType[_V] | None = ..., + mapping_validator: _ValidatorArgType[_M] | None = ..., +) -> _ValidatorType[_M]: ... +@overload +def deep_mapping( + key_validator: _ValidatorArgType[_K] | None = ..., + value_validator: _ValidatorArgType[_V] = ..., + mapping_validator: _ValidatorArgType[_M] | None = ..., +) -> _ValidatorType[_M]: ... +def is_callable() -> _ValidatorType[_T]: ... +def lt(val: _T) -> _ValidatorType[_T]: ... +def le(val: _T) -> _ValidatorType[_T]: ... +def ge(val: _T) -> _ValidatorType[_T]: ... +def gt(val: _T) -> _ValidatorType[_T]: ... +def max_len(length: int) -> _ValidatorType[_T]: ... +def min_len(length: int) -> _ValidatorType[_T]: ... +def not_( + validator: _ValidatorType[_T], + *, + msg: str | None = None, + exc_types: type[Exception] | Iterable[type[Exception]] = ..., +) -> _ValidatorType[_T]: ... +@overload +def or_( + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], +) -> _ValidatorType[_T1 | _T2]: ... +@overload +def or_( + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], + __v3: _ValidatorType[_T3], +) -> _ValidatorType[_T1 | _T2 | _T3]: ... +@overload +def or_( + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], + __v3: _ValidatorType[_T3], + __v4: _ValidatorType[_T4], +) -> _ValidatorType[_T1 | _T2 | _T3 | _T4]: ... +@overload +def or_( + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], + __v3: _ValidatorType[_T3], + __v4: _ValidatorType[_T4], + __v5: _ValidatorType[_T5], +) -> _ValidatorType[_T1 | _T2 | _T3 | _T4 | _T5]: ... +@overload +def or_( + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], + __v3: _ValidatorType[_T3], + __v4: _ValidatorType[_T4], + __v5: _ValidatorType[_T5], + __v6: _ValidatorType[_T6], +) -> _ValidatorType[_T1 | _T2 | _T3 | _T4 | _T5 | _T6]: ... +@overload +def or_( + __v1: _ValidatorType[Any], + __v2: _ValidatorType[Any], + __v3: _ValidatorType[Any], + __v4: _ValidatorType[Any], + __v5: _ValidatorType[Any], + __v6: _ValidatorType[Any], + *validators: _ValidatorType[Any], +) -> _ValidatorType[Any]: ... diff --git a/source/target-vendors/cp312/attrs-26.1.0.dist-info/METADATA b/source/target-vendors/cp312/attrs-26.1.0.dist-info/METADATA new file mode 100644 index 0000000..5cf16a0 --- /dev/null +++ b/source/target-vendors/cp312/attrs-26.1.0.dist-info/METADATA @@ -0,0 +1,199 @@ +Metadata-Version: 2.4 +Name: attrs +Version: 26.1.0 +Summary: Classes Without Boilerplate +Project-URL: Documentation, https://www.attrs.org/ +Project-URL: Changelog, https://www.attrs.org/en/stable/changelog.html +Project-URL: GitHub, https://github.com/python-attrs/attrs +Project-URL: Funding, https://github.com/sponsors/hynek +Project-URL: Tidelift, https://tidelift.com/subscription/pkg/pypi-attrs?utm_source=pypi-attrs&utm_medium=pypi +Author-email: Hynek Schlawack +License-Expression: MIT +License-File: LICENSE +Keywords: attribute,boilerplate,class +Classifier: Development Status :: 5 - Production/Stable +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Typing :: Typed +Requires-Python: >=3.9 +Description-Content-Type: text/markdown + +

+ + attrs + +

+ + +*attrs* is the Python package that will bring back the **joy** of **writing classes** by relieving you from the drudgery of implementing object protocols (aka [dunder methods](https://www.attrs.org/en/latest/glossary.html#term-dunder-methods)). +Trusted by NASA for [Mars missions since 2020](https://github.com/readme/featured/nasa-ingenuity-helicopter)! + +Its main goal is to help you to write **concise** and **correct** software without slowing down your code. + + +## Sponsors + +*attrs* would not be possible without our [amazing sponsors](https://github.com/sponsors/hynek). +Especially those generously supporting us at the *The Organization* tier and higher: + + + +

+ + + + + + + + + + + +

+ + + +

+ Please consider joining them to help make attrs’s maintenance more sustainable! +

+ + + +## Example + +*attrs* gives you a class decorator and a way to declaratively define the attributes on that class: + + + +```pycon +>>> from attrs import asdict, define, make_class, Factory + +>>> @define +... class SomeClass: +... a_number: int = 42 +... list_of_numbers: list[int] = Factory(list) +... +... def hard_math(self, another_number): +... return self.a_number + sum(self.list_of_numbers) * another_number + + +>>> sc = SomeClass(1, [1, 2, 3]) +>>> sc +SomeClass(a_number=1, list_of_numbers=[1, 2, 3]) + +>>> sc.hard_math(3) +19 +>>> sc == SomeClass(1, [1, 2, 3]) +True +>>> sc != SomeClass(2, [3, 2, 1]) +True + +>>> asdict(sc) +{'a_number': 1, 'list_of_numbers': [1, 2, 3]} + +>>> SomeClass() +SomeClass(a_number=42, list_of_numbers=[]) + +>>> C = make_class("C", ["a", "b"]) +>>> C("foo", "bar") +C(a='foo', b='bar') +``` + +After *declaring* your attributes, *attrs* gives you: + +- a concise and explicit overview of the class's attributes, +- a nice human-readable `__repr__`, +- equality-checking methods, +- an initializer, +- and much more, + +*without* writing dull boilerplate code again and again and *without* runtime performance penalties. + +--- + +This example uses *attrs*'s modern APIs that have been introduced in version 20.1.0, and the *attrs* package import name that has been added in version 21.3.0. +The classic APIs (`@attr.s`, `attr.ib`, plus their serious-business aliases) and the `attr` package import name will remain **indefinitely**. + +Check out [*On The Core API Names*](https://www.attrs.org/en/latest/names.html) for an in-depth explanation! + + +### Hate Type Annotations!? + +No problem! +Types are entirely **optional** with *attrs*. +Simply assign `attrs.field()` to the attributes instead of annotating them with types: + +```python +from attrs import define, field + +@define +class SomeClass: + a_number = field(default=42) + list_of_numbers = field(factory=list) +``` + + +## Data Classes + +On the tin, *attrs* might remind you of `dataclasses` (and indeed, `dataclasses` [are a descendant](https://hynek.me/articles/import-attrs/) of *attrs*). +In practice it does a lot more and is more flexible. +For instance, it allows you to define [special handling of NumPy arrays for equality checks](https://www.attrs.org/en/stable/comparison.html#customization), allows more ways to [plug into the initialization process](https://www.attrs.org/en/stable/init.html#hooking-yourself-into-initialization), has a replacement for `__init_subclass__`, and allows for stepping through the generated methods using a debugger. + +For more details, please refer to our [comparison page](https://www.attrs.org/en/stable/why.html#data-classes), but generally speaking, we are more likely to commit crimes against nature to make things work that one would expect to work, but that are quite complicated in practice. + + +## Project Information + +- [**Changelog**](https://www.attrs.org/en/stable/changelog.html) +- [**Documentation**](https://www.attrs.org/) +- [**PyPI**](https://pypi.org/project/attrs/) +- [**Source Code**](https://github.com/python-attrs/attrs) +- [**Contributing**](https://github.com/python-attrs/attrs/blob/main/.github/CONTRIBUTING.md) +- [**Third-party Extensions**](https://github.com/python-attrs/attrs/wiki/Extensions-to-attrs) +- **Get Help**: use the `python-attrs` tag on [Stack Overflow](https://stackoverflow.com/questions/tagged/python-attrs) + + +### *attrs* for Enterprise + +Available as part of the [Tidelift Subscription](https://tidelift.com/?utm_source=lifter&utm_medium=referral&utm_campaign=hynek). + +The maintainers of *attrs* and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. +Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use. + +## Release Information + +### Backwards-incompatible Changes + +- Field aliases are now resolved *before* calling `field_transformer`, so transformers receive fully populated `Attribute` objects with usable `alias` values instead of `None`. + The new `Attribute.alias_is_default` flag indicates whether the alias was auto-generated (`True`) or explicitly set by the user (`False`). + [#1509](https://github.com/python-attrs/attrs/issues/1509) + + +### Changes + +- Fix type annotations for `attrs.validators.optional()`, so it no longer rejects tuples with more than one validator. + [#1496](https://github.com/python-attrs/attrs/issues/1496) +- The `attrs.validators.disabled()` contextmanager can now be nested. + [#1513](https://github.com/python-attrs/attrs/issues/1513) +- Frozen classes can set `on_setattr=attrs.setters.NO_OP` in addition to `None`. + [#1515](https://github.com/python-attrs/attrs/issues/1515) +- It's now possible to pass *attrs* **instances** in addition to *attrs* **classes** to `attrs.fields()`. + [#1529](https://github.com/python-attrs/attrs/issues/1529) + + + +--- + +[Full changelog →](https://www.attrs.org/en/stable/changelog.html) diff --git a/source/target-vendors/cp312/attrs-26.1.0.dist-info/RECORD b/source/target-vendors/cp312/attrs-26.1.0.dist-info/RECORD new file mode 100644 index 0000000..d048dcf --- /dev/null +++ b/source/target-vendors/cp312/attrs-26.1.0.dist-info/RECORD @@ -0,0 +1,35 @@ +attr/__init__.py,sha256=fOYIvt1eGSqQre4uCS3sJWKZ0mwAuC8UD6qba5OS9_U,2057 +attr/__init__.pyi,sha256=pVGImAUVovq2_TYl_r_HIYnGlyOaoCuEhxo-EvsnnSc,11325 +attr/_cmp.py,sha256=3Nn1TjxllUYiX_nJoVnEkXoDk0hM1DYKj5DE7GZe4i0,4117 +attr/_cmp.pyi,sha256=U-_RU_UZOyPUEQzXE6RMYQQcjkZRY25wTH99sN0s7MM,368 +attr/_compat.py,sha256=x0g7iEUOnBVJC72zyFCgb1eKqyxS-7f2LGnNyZ_r95s,2829 +attr/_config.py,sha256=dGq3xR6fgZEF6UBt_L0T-eUHIB4i43kRmH0P28sJVw8,843 +attr/_funcs.py,sha256=Ix5IETTfz5F01F-12MF_CSFomIn2h8b67EVVz2gCtBE,16479 +attr/_make.py,sha256=H7OH2eWS5CnBzLUjNFE1WymfPrmF1r8fv2RPdt9MuYA,106129 +attr/_next_gen.py,sha256=BQtCUlzwg2gWHTYXBQvrEYBnzBUrDvO57u0Py6UCPhc,26274 +attr/_typing_compat.pyi,sha256=XDP54TUn-ZKhD62TOQebmzrwFyomhUCoGRpclb6alRA,469 +attr/_version_info.py,sha256=w4R-FYC3NK_kMkGUWJlYP4cVAlH9HRaC-um3fcjYkHM,2222 +attr/_version_info.pyi,sha256=x_M3L3WuB7r_ULXAWjx959udKQ4HLB8l-hsc1FDGNvk,209 +attr/converters.py,sha256=GlDeOzPeTFgeBBLbj9G57Ez5lAk68uhSALRYJ_exe84,3861 +attr/converters.pyi,sha256=orU2bff-VjQa2kMDyvnMQV73oJT2WRyQuw4ZR1ym1bE,643 +attr/exceptions.py,sha256=b4vMbnoQ3VpwWZhqrYi_ssXVCK8o2c4HQSS09cSUM9o,1990 +attr/exceptions.pyi,sha256=zZq8bCUnKAy9mDtBEw42ZhPhAUIHoTKedDQInJD883M,539 +attr/filters.py,sha256=ZBiKWLp3R0LfCZsq7X11pn9WX8NslS2wXM4jsnLOGc8,1795 +attr/filters.pyi,sha256=3J5BG-dTxltBk1_-RuNRUHrv2qu1v8v4aDNAQ7_mifA,208 +attr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +attr/setters.py,sha256=5-dcT63GQK35ONEzSgfXCkbB7pPkaR-qv15mm4PVSzQ,1617 +attr/setters.pyi,sha256=NnVkaFU1BB4JB8E4JuXyrzTUgvtMpj8p3wBdJY7uix4,584 +attr/validators.py,sha256=m3QRzZTANr4f2C4eVdUoFg11NgXWak8Wat4qQTGhvcs,21553 +attr/validators.pyi,sha256=gM1ZmHaBckyYWI2EirpRNzqm3B19cw5Iq6B4Kno9YCM,4087 +attrs/__init__.py,sha256=RxaAZNwYiEh-fcvHLZNpQ_DWKni73M_jxEPEftiq1Zc,1183 +attrs/__init__.pyi,sha256=2gV79g9UxJppGSM48hAZJ6h_MHb70dZoJL31ZNJeZYI,9416 +attrs/converters.py,sha256=8kQljrVwfSTRu8INwEk8SI0eGrzmWftsT7rM0EqyohM,76 +attrs/exceptions.py,sha256=ACCCmg19-vDFaDPY9vFl199SPXCQMN_bENs4DALjzms,76 +attrs/filters.py,sha256=VOUMZug9uEU6dUuA0dF1jInUK0PL3fLgP0VBS5d-CDE,73 +attrs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +attrs/setters.py,sha256=eL1YidYQV3T2h9_SYIZSZR1FAcHGb1TuCTy0E0Lv2SU,73 +attrs/validators.py,sha256=xcy6wD5TtTkdCG1f4XWbocPSO0faBjk5IfVJfP6SUj0,76 +attrs-26.1.0.dist-info/METADATA,sha256=TNQOaQ8jvzfLytNO_WdY4GLfHfB8hoM_fjzpW_H6OMw,8754 +attrs-26.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87 +attrs-26.1.0.dist-info/licenses/LICENSE,sha256=iCEVyV38KvHutnFPjsbVy8q_Znyv-HKfQkINpj9xTp8,1109 +attrs-26.1.0.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/attrs-26.1.0.dist-info/WHEEL b/source/target-vendors/cp312/attrs-26.1.0.dist-info/WHEEL new file mode 100644 index 0000000..b1b94fd --- /dev/null +++ b/source/target-vendors/cp312/attrs-26.1.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.29.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/source/target-vendors/cp312/attrs-26.1.0.dist-info/licenses/LICENSE b/source/target-vendors/cp312/attrs-26.1.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000..2bd6453 --- /dev/null +++ b/source/target-vendors/cp312/attrs-26.1.0.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Hynek Schlawack and the attrs contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/source/target-vendors/cp312/attrs/__init__.py b/source/target-vendors/cp312/attrs/__init__.py new file mode 100644 index 0000000..dc1ce4b --- /dev/null +++ b/source/target-vendors/cp312/attrs/__init__.py @@ -0,0 +1,72 @@ +# SPDX-License-Identifier: MIT + +from attr import ( + NOTHING, + Attribute, + AttrsInstance, + Converter, + Factory, + NothingType, + _make_getattr, + assoc, + cmp_using, + define, + evolve, + field, + fields, + fields_dict, + frozen, + has, + make_class, + mutable, + resolve_types, + validate, +) +from attr._make import ClassProps +from attr._next_gen import asdict, astuple, inspect + +from . import converters, exceptions, filters, setters, validators + + +__all__ = [ + "NOTHING", + "Attribute", + "AttrsInstance", + "ClassProps", + "Converter", + "Factory", + "NothingType", + "__author__", + "__copyright__", + "__description__", + "__doc__", + "__email__", + "__license__", + "__title__", + "__url__", + "__version__", + "__version_info__", + "asdict", + "assoc", + "astuple", + "cmp_using", + "converters", + "define", + "evolve", + "exceptions", + "field", + "fields", + "fields_dict", + "filters", + "frozen", + "has", + "inspect", + "make_class", + "mutable", + "resolve_types", + "setters", + "validate", + "validators", +] + +__getattr__ = _make_getattr(__name__) diff --git a/source/target-vendors/cp312/attrs/__init__.pyi b/source/target-vendors/cp312/attrs/__init__.pyi new file mode 100644 index 0000000..6364bac --- /dev/null +++ b/source/target-vendors/cp312/attrs/__init__.pyi @@ -0,0 +1,314 @@ +import sys + +from typing import ( + Any, + Callable, + Mapping, + Sequence, + overload, + TypeVar, +) + +# Because we need to type our own stuff, we have to make everything from +# attr explicitly public too. +from attr import __author__ as __author__ +from attr import __copyright__ as __copyright__ +from attr import __description__ as __description__ +from attr import __email__ as __email__ +from attr import __license__ as __license__ +from attr import __title__ as __title__ +from attr import __url__ as __url__ +from attr import __version__ as __version__ +from attr import __version_info__ as __version_info__ +from attr import assoc as assoc +from attr import Attribute as Attribute +from attr import AttrsInstance as AttrsInstance +from attr import cmp_using as cmp_using +from attr import converters as converters +from attr import Converter as Converter +from attr import evolve as evolve +from attr import exceptions as exceptions +from attr import Factory as Factory +from attr import fields as fields +from attr import fields_dict as fields_dict +from attr import filters as filters +from attr import has as has +from attr import make_class as make_class +from attr import NOTHING as NOTHING +from attr import resolve_types as resolve_types +from attr import setters as setters +from attr import validate as validate +from attr import validators as validators +from attr import attrib, asdict as asdict, astuple as astuple +from attr import NothingType as NothingType + +if sys.version_info >= (3, 11): + from typing import dataclass_transform +else: + from typing_extensions import dataclass_transform + +_T = TypeVar("_T") +_C = TypeVar("_C", bound=type) + +_EqOrderType = bool | Callable[[Any], Any] +_ValidatorType = Callable[[Any, "Attribute[_T]", _T], Any] +_CallableConverterType = Callable[[Any], Any] +_ConverterType = _CallableConverterType | Converter[Any, Any] +_ReprType = Callable[[Any], str] +_ReprArgType = bool | _ReprType +_OnSetAttrType = Callable[[Any, "Attribute[Any]", Any], Any] +_OnSetAttrArgType = _OnSetAttrType | list[_OnSetAttrType] | setters._NoOpType +_FieldTransformer = Callable[ + [type, list["Attribute[Any]"]], list["Attribute[Any]"] +] +# FIXME: in reality, if multiple validators are passed they must be in a list +# or tuple, but those are invariant and so would prevent subtypes of +# _ValidatorType from working when passed in a list or tuple. +_ValidatorArgType = _ValidatorType[_T] | Sequence[_ValidatorType[_T]] + +@overload +def field( + *, + default: None = ..., + validator: None = ..., + repr: _ReprArgType = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + converter: None = ..., + factory: None = ..., + kw_only: bool | None = ..., + eq: bool | None = ..., + order: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., + type: type | None = ..., +) -> Any: ... + +# This form catches an explicit None or no default and infers the type from the +# other arguments. +@overload +def field( + *, + default: None = ..., + validator: _ValidatorArgType[_T] | None = ..., + repr: _ReprArgType = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType, ...] + | None = ..., + factory: Callable[[], _T] | None = ..., + kw_only: bool | None = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., + type: type | None = ..., +) -> _T: ... + +# This form catches an explicit default argument. +@overload +def field( + *, + default: _T, + validator: _ValidatorArgType[_T] | None = ..., + repr: _ReprArgType = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType, ...] + | None = ..., + factory: Callable[[], _T] | None = ..., + kw_only: bool | None = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., + type: type | None = ..., +) -> _T: ... + +# This form covers type=non-Type: e.g. forward references (str), Any +@overload +def field( + *, + default: _T | None = ..., + validator: _ValidatorArgType[_T] | None = ..., + repr: _ReprArgType = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType, ...] + | None = ..., + factory: Callable[[], _T] | None = ..., + kw_only: bool | None = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., + type: type | None = ..., +) -> Any: ... +@overload +@dataclass_transform(field_specifiers=(attrib, field)) +def define( + maybe_cls: _C, + *, + these: dict[str, Any] | None = ..., + repr: bool = ..., + unsafe_hash: bool | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: bool | None = ..., + order: bool | None = ..., + auto_detect: bool = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., + match_args: bool = ..., +) -> _C: ... +@overload +@dataclass_transform(field_specifiers=(attrib, field)) +def define( + maybe_cls: None = ..., + *, + these: dict[str, Any] | None = ..., + repr: bool = ..., + unsafe_hash: bool | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: bool | None = ..., + order: bool | None = ..., + auto_detect: bool = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., + match_args: bool = ..., +) -> Callable[[_C], _C]: ... + +mutable = define + +@overload +@dataclass_transform(frozen_default=True, field_specifiers=(attrib, field)) +def frozen( + maybe_cls: _C, + *, + these: dict[str, Any] | None = ..., + repr: bool = ..., + unsafe_hash: bool | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: bool | None = ..., + order: bool | None = ..., + auto_detect: bool = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., + match_args: bool = ..., +) -> _C: ... +@overload +@dataclass_transform(frozen_default=True, field_specifiers=(attrib, field)) +def frozen( + maybe_cls: None = ..., + *, + these: dict[str, Any] | None = ..., + repr: bool = ..., + unsafe_hash: bool | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: bool | None = ..., + order: bool | None = ..., + auto_detect: bool = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., + match_args: bool = ..., +) -> Callable[[_C], _C]: ... + +class ClassProps: + # XXX: somehow when defining/using enums Mypy starts looking at our own + # (untyped) code and causes tons of errors. + Hashability: Any + KeywordOnly: Any + + is_exception: bool + is_slotted: bool + has_weakref_slot: bool + is_frozen: bool + # kw_only: ClassProps.KeywordOnly + kw_only: Any + collected_fields_by_mro: bool + added_init: bool + added_repr: bool + added_eq: bool + added_ordering: bool + # hashability: ClassProps.Hashability + hashability: Any + added_match_args: bool + added_str: bool + added_pickling: bool + on_setattr_hook: _OnSetAttrType | None + field_transformer: Callable[[Attribute[Any]], Attribute[Any]] | None + + def __init__( + self, + is_exception: bool, + is_slotted: bool, + has_weakref_slot: bool, + is_frozen: bool, + # kw_only: ClassProps.KeywordOnly + kw_only: Any, + collected_fields_by_mro: bool, + added_init: bool, + added_repr: bool, + added_eq: bool, + added_ordering: bool, + # hashability: ClassProps.Hashability + hashability: Any, + added_match_args: bool, + added_str: bool, + added_pickling: bool, + on_setattr_hook: _OnSetAttrType, + field_transformer: Callable[[Attribute[Any]], Attribute[Any]], + ) -> None: ... + @property + def is_hashable(self) -> bool: ... + +def inspect(cls: type) -> ClassProps: ... diff --git a/source/target-vendors/cp312/attrs/converters.py b/source/target-vendors/cp312/attrs/converters.py new file mode 100644 index 0000000..7821f6c --- /dev/null +++ b/source/target-vendors/cp312/attrs/converters.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.converters import * # noqa: F403 diff --git a/source/target-vendors/cp312/attrs/exceptions.py b/source/target-vendors/cp312/attrs/exceptions.py new file mode 100644 index 0000000..3323f9d --- /dev/null +++ b/source/target-vendors/cp312/attrs/exceptions.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.exceptions import * # noqa: F403 diff --git a/source/target-vendors/cp312/attrs/filters.py b/source/target-vendors/cp312/attrs/filters.py new file mode 100644 index 0000000..3080f48 --- /dev/null +++ b/source/target-vendors/cp312/attrs/filters.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.filters import * # noqa: F403 diff --git a/source/target-vendors/cp312/attrs/py.typed b/source/target-vendors/cp312/attrs/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/attrs/setters.py b/source/target-vendors/cp312/attrs/setters.py new file mode 100644 index 0000000..f3d73bb --- /dev/null +++ b/source/target-vendors/cp312/attrs/setters.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.setters import * # noqa: F403 diff --git a/source/target-vendors/cp312/attrs/validators.py b/source/target-vendors/cp312/attrs/validators.py new file mode 100644 index 0000000..037e124 --- /dev/null +++ b/source/target-vendors/cp312/attrs/validators.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.validators import * # noqa: F403 diff --git a/source/target-vendors/cp312/certifi-2026.7.22.dist-info/METADATA b/source/target-vendors/cp312/certifi-2026.7.22.dist-info/METADATA new file mode 100644 index 0000000..eb460c2 --- /dev/null +++ b/source/target-vendors/cp312/certifi-2026.7.22.dist-info/METADATA @@ -0,0 +1,78 @@ +Metadata-Version: 2.4 +Name: certifi +Version: 2026.7.22 +Summary: Python package for providing Mozilla's CA Bundle. +Home-page: https://github.com/certifi/python-certifi +Author: Kenneth Reitz +Author-email: me@kennethreitz.com +License: MPL-2.0 +Project-URL: Source, https://github.com/certifi/python-certifi +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) +Classifier: Natural Language :: English +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Requires-Python: >=3.7 +License-File: LICENSE +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: home-page +Dynamic: license +Dynamic: license-file +Dynamic: project-url +Dynamic: requires-python +Dynamic: summary + +Certifi: Python SSL Certificates +================================ + +Certifi provides Mozilla's carefully curated collection of Root Certificates for +validating the trustworthiness of SSL certificates while verifying the identity +of TLS hosts. It has been extracted from the `Requests`_ project. + +Installation +------------ + +``certifi`` is available on PyPI. Simply install it with ``pip``:: + + $ pip install certifi + +Usage +----- + +To reference the installed certificate authority (CA) bundle, you can use the +built-in function:: + + >>> import certifi + + >>> certifi.where() + '/usr/local/lib/python3.7/site-packages/certifi/cacert.pem' + +Or from the command line:: + + $ python -m certifi + /usr/local/lib/python3.7/site-packages/certifi/cacert.pem + +Enjoy! + +.. _`Requests`: https://requests.readthedocs.io/en/latest/ + +Addition/Removal of Certificates +-------------------------------- + +Certifi does not support any addition/removal or other modification of the +CA trust store content. This project is intended to provide a reliable and +highly portable root of trust to python deployments. Look to upstream projects +for methods to use alternate trust. diff --git a/source/target-vendors/cp312/certifi-2026.7.22.dist-info/RECORD b/source/target-vendors/cp312/certifi-2026.7.22.dist-info/RECORD new file mode 100644 index 0000000..f378e12 --- /dev/null +++ b/source/target-vendors/cp312/certifi-2026.7.22.dist-info/RECORD @@ -0,0 +1,12 @@ +certifi/__init__.py,sha256=elLl9CBfmz0SoxiYzqp-P2g30ZzCMHALyuGp2R0UhsE,94 +certifi/__main__.py,sha256=xBBoj905TUWBLRGANOcf7oi6e-3dMP4cEoG9OyMs11g,243 +certifi/cacert.pem,sha256=nMKndLUZjc_xTZvh5mCR9TiXXYZ84Cmpa84VpV39cw8,240216 +certifi/core.py,sha256=XFXycndG5pf37ayeF8N32HUuDafsyhkVMbO4BAPWHa0,3394 +certifi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +certifi/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +certifi/tests/test_certify.py,sha256=fBedHt839-kSsTNmNzX7RsYzjV0OX4kV0Rw7wofSJ7g,467 +certifi-2026.7.22.dist-info/licenses/LICENSE,sha256=6TcW2mucDVpKHfYP5pWzcPBpVgPSH2-D8FPkLPwQyvc,989 +certifi-2026.7.22.dist-info/METADATA,sha256=71rxY4-7I2dqw8V3ffz8LNnDSP5Bcu1bo9J3ZVskgJA,2474 +certifi-2026.7.22.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91 +certifi-2026.7.22.dist-info/top_level.txt,sha256=KMu4vUCfsjLrkPbSNdgdekS-pVJzBAJFO__nI8NF6-U,8 +certifi-2026.7.22.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/certifi-2026.7.22.dist-info/WHEEL b/source/target-vendors/cp312/certifi-2026.7.22.dist-info/WHEEL new file mode 100644 index 0000000..1d472b6 --- /dev/null +++ b/source/target-vendors/cp312/certifi-2026.7.22.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (83.0.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/source/target-vendors/cp312/certifi-2026.7.22.dist-info/licenses/LICENSE b/source/target-vendors/cp312/certifi-2026.7.22.dist-info/licenses/LICENSE new file mode 100644 index 0000000..62b076c --- /dev/null +++ b/source/target-vendors/cp312/certifi-2026.7.22.dist-info/licenses/LICENSE @@ -0,0 +1,20 @@ +This package contains a modified version of ca-bundle.crt: + +ca-bundle.crt -- Bundle of CA Root Certificates + +This is a bundle of X.509 certificates of public Certificate Authorities +(CA). These were automatically extracted from Mozilla's root certificates +file (certdata.txt). This file can be found in the mozilla source tree: +https://hg.mozilla.org/mozilla-central/file/tip/security/nss/lib/ckfw/builtins/certdata.txt +It contains the certificates in PEM format and therefore +can be directly used with curl / libcurl / php_curl, or with +an Apache+mod_ssl webserver for SSL client authentication. +Just configure this file as the SSLCACertificateFile.# + +***** BEGIN LICENSE BLOCK ***** +This Source Code Form is subject to the terms of the Mozilla Public License, +v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain +one at http://mozilla.org/MPL/2.0/. + +***** END LICENSE BLOCK ***** +@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $ diff --git a/source/target-vendors/cp312/certifi-2026.7.22.dist-info/top_level.txt b/source/target-vendors/cp312/certifi-2026.7.22.dist-info/top_level.txt new file mode 100644 index 0000000..963eac5 --- /dev/null +++ b/source/target-vendors/cp312/certifi-2026.7.22.dist-info/top_level.txt @@ -0,0 +1 @@ +certifi diff --git a/source/target-vendors/cp312/certifi/__init__.py b/source/target-vendors/cp312/certifi/__init__.py new file mode 100644 index 0000000..66dae9e --- /dev/null +++ b/source/target-vendors/cp312/certifi/__init__.py @@ -0,0 +1,4 @@ +from .core import contents, where + +__all__ = ["contents", "where"] +__version__ = "2026.07.22" diff --git a/source/target-vendors/cp312/certifi/__main__.py b/source/target-vendors/cp312/certifi/__main__.py new file mode 100644 index 0000000..8945b5d --- /dev/null +++ b/source/target-vendors/cp312/certifi/__main__.py @@ -0,0 +1,12 @@ +import argparse + +from certifi import contents, where + +parser = argparse.ArgumentParser() +parser.add_argument("-c", "--contents", action="store_true") +args = parser.parse_args() + +if args.contents: + print(contents()) +else: + print(where()) diff --git a/source/target-vendors/cp312/certifi/cacert.pem b/source/target-vendors/cp312/certifi/cacert.pem new file mode 100644 index 0000000..9a40f43 --- /dev/null +++ b/source/target-vendors/cp312/certifi/cacert.pem @@ -0,0 +1,3959 @@ + +# Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Label: "COMODO ECC Certification Authority" +# Serial: 41578283867086692638256921589707938090 +# MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23 +# SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11 +# SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7 +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +# Issuer: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) +# Subject: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) +# Label: "NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny" +# Serial: 80544274841616 +# MD5 Fingerprint: c5:a1:b7:ff:73:dd:d6:d7:34:32:18:df:fc:3c:ad:88 +# SHA1 Fingerprint: 06:08:3f:59:3f:15:a1:04:a0:69:a4:6b:a9:03:d0:06:b7:97:09:91 +# SHA256 Fingerprint: 6c:61:da:c3:a2:de:f0:31:50:6b:e0:36:d2:a6:fe:40:19:94:fb:d1:3d:f9:c8:d4:66:59:92:74:c4:46:ec:98 +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG +EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 +MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl +cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR +dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB +pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM +b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm +aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz +IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT +lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz +AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 +VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG +ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 +BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG +AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M +U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh +bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C ++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F +uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 +XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +# Issuer: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. +# Subject: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. +# Label: "Microsec e-Szigno Root CA 2009" +# Serial: 14014712776195784473 +# MD5 Fingerprint: f8:49:f4:03:bc:44:2d:83:be:48:69:7d:29:64:fc:b1 +# SHA1 Fingerprint: 89:df:74:fe:5c:f4:0f:4a:80:f9:e3:37:7d:54:da:91:e1:01:31:8e +# SHA256 Fingerprint: 3c:5f:81:fe:a5:fa:b8:2c:64:bf:a2:ea:ec:af:cd:e8:e0:77:fc:86:20:a7:ca:e5:37:16:3d:f3:6e:db:f3:78 +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G +CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y +OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx +FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp +Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP +kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc +cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U +fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 +N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC +xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 ++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM +Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG +SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h +mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk +ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c +2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t +HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Label: "GlobalSign Root CA - R3" +# Serial: 4835703278459759426209954 +# MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28 +# SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad +# SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- + +# Issuer: CN=Izenpe.com O=IZENPE S.A. +# Subject: CN=Izenpe.com O=IZENPE S.A. +# Label: "Izenpe.com" +# Serial: 917563065490389241595536686991402621 +# MD5 Fingerprint: a6:b0:cd:85:80:da:5c:50:34:a3:39:90:2f:55:67:73 +# SHA1 Fingerprint: 2f:78:3d:25:52:18:a7:4a:65:39:71:b5:2c:a2:9c:45:15:6f:e9:19 +# SHA256 Fingerprint: 25:30:cc:8e:98:32:15:02:ba:d9:6f:9b:1f:ba:1b:09:9e:2d:29:9e:0f:45:48:bb:91:4f:36:3b:c0:d4:53:1f +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 +MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 +ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD +VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j +b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq +scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO +xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H +LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX +uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD +yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ +JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q +rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN +BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L +hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB +QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ +HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu +Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg +QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB +BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA +A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb +laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 +awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo +JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw +LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT +VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk +LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb +UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ +QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ +naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls +QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +# Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Label: "Go Daddy Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01 +# SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b +# SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96 +# SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e +# SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5 +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Services Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 17:35:74:af:7b:61:1c:eb:f4:f9:3c:e2:ee:40:f9:a2 +# SHA1 Fingerprint: 92:5a:8f:8d:2c:6d:04:e0:66:5f:59:6a:ff:22:d8:63:e8:25:6f:3f +# SHA256 Fingerprint: 56:8d:69:05:a2:c8:87:08:a4:b3:02:51:90:ed:cf:ed:b1:97:4a:60:6a:13:c6:e5:29:0f:cb:2a:e6:3e:da:b5 +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Network CA" +# Serial: 279744 +# MD5 Fingerprint: d5:e9:81:40:c5:18:69:fc:46:2c:89:75:62:0f:aa:78 +# SHA1 Fingerprint: 07:e0:32:e0:20:b7:2c:3f:19:2f:06:28:a2:59:3a:19:a7:0f:06:9e +# SHA256 Fingerprint: 5c:58:46:8d:55:f5:8e:49:7e:74:39:82:d2:b5:00:10:b6:d1:65:37:4a:cf:83:a7:d4:a3:2d:b7:68:c4:40:8e +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM +MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D +ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU +cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 +WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg +Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw +IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH +UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM +TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU +BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM +kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x +AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV +HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y +sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL +I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 +J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY +VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +# Issuer: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA +# Label: "TWCA Root Certification Authority" +# Serial: 1 +# MD5 Fingerprint: aa:08:8f:f6:f9:7b:b7:f2:b1:a7:1e:9b:ea:ea:bd:79 +# SHA1 Fingerprint: cf:9e:87:6d:d3:eb:fc:42:26:97:a3:b5:a3:7a:a0:76:a9:06:23:48 +# SHA256 Fingerprint: bf:d8:8f:e1:10:1c:41:ae:3e:80:1b:f8:be:56:35:0e:e9:ba:d1:a6:b9:bd:51:5e:dc:5c:6d:5b:87:11:ac:44 +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES +MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU +V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz +WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO +LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE +AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH +K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX +RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z +rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx +3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq +hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC +MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls +XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D +lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn +aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ +YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +# Issuer: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 +# Subject: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 +# Label: "Security Communication RootCA2" +# Serial: 0 +# MD5 Fingerprint: 6c:39:7d:a4:0e:55:59:b2:3f:d6:41:b1:12:50:de:43 +# SHA1 Fingerprint: 5f:3b:8c:f2:f8:10:b3:7d:78:b4:ce:ec:19:19:c3:73:34:b9:c7:74 +# SHA256 Fingerprint: 51:3b:2c:ec:b8:10:d4:cd:e5:dd:85:39:1a:df:c6:c2:dd:60:d8:7b:b7:36:d2:b5:21:48:4a:a4:7a:0e:be:f6 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX +DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy +dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj +YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV +OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr +zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM +VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ +hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO +ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw +awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs +OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF +coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc +okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 +t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy +1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ +SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +# Issuer: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 +# Subject: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 +# Label: "Actalis Authentication Root CA" +# Serial: 6271844772424770508 +# MD5 Fingerprint: 69:c1:0d:4f:07:a3:1b:c3:fe:56:3d:04:bc:11:f6:a6 +# SHA1 Fingerprint: f3:73:b3:87:06:5a:28:84:8a:f2:f3:4a:ce:19:2b:dd:c7:8e:9c:ac +# SHA256 Fingerprint: 55:92:60:84:ec:96:3a:64:b9:6e:2a:be:01:ce:0b:a8:6a:64:fb:fe:bc:c7:aa:b5:af:c1:55:b3:7f:d7:60:66 +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 +# Subject: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 +# Label: "Buypass Class 2 Root CA" +# Serial: 2 +# MD5 Fingerprint: 46:a7:d2:fe:45:fb:64:5a:a8:59:90:9b:78:44:9b:29 +# SHA1 Fingerprint: 49:0a:75:74:de:87:0a:47:fe:58:ee:f6:c7:6b:eb:c6:0b:12:40:99 +# SHA256 Fingerprint: 9a:11:40:25:19:7c:5b:b9:5d:94:e6:3d:55:cd:43:79:08:47:b6:46:b2:3c:df:11:ad:a4:a0:0e:ff:15:fb:48 +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 +# Subject: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 +# Label: "Buypass Class 3 Root CA" +# Serial: 2 +# MD5 Fingerprint: 3d:3b:18:9e:2c:64:5a:e8:d5:88:ce:0e:f9:37:c2:ec +# SHA1 Fingerprint: da:fa:f7:fa:66:84:ec:06:8f:14:50:bd:c7:c2:81:a5:bc:a9:64:57 +# SHA256 Fingerprint: ed:f7:eb:bc:a2:7a:2a:38:4d:38:7b:7d:40:10:c6:66:e2:ed:b4:84:3e:4c:29:b4:ae:1d:5b:93:32:e6:b2:4d +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE----- + +# Issuer: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Subject: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Label: "T-TeleSec GlobalRoot Class 3" +# Serial: 1 +# MD5 Fingerprint: ca:fb:40:a8:4e:39:92:8a:1d:fe:8e:2f:c4:27:ea:ef +# SHA1 Fingerprint: 55:a6:72:3e:cb:f2:ec:cd:c3:23:74:70:19:9d:2a:be:11:e3:81:d1 +# SHA256 Fingerprint: fd:73:da:d3:1c:64:4f:f1:b4:3b:ef:0c:cd:da:96:71:0b:9c:d9:87:5e:ca:7e:31:70:7a:f3:e9:6d:52:2b:bd +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH +# Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH +# Label: "D-TRUST Root Class 3 CA 2 2009" +# Serial: 623603 +# MD5 Fingerprint: cd:e0:25:69:8d:47:ac:9c:89:35:90:f7:fd:51:3d:2f +# SHA1 Fingerprint: 58:e8:ab:b0:36:15:33:fb:80:f7:9b:1b:6d:29:d3:ff:8d:5f:00:f0 +# SHA256 Fingerprint: 49:e7:a4:42:ac:f0:ea:62:87:05:00:54:b5:25:64:b6:50:e4:f4:9e:42:e3:48:d6:aa:38:e0:39:e9:57:b1:c1 +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH +# Subject: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH +# Label: "D-TRUST Root Class 3 CA 2 EV 2009" +# Serial: 623604 +# MD5 Fingerprint: aa:c6:43:2c:5e:2d:cd:c4:34:c0:50:4f:11:02:4f:b6 +# SHA1 Fingerprint: 96:c9:1b:0b:95:b4:10:98:42:fa:d0:d8:22:79:fe:60:fa:b9:16:83 +# SHA256 Fingerprint: ee:c5:49:6b:98:8c:e9:86:25:b9:34:09:2e:ec:29:08:be:d0:b0:f3:16:c2:d4:73:0c:84:ea:f1:f3:d3:48:81 +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +# Issuer: CN=CA Disig Root R2 O=Disig a.s. +# Subject: CN=CA Disig Root R2 O=Disig a.s. +# Label: "CA Disig Root R2" +# Serial: 10572350602393338211 +# MD5 Fingerprint: 26:01:fb:d8:27:a7:17:9a:45:54:38:1a:43:01:3b:03 +# SHA1 Fingerprint: b5:61:eb:ea:a4:de:e4:25:4b:69:1a:98:a5:57:47:c2:34:c7:d9:71 +# SHA256 Fingerprint: e2:3d:4a:03:6d:7b:70:e9:f5:95:b1:42:20:79:d2:b9:1e:df:bb:1f:b6:51:a0:63:3e:aa:8a:9d:c5:f8:07:03 +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy +MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe +NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH +PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I +x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe +QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR +yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO +QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 +H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ +QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD +i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs +nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 +rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI +hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf +GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb +lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka ++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal +TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i +nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 +gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr +G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os +zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x +L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +# Issuer: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV +# Subject: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV +# Label: "ACCVRAIZ1" +# Serial: 6828503384748696800 +# MD5 Fingerprint: d0:a0:5a:ee:05:b6:09:94:21:a1:7d:f1:b2:29:82:02 +# SHA1 Fingerprint: 93:05:7a:88:15:c6:4f:ce:88:2f:fa:91:16:52:28:78:bc:53:64:17 +# SHA256 Fingerprint: 9a:6e:c0:12:e1:a7:da:9d:be:34:19:4d:47:8a:d7:c0:db:18:22:fb:07:1d:f1:29:81:49:6e:d1:04:38:41:13 +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +# Issuer: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA +# Label: "TWCA Global Root CA" +# Serial: 3262 +# MD5 Fingerprint: f9:03:7e:cf:e6:9e:3c:73:7a:2a:90:07:69:ff:2b:96 +# SHA1 Fingerprint: 9c:bb:48:53:f6:a4:f6:d3:52:a4:e8:32:52:55:60:13:f5:ad:af:65 +# SHA256 Fingerprint: 59:76:90:07:f7:68:5d:0f:cd:50:87:2f:9f:95:d5:75:5a:5b:2b:45:7d:81:f3:69:2b:61:0a:98:67:2f:0e:1b +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE----- + +# Issuer: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Subject: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Label: "T-TeleSec GlobalRoot Class 2" +# Serial: 1 +# MD5 Fingerprint: 2b:9b:9e:e4:7b:6c:1f:00:72:1a:cc:c1:77:79:df:6a +# SHA1 Fingerprint: 59:0d:2d:7d:88:4f:40:2e:61:7e:a5:62:32:17:65:cf:17:d8:94:e9 +# SHA256 Fingerprint: 91:e2:f5:78:8d:58:10:eb:a7:ba:58:73:7d:e1:54:8a:8e:ca:cd:01:45:98:bc:0b:14:3e:04:1b:17:05:25:52 +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd +AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC +FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi +1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq +jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ +wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ +WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy +NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC +uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw +IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 +g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP +BSeOE6Fuwg== +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 1 G3" +# Serial: 687049649626669250736271037606554624078720034195 +# MD5 Fingerprint: a4:bc:5b:3f:fe:37:9a:fa:64:f0:e2:fa:05:3d:0b:ab +# SHA1 Fingerprint: 1b:8e:ea:57:96:29:1a:c9:39:ea:b8:0a:81:1a:73:73:c0:93:79:67 +# SHA256 Fingerprint: 8a:86:6f:d1:b2:76:b5:7e:57:8e:92:1c:65:82:8a:2b:ed:58:e9:f2:f2:88:05:41:34:b7:f1:f4:bf:c9:cc:74 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 +MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV +wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe +rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 +68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh +4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp +UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o +abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc +3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G +KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt +hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO +Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt +zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD +ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 +cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN +qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 +YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv +b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 +8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k +NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj +ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp +q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt +nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 2 G3" +# Serial: 390156079458959257446133169266079962026824725800 +# MD5 Fingerprint: af:0c:86:6e:bf:40:2d:7f:0b:3e:12:50:ba:12:3d:06 +# SHA1 Fingerprint: 09:3c:61:f3:8b:8b:dc:7d:55:df:75:38:02:05:00:e1:25:f5:c8:36 +# SHA256 Fingerprint: 8f:e4:fb:0a:f9:3a:4d:0d:67:db:0b:eb:b2:3e:37:c7:1b:f3:25:dc:bc:dd:24:0e:a0:4d:af:58:b4:7e:18:40 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 3 G3" +# Serial: 268090761170461462463995952157327242137089239581 +# MD5 Fingerprint: df:7d:b9:ad:54:6f:68:a1:df:89:57:03:97:43:b0:d7 +# SHA1 Fingerprint: 48:12:bd:92:3c:a8:c4:39:06:e7:30:6d:27:96:e6:a4:cf:22:2e:7d +# SHA256 Fingerprint: 88:ef:81:de:20:2e:b0:18:45:2e:43:f8:64:72:5c:ea:5f:bd:1f:c2:d9:d2:05:73:07:09:c5:d8:b8:69:0f:46 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G2" +# Serial: 15385348160840213938643033620894905419 +# MD5 Fingerprint: 92:38:b9:f8:63:24:82:65:2c:57:33:e6:fe:81:8f:9d +# SHA1 Fingerprint: a1:4b:48:d9:43:ee:0a:0e:40:90:4f:3c:e0:a4:c0:91:93:51:5d:3f +# SHA256 Fingerprint: 7d:05:eb:b6:82:33:9f:8c:94:51:ee:09:4e:eb:fe:fa:79:53:a1:14:ed:b2:f4:49:49:45:2f:ab:7d:2f:c1:85 +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G3" +# Serial: 15459312981008553731928384953135426796 +# MD5 Fingerprint: 7c:7f:65:31:0c:81:df:8d:ba:3e:99:e2:5c:ad:6e:fb +# SHA1 Fingerprint: f5:17:a2:4f:9a:48:c6:c9:f8:a2:00:26:9f:dc:0f:48:2c:ab:30:89 +# SHA256 Fingerprint: 7e:37:cb:8b:4c:47:09:0c:ab:36:55:1b:a6:f4:5d:b8:40:68:0f:ba:16:6a:95:2d:b1:00:71:7f:43:05:3f:c2 +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G2" +# Serial: 4293743540046975378534879503202253541 +# MD5 Fingerprint: e4:a6:8a:c8:54:ac:52:42:46:0a:fd:72:48:1b:2a:44 +# SHA1 Fingerprint: df:3c:24:f9:bf:d6:66:76:1b:26:80:73:fe:06:d1:cc:8d:4f:82:a4 +# SHA256 Fingerprint: cb:3c:cb:b7:60:31:e5:e0:13:8f:8d:d3:9a:23:f9:de:47:ff:c3:5e:43:c1:14:4c:ea:27:d4:6a:5a:b1:cb:5f +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G3" +# Serial: 7089244469030293291760083333884364146 +# MD5 Fingerprint: f5:5d:a4:50:a5:fb:28:7e:1e:0f:0d:cc:96:57:56:ca +# SHA1 Fingerprint: 7e:04:de:89:6a:3e:66:6d:00:e6:87:d3:3f:fa:d9:3b:e8:3d:34:9e +# SHA256 Fingerprint: 31:ad:66:48:f8:10:41:38:c7:38:f3:9e:a4:32:01:33:39:3e:3a:18:cc:02:29:6e:f9:7c:2a:c9:ef:67:31:d0 +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Trusted Root G4" +# Serial: 7451500558977370777930084869016614236 +# MD5 Fingerprint: 78:f2:fc:aa:60:1f:2f:b4:eb:c9:37:ba:53:2e:75:49 +# SHA1 Fingerprint: dd:fb:16:cd:49:31:c9:73:a2:03:7d:3f:c8:3a:4d:7d:77:5d:05:e4 +# SHA256 Fingerprint: 55:2f:7b:dc:f1:a7:af:9e:6c:e6:72:01:7f:4f:12:ab:f7:72:40:c7:8e:76:1a:c2:03:d1:d9:d2:0a:c8:99:88 +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE----- + +# Issuer: CN=COMODO RSA Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO RSA Certification Authority O=COMODO CA Limited +# Label: "COMODO RSA Certification Authority" +# Serial: 101909084537582093308941363524873193117 +# MD5 Fingerprint: 1b:31:b0:71:40:36:cc:14:36:91:ad:c4:3e:fd:ec:18 +# SHA1 Fingerprint: af:e5:d2:44:a8:d1:19:42:30:ff:47:9f:e2:f8:97:bb:cd:7a:8c:b4 +# SHA256 Fingerprint: 52:f0:e1:c4:e5:8e:c6:29:29:1b:60:31:7f:07:46:71:b8:5d:7e:a8:0d:5b:07:27:34:63:53:4b:32:b4:02:34 +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE----- + +# Issuer: CN=USERTrust RSA Certification Authority O=The USERTRUST Network +# Subject: CN=USERTrust RSA Certification Authority O=The USERTRUST Network +# Label: "USERTrust RSA Certification Authority" +# Serial: 2645093764781058787591871645665788717 +# MD5 Fingerprint: 1b:fe:69:d1:91:b7:19:33:a3:72:a8:0f:e1:55:e5:b5 +# SHA1 Fingerprint: 2b:8f:1b:57:33:0d:bb:a2:d0:7a:6c:51:f7:0e:e9:0d:da:b9:ad:8e +# SHA256 Fingerprint: e7:93:c9:b0:2f:d8:aa:13:e2:1c:31:22:8a:cc:b0:81:19:64:3b:74:9c:89:89:64:b1:74:6d:46:c3:d4:cb:d2 +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +# Issuer: CN=USERTrust ECC Certification Authority O=The USERTRUST Network +# Subject: CN=USERTrust ECC Certification Authority O=The USERTRUST Network +# Label: "USERTrust ECC Certification Authority" +# Serial: 123013823720199481456569720443997572134 +# MD5 Fingerprint: fa:68:bc:d9:b5:7f:ad:fd:c9:1d:06:83:28:cc:24:c1 +# SHA1 Fingerprint: d1:cb:ca:5d:b2:d5:2a:7f:69:3b:67:4d:e5:f0:5a:1d:0c:95:7d:f0 +# SHA256 Fingerprint: 4f:f4:60:d5:4b:9c:86:da:bf:bc:fc:57:12:e0:40:0d:2b:ed:3f:bc:4d:4f:bd:aa:86:e0:6a:dc:d2:a9:ad:7a +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 +# Label: "GlobalSign ECC Root CA - R5" +# Serial: 32785792099990507226680698011560947931244 +# MD5 Fingerprint: 9f:ad:3b:1c:02:1e:8a:ba:17:74:38:81:0c:a2:bc:08 +# SHA1 Fingerprint: 1f:24:c6:30:cd:a4:18:ef:20:69:ff:ad:4f:dd:5f:46:3a:1b:69:aa +# SHA256 Fingerprint: 17:9f:bc:14:8a:3d:d0:0f:d2:4e:a1:34:58:cc:43:bf:a7:f5:9c:81:82:d7:83:a5:13:f6:eb:ec:10:0c:89:24 +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +# Issuer: CN=IdenTrust Commercial Root CA 1 O=IdenTrust +# Subject: CN=IdenTrust Commercial Root CA 1 O=IdenTrust +# Label: "IdenTrust Commercial Root CA 1" +# Serial: 13298821034946342390520003877796839426 +# MD5 Fingerprint: b3:3e:77:73:75:ee:a0:d3:e3:7e:49:63:49:59:bb:c7 +# SHA1 Fingerprint: df:71:7e:aa:4a:d9:4e:c9:55:84:99:60:2d:48:de:5f:bc:f0:3a:25 +# SHA256 Fingerprint: 5d:56:49:9b:e4:d2:e0:8b:cf:ca:d0:8a:3e:38:72:3d:50:50:3b:de:70:69:48:e4:2f:55:60:30:19:e5:28:ae +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu +VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw +MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw +JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT +3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU ++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp +S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 +bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi +T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL +vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK +Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK +dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT +c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv +l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N +iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD +ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt +LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 +nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 ++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK +W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT +AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq +l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG +4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ +mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A +7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +# Issuer: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust +# Subject: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust +# Label: "IdenTrust Public Sector Root CA 1" +# Serial: 13298821034946342390521976156843933698 +# MD5 Fingerprint: 37:06:a5:b0:fc:89:9d:ba:f4:6b:8c:1a:64:cd:d5:ba +# SHA1 Fingerprint: ba:29:41:60:77:98:3f:f4:f3:ef:f2:31:05:3b:2e:ea:6d:4d:45:fd +# SHA256 Fingerprint: 30:d0:89:5a:9a:44:8a:26:20:91:63:55:22:d1:f5:20:10:b5:86:7a:ca:e1:2c:78:ef:95:8f:d4:f4:38:9f:2f +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu +VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN +MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 +MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 +ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy +RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS +bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF +/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R +3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw +EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy +9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V +GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ +2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV +WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD +W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN +AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV +DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 +TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G +lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW +mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df +WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 ++bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ +tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA +GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv +8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +# Issuer: CN=CFCA EV ROOT O=China Financial Certification Authority +# Subject: CN=CFCA EV ROOT O=China Financial Certification Authority +# Label: "CFCA EV ROOT" +# Serial: 407555286 +# MD5 Fingerprint: 74:e1:b6:ed:26:7a:7a:44:30:33:94:ab:7b:27:81:30 +# SHA1 Fingerprint: e2:b8:29:4b:55:84:ab:6b:58:c2:90:46:6c:ac:3f:b8:39:8f:84:83 +# SHA256 Fingerprint: 5c:c3:d7:8e:4e:1d:5e:45:54:7a:04:e6:87:3e:64:f9:0c:f9:53:6d:1c:cc:2e:f8:00:f3:55:c4:c5:fd:70:fd +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD +TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y +aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx +MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j +aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP +T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 +sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL +TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 +/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp +7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz +EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt +hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP +a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot +aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg +TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV +PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv +cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL +tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT +ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL +jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS +ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy +P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 +xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d +Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN +5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe +/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z +AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ +5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +# Issuer: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed +# Subject: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed +# Label: "OISTE WISeKey Global Root GB CA" +# Serial: 157768595616588414422159278966750757568 +# MD5 Fingerprint: a4:eb:b9:61:28:2e:b7:2f:98:b0:35:26:90:99:51:1d +# SHA1 Fingerprint: 0f:f9:40:76:18:d3:d7:6a:4b:98:f0:a8:35:9e:0c:fd:27:ac:cc:ed +# SHA256 Fingerprint: 6b:9c:08:e8:6e:b0:f7:67:cf:ad:65:cd:98:b6:21:49:e5:49:4a:67:f5:84:5e:7b:d1:ed:01:9f:27:b8:6b:d6 +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt +MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg +Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i +YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x +CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG +b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 +HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx +WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX +1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk +u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P +99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r +M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB +BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh +cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 +gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO +ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf +aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- + +# Issuer: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. +# Subject: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. +# Label: "SZAFIR ROOT CA2" +# Serial: 357043034767186914217277344587386743377558296292 +# MD5 Fingerprint: 11:64:c1:89:b0:24:b1:8c:b1:07:7e:89:9e:51:9e:99 +# SHA1 Fingerprint: e2:52:fa:95:3f:ed:db:24:60:bd:6e:28:f3:9c:cc:cf:5e:b3:3f:de +# SHA256 Fingerprint: a1:33:9d:33:28:1a:0b:56:e5:57:d3:d3:2b:1c:e7:f9:36:7e:b0:94:bd:5f:a7:2a:7e:50:04:c8:de:d7:ca:fe +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 +ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw +NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L +cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg +Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN +QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT +3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw +3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 +3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 +BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN +XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF +AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw +8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG +nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP +oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy +d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg +LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Network CA 2" +# Serial: 44979900017204383099463764357512596969 +# MD5 Fingerprint: 6d:46:9e:d9:25:6d:08:23:5b:5e:74:7d:1e:27:db:f2 +# SHA1 Fingerprint: d3:dd:48:3e:2b:bf:4c:05:e8:af:10:f5:fa:76:26:cf:d3:dc:30:92 +# SHA256 Fingerprint: b6:76:f2:ed:da:e8:77:5c:d3:6c:b0:f6:3c:d1:d4:60:39:61:f4:9e:62:65:ba:01:3a:2f:03:07:b6:d0:b8:04 +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB +gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu +QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG +A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz +OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ +VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 +b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA +DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn +0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB +OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE +fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E +Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m +o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i +sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW +OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez +Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS +adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n +3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ +F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf +CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 +XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm +djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ +WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb +AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq +P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko +b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj +XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P +5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi +DrW5viSP +-----END CERTIFICATE----- + +# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Subject: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Label: "Hellenic Academic and Research Institutions RootCA 2015" +# Serial: 0 +# MD5 Fingerprint: ca:ff:e2:db:03:d9:cb:4b:e9:0f:ad:84:fd:7b:18:ce +# SHA1 Fingerprint: 01:0c:06:95:a6:98:19:14:ff:bf:5f:c6:b0:b6:95:ea:29:e9:12:a6 +# SHA256 Fingerprint: a0:40:92:9a:02:ce:53:b4:ac:f4:f2:ff:c6:98:1c:e4:49:6f:75:5e:6d:45:fe:0b:2a:69:2b:cd:52:52:3f:36 +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix +DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k +IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT +N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v +dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG +A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh +ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx +QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA +4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 +AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 +4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C +ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV +9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD +gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 +Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq +NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko +LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd +ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I +XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI +M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot +9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V +Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea +j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh +X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ +l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf +bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 +pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK +e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 +vm9qp/UsQu0yrbYhnr68 +-----END CERTIFICATE----- + +# Issuer: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Subject: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Label: "Hellenic Academic and Research Institutions ECC RootCA 2015" +# Serial: 0 +# MD5 Fingerprint: 81:e5:b4:17:eb:c2:f5:e1:4b:0d:41:7b:49:92:fe:ef +# SHA1 Fingerprint: 9f:f1:71:8d:92:d5:9a:f3:7d:74:97:b4:bc:6f:84:68:0b:ba:b6:66 +# SHA256 Fingerprint: 44:b5:45:aa:8a:25:e6:5a:73:ca:15:dc:27:fc:36:d2:4c:1c:b9:95:3a:06:65:39:b1:15:82:dc:48:7b:48:33 +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN +BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl +bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv +b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ +BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj +YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 +MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 +dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg +QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa +jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi +C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep +lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof +TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- + +# Issuer: CN=ISRG Root X1 O=Internet Security Research Group +# Subject: CN=ISRG Root X1 O=Internet Security Research Group +# Label: "ISRG Root X1" +# Serial: 172886928669790476064670243504169061120 +# MD5 Fingerprint: 0c:d2:f9:e0:da:17:73:e9:ed:86:4d:a5:e3:70:e7:4e +# SHA1 Fingerprint: ca:bd:2a:79:a1:07:6a:31:f2:1d:25:36:35:cb:03:9d:43:29:a5:e8 +# SHA256 Fingerprint: 96:bc:ec:06:26:49:76:f3:74:60:77:9a:cf:28:c5:a7:cf:e8:a3:c0:aa:e1:1a:8f:fc:ee:05:c0:bd:df:08:c6 +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- + +# Issuer: O=FNMT-RCM OU=AC RAIZ FNMT-RCM +# Subject: O=FNMT-RCM OU=AC RAIZ FNMT-RCM +# Label: "AC RAIZ FNMT-RCM" +# Serial: 485876308206448804701554682760554759 +# MD5 Fingerprint: e2:09:04:b4:d3:bd:d1:a0:14:fd:1a:d2:47:c4:57:1d +# SHA1 Fingerprint: ec:50:35:07:b2:15:c4:95:62:19:e2:a8:9a:5b:42:99:2c:4c:2c:20 +# SHA256 Fingerprint: eb:c5:57:0c:29:01:8c:4d:67:b1:aa:12:7b:af:12:f7:03:b4:61:1e:bc:17:b7:da:b5:57:38:94:17:9b:93:fa +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx +CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ +WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ +BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG +Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ +yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf +BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz +WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF +tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z +374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC +IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL +mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 +wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS +MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 +ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet +UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H +YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 +LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 +RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM +LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf +77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N +JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm +fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp +6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp +1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B +9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok +RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv +uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 1 O=Amazon +# Subject: CN=Amazon Root CA 1 O=Amazon +# Label: "Amazon Root CA 1" +# Serial: 143266978916655856878034712317230054538369994 +# MD5 Fingerprint: 43:c6:bf:ae:ec:fe:ad:2f:18:c6:88:68:30:fc:c8:e6 +# SHA1 Fingerprint: 8d:a7:f9:65:ec:5e:fc:37:91:0f:1c:6e:59:fd:c1:cc:6a:6e:de:16 +# SHA256 Fingerprint: 8e:cd:e6:88:4f:3d:87:b1:12:5b:a3:1a:c3:fc:b1:3d:70:16:de:7f:57:cc:90:4f:e1:cb:97:c6:ae:98:19:6e +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 2 O=Amazon +# Subject: CN=Amazon Root CA 2 O=Amazon +# Label: "Amazon Root CA 2" +# Serial: 143266982885963551818349160658925006970653239 +# MD5 Fingerprint: c8:e5:8d:ce:a8:42:e2:7a:c0:2a:5c:7c:9e:26:bf:66 +# SHA1 Fingerprint: 5a:8c:ef:45:d7:a6:98:59:76:7a:8c:8b:44:96:b5:78:cf:47:4b:1a +# SHA256 Fingerprint: 1b:a5:b2:aa:8c:65:40:1a:82:96:01:18:f8:0b:ec:4f:62:30:4d:83:ce:c4:71:3a:19:c3:9c:01:1e:a4:6d:b4 +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK +gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ +W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg +1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K +8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r +2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me +z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR +8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj +mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz +7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 ++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI +0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm +UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 +LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS +k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl +7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm +btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl +urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ +fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 +n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE +76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H +9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT +4PsJYGw= +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 3 O=Amazon +# Subject: CN=Amazon Root CA 3 O=Amazon +# Label: "Amazon Root CA 3" +# Serial: 143266986699090766294700635381230934788665930 +# MD5 Fingerprint: a0:d4:ef:0b:f7:b5:d8:49:95:2a:ec:f5:c4:fc:81:87 +# SHA1 Fingerprint: 0d:44:dd:8c:3c:8c:1a:1a:58:75:64:81:e9:0f:2e:2a:ff:b3:d2:6e +# SHA256 Fingerprint: 18:ce:6c:fe:7b:f1:4e:60:b2:e3:47:b8:df:e8:68:cb:31:d0:2e:bb:3a:da:27:15:69:f5:03:43:b4:6d:b3:a4 +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl +ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr +ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr +BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM +YyRIHN8wfdVoOw== +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 4 O=Amazon +# Subject: CN=Amazon Root CA 4 O=Amazon +# Label: "Amazon Root CA 4" +# Serial: 143266989758080763974105200630763877849284878 +# MD5 Fingerprint: 89:bc:27:d5:eb:17:8d:06:6a:69:d5:fd:89:47:b4:cd +# SHA1 Fingerprint: f6:10:84:07:d6:f8:bb:67:98:0c:c2:e2:44:c2:eb:ae:1c:ef:63:be +# SHA256 Fingerprint: e3:5d:28:41:9e:d0:20:25:cf:a6:90:38:cd:62:39:62:45:8d:a5:c6:95:fb:de:a3:c2:2b:0b:fb:25:89:70:92 +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi +9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk +M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB +MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw +CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW +1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- + +# Issuer: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM +# Subject: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM +# Label: "TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1" +# Serial: 1 +# MD5 Fingerprint: dc:00:81:dc:69:2f:3e:2f:b0:3b:f6:3d:5a:91:8e:49 +# SHA1 Fingerprint: 31:43:64:9b:ec:ce:27:ec:ed:3a:3f:0b:8f:0d:e4:e8:91:dd:ee:ca +# SHA256 Fingerprint: 46:ed:c3:68:90:46:d5:3a:45:3f:b3:10:4a:b8:0d:ca:ec:65:8b:26:60:ea:16:29:dd:7e:86:79:90:64:87:16 +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx +GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp +bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w +KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 +BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy +dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG +EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll +IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU +QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT +TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg +LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 +a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr +LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr +N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X +YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ +iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f +AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH +V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf +IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 +lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c +8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf +lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- + +# Issuer: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. +# Subject: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. +# Label: "GDCA TrustAUTH R5 ROOT" +# Serial: 9009899650740120186 +# MD5 Fingerprint: 63:cc:d9:3d:34:35:5c:6f:53:a3:e2:08:70:48:1f:b4 +# SHA1 Fingerprint: 0f:36:38:5b:81:1a:25:c3:9b:31:4e:83:ca:e9:34:66:70:cc:74:b4 +# SHA256 Fingerprint: bf:ff:8f:d0:44:33:48:7d:6a:8a:a6:0c:1a:29:76:7a:9f:c2:bb:b0:5e:42:0f:71:3a:13:b9:92:89:1d:38:93 +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE +BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0 +MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV +BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w +HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj +Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj +TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u +KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj +qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm +MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12 +ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP +zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk +L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC +jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA +HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC +AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm +DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5 +COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry +L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf +JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg +IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io +2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV +09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ +XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq +T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe +MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com Root Certification Authority RSA O=SSL Corporation +# Subject: CN=SSL.com Root Certification Authority RSA O=SSL Corporation +# Label: "SSL.com Root Certification Authority RSA" +# Serial: 8875640296558310041 +# MD5 Fingerprint: 86:69:12:c0:70:f1:ec:ac:ac:c2:d5:bc:a5:5b:a1:29 +# SHA1 Fingerprint: b7:ab:33:08:d1:ea:44:77:ba:14:80:12:5a:6f:bd:a9:36:49:0c:bb +# SHA256 Fingerprint: 85:66:6a:56:2e:e0:be:5c:e9:25:c1:d8:89:0a:6f:76:a8:7e:c1:6d:4d:7d:5f:29:ea:74:19:cf:20:12:3b:69 +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE +BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK +DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz +OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R +xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX +qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC +C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 +6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh +/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF +YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E +JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc +US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 +ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm ++Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi +M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G +A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV +cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc +Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs +PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ +q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 +cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr +a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I +H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y +K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu +nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf +oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY +Ic2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com Root Certification Authority ECC O=SSL Corporation +# Subject: CN=SSL.com Root Certification Authority ECC O=SSL Corporation +# Label: "SSL.com Root Certification Authority ECC" +# Serial: 8495723813297216424 +# MD5 Fingerprint: 2e:da:e4:39:7f:9c:8f:37:d1:70:9f:26:17:51:3a:8e +# SHA1 Fingerprint: c3:19:7c:39:24:e6:54:af:1b:c4:ab:20:95:7a:e2:c3:0e:13:02:6a +# SHA256 Fingerprint: 34:17:bb:06:cc:60:07:da:1b:96:1c:92:0b:8a:b4:ce:3f:ad:82:0e:4a:a3:0b:9a:cb:c4:a7:4e:bd:ce:bc:65 +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz +WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 +b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS +b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI +7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg +CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD +VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T +kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ +gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation +# Subject: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation +# Label: "SSL.com EV Root Certification Authority RSA R2" +# Serial: 6248227494352943350 +# MD5 Fingerprint: e1:1e:31:58:1a:ae:54:53:02:f6:17:6a:11:7b:4d:95 +# SHA1 Fingerprint: 74:3a:f0:52:9b:d0:32:a0:f4:4a:83:cd:d4:ba:a9:7b:7c:2e:c4:9a +# SHA256 Fingerprint: 2e:7b:f1:6c:c2:24:85:a7:bb:e2:aa:86:96:75:07:61:b0:ae:39:be:3b:2f:e9:d0:cc:6d:4e:f7:34:91:42:5c +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV +BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE +CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy +MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G +A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD +DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq +M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf +OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa +4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 +HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR +aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA +b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ +Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV +PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO +pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu +UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY +MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 +9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW +s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 +Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg +cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM +79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz +/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt +ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm +Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK +QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ +w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi +S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 +mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation +# Subject: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation +# Label: "SSL.com EV Root Certification Authority ECC" +# Serial: 3182246526754555285 +# MD5 Fingerprint: 59:53:22:65:83:42:01:54:c0:ce:42:b9:5a:7c:f2:90 +# SHA1 Fingerprint: 4c:dd:51:a3:d1:f5:20:32:14:b0:c6:c5:32:23:03:91:c7:46:42:6d +# SHA256 Fingerprint: 22:a2:c1:f7:bd:ed:70:4c:c1:e7:01:b5:f4:08:c3:10:88:0f:e9:56:b5:de:2a:4a:44:f9:9c:87:3a:25:a7:c8 +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx +NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv +bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA +VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku +WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX +5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ +ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg +h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 +# Label: "GlobalSign Root CA - R6" +# Serial: 1417766617973444989252670301619537 +# MD5 Fingerprint: 4f:dd:07:e4:d4:22:64:39:1e:0c:37:42:ea:d1:c6:ae +# SHA1 Fingerprint: 80:94:64:0e:b5:a7:a1:ca:11:9c:1f:dd:d5:9f:81:02:63:a7:fb:d1 +# SHA256 Fingerprint: 2c:ab:ea:fe:37:d0:6c:a2:2a:ba:73:91:c0:03:3d:25:98:29:52:c4:53:64:73:49:76:3a:3a:b5:ad:6c:cf:69 +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg +MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx +MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET +MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI +xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k +ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD +aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw +LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw +1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX +k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 +SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h +bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n +WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY +rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce +MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu +bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt +Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 +55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj +vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf +cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz +oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp +nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs +pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v +JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R +8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 +5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- + +# Issuer: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed +# Subject: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed +# Label: "OISTE WISeKey Global Root GC CA" +# Serial: 44084345621038548146064804565436152554 +# MD5 Fingerprint: a9:d6:b9:2d:2f:93:64:f8:a5:69:ca:91:e9:68:07:23 +# SHA1 Fingerprint: e0:11:84:5e:34:de:be:88:81:b9:9c:f6:16:26:d1:96:1f:c3:b9:31 +# SHA256 Fingerprint: 85:60:f9:1c:36:24:da:ba:95:70:b5:fe:a0:db:e3:6f:f1:1a:83:23:be:94:86:85:4f:b3:f3:4a:55:71:19:8d +-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw +CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 +bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg +Um9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ +BgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu +ZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS +b290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni +eUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W +p2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T +rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV +57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg +Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE----- + +# Issuer: CN=UCA Global G2 Root O=UniTrust +# Subject: CN=UCA Global G2 Root O=UniTrust +# Label: "UCA Global G2 Root" +# Serial: 124779693093741543919145257850076631279 +# MD5 Fingerprint: 80:fe:f0:c4:4a:f0:5c:62:32:9f:1c:ba:78:a9:50:f8 +# SHA1 Fingerprint: 28:f9:78:16:19:7a:ff:18:25:18:aa:44:fe:c1:a0:ce:5c:b6:4c:8a +# SHA256 Fingerprint: 9b:ea:11:c9:76:fe:01:47:64:c1:be:56:a6:f9:14:b5:a5:60:31:7a:bd:99:88:39:33:82:e5:16:1a:a0:49:3c +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9 +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBH +bG9iYWwgRzIgUm9vdDAeFw0xNjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0x +CzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlUcnVzdDEbMBkGA1UEAwwSVUNBIEds +b2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxeYr +b3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmToni9 +kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzm +VHqUwCoV8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/R +VogvGjqNO7uCEeBHANBSh6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDc +C/Vkw85DvG1xudLeJ1uK6NjGruFZfc8oLTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIj +tm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/R+zvWr9LesGtOxdQXGLY +D0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBeKW4bHAyv +j5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6Dl +NaBa4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6 +iIis7nCs+dwp4wwcOxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznP +O6Q0ibd5Ei9Hxeepl2n8pndntd978XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIHEjMz15DD/pQwIX4wV +ZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo5sOASD0Ee/oj +L3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 +1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl +1qnN3e92mI0ADs0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oU +b3n09tDh05S60FdRvScFDcH9yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LV +PtateJLbXDzz2K36uGt/xDYotgIVilQsnLAXc47QN6MUPJiVAAwpBVueSUmxX8fj +y88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHojhJi6IjMtX9Gl8Cb +EGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZkbxqg +DMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI ++Vg7RE+xygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGy +YiGqhkCyLmTTX8jjfhFnRR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bX +UB+K+wb1whnw0A== +-----END CERTIFICATE----- + +# Issuer: CN=UCA Extended Validation Root O=UniTrust +# Subject: CN=UCA Extended Validation Root O=UniTrust +# Label: "UCA Extended Validation Root" +# Serial: 106100277556486529736699587978573607008 +# MD5 Fingerprint: a1:f3:5f:43:c6:34:9b:da:bf:8c:7e:05:53:ad:96:e2 +# SHA1 Fingerprint: a3:a1:b0:6f:24:61:23:4a:e3:36:a5:c2:37:fc:a6:ff:dd:f0:d7:3a +# SHA256 Fingerprint: d4:3a:f9:b3:54:73:75:5c:96:84:fc:06:d7:d8:cb:70:ee:5c:28:e7:73:fb:29:4e:b4:1e:e7:17:22:92:4d:24 +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBH +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBF +eHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMx +MDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNV +BAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrsiWog +D4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvS +sPGP2KxFRv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aop +O2z6+I9tTcg1367r3CTueUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dk +sHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR59mzLC52LqGj3n5qiAno8geK+LLNEOfi +c0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH0mK1lTnj8/FtDw5lhIpj +VMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KRel7sFsLz +KuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/ +TuDvB0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41G +sx2VYVdWf6/wFlthWG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs +1+lvK9JKBZP8nm9rZ/+I8U6laUpSNwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQD +fwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS3H5aBZ8eNJr34RQwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBADaN +l8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR +ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQ +VBcZEhrxH9cMaVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5 +c6sq1WnIeJEmMX3ixzDx/BR4dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp +4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb+7lsq+KePRXBOy5nAliRn+/4Qh8s +t2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOWF3sGPjLtx7dCvHaj +2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwiGpWO +vpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2C +xR9GUeOcGMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmx +cmtpzyKEC2IPrNkZAJSidjzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbM +fjKaiJUINlK73nZfdklJrX+9ZSCyycErdhh2n1ax +-----END CERTIFICATE----- + +# Issuer: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 +# Subject: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 +# Label: "Certigna Root CA" +# Serial: 269714418870597844693661054334862075617 +# MD5 Fingerprint: 0e:5c:30:62:27:eb:5b:bc:d7:ae:62:ba:e9:d5:df:77 +# SHA1 Fingerprint: 2d:0d:52:14:ff:9e:ad:99:24:01:74:20:47:6e:6c:85:27:27:f5:43 +# SHA256 Fingerprint: d4:8d:3d:23:ee:db:50:a4:59:e5:51:97:60:1c:27:77:4b:9d:7b:18:c9:4d:5a:05:95:11:a1:02:50:b9:31:68 +-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw +WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw +MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x +MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD +VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX +BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO +ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M +CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu +I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm +TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh +C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf +ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz +IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT +Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k +JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5 +hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB +GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov +L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo +dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr +aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq +hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L +6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG +HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6 +0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB +lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi +o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1 +gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v +faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63 +Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh +jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw +3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE----- + +# Issuer: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI +# Subject: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI +# Label: "emSign Root CA - G1" +# Serial: 235931866688319308814040 +# MD5 Fingerprint: 9c:42:84:57:dd:cb:0b:a7:2e:95:ad:b6:f3:da:bc:ac +# SHA1 Fingerprint: 8a:c7:ad:8f:73:ac:4e:c1:b5:75:4d:a5:40:f4:fc:cf:7c:b5:8e:8c +# SHA256 Fingerprint: 40:f6:af:03:46:a9:9a:a1:cd:1d:55:5a:4e:9c:ce:62:c7:f9:63:46:03:ee:40:66:15:83:3d:c8:c8:d0:03:67 +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD +VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU +ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH +MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO +MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv +Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz +f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO +8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq +d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM +tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt +Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB +o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD +AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x +PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM +wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d +GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH +6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby +RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE----- + +# Issuer: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI +# Subject: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI +# Label: "emSign ECC Root CA - G3" +# Serial: 287880440101571086945156 +# MD5 Fingerprint: ce:0b:72:d1:9f:88:8e:d0:50:03:e8:e3:b8:8b:67:40 +# SHA1 Fingerprint: 30:43:fa:4f:f2:57:dc:a0:c3:80:ee:2e:58:ea:78:b2:3f:e6:bb:c1 +# SHA256 Fingerprint: 86:a1:ec:ba:08:9c:4a:8d:3b:be:27:34:c6:12:ba:34:1d:81:3e:04:3c:f9:e8:a8:62:cd:5c:57:a3:6b:be:6b +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG +EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo +bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ +TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s +b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 +WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS +fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB +zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB +CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD ++JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- + +# Issuer: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI +# Subject: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI +# Label: "emSign Root CA - C1" +# Serial: 825510296613316004955058 +# MD5 Fingerprint: d8:e3:5d:01:21:fa:78:5a:b0:df:ba:d2:ee:2a:5f:68 +# SHA1 Fingerprint: e7:2e:f1:df:fc:b2:09:28:cf:5d:d4:d5:67:37:b1:51:cb:86:4f:01 +# SHA256 Fingerprint: 12:56:09:aa:30:1d:a0:a2:49:b9:7a:82:39:cb:6a:34:21:6f:44:dc:ac:9f:39:54:b1:42:92:f2:e8:c8:60:8f +-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG +A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg +SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v +dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ +BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ +HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH +3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH +GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c +xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1 +aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq +TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87 +/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4 +kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG +YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT ++xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo +WXzhriKi4gp6D/piq1JM4fHfyr6DDUI= +-----END CERTIFICATE----- + +# Issuer: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI +# Subject: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI +# Label: "emSign ECC Root CA - C3" +# Serial: 582948710642506000014504 +# MD5 Fingerprint: 3e:53:b3:a3:81:ee:d7:10:f8:d3:b0:1d:17:92:f5:d5 +# SHA1 Fingerprint: b6:af:43:c2:9b:81:53:7d:f6:ef:6b:c3:1f:1f:60:15:0c:ee:48:66 +# SHA256 Fingerprint: bc:4d:80:9b:15:18:9d:78:db:3e:1d:8c:f4:f9:72:6a:79:5d:a1:64:3c:a5:f1:35:8e:1d:db:0e:dc:0d:7e:b3 +-----BEGIN CERTIFICATE----- +MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG +EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx +IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND +IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci +MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti +sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O +BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB +Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c +3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J +0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== +-----END CERTIFICATE----- + +# Issuer: CN=Hongkong Post Root CA 3 O=Hongkong Post +# Subject: CN=Hongkong Post Root CA 3 O=Hongkong Post +# Label: "Hongkong Post Root CA 3" +# Serial: 46170865288971385588281144162979347873371282084 +# MD5 Fingerprint: 11:fc:9f:bd:73:30:02:8a:fd:3f:f3:58:b9:cb:20:f0 +# SHA1 Fingerprint: 58:a2:d0:ec:20:52:81:5b:c1:f3:f8:64:02:24:4e:c2:8e:02:4b:02 +# SHA256 Fingerprint: 5a:2f:c0:3f:0c:83:b0:90:bb:fa:40:60:4b:09:88:44:6c:76:36:18:3d:f9:84:6e:17:10:1a:44:7f:b8:ef:d6 +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL +BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ +SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n +a29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5 +NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT +CUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u +Z2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO +dem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI +VoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV +9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY +2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY +vLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt +bNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb +x39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+ +l2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK +TE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj +Hno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e +i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw +DQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG +7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk +MpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr +gZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk +GMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS +3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm +Ozj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+ +l6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c +JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP +L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa +LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG +mpv0 +-----END CERTIFICATE----- + +# Issuer: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation +# Subject: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation +# Label: "Microsoft ECC Root Certificate Authority 2017" +# Serial: 136839042543790627607696632466672567020 +# MD5 Fingerprint: dd:a1:03:e6:4a:93:10:d1:bf:f0:19:42:cb:fe:ed:67 +# SHA1 Fingerprint: 99:9a:64:c3:7f:f4:7d:9f:ab:95:f1:47:69:89:14:60:ee:c4:c3:c5 +# SHA256 Fingerprint: 35:8d:f3:9d:76:4a:f9:e1:b7:66:e9:c9:72:df:35:2e:e1:5c:fa:c2:27:af:6a:d1:d7:0e:8e:4a:6e:dc:ba:02 +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD +VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw +MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy +b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR +ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb +hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 +FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV +L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB +iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- + +# Issuer: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation +# Subject: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation +# Label: "Microsoft RSA Root Certificate Authority 2017" +# Serial: 40975477897264996090493496164228220339 +# MD5 Fingerprint: 10:ff:00:ff:cf:c9:f8:c7:7a:c0:ee:35:8e:c9:0f:47 +# SHA1 Fingerprint: 73:a5:e6:4a:3b:ff:83:16:ff:0e:dc:cc:61:8a:90:6e:4e:ae:4d:74 +# SHA256 Fingerprint: c7:41:f7:0f:4b:2a:8d:88:bf:2e:71:c1:41:22:ef:53:ef:10:eb:a0:cf:a5:e6:4c:fa:20:f4:18:85:30:73:e0 +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N +aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ +Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 +ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 +HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm +gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ +jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc +aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG +YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 +W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K +UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH ++FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q +W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC +LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC +gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 +tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh +SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 +TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 +pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR +xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp +GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 +dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN +AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB +RA+GsCyRxj3qrg+E +-----END CERTIFICATE----- + +# Issuer: CN=e-Szigno Root CA 2017 O=Microsec Ltd. +# Subject: CN=e-Szigno Root CA 2017 O=Microsec Ltd. +# Label: "e-Szigno Root CA 2017" +# Serial: 411379200276854331539784714 +# MD5 Fingerprint: de:1f:f6:9e:84:ae:a7:b4:21:ce:1e:58:7d:d1:84:98 +# SHA1 Fingerprint: 89:d4:83:03:4f:9e:9a:48:80:5f:72:37:d4:a9:a6:ef:cb:7c:1f:d1 +# SHA256 Fingerprint: be:b0:0b:30:83:9b:9b:c3:2c:32:e4:44:79:05:95:06:41:f2:64:21:b1:5e:d0:89:19:8b:51:8a:e2:ea:1b:99 +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV +BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk +LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv +b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ +BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg +THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v +IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv +xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H +Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB +eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo +jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ ++efcMQ== +-----END CERTIFICATE----- + +# Issuer: O=CERTSIGN SA OU=certSIGN ROOT CA G2 +# Subject: O=CERTSIGN SA OU=certSIGN ROOT CA G2 +# Label: "certSIGN Root CA G2" +# Serial: 313609486401300475190 +# MD5 Fingerprint: 8c:f1:75:8a:c6:19:cf:94:b7:f7:65:20:87:c3:97:c7 +# SHA1 Fingerprint: 26:f9:93:b4:ed:3d:28:27:b0:b9:4b:a7:e9:15:1d:a3:8d:92:e5:32 +# SHA256 Fingerprint: 65:7c:fe:2f:a7:3f:aa:38:46:25:71:f3:32:a2:36:3a:46:fc:e7:02:09:51:71:07:02:cd:fb:b6:ee:da:33:05 +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV +BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g +Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ +BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ +R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF +dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw +vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ +uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp +n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs +cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW +xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P +rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF +DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx +DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy +LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C +eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ +d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq +kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl +qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0 +OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c +NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk +ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO +pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj +03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk +PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE +1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX +QRBdJ3NghVdJIgc= +-----END CERTIFICATE----- + +# Issuer: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. +# Subject: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. +# Label: "NAVER Global Root Certification Authority" +# Serial: 9013692873798656336226253319739695165984492813 +# MD5 Fingerprint: c8:7e:41:f6:25:3b:f5:09:b3:17:e8:46:3d:bf:d0:9b +# SHA1 Fingerprint: 8f:6b:f2:a9:27:4a:da:14:a0:c4:f4:8e:61:27:f9:c0:1e:78:5d:d1 +# SHA256 Fingerprint: 88:f4:38:dc:f8:ff:d1:fa:8f:42:91:15:ff:e5:f8:2a:e1:e0:6e:0c:70:c3:75:fa:ad:71:7b:34:a4:9e:72:65 +-----BEGIN CERTIFICATE----- +MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM +BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG +T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx +CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD +b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA +iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH +38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE +HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz +kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP +szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq +vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf +nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG +YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo +0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a +CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K +AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I +36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN +qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj +cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm ++LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL +hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe +lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7 +p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8 +piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR +LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX +5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO +dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul +9XXeifdy +-----END CERTIFICATE----- + +# Issuer: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres +# Subject: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres +# Label: "AC RAIZ FNMT-RCM SERVIDORES SEGUROS" +# Serial: 131542671362353147877283741781055151509 +# MD5 Fingerprint: 19:36:9c:52:03:2f:d2:d1:bb:23:cc:dd:1e:12:55:bb +# SHA1 Fingerprint: 62:ff:d9:9e:c0:65:0d:03:ce:75:93:d2:ed:3f:2d:32:c9:e3:e5:4a +# SHA256 Fingerprint: 55:41:53:b1:3d:2c:f9:dd:b7:53:bf:be:1a:4e:0a:e0:8d:0a:a4:18:70:58:fe:60:a2:b8:62:b2:e4:b8:7b:cb +-----BEGIN CERTIFICATE----- +MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw +CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw +FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S +Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 +MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL +DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS +QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH +sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK +Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu +SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC +MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy +v+c= +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign Root R46 O=GlobalSign nv-sa +# Subject: CN=GlobalSign Root R46 O=GlobalSign nv-sa +# Label: "GlobalSign Root R46" +# Serial: 1552617688466950547958867513931858518042577 +# MD5 Fingerprint: c4:14:30:e4:fa:66:43:94:2a:6a:1b:24:5f:19:d0:ef +# SHA1 Fingerprint: 53:a2:b0:4b:ca:6b:d6:45:e6:39:8a:8e:c4:0d:d2:bf:77:c3:a2:90 +# SHA256 Fingerprint: 4f:a3:12:6d:8d:3a:11:d1:c4:85:5a:4f:80:7c:ba:d6:cf:91:9d:3a:5a:88:b0:3b:ea:2c:63:72:d9:3c:40:c9 +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA +MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD +VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy +MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt +c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ +OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG +vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud +316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo +0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE +y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF +zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE ++cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN +I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs +x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa +ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC +4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4 +7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg +JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti +2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk +pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF +FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt +rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk +ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5 +u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP +4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 +N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 +vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign Root E46 O=GlobalSign nv-sa +# Subject: CN=GlobalSign Root E46 O=GlobalSign nv-sa +# Label: "GlobalSign Root E46" +# Serial: 1552617690338932563915843282459653771421763 +# MD5 Fingerprint: b5:b8:66:ed:de:08:83:e3:c9:e2:01:34:06:ac:51:6f +# SHA1 Fingerprint: 39:b4:6c:d5:fe:80:06:eb:e2:2f:4a:bb:08:33:a0:af:db:b9:dd:84 +# SHA256 Fingerprint: cb:b9:c4:4d:84:b8:04:3e:10:50:ea:31:a6:9f:51:49:55:d7:bf:d2:e2:c6:b4:93:01:01:9a:d6:1d:9f:50:58 +-----BEGIN CERTIFICATE----- +MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx +CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD +ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw +MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex +HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq +R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd +yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ +7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 ++RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= +-----END CERTIFICATE----- + +# Issuer: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz +# Subject: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz +# Label: "ANF Secure Server Root CA" +# Serial: 996390341000653745 +# MD5 Fingerprint: 26:a6:44:5a:d9:af:4e:2f:b2:1d:b6:65:b0:4e:e8:96 +# SHA1 Fingerprint: 5b:6e:68:d0:cc:15:b6:a0:5f:1e:c1:5f:ae:02:fc:6b:2f:5d:6f:74 +# SHA256 Fingerprint: fb:8f:ec:75:91:69:b9:10:6b:1e:51:16:44:c6:18:c5:13:04:37:3f:6c:06:43:08:8d:8b:ef:fd:1b:99:75:99 +-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV +BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk +YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV +BAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN +MzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF +UzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD +VQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v +dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj +cqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q +yGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH +2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX +H1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL +zc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR +p1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz +W7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/ +SiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn +LNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3 +n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B +u8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj +o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L +9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej +rw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK +pFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0 +vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq +OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ +/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9 +2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI ++PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2 +MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo +tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= +-----END CERTIFICATE----- + +# Issuer: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Subject: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Label: "Certum EC-384 CA" +# Serial: 160250656287871593594747141429395092468 +# MD5 Fingerprint: b6:65:b3:96:60:97:12:a1:ec:4e:e1:3d:a3:c6:c9:f1 +# SHA1 Fingerprint: f3:3e:78:3c:ac:df:f4:a2:cc:ac:67:55:69:56:d7:e5:16:3c:e1:ed +# SHA256 Fingerprint: 6b:32:80:85:62:53:18:aa:50:d1:73:c9:8d:8b:da:09:d5:7e:27:41:3d:11:4c:f7:87:a0:f5:d0:6c:03:0c:f6 +-----BEGIN CERTIFICATE----- +MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw +CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw +JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT +EENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0 +WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT +LkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX +BgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE +KI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm +Fy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8 +EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J +UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn +nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Root CA" +# Serial: 40870380103424195783807378461123655149 +# MD5 Fingerprint: 51:e1:c2:e7:fe:4c:84:af:59:0e:2f:f4:54:6f:ea:29 +# SHA1 Fingerprint: c8:83:44:c0:18:ae:9f:cc:f1:87:b7:8f:22:d1:c5:d7:45:84:ba:e5 +# SHA256 Fingerprint: fe:76:96:57:38:55:77:3e:37:a9:5e:7a:d4:d9:cc:96:c3:01:57:c1:5d:31:76:5b:a9:b1:57:04:e1:ae:78:fd +-----BEGIN CERTIFICATE----- +MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6 +MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu +MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV +BAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw +MzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg +U3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ +n0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q +p1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq +NwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF +8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3 +HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa +mqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi +7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF +ytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P +qafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ +v3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6 +Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 +vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD +ggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4 +WxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo +zMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR +5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ +GfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf +5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq +0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D +P78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM +qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP +0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf +E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb +-----END CERTIFICATE----- + +# Issuer: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique +# Subject: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique +# Label: "TunTrust Root CA" +# Serial: 108534058042236574382096126452369648152337120275 +# MD5 Fingerprint: 85:13:b9:90:5b:36:5c:b6:5e:b8:5a:f8:e0:31:57:b4 +# SHA1 Fingerprint: cf:e9:70:84:0f:e0:73:0f:9d:f6:0c:7f:2c:4b:ee:20:46:34:9c:bb +# SHA256 Fingerprint: 2e:44:10:2a:b5:8c:b8:54:19:45:1c:8e:19:d9:ac:f3:66:2c:af:bc:61:4b:6a:53:96:0a:30:f7:d0:e2:eb:41 +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg +Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv +b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG +EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u +IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ +n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd +2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF +VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ +GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF +li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU +r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2 +eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb +MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg +jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB +7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW +5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE +ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 +90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z +xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu +QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4 +FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH +22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP +xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn +dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5 +Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b +nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ +CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH +u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj +d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= +-----END CERTIFICATE----- + +# Issuer: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Subject: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Label: "HARICA TLS RSA Root CA 2021" +# Serial: 76817823531813593706434026085292783742 +# MD5 Fingerprint: 65:47:9b:58:86:dd:2c:f0:fc:a2:84:1f:1e:96:c4:91 +# SHA1 Fingerprint: 02:2d:05:82:fa:88:ce:14:0c:06:79:de:7f:14:10:e9:45:d7:a5:6d +# SHA256 Fingerprint: d9:5d:0e:8e:da:79:52:5b:f9:be:b1:1b:14:d2:10:0d:32:94:98:5f:0c:62:d9:fa:bd:9c:d9:99:ec:cb:7b:1d +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs +MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg +Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL +MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv +b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l +mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE +4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv +a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M +pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw +Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b +LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY +AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB +AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq +E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr +W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ +CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU +X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3 +f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja +H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP +JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P +zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt +jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0 +/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT +BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 +aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW +xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU +63ZTGI0RmLo= +-----END CERTIFICATE----- + +# Issuer: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Subject: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Label: "HARICA TLS ECC Root CA 2021" +# Serial: 137515985548005187474074462014555733966 +# MD5 Fingerprint: ae:f7:4c:e5:66:35:d1:b7:9b:8c:22:93:74:d3:4b:b0 +# SHA1 Fingerprint: bc:b0:c1:9d:e9:98:92:70:19:38:57:e9:8d:a7:b4:5d:6e:ee:01:48 +# SHA256 Fingerprint: 3f:99:cc:47:4a:cf:ce:4d:fe:d5:87:94:66:5e:47:8d:15:47:73:9f:2e:78:0f:1b:b4:ca:9b:13:30:97:d4:01 +-----BEGIN CERTIFICATE----- +MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw +CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh +cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v +dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG +A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg +Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7 +KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y +STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD +AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw +SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN +nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps +-----END CERTIFICATE----- + +# Issuer: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 +# Subject: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 +# Label: "Autoridad de Certificacion Firmaprofesional CIF A62634068" +# Serial: 1977337328857672817 +# MD5 Fingerprint: 4e:6e:9b:54:4c:ca:b7:fa:48:e4:90:b1:15:4b:1c:a3 +# SHA1 Fingerprint: 0b:be:c2:27:22:49:cb:39:aa:db:35:5c:53:e3:8c:ae:78:ff:b6:fe +# SHA256 Fingerprint: 57:de:05:83:ef:d2:b2:6e:03:61:da:99:da:9d:f4:64:8d:ef:7e:e8:44:1c:3b:72:8a:fa:9b:cd:e0:f9:b2:6a +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1 +MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1UdDgQWBBRlzeurNR4APn7VdMAc +tHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4wgZswgZgGBFUd +IAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j +b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABC +AG8AbgBhAG4AbwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAw +ADEANzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9m +iWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL4QjbEwj4KKE1soCzC1HA01aajTNF +Sa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDbLIpgD7dvlAceHabJ +hfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1ilI45P +Vf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZE +EAEeiGaPcjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV +1aUsIC+nmCjuRfzxuIgALI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2t +CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR +5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH9IBk9W6VULgRfhVwOEqw +f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9 +ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK +GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV +-----END CERTIFICATE----- + +# Issuer: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. +# Subject: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. +# Label: "vTrus ECC Root CA" +# Serial: 630369271402956006249506845124680065938238527194 +# MD5 Fingerprint: de:4b:c1:f5:52:8c:9b:43:e1:3e:8f:55:54:17:8d:85 +# SHA1 Fingerprint: f6:9c:db:b0:fc:f6:02:13:b6:52:32:a6:a3:91:3f:16:70:da:c3:e1 +# SHA256 Fingerprint: 30:fb:ba:2c:32:23:8e:2a:98:54:7a:f9:79:31:e5:50:42:8b:9b:3f:1c:8e:eb:66:33:dc:fa:86:c5:b2:7d:d3 +-----BEGIN CERTIFICATE----- +MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMw +RzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAY +BgNVBAMTEXZUcnVzIEVDQyBSb290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDcz +MTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28u +LEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+cToL0 +v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUd +e4BdS49nTPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIw +V53dVvHH4+m4SVBrm2nDb+zDfSXkV5UTQJtS0zvzQBm8JsctBp61ezaf9SXUY2sA +AjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQLYgmRWAD5Tfs0aNoJrSEG +GJTO +-----END CERTIFICATE----- + +# Issuer: CN=vTrus Root CA O=iTrusChina Co.,Ltd. +# Subject: CN=vTrus Root CA O=iTrusChina Co.,Ltd. +# Label: "vTrus Root CA" +# Serial: 387574501246983434957692974888460947164905180485 +# MD5 Fingerprint: b8:c9:37:df:fa:6b:31:84:64:c5:ea:11:6a:1b:75:fc +# SHA1 Fingerprint: 84:1a:69:fb:f5:cd:1a:25:34:13:3d:e3:f8:fc:b8:99:d0:c9:14:b7 +# SHA256 Fingerprint: 8a:71:de:65:59:33:6f:42:6c:26:e5:38:80:d0:0d:88:a1:8d:a4:c6:a9:1f:0d:cb:61:94:e2:06:c5:c9:63:87 +-----BEGIN CERTIFICATE----- +MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQEL +BQAwQzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4x +FjAUBgNVBAMTDXZUcnVzIFJvb3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMx +MDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoGA1UEChMTaVRydXNDaGluYSBDby4s +THRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZotsSKYc +IrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykU +AyyNJJrIZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+ +GrPSbcKvdmaVayqwlHeFXgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z9 +8Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KAYPxMvDVTAWqXcoKv8R1w6Jz1717CbMdH +flqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70kLJrxLT5ZOrpGgrIDajt +J8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2AXPKBlim +0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZN +pGvu/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQ +UqqzApVg+QxMaPnu1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHW +OXSuTEGC2/KmSNGzm/MzqvOmwMVO9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMB +AAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYgscasGrz2iTAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAKbqSSaet +8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd +nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1j +bhd47F18iMjrjld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvM +Kar5CKXiNxTKsbhm7xqC5PD48acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIiv +TDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJnxDHO2zTlJQNgJXtxmOTAGytfdELS +S8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554WgicEFOwE30z9J4nfr +I8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4sEb9 +b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNB +UvupLnKWnyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1P +Ti07NEPhmg4NpGaXutIcSkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929ven +sBxXVsFy6K2ir40zSbofitzmdHxghm+Hl3s= +-----END CERTIFICATE----- + +# Issuer: CN=ISRG Root X2 O=Internet Security Research Group +# Subject: CN=ISRG Root X2 O=Internet Security Research Group +# Label: "ISRG Root X2" +# Serial: 87493402998870891108772069816698636114 +# MD5 Fingerprint: d3:9e:c4:1e:23:3c:a6:df:cf:a3:7e:6d:e0:14:e6:e5 +# SHA1 Fingerprint: bd:b1:b9:3c:d5:97:8d:45:c6:26:14:55:f8:db:95:c7:5a:d1:53:af +# SHA256 Fingerprint: 69:72:9b:8e:15:a8:6e:fc:17:7a:57:af:b7:17:1d:fc:64:ad:d2:8c:2f:ca:8c:f1:50:7e:34:45:3c:cb:14:70 +-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw +CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg +R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 +MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT +ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW ++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 +ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI +zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW +tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 +/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE----- + +# Issuer: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. +# Subject: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. +# Label: "HiPKI Root CA - G1" +# Serial: 60966262342023497858655262305426234976 +# MD5 Fingerprint: 69:45:df:16:65:4b:e8:68:9a:8f:76:5f:ff:80:9e:d3 +# SHA1 Fingerprint: 6a:92:e4:a8:ee:1b:ec:96:45:37:e3:29:57:49:cd:96:e3:e5:d2:60 +# SHA256 Fingerprint: f0:15:ce:3c:c2:39:bf:ef:06:4b:e9:f1:d2:c4:17:e1:a0:26:4a:0a:94:be:1f:0c:8d:12:18:64:eb:69:49:cc +-----BEGIN CERTIFICATE----- +MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa +Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3 +YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw +qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv +Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6 +lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz +Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ +KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK +FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj +HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr +y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ +/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM +a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6 +fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG +SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi +7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc +SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza +ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc +XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg +iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho +L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF +Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr +kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+ +vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU +YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 +# Label: "GlobalSign ECC Root CA - R4" +# Serial: 159662223612894884239637590694 +# MD5 Fingerprint: 26:29:f8:6d:e1:88:bf:a2:65:7f:aa:c4:cd:0f:7f:fc +# SHA1 Fingerprint: 6b:a0:b0:98:e1:71:ef:5a:ad:fe:48:15:80:77:10:f4:bd:6f:0b:28 +# SHA256 Fingerprint: b0:85:d7:0b:96:4f:19:1a:73:e4:af:0d:54:ae:7a:0e:07:aa:fd:af:9b:71:dd:08:62:13:8a:b7:32:5a:24:a2 +-----BEGIN CERTIFICATE----- +MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD +VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw +MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g +UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx +uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV +HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ ++wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 +bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R1 O=Google Trust Services LLC +# Subject: CN=GTS Root R1 O=Google Trust Services LLC +# Label: "GTS Root R1" +# Serial: 159662320309726417404178440727 +# MD5 Fingerprint: 05:fe:d0:bf:71:a8:a3:76:63:da:01:e0:d8:52:dc:40 +# SHA1 Fingerprint: e5:8c:1c:c4:91:3b:38:63:4b:e9:10:6e:e3:ad:8e:6b:9d:d9:81:4a +# SHA256 Fingerprint: d9:47:43:2a:bd:e7:b7:fa:90:fc:2e:6b:59:10:1b:12:80:e0:e1:c7:e4:e4:0f:a3:c6:88:7f:ff:57:a7:f4:cf +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R3 O=Google Trust Services LLC +# Subject: CN=GTS Root R3 O=Google Trust Services LLC +# Label: "GTS Root R3" +# Serial: 159662495401136852707857743206 +# MD5 Fingerprint: 3e:e7:9d:58:02:94:46:51:94:e5:e0:22:4a:8b:e7:73 +# SHA1 Fingerprint: ed:e5:71:80:2b:c8:92:b9:5b:83:3c:d2:32:68:3f:09:cd:a0:1e:46 +# SHA256 Fingerprint: 34:d8:a7:3e:e2:08:d9:bc:db:0d:95:65:20:93:4b:4e:40:e6:94:82:59:6e:8b:6f:73:c8:42:6b:01:0a:6f:48 +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G +jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2 +4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7 +VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm +ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R4 O=Google Trust Services LLC +# Subject: CN=GTS Root R4 O=Google Trust Services LLC +# Label: "GTS Root R4" +# Serial: 159662532700760215368942768210 +# MD5 Fingerprint: 43:96:83:77:19:4d:76:b3:9d:65:52:e4:1d:22:a5:e8 +# SHA1 Fingerprint: 77:d3:03:67:b5:e0:0c:15:f6:0c:38:61:df:7c:e1:3b:92:46:4d:47 +# SHA256 Fingerprint: 34:9d:fa:40:58:c5:e2:63:12:3b:39:8a:e7:95:57:3c:4e:13:13:c8:3f:e6:8f:93:55:6c:d5:e8:03:1b:3c:7d +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi +QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR +HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D +9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 +p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD +-----END CERTIFICATE----- + +# Issuer: CN=Telia Root CA v2 O=Telia Finland Oyj +# Subject: CN=Telia Root CA v2 O=Telia Finland Oyj +# Label: "Telia Root CA v2" +# Serial: 7288924052977061235122729490515358 +# MD5 Fingerprint: 0e:8f:ac:aa:82:df:85:b1:f4:dc:10:1c:fc:99:d9:48 +# SHA1 Fingerprint: b9:99:cd:d1:73:50:8a:c4:47:05:08:9c:8c:88:fb:be:a0:2b:40:cd +# SHA256 Fingerprint: 24:2b:69:74:2f:cb:1e:5b:2a:bf:98:89:8b:94:57:21:87:54:4e:5b:4d:99:11:78:65:73:62:1f:6a:74:b8:2c +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx +CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE +AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1 +NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZ +MBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ76zBq +AMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9 +vVYiQJ3q9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9 +lRdU2HhE8Qx3FZLgmEKnpNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTOD +n3WhUidhOPFZPY5Q4L15POdslv5e2QJltI5c0BE0312/UqeBAMN/mUWZFdUXyApT +7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW5olWK8jjfN7j/4nlNW4o +6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNrRBH0pUPC +TEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6 +WT0EBXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63R +DolUK5X6wK0dmBR4M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZI +pEYslOqodmJHixBTB0hXbOKSTbauBcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGj +YzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7Wxy+G2CQ5MB0GA1UdDgQWBBRy +rOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ +8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi +0f6X+J8wfBj5tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMM +A8iZGok1GTzTyVR8qPAs5m4HeW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBS +SRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+Cy748fdHif64W1lZYudogsYMVoe+K +TTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF +6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15h2Er +3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMt +Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT +VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW +ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA +rBPuUBQemMc= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH +# Subject: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH +# Label: "D-TRUST BR Root CA 1 2020" +# Serial: 165870826978392376648679885835942448534 +# MD5 Fingerprint: b5:aa:4b:d5:ed:f7:e3:55:2e:8f:72:0a:f3:75:b8:ed +# SHA1 Fingerprint: 1f:5b:98:f0:e3:b5:f7:74:3c:ed:e6:b0:36:7d:32:cd:f4:09:41:67 +# SHA256 Fingerprint: e5:9a:aa:81:60:09:c2:2b:ff:5b:25:ba:d3:7d:f3:06:f0:49:79:7c:1f:81:d8:5a:b0:89:e6:57:bd:8f:00:44 +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5 +NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7dPYS +zuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0 +QVK5buXuQqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/ +VbNafAkl1bK6CKBrqx9tMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2JyX3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW +wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV +dWNbFJWcHwHP2NVypw87 +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH +# Subject: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH +# Label: "D-TRUST EV Root CA 1 2020" +# Serial: 126288379621884218666039612629459926992 +# MD5 Fingerprint: 8c:2d:9d:70:9f:48:99:11:06:11:fb:e9:cb:30:c0:6e +# SHA1 Fingerprint: 61:db:8c:21:59:69:03:90:d8:7c:9c:12:86:54:cf:9d:3d:f4:dd:07 +# SHA256 Fingerprint: 08:17:0d:1a:a3:64:53:90:1a:2f:95:92:45:e3:47:db:0c:8d:37:ab:aa:bc:56:b8:1a:a1:00:dc:95:89:70:db +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 +NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC +/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD +wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3 +OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA +y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb +gfM0agPnIjhQW+0ZT0MW +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. +# Subject: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. +# Label: "DigiCert TLS ECC P384 Root G5" +# Serial: 13129116028163249804115411775095713523 +# MD5 Fingerprint: d3:71:04:6a:43:1c:db:a6:59:e1:a8:a3:aa:c5:71:ed +# SHA1 Fingerprint: 17:f3:de:5e:9f:0f:19:e9:8e:f6:1f:32:26:6e:20:c4:07:ae:30:ee +# SHA256 Fingerprint: 01:8e:13:f0:77:25:32:cf:80:9b:d1:b1:72:81:86:72:83:fc:48:c6:e1:3b:e9:c6:98:12:85:4a:49:0c:1b:05 +-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. +# Subject: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. +# Label: "DigiCert TLS RSA4096 Root G5" +# Serial: 11930366277458970227240571539258396554 +# MD5 Fingerprint: ac:fe:f7:34:96:a9:f2:b3:b4:12:4b:e4:27:41:6f:e1 +# SHA1 Fingerprint: a7:88:49:dc:5d:7c:75:8c:8c:de:39:98:56:b3:aa:d0:b2:a5:71:35 +# SHA256 Fingerprint: 37:1a:00:dc:05:33:b3:72:1a:7e:eb:40:e8:41:9e:70:79:9d:2b:0a:0f:2c:1d:80:69:31:65:f7:ce:c4:ad:75 +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT +HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN +NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs +IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ +ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 +2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp +wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM +pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD +nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po +sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx +Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd +Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX +KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe +XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL +tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv +TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN +AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H +PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF +O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ +REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik +AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv +/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ +p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw +MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF +qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK +ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE----- + +# Issuer: CN=Certainly Root R1 O=Certainly +# Subject: CN=Certainly Root R1 O=Certainly +# Label: "Certainly Root R1" +# Serial: 188833316161142517227353805653483829216 +# MD5 Fingerprint: 07:70:d4:3e:82:87:a0:fa:33:36:13:f4:fa:33:e7:12 +# SHA1 Fingerprint: a0:50:ee:0f:28:71:f4:27:b2:12:6d:6f:50:96:25:ba:cc:86:42:af +# SHA256 Fingerprint: 77:b8:2c:d8:64:4c:43:05:f7:ac:c5:cb:15:6b:45:67:50:04:03:3d:51:c6:0c:62:02:a8:e0:c3:34:67:d3:a0 +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw +PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy +dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0 +YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2 +1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT +vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed +aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0 +1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5 +r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5 +cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ +wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ +6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA +2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH +Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR +eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u +d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr +PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d +8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi +1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd +rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di +taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7 +lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj +yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn +Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy +yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n +wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 +OV+KmalBWQewLK8= +-----END CERTIFICATE----- + +# Issuer: CN=Certainly Root E1 O=Certainly +# Subject: CN=Certainly Root E1 O=Certainly +# Label: "Certainly Root E1" +# Serial: 8168531406727139161245376702891150584 +# MD5 Fingerprint: 0a:9e:ca:cd:3e:52:50:c6:36:f3:4b:a3:ed:a7:53:e9 +# SHA1 Fingerprint: f9:e1:6d:dc:01:89:cf:d5:82:45:63:3e:c5:37:7d:c2:eb:93:6f:2b +# SHA256 Fingerprint: b4:58:5f:22:e4:ac:75:6a:4e:86:12:a1:36:1c:5d:9d:03:1a:93:fd:84:fe:bb:77:8f:a3:06:8b:0f:c4:2d:c2 +-----BEGIN CERTIFICATE----- +MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw +CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu +bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ +BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s +eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK ++IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 +QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 +hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm +ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG +BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR +-----END CERTIFICATE----- + +# Issuer: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. +# Subject: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. +# Label: "Security Communication ECC RootCA1" +# Serial: 15446673492073852651 +# MD5 Fingerprint: 7e:43:b0:92:68:ec:05:43:4c:98:ab:5d:35:2e:7e:86 +# SHA1 Fingerprint: b8:0e:26:a9:bf:d2:b2:3b:c0:ef:46:c9:ba:c7:bb:f6:1d:0d:41:41 +# SHA256 Fingerprint: e7:4f:bd:a5:5b:d5:64:c4:73:a3:6b:44:1a:a7:99:c8:a6:8e:07:74:40:e8:28:8b:9f:a1:e5:0e:4b:ba:ca:11 +-----BEGIN CERTIFICATE----- +MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT +AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD +VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx +NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT +HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5 +IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl +dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK +ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu +9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O +be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= +-----END CERTIFICATE----- + +# Issuer: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY +# Subject: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY +# Label: "BJCA Global Root CA1" +# Serial: 113562791157148395269083148143378328608 +# MD5 Fingerprint: 42:32:99:76:43:33:36:24:35:07:82:9b:28:f9:d0:90 +# SHA1 Fingerprint: d5:ec:8d:7b:4c:ba:79:f4:e7:e8:cb:9d:6b:ae:77:83:10:03:21:6a +# SHA256 Fingerprint: f3:89:6f:88:fe:7c:0a:88:27:66:a7:fa:6a:d2:74:9f:b5:7a:7f:3e:98:fb:76:9c:1f:a7:b0:9c:2c:44:d5:ae +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIQVW9l47TZkGobCdFsPsBsIDANBgkqhkiG9w0BAQsFADBU +MQswCQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRI +T1JJVFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0ExMB4XDTE5MTIxOTAz +MTYxN1oXDTQ0MTIxMjAzMTYxN1owVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJF +SUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2Jh +bCBSb290IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPFmCL3Z +xRVhy4QEQaVpN3cdwbB7+sN3SJATcmTRuHyQNZ0YeYjjlwE8R4HyDqKYDZ4/N+AZ +spDyRhySsTphzvq3Rp4Dhtczbu33RYx2N95ulpH3134rhxfVizXuhJFyV9xgw8O5 +58dnJCNPYwpj9mZ9S1WnP3hkSWkSl+BMDdMJoDIwOvqfwPKcxRIqLhy1BDPapDgR +at7GGPZHOiJBhyL8xIkoVNiMpTAK+BcWyqw3/XmnkRd4OJmtWO2y3syJfQOcs4ll +5+M7sSKGjwZteAf9kRJ/sGsciQ35uMt0WwfCyPQ10WRjeulumijWML3mG90Vr4Tq +nMfK9Q7q8l0ph49pczm+LiRvRSGsxdRpJQaDrXpIhRMsDQa4bHlW/KNnMoH1V6XK +V0Jp6VwkYe/iMBhORJhVb3rCk9gZtt58R4oRTklH2yiUAguUSiz5EtBP6DF+bHq/ +pj+bOT0CFqMYs2esWz8sgytnOYFcuX6U1WTdno9uruh8W7TXakdI136z1C2OVnZO +z2nxbkRs1CTqjSShGL+9V/6pmTW12xB3uD1IutbB5/EjPtffhZ0nPNRAvQoMvfXn +jSXWgXSHRtQpdaJCbPdzied9v3pKH9MiyRVVz99vfFXQpIsHETdfg6YmV6YBW37+ +WGgHqel62bno/1Afq8K0wM7o6v0PvY1NuLxxAgMBAAGjQjBAMB0GA1UdDgQWBBTF +7+3M2I0hxkjk49cULqcWk+WYATAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE +AwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAUoKsITQfI/Ki2Pm4rzc2IInRNwPWaZ+4 +YRC6ojGYWUfo0Q0lHhVBDOAqVdVXUsv45Mdpox1NcQJeXyFFYEhcCY5JEMEE3Kli +awLwQ8hOnThJdMkycFRtwUf8jrQ2ntScvd0g1lPJGKm1Vrl2i5VnZu69mP6u775u ++2D2/VnGKhs/I0qUJDAnyIm860Qkmss9vk/Ves6OF8tiwdneHg56/0OGNFK8YT88 +X7vZdrRTvJez/opMEi4r89fO4aL/3Xtw+zuhTaRjAv04l5U/BXCga99igUOLtFkN +SoxUnMW7gZ/NfaXvCyUeOiDbHPwfmGcCCtRzRBPbUYQaVQNW4AB+dAb/OMRyHdOo +P2gxXdMJxy6MW2Pg6Nwe0uxhHvLe5e/2mXZgLR6UcnHGCyoyx5JO1UbXHfmpGQrI ++pXObSOYqgs4rZpWDW+N8TEAiMEXnM0ZNjX+VVOg4DwzX5Ze4jLp3zO7Bkqp2IRz +znfSxqxx4VyjHQy7Ct9f4qNx2No3WqB4K/TUfet27fJhcKVlmtOJNBir+3I+17Q9 +eVzYH6Eze9mCUAyTF6ps3MKCuwJXNq+YJyo5UOGwifUll35HaBC07HPKs5fRJNz2 +YqAo07WjuGS3iGJCz51TzZm+ZGiPTx4SSPfSKcOYKMryMguTjClPPGAyzQWWYezy +r/6zcCwupvI= +-----END CERTIFICATE----- + +# Issuer: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY +# Subject: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY +# Label: "BJCA Global Root CA2" +# Serial: 58605626836079930195615843123109055211 +# MD5 Fingerprint: 5e:0a:f6:47:5f:a6:14:e8:11:01:95:3f:4d:01:eb:3c +# SHA1 Fingerprint: f4:27:86:eb:6e:b8:6d:88:31:67:02:fb:ba:66:a4:53:00:aa:7a:a6 +# SHA256 Fingerprint: 57:4d:f6:93:1e:27:80:39:66:7b:72:0a:fd:c1:60:0f:c2:7e:b6:6d:d3:09:29:79:fb:73:85:64:87:21:28:82 +-----BEGIN CERTIFICATE----- +MIICJTCCAaugAwIBAgIQLBcIfWQqwP6FGFkGz7RK6zAKBggqhkjOPQQDAzBUMQsw +CQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJ +VFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0EyMB4XDTE5MTIxOTAzMTgy +MVoXDTQ0MTIxMjAzMTgyMVowVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJFSUpJ +TkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2JhbCBS +b290IENBMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABJ3LgJGNU2e1uVCxA/jlSR9B +IgmwUVJY1is0j8USRhTFiy8shP8sbqjV8QnjAyEUxEM9fMEsxEtqSs3ph+B99iK+ ++kpRuDCK/eHeGBIK9ke35xe/J4rUQUyWPGCWwf0VHKNCMEAwHQYDVR0OBBYEFNJK +sVF/BvDRgh9Obl+rg/xI1LCRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMAoGCCqGSM49BAMDA2gAMGUCMBq8W9f+qdJUDkpd0m2xQNz0Q9XSSpkZElaA +94M04TVOSG0ED1cxMDAtsaqdAzjbBgIxAMvMh1PLet8gUXOQwKhbYdDFUDn9hf7B +43j4ptZLvZuHjw/l1lOWqzzIQNph91Oj9w== +-----END CERTIFICATE----- + +# Issuer: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited +# Subject: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited +# Label: "Sectigo Public Server Authentication Root E46" +# Serial: 88989738453351742415770396670917916916 +# MD5 Fingerprint: 28:23:f8:b2:98:5c:37:16:3b:3e:46:13:4e:b0:b3:01 +# SHA1 Fingerprint: ec:8a:39:6c:40:f0:2e:bc:42:75:d4:9f:ab:1c:1a:5b:67:be:d2:9a +# SHA256 Fingerprint: c9:0f:26:f0:fb:1b:40:18:b2:22:27:51:9b:5c:a2:b5:3e:2c:a5:b3:be:5c:f1:8e:fe:1b:ef:47:38:0c:53:83 +-----BEGIN CERTIFICATE----- +MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw +CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T +ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN +MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG +A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT +ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC +WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ +6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B +Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa +qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q +4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== +-----END CERTIFICATE----- + +# Issuer: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited +# Subject: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited +# Label: "Sectigo Public Server Authentication Root R46" +# Serial: 156256931880233212765902055439220583700 +# MD5 Fingerprint: 32:10:09:52:00:d5:7e:6c:43:df:15:c0:b1:16:93:e5 +# SHA1 Fingerprint: ad:98:f9:f3:e4:7d:75:3b:65:d4:82:b3:a4:52:17:bb:6e:f5:e4:38 +# SHA256 Fingerprint: 7b:b6:47:a6:2a:ee:ac:88:bf:25:7a:a5:22:d0:1f:fe:a3:95:e0:ab:45:c7:3f:93:f6:56:54:ec:38:f2:5a:06 +-----BEGIN CERTIFICATE----- +MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD +Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw +HhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY +MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp +YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa +ef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz +SDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf +iOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X +ME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3 +IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS +VYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE +SJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu ++Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt +8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L +HaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt +zwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P +AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c +mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ +YKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52 +gDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA +Fv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB +JYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX +DhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui +TdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5 +dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65 +LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp +0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY +QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation +# Subject: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation +# Label: "SSL.com TLS RSA Root CA 2022" +# Serial: 148535279242832292258835760425842727825 +# MD5 Fingerprint: d8:4e:c6:59:30:d8:fe:a0:d6:7a:5a:2c:2c:69:78:da +# SHA1 Fingerprint: ec:2c:83:40:72:af:26:95:10:ff:0e:f2:03:ee:31:70:f6:78:9d:ca +# SHA256 Fingerprint: 8f:af:7d:2e:2c:b4:70:9b:b8:e0:b3:36:66:bf:75:a5:dd:45:b5:de:48:0f:8e:a8:d4:bf:e6:be:bc:17:f2:ed +-----BEGIN CERTIFICATE----- +MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO +MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD +DBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX +DTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw +b3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJvb3QgQ0EgMjAyMjCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u9nTP +L3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OY +t6/wNr/y7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0ins +S657Lb85/bRi3pZ7QcacoOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3 +PnxEX4MN8/HdIGkWCVDi1FW24IBydm5MR7d1VVm0U3TZlMZBrViKMWYPHqIbKUBO +L9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDGD6C1vBdOSHtRwvzpXGk3 +R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEWTO6Af77w +dr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS ++YCk8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYS +d66UNHsef8JmAOSqg+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoG +AtUjHBPW6dvbxrB6y3snm/vg1UYk7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2f +gTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j +BBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsuN+7jhHonLs0Z +NbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt +hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsM +QtfhWsSWTVTNj8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvf +R4iyrT7gJ4eLSYwfqUdYe5byiB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJ +DPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjUo3KUQyxi4U5cMj29TH0ZR6LDSeeW +P4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqoENjwuSfr98t67wVy +lrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7EgkaibMOlq +bLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2w +AgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q +r5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji +Mho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU +98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation +# Subject: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation +# Label: "SSL.com TLS ECC Root CA 2022" +# Serial: 26605119622390491762507526719404364228 +# MD5 Fingerprint: 99:d7:5c:f1:51:36:cc:e9:ce:d9:19:2e:77:71:56:c5 +# SHA1 Fingerprint: 9f:5f:d9:1a:54:6d:f5:0c:71:f0:ee:7a:bd:17:49:98:84:73:e2:39 +# SHA256 Fingerprint: c3:2f:fd:9f:46:f9:36:d1:6c:36:73:99:09:59:43:4b:9a:d6:0a:af:bb:9e:7c:f3:36:54:f1:44:cc:1b:a1:43 +-----BEGIN CERTIFICATE----- +MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT +U0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2 +MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3Jh +dGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3QgQ0EgMjAyMjB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWyJGYm +acCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFN +SeR7T5v15wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME +GDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW +uCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp +15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN +b0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g== +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos +# Subject: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos +# Label: "Atos TrustedRoot Root CA ECC TLS 2021" +# Serial: 81873346711060652204712539181482831616 +# MD5 Fingerprint: 16:9f:ad:f1:70:ad:79:d6:ed:29:b4:d1:c5:79:70:a8 +# SHA1 Fingerprint: 9e:bc:75:10:42:b3:02:f3:81:f4:f7:30:62:d4:8f:c3:a7:51:b2:dd +# SHA256 Fingerprint: b2:fa:e5:3e:14:cc:d7:ab:92:12:06:47:01:ae:27:9c:1d:89:88:fa:cb:77:5f:a8:a0:08:91:4e:66:39:88:a8 +-----BEGIN CERTIFICATE----- +MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w +LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w +CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 +MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF +Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI +zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X +tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 +AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 +KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD +aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu +CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo +9H1/IISpQuQo +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos +# Subject: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos +# Label: "Atos TrustedRoot Root CA RSA TLS 2021" +# Serial: 111436099570196163832749341232207667876 +# MD5 Fingerprint: d4:d3:46:b8:9a:c0:9c:76:5d:9e:3a:c3:b9:99:31:d2 +# SHA1 Fingerprint: 18:52:3b:0d:06:37:e4:d6:3a:df:23:e4:98:fb:5b:16:fb:86:74:48 +# SHA256 Fingerprint: 81:a9:08:8e:a5:9f:b3:64:c5:48:a6:f8:55:59:09:9b:6f:04:05:ef:bf:18:e5:32:4e:c9:f4:57:ba:00:11:2f +-----BEGIN CERTIFICATE----- +MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBM +MS4wLAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIx +MQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00 +MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBD +QSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BBl01Z +4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYv +Ye+W/CBGvevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZ +kmGbzSoXfduP9LVq6hdKZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDs +GY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt0xU6kGpn8bRrZtkh68rZYnxGEFzedUln +nkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVKPNe0OwANwI8f4UDErmwh +3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMYsluMWuPD +0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzy +geBYBr3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8 +ANSbhqRAvNncTFd+rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezB +c6eUWsuSZIKmAMFwoW4sKeFYV+xafJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lI +pw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +dEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +DAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS +4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPs +o0UvFJ/1TCplQ3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJ +qM7F78PRreBrAwA0JrRUITWXAdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuyw +xfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9GslA9hGCZcbUztVdF5kJHdWoOsAgM +rr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2VktafcxBPTy+av5EzH4 +AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9qTFsR +0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuY +o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5 +dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE +oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ== +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. +# Label: "TrustAsia Global Root CA G3" +# Serial: 576386314500428537169965010905813481816650257167 +# MD5 Fingerprint: 30:42:1b:b7:bb:81:75:35:e4:16:4f:53:d2:94:de:04 +# SHA1 Fingerprint: 63:cf:b6:c1:27:2b:56:e4:88:8e:1c:23:9a:b6:2e:81:47:24:c3:c7 +# SHA256 Fingerprint: e0:d3:22:6a:eb:11:63:c2:e4:8f:f9:be:3b:50:b4:c6:43:1b:e7:bb:1e:ac:c5:c3:6b:5d:5e:c5:09:03:9a:08 +-----BEGIN CERTIFICATE----- +MIIFpTCCA42gAwIBAgIUZPYOZXdhaqs7tOqFhLuxibhxkw8wDQYJKoZIhvcNAQEM +BQAwWjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHMzAe +Fw0yMTA1MjAwMjEwMTlaFw00NjA1MTkwMjEwMTlaMFoxCzAJBgNVBAYTAkNOMSUw +IwYDVQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtU +cnVzdEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDAMYJhkuSUGwoqZdC+BqmHO1ES6nBBruL7dOoKjbmzTNyPtxNS +T1QY4SxzlZHFZjtqz6xjbYdT8PfxObegQ2OwxANdV6nnRM7EoYNl9lA+sX4WuDqK +AtCWHwDNBSHvBm3dIZwZQ0WhxeiAysKtQGIXBsaqvPPW5vxQfmZCHzyLpnl5hkA1 +nyDvP+uLRx+PjsXUjrYsyUQE49RDdT/VP68czH5GX6zfZBCK70bwkPAPLfSIC7Ep +qq+FqklYqL9joDiR5rPmd2jE+SoZhLsO4fWvieylL1AgdB4SQXMeJNnKziyhWTXA +yB1GJ2Faj/lN03J5Zh6fFZAhLf3ti1ZwA0pJPn9pMRJpxx5cynoTi+jm9WAPzJMs +hH/x/Gr8m0ed262IPfN2dTPXS6TIi/n1Q1hPy8gDVI+lhXgEGvNz8teHHUGf59gX +zhqcD0r83ERoVGjiQTz+LISGNzzNPy+i2+f3VANfWdP3kXjHi3dqFuVJhZBFcnAv +kV34PmVACxmZySYgWmjBNb9Pp1Hx2BErW+Canig7CjoKH8GB5S7wprlppYiU5msT +f9FkPz2ccEblooV7WIQn3MSAPmeamseaMQ4w7OYXQJXZRe0Blqq/DPNL0WP3E1jA +uPP6Z92bfW1K/zJMtSU7/xxnD4UiWQWRkUF3gdCFTIcQcf+eQxuulXUtgQIDAQAB +o2MwYTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEDk5PIj7zjKsK5Xf/Ih +MBY027ySMB0GA1UdDgQWBBRA5OTyI+84yrCuV3/yITAWNNu8kjAOBgNVHQ8BAf8E +BAMCAQYwDQYJKoZIhvcNAQEMBQADggIBACY7UeFNOPMyGLS0XuFlXsSUT9SnYaP4 +wM8zAQLpw6o1D/GUE3d3NZ4tVlFEbuHGLige/9rsR82XRBf34EzC4Xx8MnpmyFq2 +XFNFV1pF1AWZLy4jVe5jaN/TG3inEpQGAHUNcoTpLrxaatXeL1nHo+zSh2bbt1S1 +JKv0Q3jbSwTEb93mPmY+KfJLaHEih6D4sTNjduMNhXJEIlU/HHzp/LgV6FL6qj6j +ITk1dImmasI5+njPtqzn59ZW/yOSLlALqbUHM/Q4X6RJpstlcHboCoWASzY9M/eV +VHUl2qzEc4Jl6VL1XP04lQJqaTDFHApXB64ipCz5xUG3uOyfT0gA+QEEVcys+TIx +xHWVBqB/0Y0n3bOppHKH/lmLmnp0Ft0WpWIp6zqW3IunaFnT63eROfjXy9mPX1on +AX1daBli2MjN9LdyR75bl87yraKZk62Uy5P2EgmVtqvXO9A/EcswFi55gORngS1d +7XB4tmBZrOFdRWOPyN9yaFvqHbgB8X7754qz41SgOAngPN5C8sLtLpvzHzW2Ntjj +gKGLzZlkD8Kqq7HK9W+eQ42EVJmzbsASZthwEPEGNTNDqJwuuhQxzhB/HIbjj9LV ++Hfsm6vxL2PZQl/gZ4FkkfGXL/xuJvYz+NO1+MRiqzFRJQJ6+N1rZdVtTTDIZbpo +FGWsJwt0ivKH +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. +# Label: "TrustAsia Global Root CA G4" +# Serial: 451799571007117016466790293371524403291602933463 +# MD5 Fingerprint: 54:dd:b2:d7:5f:d8:3e:ed:7c:e0:0b:2e:cc:ed:eb:eb +# SHA1 Fingerprint: 57:73:a5:61:5d:80:b2:e6:ac:38:82:fc:68:07:31:ac:9f:b5:92:5a +# SHA256 Fingerprint: be:4b:56:cb:50:56:c0:13:6a:52:6d:f4:44:50:8d:aa:36:a0:b5:4f:42:e4:ac:38:f7:2a:f4:70:e4:79:65:4c +-----BEGIN CERTIFICATE----- +MIICVTCCAdygAwIBAgIUTyNkuI6XY57GU4HBdk7LKnQV1tcwCgYIKoZIzj0EAwMw +WjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHNDAeFw0y +MTA1MjAwMjEwMjJaFw00NjA1MTkwMjEwMjJaMFoxCzAJBgNVBAYTAkNOMSUwIwYD +VQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtUcnVz +dEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATx +s8045CVD5d4ZCbuBeaIVXxVjAd7Cq92zphtnS4CDr5nLrBfbK5bKfFJV4hrhPVbw +LxYI+hW8m7tH5j/uqOFMjPXTNvk4XatwmkcN4oFBButJ+bAp3TPsUKV/eSm4IJij +YzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUpbtKl86zK3+kMd6Xg1mD +pm9xy94wHQYDVR0OBBYEFKW7SpfOsyt/pDHel4NZg6ZvccveMA4GA1UdDwEB/wQE +AwIBBjAKBggqhkjOPQQDAwNnADBkAjBe8usGzEkxn0AAbbd+NvBNEU/zy4k6LHiR +UKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xkdUfFVZDj +/bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA== +-----END CERTIFICATE----- + +# Issuer: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH +# Subject: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH +# Label: "Telekom Security TLS ECC Root 2020" +# Serial: 72082518505882327255703894282316633856 +# MD5 Fingerprint: c1:ab:fe:6a:10:2c:03:8d:bc:1c:22:32:c0:85:a7:fd +# SHA1 Fingerprint: c0:f8:96:c5:a9:3b:01:06:21:07:da:18:42:48:bc:e9:9d:88:d5:ec +# SHA256 Fingerprint: 57:8a:f4:de:d0:85:3f:4e:59:98:db:4a:ea:f9:cb:ea:8d:94:5f:60:b6:20:a3:8d:1a:3c:13:b2:bc:7b:a8:e1 +-----BEGIN CERTIFICATE----- +MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQsw +CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH +bWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIw +MB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIzNTk1OVowYzELMAkGA1UEBhMCREUx +JzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkGA1UE +AwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/O +tdKPD/M12kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDP +f8iAC8GXs7s1J8nCG6NCMEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6f +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA +MGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZMo7k+5Dck2TOrbRBR2Di +z6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdUga/sf+Rn +27iQ7t0l +-----END CERTIFICATE----- + +# Issuer: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH +# Subject: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH +# Label: "Telekom Security TLS RSA Root 2023" +# Serial: 44676229530606711399881795178081572759 +# MD5 Fingerprint: bf:5b:eb:54:40:cd:48:71:c4:20:8d:7d:de:0a:42:f2 +# SHA1 Fingerprint: 54:d3:ac:b3:bd:57:56:f6:85:9d:ce:e5:c3:21:e2:d4:ad:83:d0:93 +# SHA256 Fingerprint: ef:c6:5c:ad:bb:59:ad:b6:ef:e8:4d:a2:23:11:b3:56:24:b7:1b:3b:1e:a0:da:8b:66:55:17:4e:c8:97:86:46 +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBj +MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0 +eSBHbWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAy +MDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMyNzIzNTk1OVowYzELMAkGA1UEBhMC +REUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkG +A1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9 +cUD/h3VCKSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHV +cp6R+SPWcHu79ZvB7JPPGeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMA +U6DksquDOFczJZSfvkgdmOGjup5czQRxUX11eKvzWarE4GC+j4NSuHUaQTXtvPM6 +Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWol8hHD/BeEIvnHRz+sTug +BTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9FIS3R/qy +8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73J +co4vzLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg +8qKrBC7m8kwOFjQgrIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8 +rFEz0ciD0cmfHdRHNCk+y7AO+oMLKFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12 +mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7SWWO/gLCMk3PLNaaZlSJhZQNg ++y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtqeX +gj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2 +p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQ +pGv7qHBFfLp+sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm +9S3ul0A8Yute1hTWjOKWi0FpkzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErw +M807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy/SKE8YXJN3nptT+/XOR0so8RYgDd +GGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4mZqTuXNnQkYRIer+ +CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtzaL1t +xKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+ +w6jv/naaoqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aK +L4x35bcF7DvB7L6Gs4a8wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+lj +X273CXE2whJdV/LItM3z7gLfEdxquVeEHVlNjM7IDiPCtyaaEBRx/pOyiriA8A4Q +ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm +dTdmQRCsu/WU48IxK63nI1bMNSWSs1A= +-----END CERTIFICATE----- + +# Issuer: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA +# Label: "TWCA CYBER Root CA" +# Serial: 85076849864375384482682434040119489222 +# MD5 Fingerprint: 0b:33:a0:97:52:95:d4:a9:fd:bb:db:6e:a3:55:5b:51 +# SHA1 Fingerprint: f6:b1:1c:1a:83:38:e9:7b:db:b3:a8:c8:33:24:e0:2d:9c:7f:26:66 +# SHA256 Fingerprint: 3f:63:bb:28:14:be:17:4e:c8:b6:43:9c:f0:8d:6d:56:f0:b7:c4:05:88:3a:56:48:a3:34:42:4d:6b:3e:c5:58 +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ +MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 +IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5 +WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FO +LUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1sTs6P +40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxF +avcokPFhV8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/ +34bKS1PE2Y2yHer43CdTo0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684i +JkXXYJndzk834H/nY62wuFm40AZoNWDTNq5xQwTxaWV4fPMf88oon1oglWa0zbfu +j3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK/c/WMw+f+5eesRycnupf +Xtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkHIuNZW0CP +2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDA +S9TMfAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDA +oS/xUgXJP+92ZuJF2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzC +kHDXShi8fgGwsOsVHkQGzaRP6AzRwyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW +5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83QOGt4A1WNzAd +BgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB +AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0t +tGlTITVX1olNc79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn +68xDiBaiA9a5F/gZbG0jAn/xX9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNn +TKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDRIG4kqIQnoVesqlVYL9zZyvpoBJ7t +RCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq/p1hvIbZv97Tujqx +f36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0RFxbI +Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz +8ppy6rBePm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4 +NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX +xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6 +t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. +# Subject: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. +# Label: "SecureSign Root CA14" +# Serial: 575790784512929437950770173562378038616896959179 +# MD5 Fingerprint: 71:0d:72:fa:92:19:65:5e:89:04:ac:16:33:f0:bc:d5 +# SHA1 Fingerprint: dd:50:c0:f7:79:b3:64:2e:74:a2:b8:9d:9f:d3:40:dd:bb:f0:f2:4f +# SHA256 Fingerprint: 4b:00:9c:10:34:49:4f:9a:b5:6b:ba:3b:a1:d6:27:31:fc:4d:20:d8:95:5a:dc:ec:10:a9:25:60:72:61:e3:38 +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEM +BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u +LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgw +NzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD +eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS +b290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh1oq/ +FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOg +vlIfX8xnbacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy +6pJxaeQp8E+BgQQ8sqVb1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo +/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9J +kdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOEkJTRX45zGRBdAuVwpcAQ +0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSxjVIHvXib +y8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac +18izju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs +0Wq2XSqypWa9a4X0dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIAB +SMbHdPTGrMNASRZhdCyvjG817XsYAFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVL +ApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeqYR3r6/wtbyPk +86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E +rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ib +ed87hwriZLoAymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopT +zfFP7ELyk+OZpDc8h7hi2/DsHzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHS +DCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPGFrojutzdfhrGe0K22VoF3Jpf1d+4 +2kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6qnsb58Nn4DSEC5MUo +FlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/OfVy +K4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6 +dB7h7sxaOgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtl +Lor6CZpO2oYofaphNdgOpygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB +365jJ6UeTo3cKXhZ+PmhIIynJkBugnLNeLLIjzwec+fBH7/PzqUqm9tEZDKgu39c +JRNItX+S +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. +# Subject: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. +# Label: "SecureSign Root CA15" +# Serial: 126083514594751269499665114766174399806381178503 +# MD5 Fingerprint: 13:30:fc:c4:62:a6:a9:de:b5:c1:68:af:b5:d2:31:47 +# SHA1 Fingerprint: cb:ba:83:c8:c1:5a:5d:f1:f9:73:6f:ca:d7:ef:28:13:06:4a:07:7d +# SHA256 Fingerprint: e7:78:f0:f0:95:fe:84:37:29:cd:1a:00:82:17:9e:53:14:a9:c2:91:44:28:05:e1:fb:1d:8f:b6:b8:88:6c:3a +-----BEGIN CERTIFICATE----- +MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMw +UTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBM +dGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMy +NTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpDeWJl +cnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBSb290 +IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5GdCx4 +wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSR +ZHX+AezB2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT +9DAKBggqhkjOPQQDAwNoADBlAjEA2S6Jfl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp +4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJSwdLZrWeqrqgHkHZAXQ6 +bkU6iYAZezKYVWOr62Nuk22rGwlgMU4= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH +# Subject: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH +# Label: "D-TRUST BR Root CA 2 2023" +# Serial: 153168538924886464690566649552453098598 +# MD5 Fingerprint: e1:09:ed:d3:60:d4:56:1b:47:1f:b7:0c:5f:1b:5f:85 +# SHA1 Fingerprint: 2d:b0:70:ee:71:94:af:69:68:17:db:79:ce:58:9f:a0:6b:96:f7:87 +# SHA256 Fingerprint: 05:52:e6:f8:3f:df:65:e8:fa:96:70:e6:66:df:28:a4:e2:13:40:b5:10:cb:e5:25:66:f9:7c:4f:b9:4b:2b:d1 +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEJSIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUw +OTA4NTYzMFowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAK7/CVmRgApKaOYkP7in5Mg6CjoWzckjYaCTcfKr +i3OPoGdlYNJUa2NRb0kz4HIHE304zQaSBylSa053bATTlfrdTIzZXcFhfUvnKLNE +gXtRr90zsWh81k5M/itoucpmacTsXld/9w3HnDY25QdgrMBM6ghs7wZ8T1soegj8 +k12b9py0i4a6Ibn08OhZWiihNIQaJZG2tY/vsvmA+vk9PBFy2OMvhnbFeSzBqZCT +Rphny4NqoFAjpzv2gTng7fC5v2Xx2Mt6++9zA84A9H3X4F07ZrjcjrqDy4d2A/wl +2ecjbwb9Z/Pg/4S8R7+1FhhGaRTMBffb00msa8yr5LULQyReS2tNZ9/WtT5PeB+U +cSTq3nD88ZP+npNa5JRal1QMNXtfbO4AHyTsA7oC9Xb0n9Sa7YUsOCIvx9gvdhFP +/Wxc6PWOJ4d/GUohR5AdeY0cW/jPSoXk7bNbjb7EZChdQcRurDhaTyN0dKkSw/bS +uREVMweR2Ds3OmMwBtHFIjYoYiMQ4EbMl6zWK11kJNXuHA7e+whadSr2Y23OC0K+ +0bpwHJwh5Q8xaRfX/Aq03u2AnMuStIv13lmiWAmlY0cL4UEyNEHZmrHZqLAbWt4N +DfTisl01gLmB1IRpkQLLddCNxbU9CZEJjxShFHR5PtbJFR2kWVki3PaKRT08EtY+ +XTIvAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZ5Dw1t61 +GNVGKX5cq/ieCLxklRAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfYnJfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA097N3U9swFrktpSHxQCF16+tI +FoE9c+CeJyrrd6kTpGoKWloUMz1oH4Guaf2Mn2VsNELZLdB/eBaxOqwjMa1ef67n +riv6uvw8l5VAk1/DLQOj7aRvU9f6QA4w9QAgLABMjDu0ox+2v5Eyq6+SmNMW5tTR +VFxDWy6u71cqqLRvpO8NVhTaIasgdp4D/Ca4nj8+AybmTNudX0KEPUUDAxxZiMrc +LmEkWqTqJwtzEr5SswrPMhfiHocaFpVIbVrg0M8JkiZmkdijYQ6qgYF/6FKC0ULn +4B0Y+qSFNueG4A3rvNTJ1jxD8V1Jbn6Bm2m1iWKPiFLY1/4nwSPFyysCu7Ff/vtD +hQNGvl3GyiEm/9cCnnRK3PgTFbGBVzbLZVzRHTF36SXDw7IyN9XxmAnkbWOACKsG +koHU6XCPpz+y7YaMgmo1yEJagtFSGkUPFaUA8JR7ZSdXOUPPfH/mvTWze/EZTN46 +ls/pdu4D58JDUjxqgejBWoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aS +Ecr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/5usWDiJFAbzdNpQ0qTUmiteXue4Icr80 +knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jtn/mtd+ArY0+ew+43u3gJ +hJ65bvspmZDogNOfJA== +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. +# Label: "TrustAsia TLS ECC Root CA" +# Serial: 310892014698942880364840003424242768478804666567 +# MD5 Fingerprint: 09:48:04:77:d2:fc:65:93:71:66:b1:11:95:4f:06:8c +# SHA1 Fingerprint: b5:ec:39:f3:a1:66:37:ae:c3:05:94:57:e2:be:11:be:b7:a1:7f:36 +# SHA256 Fingerprint: c0:07:6b:9e:f0:53:1f:b1:a6:56:d6:7c:4e:be:97:cd:5d:ba:a4:1e:f4:45:98:ac:c2:48:98:78:c9:2d:87:11 +-----BEGIN CERTIFICATE----- +MIICMTCCAbegAwIBAgIUNnThTXxlE8msg1UloD5Sfi9QaMcwCgYIKoZIzj0EAwMw +WDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgRUNDIFJvb3QgQ0EwHhcNMjQw +NTE1MDU0MTU2WhcNNDQwNTE1MDU0MTU1WjBYMQswCQYDVQQGEwJDTjElMCMGA1UE +ChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1c3RB +c2lhIFRMUyBFQ0MgUm9vdCBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLh/pVs/ +AT598IhtrimY4ZtcU5nb9wj/1WrgjstEpvDBjL1P1M7UiFPoXlfXTr4sP/MSpwDp +guMqWzJ8S5sUKZ74LYO1644xST0mYekdcouJtgq7nDM1D9rs3qlKH8kzsaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQULIVTu7FDzTLqnqOH/qKYqKaT6RAw +DgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMFRH18MtYYZI9HlaVQ01 +L18N9mdsd0AaRuf4aFtOJx24mH1/k78ITcTaRTChD15KeAIxAKORh/IRM4PDwYqR +OkwrULG9IpRdNYlzg8WbGf60oenUoWa2AaU2+dhoYSi3dOGiMQ== +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. +# Label: "TrustAsia TLS RSA Root CA" +# Serial: 160405846464868906657516898462547310235378010780 +# MD5 Fingerprint: 3b:9e:c3:86:0f:34:3c:6b:c5:46:c4:8e:1d:e7:19:12 +# SHA1 Fingerprint: a5:46:50:c5:62:ea:95:9a:1a:a7:04:6f:17:58:c7:29:53:3d:03:fa +# SHA256 Fingerprint: 06:c0:8d:7d:af:d8:76:97:1e:b1:12:4f:e6:7f:84:7e:c0:c7:a1:58:d3:ea:53:cb:e9:40:e2:ea:97:91:f4:c3 +-----BEGIN CERTIFICATE----- +MIIFgDCCA2igAwIBAgIUHBjYz+VTPyI1RlNUJDxsR9FcSpwwDQYJKoZIhvcNAQEM +BQAwWDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgUlNBIFJvb3QgQ0EwHhcN +MjQwNTE1MDU0MTU3WhcNNDQwNTE1MDU0MTU2WjBYMQswCQYDVQQGEwJDTjElMCMG +A1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1 +c3RBc2lhIFRMUyBSU0EgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBAMMWuBtqpERz5dZO9LnPWwvB0ZqB9WOwj0PBuwhaGnrhB3YmH49pVr7+ +NmDQDIPNlOrnxS1cLwUWAp4KqC/lYCZUlviYQB2srp10Zy9U+5RjmOMmSoPGlbYJ +Q1DNDX3eRA5gEk9bNb2/mThtfWza4mhzH/kxpRkQcwUqwzIZheo0qt1CHjCNP561 +HmHVb70AcnKtEj+qpklz8oYVlQwQX1Fkzv93uMltrOXVmPGZLmzjyUT5tUMnCE32 +ft5EebuyjBza00tsLtbDeLdM1aTk2tyKjg7/D8OmYCYozza/+lcK7Fs/6TAWe8Tb +xNRkoDD75f0dcZLdKY9BWN4ArTr9PXwaqLEX8E40eFgl1oUh63kd0Nyrz2I8sMeX +i9bQn9P+PN7F4/w6g3CEIR0JwqH8uyghZVNgepBtljhb//HXeltt08lwSUq6HTrQ +UNoyIBnkiz/r1RYmNzz7dZ6wB3C4FGB33PYPXFIKvF1tjVEK2sUYyJtt3LCDs3+j +TnhMmCWr8n4uIF6CFabW2I+s5c0yhsj55NqJ4js+k8UTav/H9xj8Z7XvGCxUq0DT +bE3txci3OE9kxJRMT6DNrqXGJyV1J23G2pyOsAWZ1SgRxSHUuPzHlqtKZFlhaxP8 +S8ySpg+kUb8OWJDZgoM5pl+z+m6Ss80zDoWo8SnTq1mt1tve1CuBAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLgHkXlcBvRG/XtZylomkadFK/hT +MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAIZtqBSBdGBanEqT3 +Rz/NyjuujsCCztxIJXgXbODgcMTWltnZ9r96nBO7U5WS/8+S4PPFJzVXqDuiGev4 +iqME3mmL5Dw8veWv0BIb5Ylrc5tvJQJLkIKvQMKtuppgJFqBTQUYo+IzeXoLH5Pt +7DlK9RME7I10nYEKqG/odv6LTytpEoYKNDbdgptvT+Bz3Ul/KD7JO6NXBNiT2Twp +2xIQaOHEibgGIOcberyxk2GaGUARtWqFVwHxtlotJnMnlvm5P1vQiJ3koP26TpUJ +g3933FEFlJ0gcXax7PqJtZwuhfG5WyRasQmr2soaB82G39tp27RIGAAtvKLEiUUj +pQ7hRGU+isFqMB3iYPg6qocJQrmBktwliJiJ8Xw18WLK7nn4GS/+X/jbh87qqA8M +pugLoDzga5SYnH+tBuYc6kIQX+ImFTw3OffXvO645e8D7r0i+yiGNFjEWn9hongP +XvPKnbwbPKfILfanIhHKA9jnZwqKDss1jjQ52MjqjZ9k4DewbNfFj8GQYSbbJIwe +SsCI3zWQzj8C9GRh3sfIB5XeMhg6j6JCQCTl1jNdfK7vsU1P1FeQNWrcrgSXSYk0 +ly4wBOeY99sLAZDBHwo/+ML+TvrbmnNzFrwFuHnYWa8G5z9nODmxfKuU4CkUpijy +323imttUQ/hHWKNddBWcwauwxzQ= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH +# Subject: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH +# Label: "D-TRUST EV Root CA 2 2023" +# Serial: 139766439402180512324132425437959641711 +# MD5 Fingerprint: 96:b4:78:09:f0:09:cb:77:eb:bb:1b:4d:6f:36:bc:b6 +# SHA1 Fingerprint: a5:5b:d8:47:6c:8f:19:f7:4c:f4:6d:6b:b6:c2:79:82:22:df:54:8b +# SHA256 Fingerprint: 8e:82:21:b2:e7:d4:00:78:36:a1:67:2f:0d:cc:29:9c:33:bc:07:d3:16:f1:32:fa:1a:20:6d:58:71:50:f1:ce +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEVWIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUw +OTA5MTAzMlowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBANiOo4mAC7JXUtypU0w3uX9jFxPvp1sjW2l1sJkK +F8GLxNuo4MwxusLyzV3pt/gdr2rElYfXR8mV2IIEUD2BCP/kPbOx1sWy/YgJ25yE +7CUXFId/MHibaljJtnMoPDT3mfd/06b4HEV8rSyMlD/YZxBTfiLNTiVR8CUkNRFe +EMbsh2aJgWi6zCudR3Mfvc2RpHJqnKIbGKBv7FD0fUDCqDDPvXPIEysQEx6Lmqg6 +lHPTGGkKSv/BAQP/eX+1SH977ugpbzZMlWGG2Pmic4ruri+W7mjNPU0oQvlFKzIb +RlUWaqZLKfm7lVa/Rh3sHZMdwGWyH6FDrlaeoLGPaxK3YG14C8qKXO0elg6DpkiV +jTujIcSuWMYAsoS0I6SWhjW42J7YrDRJmGOVxcttSEfi8i4YHtAxq9107PncjLgc +jmgjutDzUNzPZY9zOjLHfP7KgiJPvo5iR2blzYfi6NUPGJ/lBHJLRjwQ8kTCZFZx +TnXonMkmdMV9WdEKWw9t/p51HBjGGjp82A0EzM23RWV6sY+4roRIPrN6TagD4uJ+ +ARZZaBhDM7DS3LAaQzXupdqpRlyuhoFBAUp0JuyfBr/CBTdkdXgpaP3F9ev+R/nk +hbDhezGdpn9yo7nELC7MmVcOIQxFAZRl62UJxmMiCzNJkkg8/M3OsD6Onov4/knF +NXJHAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUqvyREBuH +kV8Wub9PS5FeAByxMoAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfZXZfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQCTy6UfmRHsmg1fLBWTxj++EI14 +QvBukEdHjqOSMo1wj/Zbjb6JzkcBahsgIIlbyIIQbODnmaprxiqgYzWRaoUlrRc4 +pZt+UPJ26oUFKidBK7GB0aL2QHWpDsvxVUjY7NHss+jOFKE17MJeNRqrphYBBo7q +3C+jisosketSjl8MmxfPy3MHGcRqwnNU73xDUmPBEcrCRbH0O1P1aa4846XerOhU +t7KR/aypH/KH5BfGSah82ApB9PI+53c0BFLd6IHyTS9URZ0V4U/M5d40VxDJI3IX +cI1QcB9WbMy5/zpaT2N6w25lBx2Eof+pDGOJbbJAiDnXH3dotfyc1dZnaVuodNv8 +ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT +2vFp4LJiTZ6P/4mdm13NRemUAiKN4DV/6PEEeXFsVIP4M7kFMhtYVRFP0OUnR3Hs +7dpn1mKmS00PaaLJvOwiS5THaJQXfuKOKD62xur1NGyfN4gHONuGcfrNlUhDbqNP +gofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAst +Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh +XBxvWHZks/wCuPWdCg== +-----END CERTIFICATE----- + +# Issuer: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG +# Subject: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG +# Label: "SwissSign RSA TLS Root CA 2022 - 1" +# Serial: 388078645722908516278762308316089881486363258315 +# MD5 Fingerprint: 16:2e:e4:19:76:81:85:ba:8e:91:58:f1:15:ef:72:39 +# SHA1 Fingerprint: 81:34:0a:be:4c:cd:ce:cc:e7:7d:cc:8a:d4:57:e2:45:a0:77:5d:ce +# SHA256 Fingerprint: 19:31:44:f4:31:e0:fd:db:74:07:17:d4:de:92:6a:57:11:33:88:4b:43:60:d3:0e:27:29:13:cb:e6:60:ce:41 +-----BEGIN CERTIFICATE----- +MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UE +AxMiU3dpc3NTaWduIFJTQSBUTFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgx +MTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxT +d2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0EgVExTIFJvb3QgQ0Eg +MjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmjiC8NX +vDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7 +LCTLf5ImgKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX +5XH8irCRIFucdFJtrhUnWXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyE +EPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlfGUEGjw5NBuBwQCMBauTLE5tzrE0USJIt +/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36qOTw7D59Ke4LKa2/KIj4x +0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLOEGrOyvi5 +KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM +0ZPlEuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shd +OxtYk8EXlFXIC+OCeYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrta +clXvyFu1cvh43zcgTFeRc5JzrBh3Q4IgaezprClG5QtO+DdziZaKHG29777YtvTK +wP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQABo2MwYTAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow4UD2p8P98Q+4 +DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL +BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO3 +10aewCoSPY6WlkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgz +Hqp41eZUBDqyggmNzhYzWUUo8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQ +iJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zpy1FVCypM9fJkT6lc/2cyjlUtMoIc +gC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3CjlvrzG4ngRhZi0Rjn9UM +ZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6MOuhF +LhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJp +zv1/THfQwUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/Td +Ao9QAwKxuDdollDruF/UKIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0 +rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0nhzck5npgL7XTgwSqT0N1osGDsieYK7EO +gLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rwtnu64ZzZ +-----END CERTIFICATE----- + +# Issuer: CN=OISTE Server Root ECC G1 O=OISTE Foundation +# Subject: CN=OISTE Server Root ECC G1 O=OISTE Foundation +# Label: "OISTE Server Root ECC G1" +# Serial: 47819833811561661340092227008453318557 +# MD5 Fingerprint: 42:a7:d2:35:ae:02:92:db:19:76:08:de:2f:05:b4:d4 +# SHA1 Fingerprint: 3b:f6:8b:09:ae:2a:92:7b:ba:e3:8d:3f:11:95:d9:e6:44:0c:45:e2 +# SHA256 Fingerprint: ee:c9:97:c0:c3:0f:21:6f:7e:3b:8b:30:7d:2b:ae:42:41:2d:75:3f:c8:21:9d:af:d1:52:0b:25:72:85:0f:49 +-----BEGIN CERTIFICATE----- +MIICNTCCAbqgAwIBAgIQI/nD1jWvjyhLH/BU6n6XnTAKBggqhkjOPQQDAzBLMQsw +CQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwY +T0lTVEUgU2VydmVyIFJvb3QgRUNDIEcxMB4XDTIzMDUzMTE0NDIyOFoXDTQ4MDUy +NDE0NDIyN1owSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRp +b24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IEVDQyBHMTB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABBcv+hK8rBjzCvRE1nZCnrPoH7d5qVi2+GXROiFPqOujvqQy +cvO2Ackr/XeFblPdreqqLiWStukhEaivtUwL85Zgmjvn6hp4LrQ95SjeHIC6XG4N +2xml4z+cKrhAS93mT6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQ3 +TYhlz/w9itWj8UnATgwQb0K0nDAdBgNVHQ4EFgQUN02IZc/8PYrVo/FJwE4MEG9C +tJwwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYCMQCpKjAd0MKfkFFR +QD6VVCHNFmb3U2wIFjnQEnx/Yxvf4zgAOdktUyBFCxxgZzFDJe0CMQCSia7pXGKD +YmH5LVerVrkR3SW+ak5KGoJr3M/TvEqzPNcum9v4KGm8ay3sMaE641c= +-----END CERTIFICATE----- + +# Issuer: CN=OISTE Server Root RSA G1 O=OISTE Foundation +# Subject: CN=OISTE Server Root RSA G1 O=OISTE Foundation +# Label: "OISTE Server Root RSA G1" +# Serial: 113845518112613905024960613408179309848 +# MD5 Fingerprint: 23:a7:9e:d4:70:b8:b9:14:57:41:8a:7e:44:59:e2:68 +# SHA1 Fingerprint: f7:00:34:25:94:88:68:31:e4:34:87:3f:70:fe:86:b3:86:9f:f0:6e +# SHA256 Fingerprint: 9a:e3:62:32:a5:18:9f:fd:db:35:3d:fd:26:52:0c:01:53:95:d2:27:77:da:c5:9d:b5:7b:98:c0:89:a6:51:e6 +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIQVaXZZ5Qoxu0M+ifdWwFNGDANBgkqhkiG9w0BAQwFADBL +MQswCQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UE +AwwYT0lTVEUgU2VydmVyIFJvb3QgUlNBIEcxMB4XDTIzMDUzMTE0MzcxNloXDTQ4 +MDUyNDE0MzcxNVowSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5k +YXRpb24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IFJTQSBHMTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAKqu9KuCz/vlNwvn1ZatkOhLKdxVYOPM +vLO8LZK55KN68YG0nnJyQ98/qwsmtO57Gmn7KNByXEptaZnwYx4M0rH/1ow00O7b +rEi56rAUjtgHqSSY3ekJvqgiG1k50SeH3BzN+Puz6+mTeO0Pzjd8JnduodgsIUzk +ik/HEzxux9UTl7Ko2yRpg1bTacuCErudG/L4NPKYKyqOBGf244ehHa1uzjZ0Dl4z +O8vbUZeUapU8zhhabkvG/AePLhq5SvdkNCncpo1Q4Y2LS+VIG24ugBA/5J8bZT8R +tOpXaZ+0AOuFJJkk9SGdl6r7NH8CaxWQrbueWhl/pIzY+m0o/DjH40ytas7ZTpOS +jswMZ78LS5bOZmdTaMsXEY5Z96ycG7mOaES3GK/m5Q9l3JUJsJMStR8+lKXHiHUh +sd4JJCpM4rzsTGdHwimIuQq6+cF0zowYJmXa92/GjHtoXAvuY8BeS/FOzJ8vD+Ho +mnqT8eDI278n5mUpezbgMxVz8p1rhAhoKzYHKyfMeNhqhw5HdPSqoBNdZH702xSu ++zrkL8Fl47l6QGzwBrd7KJvX4V84c5Ss2XCTLdyEr0YconosP4EmQufU2MVshGYR +i3drVByjtdgQ8K4p92cIiBdcuJd5z+orKu5YM+Vt6SmqZQENghPsJQtdLEByFSnT +kCz3GkPVavBpAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU +8snBDw1jALvsRQ5KH7WxszbNDo0wHQYDVR0OBBYEFPLJwQ8NYwC77EUOSh+1sbM2 +zQ6NMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQwFAAOCAgEANGd5sjrG5T33 +I3K5Ce+SrScfoE4KsvXaFwyihdJ+klH9FWXXXGtkFu6KRcoMQzZENdl//nk6HOjG +5D1rd9QhEOP28yBOqb6J8xycqd+8MDoX0TJD0KqKchxRKEzdNsjkLWd9kYccnbz8 +qyiWXmFcuCIzGEgWUOrKL+mlSdx/PKQZvDatkuK59EvV6wit53j+F8Bdh3foZ3dP +AGav9LEDOr4SfEE15fSmG0eLy3n31r8Xbk5l8PjaV8GUgeV6Vg27Rn9vkf195hfk +gSe7BYhW3SCl95gtkRlpMV+bMPKZrXJAlszYd2abtNUOshD+FKrDgHGdPY3ofRRs +YWSGRqbXVMW215AWRqWFyp464+YTFrYVI8ypKVL9AMb2kI5Wj4kI3Zaq5tNqqYY1 +9tVFeEJKRvwDyF7YZvZFZSS0vod7VSCd9521Kvy5YhnLbDuv0204bKt7ph6N/Ome +/msVuduCmsuY33OhkKCgxeDoAaijFJzIwZqsFVAzje18KotzlUBDJvyBpCpfOZC3 +J8tRd/iWkx7P8nd9H0aTolkelUTFLXVksNb54Dxp6gS1HAviRkRNQzuXSXERvSS2 +wq1yVAb+axj5d9spLFKebXd7Yv0PTY6YMjAwcRLWJTXjn/hvnLXrahut6hDTlhZy +BiElxky8j3C7DOReIoMt0r7+hVu05L0= +-----END CERTIFICATE----- + +# Issuer: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd. +# Subject: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd. +# Label: "e-Szigno TLS Root CA 2023" +# Serial: 71934828665710877219916191754 +# MD5 Fingerprint: 6a:e9:99:74:a5:da:5e:f1:d9:2e:f2:c8:d1:86:8b:71 +# SHA1 Fingerprint: 6f:9a:d5:d5:df:e8:2c:eb:be:37:07:ee:4f:4f:52:58:29:41:d1:fe +# SHA256 Fingerprint: b4:91:41:50:2d:00:66:3d:74:0f:2e:7e:c3:40:c5:28:00:96:26:66:12:1a:36:d0:9c:f7:dd:2b:90:38:4f:b4 +-----BEGIN CERTIFICATE----- +MIICzzCCAjGgAwIBAgINAOhvGHvWOWuYSkmYCjAKBggqhkjOPQQDBDB1MQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xFzAVBgNVBGEMDlZBVEhVLTIzNTg0NDk3MSIwIAYDVQQDDBllLVN6aWdubyBU +TFMgUm9vdCBDQSAyMDIzMB4XDTIzMDcxNzE0MDAwMFoXDTM4MDcxNzE0MDAwMFow +dTELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRYwFAYDVQQKDA1NaWNy +b3NlYyBMdGQuMRcwFQYDVQRhDA5WQVRIVS0yMzU4NDQ5NzEiMCAGA1UEAwwZZS1T +emlnbm8gVExTIFJvb3QgQ0EgMjAyMzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE +AGgP36J8PKp0iGEKjcJMpQEiFNT3YHdCnAo4YKGMZz6zY+n6kbCLS+Y53wLCMAFS +AL/fjO1ZrTJlqwlZULUZwmgcAOAFX9pQJhzDrAQixTpN7+lXWDajwRlTEArRzT/v +SzUaQ49CE0y5LBqcvjC2xN7cS53kpDzLLtmt3999Cd8ukv+ho2MwYTAPBgNVHRMB +Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUWYQCYlpGePVd3I8K +ECgj3NXW+0UwHwYDVR0jBBgwFoAUWYQCYlpGePVd3I8KECgj3NXW+0UwCgYIKoZI +zj0EAwQDgYsAMIGHAkIBLdqu9S54tma4n7Zwf2Z0z+yOfP7AAXmazlIC58PRDHpt +y7Ve7hekm9sEdu4pKeiv+62sUvTXK9Z3hBC9xdIoaDQCQTV2WnXzkoYI9bIeCvZl +C9p2x1L/Cx6AcCIwwzPbGO2E14vs7dOoY4G1VnxHx1YwlGhza9IuqbnZLBwpvQy6 +uWWL +-----END CERTIFICATE----- + +# Issuer: CN=SECOM TLS RSA Root CA 2024 O=SECOM Trust Systems Co., Ltd. +# Subject: CN=SECOM TLS RSA Root CA 2024 O=SECOM Trust Systems Co., Ltd. +# Label: "SECOM TLS RSA Root CA 2024" +# Serial: 17188327524208271538 +# MD5 Fingerprint: d0:a4:db:32:eb:44:98:d2:62:0b:3e:bc:4d:7c:5c:e9 +# SHA1 Fingerprint: fb:97:96:7c:ef:8d:98:63:06:c0:3b:b6:11:f8:e0:13:97:a2:98:d3 +# SHA256 Fingerprint: 14:35:f2:25:c5:d2:52:d7:a2:19:48:cc:3c:e6:2a:ec:fa:88:00:1e:3d:d7:2d:1c:c3:55:51:00:eb:37:2f:93 +-----BEGIN CERTIFICATE----- +MIIFmjCCA4KgAwIBAgIJAO6JNNDLgOCyMA0GCSqGSIb3DQEBDAUAMFoxCzAJBgNV +BAYTAkpQMSYwJAYDVQQKEx1TRUNPTSBUcnVzdCBTeXN0ZW1zIENvLiwgTHRkLjEj +MCEGA1UEAxMaU0VDT00gVExTIFJTQSBSb290IENBIDIwMjQwHhcNMjQwMTMxMDUx +MTU1WhcNNDkwMTE0MDUxMTU1WjBaMQswCQYDVQQGEwJKUDEmMCQGA1UEChMdU0VD +T00gVHJ1c3QgU3lzdGVtcyBDby4sIEx0ZC4xIzAhBgNVBAMTGlNFQ09NIFRMUyBS +U0EgUm9vdCBDQSAyMDI0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +4TjizUwzxbInq8Tx11gaFYNk5fO+34y7TyM4neh0UgL5JIZbJNLTz2x//L/B71+5 +m6X6nGIr7d4lFJBGtjO677hXOz93zkcWaUTm3VbOAjBlt4YWxlcccBHXuZ7o3Q+4 +R+ormrBdHeJ1CTUEG8ttQbKIl3G7OZYbnH8/pP8cjPub/0kDVNuMzp7xsVRROOis +Qt53fMoJLlYgoebbuMphOqMCtjkJ7R6efEMfLp8UAVi9ZaLRn76ET/CJkk925ndu +uufC4BatS4mnXFmxN0vUXb0ij9B8O/D8gixQEsVSD4GK8FWRPh3bVd/6bzdkHGJj +y21XI0yejVomZUbRrOfNuz0boPGV1pt18fFC39IHQEth3OFqb5NDO3L+A9bNqTgA +yUgRmIn4ucgDc/Ri/Km3V51ueZjy1/yk0qwJVadAVVrCt56iNeXOyEvzJADGgDQ8 +E1Pdaqct8Cynz/47ReQM62vFYO08wcQkrjmX/tesiko1V1yyaf6EfPzUFzmaGy9x +vkCwdbm15EdTolOjE0H2Vb5/APDOyCFEokiYGmXTLdAUl0wKZ4IyjkHGzy0jhpaX +EXE/GJcEvI6VzEchjaBL03EJ0h9pG4OqeIOycKvAo3A+TbetyfsrgYyHzU0a7/qU +jGat1AAq1nVljMpKqpinPTsf/d9H39FTUeJL7TpzzjUCAwEAAaNjMGEwHQYDVR0O +BBYEFCzrchKOWHdkNRVWNQFXB6l9DTbmMB8GA1UdIwQYMBaAFCzrchKOWHdkNRVW +NQFXB6l9DTbmMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqG +SIb3DQEBDAUAA4ICAQAVwsvluSafaez5tFPR/hRTBzRxEyMMQF3XJXCVi3yegZyK +oec7hmE6jx2ZM8KgM1kn2yJRwFXHX8zUW9nBLEDWc4wuE8LrlZqhGZM9pJQXGmGz +ResDJV6JgRBna+j4sA1M7yIdlvL0sAfFXFCTRaWTD4E1V99RLrFzWfTcC+e180hD +uNMpqOEo46+lMeW/Wvh7ifOQs+kiK0O2gHxQDNxslSavnCs4V7l8HRDJ2La10o70 +Bo7VLzf1W8MBvv0VTnxB+NjT5qTAbhGFh9Gvp4BaJpmdUf0C5CEP6dbQlfgxWfzY +r69yVT6dPQB+GFEaY03IMY+AcBCs+om1fNxrQXt9zoofMBNFbLhvpNH/JsXWdGUz +fNbO12uswTa5wah8LB18FTQN2/zPHYmvBEoLuyUgZ09VNLJo5YA0kXItVYkLjMe2 +SixzK4scUHv81IK99I91DWx7FwMVKw2xgFp+ZLYB2dnpQQrqwlW64glHUcK2N9BD +snjLSxeZ+UPECh9RxH4WAcKiZW+cqaKMmhP2WBfR4IcR7NOL32ml11ds87hhV1CZ +WWFCJAcCidYZz6CZa8exzHojP9SB5RH0/v1KdHAisqhSjtJl/UIAHIQ48elOn8wr +TdFap4Yb5aHglmMeNx+fAIhDluWVfxTO7H4dTPU+SFVRMLAh+wwKZfqb94nMeQ== +-----END CERTIFICATE----- + +# Issuer: CN=SECOM TLS ECC Root CA 2024 O=SECOM Trust Systems Co., Ltd. +# Subject: CN=SECOM TLS ECC Root CA 2024 O=SECOM Trust Systems Co., Ltd. +# Label: "SECOM TLS ECC Root CA 2024" +# Serial: 9329818985461676612 +# MD5 Fingerprint: 99:d3:9d:e4:d2:b1:2d:f0:2a:04:67:85:f3:df:46:d6 +# SHA1 Fingerprint: 7a:1f:22:2d:72:b2:c3:19:87:44:db:61:69:e8:a6:4b:d7:0d:44:0e +# SHA256 Fingerprint: 6a:b2:ab:75:f5:1c:b4:f4:f0:15:62:03:fb:f6:f6:46:23:2f:51:4b:e0:59:f6:28:33:30:8b:82:b4:d7:2d:b1 +-----BEGIN CERTIFICATE----- +MIICTDCCAdGgAwIBAgIJAIF6LO+PI3pEMAoGCCqGSM49BAMDMFoxCzAJBgNVBAYT +AkpQMSYwJAYDVQQKEx1TRUNPTSBUcnVzdCBTeXN0ZW1zIENvLiwgTHRkLjEjMCEG +A1UEAxMaU0VDT00gVExTIEVDQyBSb290IENBIDIwMjQwHhcNMjQwMTMxMDU1MjM0 +WhcNNDkwMTE0MDU1MjM0WjBaMQswCQYDVQQGEwJKUDEmMCQGA1UEChMdU0VDT00g +VHJ1c3QgU3lzdGVtcyBDby4sIEx0ZC4xIzAhBgNVBAMTGlNFQ09NIFRMUyBFQ0Mg +Um9vdCBDQSAyMDI0MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE7NzFMtu9dzQXSNC1 +2fabk0+GlC5finB3R7XaZonRUd20aFiWObtuNBCLUZSfk6QXAE55BjEXsXQ/NG8y +UqicXjsu9ksDK3JZBgCwLOVh6+nwJXTvso/dEj/GUYH5mBdoo2MwYTAdBgNVHQ4E +FgQUO3YReyl04k4GTFaCQNAhL3qzydUwHwYDVR0jBBgwFoAUO3YReyl04k4GTFaC +QNAhL3qzydUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZI +zj0EAwMDaQAwZgIxAN3ib8fi1pMYtAPjMilB5e5/H+t5CL0xPL+cZ5oTTZuSCjpA +n1v7F/VAr8bFxQXAowIxAKsBVO1ACFp7skwzPvdv1EUY5a897WGLT4lb+bjxFAWy +l8wDcZJdwGZ/pAHxt1AJ1g== +-----END CERTIFICATE----- + +# Issuer: CN=Telia EC TLS Root CA v3 O=Telia Company AB +# Subject: CN=Telia EC TLS Root CA v3 O=Telia Company AB +# Label: "Telia EC TLS Root CA v3" +# Serial: 8028200332079443110287896228123317 +# MD5 Fingerprint: b6:fa:6a:5c:42:fb:c5:67:72:c0:e0:2f:72:fb:5a:44 +# SHA1 Fingerprint: b4:d6:07:c2:a5:95:bc:5b:f4:67:4d:c9:dc:6f:6f:0a:00:7a:a5:35 +# SHA256 Fingerprint: 09:8e:08:a9:1d:bb:f7:74:78:b9:6c:ce:b8:9b:14:13:a5:da:37:b7:c8:62:60:6a:95:5d:eb:07:17:9f:43:26 +-----BEGIN CERTIFICATE----- +MIICMjCCAbegAwIBAgIPAYvSIlRjTQSLbOVHH9K1MAoGCCqGSM49BAMDMEoxCzAJ +BgNVBAYTAlNFMRkwFwYDVQQKDBBUZWxpYSBDb21wYW55IEFCMSAwHgYDVQQDDBdU +ZWxpYSBFQyBUTFMgUm9vdCBDQSB2MzAeFw0yMzExMTUwODU1MjZaFw00ODA1MjMx +MTAwMDBaMEoxCzAJBgNVBAYTAlNFMRkwFwYDVQQKDBBUZWxpYSBDb21wYW55IEFC +MSAwHgYDVQQDDBdUZWxpYSBFQyBUTFMgUm9vdCBDQSB2MzB2MBAGByqGSM49AgEG +BSuBBAAiA2IABMHIlhVDLbmFKUpW0iK4dpryT6emYOeS31JPwWnWPmkWRrAkTbPX +40sQfHI9mpR7Rbktu3ngg6W+BBSXSechtMCnBmWXj/EaVlmV5cY1jD2HoTfhBQ3A +acpCNMLJK4NpZaNjMGEwHwYDVR0jBBgwFoAU1GToQ4g6cy/QGnGCNgtehd7H3kMw +HQYDVR0OBBYEFNRk6EOIOnMv0BpxgjYLXoXex95DMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2kAMGYCMQCXAUdS/9bbJ8A1JYaG +f/bWt/s7Ta0ot5Ulno8OjSNYRWQIlS4tVWldvTAVA7heOFgCMQCvKr8+Z2Rn+OBr +5UHzlgBObpad1LuwNTRcdNgUJxIWadcki+UBLEi1/AURKV5md2M= +-----END CERTIFICATE----- + +# Issuer: CN=Telia RSA TLS Root CA v3 O=Telia Company AB +# Subject: CN=Telia RSA TLS Root CA v3 O=Telia Company AB +# Label: "Telia RSA TLS Root CA v3" +# Serial: 8028214673410753541795188766939845 +# MD5 Fingerprint: f4:8b:9c:f3:f8:63:cf:dc:25:8f:b4:bb:a2:e9:9d:e2 +# SHA1 Fingerprint: b5:2e:88:4e:40:c1:11:fb:50:c7:e2:4f:ac:18:2b:bd:68:15:d2:34 +# SHA256 Fingerprint: d1:3d:b1:29:4c:45:eb:c6:fc:86:c6:bb:f6:9f:a2:9b:df:e6:92:df:f7:c7:13:c2:43:c7:a9:56:c6:a2:28:4c +-----BEGIN CERTIFICATE----- +MIIFgjCCA2qgAwIBAgIPAYvSUKtCVSxHWr2h3BrFMA0GCSqGSIb3DQEBDAUAMEsx +CzAJBgNVBAYTAlNFMRkwFwYDVQQKDBBUZWxpYSBDb21wYW55IEFCMSEwHwYDVQQD +DBhUZWxpYSBSU0EgVExTIFJvb3QgQ0EgdjMwHhcNMjMxMTE1MDk0NzQyWhcNNDgw +NTIzMTEwMDAwWjBLMQswCQYDVQQGEwJTRTEZMBcGA1UECgwQVGVsaWEgQ29tcGFu +eSBBQjEhMB8GA1UEAwwYVGVsaWEgUlNBIFRMUyBSb290IENBIHYzMIICIjANBgkq +hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsV89KG19hCf4S1Fvk8D3TyDERhmcvx8F +7Kmb4WATx3ije1id3KHxRE0TKmcNCbAQ57bvHFEYa4hR2l20VjVadExqOW+2ld99 +MbEiO+jRVOz+BbxLxJnmGwCqI+BfuTjjVReDxsxjQvjgBsClaO/sm5i70nlZcWGR +tIkvWDK3NNkT5RtwXc/O8NTFVpbUqT6cRjIj3olAblR+lRf4Ffy5o+Q9fabjYn9Z +9S4itruElcEFf9Ljk7fwdTycT/rvJW9w/B3G2a3r0f/zXNOVruIBcqE6pkSospAC +U2bG42fYKrbM/GWnp7u+p9Frz4jaNwpb4YHuEeS8BratNcP8X62jXIvvKHxlsMDJ +Cnb4U8JzFOLsU6mohVY58BdZrvi0Gk9UOuqmgoG6dskHoksjZTlK61D/InzmEoA1 +yAYJFDVysjRxDUOu9cAwANbqmq77WIFL6BpnZgVqPtMfG6wN8BrTKdapvilVsYR5 +9BFgIsAVBMxrGh+W+QcvmJafUpASvlArKvVG2FI4i6PiLjSBT0+6F6EQLrYqefOQ +F/fBNEXb+njUQ0SUVrAqtH4Y+OjCI/a4/JJQppxeemZcQ0SUShgiI5AM5xHO5iya +UrTjYH4zxUz9j+1FEbDH/xpstr1gXBykspup+hRTaJcbA+UbpJqtWZndAPddJmt6 +YJQ+dU3pDu8CAwEAAaNjMGEwHwYDVR0jBBgwFoAUsMep0t2yKFZzBJSMFFxIbzdS +kqgwHQYDVR0OBBYEFLDHqdLdsihWcwSUjBRcSG83UpKoMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBDAUAA4ICAQBdYzFsNGDRk7bR +/AgRKq+5637YuOW+w6uhpoS0VnKMUpyHCwku86hEvqivakPtfmlm4bFwt++sb/8O +XsWBqtfbXMaBNDTZl8XRMJuLWOW2JrbKkRzgG0eBUcvsadG1rrhbmZqYvFXaAZO7 +o4TdOZzxhBB5GOAWWXB3IeraNP4J63zyo9n8Gqw3sJBG44em5hoYjBffP+npibys +lnslRi4L6xHsCYj/Pab+OlqbMCB6v+sTCLeEIukRVzoR9aQ45pEK7Z1QBnSsbAKQ +tss0JKD9d/mX143H1xePjPhTXlv5JCkhrcj+SShz0P9+EHoWe6m9lyUEOIVn0rp+ +yVJWNbmyDv3VkwFxHC1ApSQsgSimjGQ4wtr6cSmordYxkV+Ro8lOIIhRksXPyDk2 +7gW6IjUXCkZKpxFjkL3jiBSc8SkxnwCWtXg8xwNwdFVNBGLCCuJnsneYXjJNqzRq +UcoGwzsvF3Qi/ZnHUNvISdevlgIAXL4Wvrxaqvoa01wB+GCfs57RTGE4TvAGhKNK +us8K3hRT1BSpigzMIRzSxtAOrqPN6j//QSmW9f8Jcncri4j2ihSpVrFU0NdNkMhZ +eAKidTFPsxCVFuW4Aniz7jqiw5sWtjbQrlW035izIEU4sYwQoC1Nx0Svy+mMTRai +50LqFQ+A1/Hq6xHHDNx7CI83d23Erw== +-----END CERTIFICATE----- diff --git a/source/target-vendors/cp312/certifi/core.py b/source/target-vendors/cp312/certifi/core.py new file mode 100644 index 0000000..1c9661c --- /dev/null +++ b/source/target-vendors/cp312/certifi/core.py @@ -0,0 +1,83 @@ +""" +certifi.py +~~~~~~~~~~ + +This module returns the installation location of cacert.pem or its contents. +""" +import sys +import atexit + +def exit_cacert_ctx() -> None: + _CACERT_CTX.__exit__(None, None, None) # type: ignore[union-attr] + + +if sys.version_info >= (3, 11): + + from importlib.resources import as_file, files + + _CACERT_CTX = None + _CACERT_PATH = None + + def where() -> str: + # This is slightly terrible, but we want to delay extracting the file + # in cases where we're inside of a zipimport situation until someone + # actually calls where(), but we don't want to re-extract the file + # on every call of where(), so we'll do it once then store it in a + # global variable. + global _CACERT_CTX + global _CACERT_PATH + if _CACERT_PATH is None: + # This is slightly janky, the importlib.resources API wants you to + # manage the cleanup of this file, so it doesn't actually return a + # path, it returns a context manager that will give you the path + # when you enter it and will do any cleanup when you leave it. In + # the common case of not needing a temporary file, it will just + # return the file system location and the __exit__() is a no-op. + # + # We also have to hold onto the actual context manager, because + # it will do the cleanup whenever it gets garbage collected, so + # we will also store that at the global level as well. + _CACERT_CTX = as_file(files("certifi").joinpath("cacert.pem")) + _CACERT_PATH = str(_CACERT_CTX.__enter__()) + atexit.register(exit_cacert_ctx) + + return _CACERT_PATH + + def contents() -> str: + return files("certifi").joinpath("cacert.pem").read_text(encoding="ascii") + +else: + + from importlib.resources import path as get_path, read_text + + _CACERT_CTX = None + _CACERT_PATH = None + + def where() -> str: + # This is slightly terrible, but we want to delay extracting the + # file in cases where we're inside of a zipimport situation until + # someone actually calls where(), but we don't want to re-extract + # the file on every call of where(), so we'll do it once then store + # it in a global variable. + global _CACERT_CTX + global _CACERT_PATH + if _CACERT_PATH is None: + # This is slightly janky, the importlib.resources API wants you + # to manage the cleanup of this file, so it doesn't actually + # return a path, it returns a context manager that will give + # you the path when you enter it and will do any cleanup when + # you leave it. In the common case of not needing a temporary + # file, it will just return the file system location and the + # __exit__() is a no-op. + # + # We also have to hold onto the actual context manager, because + # it will do the cleanup whenever it gets garbage collected, so + # we will also store that at the global level as well. + _CACERT_CTX = get_path("certifi", "cacert.pem") + _CACERT_PATH = str(_CACERT_CTX.__enter__()) + atexit.register(exit_cacert_ctx) + + return _CACERT_PATH + + def contents() -> str: + return read_text("certifi", "cacert.pem", encoding="ascii") diff --git a/source/target-vendors/cp312/certifi/py.typed b/source/target-vendors/cp312/certifi/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/certifi/tests/__init__.py b/source/target-vendors/cp312/certifi/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/certifi/tests/test_certify.py b/source/target-vendors/cp312/certifi/tests/test_certify.py new file mode 100644 index 0000000..54670ea --- /dev/null +++ b/source/target-vendors/cp312/certifi/tests/test_certify.py @@ -0,0 +1,18 @@ +import os +import unittest + +import certifi + + +class TestCertifi(unittest.TestCase): + def test_cabundle_exists(self) -> None: + assert os.path.exists(certifi.where()) + + def test_read_contents(self) -> None: + content = certifi.contents() + assert "-----BEGIN CERTIFICATE-----" in content + + def test_py_typed_exists(self) -> None: + assert os.path.exists( + os.path.join(os.path.dirname(certifi.__file__), 'py.typed') + ) diff --git a/source/target-vendors/cp312/cffi-2.1.1.dist-info/METADATA b/source/target-vendors/cp312/cffi-2.1.1.dist-info/METADATA new file mode 100644 index 0000000..7c07654 --- /dev/null +++ b/source/target-vendors/cp312/cffi-2.1.1.dist-info/METADATA @@ -0,0 +1,67 @@ +Metadata-Version: 2.4 +Name: cffi +Version: 2.1.1 +Summary: Foreign Function Interface for Python calling C code. +Author: Armin Rigo, Maciej Fijalkowski +Maintainer: Matt Davis, Matt Clay +License-Expression: MIT-0 +Project-URL: Documentation, https://cffi.readthedocs.io/ +Project-URL: Changelog, https://cffi.readthedocs.io/en/latest/whatsnew.html +Project-URL: Download, https://github.com/python-cffi/cffi/releases +Project-URL: Contact, https://groups.google.com/forum/#!forum/python-cffi +Project-URL: Source Code, https://github.com/python-cffi/cffi +Project-URL: Issue Tracker, https://github.com/python-cffi/cffi/issues +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: 3.15 +Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta +Classifier: Programming Language :: Python :: Implementation :: CPython +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: pycparser; implementation_name != "PyPy" +Dynamic: license-file + +[![GitHub Actions Status](https://github.com/python-cffi/cffi/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/python-cffi/cffi/actions/workflows/ci.yaml?query=branch%3Amain++) +[![PyPI version](https://img.shields.io/pypi/v/cffi.svg)](https://pypi.org/project/cffi) +[![Read the Docs](https://img.shields.io/badge/docs-latest-blue.svg)][Documentation] + + +CFFI +==== + +Foreign Function Interface for Python calling C code. + +Please see the [Documentation] or uncompiled in the `doc/` subdirectory. + +Download +-------- + +[Download page](https://github.com/python-cffi/cffi/releases) + +Source Code +----------- + +Source code is publicly available on +[GitHub](https://github.com/python-cffi/cffi). + +Contact +------- + +[Mailing list](https://groups.google.com/forum/#!forum/python-cffi) + +Testing/development tips +------------------------ + +After `git clone` or `wget && tar`, we will get a directory called `cffi` or `cffi-x.x.x`. We call it `repo-directory`. To run tests under CPython, run the following in the `repo-directory`: + + pip install pytest + pip install -e . # editable install of CFFI for local development + pytest src/c/ testing/ + +[Documentation]: http://cffi.readthedocs.org/ diff --git a/source/target-vendors/cp312/cffi-2.1.1.dist-info/RECORD b/source/target-vendors/cp312/cffi-2.1.1.dist-info/RECORD new file mode 100644 index 0000000..9476fc0 --- /dev/null +++ b/source/target-vendors/cp312/cffi-2.1.1.dist-info/RECORD @@ -0,0 +1,31 @@ +_cffi_backend.cpython-312-x86_64-linux-gnu.so,sha256=m8HQgHIlWl7C2xD8dd8d2ecwl6dc2RUM_odevWcS8Xw,348856 +cffi/__init__.py,sha256=xHstsoxfGX4kLU3Ur5W367DaZXY_x2lyyv0G-T6ULNo,511 +cffi/_cffi_errors.h,sha256=Wzg-0Bu1_K2GEK0pSU5b6Q2GAAudcaF87HNG8LoFmWw,3583 +cffi/_cffi_gen_src.py,sha256=yUR53pFSbsa3ssF7_VOqXTaKeL5AJTHM_dyZ9IGY9sw,7044 +cffi/_cffi_include.h,sha256=zY6KFoo_bpa02PagQs1hzeW9O56snioloyxqvRKAot8,15234 +cffi/_embedding.h,sha256=PhAdAMcKClATVZ-lBcZeDEc12F6FqVdaHtaJ2Pkg6E8,17554 +cffi/_imp_emulation.py,sha256=RxREG8zAbI2RPGBww90u_5fi8sWdahpdipOoPzkp7C0,2960 +cffi/_shimmed_dist_utils.py,sha256=Bjj2wm8yZbvFvWEx5AEfmqaqZyZFhYfoyLLQHkXZuao,2230 +cffi/api.py,sha256=cWX1ZlscmOyTkvthiBpUWLcSRBtqlpn-moBu4esBmzI,42013 +cffi/backend_ctypes.py,sha256=T7CSc14P5vyloi5hyox68qhBWX079owHXaGDbY_U7bo,42283 +cffi/cffi_opcode.py,sha256=5-1cU6ppHGzsstF_eoglD_8tCDk76_Zhkzj3fW4Sic0,5723 +cffi/commontypes.py,sha256=7N6zPtCFlvxXMWhHV08psUjdYIK2XgsN3yo5dgua_v4,2805 +cffi/cparser.py,sha256=WY-ox4UGSuVSYTtgm02FmGgbmcuaaBK3moFlkdFSmsU,44754 +cffi/error.py,sha256=v6xTiS4U0kvDcy4h_BDRo5v39ZQuj-IMRYLv5ETddZs,877 +cffi/ffiplatform.py,sha256=avxFjdikYGJoEtmJO7ewVmwG_VEVl6EZ_WaNhZYCqv4,3584 +cffi/gen_src.py,sha256=TU_ZVDwoLnHPlal8_mPmpxqyWJ7U752jTc3UsTmE1QI,132 +cffi/lock.py,sha256=l9TTdwMIMpi6jDkJGnQgE9cvTIR7CAntIJr8EGHt3pY,747 +cffi/model.py,sha256=cA-bEAibcHUbtbqfgMq0CLQjFPAjV0JSRzMyKViNgF4,21752 +cffi/parse_c_type.h,sha256=OdwQfwM9ktq6vlCB43exFQmxDBtj2MBNdK8LYl15tjw,5976 +cffi/pkgconfig.py,sha256=uyt117TQeea3djagM1W5go8RpnySGTgoCzN8QHyZdlc,4365 +cffi/recompiler.py,sha256=TJVF07mFzYGTV6szp2zPrJd2dJKHN-eB_SuiJiGl9Kw,65140 +cffi/setuptools_ext.py,sha256=pZrnTqVCkGMqu8NxIxkdbItl65gehMP1LoraUdT7-4U,9566 +cffi/vengine_cpy.py,sha256=rRouYQ6BllAIXNjZGTPUW54rB952qi3gcm16SWXWKLc,43030 +cffi/vengine_gen.py,sha256=Z_KqaF6r54n5i4hjQThZdb-1DBTEimnU4y4E_KxSCDg,26931 +cffi/verifier.py,sha256=xIzV8iDMhZ1-qhX3RRX4DWo2k07WpcJxdhhAFXAjrmY,11154 +cffi-2.1.1.dist-info/METADATA,sha256=VYuvuQk6jnkBddns_5aIOCmB10SHfa2G_Wdw2w9NyRg,2527 +cffi-2.1.1.dist-info/WHEEL,sha256=UErIDMlRkMma6hd1UFY9eTm4_KaxQxwBwNQ150r7vQ4,151 +cffi-2.1.1.dist-info/entry_points.txt,sha256=prYQgBH44nBQ_lJpu6iMgFG0FGl4gNOwtvvjoDoLABw,132 +cffi-2.1.1.dist-info/top_level.txt,sha256=rE7WR3rZfNKxWI9-jn6hsHCAl7MDkB-FmuQbxWjFehQ,19 +cffi-2.1.1.dist-info/RECORD,, +cffi-2.1.1.dist-info/licenses/LICENSE,sha256=W6JN3FcGf5JJrdZEw6_EGl1tw34jQz73Wdld83Cwr2M,1123 diff --git a/source/target-vendors/cp312/cffi-2.1.1.dist-info/WHEEL b/source/target-vendors/cp312/cffi-2.1.1.dist-info/WHEEL new file mode 100644 index 0000000..50a7ad2 --- /dev/null +++ b/source/target-vendors/cp312/cffi-2.1.1.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: setuptools (83.0.0) +Root-Is-Purelib: false +Tag: cp312-cp312-manylinux_2_17_x86_64 +Tag: cp312-cp312-manylinux2014_x86_64 + diff --git a/source/target-vendors/cp312/cffi-2.1.1.dist-info/entry_points.txt b/source/target-vendors/cp312/cffi-2.1.1.dist-info/entry_points.txt new file mode 100644 index 0000000..46bb21b --- /dev/null +++ b/source/target-vendors/cp312/cffi-2.1.1.dist-info/entry_points.txt @@ -0,0 +1,5 @@ +[console_scripts] +cffi-gen-src = cffi._cffi_gen_src:run + +[distutils.setup_keywords] +cffi_modules = cffi.setuptools_ext:cffi_modules diff --git a/source/target-vendors/cp312/cffi-2.1.1.dist-info/licenses/LICENSE b/source/target-vendors/cp312/cffi-2.1.1.dist-info/licenses/LICENSE new file mode 100644 index 0000000..0a1dbfb --- /dev/null +++ b/source/target-vendors/cp312/cffi-2.1.1.dist-info/licenses/LICENSE @@ -0,0 +1,23 @@ + +Except when otherwise stated (look for LICENSE files in directories or +information at the beginning of each file) all software and +documentation is licensed as follows: + + MIT No Attribution + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the + Software is furnished to do so. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + diff --git a/source/target-vendors/cp312/cffi-2.1.1.dist-info/top_level.txt b/source/target-vendors/cp312/cffi-2.1.1.dist-info/top_level.txt new file mode 100644 index 0000000..f645779 --- /dev/null +++ b/source/target-vendors/cp312/cffi-2.1.1.dist-info/top_level.txt @@ -0,0 +1,2 @@ +_cffi_backend +cffi diff --git a/source/target-vendors/cp312/cffi/__init__.py b/source/target-vendors/cp312/cffi/__init__.py new file mode 100644 index 0000000..93dcdb3 --- /dev/null +++ b/source/target-vendors/cp312/cffi/__init__.py @@ -0,0 +1,14 @@ +__all__ = ['FFI', 'VerificationError', 'VerificationMissing', 'CDefError', + 'FFIError'] + +from .api import FFI +from .error import CDefError, FFIError, VerificationError, VerificationMissing +from .error import PkgConfigError + +__version__ = "2.1.1" +__version_info__ = (2, 1, 1) + +# The verifier module file names are based on the CRC32 of a string that +# contains the following version number. It may be older than __version__ +# if nothing is clearly incompatible. +__version_verifier_modules__ = "0.8.6" diff --git a/source/target-vendors/cp312/cffi/_cffi_errors.h b/source/target-vendors/cp312/cffi/_cffi_errors.h new file mode 100644 index 0000000..0931379 --- /dev/null +++ b/source/target-vendors/cp312/cffi/_cffi_errors.h @@ -0,0 +1,135 @@ +#ifndef CFFI_MESSAGEBOX +# ifdef _MSC_VER +# define CFFI_MESSAGEBOX 1 +# else +# define CFFI_MESSAGEBOX 0 +# endif +#endif + + +#if CFFI_MESSAGEBOX +/* Windows only: logic to take the Python-CFFI embedding logic + initialization errors and display them in a background thread + with MessageBox. The idea is that if the whole program closes + as a result of this problem, then likely it is already a console + program and you can read the stderr output in the console too. + If it is not a console program, then it will likely show its own + dialog to complain, or generally not abruptly close, and for this + case the background thread should stay alive. +*/ +static void *volatile _cffi_bootstrap_text; + +static PyObject *_cffi_start_error_capture(void) +{ + PyObject *result = NULL; + PyObject *x, *m, *bi; + + if (InterlockedCompareExchangePointer(&_cffi_bootstrap_text, + (void *)1, NULL) != NULL) + return (PyObject *)1; + + m = PyImport_AddModule("_cffi_error_capture"); + if (m == NULL) + goto error; + + result = PyModule_GetDict(m); + if (result == NULL) + goto error; + + bi = PyImport_ImportModule("builtins"); + if (bi == NULL) + goto error; + PyDict_SetItemString(result, "__builtins__", bi); + Py_DECREF(bi); + + x = PyRun_String( + "import sys\n" + "class FileLike:\n" + " def write(self, x):\n" + " try:\n" + " of.write(x)\n" + " except: pass\n" + " self.buf += x\n" + " def flush(self):\n" + " pass\n" + "fl = FileLike()\n" + "fl.buf = ''\n" + "of = sys.stderr\n" + "sys.stderr = fl\n" + "def done():\n" + " sys.stderr = of\n" + " return fl.buf\n", /* make sure the returned value stays alive */ + Py_file_input, + result, result); + Py_XDECREF(x); + + error: + if (PyErr_Occurred()) + { + PyErr_WriteUnraisable(Py_None); + PyErr_Clear(); + } + return result; +} + +#pragma comment(lib, "user32.lib") + +static DWORD WINAPI _cffi_bootstrap_dialog(LPVOID ignored) +{ + Sleep(666); /* may be interrupted if the whole process is closing */ + MessageBoxW(NULL, (wchar_t *)_cffi_bootstrap_text, + L"Python-CFFI error", + MB_OK | MB_ICONERROR); + _cffi_bootstrap_text = NULL; + return 0; +} + +static void _cffi_stop_error_capture(PyObject *ecap) +{ + PyObject *s; + void *text; + + if (ecap == (PyObject *)1) + return; + + if (ecap == NULL) + goto error; + + s = PyRun_String("done()", Py_eval_input, ecap, ecap); + if (s == NULL) + goto error; + + /* Show a dialog box, but in a background thread, and + never show multiple dialog boxes at once. */ + text = PyUnicode_AsWideCharString(s, NULL); + + _cffi_bootstrap_text = text; + + if (text != NULL) + { + HANDLE h; + h = CreateThread(NULL, 0, _cffi_bootstrap_dialog, + NULL, 0, NULL); + if (h != NULL) + CloseHandle(h); + } + /* decref the string, but it should stay alive as 'fl.buf' + in the small module above. It will really be freed only if + we later get another similar error. So it's a leak of at + most one copy of the small module. That's fine for this + situation which is usually a "fatal error" anyway. */ + Py_DECREF(s); + PyErr_Clear(); + return; + + error: + _cffi_bootstrap_text = NULL; + PyErr_Clear(); +} + +#else + +static PyObject *_cffi_start_error_capture(void) { return NULL; } +static void _cffi_stop_error_capture(PyObject *ecap) { } + +#endif diff --git a/source/target-vendors/cp312/cffi/_cffi_gen_src.py b/source/target-vendors/cp312/cffi/_cffi_gen_src.py new file mode 100644 index 0000000..3d4fb7e --- /dev/null +++ b/source/target-vendors/cp312/cffi/_cffi_gen_src.py @@ -0,0 +1,216 @@ +# Integrated from the cffi-buildtool project by Rose Davidson +# (https://github.com/inklesspen/cffi-buildtool), under the following +# license: +# +# MIT License +# +# Copyright (c) 2024, Rose Davidson +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice (including the +# next paragraph) shall be included in all copies or substantial portions +# of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Implementation of the ``cffi-gen-src`` command-line tool. + +This module is private; the command line is the only supported +interface. Two subcommands: + +``exec-python`` + Execute a Python script that constructs a :class:`cffi.FFI` + (the same kind of script that the CFFI docs' "Main mode of usage" + describes) and emit the generated C source. + +``read-sources`` + Create the :class:`cffi.FFI` from a separate ``cdef`` file and C + source prelude, then emit the generated C source. +""" + +import argparse +import io +import os +import sys + +from .api import FFI + + +def _execfile(pysrc, filename, globs): + compiled = compile(source=pysrc, filename=filename, mode='exec') + exec(compiled, globs, globs) + + +def find_ffi_in_python_script(pysrc, filename, ffivar): + """Execute ``pysrc`` and return the :class:`FFI` object it defines. + + The script is executed with ``__name__`` set to ``"cffi.gen_src"``, + so a trailing ``if __name__ == "__main__": ffibuilder.compile()`` + block in the script is skipped. + + ``ffivar`` is the name bound by the script to the :class:`FFI` + object, or to a callable that returns one. + + Raises :class:`NameError` if the name is not bound by the script, + or :class:`TypeError` if the name does not resolve to an + :class:`FFI` instance. + """ + filename = os.path.abspath(filename) + globs = {'__name__': 'cffi.gen_src', '__file__': filename} + old_path = sys.path[:] + sys.path.insert(0, os.path.dirname(filename)) + try: + _execfile(pysrc, filename, globs) + if ffivar not in globs: + raise NameError( + "Expected to find the FFI object with the name %r, " + "but it was not found." % (ffivar,) + ) + ffi = globs[ffivar] + if not isinstance(ffi, FFI) and callable(ffi): + # Maybe it's a callable that returns a FFI + ffi = ffi() + if not isinstance(ffi, FFI): + raise TypeError( + "Found an object with the name %r but it was not an " + "instance of cffi.api.FFI" % (ffivar,) + ) + return ffi + finally: + sys.path[:] = old_path + + +def make_ffi_from_sources(modulename, cdef, csrc): + """Create an :class:`FFI` from ``cdef`` text and a C source prelude.""" + ffibuilder = FFI() + ffibuilder.cdef(cdef) + ffibuilder.set_source(modulename, csrc) + return ffibuilder + + +def generate_c_source(ffi): + """Return the C source that :meth:`FFI.emit_c_code` would write.""" + output = io.StringIO() + ffi.emit_c_code(output) + return output.getvalue() + + +def write_c_source(output, generated): + if output == '-': + sys.stdout.write(generated) + return + with open(output, 'w', encoding='utf-8') as f: + f.write(generated) + + +def same_input_file(file1, file2): + if file1 is file2: + return True + try: + return os.path.samefile(file1.name, file2.name) + except OSError: + return False + + +def exec_python(*, output, pyfile, ffi_var): + with pyfile: + ffi = find_ffi_in_python_script(pyfile.read(), pyfile.name, ffi_var) + generated = generate_c_source(ffi) + write_c_source(output, generated) + + +def read_sources(*, output, module_name, cdef_input, csrc_input): + with csrc_input, cdef_input: + csrc = csrc_input.read() + cdef = cdef_input.read() + ffi = make_ffi_from_sources(module_name, cdef, csrc) + generated = generate_c_source(ffi) + write_c_source(output, generated) + + +def _prog(): + # The same parser serves both documented invocations; make --help + # and usage messages show the one that was actually used. + argv0 = os.path.basename(sys.argv[0]) if sys.argv else '' + if argv0.startswith('cffi-gen-src'): + return 'cffi-gen-src' + return 'python -m cffi.gen_src' + + +parser = argparse.ArgumentParser( + prog=_prog(), + description='Generate CFFI C source for extension modules.', +) +subparsers = parser.add_subparsers(dest='mode') + +exec_python_parser = subparsers.add_parser( + 'exec-python', + help='Execute a Python script that defines an FFI object', +) +exec_python_parser.add_argument( + '--ffi-var', + default='ffibuilder', + help="Name of the FFI object in the Python script; defaults to 'ffibuilder'.", +) +exec_python_parser.add_argument( + 'pyfile', + type=argparse.FileType('r', encoding='utf-8'), + help='Path to the Python script', +) +exec_python_parser.add_argument( + 'output', + help='Output path for the C source', +) + +read_sources_parser = subparsers.add_parser( + 'read-sources', + help='Read cdef and C source prelude files that define an FFI object', +) +read_sources_parser.add_argument( + 'module_name', + help='Full name of the generated module, including packages', +) +read_sources_parser.add_argument( + 'cdef', + type=argparse.FileType('r', encoding='utf-8'), + help='File containing C definitions', +) +read_sources_parser.add_argument( + 'csrc', + type=argparse.FileType('r', encoding='utf-8'), + help='File containing C source prelude', +) +read_sources_parser.add_argument( + 'output', + help='Output path for the C source', +) + + +def run(args=None): + args = parser.parse_args(args=args) + if args.mode == 'exec-python': + exec_python(output=args.output, pyfile=args.pyfile, ffi_var=args.ffi_var) + elif args.mode == 'read-sources': + if same_input_file(args.cdef, args.csrc): + parser.error('cdef and csrc are the same file and should not be') + read_sources( + output=args.output, + module_name=args.module_name, + cdef_input=args.cdef, + csrc_input=args.csrc, + ) + else: + parser.error('a subcommand is required: exec-python or read-sources') + parser.exit(0) diff --git a/source/target-vendors/cp312/cffi/_cffi_include.h b/source/target-vendors/cp312/cffi/_cffi_include.h new file mode 100644 index 0000000..c8c6d34 --- /dev/null +++ b/source/target-vendors/cp312/cffi/_cffi_include.h @@ -0,0 +1,397 @@ +#define _CFFI_ + +/* We try to define Py_LIMITED_API before including Python.h. + + Mess: we can only define it if Py_DEBUG, Py_TRACE_REFS and + Py_REF_DEBUG are not defined. This is a best-effort approximation: + we can learn about Py_DEBUG from pyconfig.h, but it is unclear if + the same works for the other two macros. Py_DEBUG implies them, + but not the other way around. + + The implementation is messy (issue #350): on Windows, with _MSC_VER, + we have to define Py_LIMITED_API even before including pyconfig.h. + In that case, we guess what pyconfig.h will do to the macros above, + and check our guess after the #include. + + Note that on Windows, with CPython 3.x, you need >= 3.5 and virtualenv + version >= 16.0.0. With older versions of either, you don't get a + copy of PYTHON3.DLL in the virtualenv. We can't check the version of + CPython *before* we even include pyconfig.h. ffi.set_source() puts + a ``#define _CFFI_NO_LIMITED_API'' at the start of this file if it is + running on Windows < 3.5, as an attempt at fixing it, but that's + arguably wrong because it may not be the target version of Python. + Still better than nothing I guess. As another workaround, you can + remove the definition of Py_LIMITED_API here. + + See also 'py_limited_api' in cffi/setuptools_ext.py. +*/ +#if !defined(_CFFI_USE_EMBEDDING) && !defined(Py_LIMITED_API) +# ifdef _MSC_VER +# if !defined(_DEBUG) && !defined(Py_DEBUG) && !defined(Py_TRACE_REFS) && !defined(Py_REF_DEBUG) && !defined(_CFFI_NO_LIMITED_API) +# if !defined(Py_GIL_DISABLED) +# define Py_LIMITED_API +# else +# define Py_LIMITED_API 0x030f0000 +# endif +# endif + +# include + /* sanity-check: Py_LIMITED_API will cause crashes if any of these + are also defined. Normally, the Python file PC/pyconfig.h does not + cause any of these to be defined, with the exception that _DEBUG + causes Py_DEBUG. Double-check that. */ +# ifdef Py_LIMITED_API +# if defined(Py_DEBUG) +# error "pyconfig.h unexpectedly defines Py_DEBUG, but Py_LIMITED_API is set" +# endif +# if defined(Py_TRACE_REFS) +# error "pyconfig.h unexpectedly defines Py_TRACE_REFS, but Py_LIMITED_API is set" +# endif +# if defined(Py_REF_DEBUG) +# error "pyconfig.h unexpectedly defines Py_REF_DEBUG, but Py_LIMITED_API is set" +# endif +# endif +# else +# include +# if !defined(Py_DEBUG) && !defined(Py_TRACE_REFS) && !defined(Py_REF_DEBUG) && !defined(_CFFI_NO_LIMITED_API) +# if !defined(Py_GIL_DISABLED) +# define Py_LIMITED_API +# else +# define Py_LIMITED_API 0x030f0000 +# endif +# endif +# endif +#endif + +#include +#ifdef __cplusplus +extern "C" { +#endif +#include +#include +#include + +#include "parse_c_type.h" + +/* this block of #ifs should be kept exactly identical between + c/_cffi_backend.c, cffi/vengine_cpy.py, cffi/vengine_gen.py + and cffi/_cffi_include.h */ +#if defined(_MSC_VER) +# include /* for alloca() */ +# if _MSC_VER < 1600 /* MSVC < 2010 */ + typedef __int8 int8_t; + typedef __int16 int16_t; + typedef __int32 int32_t; + typedef __int64 int64_t; + typedef unsigned __int8 uint8_t; + typedef unsigned __int16 uint16_t; + typedef unsigned __int32 uint32_t; + typedef unsigned __int64 uint64_t; + typedef __int8 int_least8_t; + typedef __int16 int_least16_t; + typedef __int32 int_least32_t; + typedef __int64 int_least64_t; + typedef unsigned __int8 uint_least8_t; + typedef unsigned __int16 uint_least16_t; + typedef unsigned __int32 uint_least32_t; + typedef unsigned __int64 uint_least64_t; + typedef __int8 int_fast8_t; + typedef __int16 int_fast16_t; + typedef __int32 int_fast32_t; + typedef __int64 int_fast64_t; + typedef unsigned __int8 uint_fast8_t; + typedef unsigned __int16 uint_fast16_t; + typedef unsigned __int32 uint_fast32_t; + typedef unsigned __int64 uint_fast64_t; + typedef __int64 intmax_t; + typedef unsigned __int64 uintmax_t; +# else +# include +# endif +# if _MSC_VER < 1800 /* MSVC < 2013 */ +# ifndef __cplusplus + typedef unsigned char _Bool; +# endif +# endif +# define _cffi_float_complex_t _Fcomplex /* include for it */ +# define _cffi_double_complex_t _Dcomplex /* include for it */ +#else +# include +# if (defined (__SVR4) && defined (__sun)) || defined(_AIX) || defined(__hpux) +# include +# endif +# define _cffi_float_complex_t float _Complex +# define _cffi_double_complex_t double _Complex +#endif + +#ifdef __GNUC__ +# define _CFFI_UNUSED_FN __attribute__((unused)) +#else +# define _CFFI_UNUSED_FN /* nothing */ +#endif + +#ifdef __cplusplus +# ifndef _Bool + typedef bool _Bool; /* semi-hackish: C++ has no _Bool; bool is builtin */ +# endif +#endif + +/********** CPython-specific section **********/ +#ifndef PYPY_VERSION + + +#define _cffi_from_c_double PyFloat_FromDouble +#define _cffi_from_c_float PyFloat_FromDouble +#define _cffi_from_c_long PyLong_FromLong +#define _cffi_from_c_ulong PyLong_FromUnsignedLong +#define _cffi_from_c_longlong PyLong_FromLongLong +#define _cffi_from_c_ulonglong PyLong_FromUnsignedLongLong +#define _cffi_from_c__Bool PyBool_FromLong + +#define _cffi_to_c_double PyFloat_AsDouble +#define _cffi_to_c_float PyFloat_AsDouble + +#define _cffi_from_c_int(x, type) \ + (((type)-1) > 0 ? /* unsigned */ \ + (sizeof(type) < sizeof(long) ? \ + PyLong_FromLong((long)x) : \ + sizeof(type) == sizeof(long) ? \ + PyLong_FromUnsignedLong((unsigned long)x) : \ + PyLong_FromUnsignedLongLong((unsigned long long)x)) : \ + (sizeof(type) <= sizeof(long) ? \ + PyLong_FromLong((long)x) : \ + PyLong_FromLongLong((long long)x))) + +#define _cffi_to_c_int(o, type) \ + ((type)( \ + sizeof(type) == 1 ? (((type)-1) > 0 ? (type)_cffi_to_c_u8(o) \ + : (type)_cffi_to_c_i8(o)) : \ + sizeof(type) == 2 ? (((type)-1) > 0 ? (type)_cffi_to_c_u16(o) \ + : (type)_cffi_to_c_i16(o)) : \ + sizeof(type) == 4 ? (((type)-1) > 0 ? (type)_cffi_to_c_u32(o) \ + : (type)_cffi_to_c_i32(o)) : \ + sizeof(type) == 8 ? (((type)-1) > 0 ? (type)_cffi_to_c_u64(o) \ + : (type)_cffi_to_c_i64(o)) : \ + (Py_FatalError("unsupported size for type " #type), (type)0))) + +#define _cffi_to_c_i8 \ + ((int(*)(PyObject *))_cffi_exports[1]) +#define _cffi_to_c_u8 \ + ((int(*)(PyObject *))_cffi_exports[2]) +#define _cffi_to_c_i16 \ + ((int(*)(PyObject *))_cffi_exports[3]) +#define _cffi_to_c_u16 \ + ((int(*)(PyObject *))_cffi_exports[4]) +#define _cffi_to_c_i32 \ + ((int(*)(PyObject *))_cffi_exports[5]) +#define _cffi_to_c_u32 \ + ((unsigned int(*)(PyObject *))_cffi_exports[6]) +#define _cffi_to_c_i64 \ + ((long long(*)(PyObject *))_cffi_exports[7]) +#define _cffi_to_c_u64 \ + ((unsigned long long(*)(PyObject *))_cffi_exports[8]) +#define _cffi_to_c_char \ + ((int(*)(PyObject *))_cffi_exports[9]) +#define _cffi_from_c_pointer \ + ((PyObject *(*)(char *, struct _cffi_ctypedescr *))_cffi_exports[10]) +#define _cffi_to_c_pointer \ + ((char *(*)(PyObject *, struct _cffi_ctypedescr *))_cffi_exports[11]) +#define _cffi_get_struct_layout \ + not used any more +#define _cffi_restore_errno \ + ((void(*)(void))_cffi_exports[13]) +#define _cffi_save_errno \ + ((void(*)(void))_cffi_exports[14]) +#define _cffi_from_c_char \ + ((PyObject *(*)(char))_cffi_exports[15]) +#define _cffi_from_c_deref \ + ((PyObject *(*)(char *, struct _cffi_ctypedescr *))_cffi_exports[16]) +#define _cffi_to_c \ + ((int(*)(char *, struct _cffi_ctypedescr *, PyObject *))_cffi_exports[17]) +#define _cffi_from_c_struct \ + ((PyObject *(*)(char *, struct _cffi_ctypedescr *))_cffi_exports[18]) +#define _cffi_to_c_wchar_t \ + ((_cffi_wchar_t(*)(PyObject *))_cffi_exports[19]) +#define _cffi_from_c_wchar_t \ + ((PyObject *(*)(_cffi_wchar_t))_cffi_exports[20]) +#define _cffi_to_c_long_double \ + ((long double(*)(PyObject *))_cffi_exports[21]) +#define _cffi_to_c__Bool \ + ((_Bool(*)(PyObject *))_cffi_exports[22]) +#define _cffi_prepare_pointer_call_argument \ + ((Py_ssize_t(*)(struct _cffi_ctypedescr *, \ + PyObject *, char **))_cffi_exports[23]) +#define _cffi_convert_array_from_object \ + ((int(*)(char *, struct _cffi_ctypedescr *, PyObject *))_cffi_exports[24]) +#define _CFFI_CPIDX 25 +#define _cffi_call_python \ + ((void(*)(struct _cffi_externpy_s *, char *))_cffi_exports[_CFFI_CPIDX]) +#define _cffi_to_c_wchar3216_t \ + ((int(*)(PyObject *))_cffi_exports[26]) +#define _cffi_from_c_wchar3216_t \ + ((PyObject *(*)(int))_cffi_exports[27]) +#define _CFFI_NUM_EXPORTS 28 + +struct _cffi_ctypedescr; + +static void *_cffi_exports[_CFFI_NUM_EXPORTS]; + +#define _cffi_type(index) ( \ + assert((((uintptr_t)_cffi_types[index]) & 1) == 0), \ + (struct _cffi_ctypedescr *)_cffi_types[index]) + +static PyObject *_cffi_init(const char *module_name, Py_ssize_t version, + const struct _cffi_type_context_s *ctx) +{ + PyObject *module, *o_arg, *new_module; + void *raw[] = { + (void *)module_name, + (void *)version, + (void *)_cffi_exports, + (void *)ctx, + }; + + module = PyImport_ImportModule("_cffi_backend"); + if (module == NULL) + goto failure; + + o_arg = PyLong_FromVoidPtr((void *)raw); + if (o_arg == NULL) + goto failure; + + new_module = PyObject_CallMethod( + module, (char *)"_init_cffi_1_0_external_module", (char *)"O", o_arg); + + Py_DECREF(o_arg); + Py_DECREF(module); + return new_module; + + failure: + Py_XDECREF(module); + return NULL; +} + + +#ifdef HAVE_WCHAR_H +typedef wchar_t _cffi_wchar_t; +#else +typedef uint16_t _cffi_wchar_t; /* same random pick as _cffi_backend.c */ +#endif + +_CFFI_UNUSED_FN static uint16_t _cffi_to_c_char16_t(PyObject *o) +{ + if (sizeof(_cffi_wchar_t) == 2) + return (uint16_t)_cffi_to_c_wchar_t(o); + else + return (uint16_t)_cffi_to_c_wchar3216_t(o); +} + +_CFFI_UNUSED_FN static PyObject *_cffi_from_c_char16_t(uint16_t x) +{ + if (sizeof(_cffi_wchar_t) == 2) + return _cffi_from_c_wchar_t((_cffi_wchar_t)x); + else + return _cffi_from_c_wchar3216_t((int)x); +} + +_CFFI_UNUSED_FN static int _cffi_to_c_char32_t(PyObject *o) +{ + if (sizeof(_cffi_wchar_t) == 4) + return (int)_cffi_to_c_wchar_t(o); + else + return (int)_cffi_to_c_wchar3216_t(o); +} + +_CFFI_UNUSED_FN static PyObject *_cffi_from_c_char32_t(unsigned int x) +{ + if (sizeof(_cffi_wchar_t) == 4) + return _cffi_from_c_wchar_t((_cffi_wchar_t)x); + else + return _cffi_from_c_wchar3216_t((int)x); +} + +union _cffi_union_alignment_u { + unsigned char m_char; + unsigned short m_short; + unsigned int m_int; + unsigned long m_long; + unsigned long long m_longlong; + float m_float; + double m_double; + long double m_longdouble; +}; + +struct _cffi_freeme_s { + struct _cffi_freeme_s *next; + union _cffi_union_alignment_u alignment; +}; + +_CFFI_UNUSED_FN static int +_cffi_convert_array_argument(struct _cffi_ctypedescr *ctptr, PyObject *arg, + char **output_data, Py_ssize_t datasize, + struct _cffi_freeme_s **freeme) +{ + char *p; + if (datasize < 0) + return -1; + + p = *output_data; + if (p == NULL) { + struct _cffi_freeme_s *fp = (struct _cffi_freeme_s *)PyObject_Malloc( + offsetof(struct _cffi_freeme_s, alignment) + (size_t)datasize); + if (fp == NULL) + return -1; + fp->next = *freeme; + *freeme = fp; + p = *output_data = (char *)&fp->alignment; + } + memset((void *)p, 0, (size_t)datasize); + return _cffi_convert_array_from_object(p, ctptr, arg); +} + +_CFFI_UNUSED_FN static void +_cffi_free_array_arguments(struct _cffi_freeme_s *freeme) +{ + do { + void *p = (void *)freeme; + freeme = freeme->next; + PyObject_Free(p); + } while (freeme != NULL); +} + +/********** end CPython-specific section **********/ +#else +_CFFI_UNUSED_FN +static void (*_cffi_call_python_org)(struct _cffi_externpy_s *, char *); +# define _cffi_call_python _cffi_call_python_org +#endif + + +#define _cffi_array_len(array) (sizeof(array) / sizeof((array)[0])) + +#define _cffi_prim_int(size, sign) \ + ((size) == 1 ? ((sign) ? _CFFI_PRIM_INT8 : _CFFI_PRIM_UINT8) : \ + (size) == 2 ? ((sign) ? _CFFI_PRIM_INT16 : _CFFI_PRIM_UINT16) : \ + (size) == 4 ? ((sign) ? _CFFI_PRIM_INT32 : _CFFI_PRIM_UINT32) : \ + (size) == 8 ? ((sign) ? _CFFI_PRIM_INT64 : _CFFI_PRIM_UINT64) : \ + _CFFI__UNKNOWN_PRIM) + +#define _cffi_prim_float(size) \ + ((size) == sizeof(float) ? _CFFI_PRIM_FLOAT : \ + (size) == sizeof(double) ? _CFFI_PRIM_DOUBLE : \ + (size) == sizeof(long double) ? _CFFI__UNKNOWN_LONG_DOUBLE : \ + _CFFI__UNKNOWN_FLOAT_PRIM) + +#define _cffi_check_int(got, got_nonpos, expected) \ + ((got_nonpos) == (expected <= 0) && \ + (got) == (unsigned long long)expected) + +#ifdef MS_WIN32 +# define _cffi_stdcall __stdcall +#else +# define _cffi_stdcall /* nothing */ +#endif + +#ifdef __cplusplus +} +#endif diff --git a/source/target-vendors/cp312/cffi/_embedding.h b/source/target-vendors/cp312/cffi/_embedding.h new file mode 100644 index 0000000..ba6e79b --- /dev/null +++ b/source/target-vendors/cp312/cffi/_embedding.h @@ -0,0 +1,512 @@ + +/***** Support code for embedding *****/ + +#ifdef __cplusplus +extern "C" { +#endif + + +#if defined(_WIN32) +# define CFFI_DLLEXPORT __declspec(dllexport) +#elif defined(__GNUC__) +# define CFFI_DLLEXPORT __attribute__((visibility("default"))) +#else +# define CFFI_DLLEXPORT /* nothing */ +#endif + + +/* There are two global variables of type _cffi_call_python_fnptr: + + * _cffi_call_python, which we declare just below, is the one called + by ``extern "Python"`` implementations. + + * _cffi_call_python_org, which on CPython is actually part of the + _cffi_exports[] array, is the function pointer copied from + _cffi_backend. If _cffi_start_python() fails, then this is set + to NULL; otherwise, it should never be NULL. + + After initialization is complete, both are equal. However, the + first one remains equal to &_cffi_start_and_call_python until the + very end of initialization, when we are (or should be) sure that + concurrent threads also see a completely initialized world, and + only then is it changed. +*/ +#undef _cffi_call_python +typedef void (*_cffi_call_python_fnptr)(struct _cffi_externpy_s *, char *); +static void _cffi_start_and_call_python(struct _cffi_externpy_s *, char *); +static _cffi_call_python_fnptr _cffi_call_python = &_cffi_start_and_call_python; + + +#ifndef _MSC_VER + /* --- Assuming a GCC not infinitely old --- */ +# define cffi_compare_and_swap(l,o,n) __sync_bool_compare_and_swap(l,o,n) +# define cffi_write_barrier() __sync_synchronize() +# if !defined(__amd64__) && !defined(__x86_64__) && \ + !defined(__i386__) && !defined(__i386) +# define cffi_read_barrier() __sync_synchronize() +# else +# define cffi_read_barrier() (void)0 +# endif +#else + /* --- Windows threads version --- */ +# include +# define cffi_compare_and_swap(l,o,n) \ + (InterlockedCompareExchangePointer(l,n,o) == (o)) +# define cffi_write_barrier() InterlockedCompareExchange(&_cffi_dummy,0,0) +# define cffi_read_barrier() (void)0 +static volatile LONG _cffi_dummy; +#endif + +#ifdef WITH_THREAD +# ifndef _MSC_VER +# include + static pthread_mutex_t _cffi_embed_startup_lock; +# else + static CRITICAL_SECTION _cffi_embed_startup_lock; +# endif + static char _cffi_embed_startup_lock_ready = 0; +#endif + +static void _cffi_acquire_reentrant_mutex(void) +{ + static void *volatile lock = NULL; + + while (!cffi_compare_and_swap(&lock, NULL, (void *)1)) { + /* should ideally do a spin loop instruction here, but + hard to do it portably and doesn't really matter I + think: pthread_mutex_init() should be very fast, and + this is only run at start-up anyway. */ + } + +#ifdef WITH_THREAD + if (!_cffi_embed_startup_lock_ready) { +# ifndef _MSC_VER + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(&_cffi_embed_startup_lock, &attr); +# else + InitializeCriticalSection(&_cffi_embed_startup_lock); +# endif + _cffi_embed_startup_lock_ready = 1; + } +#endif + + while (!cffi_compare_and_swap(&lock, (void *)1, NULL)) + ; + +#ifndef _MSC_VER + pthread_mutex_lock(&_cffi_embed_startup_lock); +#else + EnterCriticalSection(&_cffi_embed_startup_lock); +#endif +} + +static void _cffi_release_reentrant_mutex(void) +{ +#ifndef _MSC_VER + pthread_mutex_unlock(&_cffi_embed_startup_lock); +#else + LeaveCriticalSection(&_cffi_embed_startup_lock); +#endif +} + + +/********** CPython-specific section **********/ +#ifndef PYPY_VERSION + +#include "_cffi_errors.h" + + +#define _cffi_call_python_org _cffi_exports[_CFFI_CPIDX] + +PyMODINIT_FUNC _CFFI_PYTHON_STARTUP_FUNC(void); /* forward */ + +static void _cffi_py_initialize(void) +{ + /* XXX use initsigs=0, which "skips initialization registration of + signal handlers, which might be useful when Python is + embedded" according to the Python docs. But review and think + if it should be a user-controllable setting. + + XXX we should also give a way to write errors to a buffer + instead of to stderr. + + XXX if importing 'site' fails, CPython (any version) calls + exit(). Should we try to work around this behavior here? + */ + Py_InitializeEx(0); +} + +static int _cffi_initialize_python(void) +{ + /* This initializes Python, imports _cffi_backend, and then the + present .dll/.so is set up as a CPython C extension module. + */ + int result; + PyGILState_STATE state; + PyObject *pycode=NULL, *global_dict=NULL, *x; + PyObject *builtins; + + state = PyGILState_Ensure(); + + /* Call the initxxx() function from the present module. It will + create and initialize us as a CPython extension module, instead + of letting the startup Python code do it---it might reimport + the same .dll/.so and get maybe confused on some platforms. + It might also have troubles locating the .dll/.so again for all + I know. + */ + (void)_CFFI_PYTHON_STARTUP_FUNC(); + if (PyErr_Occurred()) + goto error; + + /* Now run the Python code provided to ffi.embedding_init_code(). + */ + pycode = Py_CompileString(_CFFI_PYTHON_STARTUP_CODE, + "", + Py_file_input); + if (pycode == NULL) + goto error; + global_dict = PyDict_New(); + if (global_dict == NULL) + goto error; + builtins = PyEval_GetBuiltins(); + if (builtins == NULL) + goto error; + if (PyDict_SetItemString(global_dict, "__builtins__", builtins) < 0) + goto error; + x = PyEval_EvalCode( + pycode, global_dict, global_dict); + if (x == NULL) + goto error; + Py_DECREF(x); + + /* Done! Now if we've been called from + _cffi_start_and_call_python() in an ``extern "Python"``, we can + only hope that the Python code did correctly set up the + corresponding @ffi.def_extern() function. Otherwise, the + general logic of ``extern "Python"`` functions (inside the + _cffi_backend module) will find that the reference is still + missing and print an error. + */ + result = 0; + done: + Py_XDECREF(pycode); + Py_XDECREF(global_dict); + PyGILState_Release(state); + return result; + + error:; + { + /* Print as much information as potentially useful. + Debugging load-time failures with embedding is not fun + */ + PyObject *ecap; + PyObject *exception, *v, *tb, *f, *modules, *mod; + PyErr_Fetch(&exception, &v, &tb); + ecap = _cffi_start_error_capture(); + f = PySys_GetObject((char *)"stderr"); + if (f != NULL && f != Py_None) { + PyFile_WriteString( + "Failed to initialize the Python-CFFI embedding logic:\n\n", f); + } + + if (exception != NULL) { + PyErr_NormalizeException(&exception, &v, &tb); + PyErr_Display(exception, v, tb); + } + Py_XDECREF(exception); + Py_XDECREF(v); + Py_XDECREF(tb); + + if (f != NULL && f != Py_None) { + PyFile_WriteString("\nFrom: " _CFFI_MODULE_NAME + "\ncompiled with cffi version: 2.1.1" + "\n_cffi_backend module: ", f); + modules = PyImport_GetModuleDict(); + mod = PyDict_GetItemString(modules, "_cffi_backend"); + if (mod == NULL) { + PyFile_WriteString("not loaded", f); + } + else { + v = PyObject_GetAttrString(mod, "__file__"); + PyFile_WriteObject(v, f, 0); + Py_XDECREF(v); + } + PyFile_WriteString("\nsys.path: ", f); + PyFile_WriteObject(PySys_GetObject((char *)"path"), f, 0); + PyFile_WriteString("\n\n", f); + } + _cffi_stop_error_capture(ecap); + } + result = -1; + goto done; +} + +static int _cffi_carefully_make_gil(void) +{ + /* This does the basic initialization of Python. It can be called + completely concurrently from unrelated threads. It assumes + that we don't hold the GIL before (if it exists), and we don't + hold it afterwards. + + (What it really does used to be completely different in Python 2 + and Python 3, with the Python 2 solution avoiding the spin-lock + around the Py_InitializeEx() call. However, after recent changes + to CPython 2.7 (issue #358) it no longer works. So we use the + Python 3 solution everywhere.) + + This initializes Python by calling Py_InitializeEx(). + Important: this must not be called concurrently at all. + So we use a global variable as a simple spin lock. This global + variable must be from 'libpythonX.Y.so', not from this + cffi-based extension module, because it must be shared from + different cffi-based extension modules. + + In Python < 3.8, we choose + _PyParser_TokenNames[0] as a completely arbitrary pointer value + that is never written to. The default is to point to the + string "ENDMARKER". We change it temporarily to point to the + next character in that string. (Yes, I know it's REALLY + obscure.) + + In Python >= 3.8, this string array is no longer writable, so + instead we pick PyCapsuleType.tp_version_tag. We can't change + Python < 3.8 because someone might use a mixture of cffi + embedded modules, some of which were compiled before this file + changed. + + In Python >= 3.12, this stopped working because that particular + tp_version_tag gets modified during interpreter startup. It's + arguably a bad idea before 3.12 too, but again we can't change + that because someone might use a mixture of cffi embedded + modules, and no-one reported a bug so far. In Python >= 3.12 + we go instead for PyCapsuleType.tp_as_buffer, which is supposed + to always be NULL. We write to it temporarily a pointer to + a struct full of NULLs, which is semantically the same. + */ + +#ifdef WITH_THREAD +# if PY_VERSION_HEX < 0x030C0000 + int volatile *lock = (int volatile *)&PyCapsule_Type.tp_version_tag; + int old_value, locked_value = -42; + assert(!(PyCapsule_Type.tp_flags & Py_TPFLAGS_HAVE_VERSION_TAG)); +# else + static struct ebp_s { PyBufferProcs buf; int mark; } empty_buffer_procs; + empty_buffer_procs.mark = -42; + PyBufferProcs *volatile *lock = (PyBufferProcs *volatile *) + &PyCapsule_Type.tp_as_buffer; + PyBufferProcs *old_value, *locked_value = &empty_buffer_procs.buf; +# endif + + while (1) { /* spin loop */ + old_value = *lock; + if (old_value == 0) { + if (cffi_compare_and_swap(lock, old_value, locked_value)) + break; + } + else { +# if PY_VERSION_HEX < 0x030C0000 + assert(old_value == locked_value); +# else + /* The pointer should point to a possibly different + empty_buffer_procs from another C extension module */ + assert(((struct ebp_s *)old_value)->mark == -42); +# endif + /* should ideally do a spin loop instruction here, but + hard to do it portably and doesn't really matter I + think: PyEval_InitThreads() should be very fast, and + this is only run at start-up anyway. */ + } + } +#endif + + /* call Py_InitializeEx() */ + if (!Py_IsInitialized()) { + _cffi_py_initialize(); + PyEval_SaveThread(); /* release the GIL */ + /* the returned tstate must be the one that has been stored into the + autoTLSkey by _PyGILState_Init() called from Py_Initialize(). */ + } + else { + } + +#ifdef WITH_THREAD + /* release the lock */ + while (!cffi_compare_and_swap(lock, locked_value, old_value)) + ; +#endif + + return 0; +} + +/********** end CPython-specific section **********/ + + +#else + + +/********** PyPy-specific section **********/ + +PyMODINIT_FUNC _CFFI_PYTHON_STARTUP_FUNC(const void *[]); /* forward */ + +static struct _cffi_pypy_init_s { + const char *name; + void *func; /* function pointer */ + const char *code; +} _cffi_pypy_init = { + _CFFI_MODULE_NAME, + _CFFI_PYTHON_STARTUP_FUNC, + _CFFI_PYTHON_STARTUP_CODE, +}; + +extern int pypy_carefully_make_gil(const char *); +extern int pypy_init_embedded_cffi_module(int, struct _cffi_pypy_init_s *); + +static int _cffi_carefully_make_gil(void) +{ + return pypy_carefully_make_gil(_CFFI_MODULE_NAME); +} + +static int _cffi_initialize_python(void) +{ + return pypy_init_embedded_cffi_module(0xB011, &_cffi_pypy_init); +} + +/********** end PyPy-specific section **********/ + + +#endif + + +#ifdef __GNUC__ +__attribute__((noinline)) +#endif +static _cffi_call_python_fnptr _cffi_start_python(void) +{ + /* Delicate logic to initialize Python. This function can be + called multiple times concurrently, e.g. when the process calls + its first ``extern "Python"`` functions in multiple threads at + once. It can also be called recursively, in which case we must + ignore it. We also have to consider what occurs if several + different cffi-based extensions reach this code in parallel + threads---it is a different copy of the code, then, and we + can't have any shared global variable unless it comes from + 'libpythonX.Y.so'. + + Idea: + + * _cffi_carefully_make_gil(): "carefully" call + PyEval_InitThreads() (possibly with Py_InitializeEx() first). + + * then we use a (local) custom lock to make sure that a call to this + cffi-based extension will wait if another call to the *same* + extension is running the initialization in another thread. + It is reentrant, so that a recursive call will not block, but + only one from a different thread. + + * then we grab the GIL and (Python 2) we call Py_InitializeEx(). + At this point, concurrent calls to Py_InitializeEx() are not + possible: we have the GIL. + + * do the rest of the specific initialization, which may + temporarily release the GIL but not the custom lock. + Only release the custom lock when we are done. + */ + static char called = 0; + + if (_cffi_carefully_make_gil() != 0) + return NULL; + + _cffi_acquire_reentrant_mutex(); + + /* Here the GIL exists, but we don't have it. We're only protected + from concurrency by the reentrant mutex. */ + + /* This file only initializes the embedded module once, the first + time this is called, even if there are subinterpreters. */ + if (!called) { + called = 1; /* invoke _cffi_initialize_python() only once, + but don't set '_cffi_call_python' right now, + otherwise concurrent threads won't call + this function at all (we need them to wait) */ + if (_cffi_initialize_python() == 0) { + /* now initialization is finished. Switch to the fast-path. */ + + /* We would like nobody to see the new value of + '_cffi_call_python' without also seeing the rest of the + data initialized. However, this is not possible. But + the new value of '_cffi_call_python' is the function + 'cffi_call_python()' from _cffi_backend. So: */ + cffi_write_barrier(); + /* ^^^ we put a write barrier here, and a corresponding + read barrier at the start of cffi_call_python(). This + ensures that after that read barrier, we see everything + done here before the write barrier. + */ + + assert(_cffi_call_python_org != NULL); + _cffi_call_python = (_cffi_call_python_fnptr)_cffi_call_python_org; + } + else { + /* initialization failed. Reset this to NULL, even if it was + already set to some other value. Future calls to + _cffi_start_python() are still forced to occur, and will + always return NULL from now on. */ + _cffi_call_python_org = NULL; + } + } + + _cffi_release_reentrant_mutex(); + + return (_cffi_call_python_fnptr)_cffi_call_python_org; +} + +static +void _cffi_start_and_call_python(struct _cffi_externpy_s *externpy, char *args) +{ + _cffi_call_python_fnptr fnptr; + int current_err = errno; +#ifdef _MSC_VER + int current_lasterr = GetLastError(); +#endif + fnptr = _cffi_start_python(); + if (fnptr == NULL) { + fprintf(stderr, "function %s() called, but initialization code " + "failed. Returning 0.\n", externpy->name); + memset(args, 0, externpy->size_of_result); + } +#ifdef _MSC_VER + SetLastError(current_lasterr); +#endif + errno = current_err; + + if (fnptr != NULL) + fnptr(externpy, args); +} + + +/* The cffi_start_python() function makes sure Python is initialized + and our cffi module is set up. It can be called manually from the + user C code. The same effect is obtained automatically from any + dll-exported ``extern "Python"`` function. This function returns + -1 if initialization failed, 0 if all is OK. */ +_CFFI_UNUSED_FN +static int cffi_start_python(void) +{ + if (_cffi_call_python == &_cffi_start_and_call_python) { + if (_cffi_start_python() == NULL) + return -1; + } + cffi_read_barrier(); + return 0; +} + +#undef cffi_compare_and_swap +#undef cffi_write_barrier +#undef cffi_read_barrier + +#ifdef __cplusplus +} +#endif diff --git a/source/target-vendors/cp312/cffi/_imp_emulation.py b/source/target-vendors/cp312/cffi/_imp_emulation.py new file mode 100644 index 0000000..136abdd --- /dev/null +++ b/source/target-vendors/cp312/cffi/_imp_emulation.py @@ -0,0 +1,83 @@ + +try: + # this works on Python < 3.12 + from imp import * + +except ImportError: + # this is a limited emulation for Python >= 3.12. + # Note that this is used only for tests or for the old ffi.verify(). + # This is copied from the source code of Python 3.11. + + from _imp import (acquire_lock, release_lock, + is_builtin, is_frozen) + + from importlib._bootstrap import _load + + from importlib import machinery + import os + import sys + import tokenize + + SEARCH_ERROR = 0 + PY_SOURCE = 1 + PY_COMPILED = 2 + C_EXTENSION = 3 + PY_RESOURCE = 4 + PKG_DIRECTORY = 5 + C_BUILTIN = 6 + PY_FROZEN = 7 + PY_CODERESOURCE = 8 + IMP_HOOK = 9 + + def get_suffixes(): + extensions = [(s, 'rb', C_EXTENSION) + for s in machinery.EXTENSION_SUFFIXES] + source = [(s, 'r', PY_SOURCE) for s in machinery.SOURCE_SUFFIXES] + bytecode = [(s, 'rb', PY_COMPILED) for s in machinery.BYTECODE_SUFFIXES] + return extensions + source + bytecode + + def find_module(name, path=None): + if not isinstance(name, str): + raise TypeError("'name' must be a str, not {}".format(type(name))) + elif not isinstance(path, (type(None), list)): + # Backwards-compatibility + raise RuntimeError("'path' must be None or a list, " + "not {}".format(type(path))) + + if path is None: + if is_builtin(name): + return None, None, ('', '', C_BUILTIN) + elif is_frozen(name): + return None, None, ('', '', PY_FROZEN) + else: + path = sys.path + + for entry in path: + package_directory = os.path.join(entry, name) + for suffix in ['.py', machinery.BYTECODE_SUFFIXES[0]]: + package_file_name = '__init__' + suffix + file_path = os.path.join(package_directory, package_file_name) + if os.path.isfile(file_path): + return None, package_directory, ('', '', PKG_DIRECTORY) + for suffix, mode, type_ in get_suffixes(): + file_name = name + suffix + file_path = os.path.join(entry, file_name) + if os.path.isfile(file_path): + break + else: + continue + break # Break out of outer loop when breaking out of inner loop. + else: + raise ImportError(name, name=name) + + encoding = None + if 'b' not in mode: + with open(file_path, 'rb') as file: + encoding = tokenize.detect_encoding(file.readline)[0] + file = open(file_path, mode, encoding=encoding) + return file, file_path, (suffix, mode, type_) + + def load_dynamic(name, path, file=None): + loader = machinery.ExtensionFileLoader(name, path) + spec = machinery.ModuleSpec(name=name, loader=loader, origin=path) + return _load(spec) diff --git a/source/target-vendors/cp312/cffi/_shimmed_dist_utils.py b/source/target-vendors/cp312/cffi/_shimmed_dist_utils.py new file mode 100644 index 0000000..c3d2312 --- /dev/null +++ b/source/target-vendors/cp312/cffi/_shimmed_dist_utils.py @@ -0,0 +1,45 @@ +""" +Temporary shim module to indirect the bits of distutils we need from setuptools/distutils while providing useful +error messages beyond `No module named 'distutils' on Python >= 3.12, or when setuptools' vendored distutils is broken. + +This is a compromise to avoid a hard-dep on setuptools for Python >= 3.12, since many users don't need runtime compilation support from CFFI. +""" +import sys + +try: + # import setuptools first; this is the most robust way to ensure its embedded distutils is available + # (the .pth shim should usually work, but this is even more robust) + import setuptools +except Exception as ex: + if sys.version_info >= (3, 12): + # Python 3.12 has no built-in distutils to fall back on, so any import problem is fatal + raise Exception("This CFFI feature requires setuptools on Python >= 3.12. The setuptools module is missing or non-functional.") from ex + + # silently ignore on older Pythons (support fallback to stdlib distutils where available) +else: + del setuptools + +try: + # bring in just the bits of distutils we need, whether they really came from setuptools or stdlib-embedded distutils + from distutils import log, sysconfig + from distutils.ccompiler import CCompiler + from distutils.command.build_ext import build_ext + from distutils.core import Distribution, Extension + from distutils.dir_util import mkpath + from distutils.errors import DistutilsSetupError, CompileError, LinkError + from distutils.log import set_threshold, set_verbosity + + if sys.platform == 'win32': + try: + # FUTURE: msvc9compiler module was removed in setuptools 74; consider removing, as it's only used by an ancient patch in `recompiler` + from distutils.msvc9compiler import MSVCCompiler + except ImportError: + MSVCCompiler = None +except Exception as ex: + if sys.version_info >= (3, 12): + raise Exception("This CFFI feature requires setuptools on Python >= 3.12. Please install the setuptools package.") from ex + + # anything older, just let the underlying distutils import error fly + raise Exception("This CFFI feature requires distutils. Please install the distutils or setuptools package.") from ex + +del sys diff --git a/source/target-vendors/cp312/cffi/api.py b/source/target-vendors/cp312/cffi/api.py new file mode 100644 index 0000000..ec8ee7d --- /dev/null +++ b/source/target-vendors/cp312/cffi/api.py @@ -0,0 +1,960 @@ +import sys, types +from .lock import allocate_lock +from .error import CDefError +from . import model + +try: + basestring +except NameError: + # Python 3.x + basestring = str + +_unspecified = object() + + + +class FFI: + r''' + The main top-level class that you instantiate once, or once per module. + + Example usage: + + ffi = FFI() + ffi.cdef(""" + int printf(const char *, ...); + """) + + C = ffi.dlopen(None) # standard library + -or- + C = ffi.verify() # use a C compiler: verify the decl above is right + + C.printf("hello, %s!\n", ffi.new("char[]", "world")) + ''' + + def __init__(self, backend=None): + """Create an FFI instance. The 'backend' argument is used to + select a non-default backend, mostly for tests. + """ + if backend is None: + # You need PyPy (>= 2.0 beta), or a CPython (>= 2.6) with + # _cffi_backend.so compiled. + import _cffi_backend as backend + from . import __version__ + if backend.__version__ != __version__: + # bad version! Try to be as explicit as possible. + if hasattr(backend, '__file__'): + # CPython + raise Exception("Version mismatch: this is the 'cffi' package version %s, located in %r. When we import the top-level '_cffi_backend' extension module, we get version %s, located in %r. The two versions should be equal; check your installation." % ( + __version__, __file__, + backend.__version__, backend.__file__)) + else: + # PyPy + raise Exception("Version mismatch: this is the 'cffi' package version %s, located in %r. This interpreter comes with a built-in '_cffi_backend' module, which is version %s. The two versions should be equal; check your installation." % ( + __version__, __file__, backend.__version__)) + # (If you insist you can also try to pass the option + # 'backend=backend_ctypes.CTypesBackend()', but don't + # rely on it! It's probably not going to work well.) + + from . import cparser + self._backend = backend + self._lock = allocate_lock() + self._parser = cparser.Parser() + self._cached_btypes = {} + self._parsed_types = types.ModuleType('parsed_types').__dict__ + self._new_types = types.ModuleType('new_types').__dict__ + self._function_caches = [] + self._libraries = [] + self._cdefsources = [] + self._included_ffis = [] + self._windows_unicode = None + self._init_once_cache = {} + self._cdef_version = None + self._embedding = None + self._typecache = model.get_typecache(backend) + if hasattr(backend, 'set_ffi'): + backend.set_ffi(self) + for name in list(backend.__dict__): + if name.startswith('RTLD_'): + setattr(self, name, getattr(backend, name)) + # + with self._lock: + self.BVoidP = self._get_cached_btype(model.voidp_type) + self.BCharA = self._get_cached_btype(model.char_array_type) + if isinstance(backend, types.ModuleType): + # _cffi_backend: attach these constants to the class + if not hasattr(FFI, 'NULL'): + FFI.NULL = self.cast(self.BVoidP, 0) + FFI.CData, FFI.CType = backend._get_types() + else: + # ctypes backend: attach these constants to the instance + self.NULL = self.cast(self.BVoidP, 0) + self.CData, self.CType = backend._get_types() + self.buffer = backend.buffer + + def cdef(self, csource, override=False, packed=False, pack=None): + """Parse the given C source. This registers all declared functions, + types, and global variables. The functions and global variables can + then be accessed via either 'ffi.dlopen()' or 'ffi.verify()'. + The types can be used in 'ffi.new()' and other functions. + If 'packed' is specified as True, all structs declared inside this + cdef are packed, i.e. laid out without any field alignment at all. + Alternatively, 'pack' can be a small integer, and requests for + alignment greater than that are ignored (pack=1 is equivalent to + packed=True). + """ + self._cdef(csource, override=override, packed=packed, pack=pack) + + def embedding_api(self, csource, packed=False, pack=None): + self._cdef(csource, packed=packed, pack=pack, dllexport=True) + if self._embedding is None: + self._embedding = '' + + def _cdef(self, csource, override=False, **options): + if not isinstance(csource, str): # unicode, on Python 2 + if not isinstance(csource, basestring): + raise TypeError("cdef() argument must be a string") + csource = csource.encode('ascii') + with self._lock: + self._cdef_version = object() + self._parser.parse(csource, override=override, **options) + self._cdefsources.append(csource) + if override: + for cache in self._function_caches: + cache.clear() + finishlist = self._parser._recomplete + if finishlist: + self._parser._recomplete = [] + for tp in finishlist: + tp.finish_backend_type(self, finishlist) + + def dlopen(self, name, flags=0): + """Load and return a dynamic library identified by 'name'. + The standard C library can be loaded by passing None. + Note that functions and types declared by 'ffi.cdef()' are not + linked to a particular library, just like C headers; in the + library we only look for the actual (untyped) symbols. + """ + if not (isinstance(name, basestring) or + name is None or + isinstance(name, self.CData)): + raise TypeError("dlopen(name): name must be a file name, None, " + "or an already-opened 'void *' handle") + with self._lock: + lib, function_cache = _make_ffi_library(self, name, flags) + self._function_caches.append(function_cache) + self._libraries.append(lib) + return lib + + def dlclose(self, lib): + """Close a library obtained with ffi.dlopen(). After this call, + access to functions or variables from the library will fail + (possibly with a segmentation fault). + """ + type(lib).__cffi_close__(lib) + + def _typeof_locked(self, cdecl): + # call me with the lock! + key = cdecl + if key in self._parsed_types: + return self._parsed_types[key] + # + if not isinstance(cdecl, str): # unicode, on Python 2 + cdecl = cdecl.encode('ascii') + # + type = self._parser.parse_type(cdecl) + really_a_function_type = type.is_raw_function + if really_a_function_type: + type = type.as_function_pointer() + btype = self._get_cached_btype(type) + result = btype, really_a_function_type + self._parsed_types[key] = result + return result + + def _typeof(self, cdecl, consider_function_as_funcptr=False): + # string -> ctype object + try: + result = self._parsed_types[cdecl] + except KeyError: + with self._lock: + result = self._typeof_locked(cdecl) + # + btype, really_a_function_type = result + if really_a_function_type and not consider_function_as_funcptr: + raise CDefError("the type %r is a function type, not a " + "pointer-to-function type" % (cdecl,)) + return btype + + def typeof(self, cdecl): + """Parse the C type given as a string and return the + corresponding object. + It can also be used on 'cdata' instance to get its C type. + """ + if isinstance(cdecl, basestring): + return self._typeof(cdecl) + if isinstance(cdecl, self.CData): + return self._backend.typeof(cdecl) + if isinstance(cdecl, types.BuiltinFunctionType): + res = _builtin_function_type(cdecl) + if res is not None: + return res + if (isinstance(cdecl, types.FunctionType) + and hasattr(cdecl, '_cffi_base_type')): + with self._lock: + return self._get_cached_btype(cdecl._cffi_base_type) + raise TypeError(type(cdecl)) + + def sizeof(self, cdecl): + """Return the size in bytes of the argument. It can be a + string naming a C type, or a 'cdata' instance. + """ + if isinstance(cdecl, basestring): + BType = self._typeof(cdecl) + return self._backend.sizeof(BType) + else: + return self._backend.sizeof(cdecl) + + def alignof(self, cdecl): + """Return the natural alignment size in bytes of the C type + given as a string. + """ + if isinstance(cdecl, basestring): + cdecl = self._typeof(cdecl) + return self._backend.alignof(cdecl) + + def offsetof(self, cdecl, *fields_or_indexes): + """Return the offset of the named field inside the given + structure or array, which must be given as a C type name. + You can give several field names in case of nested structures. + You can also give numeric values which correspond to array + items, in case of an array type. + """ + if isinstance(cdecl, basestring): + cdecl = self._typeof(cdecl) + return self._typeoffsetof(cdecl, *fields_or_indexes)[1] + + def new(self, cdecl, init=None): + """Allocate an instance according to the specified C type and + return a pointer to it. The specified C type must be either a + pointer or an array: ``new('X *')`` allocates an X and returns + a pointer to it, whereas ``new('X[n]')`` allocates an array of + n X'es and returns an array referencing it (which works + mostly like a pointer, like in C). You can also use + ``new('X[]', n)`` to allocate an array of a non-constant + length n. + + The memory is initialized following the rules of declaring a + global variable in C: by default it is zero-initialized, but + an explicit initializer can be given which can be used to + fill all or part of the memory. + + When the returned object goes out of scope, the memory + is freed. In other words the returned object has + ownership of the value of type 'cdecl' that it points to. This + means that the raw data can be used as long as this object is + kept alive, but must not be used for a longer time. Be careful + about that when copying the pointer to the memory somewhere + else, e.g. into another structure. + """ + if isinstance(cdecl, basestring): + cdecl = self._typeof(cdecl) + return self._backend.newp(cdecl, init) + + def new_allocator(self, alloc=None, free=None, + should_clear_after_alloc=True): + """Return a new allocator, i.e. a function that behaves like ffi.new() + but uses the provided low-level 'alloc' and 'free' functions. + + 'alloc' is called with the size as argument. If it returns NULL, a + MemoryError is raised. 'free' is called with the result of 'alloc' + as argument. Both can be either Python function or directly C + functions. If 'free' is None, then no free function is called. + If both 'alloc' and 'free' are None, the default is used. + + If 'should_clear_after_alloc' is set to False, then the memory + returned by 'alloc' is assumed to be already cleared (or you are + fine with garbage); otherwise CFFI will clear it. + """ + compiled_ffi = self._backend.FFI() + allocator = compiled_ffi.new_allocator(alloc, free, + should_clear_after_alloc) + def allocate(cdecl, init=None): + if isinstance(cdecl, basestring): + cdecl = self._typeof(cdecl) + return allocator(cdecl, init) + return allocate + + def cast(self, cdecl, source): + """Similar to a C cast: returns an instance of the named C + type initialized with the given 'source'. The source is + casted between integers or pointers of any type. + """ + if isinstance(cdecl, basestring): + cdecl = self._typeof(cdecl) + return self._backend.cast(cdecl, source) + + def string(self, cdata, maxlen=-1): + """Return a Python string (or unicode string) from the 'cdata'. + If 'cdata' is a pointer or array of characters or bytes, returns + the null-terminated string. The returned string extends until + the first null character, or at most 'maxlen' characters. If + 'cdata' is an array then 'maxlen' defaults to its length. + + If 'cdata' is a pointer or array of wchar_t, returns a unicode + string following the same rules. + + If 'cdata' is a single character or byte or a wchar_t, returns + it as a string or unicode string. + + If 'cdata' is an enum, returns the value of the enumerator as a + string, or 'NUMBER' if the value is out of range. + """ + return self._backend.string(cdata, maxlen) + + def unpack(self, cdata, length): + """Unpack an array of C data of the given length, + returning a Python string/unicode/list. + + If 'cdata' is a pointer to 'char', returns a byte string. + It does not stop at the first null. This is equivalent to: + ffi.buffer(cdata, length)[:] + + If 'cdata' is a pointer to 'wchar_t', returns a unicode string. + 'length' is measured in wchar_t's; it is not the size in bytes. + + If 'cdata' is a pointer to anything else, returns a list of + 'length' items. This is a faster equivalent to: + [cdata[i] for i in range(length)] + """ + return self._backend.unpack(cdata, length) + + #def buffer(self, cdata, size=-1): + # """Return a read-write buffer object that references the raw C data + # pointed to by the given 'cdata'. The 'cdata' must be a pointer or + # an array. Can be passed to functions expecting a buffer, or directly + # manipulated with: + # + # buf[:] get a copy of it in a regular string, or + # buf[idx] as a single character + # buf[:] = ... + # buf[idx] = ... change the content + # """ + # note that 'buffer' is a type, set on this instance by __init__ + + def from_buffer(self, cdecl, python_buffer=_unspecified, + require_writable=False): + """Return a cdata of the given type pointing to the data of the + given Python object, which must support the buffer interface. + Note that this is not meant to be used on the built-in types + str or unicode (you can build 'char[]' arrays explicitly) + but only on objects containing large quantities of raw data + in some other format, like 'array.array' or numpy arrays. + + The first argument is optional and default to 'char[]'. + """ + if python_buffer is _unspecified: + cdecl, python_buffer = self.BCharA, cdecl + elif isinstance(cdecl, basestring): + cdecl = self._typeof(cdecl) + return self._backend.from_buffer(cdecl, python_buffer, + require_writable) + + def memmove(self, dest, src, n): + """ffi.memmove(dest, src, n) copies n bytes of memory from src to dest. + + Like the C function memmove(), the memory areas may overlap; + apart from that it behaves like the C function memcpy(). + + 'src' can be any cdata ptr or array, or any Python buffer object. + 'dest' can be any cdata ptr or array, or a writable Python buffer + object. The size to copy, 'n', is always measured in bytes. + + Unlike other methods, this one supports all Python buffer including + byte strings and bytearrays---but it still does not support + non-contiguous buffers. + """ + return self._backend.memmove(dest, src, n) + + def callback(self, cdecl, python_callable=None, error=None, onerror=None): + """Return a callback object or a decorator making such a + callback object. 'cdecl' must name a C function pointer type. + The callback invokes the specified 'python_callable' (which may + be provided either directly or via a decorator). Important: the + callback object must be manually kept alive for as long as the + callback may be invoked from the C level. + """ + def callback_decorator_wrap(python_callable): + if not callable(python_callable): + raise TypeError("the 'python_callable' argument " + "is not callable") + return self._backend.callback(cdecl, python_callable, + error, onerror) + if isinstance(cdecl, basestring): + cdecl = self._typeof(cdecl, consider_function_as_funcptr=True) + if python_callable is None: + return callback_decorator_wrap # decorator mode + else: + return callback_decorator_wrap(python_callable) # direct mode + + def getctype(self, cdecl, replace_with=''): + """Return a string giving the C type 'cdecl', which may be itself + a string or a object. If 'replace_with' is given, it gives + extra text to append (or insert for more complicated C types), like + a variable name, or '*' to get actually the C type 'pointer-to-cdecl'. + """ + if isinstance(cdecl, basestring): + cdecl = self._typeof(cdecl) + replace_with = replace_with.strip() + if (replace_with.startswith('*') + and '&[' in self._backend.getcname(cdecl, '&')): + replace_with = '(%s)' % replace_with + elif replace_with and replace_with[0] not in '[(': + replace_with = ' ' + replace_with + return self._backend.getcname(cdecl, replace_with) + + def gc(self, cdata, destructor, size=0): + """Return a new cdata object that points to the same + data. Later, when this new cdata object is garbage-collected, + 'destructor(old_cdata_object)' will be called. + + The optional 'size' gives an estimate of the size, used to + trigger the garbage collection more eagerly. So far only used + on PyPy. It tells the GC that the returned object keeps alive + roughly 'size' bytes of external memory. + """ + return self._backend.gcp(cdata, destructor, size) + + def _get_cached_btype(self, type): + assert self._lock.acquire(False) is False + # call me with the lock! + try: + BType = self._cached_btypes[type] + except KeyError: + finishlist = [] + BType = type.get_cached_btype(self, finishlist) + for type in finishlist: + type.finish_backend_type(self, finishlist) + return BType + + def verify(self, source='', tmpdir=None, **kwargs): + """Verify that the current ffi signatures compile on this + machine, and return a dynamic library object. The dynamic + library can be used to call functions and access global + variables declared in this 'ffi'. The library is compiled + by the C compiler: it gives you C-level API compatibility + (including calling macros). This is unlike 'ffi.dlopen()', + which requires binary compatibility in the signatures. + """ + from .verifier import Verifier, _caller_dir_pycache + # + # If set_unicode(True) was called, insert the UNICODE and + # _UNICODE macro declarations + if self._windows_unicode: + self._apply_windows_unicode(kwargs) + # + # Set the tmpdir here, and not in Verifier.__init__: it picks + # up the caller's directory, which we want to be the caller of + # ffi.verify(), as opposed to the caller of Veritier(). + tmpdir = tmpdir or _caller_dir_pycache() + # + # Make a Verifier() and use it to load the library. + self.verifier = Verifier(self, source, tmpdir, **kwargs) + lib = self.verifier.load_library() + # + # Save the loaded library for keep-alive purposes, even + # if the caller doesn't keep it alive itself (it should). + self._libraries.append(lib) + return lib + + def _get_errno(self): + return self._backend.get_errno() + def _set_errno(self, errno): + self._backend.set_errno(errno) + errno = property(_get_errno, _set_errno, None, + "the value of 'errno' from/to the C calls") + + def getwinerror(self, code=-1): + return self._backend.getwinerror(code) + + def _pointer_to(self, ctype): + with self._lock: + return model.pointer_cache(self, ctype) + + def addressof(self, cdata, *fields_or_indexes): + """Return the address of a . + If 'fields_or_indexes' are given, returns the address of that + field or array item in the structure or array, recursively in + case of nested structures. + """ + try: + ctype = self._backend.typeof(cdata) + except TypeError: + if '__addressof__' in type(cdata).__dict__: + return type(cdata).__addressof__(cdata, *fields_or_indexes) + raise + if fields_or_indexes: + ctype, offset = self._typeoffsetof(ctype, *fields_or_indexes) + else: + if ctype.kind == "pointer": + raise TypeError("addressof(pointer)") + offset = 0 + ctypeptr = self._pointer_to(ctype) + return self._backend.rawaddressof(ctypeptr, cdata, offset) + + def _typeoffsetof(self, ctype, field_or_index, *fields_or_indexes): + ctype, offset = self._backend.typeoffsetof(ctype, field_or_index) + for field1 in fields_or_indexes: + ctype, offset1 = self._backend.typeoffsetof(ctype, field1, 1) + offset += offset1 + return ctype, offset + + def include(self, ffi_to_include): + """Includes the typedefs, structs, unions and enums defined + in another FFI instance. Usage is similar to a #include in C, + where a part of the program might include types defined in + another part for its own usage. Note that the include() + method has no effect on functions, constants and global + variables, which must anyway be accessed directly from the + lib object returned by the original FFI instance. + """ + if not isinstance(ffi_to_include, FFI): + raise TypeError("ffi.include() expects an argument that is also of" + " type cffi.FFI, not %r" % ( + type(ffi_to_include).__name__,)) + if ffi_to_include is self: + raise ValueError("self.include(self)") + with ffi_to_include._lock: + with self._lock: + self._parser.include(ffi_to_include._parser) + self._cdefsources.append('[') + self._cdefsources.extend(ffi_to_include._cdefsources) + self._cdefsources.append(']') + self._included_ffis.append(ffi_to_include) + + def new_handle(self, x): + return self._backend.newp_handle(self.BVoidP, x) + + def from_handle(self, x): + return self._backend.from_handle(x) + + def release(self, x): + self._backend.release(x) + + def set_unicode(self, enabled_flag): + """Windows: if 'enabled_flag' is True, enable the UNICODE and + _UNICODE defines in C, and declare the types like TCHAR and LPTCSTR + to be (pointers to) wchar_t. If 'enabled_flag' is False, + declare these types to be (pointers to) plain 8-bit characters. + This is mostly for backward compatibility; you usually want True. + """ + if self._windows_unicode is not None: + raise ValueError("set_unicode() can only be called once") + enabled_flag = bool(enabled_flag) + if enabled_flag: + self.cdef("typedef wchar_t TBYTE;" + "typedef wchar_t TCHAR;" + "typedef const wchar_t *LPCTSTR;" + "typedef const wchar_t *PCTSTR;" + "typedef wchar_t *LPTSTR;" + "typedef wchar_t *PTSTR;" + "typedef TBYTE *PTBYTE;" + "typedef TCHAR *PTCHAR;") + else: + self.cdef("typedef char TBYTE;" + "typedef char TCHAR;" + "typedef const char *LPCTSTR;" + "typedef const char *PCTSTR;" + "typedef char *LPTSTR;" + "typedef char *PTSTR;" + "typedef TBYTE *PTBYTE;" + "typedef TCHAR *PTCHAR;") + self._windows_unicode = enabled_flag + + def _apply_windows_unicode(self, kwds): + defmacros = kwds.get('define_macros', ()) + if not isinstance(defmacros, (list, tuple)): + raise TypeError("'define_macros' must be a list or tuple") + defmacros = list(defmacros) + [('UNICODE', '1'), + ('_UNICODE', '1')] + kwds['define_macros'] = defmacros + + def _apply_embedding_fix(self, kwds): + # must include an argument like "-lpython2.7" for the compiler + def ensure(key, value): + lst = kwds.setdefault(key, []) + if value not in lst: + lst.append(value) + # + if '__pypy__' in sys.builtin_module_names: + import os + if sys.platform == "win32": + # we need 'libpypy-c.lib'. Current distributions of + # pypy (>= 4.1) contain it as 'libs/python27.lib'. + pythonlib = "python{0[0]}{0[1]}".format(sys.version_info) + if hasattr(sys, 'prefix'): + ensure('library_dirs', os.path.join(sys.prefix, 'libs')) + else: + # we need 'libpypy-c.{so,dylib}', which should be by + # default located in 'sys.prefix/bin' for installed + # systems. + if sys.version_info < (3,): + pythonlib = "pypy-c" + else: + pythonlib = "pypy3-c" + if hasattr(sys, 'prefix'): + ensure('library_dirs', os.path.join(sys.prefix, 'bin')) + # On uninstalled pypy's, the libpypy-c is typically found in + # .../pypy/goal/. + if hasattr(sys, 'prefix'): + ensure('library_dirs', os.path.join(sys.prefix, 'pypy', 'goal')) + else: + if sys.platform == "win32": + template = "python%d%d" + if hasattr(sys, 'gettotalrefcount'): + template += '_d' + else: + try: + import sysconfig + except ImportError: # 2.6 + from cffi._shimmed_dist_utils import sysconfig + template = "python%d.%d" + if sysconfig.get_config_var('DEBUG_EXT'): + template += sysconfig.get_config_var('DEBUG_EXT') + pythonlib = (template % + (sys.hexversion >> 24, (sys.hexversion >> 16) & 0xff)) + if hasattr(sys, 'abiflags'): + pythonlib += sys.abiflags + ensure('libraries', pythonlib) + if sys.platform == "win32": + ensure('extra_link_args', '/MANIFEST') + + def set_source(self, module_name, source, source_extension='.c', **kwds): + import os + if hasattr(self, '_assigned_source'): + raise ValueError("set_source() cannot be called several times " + "per ffi object") + if not isinstance(module_name, basestring): + raise TypeError("'module_name' must be a string") + if os.sep in module_name or (os.altsep and os.altsep in module_name): + raise ValueError("'module_name' must not contain '/': use a dotted " + "name to make a 'package.module' location") + self._assigned_source = (str(module_name), source, + source_extension, kwds) + + def set_source_pkgconfig(self, module_name, pkgconfig_libs, source, + source_extension='.c', **kwds): + from . import pkgconfig + if not isinstance(pkgconfig_libs, list): + raise TypeError("the pkgconfig_libs argument must be a list " + "of package names") + kwds2 = pkgconfig.flags_from_pkgconfig(pkgconfig_libs) + pkgconfig.merge_flags(kwds, kwds2) + self.set_source(module_name, source, source_extension, **kwds) + + def distutils_extension(self, tmpdir='build', verbose=True): + from cffi._shimmed_dist_utils import mkpath + from .recompiler import recompile + # + if not hasattr(self, '_assigned_source'): + if hasattr(self, 'verifier'): # fallback, 'tmpdir' ignored + return self.verifier.get_extension() + raise ValueError("set_source() must be called before" + " distutils_extension()") + module_name, source, source_extension, kwds = self._assigned_source + if source is None: + raise TypeError("distutils_extension() is only for C extension " + "modules, not for dlopen()-style pure Python " + "modules") + mkpath(tmpdir) + ext, updated = recompile(self, module_name, + source, tmpdir=tmpdir, extradir=tmpdir, + source_extension=source_extension, + call_c_compiler=False, **kwds) + if verbose: + if updated: + sys.stderr.write("regenerated: %r\n" % (ext.sources[0],)) + else: + sys.stderr.write("not modified: %r\n" % (ext.sources[0],)) + return ext + + def emit_c_code(self, filename): + from .recompiler import recompile + # + if not hasattr(self, '_assigned_source'): + raise ValueError("set_source() must be called before emit_c_code()") + module_name, source, source_extension, kwds = self._assigned_source + if source is None: + raise TypeError("emit_c_code() is only for C extension modules, " + "not for dlopen()-style pure Python modules") + recompile(self, module_name, source, + c_file=filename, call_c_compiler=False, + uses_ffiplatform=False, **kwds) + + def emit_python_code(self, filename): + from .recompiler import recompile + # + if not hasattr(self, '_assigned_source'): + raise ValueError("set_source() must be called before emit_c_code()") + module_name, source, source_extension, kwds = self._assigned_source + if source is not None: + raise TypeError("emit_python_code() is only for dlopen()-style " + "pure Python modules, not for C extension modules") + recompile(self, module_name, source, + c_file=filename, call_c_compiler=False, + uses_ffiplatform=False, **kwds) + + def compile(self, tmpdir='.', verbose=0, target=None, debug=None): + """The 'target' argument gives the final file name of the + compiled DLL. Use '*' to force distutils' choice, suitable for + regular CPython C API modules. Use a file name ending in '.*' + to ask for the system's default extension for dynamic libraries + (.so/.dll/.dylib). + + The default is '*' when building a non-embedded C API extension, + and (module_name + '.*') when building an embedded library. + """ + from .recompiler import recompile + # + if not hasattr(self, '_assigned_source'): + raise ValueError("set_source() must be called before compile()") + module_name, source, source_extension, kwds = self._assigned_source + return recompile(self, module_name, source, tmpdir=tmpdir, + target=target, source_extension=source_extension, + compiler_verbose=verbose, debug=debug, **kwds) + + def init_once(self, func, tag): + # Read _init_once_cache[tag], which is either (False, lock) if + # we're calling the function now in some thread, or (True, result). + # Don't call setdefault() in most cases, to avoid allocating and + # immediately freeing a lock; but still use setdefaut() to avoid + # races. + try: + x = self._init_once_cache[tag] + except KeyError: + x = self._init_once_cache.setdefault(tag, (False, allocate_lock())) + # Common case: we got (True, result), so we return the result. + if x[0]: + return x[1] + # Else, it's a lock. Acquire it to serialize the following tests. + with x[1]: + # Read again from _init_once_cache the current status. + x = self._init_once_cache[tag] + if x[0]: + return x[1] + # Call the function and store the result back. + result = func() + self._init_once_cache[tag] = (True, result) + return result + + def embedding_init_code(self, pysource): + if self._embedding: + raise ValueError("embedding_init_code() can only be called once") + # fix 'pysource' before it gets dumped into the C file: + # - remove empty lines at the beginning, so it starts at "line 1" + # - dedent, if all non-empty lines are indented + # - check for SyntaxErrors + import re + match = re.match(r'\s*\n', pysource) + if match: + pysource = pysource[match.end():] + lines = pysource.splitlines() or [''] + prefix = re.match(r'\s*', lines[0]).group() + for i in range(1, len(lines)): + line = lines[i] + if line.rstrip(): + while not line.startswith(prefix): + prefix = prefix[:-1] + i = len(prefix) + lines = [line[i:]+'\n' for line in lines] + pysource = ''.join(lines) + # + compile(pysource, "cffi_init", "exec") + # + self._embedding = pysource + + def def_extern(self, *args, **kwds): + raise ValueError("ffi.def_extern() is only available on API-mode FFI " + "objects") + + def list_types(self): + """Returns the user type names known to this FFI instance. + This returns a tuple containing three lists of names: + (typedef_names, names_of_structs, names_of_unions) + """ + typedefs = [] + structs = [] + unions = [] + for key in self._parser._declarations: + if key.startswith('typedef '): + typedefs.append(key[8:]) + elif key.startswith('struct '): + structs.append(key[7:]) + elif key.startswith('union '): + unions.append(key[6:]) + typedefs.sort() + structs.sort() + unions.sort() + return (typedefs, structs, unions) + + +def _load_backend_lib(backend, name, flags): + import os + if not isinstance(name, basestring): + if sys.platform != "win32" or name is not None: + return backend.load_library(name, flags) + name = "c" # Windows: load_library(None) fails, but this works + # on Python 2 (backward compatibility hack only) + first_error = None + if '.' in name or '/' in name or os.sep in name: + try: + return backend.load_library(name, flags) + except OSError as e: + first_error = e + import ctypes.util + path = ctypes.util.find_library(name) + if path is None: + if name == "c" and sys.platform == "win32" and sys.version_info >= (3,): + raise OSError("dlopen(None) cannot work on Windows for Python 3 " + "(see http://bugs.python.org/issue23606)") + msg = ("ctypes.util.find_library() did not manage " + "to locate a library called %r" % (name,)) + if first_error is not None: + msg = "%s. Additionally, %s" % (first_error, msg) + raise OSError(msg) + return backend.load_library(path, flags) + +def _make_ffi_library(ffi, libname, flags): + backend = ffi._backend + backendlib = _load_backend_lib(backend, libname, flags) + # + def accessor_function(name): + key = 'function ' + name + tp, _ = ffi._parser._declarations[key] + BType = ffi._get_cached_btype(tp) + value = backendlib.load_function(BType, name) + library.__dict__[name] = value + # + def accessor_variable(name): + key = 'variable ' + name + tp, _ = ffi._parser._declarations[key] + BType = ffi._get_cached_btype(tp) + read_variable = backendlib.read_variable + write_variable = backendlib.write_variable + setattr(FFILibrary, name, property( + lambda self: read_variable(BType, name), + lambda self, value: write_variable(BType, name, value))) + # + def addressof_var(name): + try: + return addr_variables[name] + except KeyError: + with ffi._lock: + if name not in addr_variables: + key = 'variable ' + name + tp, _ = ffi._parser._declarations[key] + BType = ffi._get_cached_btype(tp) + if BType.kind != 'array': + BType = model.pointer_cache(ffi, BType) + p = backendlib.load_function(BType, name) + addr_variables[name] = p + return addr_variables[name] + # + def accessor_constant(name): + raise NotImplementedError("non-integer constant '%s' cannot be " + "accessed from a dlopen() library" % (name,)) + # + def accessor_int_constant(name): + library.__dict__[name] = ffi._parser._int_constants[name] + # + accessors = {} + accessors_version = [False] + addr_variables = {} + # + def update_accessors(): + if accessors_version[0] is ffi._cdef_version: + return + # + for key, (tp, _) in ffi._parser._declarations.items(): + if not isinstance(tp, model.EnumType): + tag, name = key.split(' ', 1) + if tag == 'function': + accessors[name] = accessor_function + elif tag == 'variable': + accessors[name] = accessor_variable + elif tag == 'constant': + accessors[name] = accessor_constant + else: + for i, enumname in enumerate(tp.enumerators): + def accessor_enum(name, tp=tp, i=i): + tp.check_not_partial() + library.__dict__[name] = tp.enumvalues[i] + accessors[enumname] = accessor_enum + for name in ffi._parser._int_constants: + accessors.setdefault(name, accessor_int_constant) + accessors_version[0] = ffi._cdef_version + # + def make_accessor(name): + with ffi._lock: + if name in library.__dict__ or name in FFILibrary.__dict__: + return # added by another thread while waiting for the lock + if name not in accessors: + update_accessors() + if name not in accessors: + raise AttributeError(name) + accessors[name](name) + # + class FFILibrary: + def __getattr__(self, name): + make_accessor(name) + return getattr(self, name) + def __setattr__(self, name, value): + try: + property = getattr(self.__class__, name) + except AttributeError: + make_accessor(name) + setattr(self, name, value) + else: + property.__set__(self, value) + def __dir__(self): + with ffi._lock: + update_accessors() + return accessors.keys() + def __addressof__(self, name): + if name in library.__dict__: + return library.__dict__[name] + if name in FFILibrary.__dict__: + return addressof_var(name) + make_accessor(name) + if name in library.__dict__: + return library.__dict__[name] + if name in FFILibrary.__dict__: + return addressof_var(name) + raise AttributeError("cffi library has no function or " + "global variable named '%s'" % (name,)) + def __cffi_close__(self): + backendlib.close_lib() + self.__dict__.clear() + # + if isinstance(libname, basestring): + try: + if not isinstance(libname, str): # unicode, on Python 2 + libname = libname.encode('utf-8') + FFILibrary.__name__ = 'FFILibrary_%s' % libname + except UnicodeError: + pass + library = FFILibrary() + return library, library.__dict__ + +def _builtin_function_type(func): + # a hack to make at least ffi.typeof(builtin_function) work, + # if the builtin function was obtained by 'vengine_cpy'. + import sys + try: + module = sys.modules[func.__module__] + ffi = module._cffi_original_ffi + types_of_builtin_funcs = module._cffi_types_of_builtin_funcs + tp = types_of_builtin_funcs[func] + except (KeyError, AttributeError, TypeError): + return None + else: + with ffi._lock: + return ffi._get_cached_btype(tp) diff --git a/source/target-vendors/cp312/cffi/backend_ctypes.py b/source/target-vendors/cp312/cffi/backend_ctypes.py new file mode 100644 index 0000000..12cd162 --- /dev/null +++ b/source/target-vendors/cp312/cffi/backend_ctypes.py @@ -0,0 +1,1120 @@ +import ctypes, ctypes.util, operator, sys +from . import model + +if sys.version_info < (3,): + bytechr = chr +else: + unicode = str + long = int + xrange = range + bytechr = lambda num: bytes([num]) + +class CTypesType(type): + pass + +class CTypesData: + __metaclass__ = CTypesType + __slots__ = ['__weakref__'] + __name__ = '' + + def __init__(self, *args): + raise TypeError("cannot instantiate %r" % (self.__class__,)) + + @classmethod + def _newp(cls, init): + raise TypeError("expected a pointer or array ctype, got '%s'" + % (cls._get_c_name(),)) + + @staticmethod + def _to_ctypes(value): + raise TypeError + + @classmethod + def _arg_to_ctypes(cls, *value): + try: + ctype = cls._ctype + except AttributeError: + raise TypeError("cannot create an instance of %r" % (cls,)) + if value: + res = cls._to_ctypes(*value) + if not isinstance(res, ctype): + res = cls._ctype(res) + else: + res = cls._ctype() + return res + + @classmethod + def _create_ctype_obj(cls, init): + if init is None: + return cls._arg_to_ctypes() + else: + return cls._arg_to_ctypes(init) + + @staticmethod + def _from_ctypes(ctypes_value): + raise TypeError + + @classmethod + def _get_c_name(cls, replace_with=''): + return cls._reftypename.replace(' &', replace_with) + + @classmethod + def _fix_class(cls): + cls.__name__ = 'CData<%s>' % (cls._get_c_name(),) + cls.__qualname__ = 'CData<%s>' % (cls._get_c_name(),) + cls.__module__ = 'ffi' + + def _get_own_repr(self): + raise NotImplementedError + + def _addr_repr(self, address): + if address == 0: + return 'NULL' + else: + if address < 0: + address += 1 << (8*ctypes.sizeof(ctypes.c_void_p)) + return '0x%x' % address + + def __repr__(self, c_name=None): + own = self._get_own_repr() + return '' % (c_name or self._get_c_name(), own) + + def _convert_to_address(self, BClass): + if BClass is None: + raise TypeError("cannot convert %r to an address" % ( + self._get_c_name(),)) + else: + raise TypeError("cannot convert %r to %r" % ( + self._get_c_name(), BClass._get_c_name())) + + @classmethod + def _get_size(cls): + return ctypes.sizeof(cls._ctype) + + def _get_size_of_instance(self): + return ctypes.sizeof(self._ctype) + + @classmethod + def _cast_from(cls, source): + raise TypeError("cannot cast to %r" % (cls._get_c_name(),)) + + def _cast_to_integer(self): + return self._convert_to_address(None) + + @classmethod + def _alignment(cls): + return ctypes.alignment(cls._ctype) + + def __iter__(self): + raise TypeError("cdata %r does not support iteration" % ( + self._get_c_name()),) + + def _make_cmp(name): + cmpfunc = getattr(operator, name) + def cmp(self, other): + v_is_ptr = not isinstance(self, CTypesGenericPrimitive) + w_is_ptr = (isinstance(other, CTypesData) and + not isinstance(other, CTypesGenericPrimitive)) + if v_is_ptr and w_is_ptr: + return cmpfunc(self._convert_to_address(None), + other._convert_to_address(None)) + elif v_is_ptr or w_is_ptr: + return NotImplemented + else: + if isinstance(self, CTypesGenericPrimitive): + self = self._value + if isinstance(other, CTypesGenericPrimitive): + other = other._value + return cmpfunc(self, other) + cmp.func_name = name + return cmp + + __eq__ = _make_cmp('__eq__') + __ne__ = _make_cmp('__ne__') + __lt__ = _make_cmp('__lt__') + __le__ = _make_cmp('__le__') + __gt__ = _make_cmp('__gt__') + __ge__ = _make_cmp('__ge__') + + def __hash__(self): + return hash(self._convert_to_address(None)) + + def _to_string(self, maxlen): + raise TypeError("string(): %r" % (self,)) + + +class CTypesGenericPrimitive(CTypesData): + __slots__ = [] + + def __hash__(self): + return hash(self._value) + + def _get_own_repr(self): + return repr(self._from_ctypes(self._value)) + + +class CTypesGenericArray(CTypesData): + __slots__ = [] + + @classmethod + def _newp(cls, init): + return cls(init) + + def __iter__(self): + for i in xrange(len(self)): + yield self[i] + + def _get_own_repr(self): + return self._addr_repr(ctypes.addressof(self._blob)) + + +class CTypesGenericPtr(CTypesData): + __slots__ = ['_address', '_as_ctype_ptr'] + _automatic_casts = False + kind = "pointer" + + @classmethod + def _newp(cls, init): + return cls(init) + + @classmethod + def _cast_from(cls, source): + if source is None: + address = 0 + elif isinstance(source, CTypesData): + address = source._cast_to_integer() + elif isinstance(source, (int, long)): + address = source + else: + raise TypeError("bad type for cast to %r: %r" % + (cls, type(source).__name__)) + return cls._new_pointer_at(address) + + @classmethod + def _new_pointer_at(cls, address): + self = cls.__new__(cls) + self._address = address + self._as_ctype_ptr = ctypes.cast(address, cls._ctype) + return self + + def _get_own_repr(self): + try: + return self._addr_repr(self._address) + except AttributeError: + return '???' + + def _cast_to_integer(self): + return self._address + + def __nonzero__(self): + return bool(self._address) + __bool__ = __nonzero__ + + @classmethod + def _to_ctypes(cls, value): + if not isinstance(value, CTypesData): + raise TypeError("unexpected %s object" % type(value).__name__) + address = value._convert_to_address(cls) + return ctypes.cast(address, cls._ctype) + + @classmethod + def _from_ctypes(cls, ctypes_ptr): + address = ctypes.cast(ctypes_ptr, ctypes.c_void_p).value or 0 + return cls._new_pointer_at(address) + + @classmethod + def _initialize(cls, ctypes_ptr, value): + if value: + ctypes_ptr.contents = cls._to_ctypes(value).contents + + def _convert_to_address(self, BClass): + if (BClass in (self.__class__, None) or BClass._automatic_casts + or self._automatic_casts): + return self._address + else: + return CTypesData._convert_to_address(self, BClass) + + +class CTypesBaseStructOrUnion(CTypesData): + __slots__ = ['_blob'] + + @classmethod + def _create_ctype_obj(cls, init): + # may be overridden + raise TypeError("cannot instantiate opaque type %s" % (cls,)) + + def _get_own_repr(self): + return self._addr_repr(ctypes.addressof(self._blob)) + + @classmethod + def _offsetof(cls, fieldname): + return getattr(cls._ctype, fieldname).offset + + def _convert_to_address(self, BClass): + if getattr(BClass, '_BItem', None) is self.__class__: + return ctypes.addressof(self._blob) + else: + return CTypesData._convert_to_address(self, BClass) + + @classmethod + def _from_ctypes(cls, ctypes_struct_or_union): + self = cls.__new__(cls) + self._blob = ctypes_struct_or_union + return self + + @classmethod + def _to_ctypes(cls, value): + return value._blob + + def __repr__(self, c_name=None): + return CTypesData.__repr__(self, c_name or self._get_c_name(' &')) + + +class CTypesBackend: + + PRIMITIVE_TYPES = { + 'char': ctypes.c_char, + 'short': ctypes.c_short, + 'int': ctypes.c_int, + 'long': ctypes.c_long, + 'long long': ctypes.c_longlong, + 'signed char': ctypes.c_byte, + 'unsigned char': ctypes.c_ubyte, + 'unsigned short': ctypes.c_ushort, + 'unsigned int': ctypes.c_uint, + 'unsigned long': ctypes.c_ulong, + 'unsigned long long': ctypes.c_ulonglong, + 'float': ctypes.c_float, + 'double': ctypes.c_double, + '_Bool': ctypes.c_bool, + } + + for _name in ['unsigned long long', 'unsigned long', + 'unsigned int', 'unsigned short', 'unsigned char']: + _size = ctypes.sizeof(PRIMITIVE_TYPES[_name]) + PRIMITIVE_TYPES['uint%d_t' % (8*_size)] = PRIMITIVE_TYPES[_name] + if _size == ctypes.sizeof(ctypes.c_void_p): + PRIMITIVE_TYPES['uintptr_t'] = PRIMITIVE_TYPES[_name] + if _size == ctypes.sizeof(ctypes.c_size_t): + PRIMITIVE_TYPES['size_t'] = PRIMITIVE_TYPES[_name] + + for _name in ['long long', 'long', 'int', 'short', 'signed char']: + _size = ctypes.sizeof(PRIMITIVE_TYPES[_name]) + PRIMITIVE_TYPES['int%d_t' % (8*_size)] = PRIMITIVE_TYPES[_name] + if _size == ctypes.sizeof(ctypes.c_void_p): + PRIMITIVE_TYPES['intptr_t'] = PRIMITIVE_TYPES[_name] + PRIMITIVE_TYPES['ptrdiff_t'] = PRIMITIVE_TYPES[_name] + if _size == ctypes.sizeof(ctypes.c_size_t): + PRIMITIVE_TYPES['ssize_t'] = PRIMITIVE_TYPES[_name] + + + def __init__(self): + self.RTLD_LAZY = 0 # not supported anyway by ctypes + self.RTLD_NOW = 0 + self.RTLD_GLOBAL = ctypes.RTLD_GLOBAL + self.RTLD_LOCAL = ctypes.RTLD_LOCAL + + def set_ffi(self, ffi): + self.ffi = ffi + + def _get_types(self): + return CTypesData, CTypesType + + def load_library(self, path, flags=0): + cdll = ctypes.CDLL(path, flags) + return CTypesLibrary(self, cdll) + + def new_void_type(self): + class CTypesVoid(CTypesData): + __slots__ = [] + _reftypename = 'void &' + @staticmethod + def _from_ctypes(novalue): + return None + @staticmethod + def _to_ctypes(novalue): + if novalue is not None: + raise TypeError("None expected, got %s object" % + (type(novalue).__name__,)) + CTypesVoid._fix_class() + return CTypesVoid + + def new_primitive_type(self, name): + if name == 'wchar_t': + raise NotImplementedError(name) + ctype = self.PRIMITIVE_TYPES[name] + if name == 'char': + kind = 'char' + elif name in ('float', 'double'): + kind = 'float' + else: + if name in ('signed char', 'unsigned char'): + kind = 'byte' + elif name == '_Bool': + kind = 'bool' + else: + kind = 'int' + is_signed = (ctype(-1).value == -1) + # + def _cast_source_to_int(source): + if isinstance(source, (int, long, float)): + source = int(source) + elif isinstance(source, CTypesData): + source = source._cast_to_integer() + elif isinstance(source, bytes): + source = ord(source) + elif source is None: + source = 0 + else: + raise TypeError("bad type for cast to %r: %r" % + (CTypesPrimitive, type(source).__name__)) + return source + # + kind1 = kind + class CTypesPrimitive(CTypesGenericPrimitive): + __slots__ = ['_value'] + _ctype = ctype + _reftypename = '%s &' % name + kind = kind1 + + def __init__(self, value): + self._value = value + + @staticmethod + def _create_ctype_obj(init): + if init is None: + return ctype() + return ctype(CTypesPrimitive._to_ctypes(init)) + + if kind in {'int', 'byte'}: + @classmethod + def _cast_from(cls, source): + source = _cast_source_to_int(source) + source = ctype(source).value # cast within range + return cls(source) + def __int__(self): + return self._value + + if kind == 'bool': + @classmethod + def _cast_from(cls, source): + if not isinstance(source, (int, long, float)): + source = _cast_source_to_int(source) + return cls(bool(source)) + def __int__(self): + return int(self._value) + + if kind == 'char': + @classmethod + def _cast_from(cls, source): + source = _cast_source_to_int(source) + source = bytechr(source & 0xFF) + return cls(source) + def __int__(self): + return ord(self._value) + + if kind == 'float': + @classmethod + def _cast_from(cls, source): + if isinstance(source, float): + pass + elif isinstance(source, CTypesGenericPrimitive): + if hasattr(source, '__float__'): + source = float(source) + else: + source = int(source) + else: + source = _cast_source_to_int(source) + source = ctype(source).value # fix precision + return cls(source) + def __int__(self): + return int(self._value) + def __float__(self): + return self._value + + _cast_to_integer = __int__ + + if kind in {'int', 'byte', 'bool'}: + @staticmethod + def _to_ctypes(x): + if not isinstance(x, (int, long)): + if isinstance(x, CTypesData): + x = int(x) + else: + raise TypeError("integer expected, got %s" % + type(x).__name__) + if ctype(x).value != x: + if not is_signed and x < 0: + raise OverflowError("%s: negative integer" % name) + else: + raise OverflowError("%s: integer out of bounds" + % name) + return x + + if kind == 'char': + @staticmethod + def _to_ctypes(x): + if isinstance(x, bytes) and len(x) == 1: + return x + if isinstance(x, CTypesPrimitive): # > + return x._value + raise TypeError("character expected, got %s" % + type(x).__name__) + def __nonzero__(self): + return ord(self._value) != 0 + else: + def __nonzero__(self): + return self._value != 0 + __bool__ = __nonzero__ + + if kind == 'float': + @staticmethod + def _to_ctypes(x): + if not isinstance(x, (int, long, float, CTypesData)): + raise TypeError("float expected, got %s" % + type(x).__name__) + return ctype(x).value + + @staticmethod + def _from_ctypes(value): + return getattr(value, 'value', value) + + @staticmethod + def _initialize(blob, init): + blob.value = CTypesPrimitive._to_ctypes(init) + + if kind == 'char': + def _to_string(self, maxlen): + return self._value + if kind == 'byte': + def _to_string(self, maxlen): + return chr(self._value & 0xff) + # + CTypesPrimitive._fix_class() + return CTypesPrimitive + + def new_pointer_type(self, BItem): + getbtype = self.ffi._get_cached_btype + if BItem is getbtype(model.PrimitiveType('char')): + kind = 'charp' + elif BItem in (getbtype(model.PrimitiveType('signed char')), + getbtype(model.PrimitiveType('unsigned char'))): + kind = 'bytep' + elif BItem is getbtype(model.void_type): + kind = 'voidp' + else: + kind = 'generic' + # + class CTypesPtr(CTypesGenericPtr): + __slots__ = ['_own'] + if kind == 'charp': + __slots__ += ['__as_strbuf'] + _BItem = BItem + if hasattr(BItem, '_ctype'): + _ctype = ctypes.POINTER(BItem._ctype) + _bitem_size = ctypes.sizeof(BItem._ctype) + else: + _ctype = ctypes.c_void_p + if issubclass(BItem, CTypesGenericArray): + _reftypename = BItem._get_c_name('(* &)') + else: + _reftypename = BItem._get_c_name(' * &') + + def __init__(self, init): + ctypeobj = BItem._create_ctype_obj(init) + if kind == 'charp': + self.__as_strbuf = ctypes.create_string_buffer( + ctypeobj.value + b'\x00') + self._as_ctype_ptr = ctypes.cast( + self.__as_strbuf, self._ctype) + else: + self._as_ctype_ptr = ctypes.pointer(ctypeobj) + self._address = ctypes.cast(self._as_ctype_ptr, + ctypes.c_void_p).value + self._own = True + + def __add__(self, other): + if isinstance(other, (int, long)): + return self._new_pointer_at(self._address + + other * self._bitem_size) + else: + return NotImplemented + + def __sub__(self, other): + if isinstance(other, (int, long)): + return self._new_pointer_at(self._address - + other * self._bitem_size) + elif type(self) is type(other): + return (self._address - other._address) // self._bitem_size + else: + return NotImplemented + + def __getitem__(self, index): + if getattr(self, '_own', False) and index != 0: + raise IndexError + return BItem._from_ctypes(self._as_ctype_ptr[index]) + + def __setitem__(self, index, value): + self._as_ctype_ptr[index] = BItem._to_ctypes(value) + + if kind in {'charp', 'voidp'}: + @classmethod + def _arg_to_ctypes(cls, *value): + if value and isinstance(value[0], bytes): + return ctypes.c_char_p(value[0]) + else: + return super()._arg_to_ctypes(*value) + + if kind in {'charp', 'bytep'}: + def _to_string(self, maxlen): + if maxlen < 0: + maxlen = sys.maxsize + p = ctypes.cast(self._as_ctype_ptr, + ctypes.POINTER(ctypes.c_char)) + n = 0 + while n < maxlen and p[n] != b'\x00': + n += 1 + return b''.join([p[i] for i in range(n)]) + + def _get_own_repr(self): + if getattr(self, '_own', False): + return 'owning %d bytes' % ( + ctypes.sizeof(self._as_ctype_ptr.contents),) + return super()._get_own_repr() + # + if (BItem is self.ffi._get_cached_btype(model.void_type) or + BItem is self.ffi._get_cached_btype(model.PrimitiveType('char'))): + CTypesPtr._automatic_casts = True + # + CTypesPtr._fix_class() + return CTypesPtr + + def new_array_type(self, CTypesPtr, length): + if length is None: + brackets = ' &[]' + else: + brackets = ' &[%d]' % length + BItem = CTypesPtr._BItem + getbtype = self.ffi._get_cached_btype + if BItem is getbtype(model.PrimitiveType('char')): + kind = 'char' + elif BItem in (getbtype(model.PrimitiveType('signed char')), + getbtype(model.PrimitiveType('unsigned char'))): + kind = 'byte' + else: + kind = 'generic' + # + class CTypesArray(CTypesGenericArray): + __slots__ = ['_blob', '_own'] + if length is not None: + _ctype = BItem._ctype * length + else: + __slots__.append('_ctype') + _reftypename = BItem._get_c_name(brackets) + _declared_length = length + _CTPtr = CTypesPtr + + def __init__(self, init): + if length is None: + if isinstance(init, (int, long)): + len1 = init + init = None + elif kind == 'char' and isinstance(init, bytes): + len1 = len(init) + 1 # extra null + else: + init = tuple(init) + len1 = len(init) + self._ctype = BItem._ctype * len1 + self._blob = self._ctype() + self._own = True + if init is not None: + self._initialize(self._blob, init) + + @staticmethod + def _initialize(blob, init): + if isinstance(init, bytes): + init = [init[i:i+1] for i in range(len(init))] + else: + if isinstance(init, CTypesGenericArray): + if (len(init) != len(blob) or + not isinstance(init, CTypesArray)): + raise TypeError("length/type mismatch: %s" % (init,)) + init = tuple(init) + if len(init) > len(blob): + raise IndexError("too many initializers") + addr = ctypes.cast(blob, ctypes.c_void_p).value + PTR = ctypes.POINTER(BItem._ctype) + itemsize = ctypes.sizeof(BItem._ctype) + for i, value in enumerate(init): + p = ctypes.cast(addr + i * itemsize, PTR) + BItem._initialize(p.contents, value) + + def __len__(self): + return len(self._blob) + + def __getitem__(self, index): + if not (0 <= index < len(self._blob)): + raise IndexError + return BItem._from_ctypes(self._blob[index]) + + def __setitem__(self, index, value): + if not (0 <= index < len(self._blob)): + raise IndexError + self._blob[index] = BItem._to_ctypes(value) + + if kind in {'char', 'byte'}: + def _to_string(self, maxlen): + if maxlen < 0: + maxlen = len(self._blob) + p = ctypes.cast(self._blob, + ctypes.POINTER(ctypes.c_char)) + n = 0 + while n < maxlen and p[n] != b'\x00': + n += 1 + return b''.join([p[i] for i in range(n)]) + + def _get_own_repr(self): + if getattr(self, '_own', False): + return 'owning %d bytes' % (ctypes.sizeof(self._blob),) + return super()._get_own_repr() + + def _convert_to_address(self, BClass): + if BClass in (CTypesPtr, None) or BClass._automatic_casts: + return ctypes.addressof(self._blob) + else: + return CTypesData._convert_to_address(self, BClass) + + @staticmethod + def _from_ctypes(ctypes_array): + self = CTypesArray.__new__(CTypesArray) + self._blob = ctypes_array + return self + + @staticmethod + def _arg_to_ctypes(value): + return CTypesPtr._arg_to_ctypes(value) + + def __add__(self, other): + if isinstance(other, (int, long)): + return CTypesPtr._new_pointer_at( + ctypes.addressof(self._blob) + + other * ctypes.sizeof(BItem._ctype)) + else: + return NotImplemented + + @classmethod + def _cast_from(cls, source): + raise NotImplementedError("casting to %r" % ( + cls._get_c_name(),)) + # + CTypesArray._fix_class() + return CTypesArray + + def _new_struct_or_union(self, kind, name, base_ctypes_class): + # + class struct_or_union(base_ctypes_class): + pass + struct_or_union.__name__ = '%s_%s' % (kind, name) + kind1 = kind + # + class CTypesStructOrUnion(CTypesBaseStructOrUnion): + __slots__ = ['_blob'] + _ctype = struct_or_union + _reftypename = '%s &' % (name,) + _kind = kind = kind1 + # + CTypesStructOrUnion._fix_class() + return CTypesStructOrUnion + + def new_struct_type(self, name): + return self._new_struct_or_union('struct', name, ctypes.Structure) + + def new_union_type(self, name): + return self._new_struct_or_union('union', name, ctypes.Union) + + def complete_struct_or_union(self, CTypesStructOrUnion, fields, tp, + totalsize=-1, totalalignment=-1, sflags=0, + pack=0): + if totalsize >= 0 or totalalignment >= 0: + raise NotImplementedError("the ctypes backend of CFFI does not support " + "structures completed by verify(); please " + "compile and install the _cffi_backend module.") + struct_or_union = CTypesStructOrUnion._ctype + fnames = [fname for (fname, BField, bitsize) in fields] + btypes = [BField for (fname, BField, bitsize) in fields] + bitfields = [bitsize for (fname, BField, bitsize) in fields] + # + bfield_types = {} + cfields = [] + for (fname, BField, bitsize) in fields: + if bitsize < 0: + cfields.append((fname, BField._ctype)) + bfield_types[fname] = BField + else: + cfields.append((fname, BField._ctype, bitsize)) + bfield_types[fname] = Ellipsis + if sflags & 8: + struct_or_union._pack_ = 1 + elif pack: + struct_or_union._pack_ = pack + struct_or_union._fields_ = cfields + CTypesStructOrUnion._bfield_types = bfield_types + # + @staticmethod + def _create_ctype_obj(init): + result = struct_or_union() + if init is not None: + initialize(result, init) + return result + CTypesStructOrUnion._create_ctype_obj = _create_ctype_obj + # + def initialize(blob, init): + if is_union: + if len(init) > 1: + raise ValueError("union initializer: %d items given, but " + "only one supported (use a dict if needed)" + % (len(init),)) + if not isinstance(init, dict): + if isinstance(init, (bytes, unicode)): + raise TypeError("union initializer: got a str") + init = tuple(init) + if len(init) > len(fnames): + raise ValueError("too many values for %s initializer" % + CTypesStructOrUnion._get_c_name()) + init = dict(zip(fnames, init)) + addr = ctypes.addressof(blob) + for fname, value in init.items(): + BField, bitsize = name2fieldtype[fname] + assert bitsize < 0, \ + "not implemented: initializer with bit fields" + offset = CTypesStructOrUnion._offsetof(fname) + PTR = ctypes.POINTER(BField._ctype) + p = ctypes.cast(addr + offset, PTR) + BField._initialize(p.contents, value) + is_union = CTypesStructOrUnion._kind == 'union' + name2fieldtype = dict(zip(fnames, zip(btypes, bitfields))) + # + for fname, BField, bitsize in fields: + if fname == '': + raise NotImplementedError("nested anonymous structs/unions") + if hasattr(CTypesStructOrUnion, fname): + raise ValueError("the field name %r conflicts in " + "the ctypes backend" % fname) + if bitsize < 0: + def getter(self, fname=fname, BField=BField, + offset=CTypesStructOrUnion._offsetof(fname), + PTR=ctypes.POINTER(BField._ctype)): + addr = ctypes.addressof(self._blob) + p = ctypes.cast(addr + offset, PTR) + return BField._from_ctypes(p.contents) + def setter(self, value, fname=fname, BField=BField): + setattr(self._blob, fname, BField._to_ctypes(value)) + # + if issubclass(BField, CTypesGenericArray): + setter = None + if BField._declared_length == 0: + def getter(self, fname=fname, BFieldPtr=BField._CTPtr, + offset=CTypesStructOrUnion._offsetof(fname), + PTR=ctypes.POINTER(BField._ctype)): + addr = ctypes.addressof(self._blob) + p = ctypes.cast(addr + offset, PTR) + return BFieldPtr._from_ctypes(p) + # + else: + def getter(self, fname=fname, BField=BField): + return BField._from_ctypes(getattr(self._blob, fname)) + def setter(self, value, fname=fname, BField=BField): + # xxx obscure workaround + value = BField._to_ctypes(value) + oldvalue = getattr(self._blob, fname) + setattr(self._blob, fname, value) + if value != getattr(self._blob, fname): + setattr(self._blob, fname, oldvalue) + raise OverflowError("value too large for bitfield") + setattr(CTypesStructOrUnion, fname, property(getter, setter)) + # + CTypesPtr = self.ffi._get_cached_btype(model.PointerType(tp)) + for fname in fnames: + if hasattr(CTypesPtr, fname): + raise ValueError("the field name %r conflicts in " + "the ctypes backend" % fname) + def getter(self, fname=fname): + return getattr(self[0], fname) + def setter(self, value, fname=fname): + setattr(self[0], fname, value) + setattr(CTypesPtr, fname, property(getter, setter)) + + def new_function_type(self, BArgs, BResult, has_varargs): + nameargs = [BArg._get_c_name() for BArg in BArgs] + if has_varargs: + nameargs.append('...') + nameargs = ', '.join(nameargs) + # + class CTypesFunctionPtr(CTypesGenericPtr): + __slots__ = ['_own_callback', '_name'] + _ctype = ctypes.CFUNCTYPE(getattr(BResult, '_ctype', None), + *[BArg._ctype for BArg in BArgs], + use_errno=True) + _reftypename = BResult._get_c_name('(* &)(%s)' % (nameargs,)) + + def __init__(self, init, error=None): + # create a callback to the Python callable init() + import traceback + assert not has_varargs, "varargs not supported for callbacks" + if getattr(BResult, '_ctype', None) is not None: + error = BResult._from_ctypes( + BResult._create_ctype_obj(error)) + else: + error = None + def callback(*args): + args2 = [] + for arg, BArg in zip(args, BArgs): + args2.append(BArg._from_ctypes(arg)) + try: + res2 = init(*args2) + res2 = BResult._to_ctypes(res2) + except: + traceback.print_exc() + res2 = error + if issubclass(BResult, CTypesGenericPtr): + if res2: + res2 = ctypes.cast(res2, ctypes.c_void_p).value + # .value: http://bugs.python.org/issue1574593 + else: + res2 = None + #print repr(res2) + return res2 + if issubclass(BResult, CTypesGenericPtr): + # The only pointers callbacks can return are void*s: + # http://bugs.python.org/issue5710 + callback_ctype = ctypes.CFUNCTYPE( + ctypes.c_void_p, + *[BArg._ctype for BArg in BArgs], + use_errno=True) + else: + callback_ctype = CTypesFunctionPtr._ctype + self._as_ctype_ptr = callback_ctype(callback) + self._address = ctypes.cast(self._as_ctype_ptr, + ctypes.c_void_p).value + self._own_callback = init + + @staticmethod + def _initialize(ctypes_ptr, value): + if value: + raise NotImplementedError("ctypes backend: not supported: " + "initializers for function pointers") + + def __repr__(self): + c_name = getattr(self, '_name', None) + if c_name: + i = self._reftypename.index('(* &)') + if self._reftypename[i-1] not in ' )*': + c_name = ' ' + c_name + c_name = self._reftypename.replace('(* &)', c_name) + return CTypesData.__repr__(self, c_name) + + def _get_own_repr(self): + if getattr(self, '_own_callback', None) is not None: + return 'calling %r' % (self._own_callback,) + return super()._get_own_repr() + + def __call__(self, *args): + if has_varargs: + assert len(args) >= len(BArgs) + extraargs = args[len(BArgs):] + args = args[:len(BArgs)] + else: + assert len(args) == len(BArgs) + ctypes_args = [] + for arg, BArg in zip(args, BArgs): + ctypes_args.append(BArg._arg_to_ctypes(arg)) + if has_varargs: + for i, arg in enumerate(extraargs): + if arg is None: + ctypes_args.append(ctypes.c_void_p(0)) # NULL + continue + if not isinstance(arg, CTypesData): + raise TypeError( + "argument %d passed in the variadic part " + "needs to be a cdata object (got %s)" % + (1 + len(BArgs) + i, type(arg).__name__)) + ctypes_args.append(arg._arg_to_ctypes(arg)) + result = self._as_ctype_ptr(*ctypes_args) + return BResult._from_ctypes(result) + # + CTypesFunctionPtr._fix_class() + return CTypesFunctionPtr + + def new_enum_type(self, name, enumerators, enumvalues, CTypesInt): + assert isinstance(name, str) + reverse_mapping = dict(zip(reversed(enumvalues), + reversed(enumerators))) + # + class CTypesEnum(CTypesInt): + __slots__ = [] + _reftypename = '%s &' % name + + def _get_own_repr(self): + value = self._value + try: + return '%d: %s' % (value, reverse_mapping[value]) + except KeyError: + return str(value) + + def _to_string(self, maxlen): + value = self._value + try: + return reverse_mapping[value] + except KeyError: + return str(value) + # + CTypesEnum._fix_class() + return CTypesEnum + + def get_errno(self): + return ctypes.get_errno() + + def set_errno(self, value): + ctypes.set_errno(value) + + def string(self, b, maxlen=-1): + return b._to_string(maxlen) + + def buffer(self, bptr, size=-1): + raise NotImplementedError("buffer() with ctypes backend") + + def sizeof(self, cdata_or_BType): + if isinstance(cdata_or_BType, CTypesData): + return cdata_or_BType._get_size_of_instance() + else: + assert issubclass(cdata_or_BType, CTypesData) + return cdata_or_BType._get_size() + + def alignof(self, BType): + assert issubclass(BType, CTypesData) + return BType._alignment() + + def newp(self, BType, source): + if not issubclass(BType, CTypesData): + raise TypeError + return BType._newp(source) + + def cast(self, BType, source): + return BType._cast_from(source) + + def callback(self, BType, source, error, onerror): + assert onerror is None # XXX not implemented + return BType(source, error) + + _weakref_cache_ref = None + + def gcp(self, cdata, destructor, size=0): + if self._weakref_cache_ref is None: + import weakref + class MyRef(weakref.ref): + def __eq__(self, other): + myref = self() + return self is other or ( + myref is not None and myref is other()) + def __ne__(self, other): + return not (self == other) + def __hash__(self): + try: + return self._hash + except AttributeError: + self._hash = hash(self()) + return self._hash + self._weakref_cache_ref = {}, MyRef + weak_cache, MyRef = self._weakref_cache_ref + + if destructor is None: + try: + del weak_cache[MyRef(cdata)] + except KeyError: + raise TypeError("Can remove destructor only on a object " + "previously returned by ffi.gc()") + return None + + def remove(k): + cdata, destructor = weak_cache.pop(k, (None, None)) + if destructor is not None: + destructor(cdata) + + new_cdata = self.cast(self.typeof(cdata), cdata) + assert new_cdata is not cdata + weak_cache[MyRef(new_cdata, remove)] = (cdata, destructor) + return new_cdata + + typeof = type + + def getcname(self, BType, replace_with): + return BType._get_c_name(replace_with) + + def typeoffsetof(self, BType, fieldname, num=0): + if isinstance(fieldname, str): + if num == 0 and issubclass(BType, CTypesGenericPtr): + BType = BType._BItem + if not issubclass(BType, CTypesBaseStructOrUnion): + raise TypeError("expected a struct or union ctype") + BField = BType._bfield_types[fieldname] + if BField is Ellipsis: + raise TypeError("not supported for bitfields") + return (BField, BType._offsetof(fieldname)) + elif isinstance(fieldname, (int, long)): + if issubclass(BType, CTypesGenericArray): + BType = BType._CTPtr + if not issubclass(BType, CTypesGenericPtr): + raise TypeError("expected an array or ptr ctype") + BItem = BType._BItem + offset = BItem._get_size() * fieldname + if offset > sys.maxsize: + raise OverflowError + return (BItem, offset) + else: + raise TypeError(type(fieldname)) + + def rawaddressof(self, BTypePtr, cdata, offset=None): + if isinstance(cdata, CTypesBaseStructOrUnion): + ptr = ctypes.pointer(type(cdata)._to_ctypes(cdata)) + elif isinstance(cdata, CTypesGenericPtr): + if offset is None or not issubclass(type(cdata)._BItem, + CTypesBaseStructOrUnion): + raise TypeError("unexpected cdata type") + ptr = type(cdata)._to_ctypes(cdata) + elif isinstance(cdata, CTypesGenericArray): + ptr = type(cdata)._to_ctypes(cdata) + else: + raise TypeError("expected a ") + if offset: + ptr = ctypes.cast( + ctypes.c_void_p( + ctypes.cast(ptr, ctypes.c_void_p).value + offset), + type(ptr)) + return BTypePtr._from_ctypes(ptr) + + +class CTypesLibrary: + + def __init__(self, backend, cdll): + self.backend = backend + self.cdll = cdll + + def load_function(self, BType, name): + c_func = getattr(self.cdll, name) + funcobj = BType._from_ctypes(c_func) + funcobj._name = name + return funcobj + + def read_variable(self, BType, name): + try: + ctypes_obj = BType._ctype.in_dll(self.cdll, name) + except AttributeError as e: + raise NotImplementedError(e) + return BType._from_ctypes(ctypes_obj) + + def write_variable(self, BType, name, value): + new_ctypes_obj = BType._to_ctypes(value) + ctypes_obj = BType._ctype.in_dll(self.cdll, name) + ctypes.memmove(ctypes.addressof(ctypes_obj), + ctypes.addressof(new_ctypes_obj), + ctypes.sizeof(BType._ctype)) diff --git a/source/target-vendors/cp312/cffi/cffi_opcode.py b/source/target-vendors/cp312/cffi/cffi_opcode.py new file mode 100644 index 0000000..c964ba1 --- /dev/null +++ b/source/target-vendors/cp312/cffi/cffi_opcode.py @@ -0,0 +1,187 @@ +from .error import VerificationError + +class CffiOp: + def __init__(self, op, arg): + self.op = op + self.arg = arg + + def as_c_expr(self): + if self.op is None: + assert isinstance(self.arg, str) + return '(_cffi_opcode_t)(%s)' % (self.arg,) + classname = CLASS_NAME[self.op] + return '_CFFI_OP(_CFFI_OP_%s, %s)' % (classname, self.arg) + + def as_python_bytes(self): + if self.op is None and self.arg.isdigit(): + value = int(self.arg) # non-negative: '-' not in self.arg + if value >= 2**31: + raise OverflowError("cannot emit %r: limited to 2**31-1" + % (self.arg,)) + return format_four_bytes(value) + if isinstance(self.arg, str): + raise VerificationError("cannot emit to Python: %r" % (self.arg,)) + return format_four_bytes((self.arg << 8) | self.op) + + def __str__(self): + classname = CLASS_NAME.get(self.op, self.op) + return '(%s %s)' % (classname, self.arg) + +def format_four_bytes(num): + return '\\x%02X\\x%02X\\x%02X\\x%02X' % ( + (num >> 24) & 0xFF, + (num >> 16) & 0xFF, + (num >> 8) & 0xFF, + (num ) & 0xFF) + +OP_PRIMITIVE = 1 +OP_POINTER = 3 +OP_ARRAY = 5 +OP_OPEN_ARRAY = 7 +OP_STRUCT_UNION = 9 +OP_ENUM = 11 +OP_FUNCTION = 13 +OP_FUNCTION_END = 15 +OP_NOOP = 17 +OP_BITFIELD = 19 +OP_TYPENAME = 21 +OP_CPYTHON_BLTN_V = 23 # varargs +OP_CPYTHON_BLTN_N = 25 # noargs +OP_CPYTHON_BLTN_O = 27 # O (i.e. a single arg) +OP_CONSTANT = 29 +OP_CONSTANT_INT = 31 +OP_GLOBAL_VAR = 33 +OP_DLOPEN_FUNC = 35 +OP_DLOPEN_CONST = 37 +OP_GLOBAL_VAR_F = 39 +OP_EXTERN_PYTHON = 41 + +PRIM_VOID = 0 +PRIM_BOOL = 1 +PRIM_CHAR = 2 +PRIM_SCHAR = 3 +PRIM_UCHAR = 4 +PRIM_SHORT = 5 +PRIM_USHORT = 6 +PRIM_INT = 7 +PRIM_UINT = 8 +PRIM_LONG = 9 +PRIM_ULONG = 10 +PRIM_LONGLONG = 11 +PRIM_ULONGLONG = 12 +PRIM_FLOAT = 13 +PRIM_DOUBLE = 14 +PRIM_LONGDOUBLE = 15 + +PRIM_WCHAR = 16 +PRIM_INT8 = 17 +PRIM_UINT8 = 18 +PRIM_INT16 = 19 +PRIM_UINT16 = 20 +PRIM_INT32 = 21 +PRIM_UINT32 = 22 +PRIM_INT64 = 23 +PRIM_UINT64 = 24 +PRIM_INTPTR = 25 +PRIM_UINTPTR = 26 +PRIM_PTRDIFF = 27 +PRIM_SIZE = 28 +PRIM_SSIZE = 29 +PRIM_INT_LEAST8 = 30 +PRIM_UINT_LEAST8 = 31 +PRIM_INT_LEAST16 = 32 +PRIM_UINT_LEAST16 = 33 +PRIM_INT_LEAST32 = 34 +PRIM_UINT_LEAST32 = 35 +PRIM_INT_LEAST64 = 36 +PRIM_UINT_LEAST64 = 37 +PRIM_INT_FAST8 = 38 +PRIM_UINT_FAST8 = 39 +PRIM_INT_FAST16 = 40 +PRIM_UINT_FAST16 = 41 +PRIM_INT_FAST32 = 42 +PRIM_UINT_FAST32 = 43 +PRIM_INT_FAST64 = 44 +PRIM_UINT_FAST64 = 45 +PRIM_INTMAX = 46 +PRIM_UINTMAX = 47 +PRIM_FLOATCOMPLEX = 48 +PRIM_DOUBLECOMPLEX = 49 +PRIM_CHAR16 = 50 +PRIM_CHAR32 = 51 + +_NUM_PRIM = 52 +_UNKNOWN_PRIM = -1 +_UNKNOWN_FLOAT_PRIM = -2 +_UNKNOWN_LONG_DOUBLE = -3 + +_IO_FILE_STRUCT = -1 + +PRIMITIVE_TO_INDEX = { + 'char': PRIM_CHAR, + 'short': PRIM_SHORT, + 'int': PRIM_INT, + 'long': PRIM_LONG, + 'long long': PRIM_LONGLONG, + 'signed char': PRIM_SCHAR, + 'unsigned char': PRIM_UCHAR, + 'unsigned short': PRIM_USHORT, + 'unsigned int': PRIM_UINT, + 'unsigned long': PRIM_ULONG, + 'unsigned long long': PRIM_ULONGLONG, + 'float': PRIM_FLOAT, + 'double': PRIM_DOUBLE, + 'long double': PRIM_LONGDOUBLE, + '_cffi_float_complex_t': PRIM_FLOATCOMPLEX, + '_cffi_double_complex_t': PRIM_DOUBLECOMPLEX, + '_Bool': PRIM_BOOL, + 'wchar_t': PRIM_WCHAR, + 'char16_t': PRIM_CHAR16, + 'char32_t': PRIM_CHAR32, + 'int8_t': PRIM_INT8, + 'uint8_t': PRIM_UINT8, + 'int16_t': PRIM_INT16, + 'uint16_t': PRIM_UINT16, + 'int32_t': PRIM_INT32, + 'uint32_t': PRIM_UINT32, + 'int64_t': PRIM_INT64, + 'uint64_t': PRIM_UINT64, + 'intptr_t': PRIM_INTPTR, + 'uintptr_t': PRIM_UINTPTR, + 'ptrdiff_t': PRIM_PTRDIFF, + 'size_t': PRIM_SIZE, + 'ssize_t': PRIM_SSIZE, + 'int_least8_t': PRIM_INT_LEAST8, + 'uint_least8_t': PRIM_UINT_LEAST8, + 'int_least16_t': PRIM_INT_LEAST16, + 'uint_least16_t': PRIM_UINT_LEAST16, + 'int_least32_t': PRIM_INT_LEAST32, + 'uint_least32_t': PRIM_UINT_LEAST32, + 'int_least64_t': PRIM_INT_LEAST64, + 'uint_least64_t': PRIM_UINT_LEAST64, + 'int_fast8_t': PRIM_INT_FAST8, + 'uint_fast8_t': PRIM_UINT_FAST8, + 'int_fast16_t': PRIM_INT_FAST16, + 'uint_fast16_t': PRIM_UINT_FAST16, + 'int_fast32_t': PRIM_INT_FAST32, + 'uint_fast32_t': PRIM_UINT_FAST32, + 'int_fast64_t': PRIM_INT_FAST64, + 'uint_fast64_t': PRIM_UINT_FAST64, + 'intmax_t': PRIM_INTMAX, + 'uintmax_t': PRIM_UINTMAX, + } + +F_UNION = 0x01 +F_CHECK_FIELDS = 0x02 +F_PACKED = 0x04 +F_EXTERNAL = 0x08 +F_OPAQUE = 0x10 + +G_FLAGS = dict([('_CFFI_' + _key, globals()[_key]) + for _key in ['F_UNION', 'F_CHECK_FIELDS', 'F_PACKED', + 'F_EXTERNAL', 'F_OPAQUE']]) + +CLASS_NAME = {} +for _name, _value in list(globals().items()): + if _name.startswith('OP_') and isinstance(_value, int): + CLASS_NAME[_value] = _name[3:] diff --git a/source/target-vendors/cp312/cffi/commontypes.py b/source/target-vendors/cp312/cffi/commontypes.py new file mode 100644 index 0000000..d4dae35 --- /dev/null +++ b/source/target-vendors/cp312/cffi/commontypes.py @@ -0,0 +1,82 @@ +import sys +from . import model +from .error import FFIError + + +COMMON_TYPES = {} + +try: + # fetch "bool" and all simple Windows types + from _cffi_backend import _get_common_types + _get_common_types(COMMON_TYPES) +except ImportError: + pass + +COMMON_TYPES['FILE'] = model.unknown_type('FILE', '_IO_FILE') +COMMON_TYPES['bool'] = '_Bool' # in case we got ImportError above +COMMON_TYPES['float _Complex'] = '_cffi_float_complex_t' +COMMON_TYPES['double _Complex'] = '_cffi_double_complex_t' + +for _type in model.PrimitiveType.ALL_PRIMITIVE_TYPES: + if _type.endswith('_t'): + COMMON_TYPES[_type] = _type +del _type + +_CACHE = {} + +def resolve_common_type(parser, commontype): + try: + return _CACHE[commontype] + except KeyError: + cdecl = COMMON_TYPES.get(commontype, commontype) + if not isinstance(cdecl, str): + result, quals = cdecl, 0 # cdecl is already a BaseType + elif cdecl in model.PrimitiveType.ALL_PRIMITIVE_TYPES: + result, quals = model.PrimitiveType(cdecl), 0 + elif cdecl == 'set-unicode-needed': + raise FFIError("The Windows type %r is only available after " + "you call ffi.set_unicode()" % (commontype,)) + else: + if commontype == cdecl: + raise FFIError( + "Unsupported type: %r. Please look at " + "http://cffi.readthedocs.io/en/latest/cdef.html#ffi-cdef-limitations " + "and file an issue if you think this type should really " + "be supported." % (commontype,)) + result, quals = parser.parse_type_and_quals(cdecl) # recursive + + assert isinstance(result, model.BaseTypeByIdentity) + _CACHE[commontype] = result, quals + return result, quals + + +# ____________________________________________________________ +# extra types for Windows (most of them are in commontypes.c) + + +def win_common_types(): + return { + "UNICODE_STRING": model.StructType( + "_UNICODE_STRING", + ["Length", + "MaximumLength", + "Buffer"], + [model.PrimitiveType("unsigned short"), + model.PrimitiveType("unsigned short"), + model.PointerType(model.PrimitiveType("wchar_t"))], + [-1, -1, -1]), + "PUNICODE_STRING": "UNICODE_STRING *", + "PCUNICODE_STRING": "const UNICODE_STRING *", + + "TBYTE": "set-unicode-needed", + "TCHAR": "set-unicode-needed", + "LPCTSTR": "set-unicode-needed", + "PCTSTR": "set-unicode-needed", + "LPTSTR": "set-unicode-needed", + "PTSTR": "set-unicode-needed", + "PTBYTE": "set-unicode-needed", + "PTCHAR": "set-unicode-needed", + } + +if sys.platform == 'win32': + COMMON_TYPES.update(win_common_types()) diff --git a/source/target-vendors/cp312/cffi/cparser.py b/source/target-vendors/cp312/cffi/cparser.py new file mode 100644 index 0000000..1896b7e --- /dev/null +++ b/source/target-vendors/cp312/cffi/cparser.py @@ -0,0 +1,1014 @@ +from . import model +from .commontypes import COMMON_TYPES, resolve_common_type +from .error import FFIError, CDefError +try: + from . import _pycparser as pycparser +except ImportError: + import pycparser +import weakref, re, sys + +try: + if sys.version_info < (3,): + import thread as _thread + else: + import _thread + lock = _thread.allocate_lock() +except ImportError: + lock = None + +def _workaround_for_static_import_finders(): + # Issue #392: packaging tools like cx_Freeze can not find these + # because pycparser uses exec dynamic import. This is an obscure + # workaround. This function is never called. + import pycparser.yacctab + import pycparser.lextab + +CDEF_SOURCE_STRING = "" +_r_comment = re.compile(r"/\*.*?\*/|//([^\n\\]|\\.)*?$", + re.DOTALL | re.MULTILINE) +_r_define = re.compile(r"^\s*#\s*define\s+([A-Za-z_][A-Za-z_0-9]*)" + r"\b((?:[^\n\\]|\\.)*?)$", + re.DOTALL | re.MULTILINE) +_r_line_directive = re.compile(r"^[ \t]*#[ \t]*(?:line|\d+)\b.*$", re.MULTILINE) +_r_partial_enum = re.compile(r"=\s*\.\.\.\s*[,}]|\.\.\.\s*\}") +_r_enum_dotdotdot = re.compile(r"__dotdotdot\d+__$") +_r_partial_array = re.compile(r"\[\s*\.\.\.\s*\]") +_r_words = re.compile(r"\w+|\S") +_parser_cache = None +_r_int_literal = re.compile(r"-?0?x?[0-9a-f]+[lu]*$", re.IGNORECASE) +_r_stdcall1 = re.compile(r"\b(__stdcall|WINAPI)\b") +_r_stdcall2 = re.compile(r"[(]\s*(__stdcall|WINAPI)\b") +_r_cdecl = re.compile(r"\b__cdecl\b") +_r_extern_python = re.compile(r'\bextern\s*"' + r'(Python|Python\s*\+\s*C|C\s*\+\s*Python)"\s*.') +_r_star_const_space = re.compile( # matches "* const " + r"[*]\s*((const|volatile|restrict)\b\s*)+") +_r_int_dotdotdot = re.compile(r"(\b(int|long|short|signed|unsigned|char)\s*)+" + r"\.\.\.") +_r_float_dotdotdot = re.compile(r"\b(double|float)\s*\.\.\.") + +def _get_parser(): + global _parser_cache + if _parser_cache is None: + _parser_cache = pycparser.CParser() + return _parser_cache + +def _workaround_for_old_pycparser(csource): + # Workaround for a pycparser issue (fixed between pycparser 2.10 and + # 2.14): "char*const***" gives us a wrong syntax tree, the same as + # for "char***(*const)". This means we can't tell the difference + # afterwards. But "char(*const(***))" gives us the right syntax + # tree. The issue only occurs if there are several stars in + # sequence with no parenthesis in between, just possibly qualifiers. + # Attempt to fix it by adding some parentheses in the source: each + # time we see "* const" or "* const *", we add an opening + # parenthesis before each star---the hard part is figuring out where + # to close them. + parts = [] + while True: + match = _r_star_const_space.search(csource) + if not match: + break + #print repr(''.join(parts)+csource), '=>', + parts.append(csource[:match.start()]) + parts.append('('); closing = ')' + parts.append(match.group()) # e.g. "* const " + endpos = match.end() + if csource.startswith('*', endpos): + parts.append('('); closing += ')' + level = 0 + i = endpos + while i < len(csource): + c = csource[i] + if c == '(': + level += 1 + elif c == ')': + if level == 0: + break + level -= 1 + elif c in ',;=': + if level == 0: + break + i += 1 + csource = csource[endpos:i] + closing + csource[i:] + #print repr(''.join(parts)+csource) + parts.append(csource) + return ''.join(parts) + +def _preprocess_extern_python(csource): + # input: `extern "Python" int foo(int);` or + # `extern "Python" { int foo(int); }` + # output: + # void __cffi_extern_python_start; + # int foo(int); + # void __cffi_extern_python_stop; + # + # input: `extern "Python+C" int foo(int);` + # output: + # void __cffi_extern_python_plus_c_start; + # int foo(int); + # void __cffi_extern_python_stop; + parts = [] + while True: + match = _r_extern_python.search(csource) + if not match: + break + endpos = match.end() - 1 + #print + #print ''.join(parts)+csource + #print '=>' + parts.append(csource[:match.start()]) + if 'C' in match.group(1): + parts.append('void __cffi_extern_python_plus_c_start; ') + else: + parts.append('void __cffi_extern_python_start; ') + if csource[endpos] == '{': + # grouping variant + closing = csource.find('}', endpos) + if closing < 0: + raise CDefError("'extern \"Python\" {': no '}' found") + if csource.find('{', endpos + 1, closing) >= 0: + raise NotImplementedError("cannot use { } inside a block " + "'extern \"Python\" { ... }'") + parts.append(csource[endpos+1:closing]) + csource = csource[closing+1:] + else: + # non-grouping variant + semicolon = csource.find(';', endpos) + if semicolon < 0: + raise CDefError("'extern \"Python\": no ';' found") + parts.append(csource[endpos:semicolon+1]) + csource = csource[semicolon+1:] + parts.append(' void __cffi_extern_python_stop;') + #print ''.join(parts)+csource + #print + parts.append(csource) + return ''.join(parts) + +def _warn_for_string_literal(csource): + if '"' not in csource: + return + for line in csource.splitlines(): + if '"' in line and not line.lstrip().startswith('#'): + import warnings + warnings.warn("String literal found in cdef() or type source. " + "String literals are ignored here, but you should " + "remove them anyway because some character sequences " + "confuse pre-parsing.") + break + +def _warn_for_non_extern_non_static_global_variable(decl): + if not decl.storage: + import warnings + warnings.warn("Global variable '%s' in cdef(): for consistency " + "with C it should have a storage class specifier " + "(usually 'extern')" % (decl.name,)) + +def _remove_line_directives(csource): + # _r_line_directive matches whole lines, without the final \n, if they + # start with '#line' with some spacing allowed, or '#NUMBER'. This + # function stores them away and replaces them with exactly the string + # '#line@N', where N is the index in the list 'line_directives'. + line_directives = [] + def replace(m): + i = len(line_directives) + line_directives.append(m.group()) + return '#line@%d' % i + csource = _r_line_directive.sub(replace, csource) + return csource, line_directives + +def _put_back_line_directives(csource, line_directives): + def replace(m): + s = m.group() + if not s.startswith('#line@'): + raise AssertionError("unexpected #line directive " + "(should have been processed and removed") + return line_directives[int(s[6:])] + return _r_line_directive.sub(replace, csource) + +def _preprocess(csource): + # First, remove the lines of the form '#line N "filename"' because + # the "filename" part could confuse the rest + csource, line_directives = _remove_line_directives(csource) + # Remove comments. NOTE: this only work because the cdef() section + # should not contain any string literals (except in line directives)! + def replace_keeping_newlines(m): + return ' ' + m.group().count('\n') * '\n' + csource = _r_comment.sub(replace_keeping_newlines, csource) + # Remove the "#define FOO x" lines + macros = {} + for match in _r_define.finditer(csource): + macroname, macrovalue = match.groups() + macrovalue = macrovalue.replace('\\\n', '').strip() + macros[macroname] = macrovalue + csource = _r_define.sub('', csource) + # + if pycparser.__version__ < '2.14': + csource = _workaround_for_old_pycparser(csource) + # + # BIG HACK: replace WINAPI or __stdcall with "volatile const". + # It doesn't make sense for the return type of a function to be + # "volatile volatile const", so we abuse it to detect __stdcall... + # Hack number 2 is that "int(volatile *fptr)();" is not valid C + # syntax, so we place the "volatile" before the opening parenthesis. + csource = _r_stdcall2.sub(' volatile volatile const(', csource) + csource = _r_stdcall1.sub(' volatile volatile const ', csource) + csource = _r_cdecl.sub(' ', csource) + # + # Replace `extern "Python"` with start/end markers + csource = _preprocess_extern_python(csource) + # + # Now there should not be any string literal left; warn if we get one + _warn_for_string_literal(csource) + # + # Replace "[...]" with "[__dotdotdotarray__]" + csource = _r_partial_array.sub('[__dotdotdotarray__]', csource) + # + # Replace "...}" with "__dotdotdotNUM__}". This construction should + # occur only at the end of enums; at the end of structs we have "...;}" + # and at the end of vararg functions "...);". Also replace "=...[,}]" + # with ",__dotdotdotNUM__[,}]": this occurs in the enums too, when + # giving an unknown value. + matches = list(_r_partial_enum.finditer(csource)) + for number, match in enumerate(reversed(matches)): + p = match.start() + if csource[p] == '=': + p2 = csource.find('...', p, match.end()) + assert p2 > p + csource = '%s,__dotdotdot%d__ %s' % (csource[:p], number, + csource[p2+3:]) + else: + assert csource[p:p+3] == '...' + csource = '%s __dotdotdot%d__ %s' % (csource[:p], number, + csource[p+3:]) + # Replace "int ..." or "unsigned long int..." with "__dotdotdotint__" + csource = _r_int_dotdotdot.sub(' __dotdotdotint__ ', csource) + # Replace "float ..." or "double..." with "__dotdotdotfloat__" + csource = _r_float_dotdotdot.sub(' __dotdotdotfloat__ ', csource) + # Replace all remaining "..." with the same name, "__dotdotdot__", + # which is declared with a typedef for the purpose of C parsing. + csource = csource.replace('...', ' __dotdotdot__ ') + # Finally, put back the line directives + csource = _put_back_line_directives(csource, line_directives) + return csource, macros + +def _common_type_names(csource): + # Look in the source for what looks like usages of types from the + # list of common types. A "usage" is approximated here as the + # appearance of the word, minus a "definition" of the type, which + # is the last word in a "typedef" statement. Approximative only + # but should be fine for all the common types. + look_for_words = set(COMMON_TYPES) + look_for_words.add(';') + look_for_words.add(',') + look_for_words.add('(') + look_for_words.add(')') + look_for_words.add('typedef') + words_used = set() + is_typedef = False + paren = 0 + previous_word = '' + for word in _r_words.findall(csource): + if word in look_for_words: + if word == ';': + if is_typedef: + words_used.discard(previous_word) + look_for_words.discard(previous_word) + is_typedef = False + elif word == 'typedef': + is_typedef = True + paren = 0 + elif word == '(': + paren += 1 + elif word == ')': + paren -= 1 + elif word == ',': + if is_typedef and paren == 0: + words_used.discard(previous_word) + look_for_words.discard(previous_word) + else: # word in COMMON_TYPES + words_used.add(word) + previous_word = word + return words_used + + +class Parser: + + def __init__(self): + self._declarations = {} + self._included_declarations = set() + self._anonymous_counter = 0 + self._structnode2type = weakref.WeakKeyDictionary() + self._options = {} + self._int_constants = {} + self._recomplete = [] + self._uses_new_feature = None + + def _parse(self, csource): + csource, macros = _preprocess(csource) + # XXX: for more efficiency we would need to poke into the + # internals of CParser... the following registers the + # typedefs, because their presence or absence influences the + # parsing itself (but what they are typedef'ed to plays no role) + ctn = _common_type_names(csource) + typenames = [] + for name in sorted(self._declarations): + if name.startswith('typedef '): + name = name[8:] + typenames.append(name) + ctn.discard(name) + typenames += sorted(ctn) + # + csourcelines = [] + csourcelines.append('# 1 ""') + for typename in typenames: + csourcelines.append('typedef int %s;' % typename) + csourcelines.append('typedef int __dotdotdotint__, __dotdotdotfloat__,' + ' __dotdotdot__;') + # this forces pycparser to consider the following in the file + # called from line 1 + csourcelines.append('# 1 "%s"' % (CDEF_SOURCE_STRING,)) + csourcelines.append(csource) + csourcelines.append('') # see test_missing_newline_bug + fullcsource = '\n'.join(csourcelines) + if lock is not None: + lock.acquire() # pycparser is not thread-safe... + try: + ast = _get_parser().parse(fullcsource) + except pycparser.c_parser.ParseError as e: + self.convert_pycparser_error(e, csource) + finally: + if lock is not None: + lock.release() + # csource will be used to find buggy source text + return ast, macros, csource + + def _convert_pycparser_error(self, e, csource): + # xxx look for ":NUM:" at the start of str(e) + # and interpret that as a line number. This will not work if + # the user gives explicit ``# NUM "FILE"`` directives. + line = None + msg = str(e) + match = re.match(r"%s:(\d+):" % (CDEF_SOURCE_STRING,), msg) + if match: + linenum = int(match.group(1), 10) + csourcelines = csource.splitlines() + if 1 <= linenum <= len(csourcelines): + line = csourcelines[linenum-1] + return line + + def convert_pycparser_error(self, e, csource): + line = self._convert_pycparser_error(e, csource) + + msg = str(e) + if line: + msg = 'cannot parse "%s"\n%s' % (line.strip(), msg) + else: + msg = 'parse error\n%s' % (msg,) + raise CDefError(msg) + + def parse(self, csource, override=False, packed=False, pack=None, + dllexport=False): + if packed: + if packed != True: + raise ValueError("'packed' should be False or True; use " + "'pack' to give another value") + if pack: + raise ValueError("cannot give both 'pack' and 'packed'") + pack = 1 + elif pack: + if pack & (pack - 1): + raise ValueError("'pack' must be a power of two, not %r" % + (pack,)) + else: + pack = 0 + prev_options = self._options + try: + self._options = {'override': override, + 'packed': pack, + 'dllexport': dllexport} + self._internal_parse(csource) + finally: + self._options = prev_options + + def _internal_parse(self, csource): + ast, macros, csource = self._parse(csource) + # add the macros + self._process_macros(macros) + # find the first "__dotdotdot__" and use that as a separator + # between the repeated typedefs and the real csource + iterator = iter(ast.ext) + for decl in iterator: + if decl.name == '__dotdotdot__': + break + else: + assert 0 + current_decl = None + # + try: + self._inside_extern_python = '__cffi_extern_python_stop' + for decl in iterator: + current_decl = decl + if isinstance(decl, pycparser.c_ast.Decl): + self._parse_decl(decl) + elif isinstance(decl, pycparser.c_ast.Typedef): + if not decl.name: + raise CDefError("typedef does not declare any name", + decl) + quals = 0 + if (isinstance(decl.type.type, pycparser.c_ast.IdentifierType) and + decl.type.type.names[-1].startswith('__dotdotdot')): + realtype = self._get_unknown_type(decl) + elif (isinstance(decl.type, pycparser.c_ast.PtrDecl) and + isinstance(decl.type.type, pycparser.c_ast.TypeDecl) and + isinstance(decl.type.type.type, + pycparser.c_ast.IdentifierType) and + decl.type.type.type.names[-1].startswith('__dotdotdot')): + realtype = self._get_unknown_ptr_type(decl) + else: + realtype, quals = self._get_type_and_quals( + decl.type, name=decl.name, partial_length_ok=True, + typedef_example="*(%s *)0" % (decl.name,)) + self._declare('typedef ' + decl.name, realtype, quals=quals) + elif decl.__class__.__name__ == 'Pragma': + # skip pragma, only in pycparser 2.15 + import warnings + warnings.warn( + "#pragma in cdef() are entirely ignored. " + "They should be removed for now, otherwise your " + "code might behave differently in a future version " + "of CFFI if #pragma support gets added. Note that " + "'#pragma pack' needs to be replaced with the " + "'packed' keyword argument to cdef().") + else: + raise CDefError("unexpected <%s>: this construct is valid " + "C but not valid in cdef()" % + decl.__class__.__name__, decl) + except CDefError as e: + if len(e.args) == 1: + e.args = e.args + (current_decl,) + raise + except FFIError as e: + msg = self._convert_pycparser_error(e, csource) + if msg: + e.args = (e.args[0] + "\n *** Err: %s" % msg,) + raise + + def _add_constants(self, key, val): + if key in self._int_constants: + if self._int_constants[key] == val: + return # ignore identical double declarations + raise FFIError( + "multiple declarations of constant: %s" % (key,)) + self._int_constants[key] = val + + def _add_integer_constant(self, name, int_str): + int_str = int_str.lower().rstrip("ul") + neg = int_str.startswith('-') + if neg: + int_str = int_str[1:] + # "010" is not valid oct in py3 + if (int_str.startswith("0") and int_str != '0' + and not int_str.startswith("0x")): + int_str = "0o" + int_str[1:] + pyvalue = int(int_str, 0) + if neg: + pyvalue = -pyvalue + self._add_constants(name, pyvalue) + self._declare('macro ' + name, pyvalue) + + def _process_macros(self, macros): + for key, value in macros.items(): + value = value.strip() + if _r_int_literal.match(value): + self._add_integer_constant(key, value) + elif value == '...': + self._declare('macro ' + key, value) + else: + raise CDefError( + 'only supports one of the following syntax:\n' + ' #define %s ... (literally dot-dot-dot)\n' + ' #define %s NUMBER (with NUMBER an integer' + ' constant, decimal/hex/octal)\n' + 'got:\n' + ' #define %s %s' + % (key, key, key, value)) + + def _declare_function(self, tp, quals, decl): + tp = self._get_type_pointer(tp, quals) + if self._options.get('dllexport'): + tag = 'dllexport_python ' + elif self._inside_extern_python == '__cffi_extern_python_start': + tag = 'extern_python ' + elif self._inside_extern_python == '__cffi_extern_python_plus_c_start': + tag = 'extern_python_plus_c ' + else: + tag = 'function ' + self._declare(tag + decl.name, tp) + + def _parse_decl(self, decl): + node = decl.type + if isinstance(node, pycparser.c_ast.FuncDecl): + tp, quals = self._get_type_and_quals(node, name=decl.name) + assert isinstance(tp, model.RawFunctionType) + self._declare_function(tp, quals, decl) + else: + if isinstance(node, pycparser.c_ast.Struct): + self._get_struct_union_enum_type('struct', node) + elif isinstance(node, pycparser.c_ast.Union): + self._get_struct_union_enum_type('union', node) + elif isinstance(node, pycparser.c_ast.Enum): + self._get_struct_union_enum_type('enum', node) + elif not decl.name: + raise CDefError("construct does not declare any variable", + decl) + # + if decl.name: + tp, quals = self._get_type_and_quals(node, + partial_length_ok=True) + if tp.is_raw_function: + self._declare_function(tp, quals, decl) + elif (tp.is_integer_type() and + hasattr(decl, 'init') and + hasattr(decl.init, 'value') and + _r_int_literal.match(decl.init.value)): + self._add_integer_constant(decl.name, decl.init.value) + elif (tp.is_integer_type() and + isinstance(decl.init, pycparser.c_ast.UnaryOp) and + decl.init.op == '-' and + hasattr(decl.init.expr, 'value') and + _r_int_literal.match(decl.init.expr.value)): + self._add_integer_constant(decl.name, + '-' + decl.init.expr.value) + elif (tp is model.void_type and + decl.name.startswith('__cffi_extern_python_')): + # hack: `extern "Python"` in the C source is replaced + # with "void __cffi_extern_python_start;" and + # "void __cffi_extern_python_stop;" + self._inside_extern_python = decl.name + else: + if self._inside_extern_python !='__cffi_extern_python_stop': + raise CDefError( + "cannot declare constants or " + "variables with 'extern \"Python\"'") + if (quals & model.Q_CONST) and not tp.is_array_type: + self._declare('constant ' + decl.name, tp, quals=quals) + else: + _warn_for_non_extern_non_static_global_variable(decl) + self._declare('variable ' + decl.name, tp, quals=quals) + + def parse_type(self, cdecl): + return self.parse_type_and_quals(cdecl)[0] + + def parse_type_and_quals(self, cdecl): + ast, macros = self._parse('void __dummy(\n%s\n);' % cdecl)[:2] + assert not macros + exprnode = ast.ext[-1].type.args.params[0] + if isinstance(exprnode, pycparser.c_ast.ID): + raise CDefError("unknown identifier '%s'" % (exprnode.name,)) + return self._get_type_and_quals(exprnode.type) + + def _declare(self, name, obj, included=False, quals=0): + if name in self._declarations: + prevobj, prevquals = self._declarations[name] + if prevobj is obj and prevquals == quals: + return + if not self._options.get('override'): + raise FFIError( + "multiple declarations of %s (for interactive usage, " + "try cdef(xx, override=True))" % (name,)) + assert '__dotdotdot__' not in name.split() + self._declarations[name] = (obj, quals) + if included: + self._included_declarations.add(obj) + + def _extract_quals(self, type): + quals = 0 + if isinstance(type, (pycparser.c_ast.TypeDecl, + pycparser.c_ast.PtrDecl)): + if 'const' in type.quals: + quals |= model.Q_CONST + if 'volatile' in type.quals: + quals |= model.Q_VOLATILE + if 'restrict' in type.quals: + quals |= model.Q_RESTRICT + return quals + + def _get_type_pointer(self, type, quals, declname=None): + if isinstance(type, model.RawFunctionType): + return type.as_function_pointer() + if (isinstance(type, model.StructOrUnionOrEnum) and + type.name.startswith('$') and type.name[1:].isdigit() and + type.forcename is None and declname is not None): + return model.NamedPointerType(type, declname, quals) + return model.PointerType(type, quals) + + def _get_type_and_quals(self, typenode, name=None, partial_length_ok=False, + typedef_example=None): + # first, dereference typedefs, if we have it already parsed, we're good + if (isinstance(typenode, pycparser.c_ast.TypeDecl) and + isinstance(typenode.type, pycparser.c_ast.IdentifierType) and + len(typenode.type.names) == 1 and + ('typedef ' + typenode.type.names[0]) in self._declarations): + tp, quals = self._declarations['typedef ' + typenode.type.names[0]] + quals |= self._extract_quals(typenode) + return tp, quals + # + if isinstance(typenode, pycparser.c_ast.ArrayDecl): + # array type + if typenode.dim is None: + length = None + else: + length = self._parse_constant( + typenode.dim, partial_length_ok=partial_length_ok) + # a hack: in 'typedef int foo_t[...][...];', don't use '...' as + # the length but use directly the C expression that would be + # generated by recompiler.py. This lets the typedef be used in + # many more places within recompiler.py + if typedef_example is not None: + if length == '...': + length = '_cffi_array_len(%s)' % (typedef_example,) + typedef_example = "*" + typedef_example + # + tp, quals = self._get_type_and_quals(typenode.type, + partial_length_ok=partial_length_ok, + typedef_example=typedef_example) + return model.ArrayType(tp, length), quals + # + if isinstance(typenode, pycparser.c_ast.PtrDecl): + # pointer type + itemtype, itemquals = self._get_type_and_quals(typenode.type) + tp = self._get_type_pointer(itemtype, itemquals, declname=name) + quals = self._extract_quals(typenode) + return tp, quals + # + if isinstance(typenode, pycparser.c_ast.TypeDecl): + quals = self._extract_quals(typenode) + type = typenode.type + if isinstance(type, pycparser.c_ast.IdentifierType): + # assume a primitive type. get it from .names, but reduce + # synonyms to a single chosen combination + names = list(type.names) + if names != ['signed', 'char']: # keep this unmodified + prefixes = {} + while names: + name = names[0] + if name in ('short', 'long', 'signed', 'unsigned'): + prefixes[name] = prefixes.get(name, 0) + 1 + del names[0] + else: + break + # ignore the 'signed' prefix below, and reorder the others + newnames = [] + for prefix in ('unsigned', 'short', 'long'): + for i in range(prefixes.get(prefix, 0)): + newnames.append(prefix) + if not names: + names = ['int'] # implicitly + if names == ['int']: # but kill it if 'short' or 'long' + if 'short' in prefixes or 'long' in prefixes: + names = [] + names = newnames + names + ident = ' '.join(names) + if ident == 'void': + return model.void_type, quals + if ident == '__dotdotdot__': + raise FFIError(':%d: bad usage of "..."' % + typenode.coord.line) + tp0, quals0 = resolve_common_type(self, ident) + return tp0, (quals | quals0) + # + if isinstance(type, pycparser.c_ast.Struct): + # 'struct foobar' + tp = self._get_struct_union_enum_type('struct', type, name) + return tp, quals + # + if isinstance(type, pycparser.c_ast.Union): + # 'union foobar' + tp = self._get_struct_union_enum_type('union', type, name) + return tp, quals + # + if isinstance(type, pycparser.c_ast.Enum): + # 'enum foobar' + tp = self._get_struct_union_enum_type('enum', type, name) + return tp, quals + # + if isinstance(typenode, pycparser.c_ast.FuncDecl): + # a function type + return self._parse_function_type(typenode, name), 0 + # + # nested anonymous structs or unions end up here + if isinstance(typenode, pycparser.c_ast.Struct): + return self._get_struct_union_enum_type('struct', typenode, name, + nested=True), 0 + if isinstance(typenode, pycparser.c_ast.Union): + return self._get_struct_union_enum_type('union', typenode, name, + nested=True), 0 + # + raise FFIError(":%d: bad or unsupported type declaration" % + typenode.coord.line) + + def _parse_function_type(self, typenode, funcname=None): + params = list(getattr(typenode.args, 'params', [])) + for i, arg in enumerate(params): + if not hasattr(arg, 'type'): + raise CDefError("%s arg %d: unknown type '%s'" + " (if you meant to use the old C syntax of giving" + " untyped arguments, it is not supported)" + % (funcname or 'in expression', i + 1, + getattr(arg, 'name', '?'))) + ellipsis = ( + len(params) > 0 and + isinstance(params[-1].type, pycparser.c_ast.TypeDecl) and + isinstance(params[-1].type.type, + pycparser.c_ast.IdentifierType) and + params[-1].type.type.names == ['__dotdotdot__']) + if ellipsis: + params.pop() + if not params: + raise CDefError( + "%s: a function with only '(...)' as argument" + " is not correct C" % (funcname or 'in expression')) + args = [self._as_func_arg(*self._get_type_and_quals(argdeclnode.type)) + for argdeclnode in params] + if not ellipsis and args == [model.void_type]: + args = [] + result, quals = self._get_type_and_quals(typenode.type) + # the 'quals' on the result type are ignored. HACK: we absure them + # to detect __stdcall functions: we textually replace "__stdcall" + # with "volatile volatile const" above. + abi = None + if hasattr(typenode.type, 'quals'): # else, probable syntax error anyway + if typenode.type.quals[-3:] == ['volatile', 'volatile', 'const']: + abi = '__stdcall' + return model.RawFunctionType(tuple(args), result, ellipsis, abi) + + def _as_func_arg(self, type, quals): + if isinstance(type, model.ArrayType): + return model.PointerType(type.item, quals) + elif isinstance(type, model.RawFunctionType): + return type.as_function_pointer() + else: + return type + + def _get_struct_union_enum_type(self, kind, type, name=None, nested=False): + # First, a level of caching on the exact 'type' node of the AST. + # This is obscure, but needed because pycparser "unrolls" declarations + # such as "typedef struct { } foo_t, *foo_p" and we end up with + # an AST that is not a tree, but a DAG, with the "type" node of the + # two branches foo_t and foo_p of the trees being the same node. + # It's a bit silly but detecting "DAG-ness" in the AST tree seems + # to be the only way to distinguish this case from two independent + # structs. See test_struct_with_two_usages. + try: + return self._structnode2type[type] + except KeyError: + pass + # + # Note that this must handle parsing "struct foo" any number of + # times and always return the same StructType object. Additionally, + # one of these times (not necessarily the first), the fields of + # the struct can be specified with "struct foo { ...fields... }". + # If no name is given, then we have to create a new anonymous struct + # with no caching; in this case, the fields are either specified + # right now or never. + # + force_name = name + name = type.name + # + # get the type or create it if needed + if name is None: + # 'force_name' is used to guess a more readable name for + # anonymous structs, for the common case "typedef struct { } foo". + if force_name is not None: + explicit_name = '$%s' % force_name + else: + self._anonymous_counter += 1 + explicit_name = '$%d' % self._anonymous_counter + tp = None + else: + explicit_name = name + key = '%s %s' % (kind, name) + tp, _ = self._declarations.get(key, (None, None)) + # + if tp is None: + if kind == 'struct': + tp = model.StructType(explicit_name, None, None, None) + elif kind == 'union': + tp = model.UnionType(explicit_name, None, None, None) + elif kind == 'enum': + if explicit_name == '__dotdotdot__': + raise CDefError("Enums cannot be declared with ...") + tp = self._build_enum_type(explicit_name, type.values) + else: + raise AssertionError("kind = %r" % (kind,)) + if name is not None: + self._declare(key, tp) + elif kind == 'enum' and type.values is not None: + raise NotImplementedError( + "enum %s: the '{}' declaration should appear on the first " + "time the enum is mentioned, not later" % explicit_name) + if not tp.forcename: + tp.force_the_name(force_name) + if tp.forcename and '$' in tp.name: + self._declare('anonymous %s' % tp.forcename, tp) + # + self._structnode2type[type] = tp + # + # enums: done here + if kind == 'enum': + return tp + # + # is there a 'type.decls'? If yes, then this is the place in the + # C sources that declare the fields. If no, then just return the + # existing type, possibly still incomplete. + if type.decls is None: + return tp + # + if tp.fldnames is not None: + raise CDefError("duplicate declaration of struct %s" % name) + fldnames = [] + fldtypes = [] + fldbitsize = [] + fldquals = [] + for decl in type.decls: + if (isinstance(decl.type, pycparser.c_ast.IdentifierType) and + ''.join(decl.type.names) == '__dotdotdot__'): + # XXX pycparser is inconsistent: 'names' should be a list + # of strings, but is sometimes just one string. Use + # str.join() as a way to cope with both. + self._make_partial(tp, nested) + continue + if decl.bitsize is None: + bitsize = -1 + else: + bitsize = self._parse_constant(decl.bitsize) + self._partial_length = False + type, fqual = self._get_type_and_quals(decl.type, + partial_length_ok=True) + if self._partial_length: + self._make_partial(tp, nested) + if isinstance(type, model.StructType) and type.partial: + self._make_partial(tp, nested) + fldnames.append(decl.name or '') + fldtypes.append(type) + fldbitsize.append(bitsize) + fldquals.append(fqual) + tp.fldnames = tuple(fldnames) + tp.fldtypes = tuple(fldtypes) + tp.fldbitsize = tuple(fldbitsize) + tp.fldquals = tuple(fldquals) + if fldbitsize != [-1] * len(fldbitsize): + if isinstance(tp, model.StructType) and tp.partial: + raise NotImplementedError("%s: using both bitfields and '...;'" + % (tp,)) + tp.packed = self._options.get('packed') + if tp.completed: # must be re-completed: it is not opaque any more + tp.completed = 0 + self._recomplete.append(tp) + return tp + + def _make_partial(self, tp, nested): + if not isinstance(tp, model.StructOrUnion): + raise CDefError("%s cannot be partial" % (tp,)) + if not tp.has_c_name() and not nested: + raise NotImplementedError("%s is partial but has no C name" %(tp,)) + tp.partial = True + + def _parse_constant(self, exprnode, partial_length_ok=False): + # for now, limited to expressions that are an immediate number + # or positive/negative number + if isinstance(exprnode, pycparser.c_ast.Constant): + s = exprnode.value + if '0' <= s[0] <= '9': + s = s.rstrip('uUlL') + try: + if s.startswith('0'): + return int(s, 8) + else: + return int(s, 10) + except ValueError: + if len(s) > 1: + if s.lower()[0:2] == '0x': + return int(s, 16) + elif s.lower()[0:2] == '0b': + return int(s, 2) + raise CDefError("invalid constant %r" % (s,)) + elif s[0] == "'" and s[-1] == "'" and ( + len(s) == 3 or (len(s) == 4 and s[1] == "\\")): + return ord(s[-2]) + else: + raise CDefError("invalid constant %r" % (s,)) + # + if (isinstance(exprnode, pycparser.c_ast.UnaryOp) and + exprnode.op == '+'): + return self._parse_constant(exprnode.expr) + # + if (isinstance(exprnode, pycparser.c_ast.UnaryOp) and + exprnode.op == '-'): + return -self._parse_constant(exprnode.expr) + # load previously defined int constant + if (isinstance(exprnode, pycparser.c_ast.ID) and + exprnode.name in self._int_constants): + return self._int_constants[exprnode.name] + # + if (isinstance(exprnode, pycparser.c_ast.ID) and + exprnode.name == '__dotdotdotarray__'): + if partial_length_ok: + self._partial_length = True + return '...' + raise FFIError(":%d: unsupported '[...]' here, cannot derive " + "the actual array length in this context" + % exprnode.coord.line) + # + if isinstance(exprnode, pycparser.c_ast.BinaryOp): + left = self._parse_constant(exprnode.left) + right = self._parse_constant(exprnode.right) + if exprnode.op == '+': + return left + right + elif exprnode.op == '-': + return left - right + elif exprnode.op == '*': + return left * right + elif exprnode.op == '/': + return self._c_div(left, right) + elif exprnode.op == '%': + return left - self._c_div(left, right) * right + elif exprnode.op == '<<': + return left << right + elif exprnode.op == '>>': + return left >> right + elif exprnode.op == '&': + return left & right + elif exprnode.op == '|': + return left | right + elif exprnode.op == '^': + return left ^ right + # + raise FFIError(":%d: unsupported expression: expected a " + "simple numeric constant" % exprnode.coord.line) + + def _c_div(self, a, b): + result = a // b + if ((a < 0) ^ (b < 0)) and (a % b) != 0: + result += 1 + return result + + def _build_enum_type(self, explicit_name, decls): + if decls is not None: + partial = False + enumerators = [] + enumvalues = [] + nextenumvalue = 0 + for enum in decls.enumerators: + if _r_enum_dotdotdot.match(enum.name): + partial = True + continue + if enum.value is not None: + nextenumvalue = self._parse_constant(enum.value) + enumerators.append(enum.name) + enumvalues.append(nextenumvalue) + self._add_constants(enum.name, nextenumvalue) + nextenumvalue += 1 + enumerators = tuple(enumerators) + enumvalues = tuple(enumvalues) + tp = model.EnumType(explicit_name, enumerators, enumvalues) + tp.partial = partial + else: # opaque enum + tp = model.EnumType(explicit_name, (), ()) + return tp + + def include(self, other): + for name, (tp, quals) in other._declarations.items(): + if name.startswith('anonymous $enum_$'): + continue # fix for test_anonymous_enum_include + kind = name.split(' ', 1)[0] + if kind in ('struct', 'union', 'enum', 'anonymous', 'typedef'): + self._declare(name, tp, included=True, quals=quals) + for k, v in other._int_constants.items(): + self._add_constants(k, v) + + def _get_unknown_type(self, decl): + typenames = decl.type.type.names + if typenames == ['__dotdotdot__']: + return model.unknown_type(decl.name) + + if typenames == ['__dotdotdotint__']: + if self._uses_new_feature is None: + self._uses_new_feature = "'typedef int... %s'" % decl.name + return model.UnknownIntegerType(decl.name) + + if typenames == ['__dotdotdotfloat__']: + # note: not for 'long double' so far + if self._uses_new_feature is None: + self._uses_new_feature = "'typedef float... %s'" % decl.name + return model.UnknownFloatType(decl.name) + + raise FFIError(':%d: unsupported usage of "..." in typedef' + % decl.coord.line) + + def _get_unknown_ptr_type(self, decl): + if decl.type.type.type.names == ['__dotdotdot__']: + return model.unknown_ptr_type(decl.name) + raise FFIError(':%d: unsupported usage of "..." in typedef' + % decl.coord.line) diff --git a/source/target-vendors/cp312/cffi/error.py b/source/target-vendors/cp312/cffi/error.py new file mode 100644 index 0000000..0a27247 --- /dev/null +++ b/source/target-vendors/cp312/cffi/error.py @@ -0,0 +1,31 @@ + +class FFIError(Exception): + __module__ = 'cffi' + +class CDefError(Exception): + __module__ = 'cffi' + def __str__(self): + try: + current_decl = self.args[1] + filename = current_decl.coord.file + linenum = current_decl.coord.line + prefix = '%s:%d: ' % (filename, linenum) + except (AttributeError, TypeError, IndexError): + prefix = '' + return '%s%s' % (prefix, self.args[0]) + +class VerificationError(Exception): + """ An error raised when verification fails + """ + __module__ = 'cffi' + +class VerificationMissing(Exception): + """ An error raised when incomplete structures are passed into + cdef, but no verification has been done + """ + __module__ = 'cffi' + +class PkgConfigError(Exception): + """ An error raised for missing modules in pkg-config + """ + __module__ = 'cffi' diff --git a/source/target-vendors/cp312/cffi/ffiplatform.py b/source/target-vendors/cp312/cffi/ffiplatform.py new file mode 100644 index 0000000..adca28f --- /dev/null +++ b/source/target-vendors/cp312/cffi/ffiplatform.py @@ -0,0 +1,113 @@ +import sys, os +from .error import VerificationError + + +LIST_OF_FILE_NAMES = ['sources', 'include_dirs', 'library_dirs', + 'extra_objects', 'depends'] + +def get_extension(srcfilename, modname, sources=(), **kwds): + from cffi._shimmed_dist_utils import Extension + allsources = [srcfilename] + for src in sources: + allsources.append(os.path.normpath(src)) + return Extension(name=modname, sources=allsources, **kwds) + +def compile(tmpdir, ext, compiler_verbose=0, debug=None): + """Compile a C extension module using distutils.""" + + saved_environ = os.environ.copy() + try: + outputfilename = _build(tmpdir, ext, compiler_verbose, debug) + outputfilename = os.path.abspath(outputfilename) + finally: + # workaround for a distutils bugs where some env vars can + # become longer and longer every time it is used + for key, value in saved_environ.items(): + if os.environ.get(key) != value: + os.environ[key] = value + return outputfilename + +def _build(tmpdir, ext, compiler_verbose=0, debug=None): + # XXX compact but horrible :-( + from cffi._shimmed_dist_utils import Distribution, CompileError, LinkError, set_threshold, set_verbosity + + dist = Distribution({'ext_modules': [ext]}) + dist.parse_config_files() + options = dist.get_option_dict('build_ext') + if debug is None: + debug = sys.flags.debug + options['debug'] = ('ffiplatform', debug) + options['force'] = ('ffiplatform', True) + options['build_lib'] = ('ffiplatform', tmpdir) + options['build_temp'] = ('ffiplatform', tmpdir) + # + try: + old_level = set_threshold(0) or 0 + try: + set_verbosity(compiler_verbose) + dist.run_command('build_ext') + cmd_obj = dist.get_command_obj('build_ext') + [soname] = cmd_obj.get_outputs() + finally: + set_threshold(old_level) + except (CompileError, LinkError) as e: + raise VerificationError('%s: %s' % (e.__class__.__name__, e)) + # + return soname + +try: + from os.path import samefile +except ImportError: + def samefile(f1, f2): + return os.path.abspath(f1) == os.path.abspath(f2) + +def maybe_relative_path(path): + if not os.path.isabs(path): + return path # already relative + dir = path + names = [] + while True: + prevdir = dir + dir, name = os.path.split(prevdir) + if dir == prevdir or not dir: + return path # failed to make it relative + names.append(name) + try: + if samefile(dir, os.curdir): + names.reverse() + return os.path.join(*names) + except OSError: + pass + +# ____________________________________________________________ + +try: + int_or_long = (int, long) + import cStringIO +except NameError: + int_or_long = int # Python 3 + import io as cStringIO + +def _flatten(x, f): + if isinstance(x, str): + f.write('%ds%s' % (len(x), x)) + elif isinstance(x, dict): + keys = sorted(x.keys()) + f.write('%dd' % len(keys)) + for key in keys: + _flatten(key, f) + _flatten(x[key], f) + elif isinstance(x, (list, tuple)): + f.write('%dl' % len(x)) + for value in x: + _flatten(value, f) + elif isinstance(x, int_or_long): + f.write('%di' % (x,)) + else: + raise TypeError( + "the keywords to verify() contains unsupported object %r" % (x,)) + +def flatten(x): + f = cStringIO.StringIO() + _flatten(x, f) + return f.getvalue() diff --git a/source/target-vendors/cp312/cffi/gen_src.py b/source/target-vendors/cp312/cffi/gen_src.py new file mode 100644 index 0000000..8eb57b1 --- /dev/null +++ b/source/target-vendors/cp312/cffi/gen_src.py @@ -0,0 +1,5 @@ +"""Entry point so ``python -m cffi.gen_src`` works.""" + +if __name__ == '__main__': + from cffi._cffi_gen_src import run + run() diff --git a/source/target-vendors/cp312/cffi/lock.py b/source/target-vendors/cp312/cffi/lock.py new file mode 100644 index 0000000..db91b71 --- /dev/null +++ b/source/target-vendors/cp312/cffi/lock.py @@ -0,0 +1,30 @@ +import sys + +if sys.version_info < (3,): + try: + from thread import allocate_lock + except ImportError: + from dummy_thread import allocate_lock +else: + try: + from _thread import allocate_lock + except ImportError: + from _dummy_thread import allocate_lock + + +##import sys +##l1 = allocate_lock + +##class allocate_lock(object): +## def __init__(self): +## self._real = l1() +## def __enter__(self): +## for i in range(4, 0, -1): +## print sys._getframe(i).f_code +## print +## return self._real.__enter__() +## def __exit__(self, *args): +## return self._real.__exit__(*args) +## def acquire(self, f): +## assert f is False +## return self._real.acquire(f) diff --git a/source/target-vendors/cp312/cffi/model.py b/source/target-vendors/cp312/cffi/model.py new file mode 100644 index 0000000..8bade1d --- /dev/null +++ b/source/target-vendors/cp312/cffi/model.py @@ -0,0 +1,617 @@ +import types +import weakref + +from .lock import allocate_lock +from .error import CDefError, VerificationError, VerificationMissing + +# type qualifiers +Q_CONST = 0x01 +Q_RESTRICT = 0x02 +Q_VOLATILE = 0x04 + +def qualify(quals, replace_with): + if quals & Q_CONST: + replace_with = ' const ' + replace_with.lstrip() + if quals & Q_VOLATILE: + replace_with = ' volatile ' + replace_with.lstrip() + if quals & Q_RESTRICT: + # It seems that __restrict is supported by gcc and msvc. + # If you hit some different compiler, add a #define in + # _cffi_include.h for it (and in its copies, documented there) + replace_with = ' __restrict ' + replace_with.lstrip() + return replace_with + + +class BaseTypeByIdentity: + is_array_type = False + is_raw_function = False + + def get_c_name(self, replace_with='', context='a C file', quals=0): + result = self.c_name_with_marker + assert result.count('&') == 1 + # some logic duplication with ffi.getctype()... :-( + replace_with = replace_with.strip() + if replace_with: + if replace_with.startswith('*') and '&[' in result: + replace_with = '(%s)' % replace_with + elif replace_with[0] not in '[(': + replace_with = ' ' + replace_with + replace_with = qualify(quals, replace_with) + result = result.replace('&', replace_with) + if '$' in result: + raise VerificationError( + "cannot generate '%s' in %s: unknown type name" + % (self._get_c_name(), context)) + return result + + def _get_c_name(self): + return self.c_name_with_marker.replace('&', '') + + def has_c_name(self): + return '$' not in self._get_c_name() + + def is_integer_type(self): + return False + + def get_cached_btype(self, ffi, finishlist, can_delay=False): + try: + BType = ffi._cached_btypes[self] + except KeyError: + BType = self.build_backend_type(ffi, finishlist) + BType2 = ffi._cached_btypes.setdefault(self, BType) + assert BType2 is BType + return BType + + def __repr__(self): + return '<%s>' % (self._get_c_name(),) + + def _get_items(self): + return [(name, getattr(self, name)) for name in self._attrs_] + + +class BaseType(BaseTypeByIdentity): + + def __eq__(self, other): + return (self.__class__ == other.__class__ and + self._get_items() == other._get_items()) + + def __ne__(self, other): + return not self == other + + def __hash__(self): + return hash((self.__class__, tuple(self._get_items()))) + + +class VoidType(BaseType): + _attrs_ = () + + def __init__(self): + self.c_name_with_marker = 'void&' + + def build_backend_type(self, ffi, finishlist): + return global_cache(self, ffi, 'new_void_type') + +void_type = VoidType() + + +class BasePrimitiveType(BaseType): + def is_complex_type(self): + return False + + +class PrimitiveType(BasePrimitiveType): + _attrs_ = ('name',) + + ALL_PRIMITIVE_TYPES = { + 'char': 'c', + 'short': 'i', + 'int': 'i', + 'long': 'i', + 'long long': 'i', + 'signed char': 'i', + 'unsigned char': 'i', + 'unsigned short': 'i', + 'unsigned int': 'i', + 'unsigned long': 'i', + 'unsigned long long': 'i', + 'float': 'f', + 'double': 'f', + 'long double': 'f', + '_cffi_float_complex_t': 'j', + '_cffi_double_complex_t': 'j', + '_Bool': 'i', + # the following types are not primitive in the C sense + 'wchar_t': 'c', + 'char16_t': 'c', + 'char32_t': 'c', + 'int8_t': 'i', + 'uint8_t': 'i', + 'int16_t': 'i', + 'uint16_t': 'i', + 'int32_t': 'i', + 'uint32_t': 'i', + 'int64_t': 'i', + 'uint64_t': 'i', + 'int_least8_t': 'i', + 'uint_least8_t': 'i', + 'int_least16_t': 'i', + 'uint_least16_t': 'i', + 'int_least32_t': 'i', + 'uint_least32_t': 'i', + 'int_least64_t': 'i', + 'uint_least64_t': 'i', + 'int_fast8_t': 'i', + 'uint_fast8_t': 'i', + 'int_fast16_t': 'i', + 'uint_fast16_t': 'i', + 'int_fast32_t': 'i', + 'uint_fast32_t': 'i', + 'int_fast64_t': 'i', + 'uint_fast64_t': 'i', + 'intptr_t': 'i', + 'uintptr_t': 'i', + 'intmax_t': 'i', + 'uintmax_t': 'i', + 'ptrdiff_t': 'i', + 'size_t': 'i', + 'ssize_t': 'i', + } + + def __init__(self, name): + assert name in self.ALL_PRIMITIVE_TYPES + self.name = name + self.c_name_with_marker = name + '&' + + def is_char_type(self): + return self.ALL_PRIMITIVE_TYPES[self.name] == 'c' + def is_integer_type(self): + return self.ALL_PRIMITIVE_TYPES[self.name] == 'i' + def is_float_type(self): + return self.ALL_PRIMITIVE_TYPES[self.name] == 'f' + def is_complex_type(self): + return self.ALL_PRIMITIVE_TYPES[self.name] == 'j' + + def build_backend_type(self, ffi, finishlist): + return global_cache(self, ffi, 'new_primitive_type', self.name) + + +class UnknownIntegerType(BasePrimitiveType): + _attrs_ = ('name',) + + def __init__(self, name): + self.name = name + self.c_name_with_marker = name + '&' + + def is_integer_type(self): + return True + + def build_backend_type(self, ffi, finishlist): + raise NotImplementedError("integer type '%s' can only be used after " + "compilation" % self.name) + +class UnknownFloatType(BasePrimitiveType): + _attrs_ = ('name', ) + + def __init__(self, name): + self.name = name + self.c_name_with_marker = name + '&' + + def build_backend_type(self, ffi, finishlist): + raise NotImplementedError("float type '%s' can only be used after " + "compilation" % self.name) + + +class BaseFunctionType(BaseType): + _attrs_ = ('args', 'result', 'ellipsis', 'abi') + + def __init__(self, args, result, ellipsis, abi=None): + self.args = args + self.result = result + self.ellipsis = ellipsis + self.abi = abi + # + reprargs = [arg._get_c_name() for arg in self.args] + if self.ellipsis: + reprargs.append('...') + reprargs = reprargs or ['void'] + replace_with = self._base_pattern % (', '.join(reprargs),) + if abi is not None: + replace_with = replace_with[:1] + abi + ' ' + replace_with[1:] + self.c_name_with_marker = ( + self.result.c_name_with_marker.replace('&', replace_with)) + + +class RawFunctionType(BaseFunctionType): + # Corresponds to a C type like 'int(int)', which is the C type of + # a function, but not a pointer-to-function. The backend has no + # notion of such a type; it's used temporarily by parsing. + _base_pattern = '(&)(%s)' + is_raw_function = True + + def build_backend_type(self, ffi, finishlist): + raise CDefError("cannot render the type %r: it is a function " + "type, not a pointer-to-function type" % (self,)) + + def as_function_pointer(self): + return FunctionPtrType(self.args, self.result, self.ellipsis, self.abi) + + +class FunctionPtrType(BaseFunctionType): + _base_pattern = '(*&)(%s)' + + def build_backend_type(self, ffi, finishlist): + result = self.result.get_cached_btype(ffi, finishlist) + args = [] + for tp in self.args: + args.append(tp.get_cached_btype(ffi, finishlist)) + abi_args = () + if self.abi == "__stdcall": + if not self.ellipsis: # __stdcall ignored for variadic funcs + try: + abi_args = (ffi._backend.FFI_STDCALL,) + except AttributeError: + pass + return global_cache(self, ffi, 'new_function_type', + tuple(args), result, self.ellipsis, *abi_args) + + def as_raw_function(self): + return RawFunctionType(self.args, self.result, self.ellipsis, self.abi) + + +class PointerType(BaseType): + _attrs_ = ('totype', 'quals') + + def __init__(self, totype, quals=0): + self.totype = totype + self.quals = quals + extra = " *&" + if totype.is_array_type: + extra = "(%s)" % (extra.lstrip(),) + extra = qualify(quals, extra) + self.c_name_with_marker = totype.c_name_with_marker.replace('&', extra) + + def build_backend_type(self, ffi, finishlist): + BItem = self.totype.get_cached_btype(ffi, finishlist, can_delay=True) + return global_cache(self, ffi, 'new_pointer_type', BItem) + +voidp_type = PointerType(void_type) + +def ConstPointerType(totype): + return PointerType(totype, Q_CONST) + +const_voidp_type = ConstPointerType(void_type) + + +class NamedPointerType(PointerType): + _attrs_ = ('totype', 'name') + + def __init__(self, totype, name, quals=0): + PointerType.__init__(self, totype, quals) + self.name = name + self.c_name_with_marker = name + '&' + + +class ArrayType(BaseType): + _attrs_ = ('item', 'length') + is_array_type = True + + def __init__(self, item, length): + self.item = item + self.length = length + # + if length is None: + brackets = '&[]' + elif length == '...': + brackets = '&[/*...*/]' + else: + brackets = '&[%s]' % length + self.c_name_with_marker = ( + self.item.c_name_with_marker.replace('&', brackets)) + + def length_is_unknown(self): + return isinstance(self.length, str) + + def resolve_length(self, newlength): + return ArrayType(self.item, newlength) + + def build_backend_type(self, ffi, finishlist): + if self.length_is_unknown(): + raise CDefError("cannot render the type %r: unknown length" % + (self,)) + self.item.get_cached_btype(ffi, finishlist) # force the item BType + BPtrItem = PointerType(self.item).get_cached_btype(ffi, finishlist) + return global_cache(self, ffi, 'new_array_type', BPtrItem, self.length) + +char_array_type = ArrayType(PrimitiveType('char'), None) + + +class StructOrUnionOrEnum(BaseTypeByIdentity): + _attrs_ = ('name',) + forcename = None + + def build_c_name_with_marker(self): + name = self.forcename or '%s %s' % (self.kind, self.name) + self.c_name_with_marker = name + '&' + + def force_the_name(self, forcename): + self.forcename = forcename + self.build_c_name_with_marker() + + def get_official_name(self): + assert self.c_name_with_marker.endswith('&') + return self.c_name_with_marker[:-1] + + +class StructOrUnion(StructOrUnionOrEnum): + fixedlayout = None + completed = 0 + partial = False + packed = 0 + + def __init__(self, name, fldnames, fldtypes, fldbitsize, fldquals=None): + self.name = name + self.fldnames = fldnames + self.fldtypes = fldtypes + self.fldbitsize = fldbitsize + self.fldquals = fldquals + self.build_c_name_with_marker() + + def anonymous_struct_fields(self): + if self.fldtypes is not None: + for name, type in zip(self.fldnames, self.fldtypes): + if name == '' and isinstance(type, StructOrUnion): + yield type + + def enumfields(self, expand_anonymous_struct_union=True): + fldquals = self.fldquals + if fldquals is None: + fldquals = (0,) * len(self.fldnames) + for name, type, bitsize, quals in zip(self.fldnames, self.fldtypes, + self.fldbitsize, fldquals): + if (name == '' and isinstance(type, StructOrUnion) + and expand_anonymous_struct_union): + # nested anonymous struct/union + yield from type.enumfields() + else: + yield (name, type, bitsize, quals) + + def force_flatten(self): + # force the struct or union to have a declaration that lists + # directly all fields returned by enumfields(), flattening + # nested anonymous structs/unions. + names = [] + types = [] + bitsizes = [] + fldquals = [] + for name, type, bitsize, quals in self.enumfields(): + names.append(name) + types.append(type) + bitsizes.append(bitsize) + fldquals.append(quals) + self.fldnames = tuple(names) + self.fldtypes = tuple(types) + self.fldbitsize = tuple(bitsizes) + self.fldquals = tuple(fldquals) + + def get_cached_btype(self, ffi, finishlist, can_delay=False): + BType = StructOrUnionOrEnum.get_cached_btype(self, ffi, finishlist, + can_delay) + if not can_delay: + self.finish_backend_type(ffi, finishlist) + return BType + + def finish_backend_type(self, ffi, finishlist): + if self.completed: + if self.completed != 2: + raise NotImplementedError("recursive structure declaration " + "for '%s'" % (self.name,)) + return + BType = ffi._cached_btypes[self] + # + self.completed = 1 + # + if self.fldtypes is None: + pass # not completing it: it's an opaque struct + # + elif self.fixedlayout is None: + fldtypes = [tp.get_cached_btype(ffi, finishlist) + for tp in self.fldtypes] + lst = list(zip(self.fldnames, fldtypes, self.fldbitsize)) + extra_flags = () + if self.packed: + if self.packed == 1: + extra_flags = (8,) # SF_PACKED + else: + extra_flags = (0, self.packed) + ffi._backend.complete_struct_or_union(BType, lst, self, + -1, -1, *extra_flags) + # + else: + fldtypes = [] + fieldofs, fieldsize, totalsize, totalalignment = self.fixedlayout + for i in range(len(self.fldnames)): + fsize = fieldsize[i] + ftype = self.fldtypes[i] + # + if isinstance(ftype, ArrayType) and ftype.length_is_unknown(): + # fix the length to match the total size + BItemType = ftype.item.get_cached_btype(ffi, finishlist) + nlen, nrest = divmod(fsize, ffi.sizeof(BItemType)) + if nrest != 0: + self._verification_error( + "field '%s.%s' has a bogus size?" % ( + self.name, self.fldnames[i] or '{}')) + ftype = ftype.resolve_length(nlen) + self.fldtypes = (self.fldtypes[:i] + (ftype,) + + self.fldtypes[i+1:]) + # + BFieldType = ftype.get_cached_btype(ffi, finishlist) + if isinstance(ftype, ArrayType) and ftype.length is None: + assert fsize == 0 + else: + bitemsize = ffi.sizeof(BFieldType) + if bitemsize != fsize: + self._verification_error( + "field '%s.%s' is declared as %d bytes, but is " + "really %d bytes" % (self.name, + self.fldnames[i] or '{}', + bitemsize, fsize)) + fldtypes.append(BFieldType) + # + lst = list(zip(self.fldnames, fldtypes, self.fldbitsize, fieldofs)) + ffi._backend.complete_struct_or_union(BType, lst, self, + totalsize, totalalignment) + self.completed = 2 + + def _verification_error(self, msg): + raise VerificationError(msg) + + def check_not_partial(self): + if self.partial and self.fixedlayout is None: + raise VerificationMissing(self._get_c_name()) + + def build_backend_type(self, ffi, finishlist): + self.check_not_partial() + finishlist.append(self) + # + return global_cache(self, ffi, 'new_%s_type' % self.kind, + self.get_official_name(), key=self) + + +class StructType(StructOrUnion): + kind = 'struct' + + +class UnionType(StructOrUnion): + kind = 'union' + + +class EnumType(StructOrUnionOrEnum): + kind = 'enum' + partial = False + partial_resolved = False + + def __init__(self, name, enumerators, enumvalues, baseinttype=None): + self.name = name + self.enumerators = enumerators + self.enumvalues = enumvalues + self.baseinttype = baseinttype + self.build_c_name_with_marker() + + def force_the_name(self, forcename): + StructOrUnionOrEnum.force_the_name(self, forcename) + if self.forcename is None: + name = self.get_official_name() + self.forcename = '$' + name.replace(' ', '_') + + def check_not_partial(self): + if self.partial and not self.partial_resolved: + raise VerificationMissing(self._get_c_name()) + + def build_backend_type(self, ffi, finishlist): + self.check_not_partial() + base_btype = self.build_baseinttype(ffi, finishlist) + return global_cache(self, ffi, 'new_enum_type', + self.get_official_name(), + self.enumerators, self.enumvalues, + base_btype, key=self) + + def build_baseinttype(self, ffi, finishlist): + if self.baseinttype is not None: + return self.baseinttype.get_cached_btype(ffi, finishlist) + # + if self.enumvalues: + smallest_value = min(self.enumvalues) + largest_value = max(self.enumvalues) + else: + import warnings + try: + # XXX! The goal is to ensure that the warnings.warn() + # will not suppress the warning. We want to get it + # several times if we reach this point several times. + __warningregistry__.clear() + except NameError: + pass + warnings.warn("%r has no values explicitly defined; " + "guessing that it is equivalent to 'unsigned int'" + % self._get_c_name()) + smallest_value = largest_value = 0 + if smallest_value < 0: # needs a signed type + sign = 1 + candidate1 = PrimitiveType("int") + candidate2 = PrimitiveType("long") + else: + sign = 0 + candidate1 = PrimitiveType("unsigned int") + candidate2 = PrimitiveType("unsigned long") + btype1 = candidate1.get_cached_btype(ffi, finishlist) + btype2 = candidate2.get_cached_btype(ffi, finishlist) + size1 = ffi.sizeof(btype1) + size2 = ffi.sizeof(btype2) + if (smallest_value >= ((-1) << (8*size1-1)) and + largest_value < (1 << (8*size1-sign))): + return btype1 + if (smallest_value >= ((-1) << (8*size2-1)) and + largest_value < (1 << (8*size2-sign))): + return btype2 + raise CDefError("%s values don't all fit into either 'long' " + "or 'unsigned long'" % self._get_c_name()) + +def unknown_type(name, structname=None): + if structname is None: + structname = '$%s' % name + tp = StructType(structname, None, None, None) + tp.force_the_name(name) + tp.origin = "unknown_type" + return tp + +def unknown_ptr_type(name, structname=None): + if structname is None: + structname = '$$%s' % name + tp = StructType(structname, None, None, None) + return NamedPointerType(tp, name) + + +global_lock = allocate_lock() +_typecache_cffi_backend = weakref.WeakValueDictionary() + +def get_typecache(backend): + # returns _typecache_cffi_backend if backend is the _cffi_backend + # module, or type(backend).__typecache if backend is an instance of + # CTypesBackend (or some FakeBackend class during tests) + if isinstance(backend, types.ModuleType): + return _typecache_cffi_backend + with global_lock: + if not hasattr(type(backend), '__typecache'): + type(backend).__typecache = weakref.WeakValueDictionary() + return type(backend).__typecache + +def global_cache(srctype, ffi, funcname, *args, **kwds): + key = kwds.pop('key', (funcname, args)) + assert not kwds + try: + return ffi._typecache[key] + except KeyError: + pass + try: + res = getattr(ffi._backend, funcname)(*args) + except NotImplementedError as e: + raise NotImplementedError("%s: %r: %s" % (funcname, srctype, e)) + # note that setdefault() on WeakValueDictionary is not atomic + # and contains a rare bug (http://bugs.python.org/issue19542); + # we have to use a lock and do it ourselves + cache = ffi._typecache + with global_lock: + res1 = cache.get(key) + if res1 is None: + cache[key] = res + return res + else: + return res1 + +def pointer_cache(ffi, BType): + return global_cache('?', ffi, 'new_pointer_type', BType) + +def attach_exception_info(e, name): + if e.args and type(e.args[0]) is str: + e.args = ('%s: %s' % (name, e.args[0]),) + e.args[1:] diff --git a/source/target-vendors/cp312/cffi/parse_c_type.h b/source/target-vendors/cp312/cffi/parse_c_type.h new file mode 100644 index 0000000..84e4ef8 --- /dev/null +++ b/source/target-vendors/cp312/cffi/parse_c_type.h @@ -0,0 +1,181 @@ + +/* This part is from file 'cffi/parse_c_type.h'. It is copied at the + beginning of C sources generated by CFFI's ffi.set_source(). */ + +typedef void *_cffi_opcode_t; + +#define _CFFI_OP(opcode, arg) (_cffi_opcode_t)(opcode | (((uintptr_t)(arg)) << 8)) +#define _CFFI_GETOP(cffi_opcode) ((unsigned char)(uintptr_t)cffi_opcode) +#define _CFFI_GETARG(cffi_opcode) (((intptr_t)cffi_opcode) >> 8) + +#define _CFFI_OP_PRIMITIVE 1 +#define _CFFI_OP_POINTER 3 +#define _CFFI_OP_ARRAY 5 +#define _CFFI_OP_OPEN_ARRAY 7 +#define _CFFI_OP_STRUCT_UNION 9 +#define _CFFI_OP_ENUM 11 +#define _CFFI_OP_FUNCTION 13 +#define _CFFI_OP_FUNCTION_END 15 +#define _CFFI_OP_NOOP 17 +#define _CFFI_OP_BITFIELD 19 +#define _CFFI_OP_TYPENAME 21 +#define _CFFI_OP_CPYTHON_BLTN_V 23 // varargs +#define _CFFI_OP_CPYTHON_BLTN_N 25 // noargs +#define _CFFI_OP_CPYTHON_BLTN_O 27 // O (i.e. a single arg) +#define _CFFI_OP_CONSTANT 29 +#define _CFFI_OP_CONSTANT_INT 31 +#define _CFFI_OP_GLOBAL_VAR 33 +#define _CFFI_OP_DLOPEN_FUNC 35 +#define _CFFI_OP_DLOPEN_CONST 37 +#define _CFFI_OP_GLOBAL_VAR_F 39 +#define _CFFI_OP_EXTERN_PYTHON 41 + +#define _CFFI_PRIM_VOID 0 +#define _CFFI_PRIM_BOOL 1 +#define _CFFI_PRIM_CHAR 2 +#define _CFFI_PRIM_SCHAR 3 +#define _CFFI_PRIM_UCHAR 4 +#define _CFFI_PRIM_SHORT 5 +#define _CFFI_PRIM_USHORT 6 +#define _CFFI_PRIM_INT 7 +#define _CFFI_PRIM_UINT 8 +#define _CFFI_PRIM_LONG 9 +#define _CFFI_PRIM_ULONG 10 +#define _CFFI_PRIM_LONGLONG 11 +#define _CFFI_PRIM_ULONGLONG 12 +#define _CFFI_PRIM_FLOAT 13 +#define _CFFI_PRIM_DOUBLE 14 +#define _CFFI_PRIM_LONGDOUBLE 15 + +#define _CFFI_PRIM_WCHAR 16 +#define _CFFI_PRIM_INT8 17 +#define _CFFI_PRIM_UINT8 18 +#define _CFFI_PRIM_INT16 19 +#define _CFFI_PRIM_UINT16 20 +#define _CFFI_PRIM_INT32 21 +#define _CFFI_PRIM_UINT32 22 +#define _CFFI_PRIM_INT64 23 +#define _CFFI_PRIM_UINT64 24 +#define _CFFI_PRIM_INTPTR 25 +#define _CFFI_PRIM_UINTPTR 26 +#define _CFFI_PRIM_PTRDIFF 27 +#define _CFFI_PRIM_SIZE 28 +#define _CFFI_PRIM_SSIZE 29 +#define _CFFI_PRIM_INT_LEAST8 30 +#define _CFFI_PRIM_UINT_LEAST8 31 +#define _CFFI_PRIM_INT_LEAST16 32 +#define _CFFI_PRIM_UINT_LEAST16 33 +#define _CFFI_PRIM_INT_LEAST32 34 +#define _CFFI_PRIM_UINT_LEAST32 35 +#define _CFFI_PRIM_INT_LEAST64 36 +#define _CFFI_PRIM_UINT_LEAST64 37 +#define _CFFI_PRIM_INT_FAST8 38 +#define _CFFI_PRIM_UINT_FAST8 39 +#define _CFFI_PRIM_INT_FAST16 40 +#define _CFFI_PRIM_UINT_FAST16 41 +#define _CFFI_PRIM_INT_FAST32 42 +#define _CFFI_PRIM_UINT_FAST32 43 +#define _CFFI_PRIM_INT_FAST64 44 +#define _CFFI_PRIM_UINT_FAST64 45 +#define _CFFI_PRIM_INTMAX 46 +#define _CFFI_PRIM_UINTMAX 47 +#define _CFFI_PRIM_FLOATCOMPLEX 48 +#define _CFFI_PRIM_DOUBLECOMPLEX 49 +#define _CFFI_PRIM_CHAR16 50 +#define _CFFI_PRIM_CHAR32 51 + +#define _CFFI__NUM_PRIM 52 +#define _CFFI__UNKNOWN_PRIM (-1) +#define _CFFI__UNKNOWN_FLOAT_PRIM (-2) +#define _CFFI__UNKNOWN_LONG_DOUBLE (-3) + +#define _CFFI__IO_FILE_STRUCT (-1) + + +struct _cffi_global_s { + const char *name; + void *address; + _cffi_opcode_t type_op; + void *size_or_direct_fn; // OP_GLOBAL_VAR: size, or 0 if unknown + // OP_CPYTHON_BLTN_*: addr of direct function +}; + +struct _cffi_getconst_s { + unsigned long long value; + const struct _cffi_type_context_s *ctx; + int gindex; +}; + +struct _cffi_struct_union_s { + const char *name; + int type_index; // -> _cffi_types, on a OP_STRUCT_UNION + int flags; // _CFFI_F_* flags below + size_t size; + int alignment; + int first_field_index; // -> _cffi_fields array + int num_fields; +}; +#define _CFFI_F_UNION 0x01 // is a union, not a struct +#define _CFFI_F_CHECK_FIELDS 0x02 // complain if fields are not in the + // "standard layout" or if some are missing +#define _CFFI_F_PACKED 0x04 // for CHECK_FIELDS, assume a packed struct +#define _CFFI_F_EXTERNAL 0x08 // in some other ffi.include() +#define _CFFI_F_OPAQUE 0x10 // opaque + +struct _cffi_field_s { + const char *name; + size_t field_offset; + size_t field_size; + _cffi_opcode_t field_type_op; +}; + +struct _cffi_enum_s { + const char *name; + int type_index; // -> _cffi_types, on a OP_ENUM + int type_prim; // _CFFI_PRIM_xxx + const char *enumerators; // comma-delimited string +}; + +struct _cffi_typename_s { + const char *name; + int type_index; /* if opaque, points to a possibly artificial + OP_STRUCT which is itself opaque */ +}; + +struct _cffi_type_context_s { + _cffi_opcode_t *types; + const struct _cffi_global_s *globals; + const struct _cffi_field_s *fields; + const struct _cffi_struct_union_s *struct_unions; + const struct _cffi_enum_s *enums; + const struct _cffi_typename_s *typenames; + int num_globals; + int num_struct_unions; + int num_enums; + int num_typenames; + const char *const *includes; + int num_types; + int flags; /* future extension */ +}; + +struct _cffi_parse_info_s { + const struct _cffi_type_context_s *ctx; + _cffi_opcode_t *output; + unsigned int output_size; + size_t error_location; + const char *error_message; +}; + +struct _cffi_externpy_s { + const char *name; + size_t size_of_result; + void *reserved1, *reserved2; +}; + +#ifdef _CFFI_INTERNAL +static int parse_c_type(struct _cffi_parse_info_s *info, const char *input); +static int search_in_globals(const struct _cffi_type_context_s *ctx, + const char *search, size_t search_len); +static int search_in_struct_unions(const struct _cffi_type_context_s *ctx, + const char *search, size_t search_len); +#endif diff --git a/source/target-vendors/cp312/cffi/pkgconfig.py b/source/target-vendors/cp312/cffi/pkgconfig.py new file mode 100644 index 0000000..e1034ad --- /dev/null +++ b/source/target-vendors/cp312/cffi/pkgconfig.py @@ -0,0 +1,121 @@ +# pkg-config, https://www.freedesktop.org/wiki/Software/pkg-config/ integration for cffi +import sys, os, subprocess + +from .error import PkgConfigError + + +def merge_flags(cfg1, cfg2): + """Merge values from cffi config flags cfg2 to cf1 + + Example: + merge_flags({"libraries": ["one"]}, {"libraries": ["two"]}) + {"libraries": ["one", "two"]} + """ + for key, value in cfg2.items(): + if key not in cfg1: + cfg1[key] = value + else: + if not isinstance(cfg1[key], list): + raise TypeError("cfg1[%r] should be a list of strings" % (key,)) + if not isinstance(value, list): + raise TypeError("cfg2[%r] should be a list of strings" % (key,)) + cfg1[key].extend(value) + return cfg1 + + +def call(libname, flag, encoding=sys.getfilesystemencoding()): + """Calls pkg-config and returns the output if found + """ + a = ["pkg-config", "--print-errors"] + a.append(flag) + a.append(libname) + try: + pc = subprocess.Popen(a, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except OSError as e: + raise PkgConfigError("cannot run pkg-config: %s" % (str(e).strip(),)) + + bout, berr = pc.communicate() + if pc.returncode != 0: + try: + berr = berr.decode(encoding) + except Exception: + pass + raise PkgConfigError(berr.strip()) + + if sys.version_info >= (3,) and not isinstance(bout, str): # Python 3.x + try: + bout = bout.decode(encoding) + except UnicodeDecodeError: + raise PkgConfigError("pkg-config %s %s returned bytes that cannot " + "be decoded with encoding %r:\n%r" % + (flag, libname, encoding, bout)) + + if os.altsep != '\\' and '\\' in bout: + raise PkgConfigError("pkg-config %s %s returned an unsupported " + "backslash-escaped output:\n%r" % + (flag, libname, bout)) + return bout + + +def flags_from_pkgconfig(libs): + r"""Return compiler line flags for FFI.set_source based on pkg-config output + + Usage + ... + ffibuilder.set_source("_foo", pkgconfig = ["libfoo", "libbar >= 1.8.3"]) + + If pkg-config is installed on build machine, then arguments include_dirs, + library_dirs, libraries, define_macros, extra_compile_args and + extra_link_args are extended with an output of pkg-config for libfoo and + libbar. + + Raises PkgConfigError in case the pkg-config call fails. + """ + + def get_include_dirs(string): + return [x[2:] for x in string.split() if x.startswith("-I")] + + def get_library_dirs(string): + return [x[2:] for x in string.split() if x.startswith("-L")] + + def get_libraries(string): + return [x[2:] for x in string.split() if x.startswith("-l")] + + # convert -Dfoo=bar to list of tuples [("foo", "bar")] expected by distutils + def get_macros(string): + def _macro(x): + x = x[2:] # drop "-D" + if '=' in x: + return tuple(x.split("=", 1)) # "-Dfoo=bar" => ("foo", "bar") + else: + return (x, None) # "-Dfoo" => ("foo", None) + return [_macro(x) for x in string.split() if x.startswith("-D")] + + def get_other_cflags(string): + return [x for x in string.split() if not x.startswith("-I") and + not x.startswith("-D")] + + def get_other_libs(string): + return [x for x in string.split() if not x.startswith("-L") and + not x.startswith("-l")] + + # return kwargs for given libname + def kwargs(libname): + fse = sys.getfilesystemencoding() + all_cflags = call(libname, "--cflags") + all_libs = call(libname, "--libs") + return { + "include_dirs": get_include_dirs(all_cflags), + "library_dirs": get_library_dirs(all_libs), + "libraries": get_libraries(all_libs), + "define_macros": get_macros(all_cflags), + "extra_compile_args": get_other_cflags(all_cflags), + "extra_link_args": get_other_libs(all_libs), + } + + # merge all arguments together + ret = {} + for libname in libs: + lib_flags = kwargs(libname) + merge_flags(ret, lib_flags) + return ret diff --git a/source/target-vendors/cp312/cffi/recompiler.py b/source/target-vendors/cp312/cffi/recompiler.py new file mode 100644 index 0000000..5e71aed --- /dev/null +++ b/source/target-vendors/cp312/cffi/recompiler.py @@ -0,0 +1,1586 @@ +import io, os, sys, sysconfig +from . import ffiplatform, model +from .error import VerificationError +from .cffi_opcode import * + +VERSION_BASE = 0x2601 +VERSION_EMBEDDED = 0x2701 +VERSION_CHAR16CHAR32 = 0x2801 + +FREE_THREADED_BUILD = sysconfig.get_config_var("Py_GIL_DISABLED") +USE_LIMITED_API = ((sys.platform != 'win32' or sys.version_info < (3, 0) or + sys.version_info >= (3, 5)) and + # free-threaded build doesn't support the stable ABI until Python 3.15 + (not FREE_THREADED_BUILD or sys.version_info >= (3, 15))) + +class GlobalExpr: + def __init__(self, name, address, type_op, size=0, check_value=0): + self.name = name + self.address = address + self.type_op = type_op + self.size = size + self.check_value = check_value + + def as_c_expr(self): + return ' { "%s", (void *)%s, %s, (void *)%s },' % ( + self.name, self.address, self.type_op.as_c_expr(), self.size) + + def as_python_expr(self): + return "b'%s%s',%d" % (self.type_op.as_python_bytes(), self.name, + self.check_value) + +class FieldExpr: + def __init__(self, name, field_offset, field_size, fbitsize, field_type_op): + self.name = name + self.field_offset = field_offset + self.field_size = field_size + self.fbitsize = fbitsize + self.field_type_op = field_type_op + + def as_c_expr(self): + spaces = " " * len(self.name) + return (' { "%s", %s,\n' % (self.name, self.field_offset) + + ' %s %s,\n' % (spaces, self.field_size) + + ' %s %s },' % (spaces, self.field_type_op.as_c_expr())) + + def as_python_expr(self): + raise NotImplementedError + + def as_field_python_expr(self): + if self.field_type_op.op == OP_NOOP: + size_expr = '' + elif self.field_type_op.op == OP_BITFIELD: + size_expr = format_four_bytes(self.fbitsize) + else: + raise NotImplementedError + return "b'%s%s%s'" % (self.field_type_op.as_python_bytes(), + size_expr, + self.name) + +class StructUnionExpr: + def __init__(self, name, type_index, flags, size, alignment, comment, + first_field_index, c_fields): + self.name = name + self.type_index = type_index + self.flags = flags + self.size = size + self.alignment = alignment + self.comment = comment + self.first_field_index = first_field_index + self.c_fields = c_fields + + def as_c_expr(self): + return (' { "%s", %d, %s,' % (self.name, self.type_index, self.flags) + + '\n %s, %s, ' % (self.size, self.alignment) + + '%d, %d ' % (self.first_field_index, len(self.c_fields)) + + ('/* %s */ ' % self.comment if self.comment else '') + + '},') + + def as_python_expr(self): + flags = eval(self.flags, G_FLAGS) + fields_expr = [c_field.as_field_python_expr() + for c_field in self.c_fields] + return "(b'%s%s%s',%s)" % ( + format_four_bytes(self.type_index), + format_four_bytes(flags), + self.name, + ','.join(fields_expr)) + +class EnumExpr: + def __init__(self, name, type_index, size, signed, allenums): + self.name = name + self.type_index = type_index + self.size = size + self.signed = signed + self.allenums = allenums + + def as_c_expr(self): + lines = [' { "%s", %d, _cffi_prim_int(%s, %s),' % (self.name, self.type_index, + self.size, self.signed,)] + pending = 0 + while len(self.allenums) > pending + 110: + j = self.allenums.find(',', pending + 100) + if j < 0: + break + j += 1 + lines.append(' "%s"' % (self.allenums[pending:j],)) + pending = j + lines.append(' "%s" },' % (self.allenums[pending:],)) + return '\n'.join(lines) + + def as_python_expr(self): + prim_index = { + (1, 0): PRIM_UINT8, (1, 1): PRIM_INT8, + (2, 0): PRIM_UINT16, (2, 1): PRIM_INT16, + (4, 0): PRIM_UINT32, (4, 1): PRIM_INT32, + (8, 0): PRIM_UINT64, (8, 1): PRIM_INT64, + }[self.size, self.signed] + return "b'%s%s%s\\x00%s'" % (format_four_bytes(self.type_index), + format_four_bytes(prim_index), + self.name, self.allenums) + +class TypenameExpr: + def __init__(self, name, type_index): + self.name = name + self.type_index = type_index + + def as_c_expr(self): + return ' { "%s", %d },' % (self.name, self.type_index) + + def as_python_expr(self): + return "b'%s%s'" % (format_four_bytes(self.type_index), self.name) + + +# ____________________________________________________________ + + +class Recompiler: + _num_externpy = 0 + + def __init__(self, ffi, module_name, target_is_python=False): + self.ffi = ffi + self.module_name = module_name + self.target_is_python = target_is_python + self._version = VERSION_BASE + + def needs_version(self, ver): + self._version = max(self._version, ver) + + def collect_type_table(self): + self._typesdict = {} + self._generate("collecttype") + # + all_decls = sorted(self._typesdict, key=str) + # + # prepare all FUNCTION bytecode sequences first + self.cffi_types = [] + for tp in all_decls: + if tp.is_raw_function: + assert self._typesdict[tp] is None + self._typesdict[tp] = len(self.cffi_types) + self.cffi_types.append(tp) # placeholder + for tp1 in tp.args: + assert isinstance(tp1, (model.VoidType, + model.BasePrimitiveType, + model.PointerType, + model.StructOrUnionOrEnum, + model.FunctionPtrType)) + if self._typesdict[tp1] is None: + self._typesdict[tp1] = len(self.cffi_types) + self.cffi_types.append(tp1) # placeholder + self.cffi_types.append('END') # placeholder + # + # prepare all OTHER bytecode sequences + for tp in all_decls: + if not tp.is_raw_function and self._typesdict[tp] is None: + self._typesdict[tp] = len(self.cffi_types) + self.cffi_types.append(tp) # placeholder + if tp.is_array_type and tp.length is not None: + self.cffi_types.append('LEN') # placeholder + assert None not in self._typesdict.values() + # + # collect all structs and unions and enums + self._struct_unions = {} + self._enums = {} + for tp in all_decls: + if isinstance(tp, model.StructOrUnion): + self._struct_unions[tp] = None + elif isinstance(tp, model.EnumType): + self._enums[tp] = None + for i, tp in enumerate(sorted(self._struct_unions, + key=lambda tp: tp.name)): + self._struct_unions[tp] = i + for i, tp in enumerate(sorted(self._enums, + key=lambda tp: tp.name)): + self._enums[tp] = i + # + # emit all bytecode sequences now + for tp in all_decls: + method = getattr(self, '_emit_bytecode_' + tp.__class__.__name__) + method(tp, self._typesdict[tp]) + # + # consistency check + for op in self.cffi_types: + assert isinstance(op, CffiOp) + self.cffi_types = tuple(self.cffi_types) # don't change any more + + def _enum_fields(self, tp): + # When producing C, expand all anonymous struct/union fields. + # That's necessary to have C code checking the offsets of the + # individual fields contained in them. When producing Python, + # don't do it and instead write it like it is, with the + # corresponding fields having an empty name. Empty names are + # recognized at runtime when we import the generated Python + # file. + expand_anonymous_struct_union = not self.target_is_python + return tp.enumfields(expand_anonymous_struct_union) + + def _do_collect_type(self, tp): + if not isinstance(tp, model.BaseTypeByIdentity): + if isinstance(tp, tuple): + for x in tp: + self._do_collect_type(x) + return + if tp not in self._typesdict: + self._typesdict[tp] = None + if isinstance(tp, model.FunctionPtrType): + self._do_collect_type(tp.as_raw_function()) + elif isinstance(tp, model.StructOrUnion): + if tp.fldtypes is not None and ( + tp not in self.ffi._parser._included_declarations): + for name1, tp1, _, _ in self._enum_fields(tp): + self._do_collect_type(self._field_type(tp, name1, tp1)) + else: + for _, x in tp._get_items(): + self._do_collect_type(x) + + def _generate(self, step_name): + lst = self.ffi._parser._declarations.items() + for name, (tp, quals) in sorted(lst): + kind, realname = name.split(' ', 1) + try: + method = getattr(self, '_generate_cpy_%s_%s' % (kind, + step_name)) + except AttributeError: + raise VerificationError( + "not implemented in recompile(): %r" % name) + try: + self._current_quals = quals + method(tp, realname) + except Exception as e: + model.attach_exception_info(e, name) + raise + + # ---------- + + ALL_STEPS = ["global", "field", "struct_union", "enum", "typename"] + + def collect_step_tables(self): + # collect the declarations for '_cffi_globals', '_cffi_typenames', etc. + self._lsts = {} + for step_name in self.ALL_STEPS: + self._lsts[step_name] = [] + self._seen_struct_unions = set() + self._generate("ctx") + self._add_missing_struct_unions() + # + for step_name in self.ALL_STEPS: + lst = self._lsts[step_name] + if step_name != "field": + lst.sort(key=lambda entry: entry.name) + self._lsts[step_name] = tuple(lst) # don't change any more + # + # check for a possible internal inconsistency: _cffi_struct_unions + # should have been generated with exactly self._struct_unions + lst = self._lsts["struct_union"] + for tp, i in self._struct_unions.items(): + assert i < len(lst) + assert lst[i].name == tp.name + assert len(lst) == len(self._struct_unions) + # same with enums + lst = self._lsts["enum"] + for tp, i in self._enums.items(): + assert i < len(lst) + assert lst[i].name == tp.name + assert len(lst) == len(self._enums) + + # ---------- + + def _prnt(self, what=''): + self._f.write(what + '\n') + + def write_source_to_f(self, f, preamble): + if self.target_is_python: + assert preamble is None + self.write_py_source_to_f(f) + else: + assert preamble is not None + self.write_c_source_to_f(f, preamble) + + def _rel_readlines(self, filename): + with open(os.path.join(os.path.dirname(__file__), filename), 'r') as g: + return g.readlines() + + def write_c_source_to_f(self, f, preamble): + self._f = f + prnt = self._prnt + if self.ffi._embedding is not None: + prnt('#define _CFFI_USE_EMBEDDING') + if not USE_LIMITED_API: + prnt('#define _CFFI_NO_LIMITED_API') + # + # first the '#include' (actually done by inlining the file's content) + lines = self._rel_readlines('_cffi_include.h') + i = lines.index('#include "parse_c_type.h"\n') + lines[i:i+1] = self._rel_readlines('parse_c_type.h') + prnt(''.join(lines)) + # + # if we have ffi._embedding != None, we give it here as a macro + # and include an extra file + base_module_name = self.module_name.split('.')[-1] + if self.ffi._embedding is not None: + prnt('#define _CFFI_MODULE_NAME "%s"' % (self.module_name,)) + prnt('static const char _CFFI_PYTHON_STARTUP_CODE[] = {') + self._print_string_literal_in_array(self.ffi._embedding) + prnt('0 };') + prnt('#ifdef PYPY_VERSION') + prnt('# define _CFFI_PYTHON_STARTUP_FUNC _cffi_pypyinit_%s' % ( + base_module_name,)) + prnt('#else') + prnt('# define _CFFI_PYTHON_STARTUP_FUNC PyInit_%s' % ( + base_module_name,)) + prnt('#endif') + lines = self._rel_readlines('_embedding.h') + i = lines.index('#include "_cffi_errors.h"\n') + lines[i:i+1] = self._rel_readlines('_cffi_errors.h') + prnt(''.join(lines)) + self.needs_version(VERSION_EMBEDDED) + # + # then paste the C source given by the user, verbatim. + prnt('/************************************************************/') + prnt() + prnt(preamble) + prnt() + prnt('/************************************************************/') + prnt() + # + # the declaration of '_cffi_types' + prnt('static void *_cffi_types[] = {') + typeindex2type = dict([(i, tp) for (tp, i) in self._typesdict.items()]) + for i, op in enumerate(self.cffi_types): + comment = '' + if i in typeindex2type: + comment = ' // ' + typeindex2type[i]._get_c_name() + prnt('/* %2d */ %s,%s' % (i, op.as_c_expr(), comment)) + if not self.cffi_types: + prnt(' 0') + prnt('};') + prnt() + # + # call generate_cpy_xxx_decl(), for every xxx found from + # ffi._parser._declarations. This generates all the functions. + self._seen_constants = set() + self._generate("decl") + # + # the declaration of '_cffi_globals' and '_cffi_typenames' + nums = {} + for step_name in self.ALL_STEPS: + lst = self._lsts[step_name] + nums[step_name] = len(lst) + if nums[step_name] > 0: + prnt('static const struct _cffi_%s_s _cffi_%ss[] = {' % ( + step_name, step_name)) + for entry in lst: + prnt(entry.as_c_expr()) + prnt('};') + prnt() + # + # the declaration of '_cffi_includes' + if self.ffi._included_ffis: + prnt('static const char * const _cffi_includes[] = {') + for ffi_to_include in self.ffi._included_ffis: + try: + included_module_name, included_source = ( + ffi_to_include._assigned_source[:2]) + except AttributeError: + raise VerificationError( + "ffi object %r includes %r, but the latter has not " + "been prepared with set_source()" % ( + self.ffi, ffi_to_include,)) + if included_source is None: + raise VerificationError( + "not implemented yet: ffi.include() of a Python-based " + "ffi inside a C-based ffi") + prnt(' "%s",' % (included_module_name,)) + prnt(' NULL') + prnt('};') + prnt() + # + # the declaration of '_cffi_type_context' + prnt('static const struct _cffi_type_context_s _cffi_type_context = {') + prnt(' _cffi_types,') + for step_name in self.ALL_STEPS: + if nums[step_name] > 0: + prnt(' _cffi_%ss,' % step_name) + else: + prnt(' NULL, /* no %ss */' % step_name) + for step_name in self.ALL_STEPS: + if step_name != "field": + prnt(' %d, /* num_%ss */' % (nums[step_name], step_name)) + if self.ffi._included_ffis: + prnt(' _cffi_includes,') + else: + prnt(' NULL, /* no includes */') + prnt(' %d, /* num_types */' % (len(self.cffi_types),)) + flags = 0 + if self._num_externpy > 0 or self.ffi._embedding is not None: + flags |= 1 # set to mean that we use extern "Python" + prnt(' %d, /* flags */' % flags) + prnt('};') + prnt() + # + # the init function + prnt('#ifdef __GNUC__') + prnt('# pragma GCC visibility push(default) /* for -fvisibility= */') + prnt('#endif') + prnt() + prnt('#ifdef PYPY_VERSION') + prnt('PyMODINIT_FUNC') + prnt('_cffi_pypyinit_%s(const void *p[])' % (base_module_name,)) + prnt('{') + if flags & 1: + prnt(' if (((intptr_t)p[0]) >= 0x0A03) {') + prnt(' _cffi_call_python_org = ' + '(void(*)(struct _cffi_externpy_s *, char *))p[1];') + prnt(' }') + prnt(' p[0] = (const void *)0x%x;' % self._version) + prnt(' p[1] = &_cffi_type_context;') + prnt(' return NULL;') + prnt('}') + # on Windows, distutils insists on putting init_cffi_xyz in + # 'export_symbols', so instead of fighting it, just give up and + # give it one + prnt('# ifdef _MSC_VER') + prnt(' PyMODINIT_FUNC') + prnt(' PyInit_%s(void) { return NULL; }' % (base_module_name,)) + prnt('# endif') + prnt('#else') + prnt('PyMODINIT_FUNC') + prnt('PyInit_%s(void)' % (base_module_name,)) + prnt('{') + prnt(' return _cffi_init("%s", 0x%x, &_cffi_type_context);' % ( + self.module_name, self._version)) + prnt('}') + prnt('#endif') + prnt() + prnt('#ifdef __GNUC__') + prnt('# pragma GCC visibility pop') + prnt('#endif') + self._version = None + + def _to_py(self, x): + if isinstance(x, str): + return "b'%s'" % (x,) + if isinstance(x, (list, tuple)): + rep = [self._to_py(item) for item in x] + if len(rep) == 1: + rep.append('') + return "(%s)" % (','.join(rep),) + return x.as_python_expr() # Py2: unicode unexpected; Py3: bytes unexp. + + def write_py_source_to_f(self, f): + self._f = f + prnt = self._prnt + # + # header + prnt("# auto-generated file") + prnt("import _cffi_backend") + # + # the 'import' of the included ffis + num_includes = len(self.ffi._included_ffis or ()) + for i in range(num_includes): + ffi_to_include = self.ffi._included_ffis[i] + try: + included_module_name, included_source = ( + ffi_to_include._assigned_source[:2]) + except AttributeError: + raise VerificationError( + "ffi object %r includes %r, but the latter has not " + "been prepared with set_source()" % ( + self.ffi, ffi_to_include,)) + if included_source is not None: + raise VerificationError( + "not implemented yet: ffi.include() of a C-based " + "ffi inside a Python-based ffi") + prnt('from %s import ffi as _ffi%d' % (included_module_name, i)) + prnt() + prnt("ffi = _cffi_backend.FFI('%s'," % (self.module_name,)) + prnt(" _version = 0x%x," % (self._version,)) + self._version = None + # + # the '_types' keyword argument + self.cffi_types = tuple(self.cffi_types) # don't change any more + types_lst = [op.as_python_bytes() for op in self.cffi_types] + prnt(' _types = %s,' % (self._to_py(''.join(types_lst)),)) + typeindex2type = dict([(i, tp) for (tp, i) in self._typesdict.items()]) + # + # the keyword arguments from ALL_STEPS + for step_name in self.ALL_STEPS: + lst = self._lsts[step_name] + if len(lst) > 0 and step_name != "field": + prnt(' _%ss = %s,' % (step_name, self._to_py(lst))) + # + # the '_includes' keyword argument + if num_includes > 0: + prnt(' _includes = (%s,),' % ( + ', '.join(['_ffi%d' % i for i in range(num_includes)]),)) + # + # the footer + prnt(')') + + # ---------- + + def _gettypenum(self, type): + # a KeyError here is a bug. please report it! :-) + return self._typesdict[type] + + def _convert_funcarg_to_c(self, tp, fromvar, tovar, errcode): + extraarg = '' + if isinstance(tp, model.BasePrimitiveType) and not tp.is_complex_type(): + if tp.is_integer_type() and tp.name != '_Bool': + converter = '_cffi_to_c_int' + extraarg = ', %s' % tp.name + elif isinstance(tp, model.UnknownFloatType): + # don't check with is_float_type(): it may be a 'long + # double' here, and _cffi_to_c_double would loose precision + converter = '(%s)_cffi_to_c_double' % (tp.get_c_name(''),) + else: + cname = tp.get_c_name('') + converter = '(%s)_cffi_to_c_%s' % (cname, + tp.name.replace(' ', '_')) + if cname in ('char16_t', 'char32_t'): + self.needs_version(VERSION_CHAR16CHAR32) + errvalue = '-1' + # + elif isinstance(tp, model.PointerType): + self._convert_funcarg_to_c_ptr_or_array(tp, fromvar, + tovar, errcode) + return + # + elif (isinstance(tp, (model.StructOrUnionOrEnum, model.BasePrimitiveType))): + # a struct (not a struct pointer) as a function argument; + # or, a complex (the same code works) + self._prnt(' if (_cffi_to_c((char *)&%s, _cffi_type(%d), %s) < 0)' + % (tovar, self._gettypenum(tp), fromvar)) + self._prnt(' %s;' % errcode) + return + # + elif isinstance(tp, model.FunctionPtrType): + converter = '(%s)_cffi_to_c_pointer' % tp.get_c_name('') + extraarg = ', _cffi_type(%d)' % self._gettypenum(tp) + errvalue = 'NULL' + # + else: + raise NotImplementedError(tp) + # + self._prnt(' %s = %s(%s%s);' % (tovar, converter, fromvar, extraarg)) + self._prnt(' if (%s == (%s)%s && PyErr_Occurred())' % ( + tovar, tp.get_c_name(''), errvalue)) + self._prnt(' %s;' % errcode) + + def _extra_local_variables(self, tp, localvars, freelines): + if isinstance(tp, model.PointerType): + localvars.add('Py_ssize_t datasize') + localvars.add('struct _cffi_freeme_s *large_args_free = NULL') + freelines.add('if (large_args_free != NULL)' + ' _cffi_free_array_arguments(large_args_free);') + + def _convert_funcarg_to_c_ptr_or_array(self, tp, fromvar, tovar, errcode): + self._prnt(' datasize = _cffi_prepare_pointer_call_argument(') + self._prnt(' _cffi_type(%d), %s, (char **)&%s);' % ( + self._gettypenum(tp), fromvar, tovar)) + self._prnt(' if (datasize != 0) {') + self._prnt(' %s = ((size_t)datasize) <= 640 ? ' + '(%s)alloca((size_t)datasize) : NULL;' % ( + tovar, tp.get_c_name(''))) + self._prnt(' if (_cffi_convert_array_argument(_cffi_type(%d), %s, ' + '(char **)&%s,' % (self._gettypenum(tp), fromvar, tovar)) + self._prnt(' datasize, &large_args_free) < 0)') + self._prnt(' %s;' % errcode) + self._prnt(' }') + + def _convert_expr_from_c(self, tp, var, context): + if isinstance(tp, model.BasePrimitiveType): + if tp.is_integer_type() and tp.name != '_Bool': + return '_cffi_from_c_int(%s, %s)' % (var, tp.name) + elif isinstance(tp, model.UnknownFloatType): + return '_cffi_from_c_double(%s)' % (var,) + elif tp.name != 'long double' and not tp.is_complex_type(): + cname = tp.name.replace(' ', '_') + if cname in ('char16_t', 'char32_t'): + self.needs_version(VERSION_CHAR16CHAR32) + return '_cffi_from_c_%s(%s)' % (cname, var) + else: + return '_cffi_from_c_deref((char *)&%s, _cffi_type(%d))' % ( + var, self._gettypenum(tp)) + elif isinstance(tp, (model.PointerType, model.FunctionPtrType)): + return '_cffi_from_c_pointer((char *)%s, _cffi_type(%d))' % ( + var, self._gettypenum(tp)) + elif isinstance(tp, model.ArrayType): + return '_cffi_from_c_pointer((char *)%s, _cffi_type(%d))' % ( + var, self._gettypenum(model.PointerType(tp.item))) + elif isinstance(tp, model.StructOrUnion): + if tp.fldnames is None: + raise TypeError("'%s' is used as %s, but is opaque" % ( + tp._get_c_name(), context)) + return '_cffi_from_c_struct((char *)&%s, _cffi_type(%d))' % ( + var, self._gettypenum(tp)) + elif isinstance(tp, model.EnumType): + return '_cffi_from_c_deref((char *)&%s, _cffi_type(%d))' % ( + var, self._gettypenum(tp)) + else: + raise NotImplementedError(tp) + + # ---------- + # typedefs + + def _typedef_type(self, tp, name): + return self._global_type(tp, "(*(%s *)0)" % (name,)) + + def _generate_cpy_typedef_collecttype(self, tp, name): + self._do_collect_type(self._typedef_type(tp, name)) + + def _generate_cpy_typedef_decl(self, tp, name): + pass + + def _typedef_ctx(self, tp, name): + type_index = self._typesdict[tp] + self._lsts["typename"].append(TypenameExpr(name, type_index)) + + def _generate_cpy_typedef_ctx(self, tp, name): + tp = self._typedef_type(tp, name) + self._typedef_ctx(tp, name) + if getattr(tp, "origin", None) == "unknown_type": + self._struct_ctx(tp, tp.name, approxname=None) + elif isinstance(tp, model.NamedPointerType): + self._struct_ctx(tp.totype, tp.totype.name, approxname=tp.name, + named_ptr=tp) + + # ---------- + # function declarations + + def _generate_cpy_function_collecttype(self, tp, name): + self._do_collect_type(tp.as_raw_function()) + if tp.ellipsis and not self.target_is_python: + self._do_collect_type(tp) + + def _generate_cpy_function_decl(self, tp, name): + assert not self.target_is_python + assert isinstance(tp, model.FunctionPtrType) + if tp.ellipsis: + # cannot support vararg functions better than this: check for its + # exact type (including the fixed arguments), and build it as a + # constant function pointer (no CPython wrapper) + self._generate_cpy_constant_decl(tp, name) + return + prnt = self._prnt + numargs = len(tp.args) + if numargs == 0: + argname = 'noarg' + elif numargs == 1: + argname = 'arg0' + else: + argname = 'args' + # + # ------------------------------ + # the 'd' version of the function, only for addressof(lib, 'func') + arguments = [] + call_arguments = [] + context = 'argument of %s' % name + for i, type in enumerate(tp.args): + arguments.append(type.get_c_name(' x%d' % i, context)) + call_arguments.append('x%d' % i) + repr_arguments = ', '.join(arguments) + repr_arguments = repr_arguments or 'void' + if tp.abi: + abi = tp.abi + ' ' + else: + abi = '' + name_and_arguments = '%s_cffi_d_%s(%s)' % (abi, name, repr_arguments) + prnt('static %s' % (tp.result.get_c_name(name_and_arguments),)) + prnt('{') + call_arguments = ', '.join(call_arguments) + result_code = 'return ' + if isinstance(tp.result, model.VoidType): + result_code = '' + prnt(' %s%s(%s);' % (result_code, name, call_arguments)) + prnt('}') + # + prnt('#ifndef PYPY_VERSION') # ------------------------------ + # + prnt('static PyObject *') + prnt('_cffi_f_%s(PyObject *self, PyObject *%s)' % (name, argname)) + prnt('{') + # + context = 'argument of %s' % name + for i, type in enumerate(tp.args): + arg = type.get_c_name(' x%d' % i, context) + prnt(' %s;' % arg) + # + localvars = set() + freelines = set() + for type in tp.args: + self._extra_local_variables(type, localvars, freelines) + for decl in sorted(localvars): + prnt(' %s;' % (decl,)) + # + if not isinstance(tp.result, model.VoidType): + result_code = 'result = ' + context = 'result of %s' % name + result_decl = ' %s;' % tp.result.get_c_name(' result', context) + prnt(result_decl) + prnt(' PyObject *pyresult;') + else: + result_decl = None + result_code = '' + # + if len(tp.args) > 1: + rng = range(len(tp.args)) + for i in rng: + prnt(' PyObject *arg%d;' % i) + prnt() + prnt(' if (!PyArg_UnpackTuple(args, "%s", %d, %d, %s))' % ( + name, len(rng), len(rng), + ', '.join(['&arg%d' % i for i in rng]))) + prnt(' return NULL;') + prnt() + # + for i, type in enumerate(tp.args): + self._convert_funcarg_to_c(type, 'arg%d' % i, 'x%d' % i, + 'return NULL') + prnt() + # + prnt(' Py_BEGIN_ALLOW_THREADS') + prnt(' _cffi_restore_errno();') + call_arguments = ['x%d' % i for i in range(len(tp.args))] + call_arguments = ', '.join(call_arguments) + prnt(' { %s%s(%s); }' % (result_code, name, call_arguments)) + prnt(' _cffi_save_errno();') + prnt(' Py_END_ALLOW_THREADS') + prnt() + # + prnt(' (void)self; /* unused */') + if numargs == 0: + prnt(' (void)noarg; /* unused */') + if result_code: + prnt(' pyresult = %s;' % + self._convert_expr_from_c(tp.result, 'result', 'result type')) + for freeline in freelines: + prnt(' ' + freeline) + prnt(' return pyresult;') + else: + for freeline in freelines: + prnt(' ' + freeline) + prnt(' Py_INCREF(Py_None);') + prnt(' return Py_None;') + prnt('}') + # + prnt('#else') # ------------------------------ + # + # the PyPy version: need to replace struct/union arguments with + # pointers, and if the result is a struct/union, insert a first + # arg that is a pointer to the result. We also do that for + # complex args and return type. + def need_indirection(type): + return (isinstance(type, model.StructOrUnion) or + (isinstance(type, model.PrimitiveType) and + type.is_complex_type())) + difference = False + arguments = [] + call_arguments = [] + context = 'argument of %s' % name + for i, type in enumerate(tp.args): + indirection = '' + if need_indirection(type): + indirection = '*' + difference = True + arg = type.get_c_name(' %sx%d' % (indirection, i), context) + arguments.append(arg) + call_arguments.append('%sx%d' % (indirection, i)) + tp_result = tp.result + if need_indirection(tp_result): + context = 'result of %s' % name + arg = tp_result.get_c_name(' *result', context) + arguments.insert(0, arg) + tp_result = model.void_type + result_decl = None + result_code = '*result = ' + difference = True + if difference: + repr_arguments = ', '.join(arguments) + repr_arguments = repr_arguments or 'void' + name_and_arguments = '%s_cffi_f_%s(%s)' % (abi, name, + repr_arguments) + prnt('static %s' % (tp_result.get_c_name(name_and_arguments),)) + prnt('{') + if result_decl: + prnt(result_decl) + call_arguments = ', '.join(call_arguments) + prnt(' { %s%s(%s); }' % (result_code, name, call_arguments)) + if result_decl: + prnt(' return result;') + prnt('}') + else: + prnt('# define _cffi_f_%s _cffi_d_%s' % (name, name)) + # + prnt('#endif') # ------------------------------ + prnt() + + def _generate_cpy_function_ctx(self, tp, name): + if tp.ellipsis and not self.target_is_python: + self._generate_cpy_constant_ctx(tp, name) + return + type_index = self._typesdict[tp.as_raw_function()] + numargs = len(tp.args) + if self.target_is_python: + meth_kind = OP_DLOPEN_FUNC + elif numargs == 0: + meth_kind = OP_CPYTHON_BLTN_N # 'METH_NOARGS' + elif numargs == 1: + meth_kind = OP_CPYTHON_BLTN_O # 'METH_O' + else: + meth_kind = OP_CPYTHON_BLTN_V # 'METH_VARARGS' + self._lsts["global"].append( + GlobalExpr(name, '_cffi_f_%s' % name, + CffiOp(meth_kind, type_index), + size='_cffi_d_%s' % name)) + + # ---------- + # named structs or unions + + def _field_type(self, tp_struct, field_name, tp_field): + if isinstance(tp_field, model.ArrayType): + actual_length = tp_field.length + if actual_length == '...': + ptr_struct_name = tp_struct.get_c_name('*') + actual_length = '_cffi_array_len(((%s)0)->%s)' % ( + ptr_struct_name, field_name) + tp_item = self._field_type(tp_struct, '%s[0]' % field_name, + tp_field.item) + tp_field = model.ArrayType(tp_item, actual_length) + return tp_field + + def _struct_collecttype(self, tp): + self._do_collect_type(tp) + if self.target_is_python: + # also requires nested anon struct/unions in ABI mode, recursively + for fldtype in tp.anonymous_struct_fields(): + self._struct_collecttype(fldtype) + + def _struct_decl(self, tp, cname, approxname): + if tp.fldtypes is None: + return + prnt = self._prnt + checkfuncname = '_cffi_checkfld_%s' % (approxname,) + prnt('_CFFI_UNUSED_FN') + prnt('static void %s(%s *p)' % (checkfuncname, cname)) + prnt('{') + prnt(' /* only to generate compile-time warnings or errors */') + prnt(' (void)p;') + for fname, ftype, fbitsize, fqual in self._enum_fields(tp): + try: + if ftype.is_integer_type() or fbitsize >= 0: + # accept all integers, but complain on float or double + if fname != '': + prnt(" (void)((p->%s) | 0); /* check that '%s.%s' is " + "an integer */" % (fname, cname, fname)) + continue + # only accept exactly the type declared, except that '[]' + # is interpreted as a '*' and so will match any array length. + # (It would also match '*', but that's harder to detect...) + while (isinstance(ftype, model.ArrayType) + and (ftype.length is None or ftype.length == '...')): + ftype = ftype.item + fname = fname + '[0]' + prnt(' { %s = &p->%s; (void)tmp; }' % ( + ftype.get_c_name('*tmp', 'field %r'%fname, quals=fqual), + fname)) + except VerificationError as e: + prnt(' /* %s */' % str(e)) # cannot verify it, ignore + prnt('}') + prnt('struct _cffi_align_%s { char x; %s y; };' % (approxname, cname)) + prnt() + + def _struct_ctx(self, tp, cname, approxname, named_ptr=None): + type_index = self._typesdict[tp] + reason_for_not_expanding = None + flags = [] + if isinstance(tp, model.UnionType): + flags.append("_CFFI_F_UNION") + if tp.fldtypes is None: + flags.append("_CFFI_F_OPAQUE") + reason_for_not_expanding = "opaque" + if (tp not in self.ffi._parser._included_declarations and + (named_ptr is None or + named_ptr not in self.ffi._parser._included_declarations)): + if tp.fldtypes is None: + pass # opaque + elif tp.partial or any(tp.anonymous_struct_fields()): + pass # field layout obtained silently from the C compiler + else: + flags.append("_CFFI_F_CHECK_FIELDS") + if tp.packed: + if tp.packed > 1: + raise NotImplementedError( + "%r is declared with 'pack=%r'; only 0 or 1 are " + "supported in API mode (try to use \"...;\", which " + "does not require a 'pack' declaration)" % + (tp, tp.packed)) + flags.append("_CFFI_F_PACKED") + else: + flags.append("_CFFI_F_EXTERNAL") + reason_for_not_expanding = "external" + flags = '|'.join(flags) or '0' + c_fields = [] + if reason_for_not_expanding is None: + enumfields = list(self._enum_fields(tp)) + for fldname, fldtype, fbitsize, fqual in enumfields: + fldtype = self._field_type(tp, fldname, fldtype) + self._check_not_opaque(fldtype, + "field '%s.%s'" % (tp.name, fldname)) + # cname is None for _add_missing_struct_unions() only + op = OP_NOOP + if fbitsize >= 0: + op = OP_BITFIELD + size = '%d /* bits */' % fbitsize + elif cname is None or ( + isinstance(fldtype, model.ArrayType) and + fldtype.length is None): + size = '(size_t)-1' + else: + size = 'sizeof(((%s)0)->%s)' % ( + tp.get_c_name('*') if named_ptr is None + else named_ptr.name, + fldname) + if cname is None or fbitsize >= 0: + offset = '(size_t)-1' + elif named_ptr is not None: + offset = '(size_t)(((char *)&((%s)4096)->%s) - (char *)4096)' % ( + named_ptr.name, fldname) + else: + offset = 'offsetof(%s, %s)' % (tp.get_c_name(''), fldname) + c_fields.append( + FieldExpr(fldname, offset, size, fbitsize, + CffiOp(op, self._typesdict[fldtype]))) + first_field_index = len(self._lsts["field"]) + self._lsts["field"].extend(c_fields) + # + if cname is None: # unknown name, for _add_missing_struct_unions + size = '(size_t)-2' + align = -2 + comment = "unnamed" + else: + if named_ptr is not None: + size = 'sizeof(*(%s)0)' % (named_ptr.name,) + align = '-1 /* unknown alignment */' + else: + size = 'sizeof(%s)' % (cname,) + align = 'offsetof(struct _cffi_align_%s, y)' % (approxname,) + comment = None + else: + size = '(size_t)-1' + align = -1 + first_field_index = -1 + comment = reason_for_not_expanding + self._lsts["struct_union"].append( + StructUnionExpr(tp.name, type_index, flags, size, align, comment, + first_field_index, c_fields)) + self._seen_struct_unions.add(tp) + + def _check_not_opaque(self, tp, location): + while isinstance(tp, model.ArrayType): + tp = tp.item + if isinstance(tp, model.StructOrUnion) and tp.fldtypes is None: + raise TypeError( + "%s is of an opaque type (not declared in cdef())" % location) + + def _add_missing_struct_unions(self): + # not very nice, but some struct declarations might be missing + # because they don't have any known C name. Check that they are + # not partial (we can't complete or verify them!) and emit them + # anonymously. + lst = list(self._struct_unions.items()) + lst.sort(key=lambda tp_order: tp_order[1]) + for tp, order in lst: + if tp not in self._seen_struct_unions: + if tp.partial: + raise NotImplementedError("internal inconsistency: %r is " + "partial but was not seen at " + "this point" % (tp,)) + if tp.name.startswith('$') and tp.name[1:].isdigit(): + approxname = tp.name[1:] + elif tp.name == '_IO_FILE' and tp.forcename == 'FILE': + approxname = 'FILE' + self._typedef_ctx(tp, 'FILE') + else: + raise NotImplementedError("internal inconsistency: %r" % + (tp,)) + self._struct_ctx(tp, None, approxname) + + def _generate_cpy_struct_collecttype(self, tp, name): + self._struct_collecttype(tp) + _generate_cpy_union_collecttype = _generate_cpy_struct_collecttype + + def _struct_names(self, tp): + cname = tp.get_c_name('') + if ' ' in cname: + return cname, cname.replace(' ', '_') + else: + return cname, '_' + cname + + def _generate_cpy_struct_decl(self, tp, name): + self._struct_decl(tp, *self._struct_names(tp)) + _generate_cpy_union_decl = _generate_cpy_struct_decl + + def _generate_cpy_struct_ctx(self, tp, name): + self._struct_ctx(tp, *self._struct_names(tp)) + _generate_cpy_union_ctx = _generate_cpy_struct_ctx + + # ---------- + # 'anonymous' declarations. These are produced for anonymous structs + # or unions; the 'name' is obtained by a typedef. + + def _generate_cpy_anonymous_collecttype(self, tp, name): + if isinstance(tp, model.EnumType): + self._generate_cpy_enum_collecttype(tp, name) + else: + self._struct_collecttype(tp) + + def _generate_cpy_anonymous_decl(self, tp, name): + if isinstance(tp, model.EnumType): + self._generate_cpy_enum_decl(tp) + else: + self._struct_decl(tp, name, 'typedef_' + name) + + def _generate_cpy_anonymous_ctx(self, tp, name): + if isinstance(tp, model.EnumType): + self._enum_ctx(tp, name) + else: + self._struct_ctx(tp, name, 'typedef_' + name) + + # ---------- + # constants, declared with "static const ..." + + def _generate_cpy_const(self, is_int, name, tp=None, category='const', + check_value=None): + if (category, name) in self._seen_constants: + raise VerificationError( + "duplicate declaration of %s '%s'" % (category, name)) + self._seen_constants.add((category, name)) + # + prnt = self._prnt + funcname = '_cffi_%s_%s' % (category, name) + if is_int: + prnt('static int %s(unsigned long long *o)' % funcname) + prnt('{') + prnt(' int n = (%s) <= 0;' % (name,)) + prnt(' *o = (unsigned long long)((%s) | 0);' + ' /* check that %s is an integer */' % (name, name)) + if check_value is not None: + if check_value > 0: + check_value = '%dU' % (check_value,) + prnt(' if (!_cffi_check_int(*o, n, %s))' % (check_value,)) + prnt(' n |= 2;') + prnt(' return n;') + prnt('}') + else: + assert check_value is None + prnt('static void %s(char *o)' % funcname) + prnt('{') + prnt(' *(%s)o = %s;' % (tp.get_c_name('*'), name)) + prnt('}') + prnt() + + def _generate_cpy_constant_collecttype(self, tp, name): + is_int = tp.is_integer_type() + if not is_int or self.target_is_python: + self._do_collect_type(tp) + + def _generate_cpy_constant_decl(self, tp, name): + is_int = tp.is_integer_type() + self._generate_cpy_const(is_int, name, tp) + + def _generate_cpy_constant_ctx(self, tp, name): + if not self.target_is_python and tp.is_integer_type(): + type_op = CffiOp(OP_CONSTANT_INT, -1) + else: + if self.target_is_python: + const_kind = OP_DLOPEN_CONST + else: + const_kind = OP_CONSTANT + type_index = self._typesdict[tp] + type_op = CffiOp(const_kind, type_index) + self._lsts["global"].append( + GlobalExpr(name, '_cffi_const_%s' % name, type_op)) + + # ---------- + # enums + + def _generate_cpy_enum_collecttype(self, tp, name): + self._do_collect_type(tp) + + def _generate_cpy_enum_decl(self, tp, name=None): + for enumerator in tp.enumerators: + self._generate_cpy_const(True, enumerator) + + def _enum_ctx(self, tp, cname): + type_index = self._typesdict[tp] + type_op = CffiOp(OP_ENUM, -1) + if self.target_is_python: + tp.check_not_partial() + for enumerator, enumvalue in zip(tp.enumerators, tp.enumvalues): + self._lsts["global"].append( + GlobalExpr(enumerator, '_cffi_const_%s' % enumerator, type_op, + check_value=enumvalue)) + # + if cname is not None and '$' not in cname and not self.target_is_python: + size = "sizeof(%s)" % cname + signed = "((%s)-1) <= 0" % cname + else: + basetp = tp.build_baseinttype(self.ffi, []) + size = self.ffi.sizeof(basetp) + signed = int(int(self.ffi.cast(basetp, -1)) < 0) + allenums = ",".join(tp.enumerators) + self._lsts["enum"].append( + EnumExpr(tp.name, type_index, size, signed, allenums)) + + def _generate_cpy_enum_ctx(self, tp, name): + self._enum_ctx(tp, tp._get_c_name()) + + # ---------- + # macros: for now only for integers + + def _generate_cpy_macro_collecttype(self, tp, name): + pass + + def _generate_cpy_macro_decl(self, tp, name): + if tp == '...': + check_value = None + else: + check_value = tp # an integer + self._generate_cpy_const(True, name, check_value=check_value) + + def _generate_cpy_macro_ctx(self, tp, name): + if tp == '...': + if self.target_is_python: + raise VerificationError( + "cannot use the syntax '...' in '#define %s ...' when " + "using the ABI mode" % (name,)) + check_value = None + else: + check_value = tp # an integer + type_op = CffiOp(OP_CONSTANT_INT, -1) + self._lsts["global"].append( + GlobalExpr(name, '_cffi_const_%s' % name, type_op, + check_value=check_value)) + + # ---------- + # global variables + + def _global_type(self, tp, global_name): + if isinstance(tp, model.ArrayType): + actual_length = tp.length + if actual_length == '...': + actual_length = '_cffi_array_len(%s)' % (global_name,) + tp_item = self._global_type(tp.item, '%s[0]' % global_name) + tp = model.ArrayType(tp_item, actual_length) + return tp + + def _generate_cpy_variable_collecttype(self, tp, name): + self._do_collect_type(self._global_type(tp, name)) + + def _generate_cpy_variable_decl(self, tp, name): + prnt = self._prnt + tp = self._global_type(tp, name) + if isinstance(tp, model.ArrayType) and tp.length is None: + tp = tp.item + ampersand = '' + else: + ampersand = '&' + # This code assumes that casts from "tp *" to "void *" is a + # no-op, i.e. a function that returns a "tp *" can be called + # as if it returned a "void *". This should be generally true + # on any modern machine. The only exception to that rule (on + # uncommon architectures, and as far as I can tell) might be + # if 'tp' were a function type, but that is not possible here. + # (If 'tp' is a function _pointer_ type, then casts from "fn_t + # **" to "void *" are again no-ops, as far as I can tell.) + decl = '*_cffi_var_%s(void)' % (name,) + prnt('static ' + tp.get_c_name(decl, quals=self._current_quals)) + prnt('{') + prnt(' return %s(%s);' % (ampersand, name)) + prnt('}') + prnt() + + def _generate_cpy_variable_ctx(self, tp, name): + tp = self._global_type(tp, name) + type_index = self._typesdict[tp] + if self.target_is_python: + op = OP_GLOBAL_VAR + else: + op = OP_GLOBAL_VAR_F + self._lsts["global"].append( + GlobalExpr(name, '_cffi_var_%s' % name, CffiOp(op, type_index))) + + # ---------- + # extern "Python" + + def _generate_cpy_extern_python_collecttype(self, tp, name): + assert isinstance(tp, model.FunctionPtrType) + self._do_collect_type(tp) + _generate_cpy_dllexport_python_collecttype = \ + _generate_cpy_extern_python_plus_c_collecttype = \ + _generate_cpy_extern_python_collecttype + + def _extern_python_decl(self, tp, name, tag_and_space): + prnt = self._prnt + if isinstance(tp.result, model.VoidType): + size_of_result = '0' + else: + context = 'result of %s' % name + size_of_result = '(int)sizeof(%s)' % ( + tp.result.get_c_name('', context),) + prnt('static struct _cffi_externpy_s _cffi_externpy__%s =' % name) + prnt(' { "%s.%s", %s, 0, 0 };' % ( + self.module_name, name, size_of_result)) + prnt() + # + arguments = [] + context = 'argument of %s' % name + for i, type in enumerate(tp.args): + arg = type.get_c_name(' a%d' % i, context) + arguments.append(arg) + # + repr_arguments = ', '.join(arguments) + repr_arguments = repr_arguments or 'void' + name_and_arguments = '%s(%s)' % (name, repr_arguments) + if tp.abi == "__stdcall": + name_and_arguments = '_cffi_stdcall ' + name_and_arguments + # + def may_need_128_bits(tp): + return (isinstance(tp, model.PrimitiveType) and + tp.name == 'long double') + # + size_of_a = max(len(tp.args)*8, 8) + if may_need_128_bits(tp.result): + size_of_a = max(size_of_a, 16) + if isinstance(tp.result, model.StructOrUnion): + size_of_a = 'sizeof(%s) > %d ? sizeof(%s) : %d' % ( + tp.result.get_c_name(''), size_of_a, + tp.result.get_c_name(''), size_of_a) + prnt('%s%s' % (tag_and_space, tp.result.get_c_name(name_and_arguments))) + prnt('{') + prnt(' char a[%s];' % size_of_a) + prnt(' char *p = a;') + for i, type in enumerate(tp.args): + arg = 'a%d' % i + if (isinstance(type, model.StructOrUnion) or + may_need_128_bits(type)): + arg = '&' + arg + type = model.PointerType(type) + prnt(' *(%s)(p + %d) = %s;' % (type.get_c_name('*'), i*8, arg)) + prnt(' _cffi_call_python(&_cffi_externpy__%s, p);' % name) + if not isinstance(tp.result, model.VoidType): + prnt(' return *(%s)p;' % (tp.result.get_c_name('*'),)) + prnt('}') + prnt() + self._num_externpy += 1 + + def _generate_cpy_extern_python_decl(self, tp, name): + self._extern_python_decl(tp, name, 'static ') + + def _generate_cpy_dllexport_python_decl(self, tp, name): + self._extern_python_decl(tp, name, 'CFFI_DLLEXPORT ') + + def _generate_cpy_extern_python_plus_c_decl(self, tp, name): + self._extern_python_decl(tp, name, '') + + def _generate_cpy_extern_python_ctx(self, tp, name): + if self.target_is_python: + raise VerificationError( + "cannot use 'extern \"Python\"' in the ABI mode") + if tp.ellipsis: + raise NotImplementedError("a vararg function is extern \"Python\"") + type_index = self._typesdict[tp] + type_op = CffiOp(OP_EXTERN_PYTHON, type_index) + self._lsts["global"].append( + GlobalExpr(name, '&_cffi_externpy__%s' % name, type_op, name)) + + _generate_cpy_dllexport_python_ctx = \ + _generate_cpy_extern_python_plus_c_ctx = \ + _generate_cpy_extern_python_ctx + + def _print_string_literal_in_array(self, s): + prnt = self._prnt + prnt('// # NB. this is not a string because of a size limit in MSVC') + if not isinstance(s, bytes): # unicode + s = s.encode('utf-8') # -> bytes + else: + s.decode('utf-8') # got bytes, check for valid utf-8 + for line in s.splitlines(True): + comment = line + if type('//') is bytes: # python2 + line = map(ord, line) # make a list of integers + else: # python3 + # type(line) is bytes, which enumerates like a list of integers + comment = ascii(comment)[1:-1] + prnt(('// ' + comment).rstrip()) + printed_line = '' + for c in line: + if len(printed_line) >= 76: + prnt(printed_line) + printed_line = '' + printed_line += '%d,' % (c,) + prnt(printed_line) + + # ---------- + # emitting the opcodes for individual types + + def _emit_bytecode_VoidType(self, tp, index): + self.cffi_types[index] = CffiOp(OP_PRIMITIVE, PRIM_VOID) + + def _emit_bytecode_PrimitiveType(self, tp, index): + prim_index = PRIMITIVE_TO_INDEX[tp.name] + self.cffi_types[index] = CffiOp(OP_PRIMITIVE, prim_index) + + def _emit_bytecode_UnknownIntegerType(self, tp, index): + s = ('_cffi_prim_int(sizeof(%s), (\n' + ' ((%s)-1) | 0 /* check that %s is an integer type */\n' + ' ) <= 0)' % (tp.name, tp.name, tp.name)) + self.cffi_types[index] = CffiOp(OP_PRIMITIVE, s) + + def _emit_bytecode_UnknownFloatType(self, tp, index): + s = ('_cffi_prim_float(sizeof(%s) *\n' + ' (((%s)1) / 2) * 2 /* integer => 0, float => 1 */\n' + ' )' % (tp.name, tp.name)) + self.cffi_types[index] = CffiOp(OP_PRIMITIVE, s) + + def _emit_bytecode_RawFunctionType(self, tp, index): + self.cffi_types[index] = CffiOp(OP_FUNCTION, self._typesdict[tp.result]) + index += 1 + for tp1 in tp.args: + realindex = self._typesdict[tp1] + if index != realindex: + if isinstance(tp1, model.PrimitiveType): + self._emit_bytecode_PrimitiveType(tp1, index) + else: + self.cffi_types[index] = CffiOp(OP_NOOP, realindex) + index += 1 + flags = int(tp.ellipsis) + if tp.abi is not None: + if tp.abi == '__stdcall': + flags |= 2 + else: + raise NotImplementedError("abi=%r" % (tp.abi,)) + self.cffi_types[index] = CffiOp(OP_FUNCTION_END, flags) + + def _emit_bytecode_PointerType(self, tp, index): + self.cffi_types[index] = CffiOp(OP_POINTER, self._typesdict[tp.totype]) + + _emit_bytecode_ConstPointerType = _emit_bytecode_PointerType + _emit_bytecode_NamedPointerType = _emit_bytecode_PointerType + + def _emit_bytecode_FunctionPtrType(self, tp, index): + raw = tp.as_raw_function() + self.cffi_types[index] = CffiOp(OP_POINTER, self._typesdict[raw]) + + def _emit_bytecode_ArrayType(self, tp, index): + item_index = self._typesdict[tp.item] + if tp.length is None: + self.cffi_types[index] = CffiOp(OP_OPEN_ARRAY, item_index) + elif tp.length == '...': + raise VerificationError( + "type %s badly placed: the '...' array length can only be " + "used on global arrays or on fields of structures" % ( + str(tp).replace('/*...*/', '...'),)) + else: + assert self.cffi_types[index + 1] == 'LEN' + self.cffi_types[index] = CffiOp(OP_ARRAY, item_index) + self.cffi_types[index + 1] = CffiOp(None, str(tp.length)) + + def _emit_bytecode_StructType(self, tp, index): + struct_index = self._struct_unions[tp] + self.cffi_types[index] = CffiOp(OP_STRUCT_UNION, struct_index) + _emit_bytecode_UnionType = _emit_bytecode_StructType + + def _emit_bytecode_EnumType(self, tp, index): + enum_index = self._enums[tp] + self.cffi_types[index] = CffiOp(OP_ENUM, enum_index) + + +if sys.version_info >= (3,): + NativeIO = io.StringIO +else: + class NativeIO(io.BytesIO): + def write(self, s): + if isinstance(s, unicode): + s = s.encode('ascii') + super().write(s) + +def _is_file_like(maybefile): + # compare to xml.etree.ElementTree._get_writer + return hasattr(maybefile, 'write') + +def _make_c_or_py_source(ffi, module_name, preamble, target_file, verbose): + if verbose: + print("generating %s" % (target_file,)) + recompiler = Recompiler(ffi, module_name, + target_is_python=(preamble is None)) + recompiler.collect_type_table() + recompiler.collect_step_tables() + if _is_file_like(target_file): + recompiler.write_source_to_f(target_file, preamble) + return True + f = NativeIO() + recompiler.write_source_to_f(f, preamble) + output = f.getvalue() + try: + with open(target_file, 'r') as f1: + if f1.read(len(output) + 1) != output: + raise OSError + if verbose: + print("(already up-to-date)") + return False # already up-to-date + except OSError: + tmp_file = '%s.~%d' % (target_file, os.getpid()) + with open(tmp_file, 'w') as f1: + f1.write(output) + try: + os.rename(tmp_file, target_file) + except OSError: + os.unlink(target_file) + os.rename(tmp_file, target_file) + return True + +def make_c_source(ffi, module_name, preamble, target_c_file, verbose=False): + assert preamble is not None + return _make_c_or_py_source(ffi, module_name, preamble, target_c_file, + verbose) + +def make_py_source(ffi, module_name, target_py_file, verbose=False): + return _make_c_or_py_source(ffi, module_name, None, target_py_file, + verbose) + +def _modname_to_file(outputdir, modname, extension): + parts = modname.split('.') + try: + os.makedirs(os.path.join(outputdir, *parts[:-1])) + except OSError: + pass + parts[-1] += extension + return os.path.join(outputdir, *parts), parts + + +# Aaargh. Distutils is not tested at all for the purpose of compiling +# DLLs that are not extension modules. Here are some hacks to work +# around that, in the _patch_for_*() functions... + +def _patch_meth(patchlist, cls, name, new_meth): + old = getattr(cls, name) + patchlist.append((cls, name, old)) + setattr(cls, name, new_meth) + return old + +def _unpatch_meths(patchlist): + for cls, name, old_meth in reversed(patchlist): + setattr(cls, name, old_meth) + +def _patch_for_embedding(patchlist): + if sys.platform == 'win32': + # we must not remove the manifest when building for embedding! + # FUTURE: this module was removed in setuptools 74; this is likely dead code and should be removed, + # since the toolchain it supports (VS2005-2008) is also long dead. + from cffi._shimmed_dist_utils import MSVCCompiler + if MSVCCompiler is not None: + _patch_meth(patchlist, MSVCCompiler, '_remove_visual_c_ref', + lambda self, manifest_file: manifest_file) + + if sys.platform == 'darwin': + # we must not make a '-bundle', but a '-dynamiclib' instead + from cffi._shimmed_dist_utils import CCompiler + def my_link_shared_object(self, *args, **kwds): + if '-bundle' in self.linker_so: + self.linker_so = list(self.linker_so) + i = self.linker_so.index('-bundle') + self.linker_so[i] = '-dynamiclib' + return old_link_shared_object(self, *args, **kwds) + old_link_shared_object = _patch_meth(patchlist, CCompiler, + 'link_shared_object', + my_link_shared_object) + +def _patch_for_target(patchlist, target): + from cffi._shimmed_dist_utils import build_ext + # if 'target' is different from '*', we need to patch some internal + # method to just return this 'target' value, instead of having it + # built from module_name + if target.endswith('.*'): + target = target[:-2] + if sys.platform == 'win32': + target += '.dll' + elif sys.platform == 'darwin': + target += '.dylib' + else: + target += '.so' + _patch_meth(patchlist, build_ext, 'get_ext_filename', + lambda self, ext_name: target) + + +def recompile(ffi, module_name, preamble, tmpdir='.', call_c_compiler=True, + c_file=None, source_extension='.c', extradir=None, + compiler_verbose=1, target=None, debug=None, + uses_ffiplatform=True, **kwds): + if not isinstance(module_name, str): + module_name = module_name.encode('ascii') + if ffi._windows_unicode: + ffi._apply_windows_unicode(kwds) + if preamble is not None: + if call_c_compiler and _is_file_like(c_file): + raise TypeError("Writing to file-like objects is not supported " + "with call_c_compiler=True") + embedding = (ffi._embedding is not None) + if embedding: + ffi._apply_embedding_fix(kwds) + if c_file is None: + c_file, parts = _modname_to_file(tmpdir, module_name, + source_extension) + if extradir: + parts = [extradir] + parts + ext_c_file = os.path.join(*parts) + else: + ext_c_file = c_file + # + if target is None: + if embedding: + target = '%s.*' % module_name + else: + target = '*' + # + if uses_ffiplatform: + ext = ffiplatform.get_extension(ext_c_file, module_name, **kwds) + else: + ext = None + updated = make_c_source(ffi, module_name, preamble, c_file, + verbose=compiler_verbose) + if call_c_compiler: + patchlist = [] + cwd = os.getcwd() + try: + if embedding: + _patch_for_embedding(patchlist) + if target != '*': + _patch_for_target(patchlist, target) + if compiler_verbose: + if tmpdir == '.': + msg = 'the current directory is' + else: + msg = 'setting the current directory to' + print('%s %r' % (msg, os.path.abspath(tmpdir))) + os.chdir(tmpdir) + outputfilename = ffiplatform.compile('.', ext, + compiler_verbose, debug) + finally: + os.chdir(cwd) + _unpatch_meths(patchlist) + return outputfilename + else: + return ext, updated + else: + if c_file is None: + c_file, _ = _modname_to_file(tmpdir, module_name, '.py') + updated = make_py_source(ffi, module_name, c_file, + verbose=compiler_verbose) + if call_c_compiler: + return c_file + else: + return None, updated + diff --git a/source/target-vendors/cp312/cffi/setuptools_ext.py b/source/target-vendors/cp312/cffi/setuptools_ext.py new file mode 100644 index 0000000..946afb4 --- /dev/null +++ b/source/target-vendors/cp312/cffi/setuptools_ext.py @@ -0,0 +1,231 @@ +import os +import sys +import sysconfig + +try: + basestring +except NameError: + # Python 3.x + basestring = str + +def error(msg): + from cffi._shimmed_dist_utils import DistutilsSetupError + raise DistutilsSetupError(msg) + + +def execfile(filename, glob): + # We use execfile() (here rewritten for Python 3) instead of + # __import__() to load the build script. The problem with + # a normal import is that in some packages, the intermediate + # __init__.py files may already try to import the file that + # we are generating. + with open(filename) as f: + src = f.read() + src += '\n' # Python 2.6 compatibility + code = compile(src, filename, 'exec') + exec(code, glob, glob) + + +def add_cffi_module(dist, mod_spec): + from cffi.api import FFI + + if not isinstance(mod_spec, basestring): + error("argument to 'cffi_modules=...' must be a str or a list of str," + " not %r" % (type(mod_spec).__name__,)) + mod_spec = str(mod_spec) + try: + build_file_name, ffi_var_name = mod_spec.split(':') + except ValueError: + error("%r must be of the form 'path/build.py:ffi_variable'" % + (mod_spec,)) + if not os.path.exists(build_file_name): + ext = '' + rewritten = build_file_name.replace('.', '/') + '.py' + if os.path.exists(rewritten): + ext = ' (rewrite cffi_modules to [%r])' % ( + rewritten + ':' + ffi_var_name,) + error("%r does not name an existing file%s" % (build_file_name, ext)) + + mod_vars = {'__name__': '__cffi__', '__file__': build_file_name} + execfile(build_file_name, mod_vars) + + try: + ffi = mod_vars[ffi_var_name] + except KeyError: + error("%r: object %r not found in module" % (mod_spec, + ffi_var_name)) + if not isinstance(ffi, FFI): + ffi = ffi() # maybe it's a function instead of directly an ffi + if not isinstance(ffi, FFI): + error("%r is not an FFI instance (got %r)" % (mod_spec, + type(ffi).__name__)) + if not hasattr(ffi, '_assigned_source'): + error("%r: the set_source() method was not called" % (mod_spec,)) + module_name, source, source_extension, kwds = ffi._assigned_source + if ffi._windows_unicode: + kwds = kwds.copy() + ffi._apply_windows_unicode(kwds) + + if source is None: + _add_py_module(dist, ffi, module_name) + else: + _add_c_module(dist, ffi, module_name, source, source_extension, kwds) + +def _set_py_limited_api(Extension, kwds): + """ + Add py_limited_api to kwds if setuptools >= 26 is in use. + Do not alter the setting if it already exists. + Setuptools takes care of ignoring the flag on Python 2 and PyPy. + + CPython itself should ignore the flag in a debugging version + (by not listing .abi3.so in the extensions it supports), but + it doesn't so far, creating troubles. That's why we check + for "not hasattr(sys, 'gettotalrefcount')" (the 2.7 compatible equivalent + of 'd' not in sys.abiflags). (http://bugs.python.org/issue28401) + + On Windows, with CPython <= 3.4, it's better not to use py_limited_api + because virtualenv *still* doesn't copy PYTHON3.DLL on these versions. + Recently (2020) we started shipping only >= 3.5 wheels, though. So + we'll give it another try and set py_limited_api on Windows >= 3.5. + """ + from cffi._shimmed_dist_utils import log + from cffi import recompiler + + if ('py_limited_api' not in kwds and not hasattr(sys, 'gettotalrefcount') + and recompiler.USE_LIMITED_API): + import setuptools + try: + setuptools_major_version = int(setuptools.__version__.partition('.')[0]) + if setuptools_major_version >= 26: + kwds['py_limited_api'] = True + except ValueError: # certain development versions of setuptools + # If we don't know the version number of setuptools, we + # try to set 'py_limited_api' anyway. At worst, we get a + # warning. + kwds['py_limited_api'] = True + + if sysconfig.get_config_var("Py_GIL_DISABLED"): + if sys.version_info < (3, 15): + if kwds.get('py_limited_api'): + log.info("Ignoring py_limited_api=True for free-threaded build.") + kwds['py_limited_api'] = False + else: + kwds.setdefault("define_macros", []).append(("Py_TARGET_ABI3T", "0x030f0000")) + + if kwds.get('py_limited_api') is False: + # avoid setting Py_LIMITED_API if py_limited_api=False + # which _cffi_include.h does unless _CFFI_NO_LIMITED_API is defined + kwds.setdefault("define_macros", []).append(("_CFFI_NO_LIMITED_API", None)) + return kwds + +def _add_c_module(dist, ffi, module_name, source, source_extension, kwds): + # We are a setuptools extension. Need this build_ext for py_limited_api. + from setuptools.command.build_ext import build_ext + from cffi._shimmed_dist_utils import Extension, log, mkpath + from cffi import recompiler + + allsources = ['$PLACEHOLDER'] + allsources.extend(kwds.pop('sources', [])) + kwds = _set_py_limited_api(Extension, kwds) + ext = Extension(name=module_name, sources=allsources, **kwds) + + def make_mod(tmpdir, pre_run=None): + c_file = os.path.join(tmpdir, module_name + source_extension) + log.info("generating cffi module %r" % c_file) + mkpath(tmpdir) + # a setuptools-only, API-only hook: called with the "ext" and "ffi" + # arguments just before we turn the ffi into C code. To use it, + # subclass the 'distutils.command.build_ext.build_ext' class and + # add a method 'def pre_run(self, ext, ffi)'. + if pre_run is not None: + pre_run(ext, ffi) + updated = recompiler.make_c_source(ffi, module_name, source, c_file) + if not updated: + log.info("already up-to-date") + return c_file + + if dist.ext_modules is None: + dist.ext_modules = [] + dist.ext_modules.append(ext) + + base_class = dist.cmdclass.get('build_ext', build_ext) + class build_ext_make_mod(base_class): + def run(self): + if ext.sources[0] == '$PLACEHOLDER': + pre_run = getattr(self, 'pre_run', None) + ext.sources[0] = make_mod(self.build_temp, pre_run) + base_class.run(self) + dist.cmdclass['build_ext'] = build_ext_make_mod + # NB. multiple runs here will create multiple 'build_ext_make_mod' + # classes. Even in this case the 'build_ext' command should be + # run once; but just in case, the logic above does nothing if + # called again. + + +def _add_py_module(dist, ffi, module_name): + from setuptools.command.build_py import build_py + from setuptools.command.build_ext import build_ext + from cffi._shimmed_dist_utils import log, mkpath + from cffi import recompiler + + def generate_mod(py_file): + log.info("generating cffi module %r" % py_file) + mkpath(os.path.dirname(py_file)) + updated = recompiler.make_py_source(ffi, module_name, py_file) + if not updated: + log.info("already up-to-date") + + base_class = dist.cmdclass.get('build_py', build_py) + class build_py_make_mod(base_class): + def run(self): + base_class.run(self) + module_path = module_name.split('.') + module_path[-1] += '.py' + generate_mod(os.path.join(self.build_lib, *module_path)) + def get_source_files(self): + # This is called from 'setup.py sdist' only. Exclude + # the generate .py module in this case. + saved_py_modules = self.py_modules + try: + if saved_py_modules: + self.py_modules = [m for m in saved_py_modules + if m != module_name] + return base_class.get_source_files(self) + finally: + self.py_modules = saved_py_modules + dist.cmdclass['build_py'] = build_py_make_mod + + # distutils and setuptools have no notion I could find of a + # generated python module. If we don't add module_name to + # dist.py_modules, then things mostly work but there are some + # combination of options (--root and --record) that will miss + # the module. So we add it here, which gives a few apparently + # harmless warnings about not finding the file outside the + # build directory. + # Then we need to hack more in get_source_files(); see above. + if dist.py_modules is None: + dist.py_modules = [] + dist.py_modules.append(module_name) + + # the following is only for "build_ext -i" + base_class_2 = dist.cmdclass.get('build_ext', build_ext) + class build_ext_make_mod(base_class_2): + def run(self): + base_class_2.run(self) + if self.inplace: + # from get_ext_fullpath() in distutils/command/build_ext.py + module_path = module_name.split('.') + package = '.'.join(module_path[:-1]) + build_py = self.get_finalized_command('build_py') + package_dir = build_py.get_package_dir(package) + file_name = module_path[-1] + '.py' + generate_mod(os.path.join(package_dir, file_name)) + dist.cmdclass['build_ext'] = build_ext_make_mod + +def cffi_modules(dist, attr, value): + assert attr == 'cffi_modules' + if isinstance(value, basestring): + value = [value] + + for cffi_module in value: + add_cffi_module(dist, cffi_module) diff --git a/source/target-vendors/cp312/cffi/vengine_cpy.py b/source/target-vendors/cp312/cffi/vengine_cpy.py new file mode 100644 index 0000000..4d2ea43 --- /dev/null +++ b/source/target-vendors/cp312/cffi/vengine_cpy.py @@ -0,0 +1,1058 @@ +# +# DEPRECATED: implementation for ffi.verify() +# +import sys +from . import model +from .error import VerificationError +from . import _imp_emulation as imp + + +class VCPythonEngine: + _class_key = 'x' + _gen_python_module = True + + def __init__(self, verifier): + self.verifier = verifier + self.ffi = verifier.ffi + self._struct_pending_verification = {} + self._types_of_builtin_functions = {} + + def patch_extension_kwds(self, kwds): + pass + + def find_module(self, module_name, path, so_suffixes): + try: + f, filename, descr = imp.find_module(module_name, path) + except ImportError: + return None + if f is not None: + f.close() + # Note that after a setuptools installation, there are both .py + # and .so files with the same basename. The code here relies on + # imp.find_module() locating the .so in priority. + if descr[0] not in so_suffixes: + return None + return filename + + def collect_types(self): + self._typesdict = {} + self._generate("collecttype") + + def _prnt(self, what=''): + self._f.write(what + '\n') + + def _gettypenum(self, type): + # a KeyError here is a bug. please report it! :-) + return self._typesdict[type] + + def _do_collect_type(self, tp): + if ((not isinstance(tp, model.PrimitiveType) + or tp.name == 'long double') + and tp not in self._typesdict): + num = len(self._typesdict) + self._typesdict[tp] = num + + def write_source_to_f(self): + self.collect_types() + # + # The new module will have a _cffi_setup() function that receives + # objects from the ffi world, and that calls some setup code in + # the module. This setup code is split in several independent + # functions, e.g. one per constant. The functions are "chained" + # by ending in a tail call to each other. + # + # This is further split in two chained lists, depending on if we + # can do it at import-time or if we must wait for _cffi_setup() to + # provide us with the objects. This is needed because we + # need the values of the enum constants in order to build the + # that we may have to pass to _cffi_setup(). + # + # The following two 'chained_list_constants' items contains + # the head of these two chained lists, as a string that gives the + # call to do, if any. + self._chained_list_constants = ['((void)lib,0)', '((void)lib,0)'] + # + prnt = self._prnt + # first paste some standard set of lines that are mostly '#define' + prnt(cffimod_header) + prnt() + # then paste the C source given by the user, verbatim. + prnt(self.verifier.preamble) + prnt() + # + # call generate_cpy_xxx_decl(), for every xxx found from + # ffi._parser._declarations. This generates all the functions. + self._generate("decl") + # + # implement the function _cffi_setup_custom() as calling the + # head of the chained list. + self._generate_setup_custom() + prnt() + # + # produce the method table, including the entries for the + # generated Python->C function wrappers, which are done + # by generate_cpy_function_method(). + prnt('static PyMethodDef _cffi_methods[] = {') + self._generate("method") + prnt(' {"_cffi_setup", _cffi_setup, METH_VARARGS, NULL},') + prnt(' {NULL, NULL, 0, NULL} /* Sentinel */') + prnt('};') + prnt() + # + # standard init. + modname = self.verifier.get_module_name() + constants = self._chained_list_constants[False] + prnt('static struct PyModuleDef _cffi_module_def = {') + prnt(' PyModuleDef_HEAD_INIT,') + prnt(' "%s",' % modname) + prnt(' NULL,') + prnt(' -1,') + prnt(' _cffi_methods,') + prnt(' NULL, NULL, NULL, NULL') + prnt('};') + prnt() + prnt('PyMODINIT_FUNC') + prnt('PyInit_%s(void)' % modname) + prnt('{') + prnt(' PyObject *lib;') + prnt(' lib = PyModule_Create(&_cffi_module_def);') + prnt(' if (lib == NULL)') + prnt(' return NULL;') + prnt(' if (%s < 0 || _cffi_init() < 0) {' % (constants,)) + prnt(' Py_DECREF(lib);') + prnt(' return NULL;') + prnt(' }') + prnt('#if Py_GIL_DISABLED') + prnt(' PyUnstable_Module_SetGIL(lib, Py_MOD_GIL_NOT_USED);') + prnt('#endif') + prnt(' return lib;') + prnt('}') + prnt() + + def load_library(self, flags=None): + # XXX review all usages of 'self' here! + # import it as a new extension module + imp.acquire_lock() + try: + if hasattr(sys, "getdlopenflags"): + previous_flags = sys.getdlopenflags() + try: + if hasattr(sys, "setdlopenflags") and flags is not None: + sys.setdlopenflags(flags) + module = imp.load_dynamic(self.verifier.get_module_name(), + self.verifier.modulefilename) + except ImportError as e: + error = "importing %r: %s" % (self.verifier.modulefilename, e) + raise VerificationError(error) + finally: + if hasattr(sys, "setdlopenflags"): + sys.setdlopenflags(previous_flags) + finally: + imp.release_lock() + # + # call loading_cpy_struct() to get the struct layout inferred by + # the C compiler + self._load(module, 'loading') + # + # the C code will need the objects. Collect them in + # order in a list. + revmapping = dict([(value, key) + for (key, value) in self._typesdict.items()]) + lst = [revmapping[i] for i in range(len(revmapping))] + lst = list(map(self.ffi._get_cached_btype, lst)) + # + # build the FFILibrary class and instance and call _cffi_setup(). + # this will set up some fields like '_cffi_types', and only then + # it will invoke the chained list of functions that will really + # build (notably) the constant objects, as if they are + # pointers, and store them as attributes on the 'library' object. + class FFILibrary: + _cffi_python_module = module + _cffi_ffi = self.ffi + _cffi_dir = [] + def __dir__(self): + return FFILibrary._cffi_dir + list(self.__dict__) + library = FFILibrary() + if module._cffi_setup(lst, VerificationError, library): + import warnings + warnings.warn("reimporting %r might overwrite older definitions" + % (self.verifier.get_module_name())) + # + # finally, call the loaded_cpy_xxx() functions. This will perform + # the final adjustments, like copying the Python->C wrapper + # functions from the module to the 'library' object, and setting + # up the FFILibrary class with properties for the global C variables. + self._load(module, 'loaded', library=library) + module._cffi_original_ffi = self.ffi + module._cffi_types_of_builtin_funcs = self._types_of_builtin_functions + return library + + def _get_declarations(self): + lst = [(key, tp) for (key, (tp, qual)) in + self.ffi._parser._declarations.items()] + lst.sort() + return lst + + def _generate(self, step_name): + for name, tp in self._get_declarations(): + kind, realname = name.split(' ', 1) + try: + method = getattr(self, '_generate_cpy_%s_%s' % (kind, + step_name)) + except AttributeError: + raise VerificationError( + "not implemented in verify(): %r" % name) + try: + method(tp, realname) + except Exception as e: + model.attach_exception_info(e, name) + raise + + def _load(self, module, step_name, **kwds): + for name, tp in self._get_declarations(): + kind, realname = name.split(' ', 1) + method = getattr(self, '_%s_cpy_%s' % (step_name, kind)) + try: + method(tp, realname, module, **kwds) + except Exception as e: + model.attach_exception_info(e, name) + raise + + def _generate_nothing(self, tp, name): + pass + + def _loaded_noop(self, tp, name, module, **kwds): + pass + + # ---------- + + def _convert_funcarg_to_c(self, tp, fromvar, tovar, errcode): + extraarg = '' + if isinstance(tp, model.PrimitiveType): + if tp.is_integer_type() and tp.name != '_Bool': + converter = '_cffi_to_c_int' + extraarg = ', %s' % tp.name + elif tp.is_complex_type(): + raise VerificationError( + "not implemented in verify(): complex types") + else: + converter = '(%s)_cffi_to_c_%s' % (tp.get_c_name(''), + tp.name.replace(' ', '_')) + errvalue = '-1' + # + elif isinstance(tp, model.PointerType): + self._convert_funcarg_to_c_ptr_or_array(tp, fromvar, + tovar, errcode) + return + # + elif isinstance(tp, (model.StructOrUnion, model.EnumType)): + # a struct (not a struct pointer) as a function argument + self._prnt(' if (_cffi_to_c((char *)&%s, _cffi_type(%d), %s) < 0)' + % (tovar, self._gettypenum(tp), fromvar)) + self._prnt(' %s;' % errcode) + return + # + elif isinstance(tp, model.FunctionPtrType): + converter = '(%s)_cffi_to_c_pointer' % tp.get_c_name('') + extraarg = ', _cffi_type(%d)' % self._gettypenum(tp) + errvalue = 'NULL' + # + else: + raise NotImplementedError(tp) + # + self._prnt(' %s = %s(%s%s);' % (tovar, converter, fromvar, extraarg)) + self._prnt(' if (%s == (%s)%s && PyErr_Occurred())' % ( + tovar, tp.get_c_name(''), errvalue)) + self._prnt(' %s;' % errcode) + + def _extra_local_variables(self, tp, localvars, freelines): + if isinstance(tp, model.PointerType): + localvars.add('Py_ssize_t datasize') + localvars.add('struct _cffi_freeme_s *large_args_free = NULL') + freelines.add('if (large_args_free != NULL)' + ' _cffi_free_array_arguments(large_args_free);') + + def _convert_funcarg_to_c_ptr_or_array(self, tp, fromvar, tovar, errcode): + self._prnt(' datasize = _cffi_prepare_pointer_call_argument(') + self._prnt(' _cffi_type(%d), %s, (char **)&%s);' % ( + self._gettypenum(tp), fromvar, tovar)) + self._prnt(' if (datasize != 0) {') + self._prnt(' %s = ((size_t)datasize) <= 640 ? ' + 'alloca((size_t)datasize) : NULL;' % (tovar,)) + self._prnt(' if (_cffi_convert_array_argument(_cffi_type(%d), %s, ' + '(char **)&%s,' % (self._gettypenum(tp), fromvar, tovar)) + self._prnt(' datasize, &large_args_free) < 0)') + self._prnt(' %s;' % errcode) + self._prnt(' }') + + def _convert_expr_from_c(self, tp, var, context): + if isinstance(tp, model.PrimitiveType): + if tp.is_integer_type() and tp.name != '_Bool': + return '_cffi_from_c_int(%s, %s)' % (var, tp.name) + elif tp.name != 'long double': + return '_cffi_from_c_%s(%s)' % (tp.name.replace(' ', '_'), var) + else: + return '_cffi_from_c_deref((char *)&%s, _cffi_type(%d))' % ( + var, self._gettypenum(tp)) + elif isinstance(tp, (model.PointerType, model.FunctionPtrType)): + return '_cffi_from_c_pointer((char *)%s, _cffi_type(%d))' % ( + var, self._gettypenum(tp)) + elif isinstance(tp, model.ArrayType): + return '_cffi_from_c_pointer((char *)%s, _cffi_type(%d))' % ( + var, self._gettypenum(model.PointerType(tp.item))) + elif isinstance(tp, model.StructOrUnion): + if tp.fldnames is None: + raise TypeError("'%s' is used as %s, but is opaque" % ( + tp._get_c_name(), context)) + return '_cffi_from_c_struct((char *)&%s, _cffi_type(%d))' % ( + var, self._gettypenum(tp)) + elif isinstance(tp, model.EnumType): + return '_cffi_from_c_deref((char *)&%s, _cffi_type(%d))' % ( + var, self._gettypenum(tp)) + else: + raise NotImplementedError(tp) + + # ---------- + # typedefs: generates no code so far + + _generate_cpy_typedef_collecttype = _generate_nothing + _generate_cpy_typedef_decl = _generate_nothing + _generate_cpy_typedef_method = _generate_nothing + _loading_cpy_typedef = _loaded_noop + _loaded_cpy_typedef = _loaded_noop + + # ---------- + # function declarations + + def _generate_cpy_function_collecttype(self, tp, name): + assert isinstance(tp, model.FunctionPtrType) + if tp.ellipsis: + self._do_collect_type(tp) + else: + # don't call _do_collect_type(tp) in this common case, + # otherwise test_autofilled_struct_as_argument fails + for type in tp.args: + self._do_collect_type(type) + self._do_collect_type(tp.result) + + def _generate_cpy_function_decl(self, tp, name): + assert isinstance(tp, model.FunctionPtrType) + if tp.ellipsis: + # cannot support vararg functions better than this: check for its + # exact type (including the fixed arguments), and build it as a + # constant function pointer (no CPython wrapper) + self._generate_cpy_const(False, name, tp) + return + prnt = self._prnt + numargs = len(tp.args) + if numargs == 0: + argname = 'noarg' + elif numargs == 1: + argname = 'arg0' + else: + argname = 'args' + prnt('static PyObject *') + prnt('_cffi_f_%s(PyObject *self, PyObject *%s)' % (name, argname)) + prnt('{') + # + context = 'argument of %s' % name + for i, type in enumerate(tp.args): + prnt(' %s;' % type.get_c_name(' x%d' % i, context)) + # + localvars = set() + freelines = set() + for type in tp.args: + self._extra_local_variables(type, localvars, freelines) + for decl in sorted(localvars): + prnt(' %s;' % (decl,)) + # + if not isinstance(tp.result, model.VoidType): + result_code = 'result = ' + context = 'result of %s' % name + prnt(' %s;' % tp.result.get_c_name(' result', context)) + prnt(' PyObject *pyresult;') + else: + result_code = '' + # + if len(tp.args) > 1: + rng = range(len(tp.args)) + for i in rng: + prnt(' PyObject *arg%d;' % i) + prnt() + prnt(' if (!PyArg_ParseTuple(args, "%s:%s", %s))' % ( + 'O' * numargs, name, ', '.join(['&arg%d' % i for i in rng]))) + prnt(' return NULL;') + prnt() + # + for i, type in enumerate(tp.args): + self._convert_funcarg_to_c(type, 'arg%d' % i, 'x%d' % i, + 'return NULL') + prnt() + # + prnt(' Py_BEGIN_ALLOW_THREADS') + prnt(' _cffi_restore_errno();') + prnt(' { %s%s(%s); }' % ( + result_code, name, + ', '.join(['x%d' % i for i in range(len(tp.args))]))) + prnt(' _cffi_save_errno();') + prnt(' Py_END_ALLOW_THREADS') + prnt() + # + prnt(' (void)self; /* unused */') + if numargs == 0: + prnt(' (void)noarg; /* unused */') + if result_code: + prnt(' pyresult = %s;' % + self._convert_expr_from_c(tp.result, 'result', 'result type')) + for freeline in freelines: + prnt(' ' + freeline) + prnt(' return pyresult;') + else: + for freeline in freelines: + prnt(' ' + freeline) + prnt(' Py_INCREF(Py_None);') + prnt(' return Py_None;') + prnt('}') + prnt() + + def _generate_cpy_function_method(self, tp, name): + if tp.ellipsis: + return + numargs = len(tp.args) + if numargs == 0: + meth = 'METH_NOARGS' + elif numargs == 1: + meth = 'METH_O' + else: + meth = 'METH_VARARGS' + self._prnt(' {"%s", _cffi_f_%s, %s, NULL},' % (name, name, meth)) + + _loading_cpy_function = _loaded_noop + + def _loaded_cpy_function(self, tp, name, module, library): + if tp.ellipsis: + return + func = getattr(module, name) + setattr(library, name, func) + self._types_of_builtin_functions[func] = tp + + # ---------- + # named structs + + _generate_cpy_struct_collecttype = _generate_nothing + def _generate_cpy_struct_decl(self, tp, name): + assert name == tp.name + self._generate_struct_or_union_decl(tp, 'struct', name) + def _generate_cpy_struct_method(self, tp, name): + self._generate_struct_or_union_method(tp, 'struct', name) + def _loading_cpy_struct(self, tp, name, module): + self._loading_struct_or_union(tp, 'struct', name, module) + def _loaded_cpy_struct(self, tp, name, module, **kwds): + self._loaded_struct_or_union(tp) + + _generate_cpy_union_collecttype = _generate_nothing + def _generate_cpy_union_decl(self, tp, name): + assert name == tp.name + self._generate_struct_or_union_decl(tp, 'union', name) + def _generate_cpy_union_method(self, tp, name): + self._generate_struct_or_union_method(tp, 'union', name) + def _loading_cpy_union(self, tp, name, module): + self._loading_struct_or_union(tp, 'union', name, module) + def _loaded_cpy_union(self, tp, name, module, **kwds): + self._loaded_struct_or_union(tp) + + def _generate_struct_or_union_decl(self, tp, prefix, name): + if tp.fldnames is None: + return # nothing to do with opaque structs + checkfuncname = '_cffi_check_%s_%s' % (prefix, name) + layoutfuncname = '_cffi_layout_%s_%s' % (prefix, name) + cname = ('%s %s' % (prefix, name)).strip() + # + prnt = self._prnt + prnt('static void %s(%s *p)' % (checkfuncname, cname)) + prnt('{') + prnt(' /* only to generate compile-time warnings or errors */') + prnt(' (void)p;') + for fname, ftype, fbitsize, fqual in tp.enumfields(): + if (isinstance(ftype, model.PrimitiveType) + and ftype.is_integer_type()) or fbitsize >= 0: + # accept all integers, but complain on float or double + prnt(' (void)((p->%s) << 1);' % fname) + else: + # only accept exactly the type declared. + try: + prnt(' { %s = &p->%s; (void)tmp; }' % ( + ftype.get_c_name('*tmp', 'field %r'%fname, quals=fqual), + fname)) + except VerificationError as e: + prnt(' /* %s */' % str(e)) # cannot verify it, ignore + prnt('}') + prnt('static PyObject *') + prnt('%s(PyObject *self, PyObject *noarg)' % (layoutfuncname,)) + prnt('{') + prnt(' struct _cffi_aligncheck { char x; %s y; };' % cname) + prnt(' static Py_ssize_t nums[] = {') + prnt(' sizeof(%s),' % cname) + prnt(' offsetof(struct _cffi_aligncheck, y),') + for fname, ftype, fbitsize, fqual in tp.enumfields(): + if fbitsize >= 0: + continue # xxx ignore fbitsize for now + prnt(' offsetof(%s, %s),' % (cname, fname)) + if isinstance(ftype, model.ArrayType) and ftype.length is None: + prnt(' 0, /* %s */' % ftype._get_c_name()) + else: + prnt(' sizeof(((%s *)0)->%s),' % (cname, fname)) + prnt(' -1') + prnt(' };') + prnt(' (void)self; /* unused */') + prnt(' (void)noarg; /* unused */') + prnt(' return _cffi_get_struct_layout(nums);') + prnt(' /* the next line is not executed, but compiled */') + prnt(' %s(0);' % (checkfuncname,)) + prnt('}') + prnt() + + def _generate_struct_or_union_method(self, tp, prefix, name): + if tp.fldnames is None: + return # nothing to do with opaque structs + layoutfuncname = '_cffi_layout_%s_%s' % (prefix, name) + self._prnt(' {"%s", %s, METH_NOARGS, NULL},' % (layoutfuncname, + layoutfuncname)) + + def _loading_struct_or_union(self, tp, prefix, name, module): + if tp.fldnames is None: + return # nothing to do with opaque structs + layoutfuncname = '_cffi_layout_%s_%s' % (prefix, name) + # + function = getattr(module, layoutfuncname) + layout = function() + if isinstance(tp, model.StructOrUnion) and tp.partial: + # use the function()'s sizes and offsets to guide the + # layout of the struct + totalsize = layout[0] + totalalignment = layout[1] + fieldofs = layout[2::2] + fieldsize = layout[3::2] + tp.force_flatten() + assert len(fieldofs) == len(fieldsize) == len(tp.fldnames) + tp.fixedlayout = fieldofs, fieldsize, totalsize, totalalignment + else: + cname = ('%s %s' % (prefix, name)).strip() + self._struct_pending_verification[tp] = layout, cname + + def _loaded_struct_or_union(self, tp): + if tp.fldnames is None: + return # nothing to do with opaque structs + self.ffi._get_cached_btype(tp) # force 'fixedlayout' to be considered + + if tp in self._struct_pending_verification: + # check that the layout sizes and offsets match the real ones + def check(realvalue, expectedvalue, msg): + if realvalue != expectedvalue: + raise VerificationError( + "%s (we have %d, but C compiler says %d)" + % (msg, expectedvalue, realvalue)) + ffi = self.ffi + BStruct = ffi._get_cached_btype(tp) + layout, cname = self._struct_pending_verification.pop(tp) + check(layout[0], ffi.sizeof(BStruct), "wrong total size") + check(layout[1], ffi.alignof(BStruct), "wrong total alignment") + i = 2 + for fname, ftype, fbitsize, fqual in tp.enumfields(): + if fbitsize >= 0: + continue # xxx ignore fbitsize for now + check(layout[i], ffi.offsetof(BStruct, fname), + "wrong offset for field %r" % (fname,)) + if layout[i+1] != 0: + BField = ffi._get_cached_btype(ftype) + check(layout[i+1], ffi.sizeof(BField), + "wrong size for field %r" % (fname,)) + i += 2 + assert i == len(layout) + + # ---------- + # 'anonymous' declarations. These are produced for anonymous structs + # or unions; the 'name' is obtained by a typedef. + + _generate_cpy_anonymous_collecttype = _generate_nothing + + def _generate_cpy_anonymous_decl(self, tp, name): + if isinstance(tp, model.EnumType): + self._generate_cpy_enum_decl(tp, name, '') + else: + self._generate_struct_or_union_decl(tp, '', name) + + def _generate_cpy_anonymous_method(self, tp, name): + if not isinstance(tp, model.EnumType): + self._generate_struct_or_union_method(tp, '', name) + + def _loading_cpy_anonymous(self, tp, name, module): + if isinstance(tp, model.EnumType): + self._loading_cpy_enum(tp, name, module) + else: + self._loading_struct_or_union(tp, '', name, module) + + def _loaded_cpy_anonymous(self, tp, name, module, **kwds): + if isinstance(tp, model.EnumType): + self._loaded_cpy_enum(tp, name, module, **kwds) + else: + self._loaded_struct_or_union(tp) + + # ---------- + # constants, likely declared with '#define' + + def _generate_cpy_const(self, is_int, name, tp=None, category='const', + vartp=None, delayed=True, size_too=False, + check_value=None): + prnt = self._prnt + funcname = '_cffi_%s_%s' % (category, name) + prnt('static int %s(PyObject *lib)' % funcname) + prnt('{') + prnt(' PyObject *o;') + prnt(' int res;') + if not is_int: + prnt(' %s;' % (vartp or tp).get_c_name(' i', name)) + else: + assert category == 'const' + # + if check_value is not None: + self._check_int_constant_value(name, check_value) + # + if not is_int: + if category == 'var': + realexpr = '&' + name + else: + realexpr = name + prnt(' i = (%s);' % (realexpr,)) + prnt(' o = %s;' % (self._convert_expr_from_c(tp, 'i', + 'variable type'),)) + assert delayed + else: + prnt(' o = _cffi_from_c_int_const(%s);' % name) + prnt(' if (o == NULL)') + prnt(' return -1;') + if size_too: + prnt(' {') + prnt(' PyObject *o1 = o;') + prnt(' o = Py_BuildValue("On", o1, (Py_ssize_t)sizeof(%s));' + % (name,)) + prnt(' Py_DECREF(o1);') + prnt(' if (o == NULL)') + prnt(' return -1;') + prnt(' }') + prnt(' res = PyObject_SetAttrString(lib, "%s", o);' % name) + prnt(' Py_DECREF(o);') + prnt(' if (res < 0)') + prnt(' return -1;') + prnt(' return %s;' % self._chained_list_constants[delayed]) + self._chained_list_constants[delayed] = funcname + '(lib)' + prnt('}') + prnt() + + def _generate_cpy_constant_collecttype(self, tp, name): + is_int = isinstance(tp, model.PrimitiveType) and tp.is_integer_type() + if not is_int: + self._do_collect_type(tp) + + def _generate_cpy_constant_decl(self, tp, name): + is_int = isinstance(tp, model.PrimitiveType) and tp.is_integer_type() + self._generate_cpy_const(is_int, name, tp) + + _generate_cpy_constant_method = _generate_nothing + _loading_cpy_constant = _loaded_noop + _loaded_cpy_constant = _loaded_noop + + # ---------- + # enums + + def _check_int_constant_value(self, name, value, err_prefix=''): + prnt = self._prnt + if value <= 0: + prnt(' if ((%s) > 0 || (long)(%s) != %dL) {' % ( + name, name, value)) + else: + prnt(' if ((%s) <= 0 || (unsigned long)(%s) != %dUL) {' % ( + name, name, value)) + prnt(' char buf[64];') + prnt(' if ((%s) <= 0)' % name) + prnt(' snprintf(buf, 63, "%%ld", (long)(%s));' % name) + prnt(' else') + prnt(' snprintf(buf, 63, "%%lu", (unsigned long)(%s));' % + name) + prnt(' PyErr_Format(_cffi_VerificationError,') + prnt(' "%s%s has the real value %s, not %s",') + prnt(' "%s", "%s", buf, "%d");' % ( + err_prefix, name, value)) + prnt(' return -1;') + prnt(' }') + + def _enum_funcname(self, prefix, name): + # "$enum_$1" => "___D_enum____D_1" + name = name.replace('$', '___D_') + return '_cffi_e_%s_%s' % (prefix, name) + + def _generate_cpy_enum_decl(self, tp, name, prefix='enum'): + if tp.partial: + for enumerator in tp.enumerators: + self._generate_cpy_const(True, enumerator, delayed=False) + return + # + funcname = self._enum_funcname(prefix, name) + prnt = self._prnt + prnt('static int %s(PyObject *lib)' % funcname) + prnt('{') + for enumerator, enumvalue in zip(tp.enumerators, tp.enumvalues): + self._check_int_constant_value(enumerator, enumvalue, + "enum %s: " % name) + prnt(' return %s;' % self._chained_list_constants[True]) + self._chained_list_constants[True] = funcname + '(lib)' + prnt('}') + prnt() + + _generate_cpy_enum_collecttype = _generate_nothing + _generate_cpy_enum_method = _generate_nothing + + def _loading_cpy_enum(self, tp, name, module): + if tp.partial: + enumvalues = [getattr(module, enumerator) + for enumerator in tp.enumerators] + tp.enumvalues = tuple(enumvalues) + tp.partial_resolved = True + + def _loaded_cpy_enum(self, tp, name, module, library): + for enumerator, enumvalue in zip(tp.enumerators, tp.enumvalues): + setattr(library, enumerator, enumvalue) + + # ---------- + # macros: for now only for integers + + def _generate_cpy_macro_decl(self, tp, name): + if tp == '...': + check_value = None + else: + check_value = tp # an integer + self._generate_cpy_const(True, name, check_value=check_value) + + _generate_cpy_macro_collecttype = _generate_nothing + _generate_cpy_macro_method = _generate_nothing + _loading_cpy_macro = _loaded_noop + _loaded_cpy_macro = _loaded_noop + + # ---------- + # global variables + + def _generate_cpy_variable_collecttype(self, tp, name): + if isinstance(tp, model.ArrayType): + tp_ptr = model.PointerType(tp.item) + else: + tp_ptr = model.PointerType(tp) + self._do_collect_type(tp_ptr) + + def _generate_cpy_variable_decl(self, tp, name): + if isinstance(tp, model.ArrayType): + tp_ptr = model.PointerType(tp.item) + self._generate_cpy_const(False, name, tp, vartp=tp_ptr, + size_too = tp.length_is_unknown()) + else: + tp_ptr = model.PointerType(tp) + self._generate_cpy_const(False, name, tp_ptr, category='var') + + _generate_cpy_variable_method = _generate_nothing + _loading_cpy_variable = _loaded_noop + + def _loaded_cpy_variable(self, tp, name, module, library): + value = getattr(library, name) + if isinstance(tp, model.ArrayType): # int a[5] is "constant" in the + # sense that "a=..." is forbidden + if tp.length_is_unknown(): + assert isinstance(value, tuple) + (value, size) = value + BItemType = self.ffi._get_cached_btype(tp.item) + length, rest = divmod(size, self.ffi.sizeof(BItemType)) + if rest != 0: + raise VerificationError( + "bad size: %r does not seem to be an array of %s" % + (name, tp.item)) + tp = tp.resolve_length(length) + # 'value' is a which we have to replace with + # a if the N is actually known + if tp.length is not None: + BArray = self.ffi._get_cached_btype(tp) + value = self.ffi.cast(BArray, value) + setattr(library, name, value) + return + # remove ptr= from the library instance, and replace + # it by a property on the class, which reads/writes into ptr[0]. + ptr = value + delattr(library, name) + def getter(library): + return ptr[0] + def setter(library, value): + ptr[0] = value + setattr(type(library), name, property(getter, setter)) + type(library)._cffi_dir.append(name) + + # ---------- + + def _generate_setup_custom(self): + prnt = self._prnt + prnt('static int _cffi_setup_custom(PyObject *lib)') + prnt('{') + prnt(' return %s;' % self._chained_list_constants[True]) + prnt('}') + +cffimod_header = r''' +#include +#include + +/* this block of #ifs should be kept exactly identical between + c/_cffi_backend.c, cffi/vengine_cpy.py, cffi/vengine_gen.py + and cffi/_cffi_include.h */ +#if defined(_MSC_VER) +# include /* for alloca() */ +# if _MSC_VER < 1600 /* MSVC < 2010 */ + typedef __int8 int8_t; + typedef __int16 int16_t; + typedef __int32 int32_t; + typedef __int64 int64_t; + typedef unsigned __int8 uint8_t; + typedef unsigned __int16 uint16_t; + typedef unsigned __int32 uint32_t; + typedef unsigned __int64 uint64_t; + typedef __int8 int_least8_t; + typedef __int16 int_least16_t; + typedef __int32 int_least32_t; + typedef __int64 int_least64_t; + typedef unsigned __int8 uint_least8_t; + typedef unsigned __int16 uint_least16_t; + typedef unsigned __int32 uint_least32_t; + typedef unsigned __int64 uint_least64_t; + typedef __int8 int_fast8_t; + typedef __int16 int_fast16_t; + typedef __int32 int_fast32_t; + typedef __int64 int_fast64_t; + typedef unsigned __int8 uint_fast8_t; + typedef unsigned __int16 uint_fast16_t; + typedef unsigned __int32 uint_fast32_t; + typedef unsigned __int64 uint_fast64_t; + typedef __int64 intmax_t; + typedef unsigned __int64 uintmax_t; +# else +# include +# endif +# if _MSC_VER < 1800 /* MSVC < 2013 */ +# ifndef __cplusplus + typedef unsigned char _Bool; +# endif +# endif +# define _cffi_float_complex_t _Fcomplex /* include for it */ +# define _cffi_double_complex_t _Dcomplex /* include for it */ +#else +# include +# if (defined (__SVR4) && defined (__sun)) || defined(_AIX) || defined(__hpux) +# include +# endif +# define _cffi_float_complex_t float _Complex +# define _cffi_double_complex_t double _Complex +#endif + +#define _cffi_from_c_double PyFloat_FromDouble +#define _cffi_from_c_float PyFloat_FromDouble +#define _cffi_from_c_long PyLong_FromLong +#define _cffi_from_c_ulong PyLong_FromUnsignedLong +#define _cffi_from_c_longlong PyLong_FromLongLong +#define _cffi_from_c_ulonglong PyLong_FromUnsignedLongLong +#define _cffi_from_c__Bool PyBool_FromLong + +#define _cffi_to_c_double PyFloat_AsDouble +#define _cffi_to_c_float PyFloat_AsDouble + +#define _cffi_from_c_int_const(x) \ + (((x) > 0) ? \ + ((unsigned long long)(x) <= (unsigned long long)LONG_MAX) ? \ + PyLong_FromLong((long)(x)) : \ + PyLong_FromUnsignedLongLong((unsigned long long)(x)) : \ + ((long long)(x) >= (long long)LONG_MIN) ? \ + PyLong_FromLong((long)(x)) : \ + PyLong_FromLongLong((long long)(x))) + +#define _cffi_from_c_int(x, type) \ + (((type)-1) > 0 ? /* unsigned */ \ + (sizeof(type) < sizeof(long) ? \ + PyLong_FromLong((long)x) : \ + sizeof(type) == sizeof(long) ? \ + PyLong_FromUnsignedLong((unsigned long)x) : \ + PyLong_FromUnsignedLongLong((unsigned long long)x)) : \ + (sizeof(type) <= sizeof(long) ? \ + PyLong_FromLong((long)x) : \ + PyLong_FromLongLong((long long)x))) + +#define _cffi_to_c_int(o, type) \ + ((type)( \ + sizeof(type) == 1 ? (((type)-1) > 0 ? (type)_cffi_to_c_u8(o) \ + : (type)_cffi_to_c_i8(o)) : \ + sizeof(type) == 2 ? (((type)-1) > 0 ? (type)_cffi_to_c_u16(o) \ + : (type)_cffi_to_c_i16(o)) : \ + sizeof(type) == 4 ? (((type)-1) > 0 ? (type)_cffi_to_c_u32(o) \ + : (type)_cffi_to_c_i32(o)) : \ + sizeof(type) == 8 ? (((type)-1) > 0 ? (type)_cffi_to_c_u64(o) \ + : (type)_cffi_to_c_i64(o)) : \ + (Py_FatalError("unsupported size for type " #type), (type)0))) + +#define _cffi_to_c_i8 \ + ((int(*)(PyObject *))_cffi_exports[1]) +#define _cffi_to_c_u8 \ + ((int(*)(PyObject *))_cffi_exports[2]) +#define _cffi_to_c_i16 \ + ((int(*)(PyObject *))_cffi_exports[3]) +#define _cffi_to_c_u16 \ + ((int(*)(PyObject *))_cffi_exports[4]) +#define _cffi_to_c_i32 \ + ((int(*)(PyObject *))_cffi_exports[5]) +#define _cffi_to_c_u32 \ + ((unsigned int(*)(PyObject *))_cffi_exports[6]) +#define _cffi_to_c_i64 \ + ((long long(*)(PyObject *))_cffi_exports[7]) +#define _cffi_to_c_u64 \ + ((unsigned long long(*)(PyObject *))_cffi_exports[8]) +#define _cffi_to_c_char \ + ((int(*)(PyObject *))_cffi_exports[9]) +#define _cffi_from_c_pointer \ + ((PyObject *(*)(char *, CTypeDescrObject *))_cffi_exports[10]) +#define _cffi_to_c_pointer \ + ((char *(*)(PyObject *, CTypeDescrObject *))_cffi_exports[11]) +#define _cffi_get_struct_layout \ + ((PyObject *(*)(Py_ssize_t[]))_cffi_exports[12]) +#define _cffi_restore_errno \ + ((void(*)(void))_cffi_exports[13]) +#define _cffi_save_errno \ + ((void(*)(void))_cffi_exports[14]) +#define _cffi_from_c_char \ + ((PyObject *(*)(char))_cffi_exports[15]) +#define _cffi_from_c_deref \ + ((PyObject *(*)(char *, CTypeDescrObject *))_cffi_exports[16]) +#define _cffi_to_c \ + ((int(*)(char *, CTypeDescrObject *, PyObject *))_cffi_exports[17]) +#define _cffi_from_c_struct \ + ((PyObject *(*)(char *, CTypeDescrObject *))_cffi_exports[18]) +#define _cffi_to_c_wchar_t \ + ((wchar_t(*)(PyObject *))_cffi_exports[19]) +#define _cffi_from_c_wchar_t \ + ((PyObject *(*)(wchar_t))_cffi_exports[20]) +#define _cffi_to_c_long_double \ + ((long double(*)(PyObject *))_cffi_exports[21]) +#define _cffi_to_c__Bool \ + ((_Bool(*)(PyObject *))_cffi_exports[22]) +#define _cffi_prepare_pointer_call_argument \ + ((Py_ssize_t(*)(CTypeDescrObject *, PyObject *, char **))_cffi_exports[23]) +#define _cffi_convert_array_from_object \ + ((int(*)(char *, CTypeDescrObject *, PyObject *))_cffi_exports[24]) +#define _CFFI_NUM_EXPORTS 25 + +typedef struct _ctypedescr CTypeDescrObject; + +static void *_cffi_exports[_CFFI_NUM_EXPORTS]; +static PyObject *_cffi_types, *_cffi_VerificationError; + +static int _cffi_setup_custom(PyObject *lib); /* forward */ + +static PyObject *_cffi_setup(PyObject *self, PyObject *args) +{ + PyObject *library; + int was_alive = (_cffi_types != NULL); + (void)self; /* unused */ + if (!PyArg_ParseTuple(args, "OOO", &_cffi_types, &_cffi_VerificationError, + &library)) + return NULL; + Py_INCREF(_cffi_types); + Py_INCREF(_cffi_VerificationError); + if (_cffi_setup_custom(library) < 0) + return NULL; + return PyBool_FromLong(was_alive); +} + +union _cffi_union_alignment_u { + unsigned char m_char; + unsigned short m_short; + unsigned int m_int; + unsigned long m_long; + unsigned long long m_longlong; + float m_float; + double m_double; + long double m_longdouble; +}; + +struct _cffi_freeme_s { + struct _cffi_freeme_s *next; + union _cffi_union_alignment_u alignment; +}; + +#ifdef __GNUC__ + __attribute__((unused)) +#endif +static int _cffi_convert_array_argument(CTypeDescrObject *ctptr, PyObject *arg, + char **output_data, Py_ssize_t datasize, + struct _cffi_freeme_s **freeme) +{ + char *p; + if (datasize < 0) + return -1; + + p = *output_data; + if (p == NULL) { + struct _cffi_freeme_s *fp = (struct _cffi_freeme_s *)PyObject_Malloc( + offsetof(struct _cffi_freeme_s, alignment) + (size_t)datasize); + if (fp == NULL) + return -1; + fp->next = *freeme; + *freeme = fp; + p = *output_data = (char *)&fp->alignment; + } + memset((void *)p, 0, (size_t)datasize); + return _cffi_convert_array_from_object(p, ctptr, arg); +} + +#ifdef __GNUC__ + __attribute__((unused)) +#endif +static void _cffi_free_array_arguments(struct _cffi_freeme_s *freeme) +{ + do { + void *p = (void *)freeme; + freeme = freeme->next; + PyObject_Free(p); + } while (freeme != NULL); +} + +static int _cffi_init(void) +{ + PyObject *module, *c_api_object = NULL; + + module = PyImport_ImportModule("_cffi_backend"); + if (module == NULL) + goto failure; + + c_api_object = PyObject_GetAttrString(module, "_C_API"); + if (c_api_object == NULL) + goto failure; + if (!PyCapsule_CheckExact(c_api_object)) { + PyErr_SetNone(PyExc_ImportError); + goto failure; + } + memcpy(_cffi_exports, PyCapsule_GetPointer(c_api_object, "cffi"), + _CFFI_NUM_EXPORTS * sizeof(void *)); + + Py_DECREF(module); + Py_DECREF(c_api_object); + return 0; + + failure: + Py_XDECREF(module); + Py_XDECREF(c_api_object); + return -1; +} + +#define _cffi_type(num) ((CTypeDescrObject *)PyList_GET_ITEM(_cffi_types, num)) + +/**********/ +''' diff --git a/source/target-vendors/cp312/cffi/vengine_gen.py b/source/target-vendors/cp312/cffi/vengine_gen.py new file mode 100644 index 0000000..0209c79 --- /dev/null +++ b/source/target-vendors/cp312/cffi/vengine_gen.py @@ -0,0 +1,679 @@ +# +# DEPRECATED: implementation for ffi.verify() +# +import sys, os +import types + +from . import model +from .error import VerificationError + + +class VGenericEngine: + _class_key = 'g' + _gen_python_module = False + + def __init__(self, verifier): + self.verifier = verifier + self.ffi = verifier.ffi + self.export_symbols = [] + self._struct_pending_verification = {} + + def patch_extension_kwds(self, kwds): + # add 'export_symbols' to the dictionary. Note that we add the + # list before filling it. When we fill it, it will thus also show + # up in kwds['export_symbols']. + kwds.setdefault('export_symbols', self.export_symbols) + + def find_module(self, module_name, path, so_suffixes): + for so_suffix in so_suffixes: + basename = module_name + so_suffix + if path is None: + path = sys.path + for dirname in path: + filename = os.path.join(dirname, basename) + if os.path.isfile(filename): + return filename + + def collect_types(self): + pass # not needed in the generic engine + + def _prnt(self, what=''): + self._f.write(what + '\n') + + def write_source_to_f(self): + prnt = self._prnt + # first paste some standard set of lines that are mostly '#include' + prnt(cffimod_header) + # then paste the C source given by the user, verbatim. + prnt(self.verifier.preamble) + # + # call generate_gen_xxx_decl(), for every xxx found from + # ffi._parser._declarations. This generates all the functions. + self._generate('decl') + # + # on Windows, distutils insists on putting init_cffi_xyz in + # 'export_symbols', so instead of fighting it, just give up and + # give it one + if sys.platform == 'win32': + if sys.version_info >= (3,): + prefix = 'PyInit_' + else: + prefix = 'init' + modname = self.verifier.get_module_name() + prnt("void %s%s(void) { }\n" % (prefix, modname)) + + def load_library(self, flags=0): + # import it with the CFFI backend + backend = self.ffi._backend + # needs to make a path that contains '/', on Posix + filename = os.path.join(os.curdir, self.verifier.modulefilename) + module = backend.load_library(filename, flags) + # + # call loading_gen_struct() to get the struct layout inferred by + # the C compiler + self._load(module, 'loading') + + # build the FFILibrary class and instance, this is a module subclass + # because modules are expected to have usually-constant-attributes and + # in PyPy this means the JIT is able to treat attributes as constant, + # which we want. + class FFILibrary(types.ModuleType): + _cffi_generic_module = module + _cffi_ffi = self.ffi + _cffi_dir = [] + def __dir__(self): + return FFILibrary._cffi_dir + library = FFILibrary("") + # + # finally, call the loaded_gen_xxx() functions. This will set + # up the 'library' object. + self._load(module, 'loaded', library=library) + return library + + def _get_declarations(self): + lst = [(key, tp) for (key, (tp, qual)) in + self.ffi._parser._declarations.items()] + lst.sort() + return lst + + def _generate(self, step_name): + for name, tp in self._get_declarations(): + kind, realname = name.split(' ', 1) + try: + method = getattr(self, '_generate_gen_%s_%s' % (kind, + step_name)) + except AttributeError: + raise VerificationError( + "not implemented in verify(): %r" % name) + try: + method(tp, realname) + except Exception as e: + model.attach_exception_info(e, name) + raise + + def _load(self, module, step_name, **kwds): + for name, tp in self._get_declarations(): + kind, realname = name.split(' ', 1) + method = getattr(self, '_%s_gen_%s' % (step_name, kind)) + try: + method(tp, realname, module, **kwds) + except Exception as e: + model.attach_exception_info(e, name) + raise + + def _generate_nothing(self, tp, name): + pass + + def _loaded_noop(self, tp, name, module, **kwds): + pass + + # ---------- + # typedefs: generates no code so far + + _generate_gen_typedef_decl = _generate_nothing + _loading_gen_typedef = _loaded_noop + _loaded_gen_typedef = _loaded_noop + + # ---------- + # function declarations + + def _generate_gen_function_decl(self, tp, name): + assert isinstance(tp, model.FunctionPtrType) + if tp.ellipsis: + # cannot support vararg functions better than this: check for its + # exact type (including the fixed arguments), and build it as a + # constant function pointer (no _cffi_f_%s wrapper) + self._generate_gen_const(False, name, tp) + return + prnt = self._prnt + numargs = len(tp.args) + argnames = [] + for i, type in enumerate(tp.args): + indirection = '' + if isinstance(type, model.StructOrUnion): + indirection = '*' + argnames.append('%sx%d' % (indirection, i)) + context = 'argument of %s' % name + arglist = [type.get_c_name(' %s' % arg, context) + for type, arg in zip(tp.args, argnames)] + tpresult = tp.result + if isinstance(tpresult, model.StructOrUnion): + arglist.insert(0, tpresult.get_c_name(' *r', context)) + tpresult = model.void_type + arglist = ', '.join(arglist) or 'void' + wrappername = '_cffi_f_%s' % name + self.export_symbols.append(wrappername) + if tp.abi: + abi = tp.abi + ' ' + else: + abi = '' + funcdecl = ' %s%s(%s)' % (abi, wrappername, arglist) + context = 'result of %s' % name + prnt(tpresult.get_c_name(funcdecl, context)) + prnt('{') + # + if isinstance(tp.result, model.StructOrUnion): + result_code = '*r = ' + elif not isinstance(tp.result, model.VoidType): + result_code = 'return ' + else: + result_code = '' + prnt(' %s%s(%s);' % (result_code, name, ', '.join(argnames))) + prnt('}') + prnt() + + _loading_gen_function = _loaded_noop + + def _loaded_gen_function(self, tp, name, module, library): + assert isinstance(tp, model.FunctionPtrType) + if tp.ellipsis: + newfunction = self._load_constant(False, tp, name, module) + else: + indirections = [] + base_tp = tp + if (any(isinstance(typ, model.StructOrUnion) for typ in tp.args) + or isinstance(tp.result, model.StructOrUnion)): + indirect_args = [] + for i, typ in enumerate(tp.args): + if isinstance(typ, model.StructOrUnion): + typ = model.PointerType(typ) + indirections.append((i, typ)) + indirect_args.append(typ) + indirect_result = tp.result + if isinstance(indirect_result, model.StructOrUnion): + if indirect_result.fldtypes is None: + raise TypeError("'%s' is used as result type, " + "but is opaque" % ( + indirect_result._get_c_name(),)) + indirect_result = model.PointerType(indirect_result) + indirect_args.insert(0, indirect_result) + indirections.insert(0, ("result", indirect_result)) + indirect_result = model.void_type + tp = model.FunctionPtrType(tuple(indirect_args), + indirect_result, tp.ellipsis) + BFunc = self.ffi._get_cached_btype(tp) + wrappername = '_cffi_f_%s' % name + newfunction = module.load_function(BFunc, wrappername) + for i, typ in indirections: + newfunction = self._make_struct_wrapper(newfunction, i, typ, + base_tp) + setattr(library, name, newfunction) + type(library)._cffi_dir.append(name) + + def _make_struct_wrapper(self, oldfunc, i, tp, base_tp): + backend = self.ffi._backend + BType = self.ffi._get_cached_btype(tp) + if i == "result": + ffi = self.ffi + def newfunc(*args): + res = ffi.new(BType) + oldfunc(res, *args) + return res[0] + else: + def newfunc(*args): + args = args[:i] + (backend.newp(BType, args[i]),) + args[i+1:] + return oldfunc(*args) + newfunc._cffi_base_type = base_tp + return newfunc + + # ---------- + # named structs + + def _generate_gen_struct_decl(self, tp, name): + assert name == tp.name + self._generate_struct_or_union_decl(tp, 'struct', name) + + def _loading_gen_struct(self, tp, name, module): + self._loading_struct_or_union(tp, 'struct', name, module) + + def _loaded_gen_struct(self, tp, name, module, **kwds): + self._loaded_struct_or_union(tp) + + def _generate_gen_union_decl(self, tp, name): + assert name == tp.name + self._generate_struct_or_union_decl(tp, 'union', name) + + def _loading_gen_union(self, tp, name, module): + self._loading_struct_or_union(tp, 'union', name, module) + + def _loaded_gen_union(self, tp, name, module, **kwds): + self._loaded_struct_or_union(tp) + + def _generate_struct_or_union_decl(self, tp, prefix, name): + if tp.fldnames is None: + return # nothing to do with opaque structs + checkfuncname = '_cffi_check_%s_%s' % (prefix, name) + layoutfuncname = '_cffi_layout_%s_%s' % (prefix, name) + cname = ('%s %s' % (prefix, name)).strip() + # + prnt = self._prnt + prnt('static void %s(%s *p)' % (checkfuncname, cname)) + prnt('{') + prnt(' /* only to generate compile-time warnings or errors */') + prnt(' (void)p;') + for fname, ftype, fbitsize, fqual in tp.enumfields(): + if (isinstance(ftype, model.PrimitiveType) + and ftype.is_integer_type()) or fbitsize >= 0: + # accept all integers, but complain on float or double + prnt(' (void)((p->%s) << 1);' % fname) + else: + # only accept exactly the type declared. + try: + prnt(' { %s = &p->%s; (void)tmp; }' % ( + ftype.get_c_name('*tmp', 'field %r'%fname, quals=fqual), + fname)) + except VerificationError as e: + prnt(' /* %s */' % str(e)) # cannot verify it, ignore + prnt('}') + self.export_symbols.append(layoutfuncname) + prnt('intptr_t %s(intptr_t i)' % (layoutfuncname,)) + prnt('{') + prnt(' struct _cffi_aligncheck { char x; %s y; };' % cname) + prnt(' static intptr_t nums[] = {') + prnt(' sizeof(%s),' % cname) + prnt(' offsetof(struct _cffi_aligncheck, y),') + for fname, ftype, fbitsize, fqual in tp.enumfields(): + if fbitsize >= 0: + continue # xxx ignore fbitsize for now + prnt(' offsetof(%s, %s),' % (cname, fname)) + if isinstance(ftype, model.ArrayType) and ftype.length is None: + prnt(' 0, /* %s */' % ftype._get_c_name()) + else: + prnt(' sizeof(((%s *)0)->%s),' % (cname, fname)) + prnt(' -1') + prnt(' };') + prnt(' return nums[i];') + prnt(' /* the next line is not executed, but compiled */') + prnt(' %s(0);' % (checkfuncname,)) + prnt('}') + prnt() + + def _loading_struct_or_union(self, tp, prefix, name, module): + if tp.fldnames is None: + return # nothing to do with opaque structs + layoutfuncname = '_cffi_layout_%s_%s' % (prefix, name) + # + BFunc = self.ffi._typeof_locked("intptr_t(*)(intptr_t)")[0] + function = module.load_function(BFunc, layoutfuncname) + layout = [] + num = 0 + while True: + x = function(num) + if x < 0: break + layout.append(x) + num += 1 + if isinstance(tp, model.StructOrUnion) and tp.partial: + # use the function()'s sizes and offsets to guide the + # layout of the struct + totalsize = layout[0] + totalalignment = layout[1] + fieldofs = layout[2::2] + fieldsize = layout[3::2] + tp.force_flatten() + assert len(fieldofs) == len(fieldsize) == len(tp.fldnames) + tp.fixedlayout = fieldofs, fieldsize, totalsize, totalalignment + else: + cname = ('%s %s' % (prefix, name)).strip() + self._struct_pending_verification[tp] = layout, cname + + def _loaded_struct_or_union(self, tp): + if tp.fldnames is None: + return # nothing to do with opaque structs + self.ffi._get_cached_btype(tp) # force 'fixedlayout' to be considered + + if tp in self._struct_pending_verification: + # check that the layout sizes and offsets match the real ones + def check(realvalue, expectedvalue, msg): + if realvalue != expectedvalue: + raise VerificationError( + "%s (we have %d, but C compiler says %d)" + % (msg, expectedvalue, realvalue)) + ffi = self.ffi + BStruct = ffi._get_cached_btype(tp) + layout, cname = self._struct_pending_verification.pop(tp) + check(layout[0], ffi.sizeof(BStruct), "wrong total size") + check(layout[1], ffi.alignof(BStruct), "wrong total alignment") + i = 2 + for fname, ftype, fbitsize, fqual in tp.enumfields(): + if fbitsize >= 0: + continue # xxx ignore fbitsize for now + check(layout[i], ffi.offsetof(BStruct, fname), + "wrong offset for field %r" % (fname,)) + if layout[i+1] != 0: + BField = ffi._get_cached_btype(ftype) + check(layout[i+1], ffi.sizeof(BField), + "wrong size for field %r" % (fname,)) + i += 2 + assert i == len(layout) + + # ---------- + # 'anonymous' declarations. These are produced for anonymous structs + # or unions; the 'name' is obtained by a typedef. + + def _generate_gen_anonymous_decl(self, tp, name): + if isinstance(tp, model.EnumType): + self._generate_gen_enum_decl(tp, name, '') + else: + self._generate_struct_or_union_decl(tp, '', name) + + def _loading_gen_anonymous(self, tp, name, module): + if isinstance(tp, model.EnumType): + self._loading_gen_enum(tp, name, module, '') + else: + self._loading_struct_or_union(tp, '', name, module) + + def _loaded_gen_anonymous(self, tp, name, module, **kwds): + if isinstance(tp, model.EnumType): + self._loaded_gen_enum(tp, name, module, **kwds) + else: + self._loaded_struct_or_union(tp) + + # ---------- + # constants, likely declared with '#define' + + def _generate_gen_const(self, is_int, name, tp=None, category='const', + check_value=None): + prnt = self._prnt + funcname = '_cffi_%s_%s' % (category, name) + self.export_symbols.append(funcname) + if check_value is not None: + assert is_int + assert category == 'const' + prnt('int %s(char *out_error)' % funcname) + prnt('{') + self._check_int_constant_value(name, check_value) + prnt(' return 0;') + prnt('}') + elif is_int: + assert category == 'const' + prnt('int %s(long long *out_value)' % funcname) + prnt('{') + prnt(' *out_value = (long long)(%s);' % (name,)) + prnt(' return (%s) <= 0;' % (name,)) + prnt('}') + else: + assert tp is not None + assert check_value is None + if category == 'var': + ampersand = '&' + else: + ampersand = '' + extra = '' + if category == 'const' and isinstance(tp, model.StructOrUnion): + extra = 'const *' + ampersand = '&' + prnt(tp.get_c_name(' %s%s(void)' % (extra, funcname), name)) + prnt('{') + prnt(' return (%s%s);' % (ampersand, name)) + prnt('}') + prnt() + + def _generate_gen_constant_decl(self, tp, name): + is_int = isinstance(tp, model.PrimitiveType) and tp.is_integer_type() + self._generate_gen_const(is_int, name, tp) + + _loading_gen_constant = _loaded_noop + + def _load_constant(self, is_int, tp, name, module, check_value=None): + funcname = '_cffi_const_%s' % name + if check_value is not None: + assert is_int + self._load_known_int_constant(module, funcname) + value = check_value + elif is_int: + BType = self.ffi._typeof_locked("long long*")[0] + BFunc = self.ffi._typeof_locked("int(*)(long long*)")[0] + function = module.load_function(BFunc, funcname) + p = self.ffi.new(BType) + negative = function(p) + value = int(p[0]) + if value < 0 and not negative: + BLongLong = self.ffi._typeof_locked("long long")[0] + value += (1 << (8*self.ffi.sizeof(BLongLong))) + else: + assert check_value is None + fntypeextra = '(*)(void)' + if isinstance(tp, model.StructOrUnion): + fntypeextra = '*' + fntypeextra + BFunc = self.ffi._typeof_locked(tp.get_c_name(fntypeextra, name))[0] + function = module.load_function(BFunc, funcname) + value = function() + if isinstance(tp, model.StructOrUnion): + value = value[0] + return value + + def _loaded_gen_constant(self, tp, name, module, library): + is_int = isinstance(tp, model.PrimitiveType) and tp.is_integer_type() + value = self._load_constant(is_int, tp, name, module) + setattr(library, name, value) + type(library)._cffi_dir.append(name) + + # ---------- + # enums + + def _check_int_constant_value(self, name, value): + prnt = self._prnt + if value <= 0: + prnt(' if ((%s) > 0 || (long)(%s) != %dL) {' % ( + name, name, value)) + else: + prnt(' if ((%s) <= 0 || (unsigned long)(%s) != %dUL) {' % ( + name, name, value)) + prnt(' char buf[64];') + prnt(' if ((%s) <= 0)' % name) + prnt(' sprintf(buf, "%%ld", (long)(%s));' % name) + prnt(' else') + prnt(' sprintf(buf, "%%lu", (unsigned long)(%s));' % + name) + prnt(' sprintf(out_error, "%s has the real value %s, not %s",') + prnt(' "%s", buf, "%d");' % (name[:100], value)) + prnt(' return -1;') + prnt(' }') + + def _load_known_int_constant(self, module, funcname): + BType = self.ffi._typeof_locked("char[]")[0] + BFunc = self.ffi._typeof_locked("int(*)(char*)")[0] + function = module.load_function(BFunc, funcname) + p = self.ffi.new(BType, 256) + if function(p) < 0: + error = self.ffi.string(p) + if sys.version_info >= (3,): + error = str(error, 'utf-8') + raise VerificationError(error) + + def _enum_funcname(self, prefix, name): + # "$enum_$1" => "___D_enum____D_1" + name = name.replace('$', '___D_') + return '_cffi_e_%s_%s' % (prefix, name) + + def _generate_gen_enum_decl(self, tp, name, prefix='enum'): + if tp.partial: + for enumerator in tp.enumerators: + self._generate_gen_const(True, enumerator) + return + # + funcname = self._enum_funcname(prefix, name) + self.export_symbols.append(funcname) + prnt = self._prnt + prnt('int %s(char *out_error)' % funcname) + prnt('{') + for enumerator, enumvalue in zip(tp.enumerators, tp.enumvalues): + self._check_int_constant_value(enumerator, enumvalue) + prnt(' return 0;') + prnt('}') + prnt() + + def _loading_gen_enum(self, tp, name, module, prefix='enum'): + if tp.partial: + enumvalues = [self._load_constant(True, tp, enumerator, module) + for enumerator in tp.enumerators] + tp.enumvalues = tuple(enumvalues) + tp.partial_resolved = True + else: + funcname = self._enum_funcname(prefix, name) + self._load_known_int_constant(module, funcname) + + def _loaded_gen_enum(self, tp, name, module, library): + for enumerator, enumvalue in zip(tp.enumerators, tp.enumvalues): + setattr(library, enumerator, enumvalue) + type(library)._cffi_dir.append(enumerator) + + # ---------- + # macros: for now only for integers + + def _generate_gen_macro_decl(self, tp, name): + if tp == '...': + check_value = None + else: + check_value = tp # an integer + self._generate_gen_const(True, name, check_value=check_value) + + _loading_gen_macro = _loaded_noop + + def _loaded_gen_macro(self, tp, name, module, library): + if tp == '...': + check_value = None + else: + check_value = tp # an integer + value = self._load_constant(True, tp, name, module, + check_value=check_value) + setattr(library, name, value) + type(library)._cffi_dir.append(name) + + # ---------- + # global variables + + def _generate_gen_variable_decl(self, tp, name): + if isinstance(tp, model.ArrayType): + if tp.length_is_unknown(): + prnt = self._prnt + funcname = '_cffi_sizeof_%s' % (name,) + self.export_symbols.append(funcname) + prnt("size_t %s(void)" % funcname) + prnt("{") + prnt(" return sizeof(%s);" % (name,)) + prnt("}") + tp_ptr = model.PointerType(tp.item) + self._generate_gen_const(False, name, tp_ptr) + else: + tp_ptr = model.PointerType(tp) + self._generate_gen_const(False, name, tp_ptr, category='var') + + _loading_gen_variable = _loaded_noop + + def _loaded_gen_variable(self, tp, name, module, library): + if isinstance(tp, model.ArrayType): # int a[5] is "constant" in the + # sense that "a=..." is forbidden + if tp.length_is_unknown(): + funcname = '_cffi_sizeof_%s' % (name,) + BFunc = self.ffi._typeof_locked('size_t(*)(void)')[0] + function = module.load_function(BFunc, funcname) + size = function() + BItemType = self.ffi._get_cached_btype(tp.item) + length, rest = divmod(size, self.ffi.sizeof(BItemType)) + if rest != 0: + raise VerificationError( + "bad size: %r does not seem to be an array of %s" % + (name, tp.item)) + tp = tp.resolve_length(length) + tp_ptr = model.PointerType(tp.item) + value = self._load_constant(False, tp_ptr, name, module) + # 'value' is a which we have to replace with + # a if the N is actually known + if tp.length is not None: + BArray = self.ffi._get_cached_btype(tp) + value = self.ffi.cast(BArray, value) + setattr(library, name, value) + type(library)._cffi_dir.append(name) + return + # remove ptr= from the library instance, and replace + # it by a property on the class, which reads/writes into ptr[0]. + funcname = '_cffi_var_%s' % name + BFunc = self.ffi._typeof_locked(tp.get_c_name('*(*)(void)', name))[0] + function = module.load_function(BFunc, funcname) + ptr = function() + def getter(library): + return ptr[0] + def setter(library, value): + ptr[0] = value + setattr(type(library), name, property(getter, setter)) + type(library)._cffi_dir.append(name) + +cffimod_header = r''' +#include +#include +#include +#include +#include /* XXX for ssize_t on some platforms */ + +/* this block of #ifs should be kept exactly identical between + c/_cffi_backend.c, cffi/vengine_cpy.py, cffi/vengine_gen.py + and cffi/_cffi_include.h */ +#if defined(_MSC_VER) +# include /* for alloca() */ +# if _MSC_VER < 1600 /* MSVC < 2010 */ + typedef __int8 int8_t; + typedef __int16 int16_t; + typedef __int32 int32_t; + typedef __int64 int64_t; + typedef unsigned __int8 uint8_t; + typedef unsigned __int16 uint16_t; + typedef unsigned __int32 uint32_t; + typedef unsigned __int64 uint64_t; + typedef __int8 int_least8_t; + typedef __int16 int_least16_t; + typedef __int32 int_least32_t; + typedef __int64 int_least64_t; + typedef unsigned __int8 uint_least8_t; + typedef unsigned __int16 uint_least16_t; + typedef unsigned __int32 uint_least32_t; + typedef unsigned __int64 uint_least64_t; + typedef __int8 int_fast8_t; + typedef __int16 int_fast16_t; + typedef __int32 int_fast32_t; + typedef __int64 int_fast64_t; + typedef unsigned __int8 uint_fast8_t; + typedef unsigned __int16 uint_fast16_t; + typedef unsigned __int32 uint_fast32_t; + typedef unsigned __int64 uint_fast64_t; + typedef __int64 intmax_t; + typedef unsigned __int64 uintmax_t; +# else +# include +# endif +# if _MSC_VER < 1800 /* MSVC < 2013 */ +# ifndef __cplusplus + typedef unsigned char _Bool; +# endif +# endif +# define _cffi_float_complex_t _Fcomplex /* include for it */ +# define _cffi_double_complex_t _Dcomplex /* include for it */ +#else +# include +# if (defined (__SVR4) && defined (__sun)) || defined(_AIX) || defined(__hpux) +# include +# endif +# define _cffi_float_complex_t float _Complex +# define _cffi_double_complex_t double _Complex +#endif +''' diff --git a/source/target-vendors/cp312/cffi/verifier.py b/source/target-vendors/cp312/cffi/verifier.py new file mode 100644 index 0000000..96baadb --- /dev/null +++ b/source/target-vendors/cp312/cffi/verifier.py @@ -0,0 +1,306 @@ +# +# DEPRECATED: implementation for ffi.verify() +# +import sys, os, binascii, shutil, io +from . import __version_verifier_modules__ +from . import ffiplatform +from .error import VerificationError + +if sys.version_info >= (3, 3): + import importlib.machinery + def _extension_suffixes(): + return importlib.machinery.EXTENSION_SUFFIXES[:] +else: + import imp + def _extension_suffixes(): + return [suffix for suffix, _, type in imp.get_suffixes() + if type == imp.C_EXTENSION] + + +if sys.version_info >= (3,): + NativeIO = io.StringIO +else: + class NativeIO(io.BytesIO): + def write(self, s): + if isinstance(s, unicode): + s = s.encode('ascii') + super().write(s) + + +class Verifier: + + def __init__(self, ffi, preamble, tmpdir=None, modulename=None, + ext_package=None, tag='', force_generic_engine=False, + source_extension='.c', flags=None, relative_to=None, **kwds): + if ffi._parser._uses_new_feature: + raise VerificationError( + "feature not supported with ffi.verify(), but only " + "with ffi.set_source(): %s" % (ffi._parser._uses_new_feature,)) + self.ffi = ffi + self.preamble = preamble + if not modulename: + flattened_kwds = ffiplatform.flatten(kwds) + vengine_class = _locate_engine_class(ffi, force_generic_engine) + self._vengine = vengine_class(self) + self._vengine.patch_extension_kwds(kwds) + self.flags = flags + self.kwds = self.make_relative_to(kwds, relative_to) + # + if modulename: + if tag: + raise TypeError("can't specify both 'modulename' and 'tag'") + else: + key = '\x00'.join(['%d.%d' % sys.version_info[:2], + __version_verifier_modules__, + preamble, flattened_kwds] + + ffi._cdefsources) + if sys.version_info >= (3,): + key = key.encode('utf-8') + k1 = hex(binascii.crc32(key[0::2]) & 0xffffffff) + k1 = k1.lstrip('0x').rstrip('L') + k2 = hex(binascii.crc32(key[1::2]) & 0xffffffff) + k2 = k2.lstrip('0').rstrip('L') + modulename = '_cffi_%s_%s%s%s' % (tag, self._vengine._class_key, + k1, k2) + suffix = _get_so_suffixes()[0] + self.tmpdir = tmpdir or _caller_dir_pycache() + self.sourcefilename = os.path.join(self.tmpdir, modulename + source_extension) + self.modulefilename = os.path.join(self.tmpdir, modulename + suffix) + self.ext_package = ext_package + self._has_source = False + self._has_module = False + + def write_source(self, file=None): + """Write the C source code. It is produced in 'self.sourcefilename', + which can be tweaked beforehand.""" + with self.ffi._lock: + if self._has_source and file is None: + raise VerificationError( + "source code already written") + self._write_source(file) + + def compile_module(self): + """Write the C source code (if not done already) and compile it. + This produces a dynamic link library in 'self.modulefilename'.""" + with self.ffi._lock: + if self._has_module: + raise VerificationError("module already compiled") + if not self._has_source: + self._write_source() + self._compile_module() + + def load_library(self): + """Get a C module from this Verifier instance. + Returns an instance of a FFILibrary class that behaves like the + objects returned by ffi.dlopen(), but that delegates all + operations to the C module. If necessary, the C code is written + and compiled first. + """ + with self.ffi._lock: + if not self._has_module: + self._locate_module() + if not self._has_module: + if not self._has_source: + self._write_source() + self._compile_module() + return self._load_library() + + def get_module_name(self): + basename = os.path.basename(self.modulefilename) + # kill both the .so extension and the other .'s, as introduced + # by Python 3: 'basename.cpython-33m.so' + basename = basename.split('.', 1)[0] + # and the _d added in Python 2 debug builds --- but try to be + # conservative and not kill a legitimate _d + if basename.endswith('_d') and hasattr(sys, 'gettotalrefcount'): + basename = basename[:-2] + return basename + + def get_extension(self): + if not self._has_source: + with self.ffi._lock: + if not self._has_source: + self._write_source() + sourcename = ffiplatform.maybe_relative_path(self.sourcefilename) + modname = self.get_module_name() + return ffiplatform.get_extension(sourcename, modname, **self.kwds) + + def generates_python_module(self): + return self._vengine._gen_python_module + + def make_relative_to(self, kwds, relative_to): + if relative_to and os.path.dirname(relative_to): + dirname = os.path.dirname(relative_to) + kwds = kwds.copy() + for key in ffiplatform.LIST_OF_FILE_NAMES: + if key in kwds: + lst = kwds[key] + if not isinstance(lst, (list, tuple)): + raise TypeError("keyword '%s' should be a list or tuple" + % (key,)) + lst = [os.path.join(dirname, fn) for fn in lst] + kwds[key] = lst + return kwds + + # ---------- + + def _locate_module(self): + if not os.path.isfile(self.modulefilename): + if self.ext_package: + try: + pkg = __import__(self.ext_package, None, None, ['__doc__']) + except ImportError: + return # cannot import the package itself, give up + # (e.g. it might be called differently before installation) + path = pkg.__path__ + else: + path = None + filename = self._vengine.find_module(self.get_module_name(), path, + _get_so_suffixes()) + if filename is None: + return + self.modulefilename = filename + self._vengine.collect_types() + self._has_module = True + + def _write_source_to(self, file): + self._vengine._f = file + try: + self._vengine.write_source_to_f() + finally: + del self._vengine._f + + def _write_source(self, file=None): + if file is not None: + self._write_source_to(file) + else: + # Write our source file to an in memory file. + f = NativeIO() + self._write_source_to(f) + source_data = f.getvalue() + + # Determine if this matches the current file + if os.path.exists(self.sourcefilename): + with open(self.sourcefilename, "r") as fp: + needs_written = fp.read() != source_data + else: + needs_written = True + + # Actually write the file out if it doesn't match + if needs_written: + _ensure_dir(self.sourcefilename) + with open(self.sourcefilename, "w") as fp: + fp.write(source_data) + + # Set this flag + self._has_source = True + + def _compile_module(self): + # compile this C source + tmpdir = os.path.dirname(self.sourcefilename) + outputfilename = ffiplatform.compile(tmpdir, self.get_extension()) + try: + same = ffiplatform.samefile(outputfilename, self.modulefilename) + except OSError: + same = False + if not same: + _ensure_dir(self.modulefilename) + shutil.move(outputfilename, self.modulefilename) + self._has_module = True + + def _load_library(self): + assert self._has_module + if self.flags is not None: + return self._vengine.load_library(self.flags) + else: + return self._vengine.load_library() + +# ____________________________________________________________ + +_FORCE_GENERIC_ENGINE = False # for tests + +def _locate_engine_class(ffi, force_generic_engine): + if _FORCE_GENERIC_ENGINE: + force_generic_engine = True + if not force_generic_engine: + if '__pypy__' in sys.builtin_module_names: + force_generic_engine = True + else: + try: + import _cffi_backend + except ImportError: + _cffi_backend = '?' + if ffi._backend is not _cffi_backend: + force_generic_engine = True + if force_generic_engine: + from . import vengine_gen + return vengine_gen.VGenericEngine + else: + from . import vengine_cpy + return vengine_cpy.VCPythonEngine + +# ____________________________________________________________ + +_TMPDIR = None + +def _caller_dir_pycache(): + if _TMPDIR: + return _TMPDIR + result = os.environ.get('CFFI_TMPDIR') + if result: + return result + filename = sys._getframe(2).f_code.co_filename + return os.path.abspath(os.path.join(os.path.dirname(filename), + '__pycache__')) + +def set_tmpdir(dirname): + """Set the temporary directory to use instead of __pycache__.""" + global _TMPDIR + _TMPDIR = dirname + +def cleanup_tmpdir(tmpdir=None, keep_so=False): + """Clean up the temporary directory by removing all files in it + called `_cffi_*.{c,so}` as well as the `build` subdirectory.""" + tmpdir = tmpdir or _caller_dir_pycache() + try: + filelist = os.listdir(tmpdir) + except OSError: + return + if keep_so: + suffix = '.c' # only remove .c files + else: + suffix = _get_so_suffixes()[0].lower() + for fn in filelist: + if fn.lower().startswith('_cffi_') and ( + fn.lower().endswith(suffix) or fn.lower().endswith('.c')): + try: + os.unlink(os.path.join(tmpdir, fn)) + except OSError: + pass + clean_dir = [os.path.join(tmpdir, 'build')] + for dir in clean_dir: + try: + for fn in os.listdir(dir): + fn = os.path.join(dir, fn) + if os.path.isdir(fn): + clean_dir.append(fn) + else: + os.unlink(fn) + except OSError: + pass + +def _get_so_suffixes(): + suffixes = _extension_suffixes() + if not suffixes: + # bah, no C_EXTENSION available. Occurs on pypy without cpyext + if sys.platform == 'win32': + suffixes = [".pyd"] + else: + suffixes = [".so"] + + return suffixes + +def _ensure_dir(filename): + dirname = os.path.dirname(filename) + if dirname and not os.path.isdir(dirname): + os.makedirs(dirname) diff --git a/source/target-vendors/cp312/click-8.5.0.dist-info/METADATA b/source/target-vendors/cp312/click-8.5.0.dist-info/METADATA new file mode 100644 index 0000000..7b78530 --- /dev/null +++ b/source/target-vendors/cp312/click-8.5.0.dist-info/METADATA @@ -0,0 +1,83 @@ +Metadata-Version: 2.4 +Name: click +Version: 8.5.0 +Summary: Composable command line interface toolkit +Maintainer-email: Pallets +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +License-Expression: BSD-3-Clause +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Typing :: Typed +License-File: LICENSE.txt +Project-URL: Changes, https://click.palletsprojects.com/page/changes/ +Project-URL: Chat, https://discord.gg/pallets +Project-URL: Documentation, https://click.palletsprojects.com/ +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Source, https://github.com/pallets/click/ + +
+ +# Click + +Click is a Python package for creating beautiful command line interfaces +in a composable way with as little code as necessary. It's the "Command +Line Interface Creation Kit". It's highly configurable but comes with +sensible defaults out of the box. + +It aims to make the process of writing command line tools quick and fun +while also preventing any frustration caused by the inability to +implement an intended CLI API. + +Click in three points: + +- Arbitrary nesting of commands +- Automatic help page generation +- Supports lazy loading of subcommands at runtime + + +## A Simple Example + +```python +import click + +@click.command() +@click.option("--count", default=1, help="Number of greetings.") +@click.option("--name", prompt="Your name", help="The person to greet.") +def hello(count, name): + """Simple program that greets NAME for a total of COUNT times.""" + for _ in range(count): + click.echo(f"Hello, {name}!") + +if __name__ == '__main__': + hello() +``` + +``` +$ python hello.py --count=3 +Your name: Click +Hello, Click! +Hello, Click! +Hello, Click! +``` + + +## Donate + +The Pallets organization develops and supports Click and other popular +packages. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, [please +donate today][]. + +[please donate today]: https://palletsprojects.com/donate + +## Contributing + +See our [detailed contributing documentation][contrib] for many ways to +contribute, including reporting issues, requesting features, asking or answering +questions, and making PRs. + +[contrib]: https://palletsprojects.com/contributing/ + diff --git a/source/target-vendors/cp312/click-8.5.0.dist-info/RECORD b/source/target-vendors/cp312/click-8.5.0.dist-info/RECORD new file mode 100644 index 0000000..b50c362 --- /dev/null +++ b/source/target-vendors/cp312/click-8.5.0.dist-info/RECORD @@ -0,0 +1,22 @@ +click/__init__.py,sha256=Wr_FTTfUfMeIt-egXpUUeH-MWgt9tCnQ8k10isiZZMo,5145 +click/_compat.py,sha256=jbHaKWXPPl3GbGxTgzy8YT6hzSRAudDraqg2DrRtSRM,17939 +click/_termui_impl.py,sha256=iLl9m3pblHsQoW2KsmmDe8hMflXCi6cupfL7M5ESM2o,32805 +click/_textwrap.py,sha256=7Z0N7Vmn-66TNSTUwp6OXJbcUXRmYET9h9c2ucD8oQQ,6270 +click/_utils.py,sha256=eCZCtwJtsYD5QYkkNWJ8MY_8ABIjy8MczgMMyVY32rQ,996 +click/_winconsole.py,sha256=KSxfNbMlYRa6GOJuCLgsg2Pb3dVkgJNPqLJPae-Pa10,8543 +click/core.py,sha256=TGWmE8HEB9zpB6ThI7Es7F_g9iCIqLn4b6vUtgxLbXg,147845 +click/decorators.py,sha256=FgaTV2FWkfzfyceUu1FfYAnrNarW96dsAXvc4G2ujVU,21680 +click/exceptions.py,sha256=HvSY34G4auj_bYRR8-T8CU8Jwq_1-OcsRU4ezfozeEk,11862 +click/formatting.py,sha256=8SW2KGkvjfz9Q1NbeojMHuZBN0cfnQJDs4mqDP6oXms,10444 +click/globals.py,sha256=gM-Nh6A4M0HB_SgkaF5M4ncGGMDHc_flHXu9_oh4GEU,1923 +click/parser.py,sha256=oJ-fU_3mvxugIuNtHaCATZ56lgEmHRggjJiSqEgYrjA,19052 +click/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +click/shell_completion.py,sha256=GFIvdd3_5lXpQ5eNGVMlXER4QoZ9P9Z_vcFUPT332gk,26172 +click/termui.py,sha256=jsgCK45FKPhz7EExlKXRYU6W2lC2xKcfYmCzJnYqfpw,35370 +click/testing.py,sha256=dvTmSbxTAA1mfIwN7_FZGXD6A0sPCFogcWWDmyMBAZo,27584 +click/types.py,sha256=HpiJBKen9oq61jDH-5wDikNF-i5nAcl2puUfEL37Ut0,46842 +click/utils.py,sha256=9D1XQ9wHJA5hL-76CmnSsWl7aQAUn4-An_zZfstILb8,21483 +click-8.5.0.dist-info/licenses/LICENSE.txt,sha256=morRBqOU6FO_4h9C9OctWSgZoigF2ZG18ydQKSkrZY0,1475 +click-8.5.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 +click-8.5.0.dist-info/METADATA,sha256=6HvOC9GU3nDfuOcI4KawSDAJ8lYRgE6vh7_rY-bCBQE,2567 +click-8.5.0.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/click-8.5.0.dist-info/WHEEL b/source/target-vendors/cp312/click-8.5.0.dist-info/WHEEL new file mode 100644 index 0000000..d8b9936 --- /dev/null +++ b/source/target-vendors/cp312/click-8.5.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.12.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/source/target-vendors/cp312/click-8.5.0.dist-info/licenses/LICENSE.txt b/source/target-vendors/cp312/click-8.5.0.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000..d12a849 --- /dev/null +++ b/source/target-vendors/cp312/click-8.5.0.dist-info/licenses/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright 2014 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/source/target-vendors/cp312/click/__init__.py b/source/target-vendors/cp312/click/__init__.py new file mode 100644 index 0000000..a34ab02 --- /dev/null +++ b/source/target-vendors/cp312/click/__init__.py @@ -0,0 +1,144 @@ +""" +Click is a simple Python module inspired by the stdlib optparse to make +writing command line scripts fun. Unlike other modules, it's based +around a simple API that does not come with too much magic and is +composable. +""" + +from __future__ import annotations + +from .core import Argument as Argument +from .core import Command as Command +from .core import CommandCollection as CommandCollection +from .core import Context as Context +from .core import Group as Group +from .core import Option as Option +from .core import Parameter as Parameter +from .core import ParameterSource as ParameterSource +from .decorators import argument as argument +from .decorators import command as command +from .decorators import confirmation_option as confirmation_option +from .decorators import custom_version_option as custom_version_option +from .decorators import group as group +from .decorators import help_option as help_option +from .decorators import make_pass_decorator as make_pass_decorator +from .decorators import option as option +from .decorators import pass_context as pass_context +from .decorators import pass_obj as pass_obj +from .decorators import password_option as password_option +from .decorators import version_option as version_option +from .exceptions import Abort as Abort +from .exceptions import BadArgumentUsage as BadArgumentUsage +from .exceptions import BadOptionUsage as BadOptionUsage +from .exceptions import BadParameter as BadParameter +from .exceptions import ClickException as ClickException +from .exceptions import FileError as FileError +from .exceptions import MissingParameter as MissingParameter +from .exceptions import NoSuchCommand as NoSuchCommand +from .exceptions import NoSuchOption as NoSuchOption +from .exceptions import UsageError as UsageError +from .formatting import HelpFormatter as HelpFormatter +from .formatting import wrap_text as wrap_text +from .globals import get_current_context as get_current_context +from .termui import clear as clear +from .termui import confirm as confirm +from .termui import echo_via_pager as echo_via_pager +from .termui import edit as edit +from .termui import get_pager_file as get_pager_file +from .termui import getchar as getchar +from .termui import launch as launch +from .termui import pause as pause +from .termui import progressbar as progressbar +from .termui import prompt as prompt +from .termui import secho as secho +from .termui import style as style +from .termui import unstyle as unstyle +from .types import BOOL as BOOL +from .types import Choice as Choice +from .types import DateTime as DateTime +from .types import File as File +from .types import FLOAT as FLOAT +from .types import FloatRange as FloatRange +from .types import INT as INT +from .types import IntRange as IntRange +from .types import ParamType as ParamType +from .types import Path as Path +from .types import STRING as STRING +from .types import Tuple as Tuple +from .types import UNPROCESSED as UNPROCESSED +from .types import UUID as UUID +from .utils import echo as echo +from .utils import format_filename as format_filename +from .utils import get_app_dir as get_app_dir +from .utils import open_file as open_file + + +def __getattr__(name: str) -> object: + import warnings + + if name == "BaseCommand": + from .core import _BaseCommand + + warnings.warn( + "'BaseCommand' is deprecated and will be removed in Click 9.0. Use" + " 'Command' instead.", + DeprecationWarning, + stacklevel=2, + ) + return _BaseCommand + + if name == "MultiCommand": + from .core import _MultiCommand + + warnings.warn( + "'MultiCommand' is deprecated and will be removed in Click 9.0. Use" + " 'Group' instead.", + DeprecationWarning, + stacklevel=2, + ) + return _MultiCommand + + if name == "OptionParser": + from .parser import _OptionParser + + warnings.warn( + "'OptionParser' is deprecated and will be removed in Click 9.0. The" + " old parser is available in 'optparse'.", + DeprecationWarning, + stacklevel=2, + ) + return _OptionParser + + if name == "__version__": + import importlib.metadata + + warnings.warn( + "The '__version__' attribute is deprecated and will be removed in" + " Click 9.1. Use feature detection or" + " 'importlib.metadata.version(\"click\")' instead.", + DeprecationWarning, + stacklevel=2, + ) + return importlib.metadata.version("click") + + if name == "get_binary_stream": + from .utils import _get_binary_stream + + warnings.warn( + "'get_binary_stream' is deprecated and will be removed in Click 9.0.", + DeprecationWarning, + stacklevel=2, + ) + return _get_binary_stream + + if name == "get_text_stream": + from .utils import _get_text_stream + + warnings.warn( + "'get_text_stream' is deprecated and will be removed in Click 9.0.", + DeprecationWarning, + stacklevel=2, + ) + return _get_text_stream + + raise AttributeError(name) diff --git a/source/target-vendors/cp312/click/_compat.py b/source/target-vendors/cp312/click/_compat.py new file mode 100644 index 0000000..8507a50 --- /dev/null +++ b/source/target-vendors/cp312/click/_compat.py @@ -0,0 +1,590 @@ +from __future__ import annotations + +import codecs +import collections.abc as cabc +import io +import os +import re +import sys +import typing as t +from types import TracebackType +from weakref import WeakKeyDictionary + +CYGWIN = sys.platform.startswith("cygwin") +WIN = sys.platform.startswith("win") +# One CSI escape sequence per the ECMA-48 grammar: parameter bytes (0x30-0x3F), +# intermediate bytes (0x20-0x2F), then a final byte (0x40-0x7E). Broader than the +# SGR codes Click emits, so foreign sequences (colon-delimited true-color, mouse +# reporting) are stripped too. +_ansi_re = re.compile(r"\033\[[0-?]*[ -/]*[@-~]") + + +def _make_text_stream( + stream: t.BinaryIO, + encoding: str | None, + errors: str | None, + force_readable: bool = False, + force_writable: bool = False, +) -> t.TextIO: + if encoding is None: + encoding = get_best_encoding(stream) + if errors is None: + errors = "replace" + return _NonClosingTextIOWrapper( + stream, + encoding, + errors, + line_buffering=True, + force_readable=force_readable, + force_writable=force_writable, + ) + + +def is_ascii_encoding(encoding: str) -> bool: + """Checks if a given encoding is ascii.""" + try: + return codecs.lookup(encoding).name == "ascii" + except LookupError: + return False + + +def get_best_encoding(stream: t.IO[t.Any]) -> str: + """Returns the default stream encoding if not found.""" + rv = getattr(stream, "encoding", None) or sys.getdefaultencoding() + if is_ascii_encoding(rv): + return "utf-8" + return rv + + +class _NonClosingTextIOWrapper(io.TextIOWrapper): + def __init__( + self, + stream: t.BinaryIO, + encoding: str | None, + errors: str | None, + force_readable: bool = False, + force_writable: bool = False, + **extra: t.Any, + ) -> None: + self._stream = stream = t.cast( + t.BinaryIO, _FixupStream(stream, force_readable, force_writable) + ) + super().__init__(stream, encoding, errors, **extra) + + def __del__(self) -> None: + try: + self.detach() + except Exception: + pass + + def isatty(self) -> bool: + # https://bitbucket.org/pypy/pypy/issue/1803 + return self._stream.isatty() + + +class _FixupStream: + """The new io interface needs more from streams than streams + traditionally implement. As such, this fix-up code is necessary in + some circumstances. + + The forcing of readable and writable flags are there because some tools + put badly patched objects on sys (one such offender are certain version + of jupyter notebook). + """ + + def __init__( + self, + stream: t.BinaryIO, + force_readable: bool = False, + force_writable: bool = False, + ): + self._stream = stream + self._force_readable = force_readable + self._force_writable = force_writable + + def __getattr__(self, name: str) -> t.Any: + return getattr(self._stream, name) + + def read1(self, size: int) -> bytes: + f = getattr(self._stream, "read1", None) + + if f is not None: + return t.cast(bytes, f(size)) + + return self._stream.read(size) + + def readable(self) -> bool: + if self._force_readable: + return True + x = getattr(self._stream, "readable", None) + if x is not None: + return t.cast(bool, x()) + try: + self._stream.read(0) + except Exception: + return False + return True + + def writable(self) -> bool: + if self._force_writable: + return True + x = getattr(self._stream, "writable", None) + if x is not None: + return t.cast(bool, x()) + try: + self._stream.write(b"") + except Exception: + try: + self._stream.write(b"") + except Exception: + return False + return True + + def seekable(self) -> bool: + x = getattr(self._stream, "seekable", None) + if x is not None: + return t.cast(bool, x()) + try: + self._stream.seek(self._stream.tell()) + except Exception: + return False + return True + + +def _is_binary_reader(stream: t.IO[t.Any], default: bool = False) -> bool: + try: + return isinstance(stream.read(0), bytes) + except Exception: + return default + # This happens in some cases where the stream was already + # closed. In this case, we assume the default. + + +def _is_binary_writer(stream: t.IO[t.Any], default: bool = False) -> bool: + try: + stream.write(b"") + except Exception: + try: + stream.write("") + return False + except Exception: + pass + return default + return True + + +def _find_binary_reader(stream: t.IO[t.Any]) -> t.BinaryIO | None: + # We need to figure out if the given stream is already binary. + # This can happen because the official docs recommend detaching + # the streams to get binary streams. Some code might do this, so + # we need to deal with this case explicitly. + if _is_binary_reader(stream, False): + return t.cast(t.BinaryIO, stream) + + buf = getattr(stream, "buffer", None) + + # Same situation here; this time we assume that the buffer is + # actually binary in case it's closed. + if buf is not None and _is_binary_reader(buf, True): + return t.cast(t.BinaryIO, buf) + + return None + + +def _find_binary_writer(stream: t.IO[t.Any]) -> t.BinaryIO | None: + # We need to figure out if the given stream is already binary. + # This can happen because the official docs recommend detaching + # the streams to get binary streams. Some code might do this, so + # we need to deal with this case explicitly. + if _is_binary_writer(stream, False): + return t.cast(t.BinaryIO, stream) + + buf = getattr(stream, "buffer", None) + + # Same situation here; this time we assume that the buffer is + # actually binary in case it's closed. + if buf is not None and _is_binary_writer(buf, True): + return t.cast(t.BinaryIO, buf) + + return None + + +def _stream_is_misconfigured(stream: t.TextIO) -> bool: + """A stream is misconfigured if its encoding is ASCII.""" + # If the stream does not have an encoding set, we assume it's set + # to ASCII. This appears to happen in certain unittest + # environments. It's not quite clear what the correct behavior is + # but this at least will force Click to recover somehow. + return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii") + + +def _is_compat_stream_attr(stream: t.TextIO, attr: str, value: str | None) -> bool: + """A stream attribute is compatible if it is equal to the + desired value or the desired value is unset and the attribute + has a value. + """ + stream_value = getattr(stream, attr, None) + return stream_value == value or (value is None and stream_value is not None) + + +def _is_compatible_text_stream( + stream: t.TextIO, encoding: str | None, errors: str | None +) -> bool: + """Check if a stream's encoding and errors attributes are + compatible with the desired values. + """ + return _is_compat_stream_attr( + stream, "encoding", encoding + ) and _is_compat_stream_attr(stream, "errors", errors) + + +def _force_correct_text_stream( + text_stream: t.IO[t.Any], + encoding: str | None, + errors: str | None, + is_binary: t.Callable[[t.IO[t.Any], bool], bool], + find_binary: t.Callable[[t.IO[t.Any]], t.BinaryIO | None], + force_readable: bool = False, + force_writable: bool = False, +) -> t.TextIO: + if is_binary(text_stream, False): + binary_reader = t.cast(t.BinaryIO, text_stream) + else: + text_stream = t.cast(t.TextIO, text_stream) + # If the stream looks compatible, and won't default to a + # misconfigured ascii encoding, return it as-is. + if _is_compatible_text_stream(text_stream, encoding, errors) and not ( + encoding is None and _stream_is_misconfigured(text_stream) + ): + return text_stream + + # Otherwise, get the underlying binary reader. + possible_binary_reader = find_binary(text_stream) + + # If that's not possible, silently use the original reader + # and get mojibake instead of exceptions. + if possible_binary_reader is None: + return text_stream + + binary_reader = possible_binary_reader + + # Default errors to replace instead of strict in order to get + # something that works. + if errors is None: + errors = "replace" + + # Wrap the binary stream in a text stream with the correct + # encoding parameters. + return _make_text_stream( + binary_reader, + encoding, + errors, + force_readable=force_readable, + force_writable=force_writable, + ) + + +def _force_correct_text_reader( + text_reader: t.IO[t.Any], + encoding: str | None, + errors: str | None, + force_readable: bool = False, +) -> t.TextIO: + return _force_correct_text_stream( + text_reader, + encoding, + errors, + _is_binary_reader, + _find_binary_reader, + force_readable=force_readable, + ) + + +def _force_correct_text_writer( + text_writer: t.IO[t.Any], + encoding: str | None, + errors: str | None, + force_writable: bool = False, +) -> t.TextIO: + return _force_correct_text_stream( + text_writer, + encoding, + errors, + _is_binary_writer, + _find_binary_writer, + force_writable=force_writable, + ) + + +def get_binary_stdin() -> t.BinaryIO: + reader = _find_binary_reader(sys.stdin) + if reader is None: + raise RuntimeError("Was not able to determine binary stream for sys.stdin.") + return reader + + +def get_binary_stdout() -> t.BinaryIO: + writer = _find_binary_writer(sys.stdout) + if writer is None: + raise RuntimeError("Was not able to determine binary stream for sys.stdout.") + return writer + + +def get_binary_stderr() -> t.BinaryIO: + writer = _find_binary_writer(sys.stderr) + if writer is None: + raise RuntimeError("Was not able to determine binary stream for sys.stderr.") + return writer + + +def get_text_stdin(encoding: str | None = None, errors: str | None = None) -> t.TextIO: + rv = _get_windows_console_stream(sys.stdin, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_reader(sys.stdin, encoding, errors, force_readable=True) + + +def get_text_stdout(encoding: str | None = None, errors: str | None = None) -> t.TextIO: + rv = _get_windows_console_stream(sys.stdout, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_writer(sys.stdout, encoding, errors, force_writable=True) + + +def get_text_stderr(encoding: str | None = None, errors: str | None = None) -> t.TextIO: + rv = _get_windows_console_stream(sys.stderr, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_writer(sys.stderr, encoding, errors, force_writable=True) + + +def _wrap_io_open( + file: str | os.PathLike[str] | int, + mode: str, + encoding: str | None, + errors: str | None, +) -> t.IO[t.Any]: + """Handles not passing ``encoding`` and ``errors`` in binary mode.""" + if "b" in mode: + return open(file, mode) + + return open(file, mode, encoding=encoding, errors=errors) + + +def open_stream( + filename: str | os.PathLike[str], + mode: str = "r", + encoding: str | None = None, + errors: str | None = "strict", + atomic: bool = False, +) -> tuple[t.IO[t.Any], bool]: + binary = "b" in mode + filename = os.fspath(filename) + + # Standard streams first. These are simple because they ignore the + # atomic flag. Use fsdecode to handle Path("-"). + if os.fsdecode(filename) == "-": + if any(m in mode for m in ["w", "a", "x"]): + if binary: + return get_binary_stdout(), False + return get_text_stdout(encoding=encoding, errors=errors), False + if binary: + return get_binary_stdin(), False + return get_text_stdin(encoding=encoding, errors=errors), False + + # Non-atomic writes directly go out through the regular open functions. + if not atomic: + return _wrap_io_open(filename, mode, encoding, errors), True + + # Some usability stuff for atomic writes + if "a" in mode: + raise ValueError( + "Appending to an existing file is not supported, because that" + " would involve an expensive `copy`-operation to a temporary" + " file. Open the file in normal `w`-mode and copy explicitly" + " if that's what you're after." + ) + if "x" in mode: + raise ValueError("Use the `overwrite`-parameter instead.") + if "w" not in mode: + raise ValueError("Atomic writes only make sense with `w`-mode.") + + # Atomic writes are more complicated. They work by opening a file + # as a proxy in the same folder and then using the fdopen + # functionality to wrap it in a Python file. Then we wrap it in an + # atomic file that moves the file over on close. + import errno + import random + + try: + perm: int | None = os.stat(filename).st_mode + except OSError: + perm = None + + flags = os.O_RDWR | os.O_CREAT | os.O_EXCL + + if binary: + flags |= getattr(os, "O_BINARY", 0) + + while True: + tmp_filename = os.path.join( + os.path.dirname(filename), + f".__atomic-write{random.randrange(1 << 32):08x}", + ) + try: + fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm) + break + except OSError as e: + if e.errno == errno.EEXIST or ( + os.name == "nt" + and e.errno == errno.EACCES + and os.path.isdir(e.filename) + and os.access(e.filename, os.W_OK) + ): + continue + raise + + if perm is not None: + os.chmod(tmp_filename, perm) # in case perm includes bits in umask + + f = _wrap_io_open(fd, mode, encoding, errors) + af = _AtomicFile(f, tmp_filename, os.path.realpath(filename)) + return t.cast(t.IO[t.Any], af), True + + +class _AtomicFile: + def __init__(self, f: t.IO[t.Any], tmp_filename: str, real_filename: str) -> None: + self._f = f + self._tmp_filename = tmp_filename + self._real_filename = real_filename + self.closed = False + + @property + def name(self) -> str: + return self._real_filename + + def close(self, delete: bool = False) -> None: + if self.closed: + return + self._f.close() + os.replace(self._tmp_filename, self._real_filename) + self.closed = True + + def __getattr__(self, name: str) -> t.Any: + return getattr(self._f, name) + + def __enter__(self) -> _AtomicFile: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.close(delete=exc_type is not None) + + def __repr__(self) -> str: + return repr(self._f) + + +def strip_ansi(value: str) -> str: + return _ansi_re.sub("", value) + + +def _is_jupyter_kernel_output(stream: t.IO[t.Any]) -> bool: + while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)): + stream = stream._stream + + return stream.__class__.__module__.startswith("ipykernel.") + + +def should_strip_ansi( + stream: t.IO[t.Any] | None = None, color: bool | None = None +) -> bool: + if color is None: + if stream is None: + stream = sys.stdin + elif hasattr(stream, "color"): + # ._termui_impl._PagerWriter handles stripping ansi itself, + # so we don't need to strip it here + return False + return not isatty(stream) and not _is_jupyter_kernel_output(stream) + return not color + + +# Double check is needed so mypy does not analyze this on Linux. +if sys.platform.startswith("win") and WIN: + from ._winconsole import _get_windows_console_stream + + def _get_argv_encoding() -> str: + import locale + + return locale.getpreferredencoding() + +else: + + def _get_argv_encoding() -> str: + return getattr(sys.stdin, "encoding", None) or sys.getfilesystemencoding() + + def _get_windows_console_stream( + f: t.TextIO, encoding: str | None, errors: str | None + ) -> t.TextIO | None: + return None + + +def term_len(x: str) -> int: + return len(strip_ansi(x)) + + +def isatty(stream: t.IO[t.Any]) -> bool: + try: + return stream.isatty() + except Exception: + return False + + +def _make_cached_stream_func( + src_func: t.Callable[[], t.TextIO | None], + wrapper_func: t.Callable[[], t.TextIO], +) -> t.Callable[[], t.TextIO | None]: + cache: cabc.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary() + + def func() -> t.TextIO | None: + stream = src_func() + + if stream is None: + return None + + try: + rv = cache.get(stream) + except Exception: + rv = None + if rv is not None: + return rv + rv = wrapper_func() + try: + cache[stream] = rv + except Exception: + pass + return rv + + return func + + +_default_text_stdin = _make_cached_stream_func(lambda: sys.stdin, get_text_stdin) +_default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_stdout) +_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr) + + +binary_streams: cabc.Mapping[str, t.Callable[[], t.BinaryIO]] = { + "stdin": get_binary_stdin, + "stdout": get_binary_stdout, + "stderr": get_binary_stderr, +} + +text_streams: cabc.Mapping[str, t.Callable[[str | None, str | None], t.TextIO]] = { + "stdin": get_text_stdin, + "stdout": get_text_stdout, + "stderr": get_text_stderr, +} diff --git a/source/target-vendors/cp312/click/_termui_impl.py b/source/target-vendors/cp312/click/_termui_impl.py new file mode 100644 index 0000000..70aabf8 --- /dev/null +++ b/source/target-vendors/cp312/click/_termui_impl.py @@ -0,0 +1,972 @@ +""" +This module contains implementations for the termui module. To keep the +import time of Click down, some infrequently used functionality is +placed in this module and only imported as needed. +""" + +from __future__ import annotations + +import collections.abc as cabc +import contextlib +import math +import os +import shlex +import sys +import time +import typing as t +from gettext import gettext as _ +from io import StringIO +from pathlib import Path +from types import TracebackType + +from ._compat import _default_text_stdout +from ._compat import CYGWIN +from ._compat import get_best_encoding +from ._compat import isatty +from ._compat import strip_ansi +from ._compat import term_len +from ._compat import WIN +from .exceptions import ClickException +from .utils import echo + +V = t.TypeVar("V") + + +if os.name == "nt": + BEFORE_BAR = "\r" + AFTER_BAR = "\n" +else: + BEFORE_BAR = "\r\033[?25l" + AFTER_BAR = "\033[?25h\n" + + +class ProgressBar(t.Generic[V]): + def __init__( + self, + iterable: cabc.Iterable[V] | None, + length: int | None = None, + fill_char: str = "#", + empty_char: str = " ", + bar_template: str = "%(bar)s", + info_sep: str = " ", + hidden: bool = False, + show_eta: bool = True, + show_percent: bool | None = None, + show_pos: bool = False, + item_show_func: t.Callable[[V | None], str | None] | None = None, + label: str | None = None, + file: t.TextIO | None = None, + color: bool | None = None, + update_min_steps: int = 1, + width: int = 30, + ) -> None: + self.fill_char = fill_char + self.empty_char = empty_char + self.bar_template = bar_template + self.info_sep = info_sep + self.hidden = hidden + self.show_eta = show_eta + self.show_percent = show_percent + self.show_pos = show_pos + self.item_show_func = item_show_func + self.label: str = label or "" + + if file is None: + file = _default_text_stdout() + + # There are no standard streams attached to write to. For example, + # pythonw on Windows. + if file is None: + file = StringIO() + + self.file = file + self.color = color + self.update_min_steps = update_min_steps + self._completed_intervals = 0 + self.width: int = width + self.autowidth: bool = width == 0 + + if length is None: + from operator import length_hint + + length = length_hint(iterable, -1) + + if length == -1: + length = None + if iterable is None: + if length is None: + raise TypeError("iterable or length is required") + iterable = t.cast("cabc.Iterable[V]", range(length)) + self.iter: cabc.Iterable[V] = iter(iterable) + self.length = length + self.pos: int = 0 + self.avg: list[float] = [] + self.last_eta: float + self.start: float + self.start = self.last_eta = time.time() + self.eta_known: bool = False + self.finished: bool = False + self.max_width: int | None = None + self.entered: bool = False + self.current_item: V | None = None + self._is_atty = isatty(self.file) + self._last_line: str | None = None + + def __enter__(self) -> ProgressBar[V]: + self.entered = True + self.render_progress() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.render_finish() + + def __iter__(self) -> cabc.Iterator[V]: + if not self.entered: + raise RuntimeError("You need to use progress bars in a with block.") + self.render_progress() + return self.generator() + + def __next__(self) -> V: + # Iteration is defined in terms of a generator function, + # returned by iter(self); use that to define next(). This works + # because `self.iter` is an iterable consumed by that generator, + # so it is re-entry safe. Calling `next(self.generator())` + # twice works and does "what you want". + return next(iter(self)) + + def render_finish(self) -> None: + # Steps stay pending until they add up to update_min_steps, so a length + # that is not a multiple of it keeps a sub-threshold remainder that + # never reaches pos. Apply it so the bar settles on its true position. + if self._completed_intervals: + self.make_step(self._completed_intervals) + self._completed_intervals = 0 + self.render_progress() + + if self.hidden or not self._is_atty: + return + self.file.write(AFTER_BAR) + self.file.flush() + + @property + def pct(self) -> float: + if self.finished: + return 1.0 + return min(self.pos / (float(self.length or 1) or 1), 1.0) + + @property + def time_per_iteration(self) -> float: + if not self.avg: + return 0.0 + return sum(self.avg) / float(len(self.avg)) + + @property + def eta(self) -> float: + if self.length is not None and not self.finished: + return self.time_per_iteration * (self.length - self.pos) + return 0.0 + + def format_eta(self) -> str: + if self.eta_known: + t = int(self.eta) + seconds = t % 60 + t //= 60 + minutes = t % 60 + t //= 60 + hours = t % 24 + t //= 24 + if t > 0: + return "{d}{day_label} {h:02}:{m:02}:{s:02}".format( + d=t, + day_label=_("d"), + h=hours, + m=minutes, + s=seconds, + ) + else: + return f"{hours:02}:{minutes:02}:{seconds:02}" + return "" + + def format_pos(self) -> str: + pos = str(self.pos) + if self.length is not None: + pos += f"/{self.length}" + return pos + + def format_pct(self) -> str: + return f"{int(self.pct * 100): 4}%"[1:] + + def format_bar(self) -> str: + if self.length is not None: + bar_length = int(self.pct * self.width) + bar = self.fill_char * bar_length + bar += self.empty_char * (self.width - bar_length) + elif self.finished: + bar = self.fill_char * self.width + else: + chars = list(self.empty_char * (self.width or 1)) + if self.time_per_iteration != 0: + chars[ + int( + (math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5) + * self.width + ) + ] = self.fill_char + bar = "".join(chars) + return bar + + def format_progress_line(self) -> str: + show_percent = self.show_percent + + info_bits = [] + if self.length is not None and show_percent is None: + show_percent = not self.show_pos + + if self.show_pos: + info_bits.append(self.format_pos()) + if show_percent: + info_bits.append(self.format_pct()) + if self.show_eta and self.eta_known and not self.finished: + info_bits.append(self.format_eta()) + if self.item_show_func is not None: + item_info = self.item_show_func(self.current_item) + if item_info is not None: + info_bits.append(item_info) + + return ( + self.bar_template + % { + "label": self.label, + "bar": self.format_bar(), + "info": self.info_sep.join(info_bits), + } + ).rstrip() + + def render_progress(self) -> None: + if self.hidden: + return + + if not self._is_atty: + # Only output the label once if the output is not a TTY. + if self._last_line != self.label: + self._last_line = self.label + echo(self.label, file=self.file, color=self.color) + return + + buf = [] + # Update width in case the terminal has been resized + if self.autowidth: + import shutil + + old_width = self.width + self.width = 0 + clutter_length = term_len(self.format_progress_line()) + new_width = max(0, shutil.get_terminal_size().columns - clutter_length) + if new_width < old_width and self.max_width is not None: + buf.append(BEFORE_BAR) + buf.append(" " * self.max_width) + self.max_width = new_width + self.width = new_width + + clear_width = self.width + if self.max_width is not None: + clear_width = self.max_width + + buf.append(BEFORE_BAR) + line = self.format_progress_line() + line_len = term_len(line) + if self.max_width is None or self.max_width < line_len: + self.max_width = line_len + + buf.append(line) + buf.append(" " * (clear_width - line_len)) + line = "".join(buf) + # Render the line only if it changed. + + if line != self._last_line: + self._last_line = line + echo(line, file=self.file, color=self.color, nl=False) + self.file.flush() + + def make_step(self, n_steps: int) -> None: + self.pos += n_steps + if self.length is not None and self.pos >= self.length: + self.finished = True + + if (time.time() - self.last_eta) < 1.0: + return + + self.last_eta = time.time() + + # self.avg is a rolling list of length <= 7 of steps where steps are + # defined as time elapsed divided by the total progress through + # self.length. + if self.pos: + step = (time.time() - self.start) / self.pos + else: + step = time.time() - self.start + + self.avg = self.avg[-6:] + [step] + + self.eta_known = self.length is not None + + def update(self, n_steps: int, current_item: V | None = None) -> None: + """Update the progress bar by advancing a specified number of + steps, and optionally set the ``current_item`` for this new + position. + + :param n_steps: Number of steps to advance. + :param current_item: Optional item to set as ``current_item`` + for the updated position. + + .. versionchanged:: 8.0 + Added the ``current_item`` optional parameter. + + .. versionchanged:: 8.0 + Only render when the number of steps meets the + ``update_min_steps`` threshold. + """ + if current_item is not None: + self.current_item = current_item + + self._completed_intervals += n_steps + + if self._completed_intervals >= self.update_min_steps: + self.make_step(self._completed_intervals) + self.render_progress() + self._completed_intervals = 0 + + def finish(self) -> None: + self.eta_known = False + self.current_item = None + self.finished = True + + def generator(self) -> cabc.Iterator[V]: + """Return a generator which yields the items added to the bar + during construction, and updates the progress bar *after* the + yielded block returns. + """ + # WARNING: the iterator interface for `ProgressBar` relies on + # this and only works because this is a simple generator which + # doesn't create or manage additional state. If this function + # changes, the impact should be evaluated both against + # `iter(bar)` and `next(bar)`. `next()` in particular may call + # `self.generator()` repeatedly, and this must remain safe in + # order for that interface to work. + if not self.entered: + raise RuntimeError("You need to use progress bars in a with block.") + + if not self._is_atty: + yield from self.iter + else: + for rv in self.iter: + self.current_item = rv + + # This allows show_item_func to be updated before the + # item is processed. Only trigger at the beginning of + # the update interval. + if self._completed_intervals == 0: + self.render_progress() + + yield rv + self.update(1) + + self.finish() + self.render_progress() + + +class _PagerWriter: + """Wrap a pager's output stream to strip ANSI styling when colors are + disabled. + + The wrapped stream is owned by the pager strategy that produced it, so this + wrapper never closes it: ``_pipepager`` closes its pipe to signal EOF to the + pager process, ``_tempfilepager`` closes and removes its temporary file, and + ``_nullpager`` leaves an external stream such as ``sys.stdout`` untouched. + + The ``color`` attribute lets :func:`click.echo` detect that ANSI stripping + is handled here, so it doesn't strip a second time (see + :func:`._compat.should_strip_ansi`). + """ + + def __init__(self, stream: t.TextIO, color: bool) -> None: + self._stream = stream + self.color = color + + def write(self, text: str) -> int: + if not self.color: + text = strip_ansi(text) + + return self._stream.write(text) + + def writelines(self, lines: cabc.Iterable[str]) -> None: + for line in lines: + self.write(line) + + def flush(self) -> None: + self._stream.flush() + + def close(self) -> None: + # The pager strategy owns the stream's lifecycle. Flush pending output, + # but never close the underlying stream from here. + self._stream.flush() + + def __getattr__(self, name: str) -> t.Any: + return getattr(self._stream, name) + + +def _resolve_pager_command(cmd_parts: list[str]) -> tuple[Path, list[str]] | None: + """Resolve a pager ``argv`` to an absolute command path and its parameters. + + The command is looked up with :func:`shutil.which`, which the + :mod:`subprocess` docs recommend on every platform for an unqualified name: + https://docs.python.org/3/library/subprocess.html#popen-constructor + + Returns ``None`` when there is no command to run or it is not on the path, + leaving the caller to pick a fallback. + """ + if not cmd_parts: + return None + + import shutil + + cmd_filepath = shutil.which(cmd_parts[0]) + + if not cmd_filepath: + return None + + # Normalize to an absolute path without resolving symlinks: multi-call + # binaries such as busybox derive their identity from the link they are + # invoked through, so resolving less -> busybox makes them misbehave. + # See https://github.com/pallets/click/issues/2943 and + # https://github.com/pallets/click/pull/2944 + return Path(cmd_filepath).absolute(), cmd_parts[1:] + + +def _pager_contextmanager( + color: bool | None = None, +) -> t.ContextManager[tuple[t.TextIO, bool | None]]: + """Decide what method to use for paging through text.""" + stdout = _default_text_stdout() + + # There are no standard streams attached to write to. For example, + # pythonw on Windows. + if stdout is None: + stdout = StringIO() + + if not isatty(sys.stdin) or not isatty(stdout): + return _nullpager(stdout, color) + + # Split using POSIX mode (the default) so that quote characters are + # stripped from tokens and quoted Windows paths are preserved. + # Non-POSIX mode retains quotes in tokens, and wrapping tokens + # with shlex.quote re-introduces quoting issues on Windows. + pager_cmd_parts = shlex.split(os.environ.get("PAGER", "")) + + if pager_cmd_parts: + # Piping to `more` on Windows adds spurious \r\n, so it gets the temp + # file strategy whatever the user asked for. + use_tempfile = WIN + else: + if os.environ.get("TERM") in ("dumb", "emacs"): + return _nullpager(stdout, color) + + use_tempfile = WIN or sys.platform.startswith("os2") + pager_cmd_parts = ["more"] if use_tempfile else ["less"] + + resolved = _resolve_pager_command(pager_cmd_parts) + + if resolved is None: + # Page through stdout, which _nullpager leaves open for its owner. + return _nullpager(stdout, color) + + cmd_path, cmd_params = resolved + + if use_tempfile: + return _tempfilepager(cmd_path, cmd_params, color) + + return _pipepager(cmd_path, cmd_params, color) + + +@contextlib.contextmanager +def get_pager_file(color: bool | None = None) -> t.Generator[t.TextIO, None, None]: + """Context manager. + + Yields a writable file-like object which can be used as an output pager. + + .. versionadded:: 8.4.0 + + :param color: controls if the pager supports ANSI colors or not. The + default is autodetection. + """ + with _pager_contextmanager(color=color) as (stream, color): + # Every pager strategy yields a text stream, so the only thing left to + # do is strip ANSI styling when colors are disabled. A strategy yields + # None when it has no opinion on colors, which settles as off here. The + # wrapper does not close the stream: each strategy owns its stream's + # lifecycle. + writer = _PagerWriter(stream, color=bool(color)) + try: + yield t.cast(t.TextIO, writer) + finally: + writer.flush() + + +def _less_uses_raw_mode(less_env: str, cmd_params: list[str]) -> bool: + """Detect a raw-control-characters request in a ``less`` invocation. + + Parses the tokens of the ``LESS`` environment variable and the command + line: only short-option clusters (``-R``, ``-rX``, ``-XrY``) and the + ``--raw-control-chars`` long option request raw mode. Filenames and + long-option values may carry the letter ``r`` without being such a + request. + """ + try: + env_tokens = shlex.split(less_env) + except ValueError: + # Unbalanced quotes make ``LESS`` unparsable: fall back to whitespace + # splitting rather than failing the pager over it. + env_tokens = less_env.split() + tokens = env_tokens + cmd_params + + for token in tokens: + if token.startswith("--"): + if token.casefold() == "--raw-control-chars": + return True + elif token.startswith("-") and len(token) > 1: + if "r" in token[1:].casefold(): + return True + + return False + + +@contextlib.contextmanager +def _pipepager( + cmd_path: Path, cmd_params: list[str], color: bool | None = None +) -> t.Iterator[tuple[t.TextIO, bool | None]]: + """Page through text by feeding it to another program. + + Invokes the pager via :class:`subprocess.Popen` with an ``argv`` list. + + Invoking a pager through this might support colors: if piping to + ``less`` and the user hasn't decided on colors, ``LESS=-R`` is set + automatically. + """ + import subprocess + + # Make a local copy of the environment to not affect the global one. + env = dict(os.environ) + + # If we're piping to less and the user hasn't decided on colors, we enable + # them: either the invocation already requests raw control characters, or + # ``LESS=-R`` is injected. Match on the stem so a resolved ``less.exe`` is + # recognized like bare ``less``, case-insensitively on Windows since its + # filesystems are. POSIX keeps an exact match. + cmd_name = cmd_path.stem + if WIN: + # ``_pager_contextmanager`` currently routes every Windows invocation + # to ``_tempfilepager``, so this branch is not reachable on Windows + # today: it keeps the detection correct should that routing ever + # change. + cmd_name = cmd_name.casefold() + + if color is None and cmd_name == "less": + less_env = os.environ.get("LESS", "") + if _less_uses_raw_mode(less_env, cmd_params): + color = True + elif not less_env and not cmd_params: + env["LESS"] = "-R" + color = True + + c = subprocess.Popen( + [str(cmd_path)] + cmd_params, + shell=False, + stdin=subprocess.PIPE, + env=env, + errors="replace", + text=True, + ) + # With ``text=True``, ``c.stdin`` is already a text stream that encodes + # writes for the pager process (honoring ``errors="replace"``). + stdin = t.cast(t.TextIO, c.stdin) + try: + yield stdin, color + except BrokenPipeError: + # In case the pager exited unexpectedly, ignore the broken pipe error. + pass + except Exception as e: + # In case there is an exception we want to close the pager immediately + # and let the caller handle it. + # Otherwise the pager will keep running, and the user may not notice + # the error message, or worse yet it may leave the terminal in a broken state. + c.terminate() + raise e + finally: + # We must close stdin and wait for the pager to exit before we continue + try: + stdin.close() + # Close implies flush, so it might throw a BrokenPipeError if the pager + # process exited already. + except BrokenPipeError: + pass + + # Less doesn't respect ^C, but catches it for its own UI purposes (aborting + # search or other commands inside less). + # + # That means when the user hits ^C, the parent process (click) terminates, + # but less is still alive, paging the output and messing up the terminal. + # + # If the user wants to make the pager exit on ^C, they should set + # `LESS='-K'`. It's not our decision to make. + while True: + try: + c.wait() + except KeyboardInterrupt: + pass + else: + break + + +@contextlib.contextmanager +def _tempfilepager( + cmd_path: Path, cmd_params: list[str], color: bool | None = None +) -> t.Iterator[tuple[t.TextIO, bool | None]]: + """Page through text by invoking a program on a temporary file. + + Used as the primary pager strategy on Windows (where piping to + ``more`` adds spurious ``\\r\\n``), and as a fallback on other + platforms. The command is invoked with any parameters from ``PAGER`` + followed by the temporary file name. + """ + import subprocess + import tempfile + + encoding = get_best_encoding(sys.stdout) + # On Windows, NamedTemporaryFile cannot be opened by another process + # while Python still has it open, so we use delete=False and clean up manually + # rather than using a contextmanager here. + f = tempfile.NamedTemporaryFile( + mode="w", encoding=encoding, errors="replace", delete=False + ) + try: + yield t.cast(t.TextIO, f), color + f.flush() + f.close() + subprocess.call([str(cmd_path), *cmd_params, f.name]) + finally: + # An error raised while paging skips the close() above, and Windows + # refuses to unlink a file the process still holds open. Closing here + # keeps that PermissionError from replacing the original exception. + # close() is idempotent, so this is a no-op on the success path. + f.close() + os.unlink(f.name) + + +@contextlib.contextmanager +def _nullpager( + stream: t.TextIO, color: bool | None = None +) -> t.Iterator[tuple[t.TextIO, bool | None]]: + """Simply print unformatted text. This is the ultimate fallback. + + The stream comes from elsewhere (typically ``sys.stdout``), so its lifecycle + is left untouched: :class:`_PagerWriter` never closes what it wraps, and it + is the only thing :func:`get_pager_file` hands to the caller. + """ + yield stream, color + + +class Editor: + def __init__( + self, + editor: str | None = None, + env: cabc.Mapping[str, str] | None = None, + require_save: bool = True, + extension: str = ".txt", + ) -> None: + self.editor = editor + self.env = env + self.require_save = require_save + self.extension = extension + + def get_editor(self) -> str: + if self.editor is not None: + return self.editor + for key in "VISUAL", "EDITOR": + rv = os.environ.get(key) + if rv: + return rv + if WIN: + return "notepad" + + from shutil import which + + for editor in "sensible-editor", "vim", "nano": + if which(editor) is not None: + return editor + return "vi" + + def edit_files(self, filenames: cabc.Iterable[str | os.PathLike[str]]) -> None: + """Open files in the user's editor.""" + import shlex + import subprocess + + editor = self.get_editor() + environ: dict[str, str] | None = None + + if self.env: + environ = os.environ.copy() + environ.update(self.env) + + try: + # Split in POSIX mode (the default) for the same reasons as + # in pager(): strips quotes from tokens and preserves quoted + # Windows paths. + c = subprocess.Popen( + args=shlex.split(editor) + list(filenames), + env=environ, + ) + exit_code = c.wait() + if exit_code != 0: + raise ClickException( + _("{editor}: Editing failed").format(editor=editor) + ) + except OSError as e: + raise ClickException( + _("{editor}: Editing failed: {e}").format(editor=editor, e=e) + ) from e + + @t.overload + def edit(self, text: bytes | bytearray) -> bytes | None: ... + + # We cannot know whether or not the type expected is str or bytes when None + # is passed, so str is returned as that was what was done before. + @t.overload + def edit(self, text: str | None) -> str | None: ... + + def edit(self, text: str | bytes | bytearray | None) -> str | bytes | None: + import tempfile + + if text is None: + data: bytes | bytearray = b"" + elif isinstance(text, (bytes, bytearray)): + data = text + else: + if text and not text.endswith("\n"): + text += "\n" + + if WIN: + data = text.replace("\n", "\r\n").encode("utf-8-sig") + else: + data = text.encode("utf-8") + + fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension) + f: t.BinaryIO + + try: + with os.fdopen(fd, "wb") as f: + f.write(data) + + # If the filesystem resolution is 1 second, like Mac OS + # 10.12 Extended, or 2 seconds, like FAT32, and the editor + # closes very fast, require_save can fail. Set the modified + # time to be 2 seconds in the past to work around this. + os.utime(name, (os.path.getatime(name), os.path.getmtime(name) - 2)) + # Depending on the resolution, the exact value might not be + # recorded, so get the new recorded value. + timestamp = os.path.getmtime(name) + + self.edit_files((name,)) + + if self.require_save and os.path.getmtime(name) == timestamp: + return None + + with open(name, "rb") as f: + rv = f.read() + + if isinstance(text, (bytes, bytearray)): + return rv + + return rv.decode("utf-8-sig").replace("\r\n", "\n") + finally: + os.unlink(name) + + +def open_url(url: str, wait: bool = False, locate: bool = False) -> int: + import subprocess + + def _unquote_file(url: str) -> str: + from urllib.parse import unquote + + if url.startswith("file://"): + url = unquote(url[7:]) + + return url + + if sys.platform == "darwin": + args = ["open"] + if wait: + args.append("-W") + if locate: + args.append("-R") + args.append(_unquote_file(url)) + null = open("/dev/null", "w") + try: + return subprocess.Popen(args, stderr=null).wait() + finally: + null.close() + elif WIN: + if locate: + url = _unquote_file(url) + args = ["explorer", "/select,", url] + try: + return subprocess.call(args) + except OSError: + return 127 + else: + try: + os.startfile(url) # type: ignore[attr-defined] + except OSError: + return 127 + return 0 + elif CYGWIN: + if locate: + url = _unquote_file(url) + args = ["cygstart", os.path.dirname(url)] + else: + args = ["cygstart"] + if wait: + args.append("-w") + args.append(url) + try: + return subprocess.call(args) + except OSError: + # Command not found + return 127 + + try: + if locate: + url = os.path.dirname(_unquote_file(url)) or "." + else: + url = _unquote_file(url) + c = subprocess.Popen(["xdg-open", url]) + if wait: + return c.wait() + return 0 + except OSError: + if url.startswith(("http://", "https://")) and not locate and not wait: + import webbrowser + + webbrowser.open(url) + return 0 + return 1 + + +def _translate_ch_to_exc(ch: str) -> None: + if ch == "\x03": + raise KeyboardInterrupt() + + if ch == "\x04" and not WIN: # Unix-like, Ctrl+D + raise EOFError() + + if ch == "\x1a" and WIN: # Windows, Ctrl+Z + raise EOFError() + + +if sys.platform == "win32": + import msvcrt + + @contextlib.contextmanager + def raw_terminal() -> cabc.Iterator[int]: + yield -1 + + def getchar(echo: bool) -> str: + # The function `getch` will return a bytes object corresponding to + # the pressed character. Since Windows 10 build 1803, it will also + # return \x00 when called a second time after pressing a regular key. + # + # `getwch` does not share this probably-bugged behavior. Moreover, it + # returns a Unicode object by default, which is what we want. + # + # Either of these functions will return \x00 or \xe0 to indicate + # a special key, and you need to call the same function again to get + # the "rest" of the code. The fun part is that \u00e0 is + # "latin small letter a with grave", so if you type that on a French + # keyboard, you _also_ get a \xe0. + # E.g., consider the Up arrow. This returns \xe0 and then \x48. The + # resulting Unicode string reads as "a with grave" + "capital H". + # This is indistinguishable from when the user actually types + # "a with grave" and then "capital H". + # + # When \xe0 is returned, we assume it's part of a special-key sequence + # and call `getwch` again, but that means that when the user types + # the \u00e0 character, `getchar` doesn't return until a second + # character is typed. + # The alternative is returning immediately, but that would mess up + # cross-platform handling of arrow keys and others that start with + # \xe0. Another option is using `getch`, but then we can't reliably + # read non-ASCII characters, because return values of `getch` are + # limited to the current 8-bit codepage. + # + # Anyway, Click doesn't claim to do this Right(tm), and using `getwch` + # is doing the right thing in more situations than with `getch`. + + if echo: + func = t.cast(t.Callable[[], str], msvcrt.getwche) + else: + func = t.cast(t.Callable[[], str], msvcrt.getwch) + + rv = func() + + if rv in ("\x00", "\xe0"): + # \x00 and \xe0 are control characters that indicate special key, + # see above. + rv += func() + + _translate_ch_to_exc(rv) + return rv + +else: + import termios + import tty + + @contextlib.contextmanager + def raw_terminal() -> cabc.Iterator[int]: + f: t.TextIO | None + fd: int + + if not isatty(sys.stdin): + f = open("/dev/tty") + fd = f.fileno() + else: + fd = sys.stdin.fileno() + f = None + + try: + old_settings = termios.tcgetattr(fd) + + try: + tty.setraw(fd) + yield fd + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + sys.stdout.flush() + + if f is not None: + f.close() + except termios.error: + pass + + def getchar(echo: bool) -> str: + with raw_terminal() as fd: + ch = os.read(fd, 32).decode(get_best_encoding(sys.stdin), "replace") + + if echo and isatty(sys.stdout): + sys.stdout.write(ch) + + _translate_ch_to_exc(ch) + return ch diff --git a/source/target-vendors/cp312/click/_textwrap.py b/source/target-vendors/cp312/click/_textwrap.py new file mode 100644 index 0000000..82840f2 --- /dev/null +++ b/source/target-vendors/cp312/click/_textwrap.py @@ -0,0 +1,188 @@ +from __future__ import annotations + +import collections.abc as cabc +import textwrap +from contextlib import contextmanager + +from ._compat import _ansi_re +from ._compat import term_len + + +def _truncate_visible(text: str, n: int) -> str: + """Return the longest prefix of ``text`` containing at most ``n`` visible + characters. + + ANSI escape sequences inside the prefix are kept intact and do not count + toward the visible width. A cut is never placed inside an escape sequence. + """ + if n <= 0: + return "" + + visible = 0 + i = 0 + cut = 0 + end = len(text) + while i < end: + m = _ansi_re.match(text, i) + if m is not None: + i = m.end() + continue + visible += 1 + i += 1 + cut = i + if visible >= n: + break + return text[:cut] + + +class TextWrapper(textwrap.TextWrapper): + """``textwrap.TextWrapper`` variant that measures widths by visible + character count. + + ANSI escape sequences embedded in chunks, indents, or the placeholder are + excluded from the width budget. Without this, styled help text (a styled + ``Usage:`` prefix, a colorized option name, ...) would be wrapped earlier + than its visible length warrants and tokens would split mid-word. + """ + + def _handle_long_word( + self, + reversed_chunks: list[str], + cur_line: list[str], + cur_len: int, + width: int, + ) -> None: + space_left = max(width - cur_len, 1) + + if self.break_long_words: + last = reversed_chunks[-1] + cut = _truncate_visible(last, space_left) + res = last[len(cut) :] + cur_line.append(cut) + reversed_chunks[-1] = res + elif not cur_line: + cur_line.append(reversed_chunks.pop()) + + def _wrap_chunks(self, chunks: list[str]) -> list[str]: + """Wrap chunks counting widths in visible characters. + + Mirrors the algorithm of :meth:`textwrap.TextWrapper._wrap_chunks` + with every width measurement routed through + :func:`click._compat.term_len` instead of :func:`len`, so ANSI escape + bytes in chunks, indents, or the placeholder do not inflate the count. + + .. seealso:: + :class:`textwrap.TextWrapper` in the Python standard library documentation: + https://docs.python.org/3/library/textwrap.html#textwrap.TextWrapper + + Reference implementation in CPython: + https://github.com/python/cpython/blob/main/Lib/textwrap.py + """ + lines: list[str] = [] + if self.width <= 0: + raise ValueError(f"invalid width {self.width!r} (must be > 0)") + if self.max_lines is not None: + if self.max_lines > 1: + indent = self.subsequent_indent + else: + indent = self.initial_indent + if term_len(indent) + term_len(self.placeholder.lstrip()) > self.width: + raise ValueError("placeholder too large for max width") + + chunks.reverse() + + while chunks: + cur_line: list[str] = [] + cur_len = 0 + + if lines: + indent = self.subsequent_indent + else: + indent = self.initial_indent + + width = self.width - term_len(indent) + + if self.drop_whitespace and chunks[-1].strip() == "" and lines: + del chunks[-1] + + while chunks: + n = term_len(chunks[-1]) + + if cur_len + n <= width: + cur_line.append(chunks.pop()) + cur_len += n + + else: + break + + if chunks and term_len(chunks[-1]) > width: + self._handle_long_word(chunks, cur_line, cur_len, width) + cur_len = sum(map(term_len, cur_line)) + + if self.drop_whitespace and cur_line and cur_line[-1].strip() == "": + cur_len -= term_len(cur_line[-1]) + del cur_line[-1] + + if cur_line: + if ( + self.max_lines is None + or len(lines) + 1 < self.max_lines + or ( + not chunks + or self.drop_whitespace + and len(chunks) == 1 + and not chunks[0].strip() + ) + and cur_len <= width + ): + lines.append(indent + "".join(cur_line)) + else: + while cur_line: + if ( + cur_line[-1].strip() + and cur_len + term_len(self.placeholder) <= width + ): + cur_line.append(self.placeholder) + lines.append(indent + "".join(cur_line)) + break + cur_len -= term_len(cur_line[-1]) + del cur_line[-1] + else: + if lines: + prev_line = lines[-1].rstrip() + if ( + term_len(prev_line) + term_len(self.placeholder) + <= self.width + ): + lines[-1] = prev_line + self.placeholder + break + lines.append(indent + self.placeholder.lstrip()) + break + + return lines + + @contextmanager + def extra_indent(self, indent: str) -> cabc.Iterator[None]: + old_initial_indent = self.initial_indent + old_subsequent_indent = self.subsequent_indent + self.initial_indent += indent + self.subsequent_indent += indent + + try: + yield + finally: + self.initial_indent = old_initial_indent + self.subsequent_indent = old_subsequent_indent + + def indent_only(self, text: str) -> str: + rv = [] + + for idx, line in enumerate(text.splitlines()): + indent = self.initial_indent + + if idx > 0: + indent = self.subsequent_indent + + rv.append(f"{indent}{line}") + + return "\n".join(rv) diff --git a/source/target-vendors/cp312/click/_utils.py b/source/target-vendors/cp312/click/_utils.py new file mode 100644 index 0000000..05ee2e9 --- /dev/null +++ b/source/target-vendors/cp312/click/_utils.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import enum +import typing as t + + +class Sentinel(enum.Enum): + """Enum used to define sentinel values. + + .. seealso:: + + `PEP 661 - Sentinel Values `_. + """ + + UNSET = object() + FLAG_NEEDS_VALUE = object() + + def __repr__(self) -> str: + return f"{self.__class__.__name__}.{self.name}" + + +UNSET: t.Literal[Sentinel.UNSET] = Sentinel.UNSET +"""Sentinel used to indicate that a value is not set.""" + +FLAG_NEEDS_VALUE: t.Literal[Sentinel.FLAG_NEEDS_VALUE] = Sentinel.FLAG_NEEDS_VALUE +"""Sentinel used to indicate an option was passed as a flag without a +value but is not a flag option. + +``Option.consume_value`` uses this to prompt or use the ``flag_value``. +""" + +T_UNSET: t.TypeAlias = t.Literal[Sentinel.UNSET] +"""Type hint for the :data:`UNSET` sentinel value.""" + +T_FLAG_NEEDS_VALUE: t.TypeAlias = t.Literal[Sentinel.FLAG_NEEDS_VALUE] +"""Type hint for the :data:`FLAG_NEEDS_VALUE` sentinel value.""" diff --git a/source/target-vendors/cp312/click/_winconsole.py b/source/target-vendors/cp312/click/_winconsole.py new file mode 100644 index 0000000..d25178d --- /dev/null +++ b/source/target-vendors/cp312/click/_winconsole.py @@ -0,0 +1,297 @@ +# This module is based on the excellent work by Adam Bartoš who +# provided a lot of what went into the implementation here in +# the discussion to issue1602 in the Python bug tracker. +# +# There are some general differences in regards to how this works +# compared to the original patches as we do not need to patch +# the entire interpreter but just work in our little world of +# echo and prompt. +from __future__ import annotations + +import collections.abc as cabc +import io +import sys +import time +import typing as t +from ctypes import Array +from ctypes import byref +from ctypes import c_char +from ctypes import c_char_p +from ctypes import c_int +from ctypes import c_ssize_t +from ctypes import c_ulong +from ctypes import c_void_p +from ctypes import POINTER +from ctypes import py_object +from ctypes import Structure +from ctypes.wintypes import DWORD +from ctypes.wintypes import HANDLE +from ctypes.wintypes import LPCWSTR +from ctypes.wintypes import LPWSTR +from gettext import gettext as _ + +from ._compat import _NonClosingTextIOWrapper + +assert sys.platform == "win32" +import msvcrt # noqa: E402 +from ctypes import windll # noqa: E402 +from ctypes import WINFUNCTYPE # noqa: E402 + +c_ssize_p = POINTER(c_ssize_t) + +kernel32 = windll.kernel32 +GetStdHandle = kernel32.GetStdHandle +ReadConsoleW = kernel32.ReadConsoleW +WriteConsoleW = kernel32.WriteConsoleW +GetConsoleMode = kernel32.GetConsoleMode +GetLastError = kernel32.GetLastError +GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32)) +CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))( + ("CommandLineToArgvW", windll.shell32) +) +LocalFree = WINFUNCTYPE(c_void_p, c_void_p)(("LocalFree", windll.kernel32)) + +STDIN_HANDLE = GetStdHandle(-10) +STDOUT_HANDLE = GetStdHandle(-11) +STDERR_HANDLE = GetStdHandle(-12) + +PyBUF_SIMPLE = 0 +PyBUF_WRITABLE = 1 + +ERROR_SUCCESS = 0 +ERROR_NOT_ENOUGH_MEMORY = 8 +ERROR_OPERATION_ABORTED = 995 + +STDIN_FILENO = 0 +STDOUT_FILENO = 1 +STDERR_FILENO = 2 + +EOF = b"\x1a" +MAX_BYTES_WRITTEN = 32767 + +if t.TYPE_CHECKING: + try: + # Using `typing_extensions.Buffer` instead of `collections.abc` + # on Windows for some reason does not have `Sized` implemented. + from collections.abc import Buffer # type: ignore + except ImportError: + from typing_extensions import Buffer + +try: + from ctypes import pythonapi +except ImportError: + # On PyPy we cannot get buffers so our ability to operate here is + # severely limited. + get_buffer = None +else: + + class Py_buffer(Structure): + _fields_ = [ # noqa: RUF012 + ("buf", c_void_p), + ("obj", py_object), + ("len", c_ssize_t), + ("itemsize", c_ssize_t), + ("readonly", c_int), + ("ndim", c_int), + ("format", c_char_p), + ("shape", c_ssize_p), + ("strides", c_ssize_p), + ("suboffsets", c_ssize_p), + ("internal", c_void_p), + ] + + PyObject_GetBuffer = pythonapi.PyObject_GetBuffer + PyBuffer_Release = pythonapi.PyBuffer_Release + + def get_buffer(obj: Buffer, writable: bool = False) -> Array[c_char]: + buf = Py_buffer() + flags: int = PyBUF_WRITABLE if writable else PyBUF_SIMPLE + PyObject_GetBuffer(py_object(obj), byref(buf), flags) + + try: + buffer_type = c_char * buf.len + out: Array[c_char] = buffer_type.from_address(buf.buf) + return out + finally: + PyBuffer_Release(byref(buf)) + + +class _WindowsConsoleRawIOBase(io.RawIOBase): + def __init__(self, handle: int | None) -> None: + self.handle = handle + + def isatty(self) -> t.Literal[True]: + super().isatty() + return True + + +class _WindowsConsoleReader(_WindowsConsoleRawIOBase): + def readable(self) -> t.Literal[True]: + return True + + def readinto(self, b: Buffer) -> int: + bytes_to_be_read = len(b) + if not bytes_to_be_read: + return 0 + elif bytes_to_be_read % 2: + raise ValueError( + "cannot read odd number of bytes from UTF-16-LE encoded console" + ) + + buffer = get_buffer(b, writable=True) + code_units_to_be_read = bytes_to_be_read // 2 + code_units_read = c_ulong() + + rv = ReadConsoleW( + HANDLE(self.handle), + buffer, + code_units_to_be_read, + byref(code_units_read), + None, + ) + if GetLastError() == ERROR_OPERATION_ABORTED: + # wait for KeyboardInterrupt + time.sleep(0.1) + if not rv: + raise OSError(_("Windows error: {error}").format(error=GetLastError())) + + if buffer[0] == EOF: + return 0 + return 2 * code_units_read.value + + +class _WindowsConsoleWriter(_WindowsConsoleRawIOBase): + def writable(self) -> t.Literal[True]: + return True + + @staticmethod + def _get_error_message(errno: int) -> str: + if errno == ERROR_SUCCESS: + return "ERROR_SUCCESS" + elif errno == ERROR_NOT_ENOUGH_MEMORY: + return "ERROR_NOT_ENOUGH_MEMORY" + return _("Windows error: {error}").format(error=errno) + + def write(self, b: Buffer) -> int: + bytes_to_be_written = len(b) + buf = get_buffer(b) + code_units_to_be_written = min(bytes_to_be_written, MAX_BYTES_WRITTEN) // 2 + code_units_written = c_ulong() + + WriteConsoleW( + HANDLE(self.handle), + buf, + code_units_to_be_written, + byref(code_units_written), + None, + ) + bytes_written = 2 * code_units_written.value + + if bytes_written == 0 and bytes_to_be_written > 0: + raise OSError(self._get_error_message(GetLastError())) + return bytes_written + + +class ConsoleStream: + def __init__(self, text_stream: t.TextIO, byte_stream: t.BinaryIO) -> None: + self._text_stream = text_stream + self.buffer = byte_stream + + @property + def name(self) -> str: + return self.buffer.name + + def write(self, x: t.AnyStr) -> int: + if isinstance(x, str): + return self._text_stream.write(x) + try: + self.flush() + except Exception: + pass + return self.buffer.write(x) + + def writelines(self, lines: cabc.Iterable[t.AnyStr]) -> None: + for line in lines: + self.write(line) + + def __getattr__(self, name: str) -> t.Any: + return getattr(self._text_stream, name) + + def isatty(self) -> bool: + return self.buffer.isatty() + + def __repr__(self) -> str: + return f"" + + +def _get_text_stdin(buffer_stream: t.BinaryIO) -> t.TextIO: + text_stream = _NonClosingTextIOWrapper( + io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)), + "utf-16-le", + "strict", + line_buffering=True, + ) + return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream)) + + +def _get_text_stdout(buffer_stream: t.BinaryIO) -> t.TextIO: + text_stream = _NonClosingTextIOWrapper( + io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)), + "utf-16-le", + "strict", + line_buffering=True, + ) + return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream)) + + +def _get_text_stderr(buffer_stream: t.BinaryIO) -> t.TextIO: + text_stream = _NonClosingTextIOWrapper( + io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)), + "utf-16-le", + "strict", + line_buffering=True, + ) + return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream)) + + +_stream_factories: cabc.Mapping[int, t.Callable[[t.BinaryIO], t.TextIO]] = { + 0: _get_text_stdin, + 1: _get_text_stdout, + 2: _get_text_stderr, +} + + +def _is_console(f: t.TextIO) -> bool: + if not hasattr(f, "fileno"): + return False + + try: + fileno = f.fileno() + except (OSError, io.UnsupportedOperation): + return False + + handle = msvcrt.get_osfhandle(fileno) + return bool(GetConsoleMode(handle, byref(DWORD()))) + + +def _get_windows_console_stream( + f: t.TextIO, encoding: str | None, errors: str | None +) -> t.TextIO | None: + if ( + get_buffer is None + or encoding not in {"utf-16-le", None} + or errors not in {"strict", None} + or not _is_console(f) + ): + return None + + func = _stream_factories.get(f.fileno()) + if func is None: + return None + + b = getattr(f, "buffer", None) + + if b is None: + return None + + return func(b) diff --git a/source/target-vendors/cp312/click/core.py b/source/target-vendors/cp312/click/core.py new file mode 100644 index 0000000..de129ec --- /dev/null +++ b/source/target-vendors/cp312/click/core.py @@ -0,0 +1,3799 @@ +from __future__ import annotations + +import collections.abc as cabc +import enum +import errno +import inspect +import os +import sys +import typing as t +from abc import ABC +from abc import abstractmethod +from collections import abc +from collections import Counter +from contextlib import AbstractContextManager +from contextlib import contextmanager +from contextlib import ExitStack +from functools import update_wrapper +from gettext import gettext as _ +from gettext import ngettext +from itertools import repeat +from types import TracebackType + +from . import types +from ._utils import FLAG_NEEDS_VALUE +from ._utils import UNSET +from .exceptions import Abort +from .exceptions import BadParameter +from .exceptions import ClickException +from .exceptions import Exit +from .exceptions import MissingParameter +from .exceptions import NoArgsIsHelpError +from .exceptions import NoSuchCommand +from .exceptions import UsageError +from .formatting import HelpFormatter +from .formatting import join_options +from .globals import pop_context +from .globals import push_context +from .parser import _OptionParser +from .parser import _split_opt +from .termui import confirm +from .termui import prompt +from .termui import style +from .utils import _detect_program_name +from .utils import _expand_args +from .utils import _make_default_short_help +from .utils import _PacifyFlushWrapper +from .utils import echo +from .utils import make_str + +if t.TYPE_CHECKING: + from typing_extensions import Self + + from .shell_completion import CompletionItem + +F = t.TypeVar("F", bound="t.Callable[..., t.Any]") +V = t.TypeVar("V") + +# Reserved storage name of the automatic help option. No user parameter is +# expected to claim it. +_HELP_OPTION_STORAGE_NAME = "_click_default_help" + + +def _complete_visible_commands( + ctx: Context, incomplete: str +) -> cabc.Iterator[tuple[str, Command]]: + """List all the subcommands of a group that start with the + incomplete value and aren't hidden. + + :param ctx: Invocation context for the group. + :param incomplete: Value being completed. May be empty. + """ + multi = t.cast(Group, ctx.command) + + for name in multi.list_commands(ctx): + if name.startswith(incomplete): + command = multi.get_command(ctx, name) + + if command is not None and not command.hidden: + yield name, command + + +def _check_nested_chain( + base_command: Group, cmd_name: str, cmd: Command, register: bool = False +) -> None: + if not base_command.chain or not isinstance(cmd, Group): + return + + if register: + message = ( + f"It is not possible to add the group {cmd_name!r} to another" + f" group {base_command.name!r} that is in chain mode." + ) + else: + message = ( + f"Found the group {cmd_name!r} as subcommand to another group " + f" {base_command.name!r} that is in chain mode. This is not supported." + ) + + raise RuntimeError(message) + + +def _format_deprecated_label(deprecated: bool | str) -> str: + """Return the parenthesized deprecation label shown in help text.""" + label = _("deprecated").upper() + if isinstance(deprecated, str): + return f"({label}: {deprecated})" + return f"({label})" + + +def _format_deprecated_suffix(deprecated: bool | str) -> str: + """Return the trailing reason for a ``DeprecationWarning`` message, + prefixed with a space, or an empty string when no reason was given. + """ + if isinstance(deprecated, str): + return f" {deprecated}" + return "" + + +def batch(iterable: cabc.Iterable[V], batch_size: int) -> list[tuple[V, ...]]: + return list(zip(*repeat(iter(iterable), batch_size), strict=False)) + + +@contextmanager +def augment_usage_errors( + ctx: Context, param: Parameter | None = None +) -> cabc.Generator[None]: + """Context manager that attaches extra information to exceptions.""" + try: + yield + except BadParameter as e: + if e.ctx is None: + e.ctx = ctx + if param is not None and e.param is None: + e.param = param + raise + except UsageError as e: + if e.ctx is None: + e.ctx = ctx + raise + + +def iter_params_for_processing( + invocation_order: cabc.Sequence[Parameter], + declaration_order: cabc.Sequence[Parameter], +) -> list[Parameter]: + """Returns all declared parameters in the order they should be processed. + + The declared parameters are re-shuffled depending on the order in which + they were invoked, as well as the eagerness of each parameters. + + The invocation order takes precedence over the declaration order. I.e. the + order in which the user provided them to the CLI is respected. + + This behavior and its effect on callback evaluation is detailed at: + https://click.palletsprojects.com/en/stable/advanced/#callback-evaluation-order + """ + + def sort_key(item: Parameter) -> tuple[bool, float]: + try: + idx: float = invocation_order.index(item) + except ValueError: + idx = float("inf") + + return not item.is_eager, idx + + return sorted(declaration_order, key=sort_key) + + +class ParameterSource(enum.IntEnum): + """This is an :class:`~enum.IntEnum` that indicates the source of a + parameter's value. + + Use :meth:`click.Context.get_parameter_source` to get the + source for a parameter by name. + + Members are ordered from most explicit to least explicit source. + This allows comparison to check if a value was explicitly provided: + + .. code-block:: python + + source = ctx.get_parameter_source("port") + if source < click.ParameterSource.DEFAULT_MAP: + ... # value was explicitly set + + .. versionchanged:: 8.3.3 + Use :class:`~enum.IntEnum` and reorder members from most to + least explicit. Supports comparison operators. + + .. versionchanged:: 8.0 + Use :class:`~enum.Enum` and drop the ``validate`` method. + + .. versionchanged:: 8.0 + Added the ``PROMPT`` value. + """ + + PROMPT = enum.auto() + """Used a prompt to confirm a default or provide a value.""" + COMMANDLINE = enum.auto() + """The value was provided by the command line args.""" + ENVIRONMENT = enum.auto() + """The value was provided with an environment variable.""" + DEFAULT_MAP = enum.auto() + """Used a default provided by :attr:`Context.default_map`.""" + DEFAULT = enum.auto() + """Used the default specified by the parameter.""" + + +class Context: + """The context is a special internal object that holds state relevant + for the script execution at every single level. It's normally invisible + to commands unless they opt-in to getting access to it. + + The context is useful as it can pass internal objects around and can + control special execution features such as reading data from + environment variables. + + A context can be used as context manager in which case it will call + :meth:`close` on teardown. + + :param command: the command class for this context. + :param parent: the parent context. + :param info_name: the info name for this invocation. Generally this + is the most descriptive name for the script or + command. For the toplevel script it is usually + the name of the script, for commands below that it's + the name of the script. + :param obj: an arbitrary object of user data. + :param auto_envvar_prefix: the prefix to use for automatic environment + variables. If this is `None` then reading + from environment variables is disabled. This + does not affect manually set environment + variables which are always read. + :param default_map: a dictionary (like object) with default values + for parameters. + :param terminal_width: the width of the terminal. The default is + inherit from parent context. If no context + defines the terminal width then auto + detection will be applied. + :param max_content_width: the maximum width for content rendered by + Click (this currently only affects help + pages). This defaults to 80 characters if + not overridden. In other words: even if the + terminal is larger than that, Click will not + format things wider than 80 characters by + default. In addition to that, formatters might + add some safety mapping on the right. + :param resilient_parsing: if this flag is enabled then Click will + parse without any interactivity or callback + invocation. Default values will also be + ignored. This is useful for implementing + things such as completion support. + :param allow_extra_args: if this is set to `True` then extra arguments + at the end will not raise an error and will be + kept on the context. The default is to inherit + from the command. + :param allow_interspersed_args: if this is set to `False` then options + and arguments cannot be mixed. The + default is to inherit from the command. + :param ignore_unknown_options: instructs click to ignore options it does + not know and keeps them for later + processing. + :param help_option_names: optionally a list of strings that define how + the default help parameter is named. The + default is ``['--help']``. + :param token_normalize_func: an optional function that is used to + normalize tokens (options, choices, + etc.). This for instance can be used to + implement case insensitive behavior. + :param color: controls if the terminal supports ANSI colors or not. The + default is autodetection. This is only needed if ANSI + codes are used in texts that Click prints which is by + default not the case. This for instance would affect + help output. + :param show_default: Show the default value for commands. If this + value is not set, it defaults to the value from the parent + context. ``Command.show_default`` overrides this default for the + specific command. + + .. versionchanged:: 8.2 + The ``protected_args`` attribute is deprecated and will be removed in + Click 9.0. ``args`` will contain remaining unparsed tokens. + + .. versionchanged:: 8.1 + The ``show_default`` parameter is overridden by + ``Command.show_default``, instead of the other way around. + + .. versionchanged:: 8.0 + The ``show_default`` parameter defaults to the value from the + parent context. + + .. versionchanged:: 7.1 + Added the ``show_default`` parameter. + + .. versionchanged:: 4.0 + Added the ``color``, ``ignore_unknown_options``, and + ``max_content_width`` parameters. + + .. versionchanged:: 3.0 + Added the ``allow_extra_args`` and ``allow_interspersed_args`` + parameters. + + .. versionchanged:: 2.0 + Added the ``resilient_parsing``, ``help_option_names``, and + ``token_normalize_func`` parameters. + """ + + #: The formatter class to create with :meth:`make_formatter`. + #: + #: .. versionadded:: 8.0 + formatter_class: type[HelpFormatter] = HelpFormatter + + parent: Context | None + command: Command + info_name: str | None + params: dict[str, t.Any] + args: list[str] + _protected_args: list[str] + _opt_prefixes: set[str] + obj: t.Any + _meta: dict[str, t.Any] + default_map: cabc.MutableMapping[str, t.Any] | None + invoked_subcommand: str | None + terminal_width: int | None + max_content_width: int | None + allow_extra_args: bool + allow_interspersed_args: bool + ignore_unknown_options: bool + help_option_names: list[str] + token_normalize_func: t.Callable[[str], str] | None + resilient_parsing: bool + auto_envvar_prefix: str | None + color: bool | None + show_default: bool | None + _close_callbacks: list[t.Callable[[], t.Any]] + _depth: int + _parameter_source: dict[str, ParameterSource] + _param_default_explicit: dict[str, bool] + _exit_stack: ExitStack + + def __init__( + self, + command: Command, + parent: Context | None = None, + info_name: str | None = None, + obj: t.Any | None = None, + auto_envvar_prefix: str | None = None, + default_map: cabc.MutableMapping[str, t.Any] | None = None, + terminal_width: int | None = None, + max_content_width: int | None = None, + resilient_parsing: bool = False, + allow_extra_args: bool | None = None, + allow_interspersed_args: bool | None = None, + ignore_unknown_options: bool | None = None, + help_option_names: list[str] | None = None, + token_normalize_func: t.Callable[[str], str] | None = None, + color: bool | None = None, + show_default: bool | None = None, + ) -> None: + #: the parent context or `None` if none exists. + self.parent = parent + #: the :class:`Command` for this context. + self.command = command + #: the descriptive information name + self.info_name = info_name + #: Map of parameter names to their parsed values. Parameters + #: with ``expose_value=False`` are not stored. + self.params = {} + #: the leftover arguments. + self.args = [] + #: protected arguments. These are arguments that are prepended + #: to `args` when certain parsing scenarios are encountered but + #: must be never propagated to another arguments. This is used + #: to implement nested parsing. + self._protected_args = [] + #: the collected prefixes of the command's options. + self._opt_prefixes = set(parent._opt_prefixes) if parent else set() + + if obj is None and parent is not None: + obj = parent.obj + + #: the user object stored. + self.obj = obj + self._meta = getattr(parent, "meta", {}) + + #: A dictionary (-like object) with defaults for parameters. + if ( + default_map is None + and info_name is not None + and parent is not None + and parent.default_map is not None + ): + default_map = parent.default_map.get(info_name) + + self.default_map = default_map + + #: This flag indicates if a subcommand is going to be executed. A + #: group callback can use this information to figure out if it's + #: being executed directly or because the execution flow passes + #: onwards to a subcommand. By default it's None, but it can be + #: the name of the subcommand to execute. + #: + #: If chaining is enabled this will be set to ``'*'`` in case + #: any commands are executed. It is however not possible to + #: figure out which ones. If you require this knowledge you + #: should use a :func:`result_callback`. + self.invoked_subcommand = None + + if terminal_width is None and parent is not None: + terminal_width = parent.terminal_width + + #: The width of the terminal (None is autodetection). + self.terminal_width = terminal_width + + if max_content_width is None and parent is not None: + max_content_width = parent.max_content_width + + #: The maximum width of formatted content (None implies a sensible + #: default which is 80 for most things). + self.max_content_width = max_content_width + + if allow_extra_args is None: + allow_extra_args = command.allow_extra_args + + #: Indicates if the context allows extra args or if it should + #: fail on parsing. + #: + #: .. versionadded:: 3.0 + self.allow_extra_args = allow_extra_args + + if allow_interspersed_args is None: + allow_interspersed_args = command.allow_interspersed_args + + #: Indicates if the context allows mixing of arguments and + #: options or not. + #: + #: .. versionadded:: 3.0 + self.allow_interspersed_args = allow_interspersed_args + + if ignore_unknown_options is None: + ignore_unknown_options = command.ignore_unknown_options + + #: Instructs click to ignore options that a command does not + #: understand and will store it on the context for later + #: processing. This is primarily useful for situations where you + #: want to call into external programs. Generally this pattern is + #: strongly discouraged because it's not possibly to losslessly + #: forward all arguments. + #: + #: .. versionadded:: 4.0 + self.ignore_unknown_options = ignore_unknown_options + + if help_option_names is None: + if parent is not None: + help_option_names = parent.help_option_names + else: + help_option_names = ["--help"] + + #: The names for the help options. + self.help_option_names = help_option_names + + if token_normalize_func is None and parent is not None: + token_normalize_func = parent.token_normalize_func + + #: An optional normalization function for tokens. This is + #: options, choices, commands etc. + self.token_normalize_func = token_normalize_func + + #: Indicates if resilient parsing is enabled. In that case Click + #: will do its best to not cause any failures and default values + #: will be ignored. Useful for completion. + self.resilient_parsing = resilient_parsing + + # If there is no envvar prefix yet, but the parent has one and + # the command on this level has a name, we can expand the envvar + # prefix automatically. + if auto_envvar_prefix is None: + if ( + parent is not None + and parent.auto_envvar_prefix is not None + and self.info_name is not None + ): + auto_envvar_prefix = ( + f"{parent.auto_envvar_prefix}_{self.info_name.upper()}" + ) + else: + auto_envvar_prefix = auto_envvar_prefix.upper() + + if auto_envvar_prefix is not None: + auto_envvar_prefix = auto_envvar_prefix.replace("-", "_") + + self.auto_envvar_prefix = auto_envvar_prefix + + if color is None and parent is not None: + color = parent.color + + #: Controls if styling output is wanted or not. + self.color = color + + if show_default is None and parent is not None: + show_default = parent.show_default + + #: Show option default values when formatting help text. + self.show_default = show_default + + self._close_callbacks = [] + self._depth = 0 + self._parameter_source = {} + # Tracks whether the option that currently owns each parameter slot in + # :attr:`params` had its ``default`` set explicitly by the user. Used + # to tie-break feature-switch groups where multiple options share a + # parameter name and both fall back to their default value. + # Refs: https://github.com/pallets/click/issues/3403 + self._param_default_explicit = {} + self._exit_stack = ExitStack() + + @property + def protected_args(self) -> list[str]: + import warnings + + warnings.warn( + "'protected_args' is deprecated and will be removed in Click 9.0." + " 'args' will contain remaining unparsed tokens.", + DeprecationWarning, + stacklevel=2, + ) + return self._protected_args + + def to_info_dict(self) -> dict[str, t.Any]: + """Gather information that could be useful for a tool generating + user-facing documentation. This traverses the entire CLI + structure. + + .. code-block:: python + + with Context(cli) as ctx: + info = ctx.to_info_dict() + + .. versionadded:: 8.0 + """ + return { + "command": self.command.to_info_dict(self), + "info_name": self.info_name, + "allow_extra_args": self.allow_extra_args, + "allow_interspersed_args": self.allow_interspersed_args, + "ignore_unknown_options": self.ignore_unknown_options, + "auto_envvar_prefix": self.auto_envvar_prefix, + } + + def __enter__(self) -> Self: + self._depth += 1 + push_context(self) + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> bool | None: + self._depth -= 1 + exit_result: bool | None = None + if self._depth == 0: + exit_result = self._close_with_exception_info(exc_type, exc_value, tb) + pop_context() + + return exit_result + + @contextmanager + def scope(self, cleanup: bool = True) -> cabc.Generator[Context]: + """This helper method can be used with the context object to promote + it to the current thread local (see :func:`get_current_context`). + The default behavior of this is to invoke the cleanup functions which + can be disabled by setting `cleanup` to `False`. The cleanup + functions are typically used for things such as closing file handles. + + If the cleanup is intended the context object can also be directly + used as a context manager. + + Example usage:: + + with ctx.scope(): + assert get_current_context() is ctx + + This is equivalent:: + + with ctx: + assert get_current_context() is ctx + + .. versionadded:: 5.0 + + :param cleanup: controls if the cleanup functions should be run or + not. The default is to run these functions. In + some situations the context only wants to be + temporarily pushed in which case this can be disabled. + Nested pushes automatically defer the cleanup. + """ + if not cleanup: + self._depth += 1 + try: + with self as rv: + yield rv + finally: + if not cleanup: + self._depth -= 1 + + @property + def meta(self) -> dict[str, t.Any]: + """This is a dictionary which is shared with all the contexts + that are nested. It exists so that click utilities can store some + state here if they need to. It is however the responsibility of + that code to manage this dictionary well. + + The keys are supposed to be unique dotted strings. For instance + module paths are a good choice for it. What is stored in there is + irrelevant for the operation of click. However what is important is + that code that places data here adheres to the general semantics of + the system. + + Example usage:: + + LANG_KEY = f'{__name__}.lang' + + def set_language(value): + ctx = get_current_context() + ctx.meta[LANG_KEY] = value + + def get_language(): + return get_current_context().meta.get(LANG_KEY, 'en_US') + + .. versionadded:: 5.0 + """ + return self._meta + + def make_formatter(self) -> HelpFormatter: + """Creates the :class:`~click.HelpFormatter` for the help and + usage output. + + To quickly customize the formatter class used without overriding + this method, set the :attr:`formatter_class` attribute. + + .. versionchanged:: 8.0 + Added the :attr:`formatter_class` attribute. + """ + return self.formatter_class( + width=self.terminal_width, max_width=self.max_content_width + ) + + def with_resource(self, context_manager: AbstractContextManager[V]) -> V: + """Register a resource as if it were used in a ``with`` + statement. The resource will be cleaned up when the context is + popped. + + Uses :meth:`contextlib.ExitStack.enter_context`. It calls the + resource's ``__enter__()`` method and returns the result. When + the context is popped, it closes the stack, which calls the + resource's ``__exit__()`` method. + + To register a cleanup function for something that isn't a + context manager, use :meth:`call_on_close`. Or use something + from :mod:`contextlib` to turn it into a context manager first. + + .. code-block:: python + + @click.group() + @click.option("--name") + @click.pass_context + def cli(ctx): + ctx.obj = ctx.with_resource(connect_db(name)) + + :param context_manager: The context manager to enter. + :return: Whatever ``context_manager.__enter__()`` returns. + + .. versionadded:: 8.0 + """ + return self._exit_stack.enter_context(context_manager) + + def call_on_close(self, f: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: + """Register a function to be called when the context tears down. + + This can be used to close resources opened during the script + execution. Resources that support Python's context manager + protocol which would be used in a ``with`` statement should be + registered with :meth:`with_resource` instead. + + :param f: The function to execute on teardown. + """ + return self._exit_stack.callback(f) + + def close(self) -> None: + """Invoke all close callbacks registered with + :meth:`call_on_close`, and exit all context managers entered + with :meth:`with_resource`. + """ + self._close_with_exception_info(None, None, None) + + def _close_with_exception_info( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> bool | None: + """Unwind the exit stack by calling its :meth:`__exit__` providing the exception + information to allow for exception handling by the various resources registered + using :meth;`with_resource` + + :return: Whatever ``exit_stack.__exit__()`` returns. + """ + exit_result = self._exit_stack.__exit__(exc_type, exc_value, tb) + # In case the context is reused, create a new exit stack. + self._exit_stack = ExitStack() + + return exit_result + + @property + def command_path(self) -> str: + """The computed command path. This is used for the ``usage`` + information on the help page. It's automatically created by + combining the info names of the chain of contexts to the root. + """ + rv = "" + if self.info_name is not None: + rv = self.info_name + if self.parent is not None: + parent_command_path = [self.parent.command_path] + + if isinstance(self.parent.command, Command): + for param in self.parent.command.get_params(self): + parent_command_path.extend(param.get_usage_pieces(self)) + + rv = f"{' '.join(parent_command_path)} {rv}" + return rv.lstrip() + + def find_root(self) -> Context: + """Finds the outermost context.""" + node = self + while node.parent is not None: + node = node.parent + return node + + def find_object(self, object_type: type[V]) -> V | None: + """Finds the closest object of a given type.""" + node: Context | None = self + + while node is not None: + if isinstance(node.obj, object_type): + return node.obj + + node = node.parent + + return None + + def ensure_object(self, object_type: type[V]) -> V: + """Like :meth:`find_object` but sets the innermost object to a + new instance of `object_type` if it does not exist. + """ + rv = self.find_object(object_type) + if rv is None: + self.obj = rv = object_type() + return rv + + def _default_map_has(self, name: str | None) -> bool: + """Check if :attr:`default_map` contains a real value for ``name``. + + Returns ``False`` when the key is absent, the map is ``None``, + ``name`` is ``None``, or the stored value is the internal + :data:`UNSET` sentinel. + """ + return ( + name is not None + and self.default_map is not None + and name in self.default_map + and self.default_map[name] is not UNSET + ) + + @t.overload + def lookup_default( + self, name: str, call: t.Literal[True] = True + ) -> t.Any | None: ... + + @t.overload + def lookup_default( + self, name: str, call: t.Literal[False] = ... + ) -> t.Any | t.Callable[[], t.Any] | None: ... + + def lookup_default(self, name: str, call: bool = True) -> t.Any | None: + """Get the default for a parameter from :attr:`default_map`. + + :param name: Name of the parameter. + :param call: If the default is a callable, call it. Disable to + return the callable instead. + + .. versionchanged:: 8.0 + Added the ``call`` parameter. + """ + if not self._default_map_has(name): + return None + + # Assert to make the type checker happy. + assert self.default_map is not None + value = self.default_map[name] + + if call and callable(value): + return value() + + return value + + def fail(self, message: str) -> t.NoReturn: + """Aborts the execution of the program with a specific error + message. + + :param message: the error message to fail with. + """ + raise UsageError(message, self) + + def abort(self) -> t.NoReturn: + """Aborts the script.""" + raise Abort() + + def exit(self, code: int = 0) -> t.NoReturn: + """Exits the application with a given exit code. + + .. versionchanged:: 8.2 + Callbacks and context managers registered with :meth:`call_on_close` + and :meth:`with_resource` are closed before exiting. + """ + self.close() + raise Exit(code) + + def get_usage(self) -> str: + """Helper method to get formatted usage string for the current + context and command. + """ + return self.command.get_usage(self) + + def get_help(self) -> str: + """Helper method to get formatted help page for the current + context and command. + """ + return self.command.get_help(self) + + def _make_sub_context(self, command: Command) -> Context: + """Create a new context of the same type as this context, but + for a new command. + + :meta private: + """ + return type(self)(command, info_name=command.name, parent=self) + + @t.overload + def invoke( + self, callback: t.Callable[..., V], /, *args: t.Any, **kwargs: t.Any + ) -> V: ... + + @t.overload + def invoke(self, callback: Command, /, *args: t.Any, **kwargs: t.Any) -> t.Any: ... + + def invoke( + self, callback: Command | t.Callable[..., V], /, *args: t.Any, **kwargs: t.Any + ) -> t.Any | V: + """Invokes a command callback in exactly the way it expects. There + are two ways to invoke this method: + + 1. the first argument can be a callback and all other arguments and + keyword arguments are forwarded directly to the function. + 2. the first argument is a click command object. In that case all + arguments are forwarded as well but proper click parameters + (options and click arguments) must be keyword arguments and Click + will fill in defaults. + + .. versionchanged:: 8.0 + All ``kwargs`` are tracked in :attr:`params` so they will be + passed if :meth:`forward` is called at multiple levels. + + .. versionchanged:: 3.2 + A new context is created, and missing arguments use default values. + """ + if isinstance(callback, Command): + other_cmd = callback + + if other_cmd.callback is None: + raise TypeError( + "The given command does not have a callback that can be invoked." + ) + else: + callback = t.cast("t.Callable[..., V]", other_cmd.callback) + + ctx = self._make_sub_context(other_cmd) + + for param in other_cmd.params: + if param.name not in kwargs and param.expose_value: + default_value = param.get_default(ctx) + # We explicitly hide the :attr:`UNSET` value to the user, as we + # choose to make it an implementation detail. And because ``invoke`` + # has been designed as part of Click public API, we return ``None`` + # instead. Refs: + # https://github.com/pallets/click/issues/3066 + # https://github.com/pallets/click/issues/3065 + # https://github.com/pallets/click/pull/3068 + if default_value is UNSET: + default_value = None + kwargs[param.name] = param.type_cast_value(ctx, default_value) + + # Track all kwargs as params, so that forward() will pass + # them on in subsequent calls. + ctx.params.update(kwargs) + else: + ctx = self + + with augment_usage_errors(self), ctx: + return callback(*args, **kwargs) + + def forward(self, cmd: Command, /, *args: t.Any, **kwargs: t.Any) -> t.Any: + """Similar to :meth:`invoke` but fills in default keyword + arguments from the current context if the other command expects + it. This cannot invoke callbacks directly, only other commands. + + .. versionchanged:: 8.0 + All ``kwargs`` are tracked in :attr:`params` so they will be + passed if ``forward`` is called at multiple levels. + """ + # Can only forward to other commands, not direct callbacks. + if not isinstance(cmd, Command): + raise TypeError("Callback is not a command.") + + for param in self.params: + if param not in kwargs: + kwargs[param] = self.params[param] + + return self.invoke(cmd, *args, **kwargs) + + def set_parameter_source(self, name: str, source: ParameterSource) -> None: + """Set the source of a parameter. This indicates the location + from which the value of the parameter was obtained. + + :param name: The name of the parameter. + :param source: A member of :class:`~click.core.ParameterSource`. + """ + self._parameter_source[name] = source + + def get_parameter_source(self, name: str) -> ParameterSource | None: + """Get the source of a parameter. This indicates the location + from which the value of the parameter was obtained. + + This can be useful for determining when a user specified a value + on the command line that is the same as the default value. It + will be :attr:`~click.core.ParameterSource.DEFAULT` only if the + value was actually taken from the default. + + :param name: The name of the parameter. + :rtype: ParameterSource + + .. versionchanged:: 8.0 + Returns ``None`` if the parameter was not provided from any + source. + """ + return self._parameter_source.get(name) + + +class Command: + """Commands are the basic building block of command line interfaces in + Click. A basic command handles command line parsing and might dispatch + more parsing to commands nested below it. + + :param name: the name of the command to use unless a group overrides it. + :param context_settings: an optional dictionary with defaults that are + passed to the context object. + :param callback: the callback to invoke. This is optional. + :param params: the parameters to register with this command. This can + be either :class:`Option` or :class:`Argument` objects. + :param help: the help string to use for this command. + :param epilog: like the help string but it's printed at the end of the + help page after everything else. + :param short_help: the short help to use for this command. This is + shown on the command listing of the parent command. + :param add_help_option: by default each command registers a ``--help`` + option. This can be disabled by this parameter. + :param no_args_is_help: this controls what happens if no arguments are + provided. This option is disabled by default. + If enabled this will add ``--help`` as argument + if no arguments are passed + :param hidden: hide this command from help outputs. + :param deprecated: If ``True`` or non-empty string, issues a message + indicating that the command is deprecated and highlights + its deprecation in --help. The message can be customized + by using a string as the value. + + .. versionchanged:: 8.2 + This is the base class for all commands, not ``BaseCommand``. + ``deprecated`` can be set to a string as well to customize the + deprecation message. + + .. versionchanged:: 8.1 + ``help``, ``epilog``, and ``short_help`` are stored unprocessed, + all formatting is done when outputting help text, not at init, + and is done even if not using the ``@command`` decorator. + + .. versionchanged:: 8.0 + Added a ``repr`` showing the command name. + + .. versionchanged:: 7.1 + Added the ``no_args_is_help`` parameter. + + .. versionchanged:: 2.0 + Added the ``context_settings`` parameter. + """ + + #: The context class to create with :meth:`make_context`. + #: + #: .. versionadded:: 8.0 + context_class: type[Context] = Context + + #: the default for the :attr:`Context.allow_extra_args` flag. + allow_extra_args = False + + #: the default for the :attr:`Context.allow_interspersed_args` flag. + allow_interspersed_args = True + + #: the default for the :attr:`Context.ignore_unknown_options` flag. + ignore_unknown_options = False + + name: str | None + context_settings: cabc.MutableMapping[str, t.Any] + callback: t.Callable[..., t.Any] | None + params: list[Parameter] + help: str | None + epilog: str | None + options_metavar: str | None + short_help: str | None + add_help_option: bool + _help_option: Option | None + no_args_is_help: bool + hidden: bool + deprecated: bool | str + + def __init__( + self, + name: str | None, + context_settings: cabc.MutableMapping[str, t.Any] | None = None, + callback: t.Callable[..., t.Any] | None = None, + params: list[Parameter] | None = None, + help: str | None = None, + epilog: str | None = None, + short_help: str | None = None, + options_metavar: str | None = "[OPTIONS]", + add_help_option: bool = True, + no_args_is_help: bool = False, + hidden: bool = False, + deprecated: bool | str = False, + ) -> None: + #: the name the command thinks it has. Upon registering a command + #: on a :class:`Group` the group will default the command name + #: with this information. You should instead use the + #: :class:`Context`\'s :attr:`~Context.info_name` attribute. + self.name = name + + if context_settings is None: + context_settings = {} + + #: an optional dictionary with defaults passed to the context. + self.context_settings = context_settings + + #: the callback to execute when the command fires. This might be + #: `None` in which case nothing happens. + self.callback = callback + #: the list of parameters for this command in the order they + #: should show up in the help page and execute. Eager parameters + #: will automatically be handled before non eager ones. + self.params = params or [] + self.help = help + self.epilog = epilog + self.options_metavar = options_metavar + self.short_help = short_help + self.add_help_option = add_help_option + self._help_option = None + self.no_args_is_help = no_args_is_help + self.hidden = hidden + self.deprecated = deprecated + + def to_info_dict(self, ctx: Context) -> dict[str, t.Any]: + return { + "name": self.name, + "params": [param.to_info_dict() for param in self.get_params(ctx)], + "help": self.help, + "epilog": self.epilog, + "short_help": self.short_help, + "hidden": self.hidden, + "deprecated": self.deprecated, + } + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self.name}>" + + def get_usage(self, ctx: Context) -> str: + """Formats the usage line into a string and returns it. + + Calls :meth:`format_usage` internally. + """ + formatter = ctx.make_formatter() + self.format_usage(ctx, formatter) + return formatter.getvalue().rstrip("\n") + + def get_params(self, ctx: Context) -> list[Parameter]: + params = self.params + help_option = self.get_help_option(ctx) + + if help_option is not None: + params = [*params, help_option] + + if __debug__: + import warnings + + opts = [opt for param in params for opt in param.opts] + opts_counter = Counter(opts) + duplicate_opts = (opt for opt, count in opts_counter.items() if count > 1) + + for duplicate_opt in duplicate_opts: + warnings.warn( + ( + f"The parameter {duplicate_opt} is used more than once. " + "Remove its duplicate as parameters should be unique." + ), + stacklevel=3, + ) + + # Options may deliberately share a storage name to compete for + # the same value (feature switches), but an argument sharing a + # name silently overwrites the other parameter's value. + names_counter = Counter(param.name for param in params) + duplicate_names = ( + name for name, count in names_counter.items() if count > 1 + ) + + for duplicate_name in duplicate_names: + sharers = [param for param in params if param.name == duplicate_name] + + if help_option in sharers: + warnings.warn( + ( + f"The name {duplicate_name!r} is reserved for the " + "automatic help option. Give the parameter a " + "different name." + ), + stacklevel=3, + ) + elif any(isinstance(param, Argument) for param in sharers): + warnings.warn( + ( + f"The name {duplicate_name!r} is used by an argument " + "and another parameter. They will overwrite each " + "other's value during parsing. Give each parameter " + "a unique name." + ), + stacklevel=3, + ) + + return params + + def format_usage(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the usage line into the formatter. + + This is a low-level method called by :meth:`get_usage`. + """ + pieces = self.collect_usage_pieces(ctx) + formatter.write_usage(ctx.command_path, " ".join(pieces)) + + def collect_usage_pieces(self, ctx: Context) -> list[str]: + """Returns all the pieces that go into the usage line and returns + it as a list of strings. + """ + rv = [self.options_metavar] if self.options_metavar else [] + + for param in self.get_params(ctx): + rv.extend(param.get_usage_pieces(ctx)) + + return rv + + def get_help_option_names(self, ctx: Context) -> list[str]: + """Returns the names for the help option. + + Drops duplicates and names already reserved by another parameter. Order of + :attr:`Context.help_option_names` is preserved, so the result is stable. + + .. versionchanged:: 8.5.0 + Names keep their declaration order. + """ + all_names = dict.fromkeys(ctx.help_option_names) + for param in self.params: + for name in (*param.opts, *param.secondary_opts): + all_names.pop(name, None) + return list(all_names) + + def get_help_option(self, ctx: Context) -> Option | None: + """Returns the help option object. + + Skipped if :attr:`add_help_option` is ``False``. + + .. versionchanged:: 8.5.0 + The help option stores its value under the reserved name + ``_click_default_help``, so a parameter named ``help`` no + longer breaks parsing. + + .. versionchanged:: 8.1.8 + The help option is now cached to avoid creating it multiple times. + """ + help_option_names = self.get_help_option_names(ctx) + + if not help_option_names or not self.add_help_option: + return None + + # Cache the help option object in private _help_option attribute to + # avoid creating it multiple times. Not doing this will break the + # callback ordering by iter_params_for_processing(), which relies on + # object comparison. + if self._help_option is None: + # Avoid circular import. + from .decorators import help_option + + # The help option never exposes its value, so it uses a reserved + # storage name, keeping it clear of user parameters (like an + # argument named "help") that would otherwise clobber its value. + help_option(*help_option_names, _HELP_OPTION_STORAGE_NAME)(self) + self._help_option = self.params.pop() # type: ignore[assignment] + + return self._help_option + + def make_parser(self, ctx: Context) -> _OptionParser: + """Creates the underlying option parser for this command.""" + parser = _OptionParser(ctx) + for param in self.get_params(ctx): + param.add_to_parser(parser, ctx) + return parser + + def get_help(self, ctx: Context) -> str: + """Formats the help into a string and returns it. + + Calls :meth:`format_help` internally. + """ + formatter = ctx.make_formatter() + self.format_help(ctx, formatter) + return formatter.getvalue().rstrip("\n") + + def get_short_help_str(self, limit: int = 45) -> str: + """Gets short help for the command or makes it by shortening the + long help string. + """ + if self.short_help: + text = inspect.cleandoc(self.short_help) + elif self.help: + text = _make_default_short_help(self.help, limit) + else: + text = "" + + if self.deprecated: + text = f"{_(text)} {_format_deprecated_label(self.deprecated)}" + + return text.strip() + + def format_help(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the help into the formatter if it exists. + + This is a low-level method called by :meth:`get_help`. + + This calls the following methods: + + - :meth:`format_usage` + - :meth:`format_help_text` + - :meth:`format_arguments` + - :meth:`format_options` + - :meth:`format_epilog` + """ + self.format_usage(ctx, formatter) + self.format_help_text(ctx, formatter) + self.format_arguments(ctx, formatter) + self.format_options(ctx, formatter) + self.format_epilog(ctx, formatter) + + def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the help text to the formatter if it exists.""" + if self.help is not None: + # truncate the help text to the first form feed + text = inspect.cleandoc(self.help).partition("\f")[0] + else: + text = "" + + if self.deprecated: + label = _format_deprecated_label(self.deprecated) + text = f"{_(text)} {label}" if text else label + + if text: + formatter.write_paragraph() + + with formatter.indentation(): + formatter.write_text(text) + + def format_options(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes all the options into the formatter if they exist.""" + opts = [] + for param in self.get_params(ctx): + rv = param.get_help_record(ctx) + if rv is not None and not isinstance(param, Argument): + opts.append(rv) + + if opts: + with formatter.section(_("Options")): + formatter.write_dl(opts) + + def format_arguments(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the arguments that have a help record into the formatter.""" + args = [] + for param in self.get_params(ctx): + rv = param.get_help_record(ctx) + if rv is not None and isinstance(param, Argument): + args.append(rv) + + if args: + with formatter.section(_("Positional arguments")): + formatter.write_dl(args) + + def format_epilog(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the epilog into the formatter if it exists.""" + if self.epilog: + epilog = inspect.cleandoc(self.epilog) + formatter.write_paragraph() + + with formatter.indentation(): + formatter.write_text(epilog) + + def make_context( + self, + info_name: str | None, + args: list[str], + parent: Context | None = None, + **extra: t.Any, + ) -> Context: + """This function when given an info name and arguments will kick + off the parsing and create a new :class:`Context`. It does not + invoke the actual command callback though. + + To quickly customize the context class used without overriding + this method, set the :attr:`context_class` attribute. + + :param info_name: the info name for this invocation. Generally this + is the most descriptive name for the script or + command. For the toplevel script it's usually + the name of the script, for commands below it's + the name of the command. + :param args: the arguments to parse as list of strings. + :param parent: the parent context if available. + :param extra: extra keyword arguments forwarded to the context + constructor. + + .. versionchanged:: 8.0 + Added the :attr:`context_class` attribute. + """ + for key, value in self.context_settings.items(): + if key not in extra: + extra[key] = value + + ctx = self.context_class(self, info_name=info_name, parent=parent, **extra) + + with ctx.scope(cleanup=False): + self.parse_args(ctx, args) + return ctx + + def parse_args(self, ctx: Context, args: list[str]) -> list[str]: + if not args and self.no_args_is_help and not ctx.resilient_parsing: + raise NoArgsIsHelpError(ctx) + + parser = self.make_parser(ctx) + opts, args, param_order = parser.parse_args(args=args) + + for param in iter_params_for_processing(param_order, self.get_params(ctx)): + _, args = param.handle_parse_result(ctx, opts, args) + + # We now have all parameters' values into `ctx.params`, but the data may contain + # the `UNSET` sentinel. + # Convert `UNSET` to `None` to ensure that the user doesn't see `UNSET`. + # + # Waiting until after the initial parse to convert allows us to treat `UNSET` + # more like a missing value when multiple params use the same name. + # Refs: + # https://github.com/pallets/click/issues/3071 + # https://github.com/pallets/click/pull/3079 + for name, value in ctx.params.items(): + if value is UNSET: + ctx.params[name] = None + + if args and not ctx.allow_extra_args and not ctx.resilient_parsing: + ctx.fail( + ngettext( + "Got unexpected extra argument ({args})", + "Got unexpected extra arguments ({args})", + len(args), + ).format(args=" ".join(map(str, args))) + ) + + ctx.args = args + ctx._opt_prefixes.update(parser._opt_prefixes) + return args + + def invoke(self, ctx: Context) -> t.Any: + """Given a context, this invokes the attached callback (if it exists) + in the right way. + """ + if self.deprecated: + message = _( + "DeprecationWarning: The command {name!r} is deprecated.{extra_message}" + ).format( + name=self.name, + extra_message=_format_deprecated_suffix(self.deprecated), + ) + echo(style(message, fg="red"), err=True) + + if self.callback is not None: + return ctx.invoke(self.callback, **ctx.params) + + def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]: + """Return a list of completions for the incomplete value. Looks + at the names of options and chained multi-commands. + + Any command could be part of a chained multi-command, so sibling + commands are valid at any point during command completion. + + :param ctx: Invocation context for this command. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + results: list[CompletionItem] = [] + + if incomplete and not incomplete[0].isalnum(): + for param in self.get_params(ctx): + if ( + not isinstance(param, Option) + or param.hidden + or ( + not param.multiple + and ctx.get_parameter_source(param.name) + is ParameterSource.COMMANDLINE + ) + ): + continue + + results.extend( + CompletionItem(name, help=param.help) + for name in [*param.opts, *param.secondary_opts] + if name.startswith(incomplete) + ) + + while ctx.parent is not None: + ctx = ctx.parent + + if isinstance(ctx.command, Group) and ctx.command.chain: + results.extend( + CompletionItem(name, help=command.get_short_help_str()) + for name, command in _complete_visible_commands(ctx, incomplete) + if name not in ctx._protected_args + ) + + return results + + @t.overload + def main( + self, + args: cabc.Sequence[str] | None = None, + prog_name: str | None = None, + complete_var: str | None = None, + standalone_mode: t.Literal[True] = True, + **extra: t.Any, + ) -> t.NoReturn: ... + + @t.overload + def main( + self, + args: cabc.Sequence[str] | None = None, + prog_name: str | None = None, + complete_var: str | None = None, + standalone_mode: bool = ..., + **extra: t.Any, + ) -> t.Any: ... + + def main( + self, + args: cabc.Sequence[str] | None = None, + prog_name: str | None = None, + complete_var: str | None = None, + standalone_mode: bool = True, + windows_expand_args: bool = True, + **extra: t.Any, + ) -> t.Any: + """This is the way to invoke a script with all the bells and + whistles as a command line application. This will always terminate + the application after a call. If this is not wanted, ``SystemExit`` + needs to be caught. + + This method is also available by directly calling the instance of + a :class:`Command`. + + :param args: the arguments that should be used for parsing. If not + provided, ``sys.argv[1:]`` is used. + :param prog_name: the program name that should be used. By default + the program name is constructed by taking the file + name from ``sys.argv[0]``. + :param complete_var: the environment variable that controls the + bash completion support. The default is + ``"__COMPLETE"`` with prog_name in + uppercase. + :param standalone_mode: the default behavior is to invoke the script + in standalone mode. Click will then + handle exceptions and convert them into + error messages and the function will never + return but shut down the interpreter. If + this is set to `False` they will be + propagated to the caller and the return + value of this function is the return value + of :meth:`invoke`. + :param windows_expand_args: Expand glob patterns, user dir, and + env vars in command line args on Windows. + :param extra: extra keyword arguments are forwarded to the context + constructor. See :class:`Context` for more information. + + .. versionchanged:: 8.0.1 + Added the ``windows_expand_args`` parameter to allow + disabling command line arg expansion on Windows. + + .. versionchanged:: 8.0 + When taking arguments from ``sys.argv`` on Windows, glob + patterns, user dir, and env vars are expanded. + + .. versionchanged:: 3.0 + Added the ``standalone_mode`` parameter. + """ + if args is None: + args = sys.argv[1:] + + if os.name == "nt" and windows_expand_args: + args = _expand_args(args) + else: + args = list(args) + + if prog_name is None: + prog_name = _detect_program_name() + + # Process shell completion requests and exit early. + self._main_shell_completion(extra, prog_name, complete_var) + + try: + try: + with self.make_context(prog_name, args, **extra) as ctx: + rv = self.invoke(ctx) + if not standalone_mode: + return rv + # it's not safe to `ctx.exit(rv)` here! + # note that `rv` may actually contain data like "1" which + # has obvious effects + # more subtle case: `rv=[None, None]` can come out of + # chained commands which all returned `None` -- so it's not + # even always obvious that `rv` indicates success/failure + # by its truthiness/falsiness + ctx.exit() + except (EOFError, KeyboardInterrupt) as e: + echo(file=sys.stderr) + raise Abort() from e + except ClickException as e: + if not standalone_mode: + raise + e.show() + sys.exit(e.exit_code) + except OSError as e: + if e.errno == errno.EPIPE: + sys.stdout = t.cast(t.TextIO, _PacifyFlushWrapper(sys.stdout)) + sys.stderr = t.cast(t.TextIO, _PacifyFlushWrapper(sys.stderr)) + sys.exit(1) + else: + raise + except Exit as e: + if standalone_mode: + sys.exit(e.exit_code) + else: + # in non-standalone mode, return the exit code + # note that this is only reached if `self.invoke` above raises + # an Exit explicitly -- thus bypassing the check there which + # would return its result + # the results of non-standalone execution may therefore be + # somewhat ambiguous: if there are codepaths which lead to + # `ctx.exit(1)` and to `return 1`, the caller won't be able to + # tell the difference between the two + return e.exit_code + except Abort: + if not standalone_mode: + raise + echo(_("Aborted!"), file=sys.stderr) + sys.exit(1) + + def _main_shell_completion( + self, + ctx_args: cabc.MutableMapping[str, t.Any], + prog_name: str, + complete_var: str | None = None, + ) -> None: + """Check if the shell is asking for tab completion, process + that, then exit early. Called from :meth:`main` before the + program is invoked. + + :param prog_name: Name of the executable in the shell. + :param complete_var: Name of the environment variable that holds + the completion instruction. Defaults to + ``_{PROG_NAME}_COMPLETE``. + + .. versionchanged:: 8.2.0 + Dots (``.``) in ``prog_name`` are replaced with underscores (``_``). + """ + if complete_var is None: + complete_name = prog_name.replace("-", "_").replace(".", "_") + complete_var = f"_{complete_name}_COMPLETE".upper() + + instruction = os.environ.get(complete_var) + + if not instruction: + return + + from .shell_completion import shell_complete + + rv = shell_complete(self, ctx_args, prog_name, complete_var, instruction) + sys.exit(rv) + + def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any: + """Alias for :meth:`main`.""" + return self.main(*args, **kwargs) + + +class _FakeSubclassCheck(type): + def __subclasscheck__(cls, subclass: type) -> bool: + return issubclass(subclass, cls.__bases__[0]) + + def __instancecheck__(cls, instance: t.Any) -> bool: + return isinstance(instance, cls.__bases__[0]) + + +class _BaseCommand(Command, metaclass=_FakeSubclassCheck): + """ + .. deprecated:: 8.2 + Will be removed in Click 9.0. Use ``Command`` instead. + """ + + +class Group(Command): + """A group is a command that nests other commands (or more groups). + + :param name: The name of the group command. + :param commands: Map names to :class:`Command` objects. Can be a list, which + will use :attr:`Command.name` as the keys. + :param invoke_without_command: Invoke the group's callback even if a + subcommand is not given. + :param no_args_is_help: If no arguments are given, show the group's help and + exit. Defaults to the opposite of ``invoke_without_command``. + :param subcommand_metavar: How to represent the subcommand argument in help. + The default will represent whether ``chain`` is set or not. + :param chain: Allow passing more than one subcommand argument. After parsing + a command's arguments, if any arguments remain another command will be + matched, and so on. + :param result_callback: A function to call after the group's and + subcommand's callbacks. The value returned by the subcommand is passed. + If ``chain`` is enabled, the value will be a list of values returned by + all the commands. If ``invoke_without_command`` is enabled, the value + will be the value returned by the group's callback, or an empty list if + ``chain`` is enabled. + :param kwargs: Other arguments passed to :class:`Command`. + + .. versionchanged:: 8.0 + The ``commands`` argument can be a list of command objects. + + .. versionchanged:: 8.2 + Merged with and replaces the ``MultiCommand`` base class. + """ + + allow_extra_args = True + allow_interspersed_args = False + + #: If set, this is used by the group's :meth:`command` decorator + #: as the default :class:`Command` class. This is useful to make all + #: subcommands use a custom command class. + #: + #: .. versionadded:: 8.0 + command_class: type[Command] | None = None + + #: If set, this is used by the group's :meth:`group` decorator + #: as the default :class:`Group` class. This is useful to make all + #: subgroups use a custom group class. + #: + #: If set to the special value :class:`type` (literally + #: ``group_class = type``), this group's class will be used as the + #: default class. This makes a custom group class continue to make + #: custom groups. + #: + #: .. versionadded:: 8.0 + group_class: type[Group | type] | None = None + # Literal[type] isn't valid, so use Type[type] + + commands: cabc.MutableMapping[str, Command] + invoke_without_command: bool + subcommand_metavar: str + chain: bool + _result_callback: t.Callable[..., t.Any] | None + + def __init__( + self, + name: str | None = None, + commands: cabc.MutableMapping[str, Command] + | cabc.Sequence[Command] + | None = None, + invoke_without_command: bool = False, + no_args_is_help: bool | None = None, + subcommand_metavar: str | None = None, + chain: bool = False, + result_callback: t.Callable[..., t.Any] | None = None, + **kwargs: t.Any, + ) -> None: + super().__init__(name, **kwargs) + + if commands is None: + commands = {} + elif isinstance(commands, abc.Sequence): + commands = {c.name: c for c in commands if c.name is not None} + + #: The registered subcommands by their exported names. + self.commands = commands + + if no_args_is_help is None: + no_args_is_help = not invoke_without_command + + self.no_args_is_help = no_args_is_help + self.invoke_without_command = invoke_without_command + + if subcommand_metavar is None: + # When the group can run without a subcommand, the leading command + # token is optional, so wrap it in brackets to reflect that. + if chain: + if invoke_without_command: + subcommand_metavar = "[COMMAND1] [ARGS]... [COMMAND2 [ARGS]...]..." + else: + subcommand_metavar = "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..." + elif invoke_without_command: + subcommand_metavar = "[COMMAND] [ARGS]..." + else: + subcommand_metavar = "COMMAND [ARGS]..." + + self.subcommand_metavar = subcommand_metavar + self.chain = chain + # The result callback that is stored. This can be set or + # overridden with the :func:`result_callback` decorator. + self._result_callback = result_callback + + if self.chain: + for param in self.params: + if isinstance(param, Argument) and not param.required: + raise RuntimeError( + "A group in chain mode cannot have optional arguments." + ) + + def to_info_dict(self, ctx: Context) -> dict[str, t.Any]: + info_dict = super().to_info_dict(ctx) + commands = {} + + for name in self.list_commands(ctx): + command = self.get_command(ctx, name) + + if command is None: + continue + + sub_ctx = ctx._make_sub_context(command) + + with sub_ctx.scope(cleanup=False): + commands[name] = command.to_info_dict(sub_ctx) + + info_dict.update(commands=commands, chain=self.chain) + return info_dict + + def add_command(self, cmd: Command, name: str | None = None) -> None: + """Registers another :class:`Command` with this group. If the name + is not provided, the name of the command is used. + """ + name = name or cmd.name + if name is None: + raise TypeError("Command has no name.") + _check_nested_chain(self, name, cmd, register=True) + self.commands[name] = cmd + + @t.overload + def command(self, __func: t.Callable[..., t.Any]) -> Command: ... + + @t.overload + def command( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], Command]: ... + + def command( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], Command] | Command: + """A shortcut decorator for declaring and attaching a command to + the group. This takes the same arguments as :func:`command` and + immediately registers the created command with this group by + calling :meth:`add_command`. + + To customize the command class used, set the + :attr:`command_class` attribute. + + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. + + .. versionchanged:: 8.0 + Added the :attr:`command_class` attribute. + """ + from .decorators import command + + func: t.Callable[..., t.Any] | None = None + + if args and callable(args[0]): + assert len(args) == 1 and not kwargs, ( + "Use 'command(**kwargs)(callable)' to provide arguments." + ) + (func,) = args + args = () + + if self.command_class and kwargs.get("cls") is None: + kwargs["cls"] = self.command_class + + def decorator(f: t.Callable[..., t.Any]) -> Command: + cmd: Command = command(*args, **kwargs)(f) + self.add_command(cmd) + return cmd + + if func is not None: + return decorator(func) + + return decorator + + @t.overload + def group(self, __func: t.Callable[..., t.Any]) -> Group: ... + + @t.overload + def group( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], Group]: ... + + def group( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], Group] | Group: + """A shortcut decorator for declaring and attaching a group to + the group. This takes the same arguments as :func:`group` and + immediately registers the created group with this group by + calling :meth:`add_command`. + + To customize the group class used, set the :attr:`group_class` + attribute. + + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. + + .. versionchanged:: 8.0 + Added the :attr:`group_class` attribute. + """ + from .decorators import group + + func: t.Callable[..., t.Any] | None = None + + if args and callable(args[0]): + assert len(args) == 1 and not kwargs, ( + "Use 'group(**kwargs)(callable)' to provide arguments." + ) + (func,) = args + args = () + + if self.group_class is not None and kwargs.get("cls") is None: + if self.group_class is type: + kwargs["cls"] = type(self) + else: + kwargs["cls"] = self.group_class + + def decorator(f: t.Callable[..., t.Any]) -> Group: + cmd: Group = group(*args, **kwargs)(f) + self.add_command(cmd) + return cmd + + if func is not None: + return decorator(func) + + return decorator + + def result_callback(self, replace: bool = False) -> t.Callable[[F], F]: + """Adds a result callback to the command. By default if a + result callback is already registered this will chain them but + this can be disabled with the `replace` parameter. The result + callback is invoked with the return value of the subcommand + (or the list of return values from all subcommands if chaining + is enabled) as well as the parameters as they would be passed + to the main callback. + + Example:: + + @click.group() + @click.option('-i', '--input', default=23) + def cli(input): + return 42 + + @cli.result_callback() + def process_result(result, input): + return result + input + + :param replace: if set to `True` an already existing result + callback will be removed. + + .. versionchanged:: 8.0 + Renamed from ``resultcallback``. + + .. versionadded:: 3.0 + """ + + def decorator(f: F) -> F: + old_callback = self._result_callback + + if old_callback is None or replace: + self._result_callback = f + return f + + def function(value: t.Any, /, *args: t.Any, **kwargs: t.Any) -> t.Any: + inner = old_callback(value, *args, **kwargs) + return f(inner, *args, **kwargs) + + self._result_callback = rv = update_wrapper(t.cast(F, function), f) + return rv # type: ignore[return-value] + + return decorator + + def get_command(self, ctx: Context, cmd_name: str) -> Command | None: + """Given a context and a command name, this returns a :class:`Command` + object if it exists or returns ``None``. + """ + return self.commands.get(cmd_name) + + def list_commands(self, ctx: Context) -> list[str]: + """Returns a list of subcommand names in the order they should appear.""" + return sorted(self.commands) + + def collect_usage_pieces(self, ctx: Context) -> list[str]: + rv = super().collect_usage_pieces(ctx) + rv.append(self.subcommand_metavar) + return rv + + def format_options(self, ctx: Context, formatter: HelpFormatter) -> None: + super().format_options(ctx, formatter) + self.format_commands(ctx, formatter) + + def format_commands(self, ctx: Context, formatter: HelpFormatter) -> None: + """Extra format methods for multi methods that adds all the commands + after the options. + """ + commands = [] + for subcommand in self.list_commands(ctx): + cmd = self.get_command(ctx, subcommand) + # What is this, the tool lied about a command. Ignore it + if cmd is None: + continue + if cmd.hidden: + continue + + commands.append((subcommand, cmd)) + + # allow for 3 times the default spacing + if len(commands): + limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands) + + rows = [] + for subcommand, cmd in commands: + help = cmd.get_short_help_str(limit) + rows.append((subcommand, help)) + + if rows: + with formatter.section(_("Commands")): + formatter.write_dl(rows) + + def parse_args(self, ctx: Context, args: list[str]) -> list[str]: + if not args and self.no_args_is_help and not ctx.resilient_parsing: + raise NoArgsIsHelpError(ctx) + + rest = super().parse_args(ctx, args) + + if self.chain: + ctx._protected_args = rest + ctx.args = [] + elif rest: + ctx._protected_args, ctx.args = rest[:1], rest[1:] + + return ctx.args + + def invoke(self, ctx: Context) -> t.Any: + def _process_result(value: t.Any) -> t.Any: + if self._result_callback is not None: + value = ctx.invoke(self._result_callback, value, **ctx.params) + return value + + if not ctx._protected_args: + if self.invoke_without_command: + # No subcommand was invoked, so the result callback is + # invoked with the group return value for regular + # groups, or an empty list for chained groups. + with ctx: + rv = super().invoke(ctx) + return _process_result([] if self.chain else rv) + ctx.fail(_("Missing command.")) + + # Fetch args back out + args = [*ctx._protected_args, *ctx.args] + ctx.args = [] + ctx._protected_args = [] + + # If we're not in chain mode, we only allow the invocation of a + # single command but we also inform the current context about the + # name of the command to invoke. + if not self.chain: + # Make sure the context is entered so we do not clean up + # resources until the result processor has worked. + with ctx: + cmd_name, cmd, args = self.resolve_command(ctx, args) + assert cmd is not None + ctx.invoked_subcommand = cmd_name + super().invoke(ctx) + sub_ctx = cmd.make_context(cmd_name, args, parent=ctx) + with sub_ctx: + return _process_result(sub_ctx.command.invoke(sub_ctx)) + + # In chain mode we create the contexts step by step, but after the + # base command has been invoked. Because at that point we do not + # know the subcommands yet, the invoked subcommand attribute is + # set to ``*`` to inform the command that subcommands are executed + # but nothing else. + with ctx: + ctx.invoked_subcommand = "*" if args else None + super().invoke(ctx) + + # Otherwise we make every single context and invoke them in a + # chain. In that case the return value to the result processor + # is the list of all invoked subcommand's results. + contexts = [] + while args: + cmd_name, cmd, args = self.resolve_command(ctx, args) + assert cmd is not None + sub_ctx = cmd.make_context( + cmd_name, + args, + parent=ctx, + allow_extra_args=True, + allow_interspersed_args=False, + ) + contexts.append(sub_ctx) + args, sub_ctx.args = sub_ctx.args, [] + + rv = [] + for sub_ctx in contexts: + with sub_ctx: + rv.append(sub_ctx.command.invoke(sub_ctx)) + return _process_result(rv) + + def resolve_command( + self, ctx: Context, args: list[str] + ) -> tuple[str | None, Command | None, list[str]]: + cmd_name = make_str(args[0]) + + # Get the command + cmd = self.get_command(ctx, cmd_name) + + # If we can't find the command but there is a normalization + # function available, we try with that one. + if cmd is None and ctx.token_normalize_func is not None: + cmd_name = ctx.token_normalize_func(cmd_name) + cmd = self.get_command(ctx, cmd_name) + + # If we don't find the command we want to show an error message + # to the user that it was not provided. However, there is + # something else we should do: if the first argument looks like + # an option we want to kick off parsing again for arguments to + # resolve things like --help which now should go to the main + # place. + if cmd is None and not ctx.resilient_parsing: + if _split_opt(cmd_name)[0]: + self.parse_args(ctx, args) + raise NoSuchCommand(cmd_name, possibilities=self.commands, ctx=ctx) + return cmd_name if cmd else None, cmd, args[1:] + + def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]: + """Return a list of completions for the incomplete value. Looks + at the names of options, subcommands, and chained + multi-commands. + + :param ctx: Invocation context for this command. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + results = [ + CompletionItem(name, help=command.get_short_help_str()) + for name, command in _complete_visible_commands(ctx, incomplete) + ] + results.extend(super().shell_complete(ctx, incomplete)) + return results + + +class _MultiCommand(Group, metaclass=_FakeSubclassCheck): + """ + .. deprecated:: 8.2 + Will be removed in Click 9.0. Use ``Group`` instead. + """ + + +class CommandCollection(Group): + """A :class:`Group` that looks up subcommands on other groups. If a command + is not found on this group, each registered source is checked in order. + Parameters on a source are not added to this group, and a source's callback + is not invoked when invoking its commands. In other words, this "flattens" + commands in many groups into this one group. + + :param name: The name of the group command. + :param sources: A list of :class:`Group` objects to look up commands from. + :param kwargs: Other arguments passed to :class:`Group`. + + .. versionchanged:: 8.2 + This is a subclass of ``Group``. Commands are looked up first on this + group, then each of its sources. + """ + + sources: list[Group] + + def __init__( + self, + name: str | None = None, + sources: list[Group] | None = None, + **kwargs: t.Any, + ) -> None: + super().__init__(name, **kwargs) + #: The list of registered groups. + self.sources = sources or [] + + def add_source(self, group: Group) -> None: + """Add a group as a source of commands.""" + self.sources.append(group) + + def get_command(self, ctx: Context, cmd_name: str) -> Command | None: + rv = super().get_command(ctx, cmd_name) + + if rv is not None: + return rv + + for source in self.sources: + rv = source.get_command(ctx, cmd_name) + + if rv is not None: + if self.chain: + _check_nested_chain(self, cmd_name, rv) + + return rv + + return None + + def list_commands(self, ctx: Context) -> list[str]: + rv: set[str] = set(super().list_commands(ctx)) + + for source in self.sources: + rv.update(source.list_commands(ctx)) + + return sorted(rv) + + +def _check_iter(value: cabc.Iterable[V]) -> cabc.Iterator[V]: + """Check if the value is iterable but not a string. Raises a type + error, or return an iterator over the value. + """ + if isinstance(value, str): + raise TypeError + + return iter(value) + + +class Parameter(ABC): + r"""A parameter to a command comes in two versions: they are either + :class:`Option`\s or :class:`Argument`\s. Other subclasses are currently + not supported by design as some of the internals for parsing are + intentionally not finalized. + + Some settings are supported by both options and arguments. + + :param param_decls: the parameter declarations for this option or + argument. This is a list of flags or argument + names. + :param type: the type that should be used. Either a :class:`ParamType` + or a Python type. The latter is converted into the former + automatically if supported. + :param required: controls if this is optional or not. + :param default: the default value if omitted. This can also be a callable, + in which case it's invoked when the default is needed + without any arguments. + :param callback: A function to further process or validate the value + after type conversion. It is called as ``f(ctx, param, value)`` + and must return the value. It is called for all sources, + including prompts. + :param nargs: the number of arguments to match. If not ``1`` the return + value is a tuple instead of single value. The default for + nargs is ``1`` (except if the type is a tuple, then it's + the arity of the tuple). If ``nargs=-1``, all remaining + parameters are collected. + :param metavar: how the value is represented in the help page. + :param expose_value: if this is `True` then the value is passed onwards + to the command callback and stored on the context, + otherwise it's skipped. + :param is_eager: eager values are processed before non eager ones. This + should not be set for arguments or it will inverse the + order of processing. + :param envvar: environment variable(s) that are used to provide a default value for + this parameter. This can be a string or a sequence of strings. If a sequence is + given, only the first non-empty environment variable is used for the parameter. + :param shell_complete: A function that returns custom shell + completions. Used instead of the param's type completion if + given. Takes ``ctx, param, incomplete`` and must return a list + of :class:`~click.shell_completion.CompletionItem` or a list of + strings. + :param deprecated: If ``True`` or non-empty string, issues a message + indicating that the argument is deprecated and highlights + its deprecation in --help. The message can be customized + by using a string as the value. A deprecated parameter + cannot be required, a ValueError will be raised otherwise. + + .. versionchanged:: 8.2.0 + Introduction of ``deprecated``. + + .. versionchanged:: 8.2 + Adding duplicate parameter names to a :class:`~click.core.Command` will + result in a ``UserWarning`` being shown. + + .. versionchanged:: 8.2 + Adding duplicate parameter names to a :class:`~click.core.Command` will + result in a ``UserWarning`` being shown. + + .. versionchanged:: 8.0 + ``process_value`` validates required parameters and bounded + ``nargs``, and invokes the parameter callback before returning + the value. This allows the callback to validate prompts. + ``full_process_value`` is removed. + + .. versionchanged:: 8.0 + ``autocompletion`` is renamed to ``shell_complete`` and has new + semantics described above. The old name is deprecated and will + be removed in 8.1, until then it will be wrapped to match the + new requirements. + + .. versionchanged:: 8.0 + For ``multiple=True, nargs>1``, the default must be a list of + tuples. + + .. versionchanged:: 8.0 + Setting a default is no longer required for ``nargs>1``, it will + default to ``None``. ``multiple=True`` or ``nargs=-1`` will + default to ``()``. + + .. versionchanged:: 7.1 + Empty environment variables are ignored rather than taking the + empty string value. This makes it possible for scripts to clear + variables if they can't unset them. + + .. versionchanged:: 2.0 + Changed signature for parameter callback to also be passed the + parameter. The old callback format will still work, but it will + raise a warning to give you a chance to migrate the code easier. + """ + + param_type_name = "parameter" + + name: str + opts: list[str] + secondary_opts: list[str] + # `Parameter.type` is annotated in `__init__` to avoid confusing mypy + required: bool + callback: t.Callable[[Context, Parameter, t.Any], t.Any] | None + nargs: int + multiple: bool + expose_value: bool + default: t.Any | t.Callable[[], t.Any] | None + _default_explicit: bool + is_eager: bool + metavar: str | None + envvar: str | cabc.Sequence[str] | None + _custom_shell_complete: ( + t.Callable[[Context, Parameter, str], list[CompletionItem] | list[str]] | None + ) + deprecated: bool | str + + def __init__( + self, + param_decls: cabc.Sequence[str] | None = None, + type: types.ParamType[t.Any] | t.Any | None = None, + required: bool = False, + # XXX The default historically embed two concepts: + # - the declaration of a Parameter object carrying the default (handy to + # arbitrage the default value of coupled Parameters sharing the same + # self.name, like flag options), + # - and the actual value of the default. + # It is confusing and is the source of many issues discussed in: + # https://github.com/pallets/click/pull/3030 + # In the future, we might think of splitting it in two, not unlike + # Option.is_flag and Option.flag_value: we could have something like + # Parameter.is_default and Parameter.default_value. + default: t.Any | t.Callable[[], t.Any] | None = UNSET, + callback: t.Callable[[Context, Parameter, t.Any], t.Any] | None = None, + nargs: int | None = None, + multiple: bool = False, + metavar: str | None = None, + expose_value: bool = True, + is_eager: bool = False, + envvar: str | cabc.Sequence[str] | None = None, + shell_complete: t.Callable[ + [Context, Parameter, str], list[CompletionItem] | list[str] + ] + | None = None, + deprecated: bool | str = False, + ) -> None: + self.name, self.opts, self.secondary_opts = self._parse_decls( + param_decls or (), expose_value + ) + self.type: types.ParamType[t.Any] = types.convert_type(type, default) + + # Default nargs to what the type tells us if we have that + # information available. + if nargs is None: + if self.type.is_composite: + nargs = self.type.arity + else: + nargs = 1 + + self.required = required + self.callback = callback + self.nargs = nargs + self.multiple = multiple + self.expose_value = expose_value + self.default = default + # Whether the user passed ``default`` explicitly to the constructor. + # Captured before any auto-derived default (like ``False`` for boolean + # flags in :class:`Option`) replaces the :data:`UNSET` sentinel, so it + # remains ``False`` when the default was inferred rather than chosen. + # Refs: https://github.com/pallets/click/issues/3403 + self._default_explicit = default is not UNSET + self.is_eager = is_eager + self.metavar = metavar + self.envvar = envvar + self._custom_shell_complete = shell_complete + self.deprecated = deprecated + + if __debug__: + if self.type.is_composite and nargs != self.type.arity: + raise ValueError( + f"'nargs' must be {self.type.arity} (or None) for" + f" type {self.type!r}, but it was {nargs}." + ) + + if required and deprecated: + raise ValueError( + f"The {self.param_type_name} '{self.human_readable_name}' " + "is deprecated and still required. A deprecated " + f"{self.param_type_name} cannot be required." + ) + + @staticmethod + def _hide_unset(value: t.Any) -> t.Any: + """Present the internal :data:`UNSET` sentinel as ``None`` at a boundary that + exposes a parameter's value (introspection, prompts), keeping the sentinel an + implementation detail. + """ + return None if value is UNSET else value + + def to_info_dict(self) -> dict[str, t.Any]: + """Gather information that could be useful for a tool generating + user-facing documentation. + + Use :meth:`click.Context.to_info_dict` to traverse the entire + CLI structure. + + .. versionchanged:: 8.3.0 + Returns ``None`` for the :attr:`default` if it was not set. + + .. versionadded:: 8.0 + """ + return { + "name": self.name, + "param_type_name": self.param_type_name, + "opts": self.opts, + "secondary_opts": self.secondary_opts, + "type": self.type.to_info_dict(), + "required": self.required, + "nargs": self.nargs, + "multiple": self.multiple, + "default": self._hide_unset(self.default), + "envvar": self.envvar, + } + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self.name}>" + + @abstractmethod + def _parse_decls( + self, decls: cabc.Sequence[str], expose_value: bool + ) -> tuple[str, list[str], list[str]]: ... + + @property + def human_readable_name(self) -> str: + """Returns the human readable name of this parameter. This is the + same as the name for options, but the metavar for arguments. + """ + return self.name + + def make_metavar(self, ctx: Context) -> str: + if self.metavar is not None: + return self.metavar + + metavar = self.type.get_metavar(param=self, ctx=ctx) + + if metavar is None: + metavar = self.type.name.upper() + + if self.nargs != 1: + metavar += "..." + + return metavar + + @t.overload + def get_default( + self, ctx: Context, call: t.Literal[True] = True + ) -> t.Any | None: ... + + @t.overload + def get_default( + self, ctx: Context, call: bool = ... + ) -> t.Any | t.Callable[[], t.Any] | None: ... + + def get_default( + self, ctx: Context, call: bool = True + ) -> t.Any | t.Callable[[], t.Any] | None: + """Get the default for the parameter. Tries + :meth:`Context.lookup_default` first, then the local default. + + :param ctx: Current context. + :param call: If the default is a callable, call it. Disable to + return the callable instead. + + .. versionchanged:: 8.0.2 + Type casting is no longer performed when getting a default. + + .. versionchanged:: 8.0.1 + Type casting can fail in resilient parsing mode. Invalid + defaults will not prevent showing help text. + + .. versionchanged:: 8.0 + Looks at ``ctx.default_map`` first. + + .. versionchanged:: 8.0 + Added the ``call`` parameter. + """ + value = ctx.lookup_default(self.name, call=False) + + if value is None and not ctx._default_map_has(self.name): + value = self.default + + if call and callable(value): + value = value() + + return value + + @abstractmethod + def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None: ... + + def consume_value( + self, ctx: Context, opts: cabc.Mapping[str, t.Any] + ) -> tuple[t.Any, ParameterSource]: + """Returns the parameter value produced by the parser. + + If the parser did not produce a value from user input, the value is either + sourced from the environment variable, the default map, or the parameter's + default value. In that order of precedence. + + If no value is found, an internal sentinel value is returned. + + :meta private: + """ + # Collect from the parse the value passed by the user to the CLI. + value = opts.get(self.name, UNSET) + # If the value is set, it means it was sourced from the command line by the + # parser, otherwise it left unset by default. + source = ( + ParameterSource.COMMANDLINE + if value is not UNSET + else ParameterSource.DEFAULT + ) + + if value is UNSET: + envvar_value = self.value_from_envvar(ctx) + if envvar_value is not None: + value = envvar_value + source = ParameterSource.ENVIRONMENT + + if value is UNSET: + default_map_value = ctx.lookup_default(self.name) + if default_map_value is not None or ctx._default_map_has(self.name): + value = default_map_value + source = ParameterSource.DEFAULT_MAP + + # A string from default_map must be split for multi-value + # parameters, matching value_from_envvar behavior. + if isinstance(value, str) and self.nargs != 1: + value = self.type.split_envvar_value(value) + + if value is UNSET: + default_value = self.get_default(ctx) + if default_value is not UNSET: + value = default_value + source = ParameterSource.DEFAULT + + return value, source + + def type_cast_value(self, ctx: Context, value: t.Any) -> t.Any: + """Convert and validate a value against the parameter's + :attr:`type`, :attr:`multiple`, and :attr:`nargs`. + """ + if value is None: + if self.multiple or self.nargs == -1: + return () + else: + return value + + def check_iter(value: t.Any) -> cabc.Iterator[t.Any]: + try: + return _check_iter(value) + except TypeError: + # This should only happen when passing in args manually, + # the parser should construct an iterable when parsing + # the command line. + raise BadParameter( + _("Value must be an iterable."), ctx=ctx, param=self + ) from None + + # Define the conversion function based on nargs and type. + + if self.nargs == 1 or self.type.is_composite: + + def convert(value: t.Any) -> t.Any: + return self.type(value, param=self, ctx=ctx) + + elif self.nargs == -1: + + def convert(value: t.Any) -> t.Any: # tuple[t.Any, ...] + return tuple(self.type(x, self, ctx) for x in check_iter(value)) + + else: # nargs > 1 + + def convert(value: t.Any) -> t.Any: # tuple[t.Any, ...] + value = tuple(check_iter(value)) + + if len(value) != self.nargs: + raise BadParameter( + ngettext( + "Takes {nargs} values but 1 was given.", + "Takes {nargs} values but {len} were given.", + len(value), + ).format(nargs=self.nargs, len=len(value)), + ctx=ctx, + param=self, + ) + + return tuple(self.type(x, self, ctx) for x in value) + + if self.multiple: + return tuple(convert(x) for x in check_iter(value)) + + return convert(value) + + def value_is_missing(self, value: t.Any) -> bool: + """A value is considered missing if: + + - it is :attr:`UNSET`, + - or if it is an empty sequence while the parameter is suppose to have + non-single value (i.e. :attr:`nargs` is not ``1`` or :attr:`multiple` is + set). + + :meta private: + """ + if value is UNSET: + return True + + if (self.nargs != 1 or self.multiple) and value == (): + return True + + return False + + def process_value(self, ctx: Context, value: t.Any) -> t.Any: + """Process the value of this parameter: + + 1. Type cast the value using :meth:`type_cast_value`. + 2. Check if the value is missing (see: :meth:`value_is_missing`), and raise + :exc:`MissingParameter` if it is required. + 3. If a :attr:`callback` is set, call it to have the value replaced by the + result of the callback. If the value was not set, the callback receive + ``None``. This keep the legacy behavior as it was before the introduction of + the :attr:`UNSET` sentinel. + + :meta private: + """ + # shelter `type_cast_value` from ever seeing an `UNSET` value by handling the + # cases in which `UNSET` gets special treatment explicitly at this layer + # + # Refs: + # https://github.com/pallets/click/issues/3069 + if value is UNSET: + if self.multiple or self.nargs == -1: + value = () + else: + value = self.type_cast_value(ctx, value) + + if self.required and self.value_is_missing(value): + raise MissingParameter(ctx=ctx, param=self) + + if self.callback is not None: + # Legacy case: UNSET is not exposed directly to the callback, but converted + # to None. + if value is UNSET: + value = None + + # Search for parameters with UNSET values in the context. + unset_keys = {k: None for k, v in ctx.params.items() if v is UNSET} + # No UNSET values, call the callback as usual. + if not unset_keys: + value = self.callback(ctx, self, value) + + # Legacy case: provide a temporarily manipulated context to the callback + # to hide UNSET values as None. + # + # Refs: + # https://github.com/pallets/click/issues/3136 + # https://github.com/pallets/click/pull/3137 + else: + # Add another layer to the context stack to clearly hint that the + # context is temporarily modified. + with ctx: + # Update the context parameters to replace UNSET with None. + ctx.params.update(unset_keys) + # Feed these fake context parameters to the callback. + value = self.callback(ctx, self, value) + # Restore the UNSET values in the context parameters. + ctx.params.update( + { + k: UNSET + for k in unset_keys + # Only restore keys that are present and still None, in case + # the callback modified other parameters. + if k in ctx.params and ctx.params[k] is None + } + ) + + return value + + def resolve_envvar_value(self, ctx: Context) -> str | None: + """Returns the value found in the environment variable(s) attached to this + parameter. + + Environment variables values are `always returned as strings + `_. + + This method returns ``None`` if: + + - the :attr:`envvar` property is not set on the :class:`Parameter`, + - the environment variable is not found in the environment, + - the variable is found in the environment but its value is empty (i.e. the + environment variable is present but has an empty string). + + If :attr:`envvar` is setup with multiple environment variables, + then only the first non-empty value is returned. + + .. caution:: + + The raw value extracted from the environment is not normalized and is + returned as-is. Any normalization or reconciliation is performed later by + the :class:`Parameter`'s :attr:`type`. + + :meta private: + """ + if not self.envvar: + return None + + if isinstance(self.envvar, str): + rv = os.environ.get(self.envvar) + + if rv: + return rv + else: + for envvar in self.envvar: + rv = os.environ.get(envvar) + + # Return the first non-empty value of the list of environment variables. + if rv: + return rv + # Else, absence of value is interpreted as an environment variable that + # is not set, so proceed to the next one. + + return None + + def value_from_envvar(self, ctx: Context) -> str | cabc.Sequence[str] | None: + """Process the raw environment variable string for this parameter. + + Returns the string as-is or splits it into a sequence of strings if the + parameter is expecting multiple values (i.e. its :attr:`nargs` property is set + to a value other than ``1``). + + :meta private: + """ + rv = self.resolve_envvar_value(ctx) + + if rv is not None and self.nargs != 1: + return self.type.split_envvar_value(rv) + + return rv + + def handle_parse_result( + self, ctx: Context, opts: cabc.Mapping[str, t.Any], args: list[str] + ) -> tuple[t.Any, list[str]]: + """Process the value produced by the parser from user input. + + Always process the value through the Parameter's :attr:`type`, wherever it + comes from. + + If the parameter is deprecated, this method warn the user about it. But only if + the value has been explicitly set by the user (and as such, is not coming from + a default). + + :meta private: + """ + # Capture the slot's existing state before we mutate + # ``_parameter_source`` so the write decision below can compare our + # incoming source against the source of the option that already wrote + # the slot (if any). + existing_value = ctx.params.get(self.name, UNSET) + existing_source = ctx.get_parameter_source(self.name) + existing_default_explicit = ctx._param_default_explicit.get(self.name, False) + + with augment_usage_errors(ctx, param=self): + value, source = self.consume_value(ctx, opts) + + # Record the source before processing so eager callbacks and type + # conversion can inspect it. Restored after arbitration if this + # option loses a feature-switch group. + ctx.set_parameter_source(self.name, source) + + # Display a deprecation warning if necessary. + if ( + self.deprecated + and value is not UNSET + and source < ParameterSource.DEFAULT_MAP + ): + message = _( + "DeprecationWarning: The {param_type} {name!r} is deprecated." + "{extra_message}" + ).format( + param_type=self.param_type_name, + name=self.human_readable_name, + extra_message=_format_deprecated_suffix(self.deprecated), + ) + echo(style(message, fg="red"), err=True) + + # Process the value through the parameter's type. + try: + value = self.process_value(ctx, value) + except Exception: + if not ctx.resilient_parsing: + raise + # In resilient parsing mode, we do not want to fail the command if the + # value is incompatible with the parameter type, so we reset the value + # to UNSET, which will be interpreted as a missing value. + value = UNSET + + # Arbitrate the slot when several parameters target the same variable + # name (feature-switch groups). See: https://github.com/pallets/click/issues/3403 + slot_empty = existing_value is UNSET + more_explicit = existing_source is not None and source < existing_source + same_source = existing_source is not None and source == existing_source + auto_would_downgrade_explicit = ( + same_source + and source == ParameterSource.DEFAULT + and existing_default_explicit + and not self._default_explicit + ) + is_winner = ( + slot_empty + or more_explicit + or (same_source and not auto_would_downgrade_explicit) + ) + + if is_winner: + if self.expose_value: + ctx.params[self.name] = value + ctx._param_default_explicit[self.name] = self._default_explicit + elif existing_source is not None: + # Lost arbitration; restore the winning option's source. + ctx.set_parameter_source(self.name, existing_source) + # else: ctx.params[self.name] was populated by code that bypassed + # handle_parse_result (from another option's callback for example). Keep + # the provisional source recorded before process_value so downstream + # lookups don't return ``None``. + + return value, args + + def get_help_record(self, ctx: Context) -> tuple[str, str] | None: + return None + + def get_usage_pieces(self, ctx: Context) -> list[str]: + return [] + + def get_error_hint(self, ctx: Context | None) -> str: + """Get a stringified version of the param for use in error messages to + indicate which param caused the error. + + .. versionchanged:: 8.4.0 + ``ctx`` can be ``None``. + """ + hint_list = self.opts or [self.human_readable_name] + return " / ".join(f"'{x}'" for x in hint_list) + + def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]: + """Return a list of completions for the incomplete value. If a + ``shell_complete`` function was given during init, it is used. + Otherwise, the :attr:`type` + :meth:`~click.types.ParamType[t.Any].shell_complete` function is used. + + :param ctx: Invocation context for this command. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + if self._custom_shell_complete is not None: + results = self._custom_shell_complete(ctx, self, incomplete) + + if results and isinstance(results[0], str): + from click.shell_completion import CompletionItem + + results = [CompletionItem(c) for c in results] + + return t.cast("list[CompletionItem]", results) + + return self.type.shell_complete(ctx, self, incomplete) + + +class Option(Parameter): + """Options are usually optional values on the command line and + have some extra features that arguments don't have. + + All other parameters are passed onwards to the parameter constructor. + + :param show_default: Show the default value for this option in its + help text. Values are not shown by default, unless + :attr:`Context.show_default` is ``True``. If this value is a + string, it shows that string in parentheses instead of the + actual value. This is particularly useful for dynamic options. + For single option boolean flags, the default remains hidden if + its value is ``False``. + :param show_envvar: Controls if an environment variable should be + shown on the help page and error messages. + Normally, environment variables are not shown. + :param prompt: If set to ``True`` or a non empty string then the + user will be prompted for input. If set to ``True`` the prompt + will be the option name capitalized. A deprecated option cannot be + prompted. + :param confirmation_prompt: Prompt a second time to confirm the + value if it was prompted for. Can be set to a string instead of + ``True`` to customize the message. + :param prompt_required: If set to ``False``, the user will be + prompted for input only when the option was specified as a flag + without a value. + :param hide_input: If this is ``True`` then the input on the prompt + will be hidden from the user. This is useful for password input. + :param is_flag: forces this option to act as a flag. The default is + auto detection. + :param flag_value: which value should be used for this flag if it's + enabled. This is set to a boolean automatically if + the option string contains a slash to mark two options. + :param multiple: if this is set to `True` then the argument is accepted + multiple times and recorded. This is similar to ``nargs`` + in how it works but supports arbitrary number of + arguments. + :param count: this flag makes an option increment an integer. + :param allow_from_autoenv: if this is enabled then the value of this + parameter will be pulled from an environment + variable in case a prefix is defined on the + context. + :param help: the help string. + :param hidden: hide this option from help outputs. + :param attrs: Other command arguments described in :class:`Parameter`. + + .. versionchanged:: 8.4.0 + Non-basic ``flag_value`` types (not ``str``, ``int``, ``float``, or + ``bool``) are passed through unchanged instead of being stringified. + Previously, ``type=click.UNPROCESSED`` was required to preserve them. + + .. versionchanged:: 8.2 + ``envvar`` used with ``flag_value`` will always use the ``flag_value``, + previously it would use the value of the environment variable. + + .. versionchanged:: 8.1 + Help text indentation is cleaned here instead of only in the + ``@option`` decorator. + + .. versionchanged:: 8.1 + The ``show_default`` parameter overrides + ``Context.show_default``. + + .. versionchanged:: 8.1 + The default of a single option boolean flag is not shown if the + default value is ``False``. + + .. versionchanged:: 8.0.1 + ``type`` is detected from ``flag_value`` if given, for basic Python + types (``str``, ``int``, ``float``, ``bool``). + """ + + param_type_name = "option" + + prompt: str | None + confirmation_prompt: bool | str + prompt_required: bool + hide_input: bool + hidden: bool + + _flag_needs_value: bool + is_flag: bool + flag_value: t.Any + type: types.ParamType[t.Any] + default: t.Any | t.Callable[[], t.Any] | None + + count: bool + allow_from_autoenv: bool + help: str | None + show_default: bool | str | None + show_choices: bool + show_envvar: bool + + def __init__( + self, + param_decls: cabc.Sequence[str] | None = None, + show_default: bool | str | None = None, + prompt: bool | str = False, + confirmation_prompt: bool | str = False, + prompt_required: bool = True, + hide_input: bool = False, + is_flag: bool | None = None, + flag_value: t.Any = UNSET, + multiple: bool = False, + count: bool = False, + allow_from_autoenv: bool = True, + type: types.ParamType[t.Any] | t.Any | None = None, + help: str | None = None, + hidden: bool = False, + show_choices: bool = True, + show_envvar: bool = False, + deprecated: bool | str = False, + **attrs: t.Any, + ) -> None: + if help: + help = inspect.cleandoc(help) + + super().__init__( + param_decls, type=type, multiple=multiple, deprecated=deprecated, **attrs + ) + + # Phase 1: prompt-related attributes. ``_infer_flag_kind`` reads ``self.prompt`` + # and ``self.prompt_required`` so this must run first. + if prompt is True: + if not self.name: + raise TypeError("'name' is required with 'prompt=True'.") + + prompt_text = self.name.replace("_", " ").capitalize() + elif prompt is False: + prompt_text = None + else: + prompt_text = prompt + + if deprecated: + label = _format_deprecated_label(deprecated) + help = f"{help} {label}" if help else label + + self.prompt = prompt_text + self.confirmation_prompt = confirmation_prompt + self.prompt_required = prompt_required + self.hide_input = hide_input + self.hidden = hidden + + # Phase 2: flag-kind inference. + self.is_flag, self._flag_needs_value = self._infer_flag_kind( + is_flag, flag_value + ) + + # Phase 3: type inference. Override the type set by :meth:`Parameter.__init__` + # when this option is a flag or a count. + self.type = self._pick_type(type, flag_value, count, self.is_flag) + + # Phase 4: store the raw ``flag_value`` and ``count`` settings. + # ``self.flag_value`` and ``self.default`` deliberately keep the :data:`UNSET` + # sentinel when the user didn't pass them. Auto-derived values are resolved + # lazily in :meth:`_resolve_lazy_default` and :attr:`flag_activation_value`. + # Keeping the raw values means ``is UNSET`` reliably answers "did the user pass + # this?": #3403 needs it for arbitration, and any future feature needing the + # same distinction can reuse it without reintroducing parallel "was it + # explicit?" tracking. + self.flag_value = flag_value + self.count = count + if count and self.default is UNSET: + self.default = 0 + + self.allow_from_autoenv = allow_from_autoenv + self.help = help + self.show_default = show_default + self.show_choices = show_choices + self.show_envvar = show_envvar + + # Phase 5: validate. Raises on illegal kwarg combinations. + self._validate(prompt, deprecated) + + @property + def is_bool_flag(self) -> bool: + """``True`` when this option is a flag with a boolean type. + + Derived from :attr:`is_flag` and :attr:`type`; computed on access so it cannot + drift if a subclass replaces :attr:`type` after construction. + """ + return self.is_flag and isinstance(self.type, types.BoolParamType) + + @property + def flag_activation_value(self) -> t.Any: + """Value the function receives when this flag is activated on the command line. + + Resolves a missing :attr:`flag_value` to ``True`` for actual flag options and + ``None`` otherwise. Used by the parser bridge (:meth:`add_to_parser`) and the + runtime (:meth:`consume_value`) so :attr:`flag_value` can keep the :data:`UNSET` + sentinel for "did the user pass one?" introspection. + """ + if self.flag_value is UNSET: + return True if self.is_flag else None + return self.flag_value + + def _infer_flag_kind( + self, is_flag: bool | None, flag_value: t.Any + ) -> tuple[bool, bool]: + """Resolve ``is_flag`` and the parser hint ``_flag_needs_value``. + + Returns ``(is_flag, flag_needs_value)``, where ``_flag_needs_value`` tells the + parser this option is a flag that cannot be used standalone and needs a value. + The parser uses it to decide whether to treat the next CLI token as the flag's + value or as a new option. If ``prompt`` is enabled with + ``prompt_required=False``, it opens the door for an interactive value, hence the + initial condition. Ref: https://github.com/pallets/click/issues/3084 + """ + needs_value = self.prompt is not None and not self.prompt_required + + if is_flag is None: + # Implicitly a flag because flag_value was set. + if flag_value is not UNSET: + return True, needs_value + # Not a flag, but when used as a flag it shows a prompt. + if needs_value: + return False, needs_value + # Implicitly a flag because secondary options names were given. + if self.secondary_opts: + return True, needs_value + return False, needs_value + + if is_flag is False and not needs_value: + # Explicit ``is_flag=False`` with a flag-like value/default still makes the + # option flag-shaped to the parser. + needs_value = flag_value is not UNSET or self.default is UNSET + + return bool(is_flag), needs_value + + def _pick_type( + self, + type_arg: t.Any, + flag_value: t.Any, + count: bool, + is_flag: bool, + ) -> types.ParamType[t.Any]: + """Pick the final :class:`ParamType` for this option. + + :meth:`Parameter.__init__` already stored ``self.type`` from the explicit + ``type`` argument or from ``default``. This method either returns that as-is, or + overrides it for flag and count options (which are inferred from ``flag_value`` + rather than ``default``). + """ + if type_arg is not None: + return self.type + + if count: + return types.IntRange(min=0) + + if not is_flag: + return self.type + + # A flag without a flag_value is a boolean flag. + if flag_value is UNSET or isinstance(flag_value, bool): + return types.BoolParamType() + + guessed: types.ParamType[t.Any] = types.convert_type(None, flag_value) + if ( + isinstance(guessed, types.StringParamType) + and not isinstance(flag_value, str) + and flag_value is not None + ): + # The flag_value type couldn't be auto-detected (not str, int, float, or + # bool). Since flag_value is a programmer-provided Python object, not CLI + # input, pass it through unchanged instead of stringifying it. + return types.UNPROCESSED + return guessed + + def _resolve_lazy_default(self, value: t.Any) -> t.Any: + """Apply lazy auto-derivations to a default-style value. + + Shared between :meth:`get_default` (the runtime path) and :meth:`to_info_dict` + (the introspection path) so the two views cannot drift apart. Callables are + *not* invoked here: that is :meth:`get_default`'s responsibility. Rules: + + * ``UNSET`` resolves to ``False`` for a non-required boolean flag, and to ``()`` + for a non-required, non-prompted multi flag. + * ``True`` resolves to :attr:`flag_value` for a non-boolean flag (the "activate + this flag by default" shorthand). Boolean flags keep ``True`` as a literal. + """ + if value is UNSET and self.is_flag: + if self.multiple and not self.required and not self.prompt: + return () + if self.is_bool_flag and not self.required: + return False + if value is True and self.is_flag and not self.is_bool_flag: + # Use ``flag_activation_value`` so an unset ``flag_value`` resolves to + # ``True`` (the bool-flag activation value) rather than leaking the + # :data:`UNSET` sentinel. + return self.flag_activation_value + return value + + def _validate(self, prompt: bool | str, deprecated: bool | str) -> None: + """Raise :class:`TypeError` / :class:`ValueError` on illegal kwarg combinations. + + Called once, after every other attribute has been assigned, so each check can + read the final state. + """ + if not __debug__: + return + if deprecated and prompt: + raise ValueError("`deprecated` options cannot use `prompt`.") + if self.nargs == -1: + raise TypeError("nargs=-1 is not supported for options.") + if not self.is_bool_flag and self.secondary_opts: + raise TypeError("Secondary flag is not valid for non-boolean flag.") + if self.is_bool_flag and self.hide_input and self.prompt is not None: + raise TypeError("'prompt' with 'hide_input' is not valid for boolean flag.") + if self.count: + if self.multiple: + raise TypeError("'count' is not valid with 'multiple'.") + if self.is_flag: + raise TypeError("'count' is not valid with 'is_flag'.") + + def to_info_dict(self) -> dict[str, t.Any]: + """ + .. versionchanged:: 8.5.0 + ``default`` and ``flag_value`` reflect the auto-derived values (``False`` + for unset boolean-flag defaults, ``True`` for unset boolean-flag activation + values, etc.) when no explicit value was passed, matching what the function + would receive at call time. + + .. versionchanged:: 8.3.0 + Returns ``None`` for the :attr:`flag_value` if it was not set. + """ + info_dict = super().to_info_dict() + info_dict.update( + default=self._hide_unset(self._resolve_lazy_default(self.default)), + help=self.help, + prompt=self.prompt, + is_flag=self.is_flag, + flag_value=self.flag_activation_value, + count=self.count, + hidden=self.hidden, + ) + return info_dict + + def get_default( + self, ctx: Context, call: bool = True + ) -> t.Any | t.Callable[[], t.Any] | None: + """Return the default value for this option. + + Several auto-derived defaults are resolved lazily here rather than eagerly in + :meth:`__init__`. This keeps :attr:`default` raw at construction time, so + ``self.default is UNSET`` reliably answers "did the user pass a default?" for + feature-switch-group arbitration and for anything else that needs to distinguish + "absent" from a chosen value. The resolution rules live in + :meth:`_resolve_lazy_default`. + + .. versionchanged:: 8.5.0 + ``UNSET`` defaults for boolean and multi flags are now resolved here instead + of being coerced in :meth:`__init__`. Reading :attr:`default` directly + returns the user-supplied value (or ``UNSET`` if none was passed) rather + than the auto-derived one. + + .. versionchanged:: 8.3.3 + ``default=True`` is no longer substituted with ``flag_value`` for boolean + flags, fixing negative boolean flags like + ``flag_value=False, default=True``. + """ + raw = super().get_default(ctx, call=False) + value = self._resolve_lazy_default(raw) + # Only invoke the value as a callable when the lazy resolver passed it through + # unchanged. If the resolver substituted ``True`` -> :attr:`flag_value`, the + # result is the programmer-supplied ``flag_value`` (often a class or enum), + # which must NOT be instantiated here. See + # https://github.com/pallets/click/issues/3121. + if value is raw and call and callable(value): + value = value() + return value + + def get_error_hint(self, ctx: Context | None) -> str: + result = super().get_error_hint(ctx) + if self.show_envvar and self.envvar is not None: + result += f" (env var: '{self.envvar}')" + return result + + def _parse_decls( + self, decls: cabc.Sequence[str], expose_value: bool + ) -> tuple[str, list[str], list[str]]: + opts = [] + secondary_opts = [] + name = None + possible_names = [] + + for decl in decls: + if decl.isidentifier(): + if name is not None: + raise TypeError(_("Name '{name}' defined twice").format(name=name)) + name = decl + else: + split_char = ";" if decl[:1] == "/" else "/" + if split_char in decl: + first, second = decl.split(split_char, 1) + first = first.rstrip() + if first: + possible_names.append(_split_opt(first)) + opts.append(first) + second = second.lstrip() + if second: + secondary_opts.append(second.lstrip()) + if first == second: + raise ValueError( + _( + "Boolean option {decl!r} cannot use the" + " same flag for true/false." + ).format(decl=decl) + ) + else: + possible_names.append(_split_opt(decl)) + opts.append(decl) + + if name is None and possible_names: + possible_names.sort(key=lambda x: -len(x[0])) # group long options first + name = possible_names[0][1].replace("-", "_").lower() + if not name.isidentifier(): + name = None + + if name is None: + if not expose_value: + return "", opts, secondary_opts + raise TypeError( + _( + "Could not determine name for option with declarations {decls!r}" + ).format(decls=decls) + ) + + if not opts and not secondary_opts: + raise TypeError( + _( + "No options defined but a name was passed ({name})." + " Did you mean to declare an argument instead? Did" + " you mean to pass '--{name}'?" + ).format(name=name) + ) + + return name, opts, secondary_opts + + def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None: + if self.multiple: + action = "append" + elif self.count: + action = "count" + else: + action = "store" + + if self.is_flag: + action = f"{action}_const" + + if self.is_bool_flag and self.secondary_opts: + parser.add_option( + obj=self, opts=self.opts, dest=self.name, action=action, const=True + ) + parser.add_option( + obj=self, + opts=self.secondary_opts, + dest=self.name, + action=action, + const=False, + ) + else: + parser.add_option( + obj=self, + opts=self.opts, + dest=self.name, + action=action, + # ``flag_activation_value`` resolves UNSET to the right parser-store + # constant (``True`` for bool flags) so the raw :attr:`flag_value` + # can keep the sentinel. + const=self.flag_activation_value, + ) + else: + parser.add_option( + obj=self, + opts=self.opts, + dest=self.name, + action=action, + nargs=self.nargs, + ) + + def get_help_record(self, ctx: Context) -> tuple[str, str] | None: + if self.hidden: + return None + + any_prefix_is_slash = False + + def _write_opts(opts: cabc.Sequence[str]) -> str: + nonlocal any_prefix_is_slash + + rv, any_slashes = join_options(opts) + + if any_slashes: + any_prefix_is_slash = True + + if not self.is_flag and not self.count: + rv += f" {self.make_metavar(ctx=ctx)}" + + return rv + + rv = [_write_opts(self.opts)] + + if self.secondary_opts: + rv.append(_write_opts(self.secondary_opts)) + + help = self.help or "" + + extra = self.get_help_extra(ctx) + extra_items = [] + if "envvars" in extra: + extra_items.append( + _("env var: {var}").format(var=", ".join(extra["envvars"])) + ) + if "default" in extra: + extra_items.append(_("default: {default}").format(default=extra["default"])) + if "range" in extra: + extra_items.append(extra["range"]) + if "required" in extra: + extra_items.append(_(extra["required"])) + + if extra_items: + extra_str = "; ".join(extra_items) + help = f"{help} [{extra_str}]" if help else f"[{extra_str}]" + + return ("; " if any_prefix_is_slash else " / ").join(rv), help + + def get_help_extra(self, ctx: Context) -> types.OptionHelpExtra: + extra: types.OptionHelpExtra = {} + + if self.show_envvar: + envvar = self.envvar + + if envvar is None: + if ( + self.allow_from_autoenv + and ctx.auto_envvar_prefix is not None + and self.name + ): + envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}" + + if envvar is not None: + if isinstance(envvar, str): + extra["envvars"] = (envvar,) + else: + extra["envvars"] = tuple(str(d) for d in envvar) + + # Temporarily enable resilient parsing to avoid type casting + # failing for the default. Might be possible to extend this to + # help formatting in general. + resilient = ctx.resilient_parsing + ctx.resilient_parsing = True + + try: + default_value = self.get_default(ctx, call=False) + finally: + ctx.resilient_parsing = resilient + + show_default = False + show_default_is_str = False + + if self.show_default is not None: + if isinstance(self.show_default, str): + show_default_is_str = show_default = True + else: + show_default = self.show_default + elif ctx.show_default is not None: + show_default = ctx.show_default + + if show_default_is_str or ( + show_default and (default_value not in (None, UNSET)) + ): + if show_default_is_str: + default_string = f"({self.show_default})" + elif isinstance(default_value, (list, tuple)): + default_string = ", ".join(str(d) for d in default_value) + elif isinstance(default_value, enum.Enum): + default_string = default_value.name + elif inspect.isfunction(default_value): + default_string = _("(dynamic)") + elif self.is_bool_flag and self.secondary_opts: + # For boolean flags that have distinct True/False opts, + # use the opt without prefix instead of the value. + default_string = _split_opt( + (self.opts if default_value else self.secondary_opts)[0] + )[1] + elif self.is_bool_flag and not self.secondary_opts and not default_value: + default_string = "" + elif isinstance(default_value, str) and default_value == "": + default_string = '""' + else: + default_string = str(default_value) + + if default_string: + extra["default"] = default_string + + if ( + isinstance(self.type, types._NumberRangeBase) + # skip count with default range type + and not (self.count and self.type.min == 0 and self.type.max is None) + ): + range_str = self.type._describe_range() + + if range_str: + extra["range"] = range_str + + if self.required: + extra["required"] = "required" + + return extra + + def prompt_for_value(self, ctx: Context) -> t.Any: + """This is an alternative flow that can be activated in the full + value processing if a value does not exist. It will prompt the + user until a valid value exists and then returns the processed + value as result. + """ + assert self.prompt is not None + + # Calculate the default before prompting anything to lock in the value before + # attempting any user interaction. + default = self.get_default(ctx) + + # A boolean flag can use a simplified [y/n] confirmation prompt. + if self.is_bool_flag: + # If we have no boolean default, we force the user to explicitly provide + # one. + if default in (UNSET, None): + default = None + # Nothing prevent you to declare an option that is simultaneously: + # 1) auto-detected as a boolean flag, + # 2) allowed to prompt, and + # 3) still declare a non-boolean default. + # This forced casting into a boolean is necessary to align any non-boolean + # default to the prompt, which is going to be a [y/n]-style confirmation + # because the option is still a boolean flag. That way, instead of [y/n], + # we get [Y/n] or [y/N] depending on the truthy value of the default. + # Refs: https://github.com/pallets/click/pull/3030#discussion_r2289180249 + else: + default = bool(default) + return confirm(self.prompt, default) + + # If show_default is given, provide this to `prompt` as well, + # otherwise we use `prompt`'s default behavior + prompt_kwargs: t.Any = {} + if self.show_default is not None: + prompt_kwargs["show_default"] = self.show_default + + return prompt( + self.prompt, + # Use ``None`` to inform the prompt() function to reiterate until a valid + # value is provided by the user if we have no default. + default=self._hide_unset(default), + type=self.type, + hide_input=self.hide_input, + show_choices=self.show_choices, + confirmation_prompt=self.confirmation_prompt, + value_proc=lambda x: self.process_value(ctx, x), + **prompt_kwargs, + ) + + def resolve_envvar_value(self, ctx: Context) -> str | None: + """:class:`Option` resolves its environment variable the same way as + :func:`Parameter.resolve_envvar_value`, but it also supports + :attr:`Context.auto_envvar_prefix`. If we could not find an environment from + the :attr:`envvar` property, we fallback on :attr:`Context.auto_envvar_prefix` + to build dynamiccaly the environment variable name using the + :python:`{ctx.auto_envvar_prefix}_{self.name.upper()}` template. + + :meta private: + """ + rv = super().resolve_envvar_value(ctx) + + if rv is not None: + return rv + + if self.allow_from_autoenv and ctx.auto_envvar_prefix is not None and self.name: + envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}" + rv = os.environ.get(envvar) + + if rv: + return rv + + return None + + def value_from_envvar(self, ctx: Context) -> t.Any: + """For :class:`Option`, this method processes the raw environment variable + string the same way as :func:`Parameter.value_from_envvar` does. + + But in the case of non-boolean flags, the value is analyzed to determine if the + flag is activated or not, and returns a boolean of its activation, or the + :attr:`flag_value` if the latter is set. + + This method also takes care of repeated options (i.e. options with + :attr:`multiple` set to ``True``). + + :meta private: + """ + rv = self.resolve_envvar_value(ctx) + + # Absent environment variable or an empty string is interpreted as unset. + if rv is None: + return None + + # Non-boolean flags are more liberal in what they accept. But a flag being a + # flag, its envvar value still needs to be analyzed to determine if the flag is + # activated or not. + if self.is_flag and not self.is_bool_flag: + # An exact match against ``flag_value`` (a non-bool flag always has one + # explicitly set) returns it directly. Otherwise the value is analyzed as a + # boolean: a truthy reading activates the flag and the function receives + # ``flag_value``; a falsy reading produces ``False``; an unrecognized + # reading falls through as ``None``. Folding the substitution here means + # :meth:`consume_value` no longer has to repeat it in a source-checked + # branch. + if rv == self.flag_value: + return self.flag_value + parsed = types.BoolParamType.str_to_bool(rv) + if parsed is None: + return None + return self.flag_value if parsed else False + + # Split the envvar value if it is allowed to be repeated. + value_depth = (self.nargs != 1) + bool(self.multiple) + if value_depth > 0: + multi_rv = self.type.split_envvar_value(rv) + if self.multiple and self.nargs != 1: + multi_rv = batch(multi_rv, self.nargs) # type: ignore[assignment] + + return multi_rv + + return rv + + def consume_value( + self, ctx: Context, opts: cabc.Mapping[str, Parameter] + ) -> tuple[t.Any, ParameterSource]: + """For :class:`Option`, the value can be collected from an interactive prompt + if the option is a flag that needs a value (and the :attr:`prompt` property is + set). + + Additionally, this method handles flag option that are activated without a + value, in which case the :attr:`flag_value` is returned. + + :meta private: + """ + value, source = super().consume_value(ctx, opts) + + # The parser emits a sentinel when a flag is allowed to be used without a value. + # Resolve it to a prompt or to the activation value depending on the option's + # configuration. + if value is FLAG_NEEDS_VALUE: + # If the option allows for a prompt, start an interaction with the user. + if self.prompt is not None and not ctx.resilient_parsing: + value = self.prompt_for_value(ctx) + source = ParameterSource.PROMPT + # Else the flag takes its activation value (resolves UNSET). + else: + value = self.flag_activation_value + source = ParameterSource.COMMANDLINE + + # Re-interpret a multiple option that the parser sent through as a list still + # containing the FLAG_NEEDS_VALUE sentinel, replacing each occurrence with the + # activation value. + elif ( + self.multiple + and value is not UNSET + and isinstance(value, cabc.Iterable) + and source < ParameterSource.DEFAULT_MAP + and any(v is FLAG_NEEDS_VALUE for v in value) + ): + value = [ + self.flag_activation_value if v is FLAG_NEEDS_VALUE else v + for v in value + ] + source = ParameterSource.COMMANDLINE + + # The value wasn't set, or used the param's default, prompt for one to the user + # if prompting is enabled. + elif ( + (value is UNSET or source >= ParameterSource.DEFAULT_MAP) + and self.prompt is not None + and (self.required or self.prompt_required) + and not ctx.resilient_parsing + ): + value = self.prompt_for_value(ctx) + source = ParameterSource.PROMPT + + return value, source + + def process_value(self, ctx: Context, value: t.Any) -> t.Any: + # process_value has to be overridden on Options in order to capture + # `value == UNSET` cases before `type_cast_value()` gets called. + # + # Refs: + # https://github.com/pallets/click/issues/3069 + if self.is_flag and not self.required and self.is_bool_flag and value is UNSET: + value = False + + if self.callback is not None: + value = self.callback(ctx, self, value) + + return value + + # in the normal case, rely on Parameter.process_value + return super().process_value(ctx, value) + + +class Argument(Parameter): + """Arguments are positional parameters to a command. They generally + provide fewer features than options but can have infinite ``nargs`` + and are required by default. + + All parameters are passed onwards to the constructor of :class:`Parameter`. + + :param help: the help string. + + .. versionchanged:: 8.5.0 + Added the ``help`` parameter. + """ + + param_type_name = "argument" + + def __init__( + self, + param_decls: cabc.Sequence[str], + required: bool | None = None, + help: str | None = None, + **attrs: t.Any, + ) -> None: + # Auto-detect the requirement status of the argument if not explicitly set. + if required is None: + # The argument gets automatically required if it has no explicit default + # value set and is setup to match at least one value. + if attrs.get("default", UNSET) is UNSET: + required = attrs.get("nargs", 1) > 0 + # If the argument has a default value, it is not required. + else: + required = False + + if "multiple" in attrs: + raise TypeError("__init__() got an unexpected keyword argument 'multiple'.") + + deprecated = attrs.get("deprecated", False) + + if help: + help = inspect.cleandoc(help) + + if deprecated: + label = _format_deprecated_label(deprecated) + help = f"{help} {label}" if help else label + + self.help = help + + super().__init__(param_decls, required=required, **attrs) + + def to_info_dict(self) -> dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict.update(help=self.help) + return info_dict + + @property + def human_readable_name(self) -> str: + if self.metavar is not None: + return self.metavar + return self.name.upper() + + def make_metavar(self, ctx: Context) -> str: + if self.metavar is not None: + return self.metavar + var = self.type.get_metavar(param=self, ctx=ctx) + if not var: + var = self.name.upper() + # Types like ``Choice`` and ``DateTime`` already surround their metavar + # with square brackets to enumerate the allowed values. Reuse those + # outer brackets as the optional-argument indicator instead of wrapping + # the metavar in a second pair, which would produce ``[[a|b|c]]``. + already_bracketed = var.startswith("[") and var.endswith("]") + if self.deprecated: + var += "!" + if not self.required and not already_bracketed: + var = f"[{var}]" + if self.nargs != 1: + var += "..." + return var + + def _parse_decls( + self, decls: cabc.Sequence[str], expose_value: bool + ) -> tuple[str, list[str], list[str]]: + if not decls: + if not expose_value: + return "", [], [] + raise TypeError("Argument is marked as exposed, but does not have a name.") + if len(decls) == 1: + name = arg = decls[0] + name = name.replace("-", "_").lower() + else: + raise TypeError( + _( + "Arguments take exactly one parameter declaration, got" + " {length}: {decls}." + ).format(length=len(decls), decls=decls) + ) + return name, [arg], [] + + def get_usage_pieces(self, ctx: Context) -> list[str]: + return [self.make_metavar(ctx)] + + def get_help_record(self, ctx: Context) -> tuple[str, str] | None: + if self.help is None: + return None + + return self.make_metavar(ctx), self.help + + def get_error_hint(self, ctx: Context | None) -> str: + if ctx is not None: + return f"'{self.make_metavar(ctx)}'" + return f"'{self.human_readable_name}'" + + def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None: + parser.add_argument(dest=self.name, nargs=self.nargs, obj=self) + + +def __getattr__(name: str) -> object: + import warnings + + if name == "BaseCommand": + warnings.warn( + "'BaseCommand' is deprecated and will be removed in Click 9.0. Use" + " 'Command' instead.", + DeprecationWarning, + stacklevel=2, + ) + return _BaseCommand + + if name == "MultiCommand": + warnings.warn( + "'MultiCommand' is deprecated and will be removed in Click 9.0. Use" + " 'Group' instead.", + DeprecationWarning, + stacklevel=2, + ) + return _MultiCommand + + raise AttributeError(name) diff --git a/source/target-vendors/cp312/click/decorators.py b/source/target-vendors/cp312/click/decorators.py new file mode 100644 index 0000000..cc42e5d --- /dev/null +++ b/source/target-vendors/cp312/click/decorators.py @@ -0,0 +1,627 @@ +from __future__ import annotations + +import inspect +import typing as t +from functools import update_wrapper +from gettext import gettext as _ + +from .core import Argument +from .core import Command +from .core import Context +from .core import Group +from .core import Option +from .core import Parameter +from .globals import get_current_context +from .utils import echo + +if t.TYPE_CHECKING: + import typing_extensions as te + + P = te.ParamSpec("P") + +R = t.TypeVar("R") +T = t.TypeVar("T") +_AnyCallable = t.Callable[..., t.Any] +FC = t.TypeVar("FC", bound="_AnyCallable | Command") + + +def pass_context(f: t.Callable[te.Concatenate[Context, P], R]) -> t.Callable[P, R]: + """Marks a callback as wanting to receive the current context + object as first argument. + """ + + def new_func(*args: P.args, **kwargs: P.kwargs) -> R: + return f(get_current_context(), *args, **kwargs) + + return update_wrapper(new_func, f) + + +def pass_obj(f: t.Callable[te.Concatenate[T, P], R]) -> t.Callable[P, R]: + """Similar to :func:`pass_context`, but only pass the object on the + context onwards (:attr:`Context.obj`). This is useful if that object + represents the state of a nested system. + """ + + def new_func(*args: P.args, **kwargs: P.kwargs) -> R: + return f(get_current_context().obj, *args, **kwargs) + + return update_wrapper(new_func, f) + + +def make_pass_decorator( + object_type: type[T], ensure: bool = False +) -> t.Callable[[t.Callable[te.Concatenate[T, P], R]], t.Callable[P, R]]: + """Given an object type this creates a decorator that will work + similar to :func:`pass_obj` but instead of passing the object of the + current context, it will find the innermost context of type + :func:`object_type`. + + This generates a decorator that works roughly like this:: + + from functools import update_wrapper + + def decorator(f): + @pass_context + def new_func(ctx, *args, **kwargs): + obj = ctx.find_object(object_type) + return ctx.invoke(f, obj, *args, **kwargs) + return update_wrapper(new_func, f) + return decorator + + :param object_type: the type of the object to pass. + :param ensure: if set to `True`, a new object will be created and + remembered on the context if it's not there yet. + """ + + def decorator(f: t.Callable[te.Concatenate[T, P], R]) -> t.Callable[P, R]: + def new_func(*args: P.args, **kwargs: P.kwargs) -> R: + ctx = get_current_context() + + obj: T | None + if ensure: + obj = ctx.ensure_object(object_type) + else: + obj = ctx.find_object(object_type) + + if obj is None: + raise RuntimeError( + "Managed to invoke callback without a context" + f" object of type {object_type.__name__!r}" + " existing." + ) + + return ctx.invoke(f, obj, *args, **kwargs) + + return update_wrapper(new_func, f) + + return decorator + + +def pass_meta_key( + key: str, *, doc_description: str | None = None +) -> t.Callable[[t.Callable[te.Concatenate[T, P], R]], t.Callable[P, R]]: + """Create a decorator that passes a key from + :attr:`click.Context.meta` as the first argument to the decorated + function. + + :param key: Key in ``Context.meta`` to pass. + :param doc_description: Description of the object being passed, + inserted into the decorator's docstring. Defaults to "the 'key' + key from Context.meta". + + .. versionadded:: 8.0 + """ + + def decorator(f: t.Callable[te.Concatenate[T, P], R]) -> t.Callable[P, R]: + def new_func(*args: P.args, **kwargs: P.kwargs) -> R: + ctx = get_current_context() + obj = ctx.meta[key] + return ctx.invoke(f, obj, *args, **kwargs) + + return update_wrapper(new_func, f) + + if doc_description is None: + doc_description = f"the {key!r} key from :attr:`click.Context.meta`" + + decorator.__doc__ = ( + f"Decorator that passes {doc_description} as the first argument" + " to the decorated function." + ) + return decorator + + +CmdType = t.TypeVar("CmdType", bound=Command) + + +# variant: no call, directly as decorator for a function. +@t.overload +def command(name: _AnyCallable) -> Command: ... + + +# variant: with positional name and with positional or keyword cls argument: +# @command(namearg, CommandCls, ...) or @command(namearg, cls=CommandCls, ...) +@t.overload +def command( + name: str | None, + cls: type[CmdType], + **attrs: t.Any, +) -> t.Callable[[_AnyCallable], CmdType]: ... + + +# variant: name omitted, cls _must_ be a keyword argument, @command(cls=CommandCls, ...) +@t.overload +def command( + name: None = None, + *, + cls: type[CmdType], + **attrs: t.Any, +) -> t.Callable[[_AnyCallable], CmdType]: ... + + +# variant: with optional string name, no cls argument provided. +@t.overload +def command( + name: str | None = ..., cls: None = None, **attrs: t.Any +) -> t.Callable[[_AnyCallable], Command]: ... + + +def command( + name: str | _AnyCallable | None = None, + cls: type[CmdType] | None = None, + **attrs: t.Any, +) -> Command | t.Callable[[_AnyCallable], Command | CmdType]: + r"""Creates a new :class:`Command` and uses the decorated function as + callback. This will also automatically attach all decorated + :func:`option`\s and :func:`argument`\s as parameters to the command. + + The name of the command defaults to the name of the function, converted to + lowercase, with underscores ``_`` replaced by dashes ``-``, and the suffixes + ``_command``, ``_cmd``, ``_group``, and ``_grp`` are removed. For example, + ``init_data_command`` becomes ``init-data``. + + All keyword arguments are forwarded to the underlying command class. + For the ``params`` argument, any decorated params are appended to + the end of the list. + + Once decorated the function turns into a :class:`Command` instance + that can be invoked as a command line utility or be attached to a + command :class:`Group`. + + :param name: The name of the command. Defaults to modifying the function's + name as described above. + :param cls: The command class to create. Defaults to :class:`Command`. + + .. versionchanged:: 8.2 + The suffixes ``_command``, ``_cmd``, ``_group``, and ``_grp`` are + removed when generating the name. + + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. + + .. versionchanged:: 8.1 + The ``params`` argument can be used. Decorated params are + appended to the end of the list. + """ + + func: t.Callable[[_AnyCallable], t.Any] | None = None + + if callable(name): + func = name + name = None + assert cls is None, "Use 'command(cls=cls)(callable)' to specify a class." + assert not attrs, "Use 'command(**kwargs)(callable)' to provide arguments." + + if cls is None: + cls = t.cast("type[CmdType]", Command) + + def decorator(f: _AnyCallable) -> CmdType: + if isinstance(f, Command): + raise TypeError("Attempted to convert a callback into a command twice.") + + attr_params = attrs.pop("params", None) + params = attr_params if attr_params is not None else [] + + try: + decorator_params = f.__click_params__ # type: ignore + except AttributeError: + pass + else: + del f.__click_params__ # type: ignore + params.extend(reversed(decorator_params)) + + if attrs.get("help") is None: + attrs["help"] = f.__doc__ + + if t.TYPE_CHECKING: + assert cls is not None + assert not callable(name) + + if name is not None: + cmd_name = name + else: + cmd_name = f.__name__.lower().replace("_", "-") + cmd_left, sep, suffix = cmd_name.rpartition("-") + + if sep and suffix in {"command", "cmd", "group", "grp"}: + cmd_name = cmd_left + + cmd = cls(name=cmd_name, callback=f, params=params, **attrs) + cmd.__doc__ = f.__doc__ + return cmd + + if func is not None: + return decorator(func) + + return decorator + + +GrpType = t.TypeVar("GrpType", bound=Group) + + +# variant: no call, directly as decorator for a function. +@t.overload +def group(name: _AnyCallable) -> Group: ... + + +# variant: with positional name and with positional or keyword cls argument: +# @group(namearg, GroupCls, ...) or @group(namearg, cls=GroupCls, ...) +@t.overload +def group( + name: str | None, + cls: type[GrpType], + **attrs: t.Any, +) -> t.Callable[[_AnyCallable], GrpType]: ... + + +# variant: name omitted, cls _must_ be a keyword argument, @group(cmd=GroupCls, ...) +@t.overload +def group( + name: None = None, + *, + cls: type[GrpType], + **attrs: t.Any, +) -> t.Callable[[_AnyCallable], GrpType]: ... + + +# variant: with optional string name, no cls argument provided. +@t.overload +def group( + name: str | None = ..., cls: None = None, **attrs: t.Any +) -> t.Callable[[_AnyCallable], Group]: ... + + +def group( + name: str | _AnyCallable | None = None, + cls: type[GrpType] | None = None, + **attrs: t.Any, +) -> Group | t.Callable[[_AnyCallable], Group | GrpType]: + """Creates a new :class:`Group` with a function as callback. This + works otherwise the same as :func:`command` just that the `cls` + parameter is set to :class:`Group`. + + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. + """ + if cls is None: + cls = t.cast("type[GrpType]", Group) + + if callable(name): + return command(cls=cls, **attrs)(name) + + return command(name, cls, **attrs) + + +def _param_memo(f: t.Callable[..., t.Any], param: Parameter) -> None: + if isinstance(f, Command): + f.params.append(param) + else: + if not hasattr(f, "__click_params__"): + f.__click_params__ = [] # type: ignore + + f.__click_params__.append(param) # type: ignore + + +def argument( + *param_decls: str, cls: type[Argument] | None = None, **attrs: t.Any +) -> t.Callable[[FC], FC]: + """Attaches an argument to the command. All positional arguments are + passed as parameter declarations to :class:`Argument`; all keyword + arguments are forwarded unchanged (except ``cls``). + This is equivalent to creating an :class:`Argument` instance manually + and attaching it to the :attr:`Command.params` list. + + For the default argument class, refer to :class:`Argument` and + :class:`Parameter` for descriptions of parameters. + + :param cls: the argument class to instantiate. This defaults to + :class:`Argument`. + :param param_decls: Passed as positional arguments to the constructor of + ``cls``. + :param attrs: Passed as keyword arguments to the constructor of ``cls``. + """ + if cls is None: + cls = Argument + + def decorator(f: FC) -> FC: + _param_memo(f, cls(param_decls, **attrs)) + return f + + return decorator + + +def option( + *param_decls: str, cls: type[Option] | None = None, **attrs: t.Any +) -> t.Callable[[FC], FC]: + """Attaches an option to the command. All positional arguments are + passed as parameter declarations to :class:`Option`; all keyword + arguments are forwarded unchanged (except ``cls``). + This is equivalent to creating an :class:`Option` instance manually + and attaching it to the :attr:`Command.params` list. + + For the default option class, refer to :class:`Option` and + :class:`Parameter` for descriptions of parameters. + + :param cls: the option class to instantiate. This defaults to + :class:`Option`. + :param param_decls: Passed as positional arguments to the constructor of + ``cls``. + :param attrs: Passed as keyword arguments to the constructor of ``cls``. + """ + if cls is None: + cls = Option + + def decorator(f: FC) -> FC: + _param_memo(f, cls(param_decls, **attrs)) + return f + + return decorator + + +def confirmation_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: + """Add a ``--yes`` option which shows a prompt before continuing if + not passed. If the prompt is declined, the program will exit. + + :param param_decls: One or more option names. Defaults to the single + value ``"--yes"``. + :param kwargs: Extra arguments are passed to :func:`option`. + """ + + def callback(ctx: Context, param: Parameter, value: bool) -> None: + if not value: + ctx.abort() + + if not param_decls: + param_decls = ("--yes",) + + kwargs.setdefault("is_flag", True) + kwargs.setdefault("callback", callback) + kwargs.setdefault("expose_value", False) + kwargs.setdefault("prompt", _("Do you want to continue?")) + kwargs.setdefault("help", _("Confirm the action without prompting.")) + return option(*param_decls, **kwargs) + + +def password_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: + """Add a ``--password`` option which prompts for a password, hiding + input and asking to enter the value again for confirmation. + + :param param_decls: One or more option names. Defaults to the single + value ``"--password"``. + :param kwargs: Extra arguments are passed to :func:`option`. + """ + if not param_decls: + param_decls = ("--password",) + + kwargs.setdefault("prompt", True) + kwargs.setdefault("confirmation_prompt", True) + kwargs.setdefault("hide_input", True) + return option(*param_decls, **kwargs) + + +def version_option( + version: str | None = None, + *param_decls: str, + package_name: str | None = None, + prog_name: str | None = None, + message: str | None = None, + **kwargs: t.Any, +) -> t.Callable[[FC], FC]: + """Add a ``--version`` option which immediately prints the version + number and exits the program. + + If ``version`` is not provided, Click will try to detect it using + :func:`importlib.metadata.version` to get the version for the + ``package_name``. + + If ``package_name`` is not provided, Click will try to detect it by + inspecting the stack frames. If the detected (or given) name does + not match an installed distribution, Click resolves it as an import + (top-level module) name via + :func:`importlib.metadata.packages_distributions`, so e.g. ``PIL`` + resolves to the ``Pillow`` distribution. + + .. note:: + The parameters and message variables accepted by this option are + frozen: no new slots will be added, to keep the common case simple + and predictable. If you need values it does not expose, such as a + file path, the Python version, or git metadata, use + :func:`custom_version_option` to render the output yourself. + + Rationale: `discussion #3527 + `_. + + :param version: The version number to show. If not provided, Click + will try to detect it. + :param param_decls: One or more option names. Defaults to the single + value ``"--version"``. + :param package_name: The package name to detect the version from. If + not provided, Click will try to detect it. + :param prog_name: The name of the CLI to show in the message. If not + provided, it will be detected from the command. + :param message: The message to show. The values ``%(prog)s``, + ``%(package)s``, and ``%(version)s`` are available. Defaults to + ``"%(prog)s, version %(version)s"``. + :param kwargs: Extra arguments are passed to :func:`option`. + :raise RuntimeError: ``version`` could not be detected. + + .. versionchanged:: 8.0 + Add the ``package_name`` parameter, and the ``%(package)s`` + value for messages. + + .. versionchanged:: 8.0 + Use :mod:`importlib.metadata` instead of ``pkg_resources``. The + version is detected based on the package name, not the entry + point name. The Python package name must match the installed + package name, or be passed with ``package_name=``. + + .. versionchanged:: 8.4.2 + When ``package_name`` does not match an installed distribution, + Click now resolves it as an import (top-level module). + """ + if message is None: + message = _("%(prog)s, version %(version)s") + + if version is None and package_name is None: + frame = inspect.currentframe() + f_back = frame.f_back if frame is not None else None + f_globals = f_back.f_globals if f_back is not None else None + # break reference cycle + # https://docs.python.org/3/library/inspect.html#the-interpreter-stack + del frame + + if f_globals is not None: + package_name = f_globals.get("__name__") + + if package_name == "__main__": + package_name = f_globals.get("__package__") + + if package_name: + package_name = package_name.partition(".")[0] + + def callback(ctx: Context, param: Parameter, value: bool) -> None: + if not value or ctx.resilient_parsing: + return + + nonlocal prog_name + nonlocal version + nonlocal package_name + + if prog_name is None: + prog_name = ctx.find_root().info_name + + if version is None and package_name is not None: + import importlib.metadata + + try: + version = importlib.metadata.version(package_name) + except importlib.metadata.PackageNotFoundError: + # The given name didn't match an installed distribution. + # Try resolving it as an import (top-level module) name, + # e.g. ``PIL`` is provided by the ``Pillow`` distribution. + distributions = importlib.metadata.packages_distributions().get( + package_name, [] + ) + if len(distributions) == 1: + package_name = distributions[0] + version = importlib.metadata.version(package_name) + elif len(distributions) > 1: + raise RuntimeError( + f"{package_name!r} maps to multiple installed" + f" distributions ({', '.join(distributions)})." + " Pass 'package_name' to disambiguate." + ) from None + else: + raise RuntimeError( + f"{package_name!r} is not installed. Try passing" + " 'package_name' instead." + ) from None + + if version is None: + raise RuntimeError( + f"Could not determine the version for {package_name!r} automatically." + ) + + echo( + message % {"prog": prog_name, "package": package_name, "version": version}, + color=ctx.color, + ) + ctx.exit() + + if not param_decls: + param_decls = ("--version",) + + kwargs.setdefault("is_flag", True) + kwargs.setdefault("expose_value", False) + kwargs.setdefault("is_eager", True) + kwargs.setdefault("help", _("Show the version and exit.")) + kwargs["callback"] = callback + return option(*param_decls, **kwargs) + + +def custom_version_option( + callback: t.Callable[[Context], str], + *param_decls: str, + **kwargs: t.Any, +) -> t.Callable[[FC], FC]: + """Add a ``--version`` option whose output is produced by ``callback``. + + This is the customizable companion to :func:`version_option`. Where + :func:`version_option` is intentionally limited to a fixed message and + a small set of values, this option calls ``callback`` to build the + whole string to print. Use it when you need values that + :func:`version_option` does not expose, such as a file path, the + Python version, or git metadata. + + :param callback: Called with the current :class:`Context` when the + option is invoked. Its return value is printed, then the program + exits. + :param param_decls: One or more option names. Defaults to the single + value ``--version``. + :param kwargs: Extra arguments are passed to :func:`option`. + + .. versionadded:: 8.5.0 + """ + + def show_version(ctx: Context, param: Parameter, value: bool) -> None: + if not value or ctx.resilient_parsing: + return + + echo(callback(ctx), color=ctx.color) + ctx.exit() + + if not param_decls: + param_decls = ("--version",) + + kwargs.setdefault("is_flag", True) + kwargs.setdefault("expose_value", False) + kwargs.setdefault("is_eager", True) + kwargs.setdefault("help", _("Show the version and exit.")) + kwargs["callback"] = show_version + return option(*param_decls, **kwargs) + + +def help_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: + """Pre-configured ``--help`` option which immediately prints the help page + and exits the program. + + :param param_decls: One or more option names. Defaults to the single + value ``"--help"``. + :param kwargs: Extra arguments are passed to :func:`option`. + """ + + def show_help(ctx: Context, param: Parameter, value: bool) -> None: + """Callback that print the help page on ```` and exits.""" + if value and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + + if not param_decls: + param_decls = ("--help",) + + kwargs.setdefault("is_flag", True) + kwargs.setdefault("expose_value", False) + kwargs.setdefault("is_eager", True) + kwargs.setdefault("help", _("Show this message and exit.")) + kwargs.setdefault("callback", show_help) + + return option(*param_decls, **kwargs) diff --git a/source/target-vendors/cp312/click/exceptions.py b/source/target-vendors/cp312/click/exceptions.py new file mode 100644 index 0000000..6272c38 --- /dev/null +++ b/source/target-vendors/cp312/click/exceptions.py @@ -0,0 +1,378 @@ +from __future__ import annotations + +import collections.abc as cabc +import typing as t +from gettext import gettext as _ +from gettext import ngettext + +from ._compat import get_text_stderr +from .globals import resolve_color_default +from .utils import echo +from .utils import format_filename + +if t.TYPE_CHECKING: + from .core import Command + from .core import Context + from .core import Parameter + + +def _join_param_hints(param_hint: cabc.Sequence[str] | str | None) -> str | None: + if param_hint is not None and not isinstance(param_hint, str): + return " / ".join(repr(x) for x in param_hint) + + return param_hint + + +def _format_possibilities(possibilities: list[str]) -> str: + possibility_str = ", ".join(repr(p) for p in sorted(possibilities)) + return ngettext( + "Did you mean {possibility}?", + "(Did you mean one of: {possibilities}?)", + len(possibilities), + ).format(possibility=possibility_str, possibilities=possibility_str) + + +class ClickException(Exception): + """An exception that Click can handle and show to the user.""" + + #: The exit code for this exception. + exit_code: t.ClassVar[int] = 1 + + show_color: t.Final[bool | None] + message: t.Final[str] + + def __init__(self, message: str) -> None: + super().__init__(message) + # The context will be removed by the time we print the message, so cache + # the color settings here to be used later on (in `show`) + self.show_color = resolve_color_default() + self.message = message + + def format_message(self) -> str: + return self.message + + def __str__(self) -> str: + return self.message + + def show(self, file: t.IO[t.Any] | None = None) -> None: + if file is None: + file = get_text_stderr() + + echo( + _("Error: {message}").format(message=self.format_message()), + file=file, + color=self.show_color, + ) + + +class UsageError(ClickException): + """An internal exception that signals a usage error. This typically + aborts any further handling. + + :param message: the error message to display. + :param ctx: optionally the context that caused this error. Click will + fill in the context automatically in some situations. + """ + + exit_code: t.ClassVar[int] = 2 + + ctx: Context | None + cmd: t.Final[Command | None] + + def __init__(self, message: str, ctx: Context | None = None) -> None: + super().__init__(message) + self.ctx = ctx + self.cmd = self.ctx.command if self.ctx else None + + def show(self, file: t.IO[t.Any] | None = None) -> None: + if file is None: + file = get_text_stderr() + color = None + hint = "" + if ( + self.ctx is not None + and self.ctx.command.get_help_option(self.ctx) is not None + ): + help_names = self.ctx.command.get_help_option_names(self.ctx) + # Pick the longest name (like ``--help`` over ``-h``) for + # readability in error messages. + hint = _("Try '{command} {option}' for help.").format( + command=self.ctx.command_path, + option=max(help_names, key=len), + ) + hint = f"{hint}\n" + if self.ctx is not None: + color = self.ctx.color + echo(f"{self.ctx.get_usage()}\n{hint}", file=file, color=color) + echo( + _("Error: {message}").format(message=self.format_message()), + file=file, + color=color, + ) + + +class BadParameter(UsageError): + """An exception that formats out a standardized error message for a + bad parameter. This is useful when thrown from a callback or type as + Click will attach contextual information to it (for instance, which + parameter it is). + + .. versionadded:: 2.0 + + :param param: the parameter object that caused this error. This can + be left out, and Click will attach this info itself + if possible. + :param param_hint: a string that shows up as parameter name. This + can be used as alternative to `param` in cases + where custom validation should happen. If it is + a string it's used as such, if it's a list then + each item is quoted and separated. + """ + + param: Parameter | None + param_hint: cabc.Sequence[str] | str | None + + def __init__( + self, + message: str, + ctx: Context | None = None, + param: Parameter | None = None, + param_hint: cabc.Sequence[str] | str | None = None, + ) -> None: + super().__init__(message, ctx) + self.param = param + self.param_hint = param_hint + + def format_message(self) -> str: + if self.param_hint is not None: + param_hint = self.param_hint + elif self.param is not None: + param_hint = self.param.get_error_hint(self.ctx) + else: + return _("Invalid value: {message}").format(message=self.message) + + return _("Invalid value for {param_hint}: {message}").format( + param_hint=_join_param_hints(param_hint), message=self.message + ) + + +class MissingParameter(BadParameter): + """Raised if click required an option or argument but it was not + provided when invoking the script. + + .. versionadded:: 4.0 + + :param param_type: a string that indicates the type of the parameter. + The default is to inherit the parameter type from + the given `param`. Valid values are ``'parameter'``, + ``'option'`` or ``'argument'``. + """ + + param_type: t.Final[str | None] + + def __init__( + self, + message: str | None = None, + ctx: Context | None = None, + param: Parameter | None = None, + param_hint: cabc.Sequence[str] | str | None = None, + param_type: str | None = None, + ) -> None: + super().__init__(message or "", ctx, param, param_hint) + self.param_type = param_type + + def format_message(self) -> str: + if self.param_hint is not None: + param_hint: cabc.Sequence[str] | str | None = self.param_hint + elif self.param is not None: + param_hint = self.param.get_error_hint(self.ctx) + else: + param_hint = None + + param_hint = _join_param_hints(param_hint) + param_hint = f" {param_hint}" if param_hint else "" + + param_type = self.param_type + if param_type is None and self.param is not None: + param_type = self.param.param_type_name + + msg = self.message + if self.param is not None: + msg_extra = self.param.type.get_missing_message( + param=self.param, ctx=self.ctx + ) + if msg_extra: + if msg: + msg += f". {msg_extra}" + else: + msg = msg_extra + + msg = f" {msg}" if msg else "" + + # Translate param_type for known types. + if param_type == "argument": + missing = _("Missing argument") + elif param_type == "option": + missing = _("Missing option") + elif param_type == "parameter": + missing = _("Missing parameter") + else: + missing = _("Missing {param_type}").format(param_type=param_type) + + return f"{missing}{param_hint}.{msg}" + + def __str__(self) -> str: + if not self.message: + param_name = self.param.name if self.param else None + return _("Missing parameter: {param_name}").format(param_name=param_name) + else: + return self.message + + +class NoSuchOption(UsageError): + """Raised if Click attempted to handle an option that does not exist. + + .. versionadded:: 4.0 + """ + + option_name: t.Final[str] + possibilities: t.Final[list[str] | None] + + def __init__( + self, + option_name: str, + message: str | None = None, + possibilities: cabc.Iterable[str] | None = None, + ctx: Context | None = None, + ) -> None: + if message is None: + message = _("No such option {name!r}.").format(name=option_name) + + super().__init__(message, ctx) + self.option_name = option_name + + if possibilities: + from difflib import get_close_matches + + possibilities_ = get_close_matches(option_name, possibilities) + else: + possibilities_ = None + self.possibilities = possibilities_ + + def format_message(self) -> str: + if not self.possibilities: + return self.message + return f"{self.message} {_format_possibilities(self.possibilities)}" + + +class NoSuchCommand(UsageError): + """Raised if Click attempted to handle a command that does not exist. + + .. versionadded:: 8.4.0 + """ + + command_name: t.Final[str] + possibilities: t.Final[list[str] | None] + + def __init__( + self, + command_name: str, + message: str | None = None, + possibilities: cabc.Iterable[str] | None = None, + ctx: Context | None = None, + ) -> None: + if message is None: + message = _("No such command {name!r}.").format(name=command_name) + + super().__init__(message, ctx) + self.command_name = command_name + + if possibilities: + from difflib import get_close_matches + + possibilities_ = get_close_matches(command_name, possibilities) + else: + possibilities_ = None + self.possibilities = possibilities_ + + def format_message(self) -> str: + if not self.possibilities: + return self.message + return f"{self.message} {_format_possibilities(self.possibilities)}" + + +class BadOptionUsage(UsageError): + """Raised if an option is generally supplied but the use of the option + was incorrect. This is for instance raised if the number of arguments + for an option is not correct. + + .. versionadded:: 4.0 + + :param option_name: the name of the option being used incorrectly. + """ + + option_name: t.Final[str] + + def __init__( + self, option_name: str, message: str, ctx: Context | None = None + ) -> None: + super().__init__(message, ctx) + self.option_name = option_name + + +class BadArgumentUsage(UsageError): + """Raised if an argument is generally supplied but the use of the argument + was incorrect. This is for instance raised if the number of values + for an argument is not correct. + + .. versionadded:: 6.0 + """ + + +class NoArgsIsHelpError(UsageError): + ctx: Context + + def __init__(self, ctx: Context) -> None: + super().__init__(ctx.get_help(), ctx=ctx) + + def show(self, file: t.IO[t.Any] | None = None) -> None: + echo(self.format_message(), file=file, err=True, color=self.ctx.color) + + +class FileError(ClickException): + """Raised if a file cannot be opened.""" + + ui_filename: t.Final[str] + filename: t.Final[str] + + def __init__(self, filename: str, hint: str | None = None) -> None: + if hint is None: + hint = _("unknown error") + + super().__init__(hint) + self.ui_filename = format_filename(filename) + self.filename = filename + + def format_message(self) -> str: + return _("Could not open file {filename!r}: {message}").format( + filename=self.ui_filename, message=self.message + ) + + +class Abort(RuntimeError): + """An internal signalling exception that signals Click to abort.""" + + +class Exit(RuntimeError): + """An exception that indicates that the application should exit with some + status code. + + :param code: the status code to exit with. + """ + + __slots__ = ("exit_code",) + + exit_code: t.Final[int] + + def __init__(self, code: int = 0) -> None: + self.exit_code = code diff --git a/source/target-vendors/cp312/click/formatting.py b/source/target-vendors/cp312/click/formatting.py new file mode 100644 index 0000000..c4aa2de --- /dev/null +++ b/source/target-vendors/cp312/click/formatting.py @@ -0,0 +1,320 @@ +from __future__ import annotations + +import collections.abc as cabc +from contextlib import contextmanager +from gettext import gettext as _ + +from ._compat import term_len +from .parser import _split_opt + +# Can force a width. This is used by the test system +FORCED_WIDTH: int | None = None + + +def measure_table(rows: cabc.Iterable[tuple[str, str]]) -> tuple[int, ...]: + widths: dict[int, int] = {} + + for row in rows: + for idx, col in enumerate(row): + widths[idx] = max(widths.get(idx, 0), term_len(col)) + + return tuple(y for x, y in sorted(widths.items())) + + +def iter_rows( + rows: cabc.Iterable[tuple[str, str]], col_count: int +) -> cabc.Iterator[tuple[str, ...]]: + for row in rows: + yield row + ("",) * (col_count - len(row)) + + +def wrap_text( + text: str, + width: int = 78, + initial_indent: str = "", + subsequent_indent: str = "", + preserve_paragraphs: bool = False, +) -> str: + """A helper function that intelligently wraps text. By default, it + assumes that it operates on a single paragraph of text but if the + `preserve_paragraphs` parameter is provided it will intelligently + handle paragraphs (defined by two empty lines). + + If paragraphs are handled, a paragraph can be prefixed with an empty + line containing the ``\\b`` character (``\\x08``) to indicate that + no rewrapping should happen in that block. + + :param text: the text that should be rewrapped. + :param width: the maximum width for the text. + :param initial_indent: the initial indent that should be placed on the + first line as a string. + :param subsequent_indent: the indent string that should be placed on + each consecutive line. + :param preserve_paragraphs: if this flag is set then the wrapping will + intelligently handle paragraphs. + + .. versionchanged:: 8.4.0 + Width is measured in visible characters. ANSI escape sequences in + ``text``, ``initial_indent``, or ``subsequent_indent`` no longer + count toward the width budget, so styled input wraps based on what + the user sees instead of raw byte length. + """ + from ._textwrap import TextWrapper + + text = text.expandtabs() + wrapper = TextWrapper( + width, + initial_indent=initial_indent, + subsequent_indent=subsequent_indent, + replace_whitespace=False, + ) + if not preserve_paragraphs: + return wrapper.fill(text) + + p: list[tuple[int, bool, str]] = [] + buf: list[str] = [] + indent = None + + def _flush_par() -> None: + if not buf: + return + if buf[0].strip() == "\b": + p.append((indent or 0, True, "\n".join(buf[1:]))) + else: + p.append((indent or 0, False, " ".join(buf))) + del buf[:] + + for line in text.splitlines(): + if not line: + _flush_par() + indent = None + else: + if indent is None: + orig_len = term_len(line) + line = line.lstrip() + indent = orig_len - term_len(line) + buf.append(line) + _flush_par() + + rv = [] + for indent, raw, text in p: + with wrapper.extra_indent(" " * indent): + if raw: + rv.append(wrapper.indent_only(text)) + else: + rv.append(wrapper.fill(text)) + + return "\n\n".join(rv) + + +class HelpFormatter: + """This class helps with formatting text-based help pages. It's + usually just needed for very special internal cases, but it's also + exposed so that developers can write their own fancy outputs. + + At present, it always writes into memory. + + :param indent_increment: the additional increment for each level. + :param width: the width for the text. This defaults to the terminal + width clamped to a maximum of 78. + """ + + indent_increment: int + width: int + current_indent: int + buffer: list[str] + + def __init__( + self, + indent_increment: int = 2, + width: int | None = None, + max_width: int | None = None, + ) -> None: + self.indent_increment = indent_increment + if max_width is None: + max_width = 80 + if width is None: + import shutil + + width = FORCED_WIDTH + if width is None: + width = max(min(shutil.get_terminal_size().columns, max_width) - 2, 50) + self.width = width + self.current_indent = 0 + self.buffer = [] + + def write(self, string: str) -> None: + """Writes a unicode string into the internal buffer.""" + self.buffer.append(string) + + def indent(self) -> None: + """Increases the indentation.""" + self.current_indent += self.indent_increment + + def dedent(self) -> None: + """Decreases the indentation.""" + self.current_indent -= self.indent_increment + + def write_usage(self, prog: str, args: str = "", prefix: str | None = None) -> None: + """Writes a usage line into the buffer. + + :param prog: the program name. + :param args: whitespace separated list of arguments. + :param prefix: The prefix for the first line. Defaults to + ``"Usage: "``. + """ + if prefix is None: + prefix = "{usage} ".format(usage=_("Usage:")) + + usage_prefix = f"{prefix:>{self.current_indent}}{prog} " + text_width = self.width - self.current_indent + + if not args: + # Without args, the prefix's trailing space and the wrap_text + # call that would normally place args on the line are both + # unnecessary. Emit just the prefix line. + self.write(usage_prefix.rstrip(" ")) + self.write("\n") + return + + if text_width >= (term_len(usage_prefix) + 20): + # The arguments will fit to the right of the prefix. + indent = " " * term_len(usage_prefix) + self.write( + wrap_text( + args, + text_width, + initial_indent=usage_prefix, + subsequent_indent=indent, + ) + ) + else: + # The prefix is too long, put the arguments on the next line. + self.write(usage_prefix) + self.write("\n") + indent = " " * (max(self.current_indent, term_len(prefix)) + 4) + self.write( + wrap_text( + args, text_width, initial_indent=indent, subsequent_indent=indent + ) + ) + + self.write("\n") + + def write_heading(self, heading: str) -> None: + """Writes a heading into the buffer.""" + self.write(f"{'':>{self.current_indent}}{heading}:\n") + + def write_paragraph(self) -> None: + """Writes a paragraph into the buffer.""" + if self.buffer: + self.write("\n") + + def write_text(self, text: str) -> None: + """Writes re-indented text into the buffer. This rewraps and + preserves paragraphs. + """ + indent = " " * self.current_indent + self.write( + wrap_text( + text, + self.width, + initial_indent=indent, + subsequent_indent=indent, + preserve_paragraphs=True, + ) + ) + self.write("\n") + + def write_dl( + self, + rows: cabc.Iterable[tuple[str, str]], + col_max: int = 30, + col_spacing: int = 2, + ) -> None: + """Writes a definition list into the buffer. This is how options + and commands are usually formatted. + + :param rows: a list of two item tuples for the terms and values. + :param col_max: the maximum width of the first column. + :param col_spacing: the number of spaces between the first and + second column. + """ + rows = list(rows) + widths = measure_table(rows) + if len(widths) != 2: + raise TypeError("Expected two columns for definition list") + + first_col = min(widths[0], col_max) + col_spacing + + for first, second in iter_rows(rows, len(widths)): + self.write(f"{'':>{self.current_indent}}{first}") + if not second: + self.write("\n") + continue + if term_len(first) <= first_col - col_spacing: + self.write(" " * (first_col - term_len(first))) + else: + self.write("\n") + self.write(" " * (first_col + self.current_indent)) + + text_width = max(self.width - first_col - 2, 10) + wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True) + lines = wrapped_text.splitlines() + + if lines: + self.write(f"{lines[0]}\n") + + for line in lines[1:]: + self.write(f"{'':>{first_col + self.current_indent}}{line}\n") + else: + self.write("\n") + + @contextmanager + def section(self, name: str) -> cabc.Generator[None]: + """Helpful context manager that writes a paragraph, a heading, + and the indents. + + :param name: the section name that is written as heading. + """ + self.write_paragraph() + self.write_heading(name) + self.indent() + try: + yield + finally: + self.dedent() + + @contextmanager + def indentation(self) -> cabc.Generator[None]: + """A context manager that increases the indentation.""" + self.indent() + try: + yield + finally: + self.dedent() + + def getvalue(self) -> str: + """Returns the buffer contents.""" + return "".join(self.buffer) + + +def join_options(options: cabc.Iterable[str]) -> tuple[str, bool]: + """Given a list of option strings this joins them in the most appropriate + way and returns them in the form ``(formatted_string, + any_prefix_is_slash)`` where the second item in the tuple is a flag that + indicates if any of the option prefixes was a slash. + """ + rv = [] + any_prefix_is_slash = False + + for opt in options: + prefix = _split_opt(opt)[0] + + if prefix == "/": + any_prefix_is_slash = True + + rv.append((len(prefix), opt)) + + rv.sort(key=lambda x: x[0]) + return ", ".join(x[1] for x in rv), any_prefix_is_slash diff --git a/source/target-vendors/cp312/click/globals.py b/source/target-vendors/cp312/click/globals.py new file mode 100644 index 0000000..a2f9172 --- /dev/null +++ b/source/target-vendors/cp312/click/globals.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import typing as t +from threading import local + +if t.TYPE_CHECKING: + from .core import Context + +_local = local() + + +@t.overload +def get_current_context(silent: t.Literal[False] = False) -> Context: ... + + +@t.overload +def get_current_context(silent: bool = ...) -> Context | None: ... + + +def get_current_context(silent: bool = False) -> Context | None: + """Returns the current click context. This can be used as a way to + access the current context object from anywhere. This is a more implicit + alternative to the :func:`pass_context` decorator. This function is + primarily useful for helpers such as :func:`echo` which might be + interested in changing its behavior based on the current context. + + To push the current context, :meth:`Context.scope` can be used. + + .. versionadded:: 5.0 + + :param silent: if set to `True` the return value is `None` if no context + is available. The default behavior is to raise a + :exc:`RuntimeError`. + """ + try: + return t.cast("Context", _local.stack[-1]) + except (AttributeError, IndexError) as e: + if not silent: + raise RuntimeError("There is no active click context.") from e + + return None + + +def push_context(ctx: Context) -> None: + """Pushes a new context to the current stack.""" + _local.__dict__.setdefault("stack", []).append(ctx) + + +def pop_context() -> None: + """Removes the top level from the stack.""" + _local.stack.pop() + + +def resolve_color_default(color: bool | None = None) -> bool | None: + """Internal helper to get the default value of the color flag. If a + value is passed it's returned unchanged, otherwise it's looked up from + the current context. + """ + if color is not None: + return color + + ctx = get_current_context(silent=True) + + if ctx is not None: + return ctx.color + + return None diff --git a/source/target-vendors/cp312/click/parser.py b/source/target-vendors/cp312/click/parser.py new file mode 100644 index 0000000..4fcbf7c --- /dev/null +++ b/source/target-vendors/cp312/click/parser.py @@ -0,0 +1,533 @@ +""" +This module started out as largely a copy paste from the stdlib's +optparse module with the features removed that we do not need from +optparse because we implement them in Click on a higher level (for +instance type handling, help formatting and a lot more). + +The plan is to remove more and more from here over time. + +The reason this is a different module and not optparse from the stdlib +is that there are differences in 2.x and 3.x about the error messages +generated and optparse in the stdlib uses gettext for no good reason +and might cause us issues. + +Click uses parts of optparse written by Gregory P. Ward and maintained +by the Python Software Foundation. This is limited to code in parser.py. + +Copyright 2001-2006 Gregory P. Ward. All rights reserved. +Copyright 2002-2006 Python Software Foundation. All rights reserved. +""" + +# This code uses parts of optparse written by Gregory P. Ward and +# maintained by the Python Software Foundation. +# Copyright 2001-2006 Gregory P. Ward +# Copyright 2002-2006 Python Software Foundation +from __future__ import annotations + +import collections.abc as cabc +import typing as t +from collections import deque +from gettext import gettext as _ +from gettext import ngettext + +from ._utils import FLAG_NEEDS_VALUE +from ._utils import UNSET +from .exceptions import BadArgumentUsage +from .exceptions import BadOptionUsage +from .exceptions import NoSuchOption +from .exceptions import UsageError + +if t.TYPE_CHECKING: + from ._utils import T_FLAG_NEEDS_VALUE + from ._utils import T_UNSET + from .core import Argument as CoreArgument + from .core import Context + from .core import Option as CoreOption + from .core import Parameter as CoreParameter + +V = t.TypeVar("V") + + +def _unpack_args( + args: cabc.Sequence[str], nargs_spec: cabc.Sequence[int] +) -> tuple[cabc.Sequence[str | cabc.Sequence[str | T_UNSET] | T_UNSET], list[str]]: + """Given an iterable of arguments and an iterable of nargs specifications, + it returns a tuple with all the unpacked arguments at the first index + and all remaining arguments as the second. + + The nargs specification is the number of arguments that should be consumed + or `-1` to indicate that this position should eat up all the remainders. + + Missing items are filled with ``UNSET``. + """ + args = deque(args) + nargs_spec = deque(nargs_spec) + rv: list[str | tuple[str | T_UNSET, ...] | T_UNSET] = [] + spos: int | None = None + + def _fetch(c: deque[str]) -> str | T_UNSET: + try: + if spos is None: + return c.popleft() + else: + return c.pop() + except IndexError: + return UNSET + + while nargs_spec: + if spos is None: + nargs = nargs_spec.popleft() + else: + nargs = nargs_spec.pop() + + if nargs == 1: + rv.append(_fetch(args)) + elif nargs > 1: + x: list[str | T_UNSET] = [_fetch(args) for _ in range(nargs)] + + # If we're reversed, we're pulling in the arguments in reverse, + # so we need to turn them around. + if spos is not None: + x.reverse() + + rv.append(tuple(x)) + elif nargs < 0: + if spos is not None: + raise TypeError("Cannot have two nargs < 0") + + spos = len(rv) + rv.append(UNSET) + + # spos is the position of the wildcard (star). If it's not `None`, + # we fill it with the remainder. + if spos is not None: + rv[spos] = tuple(args) + args = [] + rv[spos + 1 :] = reversed(rv[spos + 1 :]) + + return tuple(rv), list(args) + + +def _split_opt(opt: str) -> tuple[str, str]: + first = opt[:1] + if first.isalnum(): + return "", opt + if opt[1:2] == first: + return opt[:2], opt[2:] + return first, opt[1:] + + +def _normalize_opt(opt: str, ctx: Context | None) -> str: + if ctx is None or ctx.token_normalize_func is None: + return opt + prefix, opt = _split_opt(opt) + return f"{prefix}{ctx.token_normalize_func(opt)}" + + +class _Option: + def __init__( + self, + obj: CoreOption, + opts: cabc.Sequence[str], + dest: str | None, + action: str | None = None, + nargs: int = 1, + const: t.Any | None = None, + ): + self._short_opts = [] + self._long_opts = [] + self.prefixes: set[str] = set() + + for opt in opts: + prefix, value = _split_opt(opt) + if not prefix: + raise ValueError( + _("Invalid start character for option ({option})").format( + option=opt + ) + ) + self.prefixes.add(prefix[0]) + if len(prefix) == 1 and len(value) == 1: + self._short_opts.append(opt) + else: + self._long_opts.append(opt) + self.prefixes.add(prefix) + + if action is None: + action = "store" + + self.dest = dest + self.action = action + self.nargs = nargs + self.const = const + self.obj = obj + + @property + def takes_value(self) -> bool: + return self.action in ("store", "append") + + def process(self, value: t.Any, state: _ParsingState) -> None: + if self.action == "store": + state.opts[self.dest] = value # type: ignore + elif self.action == "store_const": + state.opts[self.dest] = self.const # type: ignore + elif self.action == "append": + state.opts.setdefault(self.dest, []).append(value) # type: ignore + elif self.action == "append_const": + state.opts.setdefault(self.dest, []).append(self.const) # type: ignore + elif self.action == "count": + state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 # type: ignore + else: + raise ValueError(f"unknown action '{self.action}'") + state.order.append(self.obj) + + +class _Argument: + def __init__(self, obj: CoreArgument, dest: str | None, nargs: int = 1): + self.dest = dest + self.nargs = nargs + self.obj = obj + + def process( + self, + value: str | cabc.Sequence[str | T_UNSET] | T_UNSET, + state: _ParsingState, + ) -> None: + if self.nargs > 1: + assert isinstance(value, cabc.Sequence) + holes = sum(x is UNSET for x in value) + if holes == len(value): + value = UNSET + elif holes != 0: + raise BadArgumentUsage( + _("Argument {name!r} takes {nargs} values.").format( + name=self.dest, nargs=self.nargs + ) + ) + + # We failed to collect any argument value so we consider the argument as unset. + if value == (): + value = UNSET + + state.opts[self.dest] = value # type: ignore + state.order.append(self.obj) + + +class _ParsingState: + def __init__(self, rargs: list[str]) -> None: + self.opts: dict[str, t.Any] = {} + self.largs: list[str] = [] + self.rargs = rargs + self.order: list[CoreParameter] = [] + + +class _OptionParser: + """The option parser is an internal class that is ultimately used to + parse options and arguments. It's modelled after optparse and brings + a similar but vastly simplified API. It should generally not be used + directly as the high level Click classes wrap it for you. + + It's not nearly as extensible as optparse or argparse as it does not + implement features that are implemented on a higher level (such as + types or defaults). + + :param ctx: optionally the :class:`~click.Context` where this parser + should go with. + + .. deprecated:: 8.2 + Will be removed in Click 9.0. + """ + + def __init__(self, ctx: Context | None = None) -> None: + #: The :class:`~click.Context` for this parser. This might be + #: `None` for some advanced use cases. + self.ctx = ctx + #: This controls how the parser deals with interspersed arguments. + #: If this is set to `False`, the parser will stop on the first + #: non-option. Click uses this to implement nested subcommands + #: safely. + self.allow_interspersed_args: bool = True + #: This tells the parser how to deal with unknown options. By + #: default it will error out (which is sensible), but there is a + #: second mode where it will ignore it and continue processing + #: after shifting all the unknown options into the resulting args. + self.ignore_unknown_options: bool = False + + if ctx is not None: + self.allow_interspersed_args = ctx.allow_interspersed_args + self.ignore_unknown_options = ctx.ignore_unknown_options + + self._short_opt: dict[str, _Option] = {} + self._long_opt: dict[str, _Option] = {} + self._opt_prefixes = {"-", "--"} + self._args: list[_Argument] = [] + + def add_option( + self, + obj: CoreOption, + opts: cabc.Sequence[str], + dest: str | None, + action: str | None = None, + nargs: int = 1, + const: t.Any | None = None, + ) -> None: + """Adds a new option named `dest` to the parser. The destination + is not inferred (unlike with optparse) and needs to be explicitly + provided. Action can be any of ``store``, ``store_const``, + ``append``, ``append_const`` or ``count``. + + The `obj` can be used to identify the option in the order list + that is returned from the parser. + """ + opts = [_normalize_opt(opt, self.ctx) for opt in opts] + option = _Option(obj, opts, dest, action=action, nargs=nargs, const=const) + self._opt_prefixes.update(option.prefixes) + for opt in option._short_opts: + self._short_opt[opt] = option + for opt in option._long_opts: + self._long_opt[opt] = option + + def add_argument(self, obj: CoreArgument, dest: str | None, nargs: int = 1) -> None: + """Adds a positional argument named `dest` to the parser. + + The `obj` can be used to identify the option in the order list + that is returned from the parser. + """ + self._args.append(_Argument(obj, dest=dest, nargs=nargs)) + + def parse_args( + self, args: list[str] + ) -> tuple[dict[str, t.Any], list[str], list[CoreParameter]]: + """Parses positional arguments and returns ``(values, args, order)`` + for the parsed options and arguments as well as the leftover + arguments if there are any. The order is a list of objects as they + appear on the command line. If arguments appear multiple times they + will be memorized multiple times as well. + """ + state = _ParsingState(args) + try: + self._process_args_for_options(state) + self._process_args_for_args(state) + except UsageError: + if self.ctx is None or not self.ctx.resilient_parsing: + raise + return state.opts, state.largs, state.order + + def _process_args_for_args(self, state: _ParsingState) -> None: + pargs, args = _unpack_args( + state.largs + state.rargs, [x.nargs for x in self._args] + ) + + for idx, arg in enumerate(self._args): + arg.process(pargs[idx], state) + + state.largs = args + state.rargs = [] + + def _process_args_for_options(self, state: _ParsingState) -> None: + while state.rargs: + arg = state.rargs.pop(0) + arglen = len(arg) + # Double dashes always handled explicitly regardless of what + # prefixes are valid. + if arg == "--": + return + elif arg[:1] in self._opt_prefixes and arglen > 1: + self._process_opts(arg, state) + elif self.allow_interspersed_args: + state.largs.append(arg) + else: + state.rargs.insert(0, arg) + return + + # Say this is the original argument list: + # [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)] + # ^ + # (we are about to process arg(i)). + # + # Then rargs is [arg(i), ..., arg(N-1)] and largs is a *subset* of + # [arg0, ..., arg(i-1)] (any options and their arguments will have + # been removed from largs). + # + # The while loop will usually consume 1 or more arguments per pass. + # If it consumes 1 (eg. arg is an option that takes no arguments), + # then after _process_arg() is done the situation is: + # + # largs = subset of [arg0, ..., arg(i)] + # rargs = [arg(i+1), ..., arg(N-1)] + # + # If allow_interspersed_args is false, largs will always be + # *empty* -- still a subset of [arg0, ..., arg(i-1)], but + # not a very interesting subset! + + def _match_long_opt( + self, opt: str, explicit_value: str | None, state: _ParsingState + ) -> None: + if opt not in self._long_opt: + raise NoSuchOption(opt, possibilities=self._long_opt, ctx=self.ctx) + + option = self._long_opt[opt] + if option.takes_value: + # At this point it's safe to modify rargs by injecting the + # explicit value, because no exception is raised in this + # branch. This means that the inserted value will be fully + # consumed. + if explicit_value is not None: + state.rargs.insert(0, explicit_value) + + value = self._get_value_from_state(opt, option, state) + + elif explicit_value is not None: + raise BadOptionUsage( + opt, _("Option {name!r} does not take a value.").format(name=opt) + ) + + else: + value = UNSET + + option.process(value, state) + + def _match_short_opt(self, arg: str, state: _ParsingState) -> None: + stop = False + i = 1 + prefix = arg[0] + unknown_options = [] + + for ch in arg[1:]: + opt = _normalize_opt(f"{prefix}{ch}", self.ctx) + option = self._short_opt.get(opt) + i += 1 + + if not option: + if self.ignore_unknown_options: + unknown_options.append(ch) + continue + raise NoSuchOption(opt, ctx=self.ctx) + if option.takes_value: + # Any characters left in arg? Pretend they're the + # next arg, and stop consuming characters of arg. + if i < len(arg): + state.rargs.insert(0, arg[i:]) + stop = True + + value = self._get_value_from_state(opt, option, state) + + else: + value = UNSET + + option.process(value, state) + + if stop: + break + + # If we got any unknown options we recombine the string of the + # remaining options and re-attach the prefix, then report that + # to the state as new large. This way there is basic combinatorics + # that can be achieved while still ignoring unknown arguments. + if self.ignore_unknown_options and unknown_options: + state.largs.append(f"{prefix}{''.join(unknown_options)}") + + def _get_value_from_state( + self, option_name: str, option: _Option, state: _ParsingState + ) -> str | cabc.Sequence[str] | T_UNSET | T_FLAG_NEEDS_VALUE: + nargs = option.nargs + + value: str | cabc.Sequence[str] | T_UNSET | T_FLAG_NEEDS_VALUE + + if len(state.rargs) < nargs: + if option.obj._flag_needs_value: + # Option allows omitting the value. + value = FLAG_NEEDS_VALUE + else: + raise BadOptionUsage( + option_name, + ngettext( + "Option {name!r} requires an argument.", + "Option {name!r} requires {nargs} arguments.", + nargs, + ).format(name=option_name, nargs=nargs), + ) + elif nargs == 1: + next_rarg = state.rargs[0] + + if ( + option.obj._flag_needs_value + and isinstance(next_rarg, str) + and next_rarg[:1] in self._opt_prefixes + and len(next_rarg) > 1 + ): + # The next arg looks like the start of an option, don't + # use it as the value if omitting the value is allowed. + value = FLAG_NEEDS_VALUE + else: + value = state.rargs.pop(0) + else: + value = tuple(state.rargs[:nargs]) + del state.rargs[:nargs] + + return value + + def _process_opts(self, arg: str, state: _ParsingState) -> None: + explicit_value = None + # Long option handling happens in two parts. The first part is + # supporting explicitly attached values. In any case, we will try + # to long match the option first. + if "=" in arg: + long_opt, explicit_value = arg.split("=", 1) + else: + long_opt = arg + norm_long_opt = _normalize_opt(long_opt, self.ctx) + + # At this point we will match the (assumed) long option through + # the long option matching code. Note that this allows options + # like "-foo" to be matched as long options. + try: + self._match_long_opt(norm_long_opt, explicit_value, state) + except NoSuchOption: + # At this point the long option matching failed, and we need + # to try with short options. However there is a special rule + # which says, that if we have a two character options prefix + # (applies to "--foo" for instance), we do not dispatch to the + # short option code and will instead raise the no option + # error. + if arg[:2] not in self._opt_prefixes: + self._match_short_opt(arg, state) + return + + if not self.ignore_unknown_options: + raise + + state.largs.append(arg) + + +def __getattr__(name: str) -> object: + import warnings + + if name in { + "OptionParser", + "Argument", + "Option", + "split_opt", + "normalize_opt", + "ParsingState", + }: + warnings.warn( + f"'parser.{name}' is deprecated and will be removed in Click 9.0." + " The old parser is available in 'optparse'.", + DeprecationWarning, + stacklevel=2, + ) + return globals()[f"_{name}"] + + if name == "split_arg_string": + from .shell_completion import split_arg_string + + warnings.warn( + "Importing 'parser.split_arg_string' is deprecated, it will only be" + " available in 'shell_completion' in Click 9.0.", + DeprecationWarning, + stacklevel=2, + ) + return split_arg_string + + raise AttributeError(name) diff --git a/source/target-vendors/cp312/click/py.typed b/source/target-vendors/cp312/click/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/click/shell_completion.py b/source/target-vendors/cp312/click/shell_completion.py new file mode 100644 index 0000000..8a2668b --- /dev/null +++ b/source/target-vendors/cp312/click/shell_completion.py @@ -0,0 +1,801 @@ +from __future__ import annotations + +import collections.abc as cabc +import os +import re +import typing as t +from gettext import gettext as _ + +from .core import Argument +from .core import Command +from .core import Context +from .core import Group +from .core import Option +from .core import Parameter +from .core import ParameterSource +from .utils import echo + + +def shell_complete( + cli: Command, + ctx_args: cabc.MutableMapping[str, t.Any], + prog_name: str, + complete_var: str, + instruction: str, +) -> t.Literal[0, 1]: + """Perform shell completion for the given CLI program. + + :param cli: Command being called. + :param ctx_args: Extra arguments to pass to + ``cli.make_context``. + :param prog_name: Name of the executable in the shell. + :param complete_var: Name of the environment variable that holds + the completion instruction. + :param instruction: Value of ``complete_var`` with the completion + instruction and shell, in the form ``instruction_shell``. + :return: Status code to exit with. + """ + shell, _, instruction = instruction.partition("_") + comp_cls = get_completion_class(shell) + + if comp_cls is None: + return 1 + + comp = comp_cls(cli, ctx_args, prog_name, complete_var) + + # Write bytes, otherwise Windows text stdout translates LF to CRLF and breaks. + if instruction == "source": + echo(comp.source().encode(), nl=False) + return 0 + + if instruction == "complete": + echo(comp.complete().encode()) + return 0 + + return 1 + + +if t.TYPE_CHECKING: + from typing_extensions import TypeVar + + # `Any` is used as default for backwards compatibility (instead of e.g. `str`) + _ValueT_co = TypeVar("_ValueT_co", covariant=True, default=t.Any) +else: + _ValueT_co = t.TypeVar("_ValueT_co", covariant=True) + + +class CompletionItem(t.Generic[_ValueT_co]): + """Represents a completion value and metadata about the value. The + default metadata is ``type`` to indicate special shell handling, + and ``help`` if a shell supports showing a help string next to the + value. + + Arbitrary parameters can be passed when creating the object, and + accessed using ``item.attr``. If an attribute wasn't passed, + accessing it returns ``None``. + + :param value: The completion suggestion. + :param type: Tells the shell script to provide special completion + support for the type. Click uses ``"dir"`` and ``"file"``. + :param help: String shown next to the value if supported. + :param kwargs: Arbitrary metadata. The built-in implementations + don't use this, but custom type completions paired with custom + shell support could use it. + """ + + __slots__ = ("value", "type", "help", "_info") + + def __init__( + self, + value: _ValueT_co, + type: str = "plain", + help: str | None = None, + **kwargs: t.Any, + ) -> None: + self.value: _ValueT_co = value + self.type: str = type + self.help: str | None = help + self._info = kwargs + + def __getattr__(self, name: str) -> t.Any: + return self._info.get(name) + + +# Only Bash >= 4.4 has the nosort option. +_SOURCE_BASH = """\ +%(complete_func)s() { + local IFS=$'\\n' + local response + + response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD \ +%(complete_var)s=bash_complete $1) + + for completion in $response; do + IFS=',' read type value <<< "$completion" + + if [[ $type == 'dir' ]]; then + COMPREPLY=() + compopt -o dirnames + elif [[ $type == 'file' ]]; then + COMPREPLY=() + compopt -o default + elif [[ $type == 'plain' ]]; then + COMPREPLY+=($value) + fi + done + + return 0 +} + +%(complete_func)s_setup() { + complete -o nosort -F %(complete_func)s %(prog_name)s +} + +%(complete_func)s_setup; +""" + +# See ZshComplete.format_completion below, and issue #2703, before +# changing this script. +# +# (TL;DR: _describe is picky about the format, but this Zsh script snippet +# is already widely deployed. So freeze this script, and use clever-ish +# handling of colons in ZshComplet.format_completion.) +_SOURCE_ZSH = """\ +#compdef %(prog_name)s + +%(complete_func)s() { + local -a completions + local -a completions_with_descriptions + local -a response + (( ! $+commands[%(prog_name)s] )) && return 1 + + response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) \ +%(complete_var)s=zsh_complete %(prog_name)s)}") + + for type key descr in ${response}; do + if [[ "$type" == "plain" ]]; then + if [[ "$descr" == "_" ]]; then + completions+=("$key") + else + completions_with_descriptions+=("$key":"$descr") + fi + elif [[ "$type" == "dir" ]]; then + _path_files -/ + elif [[ "$type" == "file" ]]; then + _path_files -f + fi + done + + if [ -n "$completions_with_descriptions" ]; then + _describe -V unsorted completions_with_descriptions -U + fi + + if [ -n "$completions" ]; then + compadd -U -V unsorted -a completions + fi +} + +if [[ $zsh_eval_context[-1] == loadautofunc ]]; then + # autoload from fpath, call function directly + %(complete_func)s "$@" +else + # eval/source/. command, register function for later + compdef %(complete_func)s %(prog_name)s +fi +""" + +_SOURCE_FISH = """\ +function %(complete_func)s; + set -l response (env %(complete_var)s=fish_complete COMP_WORDS=(commandline -cp) \ +COMP_CWORD=(commandline -t) %(prog_name)s); + + for completion in $response; + set -l metadata (string split "," $completion); + + if test $metadata[1] = "dir"; + __fish_complete_directories $metadata[2]; + else if test $metadata[1] = "file"; + __fish_complete_path $metadata[2]; + else if test $metadata[1] = "plain"; + echo $metadata[2]; + end; + end; +end; + +complete --no-files --command %(prog_name)s --arguments \ +"(%(complete_func)s)"; +""" + +# Compatible with Windows PowerShell 5.1+ and PowerShell (pwsh) 7+. +# Uses Register-ArgumentCompleter -Native, which receives the command AST +# as parsed by PowerShell. The command text is forwarded verbatim through +# COMP_WORDS so the Python side can reuse the same shlex-based splitting +# used by the bash/zsh completers. +_SOURCE_POWERSHELL = """\ +Register-ArgumentCompleter -Native -CommandName %(prog_name)s -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) + + $prev_complete = $env:%(complete_var)s + $prev_words = $env:COMP_WORDS + $prev_cword = $env:COMP_CWORD + + $env:%(complete_var)s = 'powershell_complete' + $env:COMP_WORDS = $commandAst.ToString() + if ($wordToComplete) { + $env:COMP_CWORD = $commandAst.CommandElements.Count - 1 + } else { + $env:COMP_CWORD = $commandAst.CommandElements.Count + } + + try { + $response = & %(prog_name)s 2>$null + } finally { + $env:%(complete_var)s = $prev_complete + $env:COMP_WORDS = $prev_words + $env:COMP_CWORD = $prev_cword + } + + if (-not $response) { return } + + $prefix = "$wordToComplete*" + $lines = $response -split "`n" + for ($i = 0; $i + 2 -lt $lines.Count; $i += 3) { + $type = $lines[$i] + $value = $lines[$i + 1] + $descr = $lines[$i + 2] + if (-not $type) { continue } + + if ($type -eq 'plain') { + $tip = if ($descr -and $descr -ne '_') { $descr } else { $value } + [System.Management.Automation.CompletionResult]::new( + $value, $value, 'ParameterValue', $tip) + } elseif ($type -eq 'dir') { + Get-ChildItem -Directory -Path $prefix -ErrorAction SilentlyContinue | + ForEach-Object { + [System.Management.Automation.CompletionResult]::new( + $_.FullName, $_.Name, 'ProviderContainer', $_.FullName) + } + } elseif ($type -eq 'file') { + Get-ChildItem -Path $prefix -ErrorAction SilentlyContinue | + ForEach-Object { + $kind = 'ProviderItem' + if ($_.PSIsContainer) { $kind = 'ProviderContainer' } + [System.Management.Automation.CompletionResult]::new( + $_.FullName, $_.Name, $kind, $_.FullName) + } + } + } +} +""" + + +class _SourceVarsDict(t.TypedDict): + complete_func: str + complete_var: str + prog_name: str + + +class ShellComplete: + """Base class for providing shell completion support. A subclass for + a given shell will override attributes and methods to implement the + completion instructions (``source`` and ``complete``). + + :param cli: Command being called. + :param prog_name: Name of the executable in the shell. + :param complete_var: Name of the environment variable that holds + the completion instruction. + + .. versionadded:: 8.0 + """ + + name: t.ClassVar[str] + """Name to register the shell as with :func:`add_completion_class`. + This is used in completion instructions (``{name}_source`` and + ``{name}_complete``). + """ + + source_template: t.ClassVar[str] + """Completion script template formatted by :meth:`source`. This must + be provided by subclasses. + """ + + cli: Command + ctx_args: cabc.MutableMapping[str, t.Any] + prog_name: str + complete_var: str + + def __init__( + self, + cli: Command, + ctx_args: cabc.MutableMapping[str, t.Any], + prog_name: str, + complete_var: str, + ) -> None: + self.cli = cli + self.ctx_args = ctx_args + self.prog_name = prog_name + self.complete_var = complete_var + + @property + def func_name(self) -> str: + """The name of the shell function defined by the completion + script. + """ + safe_name = re.sub(r"\W*", "", self.prog_name.replace("-", "_"), flags=re.ASCII) + return f"_{safe_name}_completion" + + def source_vars(self) -> _SourceVarsDict: + """Vars for formatting :attr:`source_template`. + + By default this provides ``complete_func``, ``complete_var``, + and ``prog_name``. + """ + return { + "complete_func": self.func_name, + "complete_var": self.complete_var, + "prog_name": self.prog_name, + } + + def source(self) -> str: + """Produce the shell script that defines the completion + function. By default this ``%``-style formats + :attr:`source_template` with the dict returned by + :meth:`source_vars`. + """ + return self.source_template % self.source_vars() + + def get_completion_args(self) -> tuple[list[str], str]: + """Use the env vars defined by the shell script to return a + tuple of ``args, incomplete``. This must be implemented by + subclasses. + """ + raise NotImplementedError + + def get_completions( + self, args: list[str], incomplete: str + ) -> list[CompletionItem[str]]: + """Determine the context and last complete command or parameter + from the complete args. Call that object's ``shell_complete`` + method to get the completions for the incomplete value. + + :param args: List of complete args before the incomplete value. + :param incomplete: Value being completed. May be empty. + """ + ctx = _resolve_context(self.cli, self.ctx_args, self.prog_name, args) + obj, incomplete = _resolve_incomplete(ctx, args, incomplete) + return obj.shell_complete(ctx, incomplete) + + def format_completion(self, item: CompletionItem[str]) -> str: + """Format a completion item into the form recognized by the + shell script. This must be implemented by subclasses. + + :param item: Completion item to format. + """ + raise NotImplementedError + + def complete(self) -> str: + """Produce the completion data to send back to the shell. + + By default this calls :meth:`get_completion_args`, gets the + completions, then calls :meth:`format_completion` for each + completion. + """ + args, incomplete = self.get_completion_args() + completions = self.get_completions(args, incomplete) + out = [self.format_completion(item) for item in completions] + return "\n".join(out) + + +class BashComplete(ShellComplete): + """Shell completion for Bash.""" + + name: t.ClassVar[str] = "bash" + source_template: t.ClassVar[str] = _SOURCE_BASH + + @staticmethod + def _check_version() -> None: + import shutil + import subprocess + + bash_exe = shutil.which("bash") + + if bash_exe is None: + match = None + else: + output = subprocess.run( + [bash_exe, "--norc", "-c", 'echo "${BASH_VERSION}"'], + stdout=subprocess.PIPE, + ) + match = re.search(r"^(\d+)\.(\d+)\.\d+", output.stdout.decode()) + + if match is not None: + major, minor = match.groups() + + if major < "4" or major == "4" and minor < "4": + echo( + _( + "Shell completion is not supported for Bash" + " versions older than 4.4." + ), + err=True, + ) + else: + echo( + _("Couldn't detect Bash version, shell completion is not supported."), + err=True, + ) + + def source(self) -> str: + self._check_version() + return super().source() + + def get_completion_args(self) -> tuple[list[str], str]: + cwords = split_arg_string(os.environ["COMP_WORDS"]) + cword = int(os.environ["COMP_CWORD"]) + args = cwords[1:cword] + + try: + incomplete = cwords[cword] + except IndexError: + incomplete = "" + + return args, incomplete + + def format_completion(self, item: CompletionItem[t.Any]) -> str: + return f"{item.type},{item.value}" + + +class ZshComplete(ShellComplete): + """Shell completion for Zsh.""" + + name: t.ClassVar[str] = "zsh" + source_template: t.ClassVar[str] = _SOURCE_ZSH + + def get_completion_args(self) -> tuple[list[str], str]: + cwords = split_arg_string(os.environ["COMP_WORDS"]) + cword = int(os.environ["COMP_CWORD"]) + args = cwords[1:cword] + + try: + incomplete = cwords[cword] + except IndexError: + incomplete = "" + + return args, incomplete + + def format_completion(self, item: CompletionItem[str]) -> str: + help_ = item.help or "_" + # The zsh completion script uses `_describe` on items with help + # texts (which splits the item help from the item value at the + # first unescaped colon) and `compadd` on items without help + # text (which uses the item value as-is and does not support + # colon escaping). So escape colons in the item value if and + # only if the item help is not the sentinel "_" value, as used + # by the completion script. + # + # (The zsh completion script is potentially widely deployed, and + # thus harder to fix than this method.) + # + # See issue #1812 and issue #2703 for further context. + value = item.value.replace(":", r"\:") if help_ != "_" else item.value + return f"{item.type}\n{value}\n{help_}" + + +class FishComplete(ShellComplete): + """Shell completion for Fish.""" + + name: t.ClassVar[str] = "fish" + source_template: t.ClassVar[str] = _SOURCE_FISH + + def get_completion_args(self) -> tuple[list[str], str]: + cwords = split_arg_string(os.environ["COMP_WORDS"]) + incomplete = os.environ["COMP_CWORD"] + if incomplete: + incomplete = split_arg_string(incomplete)[0] + args = cwords[1:] + + # Fish stores the partial word in both COMP_WORDS and + # COMP_CWORD, remove it from complete args. + if incomplete and args and args[-1] == incomplete: + args.pop() + + return args, incomplete + + def format_completion(self, item: CompletionItem[str]) -> str: + """ + .. versionchanged:: 8.4.2 + Escape newlines and replace tabs with spaces in the help text to + fix completion errors with multi-line help strings. + """ + # According to https://fishshell.com/docs/current/cmds/complete.html + # Command substitutions found in ARGUMENTS should return a newline- + # separated list of arguments, and each argument may optionally have a tab + # character followed by the argument description. + if item.help: + help_ = item.help.replace("\n", "\\n").replace("\t", " ") + return f"{item.type},{item.value}\t{help_}" + + return f"{item.type},{item.value}" + + +class PowerShellComplete(ShellComplete): + """Shell completion for PowerShell (Windows PowerShell 5.1+ and pwsh 7+). + + .. versionadded:: 8.5.0 + """ + + name: t.ClassVar[str] = "powershell" + source_template: t.ClassVar[str] = _SOURCE_POWERSHELL + + def get_completion_args(self) -> tuple[list[str], str]: + cwords = split_arg_string(os.environ["COMP_WORDS"]) + cword = int(os.environ["COMP_CWORD"]) + args = cwords[1:cword] + + try: + incomplete = cwords[cword] + except IndexError: + incomplete = "" + + return args, incomplete + + def format_completion(self, item: CompletionItem[str]) -> str: + # PowerShell parses the response by splitting on newlines and + # consuming three lines per completion (type, value, help). + # Multi-line help text would break that framing, so collapse + # newlines and carriage returns to spaces. The literal "_" + # sentinel matches what ZshComplete uses for "no help text". + if item.help: + help_ = item.help.replace("\r", " ").replace("\n", " ") + else: + help_ = "_" + return f"{item.type}\n{item.value}\n{help_}" + + +_available_shells: t.Final[dict[str, type[ShellComplete]]] = { + "bash": BashComplete, + "fish": FishComplete, + "powershell": PowerShellComplete, + "zsh": ZshComplete, +} + +_ShellCompleteT = t.TypeVar("_ShellCompleteT", bound="ShellComplete") + + +def add_completion_class( + cls: type[_ShellCompleteT], name: str | None = None +) -> type[_ShellCompleteT]: + """Register a :class:`ShellComplete` subclass under the given name. + The name will be provided by the completion instruction environment + variable during completion. + + :param cls: The completion class that will handle completion for the + shell. + :param name: Name to register the class under. Defaults to the + class's ``name`` attribute. + """ + if name is None: + name = cls.name + + _available_shells[name] = cls + + return cls + + +@t.overload +def get_completion_class(shell: t.Literal["bash"]) -> type[BashComplete]: ... +@t.overload +def get_completion_class(shell: t.Literal["fish"]) -> type[FishComplete]: ... +@t.overload +def get_completion_class(shell: t.Literal["zsh"]) -> type[ZshComplete]: ... +@t.overload +def get_completion_class(shell: str) -> type[ShellComplete] | None: ... +def get_completion_class(shell: str) -> type[ShellComplete] | None: + """Look up a registered :class:`ShellComplete` subclass by the name + provided by the completion instruction environment variable. If the + name isn't registered, returns ``None``. + + :param shell: Name the class is registered under. + """ + return _available_shells.get(shell) + + +def split_arg_string(string: str) -> list[str]: + """Split an argument string as with :func:`shlex.split`, but don't + fail if the string is incomplete. Ignores a missing closing quote or + incomplete escape sequence and uses the partial token as-is. + + .. code-block:: python + + split_arg_string("example 'my file") + ["example", "my file"] + + split_arg_string("example my\\") + ["example", "my"] + + :param string: String to split. + + .. versionchanged:: 8.2 + Moved to ``shell_completion`` from ``parser``. + """ + import shlex + + lex = shlex.shlex(string, posix=True) + lex.whitespace_split = True + lex.commenters = "" + out: list[str] = [] + + try: + out.extend(lex) + except ValueError: + # Raised when end-of-string is reached in an invalid state. Use + # the partial token as-is. The quote or escape character is in + # lex.state, not lex.token. + out.append(lex.token) + + return out + + +def _is_incomplete_argument(ctx: Context, param: Parameter) -> bool: + """Determine if the given parameter is an argument that can still + accept values. + + :param ctx: Invocation context for the command represented by the + parsed complete args. + :param param: Argument object being checked. + """ + if not isinstance(param, Argument): + return False + + value = ctx.params.get(param.name) + return ( + param.nargs == -1 + or ctx.get_parameter_source(param.name) is not ParameterSource.COMMANDLINE + or ( + param.nargs > 1 + and isinstance(value, (tuple, list)) + and len(value) < param.nargs + ) + ) + + +def _start_of_option(ctx: Context, value: str) -> bool: + """Check if the value looks like the start of an option.""" + if not value: + return False + + c = value[0] + return c in ctx._opt_prefixes + + +def _is_incomplete_option(ctx: Context, args: list[str], param: Parameter) -> bool: + """Determine if the given parameter is an option that needs a value. + + :param args: List of complete args before the incomplete value. + :param param: Option object being checked. + """ + if not isinstance(param, Option): + return False + + if param.is_flag or param.count: + return False + + last_option = None + + for index, arg in enumerate(reversed(args)): + if index + 1 > param.nargs: + break + + if _start_of_option(ctx, arg): + last_option = arg + break + + return last_option is not None and last_option in param.opts + + +def _resolve_context( + cli: Command, + ctx_args: cabc.MutableMapping[str, t.Any], + prog_name: str, + args: list[str], +) -> Context: + """Produce the context hierarchy starting with the command and + traversing the complete arguments. This only follows the commands, + it doesn't trigger input prompts or callbacks. + + :param cli: Command being called. + :param prog_name: Name of the executable in the shell. + :param args: List of complete args before the incomplete value. + """ + ctx_args["resilient_parsing"] = True + with cli.make_context(prog_name, args.copy(), **ctx_args) as ctx: + args = ctx._protected_args + ctx.args + + while args: + command = ctx.command + + if isinstance(command, Group): + if not command.chain: + name, cmd, args = command.resolve_command(ctx, args) + + if cmd is None: + return ctx + + with cmd.make_context( + name, args, parent=ctx, resilient_parsing=True + ) as sub_ctx: + ctx = sub_ctx + args = ctx._protected_args + ctx.args + else: + sub_ctx = ctx + + while args: + name, cmd, args = command.resolve_command(ctx, args) + + if cmd is None: + return ctx + + with cmd.make_context( + name, + args, + parent=ctx, + allow_extra_args=True, + allow_interspersed_args=False, + resilient_parsing=True, + ) as sub_sub_ctx: + sub_ctx = sub_sub_ctx + args = sub_ctx.args + + ctx = sub_ctx + args = [*sub_ctx._protected_args, *sub_ctx.args] + else: + break + + return ctx + + +def _resolve_incomplete( + ctx: Context, args: list[str], incomplete: str +) -> tuple[Command | Parameter, str]: + """Find the Click object that will handle the completion of the + incomplete value. Return the object and the incomplete value. + + :param ctx: Invocation context for the command represented by + the parsed complete args. + :param args: List of complete args before the incomplete value. + :param incomplete: Value being completed. May be empty. + """ + # Different shells treat an "=" between a long option name and + # value differently. Might keep the value joined, return the "=" + # as a separate item, or return the split name and value. Always + # split and discard the "=" to make completion easier. + if incomplete == "=": + incomplete = "" + elif "=" in incomplete and _start_of_option(ctx, incomplete): + name, _, incomplete = incomplete.partition("=") + args.append(name) + + # The "--" marker tells Click to stop treating values as options + # even if they start with the option character. If it hasn't been + # given and the incomplete arg looks like an option, the current + # command will provide option name completions. + if "--" not in args and _start_of_option(ctx, incomplete): + return ctx.command, incomplete + + params = ctx.command.get_params(ctx) + + # If the last complete arg is an option name with an incomplete + # value, the option will provide value completions. + for param in params: + if _is_incomplete_option(ctx, args, param): + return param, incomplete + + # It's not an option name or value. The first argument without a + # parsed value will provide value completions. + for param in params: + if _is_incomplete_argument(ctx, param): + return param, incomplete + + # There were no unparsed arguments, the command may be a group that + # will provide command name completions. + return ctx.command, incomplete diff --git a/source/target-vendors/cp312/click/termui.py b/source/target-vendors/cp312/click/termui.py new file mode 100644 index 0000000..e1315f2 --- /dev/null +++ b/source/target-vendors/cp312/click/termui.py @@ -0,0 +1,1014 @@ +from __future__ import annotations + +import collections.abc as cabc +import inspect +import io +import itertools +import os +import re +import sys +import typing as t +from contextlib import AbstractContextManager +from contextlib import redirect_stdout +from gettext import gettext as _ + +from . import _compat +from ._compat import isatty +from ._compat import strip_ansi +from .exceptions import Abort +from .exceptions import UsageError +from .globals import resolve_color_default +from .types import Choice +from .types import convert_type +from .types import ParamType +from .utils import _LazyFile +from .utils import echo + +if t.TYPE_CHECKING: + from ._termui_impl import ProgressBar + +V = t.TypeVar("V") + +# The prompt functions to use. The doc tools currently override these +# functions to customize how they work. +visible_prompt_func: t.Callable[[str], str] = input + +_ansi_colors = { + "black": 30, + "red": 31, + "green": 32, + "yellow": 33, + "blue": 34, + "magenta": 35, + "cyan": 36, + "white": 37, + "reset": 39, + "bright_black": 90, + "bright_red": 91, + "bright_green": 92, + "bright_yellow": 93, + "bright_blue": 94, + "bright_magenta": 95, + "bright_cyan": 96, + "bright_white": 97, +} +_ansi_reset_all = "\033[0m" + + +_HIDDEN_INPUT_MASK = "'***'" + + +def _mask_hidden_input(message: str, value: str) -> str: + """Replace occurrences of ``value`` in ``message`` with a fixed mask. + + Both ``repr(value)`` (the form built-in :class:`ParamType` errors use + via ``{value!r}``) and the raw value are masked. The raw-value pass + uses word-boundary lookarounds so a substring like ``"1"`` does not + match inside ``"10"``, and ``"ent"`` does not match inside + ``"Authentication"``. The empty string is skipped to avoid matching + at every boundary. + """ + message = message.replace(repr(value), _HIDDEN_INPUT_MASK) + if value: + message = re.sub( + rf"(? str: + import getpass + + return getpass.getpass(prompt) + + +def _readline_prompt(func: t.Callable[[str], str], text: str, err: bool) -> str: + """Call a prompt function, passing the full prompt so readline can + handle line editing and cursor positioning correctly. + + The prompt is handed to *func* (such as :func:`input`) rather than + written through :func:`echo`, so it has to strip ANSI color and style + codes itself when the destination stream does not support them. Without + this the prompt would keep codes that :func:`echo` removes from the + rest of the output. + """ + stream = sys.stderr if err else sys.stdout + + # Look up ``should_strip_ansi`` on the module so that ``CliRunner``, + # which patches it there during test isolation, is honored. + if _compat.should_strip_ansi(stream, resolve_color_default()): + text = strip_ansi(text) + + if err: + with redirect_stdout(sys.stderr): + return func(text) + return func(text) + + +def _build_prompt( + text: str, + suffix: str, + show_default: bool | str = False, + default: object | None = None, + show_choices: bool = True, + type: object | None = None, +) -> str: + prompt = text + if type is not None and show_choices and isinstance(type, Choice): + prompt += f" ({', '.join(map(str, type.choices))})" + default_preview = "" + if show_default: + if isinstance(show_default, str): + default_preview = f" [({show_default})]" + elif default is not None: + default_preview = f" [{_format_default(default)}]" + return f"{prompt}{default_preview}{suffix}" + + +def _format_default(default: V) -> V | str: + if isinstance(default, (io.IOBase, _LazyFile)): + name = getattr(default, "name", None) + + if name is not None: + return str(name) + + return default + + +@t.overload +def prompt( + text: str, + default: str | None = None, + hide_input: bool = False, + confirmation_prompt: bool | str = False, + type: None = None, + value_proc: None = None, + prompt_suffix: str = ": ", + show_default: bool | str = True, + err: bool = False, + show_choices: bool = True, +) -> str: ... + + +@t.overload +def prompt( + text: str, + default: V | str | None = None, + hide_input: bool = False, + confirmation_prompt: bool | str = False, + type: ParamType[V, str] | type[V] | None = None, + value_proc: t.Callable[[str], V] | None = None, + prompt_suffix: str = ": ", + show_default: bool | str = True, + err: bool = False, + show_choices: bool = True, +) -> V: ... + + +def prompt( + text: str, + default: V | str | None = None, + hide_input: bool = False, + confirmation_prompt: bool | str = False, + type: ParamType[V, str] | type[V] | None = None, + value_proc: t.Callable[[str], V] | None = None, + prompt_suffix: str = ": ", + show_default: bool | str = True, + err: bool = False, + show_choices: bool = True, +) -> V: + """Prompts a user for input. This is a convenience function that can + be used to prompt a user for input later. + + If the user aborts the input by sending an interrupt signal, this + function will catch it and raise a :exc:`Abort` exception. + + :param text: the text to show for the prompt. + :param default: the default value to use if no input happens. If this + is not given it will prompt until it's aborted. + :param hide_input: if this is set to true then the input value will + be hidden. + :param confirmation_prompt: Prompt a second time to confirm the + value. Can be set to a string instead of ``True`` to customize + the message. + :param type: the type to use to check the value against. + :param value_proc: if this parameter is provided it's a function that + is invoked instead of the type conversion to + convert a value. + :param prompt_suffix: a suffix that should be added to the prompt. + :param show_default: shows or hides the default value in the prompt. + If this value is a string, it shows that string + in parentheses instead of the actual value. + :param err: if set to true the file defaults to ``stderr`` instead of + ``stdout``, the same as with echo. + :param show_choices: Show or hide choices if the passed type is a Choice. + For example if type is a Choice of either day or week, + show_choices is true and text is "Group by" then the + prompt will be "Group by (day, week): ". + + .. versionchanged:: 8.5.0 + Generically typed: the return type is narrowed by ``type``, + ``value_proc``, or ``default`` instead of being ``Any``. Runtime + behavior is unchanged. + + .. versionchanged:: 8.3.3 + ``show_default`` can be a string to show a custom value instead + of the actual default, matching the help text behavior. + + .. versionchanged:: 8.3.1 + A space is no longer appended to the prompt. + + .. versionadded:: 8.0 + ``confirmation_prompt`` can be a custom string. + + .. versionadded:: 7.0 + Added the ``show_choices`` parameter. + + .. versionadded:: 6.0 + Added unicode support for cmd.exe on Windows. + + .. versionadded:: 4.0 + Added the `err` parameter. + + """ + + def prompt_func(text: str) -> str: + f = hidden_prompt_func if hide_input else visible_prompt_func + try: + return _readline_prompt(f, text, err) + except (KeyboardInterrupt, EOFError): + # getpass doesn't print a newline if the user aborts input with ^C. + # Allegedly this behavior is inherited from getpass(3). + # A doc bug has been filed at https://bugs.python.org/issue24711 + if hide_input: + echo(None, err=err) + raise Abort() from None + + if value_proc is None: + value_proc = convert_type(type, default) + + prompt = _build_prompt( + text, prompt_suffix, show_default, default, show_choices, type + ) + + if confirmation_prompt: + if confirmation_prompt is True: + confirmation_prompt = _("Repeat for confirmation") + + confirmation_prompt = _build_prompt(confirmation_prompt, prompt_suffix) + + while True: + while True: + value = prompt_func(prompt) + if value: + break + elif default is not None: + # Defaults of any type are accepted and round trip through + # value_proc like typed input, so the annotation is only + # accurate for typed input. + value = t.cast("str", default) + break + try: + result = value_proc(value) + except UsageError as e: + message = _mask_hidden_input(e.message, value) if hide_input else e.message + echo(_("Error: {message}").format(message=message), err=err) + continue + if not confirmation_prompt: + return result + while True: + value2 = prompt_func(confirmation_prompt) + is_empty = not value and not value2 + if value2 or is_empty: + break + if value == value2: + return result + echo(_("Error: The two entered values do not match."), err=err) + + +def confirm( + text: str, + default: bool | None = False, + abort: bool = False, + prompt_suffix: str = ": ", + show_default: bool = True, + err: bool = False, +) -> bool: + """Prompts for confirmation (yes/no question). + + If the user aborts the input by sending a interrupt signal this + function will catch it and raise a :exc:`Abort` exception. + + :param text: the question to ask. + :param default: The default value to use when no input is given. If + ``None``, repeat until input is given. + :param abort: if this is set to `True` a negative answer aborts the + exception by raising :exc:`Abort`. + :param prompt_suffix: a suffix that should be added to the prompt. + :param show_default: shows or hides the default value in the prompt. + :param err: if set to true the file defaults to ``stderr`` instead of + ``stdout``, the same as with echo. + + .. versionchanged:: 8.3.1 + A space is no longer appended to the prompt. + + .. versionchanged:: 8.0 + Repeat until input is given if ``default`` is ``None``. + + .. versionadded:: 4.0 + Added the ``err`` parameter. + """ + prompt = _build_prompt( + text, + prompt_suffix, + show_default, + "y/n" if default is None else ("Y/n" if default else "y/N"), + ) + + while True: + try: + value = _readline_prompt(visible_prompt_func, prompt, err).lower().strip() + except (KeyboardInterrupt, EOFError): + raise Abort() from None + if value in ("y", "yes"): + rv = True + elif value in ("n", "no"): + rv = False + elif default is not None and value == "": + rv = default + else: + echo(_("Error: invalid input"), err=err) + continue + break + if abort and not rv: + raise Abort() + return rv + + +def get_pager_file( + color: bool | None = None, +) -> t.ContextManager[t.TextIO]: + """Context manager. + + Yields a writable file-like object which can be used as an output pager. + + .. versionadded:: 8.4.0 + + :param color: controls if the pager supports ANSI colors or not. The + default is autodetection. + """ + from ._termui_impl import get_pager_file + + color = resolve_color_default(color) + + return get_pager_file(color=color) + + +def echo_via_pager( + text_or_generator: cabc.Iterable[str] | t.Callable[[], cabc.Iterable[str]] | str, + color: bool | None = None, +) -> None: + """This function takes a text and shows it via an environment specific + pager on stdout. + + .. versionchanged:: 3.0 + Added the `color` flag. + + :param text_or_generator: the text to page, or alternatively, a + generator emitting the text to page. + :param color: controls if the pager supports ANSI colors or not. The + default is autodetection. + """ + + if inspect.isgeneratorfunction(text_or_generator): + i = t.cast("t.Callable[[], cabc.Iterable[str]]", text_or_generator)() + elif isinstance(text_or_generator, str): + i = [text_or_generator] + else: + i = iter(t.cast("cabc.Iterable[str]", text_or_generator)) + + # convert every element of i to a text type if necessary + text_generator = (el if isinstance(el, str) else str(el) for el in i) + + with get_pager_file(color=color) as pager: + for text in itertools.chain(text_generator, "\n"): + pager.write(text) + # Flush after each write so a slow generator streams to the pager + # incrementally rather than staying invisible until the pipe buffer + # fills (~8 KB). + pager.flush() + + +@t.overload +def progressbar( + *, + length: int, + label: str | None = None, + hidden: bool = False, + show_eta: bool = True, + show_percent: bool | None = None, + show_pos: bool = False, + fill_char: str = "#", + empty_char: str = "-", + bar_template: str = "%(label)s [%(bar)s] %(info)s", + info_sep: str = " ", + width: int = 36, + file: t.TextIO | None = None, + color: bool | None = None, + update_min_steps: int = 1, +) -> ProgressBar[int]: ... + + +@t.overload +def progressbar( + iterable: cabc.Iterable[V] | None = None, + length: int | None = None, + label: str | None = None, + hidden: bool = False, + show_eta: bool = True, + show_percent: bool | None = None, + show_pos: bool = False, + item_show_func: t.Callable[[V | None], str | None] | None = None, + fill_char: str = "#", + empty_char: str = "-", + bar_template: str = "%(label)s [%(bar)s] %(info)s", + info_sep: str = " ", + width: int = 36, + file: t.TextIO | None = None, + color: bool | None = None, + update_min_steps: int = 1, +) -> ProgressBar[V]: ... + + +def progressbar( + iterable: cabc.Iterable[V] | None = None, + length: int | None = None, + label: str | None = None, + hidden: bool = False, + show_eta: bool = True, + show_percent: bool | None = None, + show_pos: bool = False, + item_show_func: t.Callable[[V | None], str | None] | None = None, + fill_char: str = "#", + empty_char: str = "-", + bar_template: str = "%(label)s [%(bar)s] %(info)s", + info_sep: str = " ", + width: int = 36, + file: t.TextIO | None = None, + color: bool | None = None, + update_min_steps: int = 1, +) -> ProgressBar[V]: + """This function creates an iterable context manager that can be used + to iterate over something while showing a progress bar. It will + either iterate over the `iterable` or `length` items (that are counted + up). While iteration happens, this function will print a rendered + progress bar to the given `file` (defaults to stdout) and will attempt + to calculate remaining time and more. By default, this progress bar + will not be rendered if the file is not a terminal. + + The context manager creates the progress bar. When the context + manager is entered the progress bar is already created. With every + iteration over the progress bar, the iterable passed to the bar is + advanced and the bar is updated. When the context manager exits, + a newline is printed and the progress bar is finalized on screen. + + Note: The progress bar is currently designed for use cases where the + total progress can be expected to take at least several seconds. + Because of this, the ProgressBar class object won't display + progress that is considered too fast, and progress where the time + between steps is less than a second. + + No printing must happen or the progress bar will be unintentionally + destroyed. + + Example usage:: + + with progressbar(items) as bar: + for item in bar: + do_something_with(item) + + Alternatively, if no iterable is specified, one can manually update the + progress bar through the `update()` method instead of directly + iterating over the progress bar. The update method accepts the number + of steps to increment the bar with:: + + with progressbar(length=chunks.total_bytes) as bar: + for chunk in chunks: + process_chunk(chunk) + bar.update(chunks.bytes) + + The ``update()`` method also takes an optional value specifying the + ``current_item`` at the new position. This is useful when used + together with ``item_show_func`` to customize the output for each + manual step:: + + with click.progressbar( + length=total_size, + label='Unzipping archive', + item_show_func=lambda a: a.filename + ) as bar: + for archive in zip_file: + archive.extract() + bar.update(archive.size, archive) + + :param iterable: an iterable to iterate over. If not provided the length + is required. + :param length: the number of items to iterate over. By default the + progressbar will attempt to ask the iterator about its + length, which might or might not work. If an iterable is + also provided this parameter can be used to override the + length. If an iterable is not provided the progress bar + will iterate over a range of that length. + :param label: the label to show next to the progress bar. + :param hidden: hide the progressbar. Defaults to ``False``. When no tty is + detected, it will only print the progressbar label. Setting this to + ``False`` also disables that. + :param show_eta: enables or disables the estimated time display. This is + automatically disabled if the length cannot be + determined. + :param show_percent: enables or disables the percentage display. The + default is `True` if the iterable has a length or + `False` if not. + :param show_pos: enables or disables the absolute position display. The + default is `False`. + :param item_show_func: A function called with the current item which + can return a string to show next to the progress bar. If the + function returns ``None`` nothing is shown. The current item can + be ``None``, such as when entering and exiting the bar. + :param fill_char: the character to use to show the filled part of the + progress bar. + :param empty_char: the character to use to show the non-filled part of + the progress bar. + :param bar_template: the format string to use as template for the bar. + The parameters in it are ``label`` for the label, + ``bar`` for the progress bar and ``info`` for the + info section. + :param info_sep: the separator between multiple info items (eta etc.) + :param width: the width of the progress bar in characters, 0 means full + terminal width + :param file: The file to write to. If this is not a terminal then + only the label is printed. + :param color: controls if the terminal supports ANSI colors or not. The + default is autodetection. This is only needed if ANSI + codes are included anywhere in the progress bar output + which is not the case by default. + :param update_min_steps: Render only when this many updates have + completed. This allows tuning for very fast iterators. + + .. versionadded:: 8.2 + The ``hidden`` argument. + + .. versionchanged:: 8.0 + Output is shown even if execution time is less than 0.5 seconds. + + .. versionchanged:: 8.0 + ``item_show_func`` shows the current item, not the previous one. + + .. versionchanged:: 8.0 + Labels are echoed if the output is not a TTY. Reverts a change + in 7.0 that removed all output. + + .. versionadded:: 8.0 + The ``update_min_steps`` parameter. + + .. versionadded:: 4.0 + The ``color`` parameter and ``update`` method. + + .. versionadded:: 2.0 + """ + from ._termui_impl import ProgressBar + + color = resolve_color_default(color) + return ProgressBar( + iterable=iterable, + length=length, + hidden=hidden, + show_eta=show_eta, + show_percent=show_percent, + show_pos=show_pos, + item_show_func=item_show_func, + fill_char=fill_char, + empty_char=empty_char, + bar_template=bar_template, + info_sep=info_sep, + file=file, + label=label, + width=width, + color=color, + update_min_steps=update_min_steps, + ) + + +def clear() -> None: + """Clears the terminal screen. This will have the effect of clearing + the whole visible space of the terminal and moving the cursor to the + top left. This does not do anything if not connected to a terminal. + + .. versionadded:: 2.0 + """ + if not isatty(sys.stdout): + return + + # ANSI escape \033[2J clears the screen, \033[1;1H moves the cursor + echo("\033[2J\033[1;1H", nl=False) + + +def _interpret_color(color: int | tuple[int, int, int] | str, offset: int = 0) -> str: + """Interprets a color value and returns the corresponding ANSI code.""" + if isinstance(color, str) and color in _ansi_colors: + return str(_ansi_colors[color] + offset) + + # bool is an int subclass: without the exclusion, True and False would + # silently render as the palette indices 1 and 0. + elif isinstance(color, int) and not isinstance(color, bool): + if 0 <= color <= 255: + return f"{38 + offset};5;{color:d}" + + elif ( + isinstance(color, (tuple, list)) + and len(color) == 3 + and all( + isinstance(c, int) and not isinstance(c, bool) and 0 <= c <= 255 + for c in color + ) + ): + r, g, b = color + return f"{38 + offset};2;{r:d};{g:d};{b:d}" + + raise ValueError(_("Unknown color {colour!r}").format(colour=color)) + + +def style( + text: t.Any, + fg: int | tuple[int, int, int] | str | None = None, + bg: int | tuple[int, int, int] | str | None = None, + bold: bool | None = None, + dim: bool | None = None, + underline: bool | None = None, + overline: bool | None = None, + italic: bool | None = None, + blink: bool | None = None, + reverse: bool | None = None, + strikethrough: bool | None = None, + reset: bool = True, +) -> str: + """Styles a text with ANSI styles and returns the new string. By + default the styling is self contained which means that at the end + of the string a reset code is issued. This can be prevented by + passing ``reset=False``. + + Examples:: + + click.echo(click.style('Hello World!', fg='green')) + click.echo(click.style('ATTENTION!', blink=True)) + click.echo(click.style('Some things', reverse=True, fg='cyan')) + click.echo(click.style('More colors', fg=(255, 12, 128), bg=117)) + + Supported color names: + + * ``black`` (might be a gray) + * ``red`` + * ``green`` + * ``yellow`` (might be an orange) + * ``blue`` + * ``magenta`` + * ``cyan`` + * ``white`` (might be light gray) + * ``bright_black`` + * ``bright_red`` + * ``bright_green`` + * ``bright_yellow`` + * ``bright_blue`` + * ``bright_magenta`` + * ``bright_cyan`` + * ``bright_white`` + * ``reset`` (reset the color code only) + + If the terminal supports it, color may also be specified as: + + - An integer in the interval [0, 255]. The terminal must support + 8-bit/256-color mode. + - An RGB tuple of three integers in [0, 255]. The terminal must + support 24-bit/true-color mode. + + See https://en.wikipedia.org/wiki/ANSI_color and + https://gist.github.com/XVilka/8346728 for more information. + + :param text: the string to style with ansi codes. + :param fg: if provided this will become the foreground color. + :param bg: if provided this will become the background color. + :param bold: if provided this will enable or disable bold mode. + :param dim: if provided this will enable or disable dim mode. This is + badly supported. + :param underline: if provided this will enable or disable underline. + :param overline: if provided this will enable or disable overline. + :param italic: if provided this will enable or disable italic. + :param blink: if provided this will enable or disable blinking. + :param reverse: if provided this will enable or disable inverse + rendering (foreground becomes background and the + other way round). + :param strikethrough: if provided this will enable or disable + striking through text. + :param reset: by default a reset-all code is added at the end of the + string which means that styles do not carry over. This + can be disabled to compose styles. + + .. versionchanged:: 8.5.0 + All invalid color values raise :exc:`ValueError`. 256-color index + ``0`` is no longer ignored. + + .. versionchanged:: 8.0 + A non-string ``message`` is converted to a string. + + .. versionchanged:: 8.0 + Added support for 256 and RGB color codes. + + .. versionchanged:: 8.0 + Added the ``strikethrough``, ``italic``, and ``overline`` + parameters. + + .. versionchanged:: 7.0 + Added support for bright colors. + + .. versionadded:: 2.0 + """ + if not isinstance(text, str): + text = str(text) + + bits = [] + + if fg is not None: + bits.append(f"\033[{_interpret_color(fg)}m") + + if bg is not None: + bits.append(f"\033[{_interpret_color(bg, 10)}m") + + if bold is not None: + bits.append(f"\033[{1 if bold else 22}m") + if dim is not None: + bits.append(f"\033[{2 if dim else 22}m") + if underline is not None: + bits.append(f"\033[{4 if underline else 24}m") + if overline is not None: + bits.append(f"\033[{53 if overline else 55}m") + if italic is not None: + bits.append(f"\033[{3 if italic else 23}m") + if blink is not None: + bits.append(f"\033[{5 if blink else 25}m") + if reverse is not None: + bits.append(f"\033[{7 if reverse else 27}m") + if strikethrough is not None: + bits.append(f"\033[{9 if strikethrough else 29}m") + bits.append(text) + if reset: + bits.append(_ansi_reset_all) + return "".join(bits) + + +def unstyle(text: str) -> str: + """Removes ANSI styling information from a string. Usually it's not + necessary to use this function as Click's echo function will + automatically remove styling if necessary. + + .. versionadded:: 2.0 + + :param text: the text to remove style information from. + """ + return strip_ansi(text) + + +def secho( + message: t.Any | None = None, + file: t.IO[t.AnyStr] | None = None, + nl: bool = True, + err: bool = False, + color: bool | None = None, + **styles: t.Any, +) -> None: + """This function combines :func:`echo` and :func:`style` into one + call. As such the following two calls are the same:: + + click.secho('Hello World!', fg='green') + click.echo(click.style('Hello World!', fg='green')) + + All keyword arguments are forwarded to the underlying functions + depending on which one they go with. + + Non-string types will be converted to :class:`str`. However, + :class:`bytes` are passed directly to :meth:`echo` without applying + style. If you want to style bytes that represent text, call + :meth:`bytes.decode` first. + + .. versionchanged:: 8.0 + A non-string ``message`` is converted to a string. Bytes are + passed through without style applied. + + .. versionadded:: 2.0 + """ + if message is not None and not isinstance(message, (bytes, bytearray)): + message = style(message, **styles) + + return echo(message, file=file, nl=nl, err=err, color=color) + + +@t.overload +def edit( + text: bytes | bytearray, + editor: str | None = None, + env: cabc.Mapping[str, str] | None = None, + require_save: bool = False, + extension: str = ".txt", +) -> bytes | None: ... + + +@t.overload +def edit( + text: str, + editor: str | None = None, + env: cabc.Mapping[str, str] | None = None, + require_save: bool = True, + extension: str = ".txt", +) -> str | None: ... + + +@t.overload +def edit( + text: None = None, + editor: str | None = None, + env: cabc.Mapping[str, str] | None = None, + require_save: bool = True, + extension: str = ".txt", + filename: str + | os.PathLike[str] + | cabc.Iterable[str | os.PathLike[str]] + | None = None, +) -> None: ... + + +def edit( + text: str | bytes | bytearray | None = None, + editor: str | None = None, + env: cabc.Mapping[str, str] | None = None, + require_save: bool = True, + extension: str = ".txt", + filename: str + | os.PathLike[str] + | cabc.Iterable[str | os.PathLike[str]] + | None = None, +) -> str | bytes | bytearray | None: + r"""Edits the given text in the defined editor. If an editor is given + (should be the full path to the executable but the regular operating + system search path is used for finding the executable) it overrides + the detected editor. Optionally, some environment variables can be + used. If the editor is closed without changes, `None` is returned. In + case a file is edited directly the return value is always `None` and + `require_save` and `extension` are ignored. + + If the editor cannot be opened a :exc:`UsageError` is raised. + + Note for Windows: to simplify cross-platform usage, the newlines are + automatically converted from POSIX to Windows and vice versa. As such, + the message here will have ``\n`` as newline markers. + + :param text: the text to edit. + :param editor: optionally the editor to use. Defaults to automatic + detection. + :param env: environment variables to forward to the editor. + :param require_save: if this is true, then not saving in the editor + will make the return value become `None`. + :param extension: the extension to tell the editor about. This defaults + to `.txt` but changing this might change syntax + highlighting. + :param filename: if provided it will edit this file instead of the + provided text contents. It will not use a temporary + file as an indirection in that case. It accepts a path + or any iterable of paths. If the editor supports + editing multiple files at once, a sequence of files may + be passed as well. Invoke `click.file` once per file + instead if multiple files cannot be managed at once or + editing the files serially is desired. + + .. versionchanged:: 8.2.0 + ``filename`` now accepts any ``Iterable[str]`` in addition to a ``str`` + if the ``editor`` supports editing multiple files at once. + + .. versionchanged:: 8.5.0 + ``filename`` accepts ``os.PathLike`` values in addition to strings. + + """ + from ._termui_impl import Editor + + ed = Editor(editor=editor, env=env, require_save=require_save, extension=extension) + + if filename is None: + return ed.edit(text) + + if isinstance(filename, (str, os.PathLike)): + filename = (filename,) + + ed.edit_files(filenames=filename) + return None + + +def launch(url: str, wait: bool = False, locate: bool = False) -> int: + """This function launches the given URL (or filename) in the default + viewer application for this file type. If this is an executable, it + might launch the executable in a new session. The return value is + the exit code of the launched application. Usually, ``0`` indicates + success. + + Examples:: + + click.launch('https://click.palletsprojects.com/') + click.launch('/my/downloaded/file', locate=True) + + .. versionadded:: 2.0 + + :param url: URL or filename of the thing to launch. + :param wait: Wait for the program to exit before returning. This + only works if the launched program blocks. In particular, + ``xdg-open`` on Linux does not block. + :param locate: if this is set to `True` then instead of launching the + application associated with the URL it will attempt to + launch a file manager with the file located. This + might have weird effects if the URL does not point to + the filesystem. + """ + from ._termui_impl import open_url + + return open_url(url, wait=wait, locate=locate) + + +# If this is provided, getchar() calls into this instead. This is used +# for unittesting purposes. +_getchar: t.Callable[[bool], str] | None = None + + +def getchar(echo: bool = False) -> str: + """Fetches a single character from the terminal and returns it. This + will always return a unicode character and under certain rare + circumstances this might return more than one character. The + situations which more than one character is returned is when for + whatever reason multiple characters end up in the terminal buffer or + standard input was not actually a terminal. + + Note that this will always read from the terminal, even if something + is piped into the standard input. + + Note for Windows: in rare cases when typing non-ASCII characters, this + function might wait for a second character and then return both at once. + This is because certain Unicode characters look like special-key markers. + + .. versionadded:: 2.0 + + :param echo: if set to `True`, the character read will also show up on + the terminal. The default is to not show it. + """ + global _getchar + + if _getchar is None: + from ._termui_impl import getchar as f + + _getchar = f + + return _getchar(echo) + + +def raw_terminal() -> AbstractContextManager[int]: + from ._termui_impl import raw_terminal as f + + return f() + + +def pause(info: str | None = None, err: bool = False) -> None: + """This command stops execution and waits for the user to press any + key to continue. This is similar to the Windows batch "pause" + command. If the program is not run through a terminal, this command + will instead do nothing. + + .. versionadded:: 2.0 + + .. versionadded:: 4.0 + Added the `err` parameter. + + :param info: The message to print before pausing. Defaults to + ``"Press any key to continue..."``. + :param err: if set to message goes to ``stderr`` instead of + ``stdout``, the same as with echo. + """ + if not isatty(sys.stdin) or not isatty(sys.stdout): + return + + if info is None: + info = _("Press any key to continue...") + + try: + if info: + echo(info, nl=False, err=err) + try: + getchar() + except (KeyboardInterrupt, EOFError): + pass + finally: + if info: + echo(err=err) diff --git a/source/target-vendors/cp312/click/testing.py b/source/target-vendors/cp312/click/testing.py new file mode 100644 index 0000000..cc123d4 --- /dev/null +++ b/source/target-vendors/cp312/click/testing.py @@ -0,0 +1,798 @@ +from __future__ import annotations + +import collections.abc as cabc +import contextlib +import io +import os +import pdb +import shlex +import sys +import tempfile +import typing as t +from types import TracebackType + +from . import _compat +from . import formatting +from . import termui +from . import utils +from ._compat import _find_binary_reader + +if t.TYPE_CHECKING: + from _typeshed import ReadableBuffer + + from .core import Command + +if sys.platform == "win32": + CaptureMode: t.TypeAlias = t.Literal["sys"] # pyright: ignore[reportRedeclaration] +else: + CaptureMode: t.TypeAlias = t.Literal["sys", "fd"] # pyright: ignore[reportRedeclaration] +ExceptionInfo: t.TypeAlias = tuple[type[BaseException], BaseException, TracebackType] + + +class EchoingStdin: + _input: t.BinaryIO + _output: t.BinaryIO + _paused: bool + + def __init__(self, input: t.BinaryIO, output: t.BinaryIO) -> None: + self._input = input + self._output = output + self._paused = False + + def __getattr__(self, x: str) -> t.Any: + return getattr(self._input, x) + + def _echo(self, rv: bytes) -> bytes: + if not self._paused: + self._output.write(rv) + + return rv + + def read(self, n: int = -1) -> bytes: + return self._echo(self._input.read(n)) + + def read1(self, n: int = -1) -> bytes: + return self._echo(self._input.read1(n)) # type: ignore + + def readline(self, n: int = -1) -> bytes: + return self._echo(self._input.readline(n)) + + def readlines(self) -> list[bytes]: + return [self._echo(x) for x in self._input.readlines()] + + def __iter__(self) -> cabc.Iterator[bytes]: + return iter(self._echo(x) for x in self._input) + + def __repr__(self) -> str: + return repr(self._input) + + +@contextlib.contextmanager +def _pause_echo(stream: EchoingStdin | None) -> cabc.Generator[None]: + if stream is None: + yield + else: + stream._paused = True + yield + stream._paused = False + + +class _FDCapture: + """Redirect a file descriptor to a temporary file for capture. + + Saves the current target of *targetfd* via :func:`os.dup`, then + redirects it to a temporary file via :func:`os.dup2`. On + :meth:`stop`, restores the original ``fd`` and returns the captured + bytes. Inspired by Pytest's ``FDCapture``. + + .. versionadded:: 8.4.0 + """ + + _targetfd: int + saved_fd: int + _tmpfile: t.BinaryIO | None + + def __init__(self, targetfd: int) -> None: + self._targetfd = targetfd + self.saved_fd = -1 + self._tmpfile = None + + def start(self) -> None: + self.saved_fd = os.dup(self._targetfd) + self._tmpfile = tempfile.TemporaryFile(buffering=0) + os.dup2(self._tmpfile.fileno(), self._targetfd) + + def stop(self) -> bytes: + assert self._tmpfile is not None, "_FDCapture.start() was not called" + os.dup2(self.saved_fd, self._targetfd) + os.close(self.saved_fd) + self.saved_fd = -1 + self._tmpfile.seek(0) + data = self._tmpfile.read() + self._tmpfile.close() + self._tmpfile = None + return data + + +class BytesIOCopy(io.BytesIO): + """Patch ``io.BytesIO`` to let the written stream be copied to another. + + .. versionadded:: 8.2 + """ + + copy_to: io.BytesIO + + def __init__(self, copy_to: io.BytesIO) -> None: + super().__init__() + self.copy_to = copy_to + + def flush(self) -> None: + super().flush() + self.copy_to.flush() + + def write(self, b: ReadableBuffer) -> int: + self.copy_to.write(b) + return super().write(b) + + +class StreamMixer: + """Mixes `` and `` streams. + + The result is available in the ``output`` attribute. + + .. versionadded:: 8.2 + """ + + output: io.BytesIO + stdout: BytesIOCopy + stderr: BytesIOCopy + + def __init__(self) -> None: + self.output = io.BytesIO() + self.stdout = BytesIOCopy(copy_to=self.output) + self.stderr = BytesIOCopy(copy_to=self.output) + + +class _NamedTextIOWrapper(io.TextIOWrapper): + """A :class:`~io.TextIOWrapper` with custom ``name`` and ``mode`` + that does not close its underlying buffer. + + When ``CliRunner`` runs in ``fd`` mode, ``_original_fd`` is patched to + point at the saved (pre-redirection) ``fd``, so C-level consumers that call + :meth:`fileno` (like ``faulthandler`` or ``subprocess``) keep working. In + the default ``sys`` mode ``_original_fd`` stays at ``-1`` and + :meth:`fileno` raises :exc:`io.UnsupportedOperation`, matching the + pre-``8.3.3`` behavior. + """ + + _name: str + _mode: str + _original_fd: int + + def __init__( + self, + buffer: t.BinaryIO, + name: str, + mode: str, + **kwargs: t.Any, + ) -> None: + super().__init__(buffer, **kwargs) + self._name = name + self._mode = mode + self._original_fd = -1 + + def close(self) -> None: + """The buffer this object contains belongs to some other object, + so prevent the default ``__del__`` implementation from closing + that buffer. + + .. versionadded:: 8.3.2 + """ + + def fileno(self) -> int: + """Return the file descriptor of the saved original stream when + ``CliRunner`` runs in ``fd`` mode. Otherwise delegate to + :class:`~io.TextIOWrapper`, which raises + :exc:`io.UnsupportedOperation` for a ``BytesIO``-backed buffer. + """ + if self._original_fd >= 0: + return self._original_fd + return super().fileno() + + @property + def name(self) -> str: + return self._name + + @property + def mode(self) -> str: + return self._mode + + +def make_input_stream( + input: str | bytes | t.IO[t.Any] | None, charset: str +) -> t.BinaryIO: + # Is already an input stream. + if hasattr(input, "read"): + rv = _find_binary_reader(t.cast("t.IO[t.Any]", input)) + + if rv is not None: + return rv + + raise TypeError("Could not find binary reader for input stream.") + + if input is None: + input = b"" + elif isinstance(input, str): + input = input.encode(charset) + + return io.BytesIO(input) + + +class Result: + """Holds the captured result of an invoked CLI script. + + :param runner: The runner that created the result + :param stdout_bytes: The standard output as bytes. + :param stderr_bytes: The standard error as bytes. + :param output_bytes: A mix of ``stdout_bytes`` and ``stderr_bytes``, as the + user would see it in its terminal. + :param return_value: The value returned from the invoked command. + :param exit_code: The exit code as integer. + :param exception: The exception that happened if one did. + :param exc_info: Exception information (exception type, exception instance, + traceback type). + + .. versionchanged:: 8.2 + ``stderr_bytes`` no longer optional, ``output_bytes`` introduced and + ``mix_stderr`` has been removed. + + .. versionadded:: 8.0 + Added ``return_value``. + """ + + runner: CliRunner + stdout_bytes: bytes + stderr_bytes: bytes + output_bytes: bytes + return_value: t.Any + exit_code: int + exception: BaseException | None + exc_info: ExceptionInfo | None + + def __init__( + self, + runner: CliRunner, + stdout_bytes: bytes, + stderr_bytes: bytes, + output_bytes: bytes, + return_value: t.Any, + exit_code: int, + exception: BaseException | None, + exc_info: ExceptionInfo | None = None, + ) -> None: + self.runner = runner + self.stdout_bytes = stdout_bytes + self.stderr_bytes = stderr_bytes + self.output_bytes = output_bytes + self.return_value = return_value + self.exit_code = exit_code + self.exception = exception + self.exc_info = exc_info + + @property + def output(self) -> str: + """The terminal output as unicode string, as the user would see it. + + .. versionchanged:: 8.2 + No longer a proxy for ``self.stdout``. Now has its own independent stream + that is mixing `` and ``, in the order they were written. + """ + return self.output_bytes.decode(self.runner.charset, "replace").replace( + "\r\n", "\n" + ) + + @property + def stdout(self) -> str: + """The standard output as unicode string.""" + return self.stdout_bytes.decode(self.runner.charset, "replace").replace( + "\r\n", "\n" + ) + + @property + def stderr(self) -> str: + """The standard error as unicode string. + + .. versionchanged:: 8.2 + No longer raise an exception, always returns the `` string. + """ + return self.stderr_bytes.decode(self.runner.charset, "replace").replace( + "\r\n", "\n" + ) + + def __repr__(self) -> str: + exc_str = repr(self.exception) if self.exception else "okay" + return f"<{type(self).__name__} {exc_str}>" + + +class CliRunner: + """The CLI runner provides functionality to invoke a Click command line + script for unittesting purposes in a isolated environment. This only + works in single-threaded systems without any concurrency as it changes the + global interpreter state. + + :param charset: the character set for the input and output data. + :param env: a dictionary with environment variables for overriding. + :param echo_stdin: if this is set to `True`, then reading from `` writes + to ``. This is useful for showing examples in + some circumstances. Note that regular prompts + will automatically echo the input. + :param catch_exceptions: Whether to catch any exceptions other than + ``SystemExit`` when running :meth:`~CliRunner.invoke`. + :param capture: Selects the output capture strategy. ``sys`` (default) + captures Python-level writes only and leaves + :meth:`sys.stdout.fileno` raising :exc:`io.UnsupportedOperation`, so + user code that calls :func:`os.dup2` on ``sys.stdout.fileno()`` cannot + clobber the host runner's stdout. ``fd`` redirects file descriptors + ``1`` and ``2`` via :func:`os.dup2` to a temporary file, also catching + output from stale stream references, C extensions, and subprocesses. + ``fd`` is not supported on Windows. + + .. versionchanged:: 8.4.0 + Added the ``capture`` parameter. The default ``sys`` mode no longer + exposes the original fd through :meth:`fileno`, reverting the change + introduced in ``8.3.3`` that broke Pytest's ``fd``-level capture + teardown. Use ``capture="fd"`` to restore that behavior with proper + isolation. :issue:`3384` + + .. versionchanged:: 8.2 + Added the ``catch_exceptions`` parameter. + + .. versionchanged:: 8.2 + ``mix_stderr`` parameter has been removed. + """ + + charset: str + env: cabc.Mapping[str, str | None] + echo_stdin: bool + catch_exceptions: bool + capture: CaptureMode + + def __init__( + self, + charset: str = "utf-8", + env: cabc.Mapping[str, str | None] | None = None, + echo_stdin: bool = False, + catch_exceptions: bool = True, + capture: CaptureMode = "sys", + ) -> None: + if capture not in {"sys", "fd"}: + raise ValueError( + f"capture={capture!r} is not valid. Choose from 'sys' or 'fd'." + ) + if capture == "fd" and sys.platform == "win32": + raise ValueError( + f"capture={capture!r} is not supported on Windows. Use 'sys'." + ) + self.charset = charset + self.env = env or {} + self.echo_stdin = echo_stdin + self.catch_exceptions = catch_exceptions + self.capture = capture + + def get_default_prog_name(self, cli: Command) -> str: + """Given a command object it will return the default program name + for it. The default is the `name` attribute or ``"root"`` if not + set. + """ + return cli.name or "root" + + def make_env( + self, overrides: cabc.Mapping[str, str | None] | None = None + ) -> cabc.Mapping[str, str | None]: + """Returns the environment overrides for invoking a script.""" + rv = dict(self.env) + if overrides: + rv.update(overrides) + return rv + + @contextlib.contextmanager + def isolation( + self, + input: str | bytes | t.IO[t.Any] | None = None, + env: cabc.Mapping[str, str | None] | None = None, + color: bool = False, + ) -> cabc.Generator[tuple[io.BytesIO, io.BytesIO, io.BytesIO]]: + """A context manager that sets up the isolation for invoking of a + command line tool. This sets up `` with the given input data + and `os.environ` with the overrides from the given dictionary. + This also rebinds some internals in Click to be mocked (like the + prompt functionality). + + This is automatically done in the :meth:`invoke` method. + + :param input: the input stream to put into `sys.stdin`. + :param env: the environment overrides as dictionary. + :param color: whether the output should contain color codes. The + application can still override this explicitly. + + .. versionadded:: 8.2 + An additional output stream is returned, which is a mix of + `` and `` streams. + + .. versionchanged:: 8.2 + Always returns the `` stream. + + .. versionchanged:: 8.0 + `` is opened with ``errors="backslashreplace"`` + instead of the default ``"strict"``. + + .. versionchanged:: 4.0 + Added the ``color`` parameter. + """ + bytes_input = make_input_stream(input, self.charset) + echo_input = None + + old_stdin = sys.stdin + old_stdout = sys.stdout + old_stderr = sys.stderr + old_forced_width = formatting.FORCED_WIDTH + formatting.FORCED_WIDTH = 80 + + env = self.make_env(env) + + stream_mixer = StreamMixer() + + if self.echo_stdin: + bytes_input = echo_input = t.cast( + t.BinaryIO, EchoingStdin(bytes_input, stream_mixer.stdout) + ) + + sys.stdin = text_input = _NamedTextIOWrapper( + bytes_input, encoding=self.charset, name="", mode="r" + ) + + if self.echo_stdin: + # Force unbuffered reads, otherwise TextIOWrapper reads a + # large chunk which is echoed early. + text_input._CHUNK_SIZE = 1 # type: ignore + + sys.stdout = _NamedTextIOWrapper( + stream_mixer.stdout, + encoding=self.charset, + name="", + mode="w", + ) + + sys.stderr = _NamedTextIOWrapper( + stream_mixer.stderr, + encoding=self.charset, + name="", + mode="w", + errors="backslashreplace", + ) + + @_pause_echo(echo_input) # type: ignore + def visible_input(prompt: str | None = None) -> str: + sys.stdout.write(prompt or "") + try: + val = next(text_input).rstrip("\r\n") + except StopIteration as e: + raise EOFError() from e + sys.stdout.write(f"{val}\n") + sys.stdout.flush() + return val + + @_pause_echo(echo_input) # type: ignore + def hidden_input(prompt: str | None = None) -> str: + sys.stdout.write(f"{prompt or ''}\n") + sys.stdout.flush() + try: + return next(text_input).rstrip("\r\n") + except StopIteration as e: + raise EOFError() from e + + @_pause_echo(echo_input) # type: ignore + def _getchar(echo: bool) -> str: + char = sys.stdin.read(1) + + if echo: + sys.stdout.write(char) + + sys.stdout.flush() + return char + + default_color = color + + def should_strip_ansi( + stream: t.IO[t.Any] | None = None, color: bool | None = None + ) -> bool: + if color is None: + return not default_color + return not color + + old_visible_prompt_func = termui.visible_prompt_func + old_hidden_prompt_func = termui.hidden_prompt_func + old__getchar_func = termui._getchar + old_should_strip_ansi = utils.should_strip_ansi # type: ignore + old__compat_should_strip_ansi = _compat.should_strip_ansi + old_pdb_init = pdb.Pdb.__init__ + termui.visible_prompt_func = visible_input + termui.hidden_prompt_func = hidden_input + termui._getchar = _getchar + utils.should_strip_ansi = should_strip_ansi # type: ignore + _compat.should_strip_ansi = should_strip_ansi + + def _patched_pdb_init( + self: pdb.Pdb, + completekey: str = "tab", + stdin: t.IO[str] | None = None, + stdout: t.IO[str] | None = None, + **kwargs: t.Any, + ) -> None: + """Default ``pdb.Pdb`` to real terminal streams during + ``CliRunner`` isolation. + + Without this patch, ``pdb.Pdb.__init__`` inherits from + ``cmd.Cmd`` which falls back to ``sys.stdin``/``sys.stdout`` + when no explicit streams are provided. During isolation + those are ``BytesIO``-backed wrappers, so the debugger + reads from an empty buffer and writes to captured output, + making interactive debugging impossible. + + By defaulting to ``sys.__stdin__``/``sys.__stdout__`` (the + original terminal streams Python preserves regardless of + redirection), debuggers can interact with the user while + ``click.echo`` output is still captured normally. + + This covers ``pdb.set_trace()``, ``breakpoint()``, + ``pdb.post_mortem()``, and debuggers that subclass + ``pdb.Pdb`` (ipdb, pdbpp). Explicit ``stdin``/``stdout`` + arguments are honored and not overridden. Debuggers that + do not subclass ``pdb.Pdb`` (pudb, debugpy) are not + covered. + """ + if stdin is None: + stdin = sys.__stdin__ + if stdout is None: + stdout = sys.__stdout__ + old_pdb_init( + self, completekey=completekey, stdin=stdin, stdout=stdout, **kwargs + ) + + pdb.Pdb.__init__ = _patched_pdb_init # type: ignore[assignment] + + old_env = {} + try: + for key, value in env.items(): + old_env[key] = os.environ.get(key) + if value is None: + try: + del os.environ[key] + except Exception: + pass + else: + os.environ[key] = value + yield (stream_mixer.stdout, stream_mixer.stderr, stream_mixer.output) + finally: + for key, value in old_env.items(): + if value is None: + try: + del os.environ[key] + except Exception: + pass + else: + os.environ[key] = value + sys.stdout = old_stdout + sys.stderr = old_stderr + sys.stdin = old_stdin + termui.visible_prompt_func = old_visible_prompt_func + termui.hidden_prompt_func = old_hidden_prompt_func + termui._getchar = old__getchar_func + utils.should_strip_ansi = old_should_strip_ansi # type: ignore + _compat.should_strip_ansi = old__compat_should_strip_ansi + formatting.FORCED_WIDTH = old_forced_width + pdb.Pdb.__init__ = old_pdb_init # type: ignore[method-assign] + + def invoke( + self, + cli: Command, + args: str | cabc.Sequence[str] | None = None, + input: str | bytes | t.IO[t.Any] | None = None, + env: cabc.Mapping[str, str | None] | None = None, + catch_exceptions: bool | None = None, + color: bool = False, + **extra: t.Any, + ) -> Result: + """Invokes a command in an isolated environment. The arguments are + forwarded directly to the command line script, the `extra` keyword + arguments are passed to the :meth:`~clickpkg.Command.main` function of + the command. + + This returns a :class:`Result` object. + + :param cli: the command to invoke + :param args: the arguments to invoke. It may be given as an iterable + or a string. When given as string it will be interpreted + as a Unix shell command. More details at + :func:`shlex.split`. + :param input: the input data for `sys.stdin`. + :param env: the environment overrides. + :param catch_exceptions: Whether to catch any other exceptions than + ``SystemExit``. If :data:`None`, the value + from :class:`CliRunner` is used. + :param extra: the keyword arguments to pass to :meth:`main`. + :param color: whether the output should contain color codes. The + application can still override this explicitly. + + .. versionadded:: 8.2 + The result object has the ``output_bytes`` attribute with + the mix of ``stdout_bytes`` and ``stderr_bytes``, as the user would + see it in its terminal. + + .. versionchanged:: 8.2 + The result object always returns the ``stderr_bytes`` stream. + + .. versionchanged:: 8.0 + The result object has the ``return_value`` attribute with + the value returned from the invoked command. + + .. versionchanged:: 4.0 + Added the ``color`` parameter. + + .. versionchanged:: 3.0 + Added the ``catch_exceptions`` parameter. + + .. versionchanged:: 3.0 + The result object has the ``exc_info`` attribute with the + traceback if available. + """ + exc_info = None + if catch_exceptions is None: + catch_exceptions = self.catch_exceptions + + # Set up fd capture before isolation replaces sys.stdout and sys.stderr. + cap_out: _FDCapture | None = None + cap_err: _FDCapture | None = None + + if self.capture == "fd": + cap_out = _FDCapture(1) + cap_err = _FDCapture(2) + try: + cap_out.start() + cap_err.start() + except OSError: + cap_out = cap_err = None + + with self.isolation(input=input, env=env, color=color) as outstreams: + # Point the captured streams' fileno() at the saved (original) + # fd so that C-level consumers like faulthandler keep working + # while fd 1/2 are redirected to the capture tmpfile. + if cap_out is not None and cap_err is not None: + sys.stdout._original_fd = cap_out.saved_fd # type: ignore[union-attr] + sys.stderr._original_fd = cap_err.saved_fd # type: ignore[union-attr] + + return_value = None + exception: BaseException | None = None + exit_code = 0 + + if isinstance(args, str): + args = shlex.split(args) + + try: + prog_name = extra.pop("prog_name") + except KeyError: + prog_name = self.get_default_prog_name(cli) + + try: + return_value = cli.main(args=args or (), prog_name=prog_name, **extra) + except SystemExit as e: + exc_info = sys.exc_info() + e_code = t.cast("int | t.Any | None", e.code) + + if e_code is None: + e_code = 0 + + if e_code != 0: + exception = e + + if not isinstance(e_code, int): + sys.stdout.write(str(e_code)) + sys.stdout.write("\n") + e_code = 1 + + exit_code = e_code + + except Exception as e: + if not catch_exceptions: + raise + exception = e + exit_code = 1 + exc_info = sys.exc_info() + finally: + sys.stdout.flush() + sys.stderr.flush() + + # Stop fd capture and merge the captured bytes into + # the stdout/stderr BytesIO streams. BytesIOCopy mirrors + # those writes into outstreams[2] automatically. + if cap_out is not None and cap_err is not None: + fd_out = cap_out.stop() + fd_err = cap_err.stop() + if fd_out: + outstreams[0].write(fd_out) + if fd_err: + outstreams[1].write(fd_err) + + stdout = outstreams[0].getvalue() + stderr = outstreams[1].getvalue() + output = outstreams[2].getvalue() + + return Result( + runner=self, + stdout_bytes=stdout, + stderr_bytes=stderr, + output_bytes=output, + return_value=return_value, + exit_code=exit_code, + exception=exception, + exc_info=exc_info, # type: ignore + ) + + @contextlib.contextmanager + def isolated_filesystem( + self, temp_dir: str | os.PathLike[str] | None = None + ) -> cabc.Generator[str]: + """A context manager that creates a temporary directory and + changes the current working directory to it. This isolates tests + that affect the contents of the CWD to prevent them from + interfering with each other. + + .. warning:: + This helper predates Python 3 and modern pytest, and is not + thread-safe: it relies on :func:`os.chdir`, which mutates + process-global state, and :meth:`invoke` swaps the + process-global standard streams too. Parallelize tests with + processes (``pytest-xdist``), not threads. Locking the + runner (:pr:`3511`, :pr:`3520`, :pr:`3530`) and a + ``set_filesystem()`` API (:issue:`3123`) were declined: + neither removes the global-state mutation. See :issue:`3700` + and :issue:`3501`. + + :param temp_dir: Create the temporary directory under this + directory. If given, the created directory is not removed + when exiting. + + .. deprecated:: 8.5.0 + Will be removed in Click 9.0. Use + :class:`tempfile.TemporaryDirectory` or pytest's + ``tmp_path`` fixture with absolute paths instead. + + .. versionchanged:: 8.0 + Added the ``temp_dir`` parameter. + """ + import warnings + + warnings.warn( + "'isolated_filesystem' is deprecated and will be removed in Click" + " 9.0. Use 'tempfile.TemporaryDirectory' or pytest's 'tmp_path'" + " fixture with absolute paths instead.", + DeprecationWarning, + stacklevel=3, + ) + + cwd = os.getcwd() + dt = tempfile.mkdtemp(dir=temp_dir) + os.chdir(dt) + + try: + yield dt + finally: + os.chdir(cwd) + + if temp_dir is None: + import shutil + + try: + shutil.rmtree(dt) + except OSError: + pass diff --git a/source/target-vendors/cp312/click/types.py b/source/target-vendors/cp312/click/types.py new file mode 100644 index 0000000..0eb8462 --- /dev/null +++ b/source/target-vendors/cp312/click/types.py @@ -0,0 +1,1422 @@ +from __future__ import annotations + +import abc +import collections.abc as cabc +import enum +import os +import stat +import sys +import typing as t +import uuid +from datetime import datetime +from gettext import gettext as _ +from gettext import ngettext + +from ._compat import _get_argv_encoding +from ._compat import open_stream +from .exceptions import BadParameter +from .utils import _LazyFile +from .utils import _safecall +from .utils import format_filename + +if t.TYPE_CHECKING: + import typing_extensions as te + + from .core import Context + from .core import Parameter + from .shell_completion import CompletionItem + +_ValueT = t.TypeVar("_ValueT") +_ValueT_contra = t.TypeVar("_ValueT_contra", contravariant=True) +_ValueT_co = t.TypeVar("_ValueT_co", covariant=True) + +# The input type parameter of ParamType defaults to Any. TypeVar defaults +# (PEP 696) landed in typing on Python 3.13, so type checkers get the +# default from typing_extensions, which is never imported at runtime. On +# older Pythons the runtime TypeVar carries no default; +# ParamType.__class_getitem__ fills in the omitted parameter instead. +if t.TYPE_CHECKING: + _InputT_contra = te.TypeVar("_InputT_contra", contravariant=True, default=t.Any) +elif sys.version_info >= (3, 13): + _InputT_contra = t.TypeVar("_InputT_contra", contravariant=True, default=t.Any) +else: + _InputT_contra = t.TypeVar("_InputT_contra", contravariant=True) + +_FloatValueT = t.TypeVar("_FloatValueT", bound=float) +_FloatValueT_co = t.TypeVar("_FloatValueT_co", bound=float, covariant=True) + + +class ParamTypeInfoDict(t.TypedDict): + param_type: str + name: str + + +class ParamType(t.Generic[_ValueT_co, _InputT_contra], abc.ABC): + """Represents the type of a parameter. Validates and converts values + from the command line or Python into the correct type. + + To implement a custom type, subclass and implement at least the + following: + + - The :attr:`name` class attribute must be set. + - Calling an instance of the type with ``None`` must return + ``None``. This is already implemented by default. + - :meth:`convert` must convert string values to the correct type. + - :meth:`convert` must accept values that are already the correct + type. + - It must be able to convert a value if the ``ctx`` and ``param`` + arguments are ``None``. This can occur when converting prompt + input. + + .. versionchanged:: 8.4.0 + Now a generic abstract base class. Parameterize with the + converted value type (``ParamType[int]`` for an integer-returning + type) so that :meth:`convert` and downstream consumers carry the + narrowed return type. + + .. versionchanged:: 8.5.0 + Accepts a second optional type parameter for the input value type + that :meth:`convert` accepts (``ParamType[int, str]`` for a type + converting strings to integers), defaulting to ``Any``. + """ + + is_composite: t.ClassVar[bool] = False + arity: int = 1 # read-only + + #: the descriptive name of this type + name: str + + #: if a list of this type is expected and the value is pulled from a + #: string environment variable, this is what splits it up. `None` + #: means any whitespace. For all parameters the general rule is that + #: whitespace splits them up. The exception are paths and files which + #: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on + #: Windows). + envvar_list_splitter: t.ClassVar[str | None] = None + + if sys.version_info < (3, 13): + # ``_InputT_contra`` carries its ``Any`` default only for type + # checkers: ``TypeVar(default=...)`` (PEP 696) is unavailable at + # runtime before Python 3.13. Fill in the omitted input type + # parameter by hand so ``ParamType[int]`` keeps working. + def __class_getitem__(cls, params: t.Any) -> t.Any: + if cls is ParamType: + if not isinstance(params, tuple): + params = (params,) + if len(params) == 1: + params = (*params, t.Any) + # Checkers cannot see Generic.__class_getitem__ through super(). + return super().__class_getitem__(params) # type: ignore[misc] + + def to_info_dict(self) -> ParamTypeInfoDict: + """Gather information that could be useful for a tool generating + user-facing documentation. + + Use :meth:`click.Context.to_info_dict` to traverse the entire + CLI structure. + + .. versionadded:: 8.0 + """ + # The class name without the "ParamType" suffix. + param_type = type(self).__name__.partition("ParamType")[0] + param_type = param_type.partition("ParameterType")[0] + + # Custom subclasses might not remember to set a name. + if hasattr(self, "name"): + name = self.name + else: + name = param_type + + return {"param_type": param_type, "name": name} + + @t.overload + def __call__( + self, + value: None, + param: Parameter | None = None, + ctx: Context | None = None, + ) -> None: ... + + @t.overload + def __call__( + self, + value: _InputT_contra, + param: Parameter | None = None, + ctx: Context | None = None, + ) -> _ValueT_co: ... + + def __call__( + self, + value: _InputT_contra | None, + param: Parameter | None = None, + ctx: Context | None = None, + ) -> _ValueT_co | None: + if value is not None: + return self.convert(value, param, ctx) + return None + + def get_metavar(self, param: Parameter, ctx: Context) -> str | None: + """Returns the metavar default for this param if it provides one.""" + + def get_missing_message(self, param: Parameter, ctx: Context | None) -> str | None: + """Optionally might return extra information about a missing + parameter. + + .. versionadded:: 2.0 + """ + + def convert( + self, value: _InputT_contra, param: Parameter | None, ctx: Context | None + ) -> _ValueT_co: + """Convert the value to the correct type. This is not called if + the value is ``None`` (the missing value). + + This must accept string values from the command line, as well as + values that are already the correct type. It may also convert + other compatible types. + + The ``param`` and ``ctx`` arguments may be ``None`` in certain + situations, such as when converting prompt input. + + If the value cannot be converted, call :meth:`fail` with a + descriptive message. + + :param value: The value to convert. + :param param: The parameter that is using this type to convert + its value. May be ``None``. + :param ctx: The current context that arrived at this value. May + be ``None``. + """ + # The default returns the value as-is so subclasses that only customize + # metadata are not forced to redeclare ``convert``. + return t.cast("_ValueT_co", value) + + def split_envvar_value(self, rv: str) -> cabc.Sequence[str]: + """Given a value from an environment variable this splits it up + into small chunks depending on the defined envvar list splitter. + + If the splitter is set to `None`, which means that whitespace splits, + then leading and trailing whitespace is ignored. Otherwise, leading + and trailing splitters usually lead to empty items being included. + """ + return (rv or "").split(self.envvar_list_splitter) + + def fail( + self, + message: str, + param: Parameter | None = None, + ctx: Context | None = None, + ) -> t.NoReturn: + """Helper method to fail with an invalid value message.""" + raise BadParameter(message, ctx=ctx, param=param) + + def shell_complete( + self, ctx: Context, param: Parameter, incomplete: str + ) -> list[CompletionItem]: + """Return a list of + :class:`~click.shell_completion.CompletionItem` objects for the + incomplete value. Most types do not provide completions, but + some do, and this allows custom types to provide custom + completions as well. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + return [] + + +class CompositeParamType(ParamType[_ValueT_co]): + is_composite: t.ClassVar[bool] = True + + @property + @abc.abstractmethod + def arity(self) -> int: ... # type: ignore[override] + + +if t.TYPE_CHECKING: + # on Python 3.10 this will raise a TypeError + + class FuncParamTypeInfoDict( + ParamTypeInfoDict, + t.Generic[_ValueT_contra, _ValueT_co], + ): + func: t.Callable[[_ValueT_contra], _ValueT_co] +else: + + class FuncParamTypeInfoDict(ParamTypeInfoDict): + func: t.Callable[[t.Any], t.Any] + + +class FuncParamType(ParamType[_ValueT_co], t.Generic[_ValueT_contra, _ValueT_co]): + name: str + func: t.Callable[[_ValueT_contra], _ValueT_co] + + def __init__(self, func: t.Callable[[_ValueT_contra], _ValueT_co]) -> None: + self.name = func.__name__ + self.func = func + + def to_info_dict(self) -> FuncParamTypeInfoDict[_ValueT_contra, _ValueT_co]: + return {"func": self.func, **super().to_info_dict()} + + def convert( + self, value: _ValueT_contra, param: Parameter | None, ctx: Context | None + ) -> _ValueT_co: + try: + return self.func(value) + except ValueError as exc: + message = str(exc) + + if not message: + try: + message = str(value) + except UnicodeError: + message = t.cast("bytes", value).decode("utf-8", "replace") + + self.fail(message, param, ctx) + + +class UnprocessedParamType(ParamType[t.Any]): + name = "text" + + def convert( + self, value: _ValueT, param: Parameter | None, ctx: Context | None + ) -> _ValueT: + return value + + def __repr__(self) -> str: + return "UNPROCESSED" + + +class StringParamType(ParamType[str]): + name = "text" + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> str: + if isinstance(value, bytes): + enc = _get_argv_encoding() + try: + return value.decode(enc) + except UnicodeError: + fs_enc = sys.getfilesystemencoding() + if fs_enc != enc: + try: + return value.decode(fs_enc) + except UnicodeError: + return value.decode("utf-8", "replace") + else: + return value.decode("utf-8", "replace") + return str(value) + + def __repr__(self) -> str: + return "STRING" + + +if t.TYPE_CHECKING: + # on Python 3.10 this will raise a TypeError + + class ChoiceInfoDict(ParamTypeInfoDict, t.Generic[_ValueT_co]): + choices: tuple[_ValueT_co, ...] + case_sensitive: bool +else: + + class ChoiceInfoDict(ParamTypeInfoDict): + choices: tuple[t.Any, ...] + case_sensitive: bool + + +class Choice(ParamType[_ValueT_co], t.Generic[_ValueT_co]): + """The choice type allows a value to be checked against a fixed set + of supported values. + + You may pass any iterable value which will be converted to a tuple + and thus will only be iterated once. + + The resulting value will always be one of the originally passed choices. + See :meth:`normalize_choice` for more info on the mapping of strings + to choices. See :ref:`choice-opts` for an example. + + :param case_sensitive: Set to false to make choices case + insensitive. Defaults to true. + + .. versionchanged:: 8.4.0 + Now generic in the choice value type. Parameterize with the type of + the choice values (``Choice[HashType]`` for an enum, ``Choice[str]`` + for plain strings) to enable type-checked consumers. + + .. versionchanged:: 8.2.0 + Non-``str`` ``choices`` are now supported. It can additionally be any + iterable. Before you were not recommended to pass anything but a list or + tuple. + + .. versionadded:: 8.2.0 + Choice normalization can be overridden via :meth:`normalize_choice`. + """ + + name: str = "choice" + + choices: tuple[_ValueT_co, ...] + case_sensitive: bool + + def __init__( + self, choices: cabc.Iterable[_ValueT_co], case_sensitive: bool = True + ) -> None: + self.choices = tuple(choices) + self.case_sensitive = case_sensitive + + def to_info_dict(self) -> ChoiceInfoDict[_ValueT_co]: + return { + "choices": self.choices, + "case_sensitive": self.case_sensitive, + **super().to_info_dict(), + } + + def _normalized_mapping( + self, ctx: Context | None = None + ) -> cabc.Mapping[_ValueT_co, str]: + """ + Returns mapping where keys are the original choices and the values are + the normalized values that are accepted via the command line. + + This is a simple wrapper around :meth:`normalize_choice`, use that + instead which is supported. + """ + return { + choice: self.normalize_choice( + choice=choice, + ctx=ctx, + ) + for choice in self.choices + } + + def normalize_choice(self, choice: object, ctx: Context | None) -> str: + """ + Normalize a choice value, used to map a passed string to a choice. + Each choice must have a unique normalized value. + + By default uses :meth:`Context.token_normalize_func` and if not case + sensitive, convert it to a casefolded value. + + .. versionadded:: 8.2.0 + """ + normed_value = choice.name if isinstance(choice, enum.Enum) else str(choice) + + if ctx is not None and ctx.token_normalize_func is not None: + normed_value = ctx.token_normalize_func(normed_value) + + if not self.case_sensitive: + normed_value = normed_value.casefold() + + return normed_value + + def get_metavar(self, param: Parameter, ctx: Context) -> str | None: + if param.param_type_name == "option" and not param.show_choices: # type: ignore[attr-defined] + choice_metavars = [ + convert_type(type(choice)).name.upper() for choice in self.choices + ] + choices_str = "|".join([*dict.fromkeys(choice_metavars)]) + else: + choices_str = "|".join( + [str(i) for i in self._normalized_mapping(ctx=ctx).values()] + ) + + # Use curly braces to indicate a required argument. + if param.required and param.param_type_name == "argument": + return f"{{{choices_str}}}" + + # Use square braces to indicate an option or optional argument. + return f"[{choices_str}]" + + def get_missing_message(self, param: Parameter, ctx: Context | None) -> str: + """ + Message shown when no choice is passed. + + .. versionchanged:: 8.2.0 Added ``ctx`` argument. + """ + return _("Choose from:\n\t{choices}").format( + choices=",\n\t".join(self._normalized_mapping(ctx=ctx).values()) + ) + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> _ValueT_co: + """ + For a given value from the parser, normalize it and find its + matching normalized value in the list of choices. Then return the + matched "original" choice. + """ + normed_value = self.normalize_choice(choice=value, ctx=ctx) + normalized_mapping = self._normalized_mapping(ctx=ctx) + + try: + return next( + original + for original, normalized in normalized_mapping.items() + if normalized == normed_value + ) + except StopIteration: + self.fail( + self.get_invalid_choice_message(value=value, ctx=ctx), + param=param, + ctx=ctx, + ) + + def get_invalid_choice_message(self, value: t.Any, ctx: Context | None) -> str: + """Get the error message when the given choice is invalid. + + :param value: The invalid value. + + .. versionadded:: 8.2 + """ + choices_str = ", ".join(map(repr, self._normalized_mapping(ctx=ctx).values())) + return ngettext( + "{value!r} is not {choice}.", + "{value!r} is not one of {choices}.", + len(self.choices), + ).format(value=value, choice=choices_str, choices=choices_str) + + def __repr__(self) -> str: + return _("Choice({choices})").format(choices=list(self.choices)) + + def shell_complete( + self, ctx: Context, param: Parameter, incomplete: str + ) -> list[CompletionItem]: + """Complete choices that start with the incomplete value. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + str_choices = [self.normalize_choice(choice, ctx) for choice in self.choices] + if self.case_sensitive: + matched = (c for c in str_choices if c.startswith(incomplete)) + else: + incomplete = incomplete.lower() + matched = (c for c in str_choices if c.lower().startswith(incomplete)) + + return [CompletionItem(c) for c in matched] + + +class DateTimeInfoDict(ParamTypeInfoDict): + formats: cabc.Sequence[str] + + +class DateTime(ParamType[datetime]): + """The DateTime type converts date strings into `datetime` objects. + + The format strings which are checked are configurable, but default to some + common (non-timezone aware) ISO 8601 formats. + + When specifying *DateTime* formats, you should only pass a list or a tuple. + Other iterables, like generators, may lead to surprising results. + + The format strings are processed using ``datetime.strptime``, and this + consequently defines the format strings which are allowed. + + Parsing is tried using each format, in order, and the first format which + parses successfully is used. + + :param formats: A list or tuple of date format strings, in the order in + which they should be tried. Defaults to + ``'%Y-%m-%d'``, ``'%Y-%m-%dT%H:%M:%S'``, + ``'%Y-%m-%d %H:%M:%S'``. + """ + + name = "datetime" + + formats: cabc.Sequence[str] + + def __init__(self, formats: cabc.Sequence[str] | None = None): + self.formats = formats or [ + "%Y-%m-%d", + "%Y-%m-%dT%H:%M:%S", + "%Y-%m-%d %H:%M:%S", + ] + + def to_info_dict(self) -> DateTimeInfoDict: + return {"formats": self.formats, **super().to_info_dict()} + + def get_metavar(self, param: Parameter, ctx: Context) -> str: + return f"[{'|'.join(self.formats)}]" + + def _try_to_convert_date(self, value: t.Any, format: str) -> datetime | None: + try: + return datetime.strptime(value, format) + except ValueError: + return None + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> datetime: + if isinstance(value, datetime): + return value + + for format in self.formats: + converted = self._try_to_convert_date(value, format) + + if converted is not None: + return converted + + formats_str = ", ".join(map(repr, self.formats)) + self.fail( + ngettext( + "{value!r} does not match the format {format}.", + "{value!r} does not match the formats {formats}.", + len(self.formats), + ).format(value=value, format=formats_str, formats=formats_str), + param, + ctx, + ) + + def __repr__(self) -> str: + return "DateTime" + + +class _NumberParamTypeBase( + ParamType[_ValueT_co], t.Generic[_ValueT_contra, _ValueT_co] +): + _number_class: t.Callable[[_ValueT_contra], _ValueT_co] + + def convert( + self, value: _ValueT_contra, param: Parameter | None, ctx: Context | None + ) -> _ValueT_co: + try: + return self._number_class(value) + except ValueError: + self.fail( + _("{value!r} is not a valid {number_type}.").format( + value=value, number_type=self.name + ), + param, + ctx, + ) + + +if t.TYPE_CHECKING: + # on Python 3.10 this will raise a TypeError + + class NumberRangeInfoDict(ParamTypeInfoDict, t.Generic[_FloatValueT_co]): + min: _FloatValueT_co | None + max: _FloatValueT_co | None + min_open: bool + max_open: bool + clamp: bool +else: + + class NumberRangeInfoDict(ParamTypeInfoDict): + min: t.Any | None + max: t.Any | None + min_open: bool + max_open: bool + clamp: bool + + +class _NumberRangeBase( + _NumberParamTypeBase[_ValueT_contra, _FloatValueT_co], + t.Generic[_ValueT_contra, _FloatValueT_co], +): + min: _FloatValueT_co | None + max: _FloatValueT_co | None + min_open: bool + max_open: bool + clamp: bool + + def __init__( + self, + min: _FloatValueT_co | None = None, + max: _FloatValueT_co | None = None, + min_open: bool = False, + max_open: bool = False, + clamp: bool = False, + ) -> None: + self.min = min + self.max = max + self.min_open = min_open + self.max_open = max_open + self.clamp = clamp + + def to_info_dict(self) -> NumberRangeInfoDict[_FloatValueT_co]: + return { + "min": self.min, + "max": self.max, + "min_open": self.min_open, + "max_open": self.max_open, + "clamp": self.clamp, + **super().to_info_dict(), + } + + def convert( + self, value: _ValueT_contra, param: Parameter | None, ctx: Context | None + ) -> _FloatValueT_co: + import operator + + rv = super().convert(value, param, ctx) + min = self.min + max = self.max + lt_min: bool = min is not None and ( + operator.le if self.min_open else operator.lt + )(rv, min) + gt_max: bool = max is not None and ( + operator.ge if self.max_open else operator.gt + )(rv, max) + + if self.clamp: + if min is not None and lt_min: + return self._clamp(min, 1, self.min_open) + + if max is not None and gt_max: + return self._clamp(max, -1, self.max_open) + + if lt_min or gt_max: + self.fail( + _("{value} is not in the range {range}.").format( + value=rv, range=self._describe_range() + ), + param, + ctx, + ) + + return rv + + @abc.abstractmethod + def _clamp( + # Covariant type variables cannot be used in input positions, so we use a + # separate method-scoped type variable instead. + self: _NumberRangeBase[t.Any, _FloatValueT], + bound: _FloatValueT, + dir: t.Literal[1, -1], + open: bool, + ) -> _FloatValueT: + """Find the valid value to clamp to bound in the given + direction. + + :param bound: The boundary value. + :param dir: 1 or -1 indicating the direction to move. + :param open: If true, the range does not include the bound. + """ + ... + + def _describe_range(self) -> str: + """Describe the range for use in help text.""" + if self.min is None: + op = "<" if self.max_open else "<=" + return f"x{op}{self.max}" + + if self.max is None: + op = ">" if self.min_open else ">=" + return f"x{op}{self.min}" + + lop = "<" if self.min_open else "<=" + rop = "<" if self.max_open else "<=" + return f"{self.min}{lop}x{rop}{self.max}" + + def __repr__(self) -> str: + clamp = " clamped" if self.clamp else "" + return f"<{type(self).__name__} {self._describe_range()}{clamp}>" + + +class IntParamType(_NumberParamTypeBase[t.SupportsInt | t.SupportsIndex, int]): + name = "integer" + _number_class = int + + def __repr__(self) -> str: + return "INT" + + +class IntRange(_NumberRangeBase[int, int], IntParamType): + """Restrict an :data:`click.INT` value to a range of accepted + values. See :ref:`ranges`. + + If ``min`` or ``max`` are not passed, any value is accepted in that + direction. If ``min_open`` or ``max_open`` are enabled, the + corresponding boundary is not included in the range. + + If ``clamp`` is enabled, a value outside the range is clamped to the + boundary instead of failing. + + .. versionchanged:: 8.0 + Added the ``min_open`` and ``max_open`` parameters. + """ + + name = "integer range" + + def _clamp(self, bound: int, dir: t.Literal[1, -1], open: bool) -> int: + if not open: + return bound + + return bound + dir + + +class FloatParamType(_NumberParamTypeBase[t.SupportsFloat | t.SupportsIndex, float]): + name = "float" + _number_class = float + + def __repr__(self) -> str: + return "FLOAT" + + +class FloatRange(_NumberRangeBase[float, float], FloatParamType): + """Restrict a :data:`click.FLOAT` value to a range of accepted + values. See :ref:`ranges`. + + If ``min`` or ``max`` are not passed, any value is accepted in that + direction. If ``min_open`` or ``max_open`` are enabled, the + corresponding boundary is not included in the range. + + If ``clamp`` is enabled, a value outside the range is clamped to the + boundary instead of failing. This is not supported if either + boundary is marked ``open``. + + .. versionchanged:: 8.0 + Added the ``min_open`` and ``max_open`` parameters. + """ + + name = "float range" + + def __init__( + self, + min: float | None = None, + max: float | None = None, + min_open: bool = False, + max_open: bool = False, + clamp: bool = False, + ) -> None: + super().__init__( + min=min, max=max, min_open=min_open, max_open=max_open, clamp=clamp + ) + + if (min_open or max_open) and clamp: + raise TypeError("Clamping is not supported for open bounds.") + + def _clamp(self, bound: float, dir: t.Literal[1, -1], open: bool) -> float: + if not open: + return bound + + # Could use math.nextafter here, but clamping an + # open float range doesn't seem to be particularly useful. It's + # left up to the user to write a callback to do it if needed. + raise RuntimeError("Clamping is not supported for open bounds.") + + +class BoolParamType(ParamType[bool]): + name = "boolean" + + bool_states: dict[str, bool] = { + "1": True, + "0": False, + "yes": True, + "no": False, + "true": True, + "false": False, + "on": True, + "off": False, + "t": True, + "f": False, + "y": True, + "n": False, + # Absence of value is considered False. + "": False, + } + """A mapping of string values to boolean states. + + Mapping is inspired by :py:attr:`configparser.ConfigParser.BOOLEAN_STATES` + and extends it. + + .. caution:: + String values are lower-cased, as the ``str_to_bool`` comparison function + below is case-insensitive. + + .. warning:: + The mapping is not exhaustive, and does not cover all possible boolean strings + representations. It will remains as it is to avoid endless bikeshedding. + + Future work my be considered to make this mapping user-configurable from public + API. + """ + + @staticmethod + def str_to_bool(value: str | bool) -> bool | None: + """Convert a string to a boolean value. + + If the value is already a boolean, it is returned as-is. If the value is a + string, it is stripped of whitespaces and lower-cased, then checked against + the known boolean states pre-defined in the `BoolParamType.bool_states` mapping + above. + + Returns `None` if the value does not match any known boolean state. + """ + if isinstance(value, bool): + return value + return BoolParamType.bool_states.get(value.strip().lower()) + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> bool: + normalized = self.str_to_bool(value) + if normalized is None: + self.fail( + _( + "{value!r} is not a valid boolean. Recognized values: {states}" + ).format(value=value, states=", ".join(sorted(self.bool_states))), + param, + ctx, + ) + return normalized + + def __repr__(self) -> str: + return "BOOL" + + +class UUIDParameterType(ParamType[uuid.UUID]): + name = "uuid" + + def convert( + self, value: uuid.UUID | str, param: Parameter | None, ctx: Context | None + ) -> uuid.UUID: + if isinstance(value, uuid.UUID): + return value + + value = value.strip() + + try: + return uuid.UUID(value) + except ValueError: + self.fail( + _("{value!r} is not a valid UUID.").format(value=value), param, ctx + ) + + def __repr__(self) -> str: + return "UUID" + + +class FileInfoDict(ParamTypeInfoDict): + mode: str + encoding: str | None + + +class File(ParamType[t.IO[t.Any]]): + """Declares a parameter to be a file for reading or writing. The file + is automatically closed once the context tears down (after the command + finished working). + + Files can be opened for reading or writing. The special value ``-`` + indicates stdin or stdout depending on the mode. + + By default, the file is opened for reading text data, but it can also be + opened in binary mode or for writing. The encoding parameter can be used + to force a specific encoding. + + The `lazy` flag controls if the file should be opened immediately or upon + first IO. The default is to be non-lazy for standard input and output + streams as well as files opened for reading, `lazy` otherwise. When opening a + file lazily for reading, it is still opened temporarily for validation, but + will not be held open until first IO. lazy is mainly useful when opening + for writing to avoid creating the file until it is needed. + + Files can also be opened atomically in which case all writes go into a + separate file in the same folder and upon completion the file will + be moved over to the original location. This is useful if a file + regularly read by other users is modified. + + See :ref:`file-args` for more information. + + .. versionchanged:: 2.0 + Added the ``atomic`` parameter. + """ + + name = "filename" + envvar_list_splitter: t.ClassVar[str] = os.path.pathsep + + mode: str + encoding: str | None + errors: str | None + lazy: bool | None + atomic: bool + + def __init__( + self, + mode: str = "r", + encoding: str | None = None, + errors: str | None = "strict", + lazy: bool | None = None, + atomic: bool = False, + ) -> None: + self.mode = mode + self.encoding = encoding + self.errors = errors + self.lazy = lazy + self.atomic = atomic + + def to_info_dict(self) -> FileInfoDict: + return { + "mode": self.mode, + "encoding": self.encoding, + **super().to_info_dict(), + } + + def resolve_lazy_flag(self, value: str | os.PathLike[str]) -> bool: + if self.lazy is not None: + return self.lazy + if os.fspath(value) == "-": + return False + elif "w" in self.mode: + return True + return False + + def convert( + self, + value: str | os.PathLike[str] | t.IO[t.Any], + param: Parameter | None, + ctx: Context | None, + ) -> t.IO[t.Any]: + if _is_file_like(value): + return value + + try: + lazy = self.resolve_lazy_flag(value) + + if lazy: + lf = _LazyFile( + value, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + + if ctx is not None: + ctx.call_on_close(lf.close_intelligently) + + return t.cast("t.IO[t.Any]", lf) + + f, should_close = open_stream( + value, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + + # If a context is provided, we automatically close the file + # at the end of the context execution (or flush out). If a + # context does not exist, it's the caller's responsibility to + # properly close the file. This for instance happens when the + # type is used with prompts. + if ctx is not None: + if should_close: + ctx.call_on_close(_safecall(f.close)) + else: + ctx.call_on_close(_safecall(f.flush)) + + return f + except OSError as e: + self.fail( + f"'{format_filename(value)}': {e.strerror}", + param, + ctx, + ) + + def shell_complete( + self, ctx: Context, param: Parameter, incomplete: str + ) -> list[CompletionItem]: + """Return a special completion marker that tells the completion + system to use the shell to provide file path completions. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + return [CompletionItem(incomplete, type="file")] + + +def _is_file_like(value: t.Any) -> te.TypeIs[t.IO[t.Any]]: + return hasattr(value, "read") or hasattr(value, "write") + + +class PathInfoDict(ParamTypeInfoDict): + exists: bool + file_okay: bool + dir_okay: bool + writable: bool + readable: bool + allow_dash: bool + + +class Path(ParamType[str | bytes | os.PathLike[str]]): + """The ``Path`` type is similar to the :class:`File` type, but + returns the filename instead of an open file. Various checks can be + enabled to validate the type of file and permissions. + + :param exists: The file or directory needs to exist for the value to + be valid. If this is not set to ``True``, and the file does not + exist, then all further checks are silently skipped. + :param file_okay: Allow a file as a value. + :param dir_okay: Allow a directory as a value. + :param readable: if true, a readable check is performed. + :param writable: if true, a writable check is performed. + :param executable: if true, an executable check is performed. + :param resolve_path: Make the value absolute and resolve any + symlinks. A ``~`` is not expanded, as this is supposed to be + done by the shell only. + :param allow_dash: Allow a single dash as a value, which indicates + a standard stream (but does not open it). Use + :func:`~click.open_file` to handle opening this value. + :param path_type: Convert the incoming path value to this type. If + ``None``, keep Python's default, which is ``str``. Useful to + convert to :class:`pathlib.Path`. + + .. versionchanged:: 8.1 + Added the ``executable`` parameter. + + .. versionchanged:: 8.0 + Allow passing ``path_type=pathlib.Path``. + + .. versionchanged:: 6.0 + Added the ``allow_dash`` parameter. + """ + + envvar_list_splitter: t.ClassVar[str] = os.path.pathsep + + exists: bool + file_okay: bool + dir_okay: bool + readable: bool + writable: bool + executable: bool + resolve_path: bool + allow_dash: bool + name: str + + def __init__( + self, + exists: bool = False, + file_okay: bool = True, + dir_okay: bool = True, + writable: bool = False, + readable: bool = True, + resolve_path: bool = False, + allow_dash: bool = False, + path_type: type | None = None, + executable: bool = False, + ) -> None: + self.exists = exists + self.file_okay = file_okay + self.dir_okay = dir_okay + self.readable = readable + self.writable = writable + self.executable = executable + self.resolve_path = resolve_path + self.allow_dash = allow_dash + self.type: type | None = path_type + + if self.file_okay and not self.dir_okay: + self.name = _("file") + elif self.dir_okay and not self.file_okay: + self.name = _("directory") + else: + self.name = _("path") + + def to_info_dict(self) -> PathInfoDict: + return { + "exists": self.exists, + "file_okay": self.file_okay, + "dir_okay": self.dir_okay, + "writable": self.writable, + "readable": self.readable, + "allow_dash": self.allow_dash, + **super().to_info_dict(), + } + + def coerce_path_result( + self, value: str | os.PathLike[str] + ) -> str | bytes | os.PathLike[str]: + if self.type is not None and not isinstance(value, self.type): + if self.type is str: + return os.fsdecode(value) + elif self.type is bytes: + return os.fsencode(value) + else: + return t.cast("os.PathLike[str]", self.type(value)) + + return value + + def convert( + self, + value: str | os.PathLike[str], + param: Parameter | None, + ctx: Context | None, + ) -> str | bytes | os.PathLike[str]: + rv = value + + dash = b"-" if isinstance(rv, bytes) else "-" + is_dash = self.file_okay and self.allow_dash and rv == dash + + if not is_dash: + if self.resolve_path: + rv = os.path.realpath(rv) + + try: + st = os.stat(rv) + except OSError: + if not self.exists: + return self.coerce_path_result(rv) + self.fail( + _("{name} {filename!r} does not exist.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + + if not self.file_okay and stat.S_ISREG(st.st_mode): + self.fail( + _("{name} {filename!r} is a file.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + if not self.dir_okay and stat.S_ISDIR(st.st_mode): + self.fail( + _("{name} {filename!r} is a directory.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + + if self.readable and not os.access(rv, os.R_OK): + self.fail( + _("{name} {filename!r} is not readable.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + + if self.writable and not os.access(rv, os.W_OK): + self.fail( + _("{name} {filename!r} is not writable.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + + if self.executable and not os.access(value, os.X_OK): + self.fail( + _("{name} {filename!r} is not executable.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + + return self.coerce_path_result(rv) + + def shell_complete( + self, ctx: Context, param: Parameter, incomplete: str + ) -> list[CompletionItem]: + """Return a special completion marker that tells the completion + system to use the shell to provide path completions for only + directories or any paths. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + type = "dir" if self.dir_okay and not self.file_okay else "file" + return [CompletionItem(incomplete, type=type)] + + +class TupleInfoDict(ParamTypeInfoDict): + types: cabc.Sequence[ParamTypeInfoDict] + + +class Tuple(CompositeParamType[tuple[t.Any, ...]]): + """The default behavior of Click is to apply a type on a value directly. + This works well in most cases, except for when `nargs` is set to a fixed + count and different types should be used for different items. In this + case the :class:`Tuple` type can be used. This type can only be used + if `nargs` is set to a fixed number. + + For more information see :ref:`tuple-type`. + + This can be selected by using a Python tuple literal as a type. + + :param types: a list of types that should be used for the tuple items. + """ + + def __init__(self, types: cabc.Sequence[type[t.Any] | ParamType[t.Any]]) -> None: + self.types: cabc.Sequence[ParamType[t.Any]] = [convert_type(ty) for ty in types] + + def to_info_dict(self) -> TupleInfoDict: + return { + "types": [ty.to_info_dict() for ty in self.types], + **super().to_info_dict(), + } + + @property + def name(self) -> str: # type: ignore[override] + return f"<{' '.join(ty.name for ty in self.types)}>" + + @property + def arity(self) -> int: # type: ignore[override] + return len(self.types) + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> tuple[t.Any, ...]: + len_type = len(self.types) + len_value = len(value) + + if len_value != len_type: + self.fail( + ngettext( + "{len_type} values are required, but {len_value} was given.", + "{len_type} values are required, but {len_value} were given.", + len_value, + ).format(len_type=len_type, len_value=len_value), + param=param, + ctx=ctx, + ) + + return tuple( + ty(x, param, ctx) for ty, x in zip(self.types, value, strict=False) + ) + + +def _guess_type( + ty: type[t.Any] | ParamType[t.Any] | None, + default: t.Any | None, +) -> type[t.Any] | tuple[type[t.Any], ...] | ParamType[t.Any] | None: + """Infer a type from *ty* or *default*. + + Returns *ty* unchanged when it is not ``None``. Otherwise inspects + *default* to produce a ``type``, a ``tuple`` of types (for tuple + defaults), or ``None``. + """ + if ty is not None: + return ty + + if default is None: + return None + + if not isinstance(default, (tuple, list)): + return type(default) + + # If the default is empty, return None so convert_type falls + # through to STRING. + if not default: + return None + + item = default[0] + + # A sequence of iterables needs to detect the inner types. + # Can't call convert_type recursively because that would + # incorrectly unwind the tuple to a single type. + if isinstance(item, (tuple, list)): + return tuple(map(type, item)) + + return type(item) + + +@t.overload +def convert_type(ty: None, default: None = None) -> StringParamType: ... +@t.overload +def convert_type( + ty: type | ParamType[t.Any], default: t.Any | None = None +) -> ParamType[t.Any]: ... +@t.overload +def convert_type( + ty: t.Any | None, default: t.Any | None = None +) -> ParamType[t.Any]: ... +def convert_type( + ty: t.Any | None = None, default: t.Any | None = None +) -> ParamType[t.Any]: + """Find the most appropriate :class:`ParamType` for the given Python + type. If the type isn't provided, it can be inferred from a default + value. + """ + guessed = _guess_type(ty, default) + is_guessed = guessed is not ty + + if isinstance(guessed, tuple): + return Tuple(guessed) + + if isinstance(guessed, ParamType): + return guessed + + if guessed is str or guessed is None: + return STRING + + if guessed is int: + return INT + + if guessed is float: + return FLOAT + + if guessed is bool: + return BOOL + + if is_guessed: + return STRING + + if __debug__: + try: + if issubclass(guessed, ParamType): + raise AssertionError( + f"Attempted to use an uninstantiated parameter type ({guessed})." + ) + except TypeError: + # guessed is an instance (correct), so issubclass fails. + pass + + return FuncParamType(guessed) + + +#: A dummy parameter type that just does nothing. From a user's +#: perspective this appears to just be the same as `STRING` but +#: internally no string conversion takes place if the input was bytes. +#: This is usually useful when working with file paths as they can +#: appear in bytes and unicode. +#: +#: For path related uses the :class:`Path` type is a better choice but +#: there are situations where an unprocessed type is useful which is why +#: it is provided. +#: +#: .. versionadded:: 4.0 +UNPROCESSED: t.Final[UnprocessedParamType] = UnprocessedParamType() + +#: A unicode string parameter type which is the implicit default. This +#: can also be selected by using ``str`` as type. +STRING: t.Final[StringParamType] = StringParamType() + +#: An integer parameter. This can also be selected by using ``int`` as +#: type. +INT: t.Final[IntParamType] = IntParamType() + +#: A floating point value parameter. This can also be selected by using +#: ``float`` as type. +FLOAT: t.Final[FloatParamType] = FloatParamType() + +#: A boolean parameter. This is the default for boolean flags. This can +#: also be selected by using ``bool`` as a type. +BOOL: t.Final[BoolParamType] = BoolParamType() + +#: A UUID parameter. +UUID: t.Final[UUIDParameterType] = UUIDParameterType() + + +class OptionHelpExtra(t.TypedDict, total=False): + envvars: tuple[str, ...] + default: str + range: str + required: str diff --git a/source/target-vendors/cp312/click/utils.py b/source/target-vendors/cp312/click/utils.py new file mode 100644 index 0000000..b529eb0 --- /dev/null +++ b/source/target-vendors/cp312/click/utils.py @@ -0,0 +1,688 @@ +from __future__ import annotations + +import collections.abc as cabc +import os +import re +import sys +import typing as t +from functools import update_wrapper +from gettext import gettext as _ +from types import ModuleType +from types import TracebackType + +from ._compat import _default_text_stderr +from ._compat import _default_text_stdout +from ._compat import _find_binary_writer +from ._compat import binary_streams +from ._compat import open_stream +from ._compat import should_strip_ansi +from ._compat import strip_ansi +from ._compat import text_streams +from ._compat import WIN +from .globals import resolve_color_default + +if t.TYPE_CHECKING: + import typing_extensions as te + + P = te.ParamSpec("P") + +R = t.TypeVar("R") + + +def _posixify(name: str) -> str: + return "-".join(name.split()).lower() + + +def _safecall(func: t.Callable[P, R]) -> t.Callable[P, R | None]: + """Wraps a function so that it swallows exceptions. + + :meta private: + """ + + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None: + try: + return func(*args, **kwargs) + except Exception: + pass + return None + + return update_wrapper(wrapper, func) + + +def make_str(value: t.Any) -> str: + """Converts a value into a valid string.""" + if isinstance(value, bytes): + try: + return value.decode(sys.getfilesystemencoding()) + except UnicodeError: + return value.decode("utf-8", "replace") + return str(value) + + +def _make_default_short_help(help: str, max_length: int = 45) -> str: + """Returns a condensed version of help string. + + :meta private: + """ + # Consider only the first paragraph. + paragraph_end = help.find("\n\n") + + if paragraph_end != -1: + help = help[:paragraph_end] + + # Collapse newlines, tabs, and spaces. + words = help.split() + + if not words: + return "" + + # The first paragraph started with a "no rewrap" marker, ignore it. + if words[0] == "\b": + words = words[1:] + + total_length = 0 + last_index = len(words) - 1 + + for i, word in enumerate(words): + total_length += len(word) + (i > 0) + + if total_length > max_length: # too long, truncate + break + + if word[-1] == ".": # sentence end, truncate without "..." + return " ".join(words[: i + 1]) + + if total_length == max_length and i != last_index: + break # not at sentence end, truncate with "..." + else: + return " ".join(words) # no truncation needed + + # Account for the length of the suffix. + total_length += len("...") + + # remove words until the length is short enough + while i > 0: + total_length -= len(words[i]) + (i > 0) + + if total_length <= max_length: + break + + i -= 1 + + return " ".join(words[:i]) + "..." + + +class _LazyFile: + """A lazy file works like a regular file but it does not fully open + the file but it does perform some basic checks early to see if the + filename parameter does make sense. This is useful for safely opening + files for writing. + + :meta private: + """ + + name: str + mode: str + encoding: str | None + errors: str | None + atomic: bool + _f: t.IO[t.Any] | None + should_close: bool + + def __init__( + self, + filename: str | os.PathLike[str], + mode: str = "r", + encoding: str | None = None, + errors: str | None = "strict", + atomic: bool = False, + ) -> None: + self.name = os.fspath(filename) + self.mode = mode + self.encoding = encoding + self.errors = errors + self.atomic = atomic + + if self.name == "-": + self._f, self.should_close = open_stream(filename, mode, encoding, errors) + else: + if "r" in mode: + # Open and close the file in case we're opening it for + # reading so that we can catch at least some errors in + # some cases early. + open(filename, mode).close() + self._f = None + self.should_close = True + + def __getattr__(self, name: str) -> t.Any: + return getattr(self.open(), name) + + def __repr__(self) -> str: + if self._f is not None: + return repr(self._f) + return f"" + + def open(self) -> t.IO[t.Any]: + """Opens the file if it's not yet open. This call might fail with + a :exc:`FileError`. Not handling this error will produce an error + that Click shows. + """ + if self._f is not None: + return self._f + try: + rv, self.should_close = open_stream( + self.name, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + except OSError as e: + from .exceptions import FileError + + raise FileError(self.name, hint=e.strerror) from e + self._f = rv + return rv + + def close(self) -> None: + """Closes the underlying file, no matter what.""" + if self._f is not None: + self._f.close() + + def close_intelligently(self) -> None: + """This function only closes the file if it was opened by the lazy + file wrapper. For instance this will never close stdin. + """ + if self.should_close: + self.close() + + def __enter__(self) -> _LazyFile: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.close_intelligently() + + def __iter__(self) -> cabc.Iterator[t.AnyStr]: + self.open() + return iter(self._f) # type: ignore + + +class _KeepOpenFile: + """Proxy a file object but keep it open across a ``with`` block. + + Wraps a borrowed file (such as ``sys.stdin`` or ``sys.stdout``) so that + leaving a ``with`` block does not close it, as used by :func:`open_file` + for the ``-`` filename. The caller stays responsible for the file: an + explicit :meth:`close` still passes through to the wrapped object. + + Dunder methods are proxied explicitly: implicit special-method lookups + bypass :meth:`__getattr__`, because Python resolves them on the type rather + than the instance. + + :meta private: + """ + + _file: t.IO[t.Any] + + def __init__(self, file: t.IO[t.Any]) -> None: + self._file = file + + def __getattr__(self, name: str) -> t.Any: + return getattr(self._file, name) + + def __enter__(self) -> _KeepOpenFile: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + pass + + def __repr__(self) -> str: + return repr(self._file) + + def __iter__(self) -> cabc.Iterator[t.AnyStr]: + return iter(self._file) + + +def echo( + message: object = None, + file: t.IO[t.Any] | None = None, + nl: bool = True, + err: bool = False, + color: bool | None = None, +) -> None: + """Print a message and newline to stdout or a file. This should be + used instead of :func:`print` because it provides better support + for different data, files, and environments. + + Compared to :func:`print`, this does the following: + + - Ensures that the output encoding is not misconfigured on Linux. + - Supports Unicode in the Windows console. + - Supports writing to binary outputs, and supports writing bytes + to text outputs. + - Removes ANSI color and style codes if the output does not look + like an interactive terminal. + - Always flushes the output. + + :param message: The string or bytes to output. Other objects are + converted to strings. + :param file: The file to write to. Defaults to ``stdout``. + :param err: Write to ``stderr`` instead of ``stdout``. + :param nl: Print a newline after the message. Enabled by default. + :param color: Force showing or hiding colors and other styles. By + default Click will remove color if the output does not look like + an interactive terminal. + + .. versionchanged:: 8.5.0 + Colorama is no longer used for color on Windows. + + .. versionchanged:: 6.0 + Support Unicode output on the Windows console. Click does not + modify ``sys.stdout``, so ``sys.stdout.write()`` and ``print()`` + will still not support Unicode. + + .. versionchanged:: 4.0 + Added the ``color`` parameter. + + .. versionadded:: 3.0 + Added the ``err`` parameter. + + .. versionchanged:: 2.0 + Support colors on Windows if colorama is installed. + """ + if file is None: + if err: + file = _default_text_stderr() + else: + file = _default_text_stdout() + + # There are no standard streams attached to write to. For example, + # pythonw on Windows. + if file is None: + return + + match message: + case str() | bytes() | bytearray(): + out = message + case None: + out = "" + case _: + out = str(message) + + if nl: + if isinstance(out, str): + out += "\n" + else: + out += b"\n" + + if not out: + file.flush() + return + + # If there is a message and the value looks like bytes, we manually + # need to find the binary stream and write the message in there. + # This is done separately so that most stream types will work as you + # would expect. Eg: you can write to StringIO for other cases. + if isinstance(out, (bytes, bytearray)): + binary_file = _find_binary_writer(file) + if binary_file is not None: + file.flush() + binary_file.write(out) + binary_file.flush() + return + + # ANSI style code support. For no message or bytes, nothing happens. + # When outputting to a file instead of a terminal, strip codes. + elif should_strip_ansi(file, resolve_color_default(color)): + out = strip_ansi(out) + + file.write(out) # type: ignore + file.flush() + + +def _get_binary_stream(name: t.Literal["stdin", "stdout", "stderr"]) -> t.BinaryIO: + """Returns a system stream for byte processing. + + .. deprecated:: 8.5.0 + Will be removed in Click 9.0. + + :param name: the name of the stream to open. Valid names are ``'stdin'``, + ``'stdout'`` and ``'stderr'`` + + :meta private: + """ + opener = binary_streams.get(name) + if opener is None: + raise TypeError(_("Unknown standard stream '{name}'").format(name=name)) + return opener() + + +def _get_text_stream( + name: t.Literal["stdin", "stdout", "stderr"], + encoding: str | None = None, + errors: str | None = "strict", +) -> t.TextIO: + """Returns a system stream for text processing. + + .. deprecated:: 8.5.0 + Will be removed in Click 9.0. + + This usually returns a wrapped stream around a binary stream returned from + :func:`get_binary_stream` but it also can take shortcuts for already + correctly configured streams. + + + :param name: the name of the stream to open. Valid names are ``'stdin'``, + ``'stdout'`` and ``'stderr'`` + :param encoding: overrides the detected default encoding. + :param errors: overrides the default error mode. + :meta private: + """ + opener = text_streams.get(name) + if opener is None: + raise TypeError(_("Unknown standard stream '{name}'").format(name=name)) + return opener(encoding, errors) + + +def open_file( + filename: str | os.PathLike[str], + mode: str = "r", + encoding: str | None = None, + errors: str | None = "strict", + lazy: bool = False, + atomic: bool = False, +) -> t.IO[t.Any]: + """Open a file, with extra behavior to handle ``'-'`` to indicate + a standard stream, lazy open on write, and atomic write. Similar to + the behavior of the :class:`~click.File` param type. + + If ``'-'`` is given to open ``stdout`` or ``stdin``, the stream is + wrapped so that using it in a context manager will not close it. + This makes it possible to use the function without accidentally + closing a standard stream: + + .. code-block:: python + + with open_file(filename) as f: + ... + + :param filename: The name or Path of the file to open, or ``'-'`` for + ``stdin``/``stdout``. + :param mode: The mode in which to open the file. + :param encoding: The encoding to decode or encode a file opened in + text mode. + :param errors: The error handling mode. + :param lazy: Wait to open the file until it is accessed. For read + mode, the file is temporarily opened to raise access errors + early, then closed until it is read again. + :param atomic: Write to a temporary file and replace the given file + on close. + + .. versionadded:: 3.0 + """ + if lazy: + return t.cast( + "t.IO[t.Any]", _LazyFile(filename, mode, encoding, errors, atomic=atomic) + ) + + f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic) + + if not should_close: + f = t.cast("t.IO[t.Any]", _KeepOpenFile(f)) + + return f + + +def format_filename( + filename: str | bytes | os.PathLike[str] | os.PathLike[bytes], + shorten: bool = False, +) -> str: + """Format a filename as a string for display. Ensures the filename can be + displayed by replacing any invalid bytes or surrogate escapes in the name + with the replacement character ``�``. + + Invalid bytes or surrogate escapes will raise an error when written to a + stream with ``errors="strict"``. This will typically happen with ``stdout`` + when the locale is something like ``en_GB.UTF-8``. + + Many scenarios *are* safe to write surrogates though, due to PEP 538 and + PEP 540, including: + + - Writing to ``stderr``, which uses ``errors="backslashreplace"``. + - The system has ``LANG=C.UTF-8``, ``C``, or ``POSIX``. Python opens + stdout and stderr with ``errors="surrogateescape"``. + - None of ``LANG/LC_*`` are set. Python assumes ``LANG=C.UTF-8``. + - Python is started in UTF-8 mode with ``PYTHONUTF8=1`` or ``-X utf8``. + Python opens stdout and stderr with ``errors="surrogateescape"``. + + :param filename: formats a filename for UI display. This will also convert + the filename into unicode without failing. + :param shorten: this optionally shortens the filename to strip of the + path that leads up to it. + """ + if shorten: + filename = os.path.basename(filename) + else: + filename = os.fspath(filename) + + if isinstance(filename, bytes): + filename = filename.decode(sys.getfilesystemencoding(), "replace") + else: + filename = filename.encode("utf-8", "surrogateescape").decode( + "utf-8", "replace" + ) + + return filename + + +def get_app_dir(app_name: str, roaming: bool = True, force_posix: bool = False) -> str: + r"""Returns the config folder for the application. The default behavior + is to return whatever is most appropriate for the operating system. + + To give you an idea, for an app called ``"Foo Bar"``, something like + the following folders could be returned: + + Mac OS X: + ``~/Library/Application Support/Foo Bar`` + Mac OS X (POSIX): + ``~/.foo-bar`` + Unix: + ``~/.config/foo-bar`` + Unix (POSIX): + ``~/.foo-bar`` + Windows (roaming): + ``C:\Users\\AppData\Roaming\Foo Bar`` + Windows (not roaming): + ``C:\Users\\AppData\Local\Foo Bar`` + + .. versionadded:: 2.0 + + :param app_name: the application name. This should be properly capitalized + and can contain whitespace. + :param roaming: controls if the folder should be roaming or not on Windows. + Has no effect otherwise. + :param force_posix: if this is set to `True` then on any POSIX system the + folder will be stored in the home folder with a leading + dot instead of the XDG config home or darwin's + application support folder. + """ + if WIN: + key = "APPDATA" if roaming else "LOCALAPPDATA" + folder = os.environ.get(key) + if folder is None: + folder = os.path.expanduser("~") + return os.path.join(folder, app_name) + if force_posix: + return os.path.join(os.path.expanduser(f"~/.{_posixify(app_name)}")) + if sys.platform == "darwin": + return os.path.join( + os.path.expanduser("~/Library/Application Support"), app_name + ) + return os.path.join( + os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), + _posixify(app_name), + ) + + +class _PacifyFlushWrapper: + """This wrapper is used to catch and suppress BrokenPipeErrors resulting + from ``.flush()`` being called on broken pipe during the shutdown/final-GC + of the Python interpreter. Notably ``.flush()`` is always called on + ``sys.stdout`` and ``sys.stderr``. So as to have minimal impact on any + other cleanup code, and the case where the underlying file is not a broken + pipe, all calls and attributes are proxied. + + :meta private: + """ + + wrapped: t.IO[t.Any] + + def __init__(self, wrapped: t.IO[t.Any]) -> None: + self.wrapped = wrapped + + def flush(self) -> None: + try: + self.wrapped.flush() + except OSError as e: + import errno + + if e.errno != errno.EPIPE: + raise + + def __getattr__(self, attr: str) -> t.Any: + return getattr(self.wrapped, attr) + + +def _detect_program_name( + path: str | None = None, _main: ModuleType | None = None +) -> str: + """Determine the command used to run the program, for use in help + text. If a file or entry point was executed, the file name is + returned. If ``python -m`` was used to execute a module or package, + ``python -m name`` is returned. + + This doesn't try to be too precise, the goal is to give a concise + name for help text. Files are only shown as their name without the + path. ``python`` is only shown for modules, and the full path to + ``sys.executable`` is not shown. + + :param path: The Python file being executed. Python puts this in + ``sys.argv[0]``, which is used by default. + :param _main: The ``__main__`` module. This should only be passed + during internal testing. + + .. versionadded:: 8.0 + Based on command args detection in the Werkzeug reloader. + + :meta private: + """ + if _main is None: + _main = sys.modules["__main__"] + + if not path: + path = sys.argv[0] + + # The value of __package__ indicates how Python was called. It may + # not exist if a setuptools script is installed as an egg. It may be + # set incorrectly for entry points created with pip on Windows. + # It is set to "" inside a Shiv or PEX zipapp. + if getattr(_main, "__package__", None) in {None, ""} or ( + os.name == "nt" + and _main.__package__ == "" + and not os.path.exists(path) + and os.path.exists(f"{path}.exe") + ): + # Executed a file, like "python app.py". + return os.path.basename(path) + + # Executed a module, like "python -m example". + # Rewritten by Python from "-m script" to "/path/to/script.py". + # Need to look at main module to determine how it was executed. + py_module = t.cast(str, _main.__package__) + name = os.path.splitext(os.path.basename(path))[0] + + # A submodule like "example.cli". + if name != "__main__": + py_module = f"{py_module}.{name}" + + return f"python -m {py_module.lstrip('.')}" + + +def _expand_args( + args: cabc.Iterable[str], + *, + user: bool = True, + env: bool = True, + glob_recursive: bool = True, +) -> list[str]: + """Simulate Unix shell expansion with Python functions. + + See :func:`glob.glob`, :func:`os.path.expanduser`, and + :func:`os.path.expandvars`. + + This is intended for use on Windows, where the shell does not do any + expansion. It may not exactly match what a Unix shell would do. + + :param args: List of command line arguments to expand. + :param user: Expand user home directory. + :param env: Expand environment variables. + :param glob_recursive: ``**`` matches directories recursively. + + .. versionchanged:: 8.1 + Invalid glob patterns are treated as empty expansions rather + than raising an error. + + .. versionadded:: 8.0 + + :meta private: + """ + from glob import glob + + out = [] + + for arg in args: + if user: + arg = os.path.expanduser(arg) + + if env: + arg = os.path.expandvars(arg) + + try: + matches = glob(arg, recursive=glob_recursive) + except re.error: + matches = [] + + if not matches: + out.append(arg) + else: + out.extend(matches) + + return out + + +def __getattr__(name: str) -> object: + import warnings + + if name in { + "LazyFile", + "KeepOpenFile", + "make_default_short_help", + "PacifyFlushWrapper", + "safecall", + "get_text_stream", + "get_binary_stream", + }: + warnings.warn( + f"'click.utils.{name}' is deprecated and will be removed in Click 9.0.", + DeprecationWarning, + stacklevel=2, + ) + return globals()[f"_{name}"] + + raise AttributeError(name) diff --git a/source/target-vendors/cp312/cryptography-50.0.1.dist-info/METADATA b/source/target-vendors/cp312/cryptography-50.0.1.dist-info/METADATA new file mode 100644 index 0000000..530b398 --- /dev/null +++ b/source/target-vendors/cp312/cryptography-50.0.1.dist-info/METADATA @@ -0,0 +1,110 @@ +Metadata-Version: 2.4 +Name: cryptography +Version: 50.0.1 +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Natural Language :: English +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: POSIX +Classifier: Operating System :: POSIX :: BSD +Classifier: Operating System :: POSIX :: Linux +Classifier: Operating System :: Microsoft :: Windows +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable +Classifier: Topic :: Security :: Cryptography +Requires-Dist: cffi>=2.0.0 ; platform_python_implementation != 'PyPy' +Requires-Dist: typing-extensions>=4.13.2 ; python_full_version < '3.11' +Requires-Dist: bcrypt>=3.1.5 ; extra == 'ssh' +Provides-Extra: ssh +License-File: LICENSE +License-File: LICENSE.APACHE +License-File: LICENSE.BSD +Summary: cryptography is a package which provides cryptographic recipes and primitives to Python developers. +Author-email: The Python Cryptographic Authority and individual contributors +License-Expression: Apache-2.0 OR BSD-3-Clause +Requires-Python: >=3.9, !=3.9.0, !=3.9.1 +Description-Content-Type: text/x-rst; charset=UTF-8 +Project-URL: changelog, https://cryptography.io/en/latest/changelog/ +Project-URL: documentation, https://cryptography.io/ +Project-URL: homepage, https://github.com/pyca/cryptography +Project-URL: issues, https://github.com/pyca/cryptography/issues +Project-URL: source, https://github.com/pyca/cryptography/ + +pyca/cryptography +================= + +.. image:: https://img.shields.io/pypi/v/cryptography.svg + :target: https://pypi.org/project/cryptography/ + :alt: Latest Version + +.. image:: https://readthedocs.org/projects/cryptography/badge/?version=latest + :target: https://cryptography.io + :alt: Latest Docs + +.. image:: https://github.com/pyca/cryptography/actions/workflows/ci.yml/badge.svg + :target: https://github.com/pyca/cryptography/actions/workflows/ci.yml?query=branch%3Amain + +``cryptography`` is a package which provides cryptographic recipes and +primitives to Python developers. Our goal is for it to be your "cryptographic +standard library". It supports Python 3.9+ and PyPy3 7.3.11+. + +``cryptography`` includes both high level recipes and low level interfaces to +common cryptographic algorithms such as symmetric ciphers, message digests, and +key derivation functions. For example, to encrypt something with +``cryptography``'s high level symmetric encryption recipe: + +.. code-block:: pycon + + >>> from cryptography.fernet import Fernet + >>> # Put this somewhere safe! + >>> key = Fernet.generate_key() + >>> f = Fernet(key) + >>> token = f.encrypt(b"A really secret message. Not for prying eyes.") + >>> token + b'...' + >>> f.decrypt(token) + b'A really secret message. Not for prying eyes.' + +You can find more information in the `documentation`_. + +You can install ``cryptography`` with: + +.. code-block:: console + + $ pip install cryptography + +For full details see `the installation documentation`_. + +Discussion +~~~~~~~~~~ + +If you run into bugs, you can file them in our `issue tracker`_. + +We maintain a `cryptography-dev`_ mailing list for development discussion. + +You can also join ``#pyca`` on ``irc.libera.chat`` to ask questions or get +involved. + +Security +~~~~~~~~ + +Need to report a security issue? Please consult our `security reporting`_ +documentation. + + +.. _`documentation`: https://cryptography.io/ +.. _`the installation documentation`: https://cryptography.io/en/latest/installation/ +.. _`issue tracker`: https://github.com/pyca/cryptography/issues +.. _`cryptography-dev`: https://mail.python.org/mailman/listinfo/cryptography-dev +.. _`security reporting`: https://cryptography.io/en/latest/security/ + diff --git a/source/target-vendors/cp312/cryptography-50.0.1.dist-info/RECORD b/source/target-vendors/cp312/cryptography-50.0.1.dist-info/RECORD new file mode 100644 index 0000000..5a9bb03 --- /dev/null +++ b/source/target-vendors/cp312/cryptography-50.0.1.dist-info/RECORD @@ -0,0 +1,120 @@ +cryptography/__about__.py,sha256=SUJRVMWtZoUu-WdZLfY7p_s2xm5YQjNLFj80YrUP9F0,445 +cryptography/__init__.py,sha256=mthuUrTd4FROCpUYrTIqhjz6s6T9djAZrV7nZ1oMm2o,364 +cryptography/cobblestone.py,sha256=T_-F08-AUf3ljaypZeklNTV1hWObH57oo45mVSzk9wc,693 +cryptography/exceptions.py,sha256=835EWILc2fwxw-gyFMriciC2SqhViETB10LBSytnDIc,1087 +cryptography/fernet.py,sha256=2sLCPOmejhOVbudcS3QsM1PIuHbxCeRcRtWkncQ3mCo,7687 +cryptography/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +cryptography/utils.py,sha256=Ia7S0QZzS0ezwpIHQfY8G1e-o4GjrHijEXXjwAr1iuI,4635 +cryptography/hazmat/__init__.py,sha256=5IwrLWrVp0AjEr_4FdWG_V057NSJGY_W4egNNsuct0g,455 +cryptography/hazmat/_oid.py,sha256=Ohe2nUE6XjKx1UF6TAsGUy2v8GUJ-Vi9DS4yxSukt5U,18150 +cryptography/hazmat/asn1/__init__.py,sha256=YqYToHGFjLOKigRC00HGj6hrjEwbu1SPpAiu6-u-fao,812 +cryptography/hazmat/asn1/asn1.py,sha256=9mcfrmdvK7KtjhRYofMzAjW0u7nvqbfsvPfbhNQK1BQ,19152 +cryptography/hazmat/backends/__init__.py,sha256=O5jvKFQdZnXhKeqJ-HtulaEL9Ni7mr1mDzZY5kHlYhI,361 +cryptography/hazmat/backends/openssl/__init__.py,sha256=p3jmJfnCag9iE5sdMrN6VvVEu55u46xaS_IjoI0SrmA,305 +cryptography/hazmat/backends/openssl/backend.py,sha256=0NczFbiwGxfq9qi28oHYr08r_r7VQg4AXHjb0oFPwqA,10475 +cryptography/hazmat/bindings/__init__.py,sha256=wHCbWfaefa-fk6THSw9th9fJUsStJo7245wfFBqmduA,216 +cryptography/hazmat/bindings/_rust.abi3.so,sha256=PWQjZkv_6Sb9kdGRPRxd3aXDHuf_HK4G_ItxxHyVAaE,14389392 +cryptography/hazmat/bindings/_rust/__init__.pyi,sha256=uUnnvOq_uameYnBNpmsv3SRAVO0WQGpjI58aR1lL4Kw,2285 +cryptography/hazmat/bindings/_rust/_openssl.pyi,sha256=T_5TpYO8bghIPjAQ3BtAABqw-CcYskHp92wm5PNY6dc,228 +cryptography/hazmat/bindings/_rust/asn1.pyi,sha256=BrGjC8J6nwuS-r3EVcdXJB8ndotfY9mbQYOfpbPG0HA,354 +cryptography/hazmat/bindings/_rust/cobblestone.pyi,sha256=9lUf1W1hTrszEJ4YpScJNIPSD9BOM7o-C82-mtSDG9o,1325 +cryptography/hazmat/bindings/_rust/declarative_asn1.pyi,sha256=MBmOZzLTdmPJ6-1K6Aq3PTuHJ1MGgRfIu0hIgag7ocE,3866 +cryptography/hazmat/bindings/_rust/exceptions.pyi,sha256=exXr2xw_0pB1kk93cYbM3MohbzoUkjOms1ZMUi0uQZE,640 +cryptography/hazmat/bindings/_rust/ocsp.pyi,sha256=VPVWuKHI9EMs09ZLRYAGvR0Iz0mCMmEzXAkgJHovpoM,4020 +cryptography/hazmat/bindings/_rust/pkcs12.pyi,sha256=vEEd5wDiZvb8ZGFaziLCaWLzAwoG_tvPUxLQw5_uOl8,1605 +cryptography/hazmat/bindings/_rust/pkcs7.pyi,sha256=txGBJijqZshEcqra6byPNbnisIdlxzOSIHP2hl9arPs,1601 +cryptography/hazmat/bindings/_rust/test_support.pyi,sha256=PPhld-WkO743iXFPebeG0LtgK0aTzGdjcIsay1Gm5GE,757 +cryptography/hazmat/bindings/_rust/x509.pyi,sha256=LVT4htBNse5gijQh4GhVNYEbAO0E1ANgE28tWqw3g4I,10211 +cryptography/hazmat/bindings/_rust/openssl/__init__.pyi,sha256=O6GOyvDxt6ylicB2aFGuu5RcGZy9UjKnYqGR8rqzvdw,1578 +cryptography/hazmat/bindings/_rust/openssl/aead.pyi,sha256=7d--xdc0vuzRe4S2uuQaGm-9GGKv4NcSLQcLyWcaD6s,4582 +cryptography/hazmat/bindings/_rust/openssl/ciphers.pyi,sha256=LhPzHWSXJq4grAJXn6zSvSSdV-aYIIscHDwIPlJGGPs,1315 +cryptography/hazmat/bindings/_rust/openssl/cmac.pyi,sha256=nPH0X57RYpsAkRowVpjQiHE566ThUTx7YXrsadmrmHk,564 +cryptography/hazmat/bindings/_rust/openssl/dh.pyi,sha256=Z3TC-G04-THtSdAOPLM1h2G7ml5bda1ElZUcn5wpuhk,1564 +cryptography/hazmat/bindings/_rust/openssl/dsa.pyi,sha256=qBtkgj2albt2qFcnZ9UDrhzoNhCVO7HTby5VSf1EXMI,1299 +cryptography/hazmat/bindings/_rust/openssl/ec.pyi,sha256=zJy0pRa5n-_p2dm45PxECB_-B6SVZyNKfjxFDpPqT38,1691 +cryptography/hazmat/bindings/_rust/openssl/ed25519.pyi,sha256=VXfXd5G6hUivg399R1DYdmW3eTb0EebzDTqjRC2gaRw,532 +cryptography/hazmat/bindings/_rust/openssl/ed448.pyi,sha256=Yx49lqdnjsD7bxiDV1kcaMrDktug5evi5a6zerMiy2s,514 +cryptography/hazmat/bindings/_rust/openssl/hashes.pyi,sha256=bFe3T13UeyRInUJMD-R5ArGTGLqWDiWo2Lv2KGzUbf4,1076 +cryptography/hazmat/bindings/_rust/openssl/hmac.pyi,sha256=BXZn7NDjL3JAbYW0SQ8pg1iyC5DbQXVhUAiwsi8DFR8,702 +cryptography/hazmat/bindings/_rust/openssl/hpke.pyi,sha256=p_hGLn6YH-EarJF1V5bUsUtjipviEApcR5O0sUHJLtc,2914 +cryptography/hazmat/bindings/_rust/openssl/kdf.pyi,sha256=HiiLdEB9nqMsTvMwiFppQpfPz6fHUz9_gpjEB_XAOXA,6619 +cryptography/hazmat/bindings/_rust/openssl/keys.pyi,sha256=teIt8M6ZEMJrn4s3W0UnW0DZ-30Jd68WnSsKKG124l0,912 +cryptography/hazmat/bindings/_rust/openssl/keywrap.pyi,sha256=mGCJEoQ4lxi4CROTBEuUzb9U4ky8n52L3iDvCOXNBRI,733 +cryptography/hazmat/bindings/_rust/openssl/mldsa.pyi,sha256=H84XDW0TQ3LHhdGJD5uPaDT5ijEiK6MZytXYc8rBNf4,1421 +cryptography/hazmat/bindings/_rust/openssl/mlkem.pyi,sha256=byoeus4-lIOlWlEiRQdCc00qCkXpOB5peBY6yo9ik8s,835 +cryptography/hazmat/bindings/_rust/openssl/poly1305.pyi,sha256=_SW9NtQ5FDlAbdclFtWpT4lGmxKIKHpN-4j8J2BzYfQ,585 +cryptography/hazmat/bindings/_rust/openssl/rsa.pyi,sha256=2OQCNSXkxgc-3uw1xiCCloIQTV6p9_kK79Yu0rhZgPc,1364 +cryptography/hazmat/bindings/_rust/openssl/x25519.pyi,sha256=ewn4GpQyb7zPwE-ni7GtyQgMC0A1mLuqYsSyqv6nI_s,523 +cryptography/hazmat/bindings/_rust/openssl/x448.pyi,sha256=juTZTmli8jO_5Vcufg-vHvx_tCyezmSLIh_9PU3TczI,505 +cryptography/hazmat/bindings/openssl/__init__.py,sha256=wHCbWfaefa-fk6THSw9th9fJUsStJo7245wfFBqmduA,216 +cryptography/hazmat/bindings/openssl/_conditional.py,sha256=vfzi-xHdStAcJQjLqqOlSos94L8D_tqFtBe7Ovw-KtY,5611 +cryptography/hazmat/bindings/openssl/binding.py,sha256=FsuLxbTnrqW60-dAWtakgufeq7TnqaQai1Pc7Dowrjo,3706 +cryptography/hazmat/decrepit/__init__.py,sha256=wHCbWfaefa-fk6THSw9th9fJUsStJo7245wfFBqmduA,216 +cryptography/hazmat/decrepit/ciphers/__init__.py,sha256=wHCbWfaefa-fk6THSw9th9fJUsStJo7245wfFBqmduA,216 +cryptography/hazmat/decrepit/ciphers/algorithms.py,sha256=WSPjM2sXfrLDXrtN5R7N6rA1XF7b93I1e_lvD4yx2oA,3752 +cryptography/hazmat/decrepit/ciphers/modes.py,sha256=Oq_PEwCke5OLczOfr_vzAOJ6wPx-rlsvHAXPBuh5b9o,1649 +cryptography/hazmat/primitives/__init__.py,sha256=wHCbWfaefa-fk6THSw9th9fJUsStJo7245wfFBqmduA,216 +cryptography/hazmat/primitives/_asymmetric.py,sha256=RhgcouUB6HTiFDBrR1LxqkMjpUxIiNvQ1r_zJjRG6qQ,532 +cryptography/hazmat/primitives/_cipheralgorithm.py,sha256=Eh3i7lwedHfi0eLSsH93PZxQKzY9I6lkK67vL4V5tOc,1522 +cryptography/hazmat/primitives/_modes.py,sha256=_EiAOD8Jb6WpFXluwkcUT3ECz_TH8xjGvbQlGkNm59Q,3075 +cryptography/hazmat/primitives/_serialization.py,sha256=hi0xJblBAZ8pmZx2lWa-lruphxvvvG3g9DNK0G8Odfs,4440 +cryptography/hazmat/primitives/cmac.py,sha256=sz_s6H_cYnOvx-VNWdIKhRhe3Ymp8z8J0D3CBqOX3gg,338 +cryptography/hazmat/primitives/constant_time.py,sha256=xdunWT0nf8OvKdcqUhhlFKayGp4_PgVJRU2W1wLSr_A,422 +cryptography/hazmat/primitives/hashes.py,sha256=Srtwgc3_pFvZfIhRj9e1p4Wdi-F_PpleGHlqJARgtOg,7916 +cryptography/hazmat/primitives/hmac.py,sha256=RpB3z9z5skirCQrm7zQbtnp9pLMnAjrlTUvKqF5aDDc,423 +cryptography/hazmat/primitives/hpke.py,sha256=RsHissC5l-dTPn1p2JbcrIrAnDBrLqM5ugBFSGpmKu4,865 +cryptography/hazmat/primitives/keywrap.py,sha256=t9t8KZuzZ4Us30ASUhI27MZPPPKy4EERRDkAK-lM7TQ,735 +cryptography/hazmat/primitives/padding.py,sha256=QT-U-NvV2eQGO1wVPbDiNGNSc9keRDS-ig5cQOrLz0E,1865 +cryptography/hazmat/primitives/poly1305.py,sha256=P5EPQV-RB_FJPahpg01u0Ts4S_PnAmsroxIGXbGeRRo,355 +cryptography/hazmat/primitives/asymmetric/__init__.py,sha256=wHCbWfaefa-fk6THSw9th9fJUsStJo7245wfFBqmduA,216 +cryptography/hazmat/primitives/asymmetric/dh.py,sha256=Rnt2fLaDpQEjQSX8fNtlmTXOJHjlt_DKQZ1Vlfhdja4,5793 +cryptography/hazmat/primitives/asymmetric/dsa.py,sha256=kQzTViqAI9PiELNOBco_n6MfP3N9ehIPkSduOpupt0M,4482 +cryptography/hazmat/primitives/asymmetric/ec.py,sha256=LklVCLLxbbQzqiE6HRaYTRauLMphYUw3tE6c8oBIxYQ,10183 +cryptography/hazmat/primitives/asymmetric/ed25519.py,sha256=mbffLvs-3PgCDeg1rQXVXh7GLntlVL4ATjkIEc4pt5M,2998 +cryptography/hazmat/primitives/asymmetric/ed448.py,sha256=n0dK6y-VLxuw-zvrDxuPT4598hymgX-1Ry_jq9aUQhE,4002 +cryptography/hazmat/primitives/asymmetric/mldsa.py,sha256=LHMaaU0m66EsSrSodWf4e2kdmdDRTEGRC1y-fCKxGK0,13803 +cryptography/hazmat/primitives/asymmetric/mlkem.py,sha256=wvVkeFT-4of-VcUAER2-zI_5hXT6rwTJPMCQ8gF9GTA,7901 +cryptography/hazmat/primitives/asymmetric/padding.py,sha256=jbgajvGHYwkPjMDljHu5lxMBlDfmCxWUe2AF2S6KE10,3678 +cryptography/hazmat/primitives/asymmetric/rsa.py,sha256=lIE7lGe449W5URLTKCpvFGerITouKoZV9GwuTzScsSo,8492 +cryptography/hazmat/primitives/asymmetric/types.py,sha256=SjJhTrnFRLlTPdYrBD1poMi6cjYG-fh0fSIaQn3QUiw,2588 +cryptography/hazmat/primitives/asymmetric/utils.py,sha256=Qs8Re9GFPjW_tNp_73IeJEjPCf0slOIsWOxo6qymT6k,821 +cryptography/hazmat/primitives/asymmetric/x25519.py,sha256=lQcgUk-Piubj8ynFNWQeYme3tYZMgHQqkRKkIDOHLzo,3888 +cryptography/hazmat/primitives/asymmetric/x448.py,sha256=b37ig7k7poG6SJDry42j-ElCj5q4eQ0X5CFmN0Sn8z8,3913 +cryptography/hazmat/primitives/ciphers/__init__.py,sha256=eyEXmjk6_CZXaOPYDr7vAYGXr29QvzgWL2-4CSolLFs,680 +cryptography/hazmat/primitives/ciphers/aead.py,sha256=Fzlyx7w8KYQakzDp1zWgJnIr62zgZrgVh1u2h4exB54,634 +cryptography/hazmat/primitives/ciphers/algorithms.py,sha256=IGfCycYJOh1TvoI34rfw8KfJ_xmt1p_DpBht2Fghceo,3496 +cryptography/hazmat/primitives/ciphers/base.py,sha256=aBC7HHBBoixebmparVr0UlODs3VD0A7B6oz_AaRjDv8,4253 +cryptography/hazmat/primitives/ciphers/modes.py,sha256=H7AZSgJiSTBCi0grL97NxakS950JyFKYLsra8tjHPxQ,6003 +cryptography/hazmat/primitives/kdf/__init__.py,sha256=v3yiYBGU272EojNXbwfYZdbbfI9cVOCCG3nXhTDda3k,1037 +cryptography/hazmat/primitives/kdf/argon2.py,sha256=ZJx-enUlAA4o7b46C1wlzG_XAQQW0wIkFgUKkrM_wA8,632 +cryptography/hazmat/primitives/kdf/concatkdf.py,sha256=BFnOKS72txKF0yQvASr0Na7uhzvLHpUCckK5iNAC_aQ,591 +cryptography/hazmat/primitives/kdf/hkdf.py,sha256=M0lAEfRoc4kpp4-nwDj9yB-vNZukIOYEQrUlWsBNn9o,543 +cryptography/hazmat/primitives/kdf/kbkdf.py,sha256=C3w99vjFqhqzAxzFOH4zzvVoHkb5AC6Lz6AEQGV7d_g,737 +cryptography/hazmat/primitives/kdf/pbkdf2.py,sha256=Xchkk99s-Mk6b6KSCouqdk8FVyiNVOSFIenmnW3tLgQ,468 +cryptography/hazmat/primitives/kdf/scrypt.py,sha256=XyWUdUUmhuI9V6TqAPOvujCSMGv1XQdg0a21IWCmO-U,590 +cryptography/hazmat/primitives/kdf/x963kdf.py,sha256=QKjhRehuTsAstL384bKfvjuv1yfdamAHVhsBRWen2Vw,456 +cryptography/hazmat/primitives/serialization/__init__.py,sha256=fXcu49ugL9rxhpRl_wZjnMqE9DCkhCMcsRxfj_qTveg,2217 +cryptography/hazmat/primitives/serialization/base.py,sha256=SABrvo0S86QcoBlj7DpjeLWy2lzQMryiioJ6DrEYQVA,651 +cryptography/hazmat/primitives/serialization/pkcs12.py,sha256=mS9cFNG4afzvseoc5e1MWoY2VskfL8N8Y_OFjl67luY,5104 +cryptography/hazmat/primitives/serialization/pkcs7.py,sha256=mFM7OFuZ8cCxUGUoo4Cof6BnCHc_Ibx1AQjjfxnPlxo,13998 +cryptography/hazmat/primitives/serialization/ssh.py,sha256=FisXe_nUKdPGfqENVhKpmoazmaI8qH8Ge-VGbc0dykw,53592 +cryptography/hazmat/primitives/twofactor/__init__.py,sha256=tmMZGB-g4IU1r7lIFqASU019zr0uPp_wEBYcwdDCKCA,258 +cryptography/hazmat/primitives/twofactor/hotp.py,sha256=ivZo5BrcCGWLsqql4nZV0XXCjyGPi_iHfDFltGlOJwk,3256 +cryptography/hazmat/primitives/twofactor/totp.py,sha256=m5LPpRL00kp4zY8gTjr55Hfz9aMlPS53kHmVkSQCmdY,1652 +cryptography/x509/__init__.py,sha256=0KVNTmRtdYNX_i_ewsTNzbTm2e1qyfoOhc4emog5hL4,8253 +cryptography/x509/base.py,sha256=UIMKkwmsx10BR3_gnBRBCBTwQp1-O8wkfVm7azH-_mE,28311 +cryptography/x509/certificate_transparency.py,sha256=JqoOIDhlwInrYMFW6IFn77WJ0viF-PB_rlZV3vs9MYc,797 +cryptography/x509/extensions.py,sha256=-iXcwWnIL1zl5Ml3BVJFkEo_71ncXe84ehvqzW1sK4M,77975 +cryptography/x509/general_name.py,sha256=sP_rV11Qlpsk4x3XXGJY_Mv0Q_s9dtjeLckHsjpLQoQ,7836 +cryptography/x509/name.py,sha256=oI2w6VdY8zgw7qpNhlxEx512W0AhWDXQrDYf5EsM2PY,15566 +cryptography/x509/ocsp.py,sha256=Yey6NdFV1MPjop24Mj_VenjEpg3kUaMopSWOK0AbeBs,12699 +cryptography/x509/oid.py,sha256=BUzgXXGVWilkBkdKPTm9R4qElE9gAGHgdYPMZAp7PJo,931 +cryptography/x509/verification.py,sha256=gR2C2c-XZQtblZhT5T5vjSKOtCb74ef2alPVmEcwFlM,958 +cryptography-50.0.1.dist-info/METADATA,sha256=jptVcNpGhoSO5ca7g__8IkvN35eTE2cx3QUM4MBf_-I,4343 +cryptography-50.0.1.dist-info/WHEEL,sha256=ObQlzmyJjoP4uM3utl2uaQ06ufI4LKnDhg53v200B3o,109 +cryptography-50.0.1.dist-info/RECORD,, +cryptography-50.0.1.dist-info/licenses/LICENSE,sha256=Pgx8CRqUi4JTO6mP18u0BDLW8amsv4X1ki0vmak65rs,197 +cryptography-50.0.1.dist-info/licenses/LICENSE.APACHE,sha256=qsc7MUj20dcRHbyjIJn2jSbGRMaBOuHk8F9leaomY_4,11360 +cryptography-50.0.1.dist-info/licenses/LICENSE.BSD,sha256=YCxMdILeZHndLpeTzaJ15eY9dz2s0eymiSMqtwCPtPs,1532 +cryptography-50.0.1.dist-info/sboms/cryptography-rust.cyclonedx.json,sha256=m9rCnuTZv9tCQeBPD9vRLlti_jFwa9OovtKMjK_LVaA,45685 +cryptography-50.0.1.dist-info/sboms/sbom.json,sha256=S9XP7BDLleOELcWlLWOrYuGy3RtuaXtFh5A-yN5T9yA,1206 diff --git a/source/target-vendors/cp312/cryptography-50.0.1.dist-info/WHEEL b/source/target-vendors/cp312/cryptography-50.0.1.dist-info/WHEEL new file mode 100644 index 0000000..08b80b3 --- /dev/null +++ b/source/target-vendors/cp312/cryptography-50.0.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: maturin (1.14.1) +Root-Is-Purelib: false +Tag: cp311-abi3-manylinux_2_34_x86_64 + diff --git a/source/target-vendors/cp312/cryptography-50.0.1.dist-info/licenses/LICENSE b/source/target-vendors/cp312/cryptography-50.0.1.dist-info/licenses/LICENSE new file mode 100644 index 0000000..b11f379 --- /dev/null +++ b/source/target-vendors/cp312/cryptography-50.0.1.dist-info/licenses/LICENSE @@ -0,0 +1,3 @@ +This software is made available under the terms of *either* of the licenses +found in LICENSE.APACHE or LICENSE.BSD. Contributions to cryptography are made +under the terms of *both* these licenses. diff --git a/source/target-vendors/cp312/cryptography-50.0.1.dist-info/licenses/LICENSE.APACHE b/source/target-vendors/cp312/cryptography-50.0.1.dist-info/licenses/LICENSE.APACHE new file mode 100644 index 0000000..62589ed --- /dev/null +++ b/source/target-vendors/cp312/cryptography-50.0.1.dist-info/licenses/LICENSE.APACHE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/source/target-vendors/cp312/cryptography-50.0.1.dist-info/licenses/LICENSE.BSD b/source/target-vendors/cp312/cryptography-50.0.1.dist-info/licenses/LICENSE.BSD new file mode 100644 index 0000000..ec1a29d --- /dev/null +++ b/source/target-vendors/cp312/cryptography-50.0.1.dist-info/licenses/LICENSE.BSD @@ -0,0 +1,27 @@ +Copyright (c) Individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of PyCA Cryptography nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/source/target-vendors/cp312/cryptography-50.0.1.dist-info/sboms/cryptography-rust.cyclonedx.json b/source/target-vendors/cp312/cryptography-50.0.1.dist-info/sboms/cryptography-rust.cyclonedx.json new file mode 100644 index 0000000..e7df0f2 --- /dev/null +++ b/source/target-vendors/cp312/cryptography-50.0.1.dist-info/sboms/cryptography-rust.cyclonedx.json @@ -0,0 +1,1365 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "urn:uuid:8937a838-8d73-4fdd-9a20-bac2da7b0240", + "metadata": { + "timestamp": "2026-08-25T19:33:44.118311124Z", + "tools": [ + { + "vendor": "CycloneDX", + "name": "cargo-cyclonedx", + "version": "0.5.9" + } + ], + "authors": [ + { + "name": "The cryptography developers", + "email": "cryptography-dev@python.org" + } + ], + "component": { + "type": "library", + "bom-ref": "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust#cryptography-rust@0.50.1", + "author": "The cryptography developers ", + "name": "cryptography-rust", + "version": "0.50.1", + "scope": "required", + "licenses": [ + { + "expression": "Apache-2.0 OR BSD-3-Clause" + } + ], + "purl": "pkg:cargo/cryptography-rust@0.50.1?download_url=file://.", + "components": [ + { + "type": "library", + "bom-ref": "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust#cryptography-rust@0.50.1 bin-target-0", + "name": "cryptography_rust", + "version": "0.50.1", + "purl": "pkg:cargo/cryptography-rust@0.50.1?download_url=file://.#src/lib.rs" + } + ] + }, + "properties": [ + { + "name": "cdx:rustc:sbom:target:all_targets", + "value": "true" + } + ] + }, + "components": [ + { + "type": "library", + "bom-ref": "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-cffi#0.50.1", + "author": "The cryptography developers ", + "name": "cryptography-cffi", + "version": "0.50.1", + "scope": "required", + "licenses": [ + { + "expression": "Apache-2.0 OR BSD-3-Clause" + } + ], + "purl": "pkg:cargo/cryptography-cffi@0.50.1?download_url=file://cryptography-cffi" + }, + { + "type": "library", + "bom-ref": "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-crypto#0.50.1", + "author": "The cryptography developers ", + "name": "cryptography-crypto", + "version": "0.50.1", + "scope": "required", + "licenses": [ + { + "expression": "Apache-2.0 OR BSD-3-Clause" + } + ], + "purl": "pkg:cargo/cryptography-crypto@0.50.1?download_url=file://cryptography-crypto" + }, + { + "type": "library", + "bom-ref": "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-keepalive#0.50.1", + "author": "The cryptography developers ", + "name": "cryptography-keepalive", + "version": "0.50.1", + "scope": "required", + "licenses": [ + { + "expression": "Apache-2.0 OR BSD-3-Clause" + } + ], + "purl": "pkg:cargo/cryptography-keepalive@0.50.1?download_url=file://cryptography-keepalive" + }, + { + "type": "library", + "bom-ref": "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-key-parsing#0.50.1", + "author": "The cryptography developers ", + "name": "cryptography-key-parsing", + "version": "0.50.1", + "scope": "required", + "licenses": [ + { + "expression": "Apache-2.0 OR BSD-3-Clause" + } + ], + "purl": "pkg:cargo/cryptography-key-parsing@0.50.1?download_url=file://cryptography-key-parsing" + }, + { + "type": "library", + "bom-ref": "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-openssl#0.50.1", + "author": "The cryptography developers ", + "name": "cryptography-openssl", + "version": "0.50.1", + "scope": "required", + "licenses": [ + { + "expression": "Apache-2.0 OR BSD-3-Clause" + } + ], + "purl": "pkg:cargo/cryptography-openssl@0.50.1?download_url=file://cryptography-openssl" + }, + { + "type": "library", + "bom-ref": "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-x509#0.50.1", + "author": "The cryptography developers ", + "name": "cryptography-x509", + "version": "0.50.1", + "scope": "required", + "licenses": [ + { + "expression": "Apache-2.0 OR BSD-3-Clause" + } + ], + "purl": "pkg:cargo/cryptography-x509@0.50.1?download_url=file://cryptography-x509" + }, + { + "type": "library", + "bom-ref": "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-x509-verification#0.50.1", + "author": "The cryptography developers ", + "name": "cryptography-x509-verification", + "version": "0.50.1", + "scope": "required", + "licenses": [ + { + "expression": "Apache-2.0 OR BSD-3-Clause" + } + ], + "purl": "pkg:cargo/cryptography-x509-verification@0.50.1?download_url=file://cryptography-x509-verification" + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#asn1@0.24.1", + "author": "Alex Gaynor ", + "name": "asn1", + "version": "0.24.1", + "description": "ASN.1 (DER) parser and writer for Rust.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "c9795210620c0cb3f9a7ce4f882808c38e1ef7b347c90591dceae0886e031fb1" + } + ], + "licenses": [ + { + "expression": "BSD-3-Clause" + } + ], + "purl": "pkg:cargo/asn1@0.24.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/alex/rust-asn1" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#asn1_derive@0.24.1", + "author": "Alex Gaynor ", + "name": "asn1_derive", + "version": "0.24.1", + "description": "#[derive] support for asn1", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "909e307f1cc32bb8bccbd98f446e6d1bf03fa30f7b53a4337da7181ad30fa11a" + } + ], + "licenses": [ + { + "expression": "BSD-3-Clause" + } + ], + "purl": "pkg:cargo/asn1_derive@0.24.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/alex/rust-asn1" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#base64@0.23.0", + "author": "Marshall Pierce ", + "name": "base64", + "version": "0.23.0", + "description": "encodes and decodes base64 as bytes or utf8", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/base64@0.23.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/base64" + }, + { + "type": "vcs", + "url": "https://github.com/marshallpierce/rust-base64" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#bitflags@2.13.1", + "author": "The Rust Project Developers", + "name": "bitflags", + "version": "2.13.1", + "description": "A macro to generate structures which behave like bitflags. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/bitflags@2.13.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/bitflags" + }, + { + "type": "website", + "url": "https://github.com/bitflags/bitflags" + }, + { + "type": "vcs", + "url": "https://github.com/bitflags/bitflags" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#cc@1.4.0", + "author": "Alex Crichton ", + "name": "cc", + "version": "1.4.0", + "description": "A build-time dependency for Cargo build scripts to assist in invoking the native C compiler to compile native C code into a static archive to be linked into Rust code. ", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/cc@1.4.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/cc" + }, + { + "type": "website", + "url": "https://github.com/rust-lang/cc-rs" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/cc-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "author": "Alex Crichton ", + "name": "cfg-if", + "version": "1.0.4", + "description": "A macro to ergonomically define an item depending on a large number of #[cfg] parameters. Structured like an if-else chain, the first matching branch is the item that gets emitted. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/cfg-if@1.0.4", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-lang/cfg-if" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#find-msvc-tools@0.1.9", + "name": "find-msvc-tools", + "version": "0.1.9", + "description": "Find windows-specific tools, read MSVC versions from the registry and from COM interfaces", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/find-msvc-tools@0.1.9", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/find-msvc-tools" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/cc-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#foreign-types-shared@0.1.1", + "author": "Steven Fackler ", + "name": "foreign-types-shared", + "version": "0.1.1", + "description": "An internal crate used by foreign-types", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/foreign-types-shared@0.1.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/sfackler/foreign-types" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#foreign-types@0.3.2", + "author": "Steven Fackler ", + "name": "foreign-types", + "version": "0.3.2", + "description": "A framework for Rust wrappers over C APIs", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/foreign-types@0.3.2", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/sfackler/foreign-types" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0", + "name": "heck", + "version": "0.5.0", + "description": "heck is a case conversion library.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/heck@0.5.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/withoutboats/heck" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.18", + "author": "David Tolnay ", + "name": "itoa", + "version": "1.0.18", + "description": "Fast integer primitive to string conversion", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/itoa@1.0.18", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/itoa" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/itoa" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.189", + "name": "libc", + "version": "0.2.189", + "description": "Raw FFI bindings to platform libraries like libc.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/libc@0.2.189", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-lang/libc" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.4", + "author": "Aleksey Kladov ", + "name": "once_cell", + "version": "1.21.4", + "description": "Single assignment cells and lazy values.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/once_cell@1.21.4", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/once_cell" + }, + { + "type": "vcs", + "url": "https://github.com/matklad/once_cell" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#openssl-macros@0.1.1", + "name": "openssl-macros", + "version": "0.1.1", + "description": "Internal macros used by the openssl crate.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/openssl-macros@0.1.1" + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#openssl-sys@0.9.117", + "author": "Alex Crichton , Steven Fackler ", + "name": "openssl-sys", + "version": "0.9.117", + "description": "FFI bindings to OpenSSL", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/openssl-sys@0.9.117", + "externalReferences": [ + { + "type": "other", + "url": "openssl" + }, + { + "type": "vcs", + "url": "https://github.com/rust-openssl/rust-openssl" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#openssl@0.10.81", + "author": "Steven Fackler ", + "name": "openssl", + "version": "0.10.81", + "description": "OpenSSL bindings", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" + } + ], + "licenses": [ + { + "expression": "Apache-2.0" + } + ], + "purl": "pkg:cargo/openssl@0.10.81", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-openssl/rust-openssl" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pem@4.0.0", + "author": "Jonathan Creekmore ", + "name": "pem", + "version": "4.0.0", + "description": "Parse and encode PEM-encoded data.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/pem@4.0.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/pem/" + }, + { + "type": "website", + "url": "https://github.com/jcreekmore/pem-rs.git" + }, + { + "type": "vcs", + "url": "https://github.com/jcreekmore/pem-rs.git" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pkg-config@0.3.33", + "author": "Alex Crichton ", + "name": "pkg-config", + "version": "0.3.33", + "description": "A library to run the pkg-config system tool at build time in order to be used in Cargo build scripts. ", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pkg-config@0.3.33", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/pkg-config" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/pkg-config-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#portable-atomic@1.14.0", + "name": "portable-atomic", + "version": "1.14.0", + "description": "Portable atomic types including support for 128-bit atomics, atomic float, etc. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/portable-atomic@1.14.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/taiki-e/portable-atomic" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.107", + "author": "David Tolnay , Alex Crichton ", + "name": "proc-macro2", + "version": "1.0.107", + "description": "A substitute implementation of the compiler's `proc_macro` API to decouple token-based libraries from the procedural macro use case.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/proc-macro2@1.0.107", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/proc-macro2" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/proc-macro2" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.29.0", + "author": "PyO3 Project and Contributors ", + "name": "pyo3-build-config", + "version": "0.29.0", + "description": "Build configuration for the PyO3 ecosystem", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pyo3-build-config@0.29.0", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/pyo3/pyo3" + }, + { + "type": "vcs", + "url": "https://github.com/pyo3/pyo3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-ffi@0.29.0", + "author": "PyO3 Project and Contributors ", + "name": "pyo3-ffi", + "version": "0.29.0", + "description": "Python-API bindings for the PyO3 ecosystem", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pyo3-ffi@0.29.0", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/pyo3/pyo3" + }, + { + "type": "other", + "url": "python" + }, + { + "type": "vcs", + "url": "https://github.com/pyo3/pyo3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros-backend@0.29.0", + "author": "PyO3 Project and Contributors ", + "name": "pyo3-macros-backend", + "version": "0.29.0", + "description": "Code generation for PyO3 package", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pyo3-macros-backend@0.29.0", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/pyo3/pyo3" + }, + { + "type": "vcs", + "url": "https://github.com/pyo3/pyo3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros@0.29.0", + "author": "PyO3 Project and Contributors ", + "name": "pyo3-macros", + "version": "0.29.0", + "description": "Proc macros for PyO3 package", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pyo3-macros@0.29.0", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/pyo3/pyo3" + }, + { + "type": "vcs", + "url": "https://github.com/pyo3/pyo3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3@0.29.0", + "author": "PyO3 Project and Contributors ", + "name": "pyo3", + "version": "0.29.0", + "description": "Bindings to Python interpreter", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pyo3@0.29.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/crate/pyo3/" + }, + { + "type": "website", + "url": "https://github.com/pyo3/pyo3" + }, + { + "type": "other", + "url": "pyo3-python" + }, + { + "type": "vcs", + "url": "https://github.com/pyo3/pyo3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.47", + "author": "David Tolnay ", + "name": "quote", + "version": "1.0.47", + "description": "Quasi-quoting macro quote!(...)", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/quote@1.0.47", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/quote/" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/quote" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#self_cell@1.3.0", + "author": "Lukas Bergdoll ", + "name": "self_cell", + "version": "1.3.0", + "description": "Safe-to-use proc-macro-free self-referential structs in stable Rust.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR GPL-2.0-only" + } + ], + "purl": "pkg:cargo/self_cell@1.3.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/self_cell" + }, + { + "type": "vcs", + "url": "https://github.com/Voultapher/self_cell" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#shlex@2.0.1", + "author": "comex , Fenhl , Adrian Taylor , Alex Touchet , Daniel Parks , Garrett Berg ", + "name": "shlex", + "version": "2.0.1", + "description": "Split a string into shell words, like Python's shlex.", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/shlex@2.0.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/comex/rust-shlex" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.119", + "author": "David Tolnay ", + "name": "syn", + "version": "2.0.119", + "description": "Parser for Rust source code", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/syn@2.0.119", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/syn" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/syn" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#target-lexicon@0.13.5", + "author": "Dan Gohman ", + "name": "target-lexicon", + "version": "0.13.5", + "description": "LLVM target triple types", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 WITH LLVM-exception" + } + ], + "purl": "pkg:cargo/target-lexicon@0.13.5", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/target-lexicon/" + }, + { + "type": "vcs", + "url": "https://github.com/bytecodealliance/target-lexicon" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.24", + "author": "David Tolnay ", + "name": "unicode-ident", + "version": "1.0.24", + "description": "Determine whether characters have the XID_Start or XID_Continue properties according to Unicode Standard Annex #31", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + } + ], + "licenses": [ + { + "expression": "(MIT OR Apache-2.0) AND Unicode-3.0" + } + ], + "purl": "pkg:cargo/unicode-ident@1.0.24", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/unicode-ident" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/unicode-ident" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#vcpkg@0.2.15", + "author": "Jim McGrath ", + "name": "vcpkg", + "version": "0.2.15", + "description": "A library to find native dependencies in a vcpkg tree at build time in order to be used in Cargo build scripts. ", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/vcpkg@0.2.15", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/vcpkg" + }, + { + "type": "vcs", + "url": "https://github.com/mcgoo/vcpkg-rs" + } + ] + } + ], + "dependencies": [ + { + "ref": "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust#cryptography-rust@0.50.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#asn1@0.24.1", + "registry+https://github.com/rust-lang/crates.io-index#base64@0.23.0", + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-cffi#0.50.1", + "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-crypto#0.50.1", + "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-keepalive#0.50.1", + "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-key-parsing#0.50.1", + "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-openssl#0.50.1", + "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-x509#0.50.1", + "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-x509-verification#0.50.1", + "registry+https://github.com/rust-lang/crates.io-index#foreign-types-shared@0.1.1", + "registry+https://github.com/rust-lang/crates.io-index#openssl@0.10.81", + "registry+https://github.com/rust-lang/crates.io-index#openssl-sys@0.9.117", + "registry+https://github.com/rust-lang/crates.io-index#pem@4.0.0", + "registry+https://github.com/rust-lang/crates.io-index#pyo3@0.29.0", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.29.0", + "registry+https://github.com/rust-lang/crates.io-index#self_cell@1.3.0" + ] + }, + { + "ref": "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-cffi#0.50.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cc@1.4.0", + "registry+https://github.com/rust-lang/crates.io-index#openssl-sys@0.9.117", + "registry+https://github.com/rust-lang/crates.io-index#pyo3@0.29.0", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.29.0" + ] + }, + { + "ref": "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-crypto#0.50.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#openssl@0.10.81" + ] + }, + { + "ref": "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-keepalive#0.50.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#pyo3@0.29.0" + ] + }, + { + "ref": "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-key-parsing#0.50.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#asn1@0.24.1", + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-crypto#0.50.1", + "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-openssl#0.50.1", + "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-x509#0.50.1", + "registry+https://github.com/rust-lang/crates.io-index#openssl@0.10.81", + "registry+https://github.com/rust-lang/crates.io-index#openssl-sys@0.9.117", + "registry+https://github.com/rust-lang/crates.io-index#pem@4.0.0" + ] + }, + { + "ref": "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-openssl#0.50.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "registry+https://github.com/rust-lang/crates.io-index#foreign-types@0.3.2", + "registry+https://github.com/rust-lang/crates.io-index#foreign-types-shared@0.1.1", + "registry+https://github.com/rust-lang/crates.io-index#openssl@0.10.81", + "registry+https://github.com/rust-lang/crates.io-index#openssl-sys@0.9.117" + ] + }, + { + "ref": "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-x509#0.50.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#asn1@0.24.1" + ] + }, + { + "ref": "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-x509-verification#0.50.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#asn1@0.24.1", + "path+file:///__w/cryptography/cryptography/tmpwheelhouse/.tmpHjnbGg/cryptography-50.0.1/src/rust/cryptography-x509#0.50.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#asn1@0.24.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#asn1_derive@0.24.1", + "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.18" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#asn1_derive@0.24.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.107", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.47", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.119" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#base64@0.23.0" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#bitflags@2.13.1" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#cc@1.4.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#find-msvc-tools@0.1.9", + "registry+https://github.com/rust-lang/crates.io-index#shlex@2.0.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#find-msvc-tools@0.1.9" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#foreign-types-shared@0.1.1" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#foreign-types@0.3.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#foreign-types-shared@0.1.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.18" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.189" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.4" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#openssl-macros@0.1.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.107", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.47", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.119" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#openssl-sys@0.9.117", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cc@1.4.0", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.189", + "registry+https://github.com/rust-lang/crates.io-index#pkg-config@0.3.33", + "registry+https://github.com/rust-lang/crates.io-index#vcpkg@0.2.15" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#openssl@0.10.81", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#bitflags@2.13.1", + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "registry+https://github.com/rust-lang/crates.io-index#foreign-types@0.3.2", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.189", + "registry+https://github.com/rust-lang/crates.io-index#openssl-macros@0.1.1", + "registry+https://github.com/rust-lang/crates.io-index#openssl-sys@0.9.117" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pem@4.0.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#base64@0.23.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pkg-config@0.3.33" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#portable-atomic@1.14.0" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.107", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.24" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.29.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#target-lexicon@0.13.5" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-ffi@0.29.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.189", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.29.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros-backend@0.29.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0", + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.107", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.47", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.119" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros@0.29.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.107", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros-backend@0.29.0", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.47", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.119" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3@0.29.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.189", + "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.4", + "registry+https://github.com/rust-lang/crates.io-index#portable-atomic@1.14.0", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.29.0", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-ffi@0.29.0", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros@0.29.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.47", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.107" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#self_cell@1.3.0" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#shlex@2.0.1" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.119", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.107", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.47", + "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.24" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#target-lexicon@0.13.5" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.24" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#vcpkg@0.2.15" + } + ] +} \ No newline at end of file diff --git a/source/target-vendors/cp312/cryptography-50.0.1.dist-info/sboms/sbom.json b/source/target-vendors/cp312/cryptography-50.0.1.dist-info/sboms/sbom.json new file mode 100644 index 0000000..b35f4b1 --- /dev/null +++ b/source/target-vendors/cp312/cryptography-50.0.1.dist-info/sboms/sbom.json @@ -0,0 +1,43 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "urn:uuid:8993a62f-53da-4d3f-b423-fc6d6d57604c", + "metadata": { + "timestamp": "2026-08-25T18:07:43Z" + }, + "components": [ + { + "type": "library", + "name": "openssl", + "version": "4.0.2", + "purl": "pkg:generic/openssl@4.0.2?download_url=https://github.com/openssl/openssl/releases/download/openssl-4.0.2/openssl-4.0.2.tar.gz", + "hashes": [ + { + "alg": "SHA-256", + "content": "736b467530f916737b7031310ccb21d8218c6229e61e8e160cd1d3458cd543a8" + } + ], + "externalReferences": [ + { + "type": "distribution", + "url": "https://github.com/openssl/openssl/releases/download/openssl-4.0.2/openssl-4.0.2.tar.gz" + } + ], + "properties": [ + { + "name": "build:operating-system", + "value": "linux" + }, + { + "name": "build:architecture", + "value": "x86_64" + }, + { + "name": "build:flags", + "value": "no-zlib no-shared no-module no-comp no-apps no-docs no-sm2-precomp no-atexit enable-ec_nistp_64_gcc_128" + } + ] + } + ] +} diff --git a/source/target-vendors/cp312/cryptography/__about__.py b/source/target-vendors/cp312/cryptography/__about__.py new file mode 100644 index 0000000..d6fc1ae --- /dev/null +++ b/source/target-vendors/cp312/cryptography/__about__.py @@ -0,0 +1,17 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +__all__ = [ + "__author__", + "__copyright__", + "__version__", +] + +__version__ = "50.0.1" + + +__author__ = "The Python Cryptographic Authority and individual contributors" +__copyright__ = f"Copyright 2013-2026 {__author__}" diff --git a/source/target-vendors/cp312/cryptography/__init__.py b/source/target-vendors/cp312/cryptography/__init__.py new file mode 100644 index 0000000..d374f75 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/__init__.py @@ -0,0 +1,13 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.__about__ import __author__, __copyright__, __version__ + +__all__ = [ + "__author__", + "__copyright__", + "__version__", +] diff --git a/source/target-vendors/cp312/cryptography/cobblestone.py b/source/target-vendors/cp312/cryptography/cobblestone.py new file mode 100644 index 0000000..09f67af --- /dev/null +++ b/source/target-vendors/cp312/cryptography/cobblestone.py @@ -0,0 +1,21 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.hazmat.bindings._rust import ( + cobblestone as _cobblestone, +) + +Cobblestone128Decryptor = _cobblestone.Cobblestone128Decryptor +Cobblestone128Encryptor = _cobblestone.Cobblestone128Encryptor +Cobblestone256Decryptor = _cobblestone.Cobblestone256Decryptor +Cobblestone256Encryptor = _cobblestone.Cobblestone256Encryptor + +__all__ = [ + "Cobblestone128Decryptor", + "Cobblestone128Encryptor", + "Cobblestone256Decryptor", + "Cobblestone256Encryptor", +] diff --git a/source/target-vendors/cp312/cryptography/exceptions.py b/source/target-vendors/cp312/cryptography/exceptions.py new file mode 100644 index 0000000..fe125ea --- /dev/null +++ b/source/target-vendors/cp312/cryptography/exceptions.py @@ -0,0 +1,52 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import typing + +from cryptography.hazmat.bindings._rust import exceptions as rust_exceptions + +if typing.TYPE_CHECKING: + from cryptography.hazmat.bindings._rust import openssl as rust_openssl + +_Reasons = rust_exceptions._Reasons + + +class UnsupportedAlgorithm(Exception): + def __init__(self, message: str, reason: _Reasons | None = None) -> None: + super().__init__(message) + self._reason = reason + + +class AlreadyFinalized(Exception): + pass + + +class AlreadyUpdated(Exception): + pass + + +class NotYetFinalized(Exception): + pass + + +class InvalidTag(Exception): + pass + + +class InvalidSignature(Exception): + pass + + +class InternalError(Exception): + def __init__( + self, msg: str, err_code: list[rust_openssl.OpenSSLError] + ) -> None: + super().__init__(msg) + self.err_code = err_code + + +class InvalidKey(Exception): + pass diff --git a/source/target-vendors/cp312/cryptography/fernet.py b/source/target-vendors/cp312/cryptography/fernet.py new file mode 100644 index 0000000..582e594 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/fernet.py @@ -0,0 +1,240 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import base64 +import binascii +import os +import time +import typing +from collections.abc import Iterable + +from cryptography import utils +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives import hashes, padding +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives.hmac import HMAC + + +class InvalidToken(Exception): + pass + + +_MAX_CLOCK_SKEW = 60 + +# Hoisted to module level so each operation doesn't reconstruct them. +_PKCS7_128 = padding.PKCS7(128) +_SHA256 = hashes.SHA256() + + +class Fernet: + def __init__( + self, + key: bytes | str, + backend: typing.Any = None, + ) -> None: + try: + key = base64.urlsafe_b64decode(key) + except binascii.Error as exc: + raise ValueError( + "Fernet key must be 32 url-safe base64-encoded bytes." + ) from exc + if len(key) != 32: + raise ValueError( + "Fernet key must be 32 url-safe base64-encoded bytes." + ) + + self._signing_key = key[:16] + self._encryption_key = key[16:] + self._aes = algorithms.AES(self._encryption_key) + + @classmethod + def generate_key(cls) -> bytes: + return base64.urlsafe_b64encode(os.urandom(32)) + + def encrypt(self, data: bytes) -> bytes: + return self.encrypt_at_time(data, int(time.time())) + + def encrypt_at_time(self, data: bytes, current_time: int) -> bytes: + iv = os.urandom(16) + return self._encrypt_from_parts(data, current_time, iv) + + def _encrypt_from_parts( + self, data: bytes, current_time: int, iv: bytes + ) -> bytes: + utils._check_bytes("data", data) + + padder = _PKCS7_128.padder() + padded_data = padder.update(data) + padder.finalize() + encryptor = Cipher(self._aes, modes.CBC(iv)).encryptor() + ciphertext = encryptor.update(padded_data) + encryptor.finalize() + + basic_parts = ( + b"\x80" + + current_time.to_bytes(length=8, byteorder="big") + + iv + + ciphertext + ) + + h = HMAC(self._signing_key, _SHA256) + h.update(basic_parts) + hmac = h.finalize() + return base64.urlsafe_b64encode(basic_parts + hmac) + + def decrypt(self, token: bytes | str, ttl: int | None = None) -> bytes: + timestamp, data = Fernet._get_unverified_token_data(token) + if ttl is None: + time_info = None + else: + time_info = (ttl, int(time.time())) + return self._decrypt_data(data, timestamp, time_info) + + def decrypt_at_time( + self, token: bytes | str, ttl: int, current_time: int + ) -> bytes: + if ttl is None: + raise ValueError( + "decrypt_at_time() can only be used with a non-None ttl" + ) + timestamp, data = Fernet._get_unverified_token_data(token) + return self._decrypt_data(data, timestamp, (ttl, current_time)) + + def extract_timestamp(self, token: bytes | str) -> int: + timestamp, data = Fernet._get_unverified_token_data(token) + # Verify the token was not tampered with. + self._verify_signature(data) + return timestamp + + @staticmethod + def _get_unverified_token_data(token: bytes | str) -> tuple[int, bytes]: + if not isinstance(token, (str, bytes)): + raise TypeError("token must be bytes or str") + + try: + data = base64.urlsafe_b64decode(token) + except (TypeError, binascii.Error): + raise InvalidToken + + if not data or data[0] != 0x80: + raise InvalidToken + + if len(data) < 9: + raise InvalidToken + + timestamp = int.from_bytes(data[1:9], byteorder="big") + return timestamp, data + + def _verify_signature(self, data: bytes) -> None: + h = HMAC(self._signing_key, _SHA256) + h.update(memoryview(data)[:-32]) + try: + h.verify(data[-32:]) + except InvalidSignature: + raise InvalidToken + + def _decrypt_data( + self, + data: bytes, + timestamp: int, + time_info: tuple[int, int] | None, + ) -> bytes: + if time_info is not None: + ttl, current_time = time_info + if timestamp + ttl < current_time: + raise InvalidToken + + if current_time + _MAX_CLOCK_SKEW < timestamp: + raise InvalidToken + + self._verify_signature(data) + + mv = memoryview(data) + iv = data[9:25] + ciphertext = mv[25:-32] + decryptor = Cipher(self._aes, modes.CBC(iv)).decryptor() + plaintext_padded = decryptor.update(ciphertext) + try: + plaintext_padded += decryptor.finalize() + except ValueError: + raise InvalidToken + unpadder = _PKCS7_128.unpadder() + + unpadded = unpadder.update(plaintext_padded) + try: + unpadded += unpadder.finalize() + except ValueError: + raise InvalidToken + return unpadded + + +class MultiFernet: + def __init__(self, fernets: Iterable[Fernet]): + fernets = list(fernets) + if not fernets: + raise ValueError( + "MultiFernet requires at least one Fernet instance" + ) + self._fernets = fernets + + def encrypt(self, msg: bytes) -> bytes: + return self.encrypt_at_time(msg, int(time.time())) + + def encrypt_at_time(self, msg: bytes, current_time: int) -> bytes: + return self._fernets[0].encrypt_at_time(msg, current_time) + + def rotate(self, msg: bytes | str) -> bytes: + timestamp, data = Fernet._get_unverified_token_data(msg) + for f in self._fernets: + try: + p = f._decrypt_data(data, timestamp, None) + break + except InvalidToken: + pass + else: + raise InvalidToken + + iv = os.urandom(16) + return self._fernets[0]._encrypt_from_parts(p, timestamp, iv) + + def decrypt(self, msg: bytes | str, ttl: int | None = None) -> bytes: + if ttl is None: + time_info = None + else: + time_info = (ttl, int(time.time())) + # Parse the token once rather than once per key. + timestamp, data = Fernet._get_unverified_token_data(msg) + for f in self._fernets: + try: + return f._decrypt_data(data, timestamp, time_info) + except InvalidToken: + pass + raise InvalidToken + + def decrypt_at_time( + self, msg: bytes | str, ttl: int, current_time: int + ) -> bytes: + if ttl is None: + raise ValueError( + "decrypt_at_time() can only be used with a non-None ttl" + ) + # Parse the token once rather than once per key. + timestamp, data = Fernet._get_unverified_token_data(msg) + for f in self._fernets: + try: + return f._decrypt_data(data, timestamp, (ttl, current_time)) + except InvalidToken: + pass + raise InvalidToken + + def extract_timestamp(self, msg: bytes | str) -> int: + # Parse the token once rather than once per key. + timestamp, data = Fernet._get_unverified_token_data(msg) + for f in self._fernets: + try: + f._verify_signature(data) + return timestamp + except InvalidToken: + pass + raise InvalidToken diff --git a/source/target-vendors/cp312/cryptography/hazmat/__init__.py b/source/target-vendors/cp312/cryptography/hazmat/__init__.py new file mode 100644 index 0000000..b9f1187 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/__init__.py @@ -0,0 +1,13 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +""" +Hazardous Materials + +This is a "Hazardous Materials" module. You should ONLY use it if you're +100% absolutely sure that you know what you're doing because this module +is full of land mines, dragons, and dinosaurs with laser guns. +""" diff --git a/source/target-vendors/cp312/cryptography/hazmat/_oid.py b/source/target-vendors/cp312/cryptography/hazmat/_oid.py new file mode 100644 index 0000000..244a1b3 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/_oid.py @@ -0,0 +1,373 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.hazmat.bindings._rust import ( + ObjectIdentifier as ObjectIdentifier, +) +from cryptography.hazmat.primitives import hashes + + +class ExtensionOID: + SUBJECT_DIRECTORY_ATTRIBUTES = ObjectIdentifier("2.5.29.9") + SUBJECT_KEY_IDENTIFIER = ObjectIdentifier("2.5.29.14") + KEY_USAGE = ObjectIdentifier("2.5.29.15") + PRIVATE_KEY_USAGE_PERIOD = ObjectIdentifier("2.5.29.16") + SUBJECT_ALTERNATIVE_NAME = ObjectIdentifier("2.5.29.17") + ISSUER_ALTERNATIVE_NAME = ObjectIdentifier("2.5.29.18") + BASIC_CONSTRAINTS = ObjectIdentifier("2.5.29.19") + NAME_CONSTRAINTS = ObjectIdentifier("2.5.29.30") + CRL_DISTRIBUTION_POINTS = ObjectIdentifier("2.5.29.31") + CERTIFICATE_POLICIES = ObjectIdentifier("2.5.29.32") + POLICY_MAPPINGS = ObjectIdentifier("2.5.29.33") + AUTHORITY_KEY_IDENTIFIER = ObjectIdentifier("2.5.29.35") + POLICY_CONSTRAINTS = ObjectIdentifier("2.5.29.36") + EXTENDED_KEY_USAGE = ObjectIdentifier("2.5.29.37") + FRESHEST_CRL = ObjectIdentifier("2.5.29.46") + INHIBIT_ANY_POLICY = ObjectIdentifier("2.5.29.54") + ISSUING_DISTRIBUTION_POINT = ObjectIdentifier("2.5.29.28") + AUTHORITY_INFORMATION_ACCESS = ObjectIdentifier("1.3.6.1.5.5.7.1.1") + SUBJECT_INFORMATION_ACCESS = ObjectIdentifier("1.3.6.1.5.5.7.1.11") + OCSP_NO_CHECK = ObjectIdentifier("1.3.6.1.5.5.7.48.1.5") + TLS_FEATURE = ObjectIdentifier("1.3.6.1.5.5.7.1.24") + CRL_NUMBER = ObjectIdentifier("2.5.29.20") + DELTA_CRL_INDICATOR = ObjectIdentifier("2.5.29.27") + PRECERT_SIGNED_CERTIFICATE_TIMESTAMPS = ObjectIdentifier( + "1.3.6.1.4.1.11129.2.4.2" + ) + PRECERT_POISON = ObjectIdentifier("1.3.6.1.4.1.11129.2.4.3") + SIGNED_CERTIFICATE_TIMESTAMPS = ObjectIdentifier("1.3.6.1.4.1.11129.2.4.5") + MS_CERTIFICATE_TEMPLATE = ObjectIdentifier("1.3.6.1.4.1.311.21.7") + ADMISSIONS = ObjectIdentifier("1.3.36.8.3.3") + + +class OCSPExtensionOID: + NONCE = ObjectIdentifier("1.3.6.1.5.5.7.48.1.2") + ACCEPTABLE_RESPONSES = ObjectIdentifier("1.3.6.1.5.5.7.48.1.4") + + +class CRLEntryExtensionOID: + CERTIFICATE_ISSUER = ObjectIdentifier("2.5.29.29") + CRL_REASON = ObjectIdentifier("2.5.29.21") + INVALIDITY_DATE = ObjectIdentifier("2.5.29.24") + + +class NameOID: + COMMON_NAME = ObjectIdentifier("2.5.4.3") + COUNTRY_NAME = ObjectIdentifier("2.5.4.6") + LOCALITY_NAME = ObjectIdentifier("2.5.4.7") + STATE_OR_PROVINCE_NAME = ObjectIdentifier("2.5.4.8") + STREET_ADDRESS = ObjectIdentifier("2.5.4.9") + ORGANIZATION_IDENTIFIER = ObjectIdentifier("2.5.4.97") + ORGANIZATION_NAME = ObjectIdentifier("2.5.4.10") + ORGANIZATIONAL_UNIT_NAME = ObjectIdentifier("2.5.4.11") + SERIAL_NUMBER = ObjectIdentifier("2.5.4.5") + SURNAME = ObjectIdentifier("2.5.4.4") + GIVEN_NAME = ObjectIdentifier("2.5.4.42") + TITLE = ObjectIdentifier("2.5.4.12") + INITIALS = ObjectIdentifier("2.5.4.43") + GENERATION_QUALIFIER = ObjectIdentifier("2.5.4.44") + X500_UNIQUE_IDENTIFIER = ObjectIdentifier("2.5.4.45") + DN_QUALIFIER = ObjectIdentifier("2.5.4.46") + PSEUDONYM = ObjectIdentifier("2.5.4.65") + USER_ID = ObjectIdentifier("0.9.2342.19200300.100.1.1") + DOMAIN_COMPONENT = ObjectIdentifier("0.9.2342.19200300.100.1.25") + EMAIL_ADDRESS = ObjectIdentifier("1.2.840.113549.1.9.1") + JURISDICTION_COUNTRY_NAME = ObjectIdentifier("1.3.6.1.4.1.311.60.2.1.3") + JURISDICTION_LOCALITY_NAME = ObjectIdentifier("1.3.6.1.4.1.311.60.2.1.1") + JURISDICTION_STATE_OR_PROVINCE_NAME = ObjectIdentifier( + "1.3.6.1.4.1.311.60.2.1.2" + ) + BUSINESS_CATEGORY = ObjectIdentifier("2.5.4.15") + POSTAL_ADDRESS = ObjectIdentifier("2.5.4.16") + POSTAL_CODE = ObjectIdentifier("2.5.4.17") + INN = ObjectIdentifier("1.2.643.3.131.1.1") + OGRN = ObjectIdentifier("1.2.643.100.1") + SNILS = ObjectIdentifier("1.2.643.100.3") + UNSTRUCTURED_NAME = ObjectIdentifier("1.2.840.113549.1.9.2") + UNSIGNED = ObjectIdentifier("1.3.6.1.5.5.7.25.1") + + +class SignatureAlgorithmOID: + RSA_WITH_MD5 = ObjectIdentifier("1.2.840.113549.1.1.4") + RSA_WITH_SHA1 = ObjectIdentifier("1.2.840.113549.1.1.5") + # This is an alternate OID for RSA with SHA1 that is occasionally seen + _RSA_WITH_SHA1 = ObjectIdentifier("1.3.14.3.2.29") + RSA_WITH_SHA224 = ObjectIdentifier("1.2.840.113549.1.1.14") + RSA_WITH_SHA256 = ObjectIdentifier("1.2.840.113549.1.1.11") + RSA_WITH_SHA384 = ObjectIdentifier("1.2.840.113549.1.1.12") + RSA_WITH_SHA512 = ObjectIdentifier("1.2.840.113549.1.1.13") + RSA_WITH_SHA3_224 = ObjectIdentifier("2.16.840.1.101.3.4.3.13") + RSA_WITH_SHA3_256 = ObjectIdentifier("2.16.840.1.101.3.4.3.14") + RSA_WITH_SHA3_384 = ObjectIdentifier("2.16.840.1.101.3.4.3.15") + RSA_WITH_SHA3_512 = ObjectIdentifier("2.16.840.1.101.3.4.3.16") + RSASSA_PSS = ObjectIdentifier("1.2.840.113549.1.1.10") + ECDSA_WITH_SHA1 = ObjectIdentifier("1.2.840.10045.4.1") + ECDSA_WITH_SHA224 = ObjectIdentifier("1.2.840.10045.4.3.1") + ECDSA_WITH_SHA256 = ObjectIdentifier("1.2.840.10045.4.3.2") + ECDSA_WITH_SHA384 = ObjectIdentifier("1.2.840.10045.4.3.3") + ECDSA_WITH_SHA512 = ObjectIdentifier("1.2.840.10045.4.3.4") + ECDSA_WITH_SHA3_224 = ObjectIdentifier("2.16.840.1.101.3.4.3.9") + ECDSA_WITH_SHA3_256 = ObjectIdentifier("2.16.840.1.101.3.4.3.10") + ECDSA_WITH_SHA3_384 = ObjectIdentifier("2.16.840.1.101.3.4.3.11") + ECDSA_WITH_SHA3_512 = ObjectIdentifier("2.16.840.1.101.3.4.3.12") + DSA_WITH_SHA1 = ObjectIdentifier("1.2.840.10040.4.3") + DSA_WITH_SHA224 = ObjectIdentifier("2.16.840.1.101.3.4.3.1") + DSA_WITH_SHA256 = ObjectIdentifier("2.16.840.1.101.3.4.3.2") + DSA_WITH_SHA384 = ObjectIdentifier("2.16.840.1.101.3.4.3.3") + DSA_WITH_SHA512 = ObjectIdentifier("2.16.840.1.101.3.4.3.4") + ED25519 = ObjectIdentifier("1.3.101.112") + ED448 = ObjectIdentifier("1.3.101.113") + ML_DSA_44 = ObjectIdentifier("2.16.840.1.101.3.4.3.17") + ML_DSA_65 = ObjectIdentifier("2.16.840.1.101.3.4.3.18") + ML_DSA_87 = ObjectIdentifier("2.16.840.1.101.3.4.3.19") + GOSTR3411_94_WITH_3410_2001 = ObjectIdentifier("1.2.643.2.2.3") + GOSTR3410_2012_WITH_3411_2012_256 = ObjectIdentifier("1.2.643.7.1.1.3.2") + GOSTR3410_2012_WITH_3411_2012_512 = ObjectIdentifier("1.2.643.7.1.1.3.3") + UNSIGNED = ObjectIdentifier("1.3.6.1.5.5.7.6.36") + + +_SIG_OIDS_TO_HASH: dict[ObjectIdentifier, hashes.HashAlgorithm | None] = { + SignatureAlgorithmOID.RSA_WITH_MD5: hashes.MD5(), + SignatureAlgorithmOID.RSA_WITH_SHA1: hashes.SHA1(), + SignatureAlgorithmOID._RSA_WITH_SHA1: hashes.SHA1(), + SignatureAlgorithmOID.RSA_WITH_SHA224: hashes.SHA224(), + SignatureAlgorithmOID.RSA_WITH_SHA256: hashes.SHA256(), + SignatureAlgorithmOID.RSA_WITH_SHA384: hashes.SHA384(), + SignatureAlgorithmOID.RSA_WITH_SHA512: hashes.SHA512(), + SignatureAlgorithmOID.RSA_WITH_SHA3_224: hashes.SHA3_224(), + SignatureAlgorithmOID.RSA_WITH_SHA3_256: hashes.SHA3_256(), + SignatureAlgorithmOID.RSA_WITH_SHA3_384: hashes.SHA3_384(), + SignatureAlgorithmOID.RSA_WITH_SHA3_512: hashes.SHA3_512(), + SignatureAlgorithmOID.ECDSA_WITH_SHA1: hashes.SHA1(), + SignatureAlgorithmOID.ECDSA_WITH_SHA224: hashes.SHA224(), + SignatureAlgorithmOID.ECDSA_WITH_SHA256: hashes.SHA256(), + SignatureAlgorithmOID.ECDSA_WITH_SHA384: hashes.SHA384(), + SignatureAlgorithmOID.ECDSA_WITH_SHA512: hashes.SHA512(), + SignatureAlgorithmOID.ECDSA_WITH_SHA3_224: hashes.SHA3_224(), + SignatureAlgorithmOID.ECDSA_WITH_SHA3_256: hashes.SHA3_256(), + SignatureAlgorithmOID.ECDSA_WITH_SHA3_384: hashes.SHA3_384(), + SignatureAlgorithmOID.ECDSA_WITH_SHA3_512: hashes.SHA3_512(), + SignatureAlgorithmOID.DSA_WITH_SHA1: hashes.SHA1(), + SignatureAlgorithmOID.DSA_WITH_SHA224: hashes.SHA224(), + SignatureAlgorithmOID.DSA_WITH_SHA256: hashes.SHA256(), + SignatureAlgorithmOID.ED25519: None, + SignatureAlgorithmOID.ED448: None, + SignatureAlgorithmOID.ML_DSA_44: None, + SignatureAlgorithmOID.ML_DSA_65: None, + SignatureAlgorithmOID.ML_DSA_87: None, + SignatureAlgorithmOID.GOSTR3411_94_WITH_3410_2001: None, + SignatureAlgorithmOID.GOSTR3410_2012_WITH_3411_2012_256: None, + SignatureAlgorithmOID.GOSTR3410_2012_WITH_3411_2012_512: None, + SignatureAlgorithmOID.UNSIGNED: None, +} + + +class HashAlgorithmOID: + SHA1 = ObjectIdentifier("1.3.14.3.2.26") + SHA224 = ObjectIdentifier("2.16.840.1.101.3.4.2.4") + SHA256 = ObjectIdentifier("2.16.840.1.101.3.4.2.1") + SHA384 = ObjectIdentifier("2.16.840.1.101.3.4.2.2") + SHA512 = ObjectIdentifier("2.16.840.1.101.3.4.2.3") + SHA3_224 = ObjectIdentifier("1.3.6.1.4.1.37476.3.2.1.99.7.224") + SHA3_256 = ObjectIdentifier("1.3.6.1.4.1.37476.3.2.1.99.7.256") + SHA3_384 = ObjectIdentifier("1.3.6.1.4.1.37476.3.2.1.99.7.384") + SHA3_512 = ObjectIdentifier("1.3.6.1.4.1.37476.3.2.1.99.7.512") + SHA3_224_NIST = ObjectIdentifier("2.16.840.1.101.3.4.2.7") + SHA3_256_NIST = ObjectIdentifier("2.16.840.1.101.3.4.2.8") + SHA3_384_NIST = ObjectIdentifier("2.16.840.1.101.3.4.2.9") + SHA3_512_NIST = ObjectIdentifier("2.16.840.1.101.3.4.2.10") + + +class PublicKeyAlgorithmOID: + DSA = ObjectIdentifier("1.2.840.10040.4.1") + EC_PUBLIC_KEY = ObjectIdentifier("1.2.840.10045.2.1") + RSAES_PKCS1_v1_5 = ObjectIdentifier("1.2.840.113549.1.1.1") + RSASSA_PSS = ObjectIdentifier("1.2.840.113549.1.1.10") + X25519 = ObjectIdentifier("1.3.101.110") + X448 = ObjectIdentifier("1.3.101.111") + ED25519 = ObjectIdentifier("1.3.101.112") + ED448 = ObjectIdentifier("1.3.101.113") + ML_DSA_44 = ObjectIdentifier("2.16.840.1.101.3.4.3.17") + ML_DSA_65 = ObjectIdentifier("2.16.840.1.101.3.4.3.18") + ML_DSA_87 = ObjectIdentifier("2.16.840.1.101.3.4.3.19") + ML_KEM_768 = ObjectIdentifier("2.16.840.1.101.3.4.4.2") + ML_KEM_1024 = ObjectIdentifier("2.16.840.1.101.3.4.4.3") + + +class ExtendedKeyUsageOID: + SERVER_AUTH = ObjectIdentifier("1.3.6.1.5.5.7.3.1") + CLIENT_AUTH = ObjectIdentifier("1.3.6.1.5.5.7.3.2") + CODE_SIGNING = ObjectIdentifier("1.3.6.1.5.5.7.3.3") + EMAIL_PROTECTION = ObjectIdentifier("1.3.6.1.5.5.7.3.4") + TIME_STAMPING = ObjectIdentifier("1.3.6.1.5.5.7.3.8") + OCSP_SIGNING = ObjectIdentifier("1.3.6.1.5.5.7.3.9") + ANY_EXTENDED_KEY_USAGE = ObjectIdentifier("2.5.29.37.0") + SMARTCARD_LOGON = ObjectIdentifier("1.3.6.1.4.1.311.20.2.2") + KERBEROS_PKINIT_KDC = ObjectIdentifier("1.3.6.1.5.2.3.5") + IPSEC_IKE = ObjectIdentifier("1.3.6.1.5.5.7.3.17") + BUNDLE_SECURITY = ObjectIdentifier("1.3.6.1.5.5.7.3.35") + CERTIFICATE_TRANSPARENCY = ObjectIdentifier("1.3.6.1.4.1.11129.2.4.4") + + +class OtherNameFormOID: + PERMANENT_IDENTIFIER = ObjectIdentifier("1.3.6.1.5.5.7.8.3") + HW_MODULE_NAME = ObjectIdentifier("1.3.6.1.5.5.7.8.4") + DNS_SRV = ObjectIdentifier("1.3.6.1.5.5.7.8.7") + NAI_REALM = ObjectIdentifier("1.3.6.1.5.5.7.8.8") + SMTP_UTF8_MAILBOX = ObjectIdentifier("1.3.6.1.5.5.7.8.9") + ACP_NODE_NAME = ObjectIdentifier("1.3.6.1.5.5.7.8.10") + BUNDLE_EID = ObjectIdentifier("1.3.6.1.5.5.7.8.11") + + +class AuthorityInformationAccessOID: + CA_ISSUERS = ObjectIdentifier("1.3.6.1.5.5.7.48.2") + OCSP = ObjectIdentifier("1.3.6.1.5.5.7.48.1") + + +class SubjectInformationAccessOID: + CA_REPOSITORY = ObjectIdentifier("1.3.6.1.5.5.7.48.5") + + +class CertificatePoliciesOID: + CPS_QUALIFIER = ObjectIdentifier("1.3.6.1.5.5.7.2.1") + CPS_USER_NOTICE = ObjectIdentifier("1.3.6.1.5.5.7.2.2") + ANY_POLICY = ObjectIdentifier("2.5.29.32.0") + + +class AttributeOID: + CHALLENGE_PASSWORD = ObjectIdentifier("1.2.840.113549.1.9.7") + UNSTRUCTURED_NAME = ObjectIdentifier("1.2.840.113549.1.9.2") + + +_OID_NAMES = { + NameOID.COMMON_NAME: "commonName", + NameOID.COUNTRY_NAME: "countryName", + NameOID.LOCALITY_NAME: "localityName", + NameOID.STATE_OR_PROVINCE_NAME: "stateOrProvinceName", + NameOID.STREET_ADDRESS: "streetAddress", + NameOID.ORGANIZATION_NAME: "organizationName", + NameOID.ORGANIZATIONAL_UNIT_NAME: "organizationalUnitName", + NameOID.SERIAL_NUMBER: "serialNumber", + NameOID.SURNAME: "surname", + NameOID.GIVEN_NAME: "givenName", + NameOID.TITLE: "title", + NameOID.GENERATION_QUALIFIER: "generationQualifier", + NameOID.X500_UNIQUE_IDENTIFIER: "x500UniqueIdentifier", + NameOID.DN_QUALIFIER: "dnQualifier", + NameOID.PSEUDONYM: "pseudonym", + NameOID.USER_ID: "userID", + NameOID.DOMAIN_COMPONENT: "domainComponent", + NameOID.EMAIL_ADDRESS: "emailAddress", + NameOID.JURISDICTION_COUNTRY_NAME: "jurisdictionCountryName", + NameOID.JURISDICTION_LOCALITY_NAME: "jurisdictionLocalityName", + NameOID.JURISDICTION_STATE_OR_PROVINCE_NAME: ( + "jurisdictionStateOrProvinceName" + ), + NameOID.BUSINESS_CATEGORY: "businessCategory", + NameOID.POSTAL_ADDRESS: "postalAddress", + NameOID.POSTAL_CODE: "postalCode", + NameOID.INN: "INN", + NameOID.OGRN: "OGRN", + NameOID.SNILS: "SNILS", + NameOID.UNSTRUCTURED_NAME: "unstructuredName", + SignatureAlgorithmOID.RSA_WITH_MD5: "md5WithRSAEncryption", + SignatureAlgorithmOID.RSA_WITH_SHA1: "sha1WithRSAEncryption", + SignatureAlgorithmOID.RSA_WITH_SHA224: "sha224WithRSAEncryption", + SignatureAlgorithmOID.RSA_WITH_SHA256: "sha256WithRSAEncryption", + SignatureAlgorithmOID.RSA_WITH_SHA384: "sha384WithRSAEncryption", + SignatureAlgorithmOID.RSA_WITH_SHA512: "sha512WithRSAEncryption", + SignatureAlgorithmOID.RSASSA_PSS: "rsassaPss", + SignatureAlgorithmOID.ECDSA_WITH_SHA1: "ecdsa-with-SHA1", + SignatureAlgorithmOID.ECDSA_WITH_SHA224: "ecdsa-with-SHA224", + SignatureAlgorithmOID.ECDSA_WITH_SHA256: "ecdsa-with-SHA256", + SignatureAlgorithmOID.ECDSA_WITH_SHA384: "ecdsa-with-SHA384", + SignatureAlgorithmOID.ECDSA_WITH_SHA512: "ecdsa-with-SHA512", + SignatureAlgorithmOID.DSA_WITH_SHA1: "dsa-with-sha1", + SignatureAlgorithmOID.DSA_WITH_SHA224: "dsa-with-sha224", + SignatureAlgorithmOID.DSA_WITH_SHA256: "dsa-with-sha256", + SignatureAlgorithmOID.ED25519: "ed25519", + SignatureAlgorithmOID.ED448: "ed448", + SignatureAlgorithmOID.ML_DSA_44: "ML-DSA-44", + SignatureAlgorithmOID.ML_DSA_65: "ML-DSA-65", + SignatureAlgorithmOID.ML_DSA_87: "ML-DSA-87", + SignatureAlgorithmOID.GOSTR3411_94_WITH_3410_2001: ( + "GOST R 34.11-94 with GOST R 34.10-2001" + ), + SignatureAlgorithmOID.GOSTR3410_2012_WITH_3411_2012_256: ( + "GOST R 34.10-2012 with GOST R 34.11-2012 (256 bit)" + ), + SignatureAlgorithmOID.GOSTR3410_2012_WITH_3411_2012_512: ( + "GOST R 34.10-2012 with GOST R 34.11-2012 (512 bit)" + ), + HashAlgorithmOID.SHA1: "sha1", + HashAlgorithmOID.SHA224: "sha224", + HashAlgorithmOID.SHA256: "sha256", + HashAlgorithmOID.SHA384: "sha384", + HashAlgorithmOID.SHA512: "sha512", + HashAlgorithmOID.SHA3_224: "sha3_224", + HashAlgorithmOID.SHA3_256: "sha3_256", + HashAlgorithmOID.SHA3_384: "sha3_384", + HashAlgorithmOID.SHA3_512: "sha3_512", + HashAlgorithmOID.SHA3_224_NIST: "sha3_224", + HashAlgorithmOID.SHA3_256_NIST: "sha3_256", + HashAlgorithmOID.SHA3_384_NIST: "sha3_384", + HashAlgorithmOID.SHA3_512_NIST: "sha3_512", + PublicKeyAlgorithmOID.DSA: "dsaEncryption", + PublicKeyAlgorithmOID.EC_PUBLIC_KEY: "id-ecPublicKey", + PublicKeyAlgorithmOID.RSAES_PKCS1_v1_5: "rsaEncryption", + PublicKeyAlgorithmOID.X25519: "X25519", + PublicKeyAlgorithmOID.X448: "X448", + ExtendedKeyUsageOID.SERVER_AUTH: "serverAuth", + ExtendedKeyUsageOID.CLIENT_AUTH: "clientAuth", + ExtendedKeyUsageOID.CODE_SIGNING: "codeSigning", + ExtendedKeyUsageOID.EMAIL_PROTECTION: "emailProtection", + ExtendedKeyUsageOID.TIME_STAMPING: "timeStamping", + ExtendedKeyUsageOID.OCSP_SIGNING: "OCSPSigning", + ExtendedKeyUsageOID.SMARTCARD_LOGON: "msSmartcardLogin", + ExtendedKeyUsageOID.KERBEROS_PKINIT_KDC: "pkInitKDC", + ExtensionOID.SUBJECT_DIRECTORY_ATTRIBUTES: "subjectDirectoryAttributes", + ExtensionOID.SUBJECT_KEY_IDENTIFIER: "subjectKeyIdentifier", + ExtensionOID.KEY_USAGE: "keyUsage", + ExtensionOID.PRIVATE_KEY_USAGE_PERIOD: "privateKeyUsagePeriod", + ExtensionOID.SUBJECT_ALTERNATIVE_NAME: "subjectAltName", + ExtensionOID.ISSUER_ALTERNATIVE_NAME: "issuerAltName", + ExtensionOID.BASIC_CONSTRAINTS: "basicConstraints", + ExtensionOID.PRECERT_SIGNED_CERTIFICATE_TIMESTAMPS: ( + "signedCertificateTimestampList" + ), + ExtensionOID.SIGNED_CERTIFICATE_TIMESTAMPS: ( + "signedCertificateTimestampList" + ), + ExtensionOID.PRECERT_POISON: "ctPoison", + ExtensionOID.MS_CERTIFICATE_TEMPLATE: "msCertificateTemplate", + ExtensionOID.ADMISSIONS: "Admissions", + CRLEntryExtensionOID.CRL_REASON: "cRLReason", + CRLEntryExtensionOID.INVALIDITY_DATE: "invalidityDate", + CRLEntryExtensionOID.CERTIFICATE_ISSUER: "certificateIssuer", + ExtensionOID.NAME_CONSTRAINTS: "nameConstraints", + ExtensionOID.CRL_DISTRIBUTION_POINTS: "cRLDistributionPoints", + ExtensionOID.CERTIFICATE_POLICIES: "certificatePolicies", + ExtensionOID.POLICY_MAPPINGS: "policyMappings", + ExtensionOID.AUTHORITY_KEY_IDENTIFIER: "authorityKeyIdentifier", + ExtensionOID.POLICY_CONSTRAINTS: "policyConstraints", + ExtensionOID.EXTENDED_KEY_USAGE: "extendedKeyUsage", + ExtensionOID.FRESHEST_CRL: "freshestCRL", + ExtensionOID.INHIBIT_ANY_POLICY: "inhibitAnyPolicy", + ExtensionOID.ISSUING_DISTRIBUTION_POINT: "issuingDistributionPoint", + ExtensionOID.AUTHORITY_INFORMATION_ACCESS: "authorityInfoAccess", + ExtensionOID.SUBJECT_INFORMATION_ACCESS: "subjectInfoAccess", + ExtensionOID.OCSP_NO_CHECK: "OCSPNoCheck", + ExtensionOID.CRL_NUMBER: "cRLNumber", + ExtensionOID.DELTA_CRL_INDICATOR: "deltaCRLIndicator", + ExtensionOID.TLS_FEATURE: "TLSFeature", + AuthorityInformationAccessOID.OCSP: "OCSP", + AuthorityInformationAccessOID.CA_ISSUERS: "caIssuers", + SubjectInformationAccessOID.CA_REPOSITORY: "caRepository", + CertificatePoliciesOID.CPS_QUALIFIER: "id-qt-cps", + CertificatePoliciesOID.CPS_USER_NOTICE: "id-qt-unotice", + OCSPExtensionOID.NONCE: "OCSPNonce", + AttributeOID.CHALLENGE_PASSWORD: "challengePassword", +} diff --git a/source/target-vendors/cp312/cryptography/hazmat/asn1/__init__.py b/source/target-vendors/cp312/cryptography/hazmat/asn1/__init__.py new file mode 100644 index 0000000..d19e2a6 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/asn1/__init__.py @@ -0,0 +1,47 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.hazmat.asn1.asn1 import ( + TLV, + BitString, + Default, + Explicit, + GeneralizedTime, + IA5String, + Implicit, + Null, + PrintableString, + SetOf, + Size, + UTCTime, + Variant, + decode_der, + encode_der, + sequence, + set, + value_set, +) + +__all__ = [ + "TLV", + "BitString", + "Default", + "Explicit", + "GeneralizedTime", + "IA5String", + "Implicit", + "Null", + "PrintableString", + "SetOf", + "Size", + "UTCTime", + "Variant", + "decode_der", + "encode_der", + "sequence", + "set", + "value_set", +] diff --git a/source/target-vendors/cp312/cryptography/hazmat/asn1/asn1.py b/source/target-vendors/cp312/cryptography/hazmat/asn1/asn1.py new file mode 100644 index 0000000..62d3811 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/asn1/asn1.py @@ -0,0 +1,533 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import builtins +import dataclasses +import enum +import sys +import types +import typing + +if sys.version_info < (3, 11): + import typing_extensions + + LiteralString = typing_extensions.LiteralString +else: + LiteralString = typing.LiteralString + +from cryptography.hazmat.bindings._rust import declarative_asn1 +from cryptography.hazmat.bindings._rust import x509 as rust_x509 + +if sys.version_info < (3, 10): + NoneType = type(None) +else: + NoneType = types.NoneType # type: ignore[nonetype-type] + +T = typing.TypeVar("T", covariant=True) +U = typing.TypeVar("U") +Tag = typing.TypeVar("Tag", bound=LiteralString) + + +@dataclasses.dataclass(frozen=True) +class Variant(typing.Generic[U, Tag]): + """ + A tagged variant for CHOICE fields with the same underlying type. + + Use this when you have multiple CHOICE alternatives with the same type + and need to distinguish between them: + + foo: ( + Annotated[Variant[int, typing.Literal["IntA"]], Implicit(0)] + | Annotated[Variant[int, typing.Literal["IntB"]], Implicit(1)] + ) + + Usage: + example = Example(foo=Variant(5, "IntA")) + decoded.foo.value # The int value + decoded.foo.tag # "IntA" or "IntB" + """ + + value: U + tag: str + + +decode_der = declarative_asn1.decode_der +encode_der = declarative_asn1.encode_der + + +_X509_TYPES = ( + rust_x509.Certificate, + rust_x509.CertificateSigningRequest, + rust_x509.CertificateRevocationList, +) + + +def _check_x509_field_annotations( + field_type: typing.Any, + annotation: declarative_asn1.Annotation, + field_name: str, +) -> None: + if field_type in _X509_TYPES and isinstance(annotation.encoding, Implicit): + raise TypeError( + f"field '{field_name}' has an IMPLICIT annotation, but " + "IMPLICIT annotations are not supported for X.509 types." + ) + + +def _is_union(field_type: type) -> bool: + # NOTE: types.UnionType for `T | U`, typing.Union for `Union[T, U]`. + # TODO: Drop the `hasattr()` once the minimum supported Python version + # is >= 3.10. + union_types = ( + (types.UnionType, typing.Union) + if hasattr(types, "UnionType") + else (typing.Union,) + ) + return typing.get_origin(field_type) in union_types + + +def _resolve_type_aliases(field_type: typing.Any) -> typing.Any: + # Recursively resolve PEP 695 (`type X = ...`) type aliases (Python + # 3.12+) to their underlying value, so that the rest of the + # normalization logic never encounters an alias. Aliases can refer + # to other aliases and can appear at any level of nesting (e.g. + # inside `Annotated[...]`, unions, or `list[...]`). + if sys.version_info < (3, 12): + return field_type + + while isinstance(field_type, typing.TypeAliasType): + field_type = field_type.__value__ + + args = typing.get_args(field_type) + resolved_args = tuple(_resolve_type_aliases(arg) for arg in args) + if resolved_args == args: + # No aliases anywhere inside: return the type unchanged. + return field_type + + if _is_union(field_type): + # `X | Y` unions can't be rebuilt through their origin like + # other generics below: `typing.get_origin` returns + # `types.UnionType` for them, which is only subscriptable on + # Python 3.14+. Rebuilding through `typing.Union` also + # flattens any nested union introduced by an alias of a union + # (e.g. `Time | int` where `type Time = UTCTime | + # GeneralizedTime`), just like `typing.Union` would have done + # if the alias had been written inline. + return typing.Union[resolved_args] + + # An alias appeared inside a generic (e.g. `Annotated[Time, ...]`, + # `list[MyInt]`, or `SetOf[MyInt]`): re-parameterize the generic + # with the resolved arguments. Subscripting with a tuple is + # equivalent to subscripting with multiple arguments. + return typing.get_origin(field_type)[resolved_args] + + +def _extract_annotation( + metadata: tuple, field_name: str +) -> declarative_asn1.Annotation: + default = None + encoding = None + size = None + for raw_annotation in metadata: + if isinstance(raw_annotation, Default): + if default is not None: + raise TypeError( + f"multiple DEFAULT annotations found in field " + f"'{field_name}'" + ) + default = raw_annotation.value + elif isinstance(raw_annotation, declarative_asn1.Encoding): + if encoding is not None: + raise TypeError( + f"multiple IMPLICIT/EXPLICIT annotations found in field " + f"'{field_name}'" + ) + encoding = raw_annotation + elif isinstance(raw_annotation, declarative_asn1.Size): + if size is not None: + raise TypeError( + f"multiple SIZE annotations found in field '{field_name}'" + ) + size = raw_annotation + else: + raise TypeError(f"unsupported annotation: {raw_annotation}") + + return declarative_asn1.Annotation( + default=default, encoding=encoding, size=size + ) + + +def _normalize_field_type( + field_type: typing.Any, field_name: str +) -> declarative_asn1.AnnotatedType: + field_type = _resolve_type_aliases(field_type) + + # Strip the `Annotated[...]` off, and populate the annotation + # from it if it exists. + if typing.get_origin(field_type) is typing.Annotated: + annotation = _extract_annotation(field_type.__metadata__, field_name) + field_type, *_ = typing.get_args(field_type) + else: + annotation = declarative_asn1.Annotation() + + if annotation.size is not None and ( + typing.get_origin(field_type) not in (builtins.list, SetOf) + and field_type + not in ( + builtins.bytes, + builtins.str, + BitString, + IA5String, + PrintableString, + ) + ): + raise TypeError( + f"field '{field_name}' has a SIZE annotation, but SIZE " + "annotations are only supported for fields of types: " + "[SEQUENCE OF, SET OF, BIT STRING, OCTET STRING, UTF8String, " + "PrintableString, IA5String]" + ) + + if field_type is TLV: + if isinstance(annotation.encoding, Implicit): + raise TypeError( + f"field '{field_name}' has an IMPLICIT annotation, but " + "IMPLICIT annotations are not supported for TLV types." + ) + elif annotation.default is not None: + raise TypeError( + f"field '{field_name}' has a DEFAULT annotation, but " + "DEFAULT annotations are not supported for TLV types." + ) + + _check_x509_field_annotations(field_type, annotation, field_name) + + if hasattr(field_type, "__asn1_root__"): + root_type = field_type.__asn1_root__ + if not isinstance( + root_type, + ( + declarative_asn1.Type.Sequence, + declarative_asn1.Type.Set, + declarative_asn1.Type.ValueSet, + ), + ): + raise TypeError(f"unsupported root type: {root_type}") + return declarative_asn1.AnnotatedType( + typing.cast(declarative_asn1.Type, root_type), annotation + ) + elif _is_union(field_type): + union_args = typing.get_args(field_type) + if len(union_args) == 2 and NoneType in union_args: + # A Union between a type and None is an OPTIONAL + optional_type = ( + union_args[0] if union_args[1] is type(None) else union_args[1] + ) + if optional_type is TLV: + raise TypeError( + "optional TLV types (`TLV | None`) are not " + "currently supported" + ) + # For optional types, the annotation is associated with the + # union, so we check it against the inner type here. + _check_x509_field_annotations( + optional_type, annotation, field_name + ) + annotated_type = _normalize_field_type(optional_type, field_name) + + if not annotated_type.annotation.is_empty(): + raise TypeError( + "optional (`X | None`) types cannot have `X` " + "annotated: annotations must apply to the union " + "(i.e: `Annotated[X | None, annotation]`)" + ) + + if annotation.default is not None: + raise TypeError( + "optional (`X | None`) types should not have a DEFAULT " + "annotation" + ) + + rust_field_type = declarative_asn1.Type.Option(annotated_type) + + else: + # Otherwise, the Union is a CHOICE + if isinstance(annotation.encoding, Implicit): + # CHOICEs cannot be IMPLICIT. See X.680 section 31.2.9. + raise TypeError( + "CHOICE (`X | Y | ...`) types should not have an IMPLICIT " + "annotation" + ) + variants = [ + _type_to_variant(arg, field_name) + for arg in union_args + if arg is not type(None) + ] + + # Union types should either be all Variants + # (`Variant[..] | Variant[..] | etc`) or all non Variants + are_union_types_tagged = variants[0].tag_name is not None + if any( + (v.tag_name is not None) != are_union_types_tagged + for v in variants + ): + raise TypeError( + "When using `asn1.Variant` in a union, all the other " + "types in the union must also be `asn1.Variant`" + ) + + if are_union_types_tagged: + tags = {v.tag_name for v in variants} + if len(variants) != len(tags): + raise TypeError( + "When using `asn1.Variant` in a union, the tags used " + "must be unique" + ) + + rust_choice_type = declarative_asn1.Type.Choice(variants) + # If None is part of the union types, this is an OPTIONAL CHOICE + rust_field_type = ( + declarative_asn1.Type.Option( + declarative_asn1.AnnotatedType( + rust_choice_type, declarative_asn1.Annotation() + ) + ) + if NoneType in union_args + else rust_choice_type + ) + + elif typing.get_origin(field_type) is builtins.list: + inner_type = _normalize_field_type( + typing.get_args(field_type)[0], field_name + ) + rust_field_type = declarative_asn1.Type.SequenceOf(inner_type) + elif typing.get_origin(field_type) is SetOf: + inner_type = _normalize_field_type( + typing.get_args(field_type)[0], field_name + ) + rust_field_type = declarative_asn1.Type.SetOf(inner_type) + else: + rust_field_type = declarative_asn1.non_root_python_to_rust(field_type) + + return declarative_asn1.AnnotatedType(rust_field_type, annotation) + + +# Convert a type to a Variant. Used with types inside Union +# annotations (T1, T2, etc in `Union[T1, T2, ...]`). +def _type_to_variant( + t: typing.Any, field_name: str +) -> declarative_asn1.Variant: + is_annotated = typing.get_origin(t) is typing.Annotated + inner_type = typing.get_args(t)[0] if is_annotated else t + + # Check if this is a Variant[T, Tag] type + if typing.get_origin(inner_type) is Variant: + value_type, tag_literal = typing.get_args(inner_type) + if typing.get_origin(tag_literal) is not typing.Literal: + raise TypeError( + "When using `asn1.Variant` in a type annotation, the second " + "type parameter must be a `typing.Literal` type. E.g: " + '`Variant[int, typing.Literal["MyInt"]]`.' + ) + tag_name = typing.get_args(tag_literal)[0] + + if hasattr(value_type, "__asn1_root__"): + rust_type = value_type.__asn1_root__ + else: + rust_type = declarative_asn1.non_root_python_to_rust(value_type) + + if is_annotated: + ann_type = declarative_asn1.AnnotatedType( + rust_type, + _extract_annotation(t.__metadata__, field_name), + ) + else: + ann_type = declarative_asn1.AnnotatedType( + rust_type, + declarative_asn1.Annotation(), + ) + + return declarative_asn1.Variant(Variant, ann_type, tag_name) + else: + # Plain type (not a tagged Variant) + return declarative_asn1.Variant( + inner_type, + _normalize_field_type(t, field_name), + None, + ) + + +def _annotate_fields( + raw_fields: dict[str, type], +) -> dict[str, declarative_asn1.AnnotatedType]: + fields = {} + for field_name, field_type in raw_fields.items(): + # Recursively normalize the field type into something that the + # Rust code can understand. + annotated_field_type = _normalize_field_type(field_type, field_name) + fields[field_name] = annotated_field_type + + return fields + + +def _register_asn1_sequence(cls: type[U]) -> None: + raw_fields = typing.get_type_hints(cls, include_extras=True) + root = declarative_asn1.Type.Sequence(cls, _annotate_fields(raw_fields)) + + setattr(cls, "__asn1_root__", root) + + +def _register_asn1_set(cls: type[U]) -> None: + raw_fields = typing.get_type_hints(cls, include_extras=True) + root = declarative_asn1.Type.Set(cls, _annotate_fields(raw_fields)) + + setattr(cls, "__asn1_root__", root) + + +# Due to https://github.com/python/mypy/issues/19731, we can't define an alias +# for `dataclass_transform` that conditionally points to `typing` or +# `typing_extensions` depending on the Python version. We work around it by +# making the whole decorated class conditional on the Python version. +if sys.version_info < (3, 11): + + @typing_extensions.dataclass_transform(kw_only_default=True) + def sequence(cls: type[U]) -> type[U]: + # We use `dataclasses.dataclass` to add an __init__ method + # to the class with keyword-only parameters. + if sys.version_info >= (3, 10): + dataclass_cls = dataclasses.dataclass( + repr=False, + eq=False, + # `match_args` was added in Python 3.10 and defaults + # to True + match_args=False, + # `kw_only` was added in Python 3.10 and defaults to + # False + kw_only=True, + )(cls) + else: + dataclass_cls = dataclasses.dataclass( + repr=False, + eq=False, + )(cls) + _register_asn1_sequence(dataclass_cls) + return dataclass_cls + + @typing_extensions.dataclass_transform(kw_only_default=True) + def set(cls: type[U]) -> type[U]: + # We use `dataclasses.dataclass` to add an __init__ method + # to the class with keyword-only parameters. + if sys.version_info >= (3, 10): + dataclass_cls = dataclasses.dataclass( + repr=False, + eq=False, + # `match_args` was added in Python 3.10 and defaults + # to True + match_args=False, + # `kw_only` was added in Python 3.10 and defaults to + # False + kw_only=True, + )(cls) + else: + dataclass_cls = dataclasses.dataclass( + repr=False, + eq=False, + )(cls) + _register_asn1_set(dataclass_cls) + return dataclass_cls + +else: + + @typing.dataclass_transform(kw_only_default=True) + def sequence(cls: type[U]) -> type[U]: + # Only add an __init__ method, with keyword-only + # parameters. + dataclass_cls = dataclasses.dataclass( + repr=False, + eq=False, + match_args=False, + kw_only=True, + )(cls) + _register_asn1_sequence(dataclass_cls) + return dataclass_cls + + @typing.dataclass_transform(kw_only_default=True) + def set(cls: type[U]) -> type[U]: + # Only add an __init__ method, with keyword-only + # parameters. + dataclass_cls = dataclasses.dataclass( + repr=False, + eq=False, + match_args=False, + kw_only=True, + )(cls) + _register_asn1_set(dataclass_cls) + return dataclass_cls + + +def value_set( + value_type: type, +) -> typing.Callable[[type[U]], type[U]]: + """ + A class decorator that registers an `enum.Enum` subclass as an + ASN.1 value set of the given underlying type. All the member + values must be instances of `value_type`. Members are encoded as + their value; decoding fails if the decoded value does not match + any member. + """ + rust_type = declarative_asn1.non_root_python_to_rust(value_type) + + def decorator(cls: type[U]) -> type[U]: + if not issubclass(cls, enum.Enum): + raise TypeError( + "value sets can only be defined from enum.Enum subclasses" + ) + members = list(cls) + if not members: + raise TypeError( + f"value set '{cls.__name__}' must have at least one member" + ) + for member in members: + if not isinstance(member.value, value_type): + raise TypeError( + f"member '{member.name}' of value set '{cls.__name__}' " + f"must have a value of type " + f"'{value_type.__name__}', got: " + f"'{type(member.value).__name__}'" + ) + inner = declarative_asn1.AnnotatedType( + rust_type, declarative_asn1.Annotation() + ) + # Map from member value to member, used for O(1) lookups when + # decoding. This requires the member values to be hashable. + value_map = {member.value: member for member in members} + root = declarative_asn1.Type.ValueSet(cls, inner, value_map) + + setattr(cls, "__asn1_root__", root) + return cls + + return decorator + + +# TODO: replace with `Default[U]` once the min Python version is >= 3.12 +@dataclasses.dataclass(frozen=True) +class Default(typing.Generic[U]): + value: U + + +SetOf = declarative_asn1.SetOf + +Explicit = declarative_asn1.Encoding.Explicit +Implicit = declarative_asn1.Encoding.Implicit +Size = declarative_asn1.Size + +PrintableString = declarative_asn1.PrintableString +IA5String = declarative_asn1.IA5String +UTCTime = declarative_asn1.UTCTime +GeneralizedTime = declarative_asn1.GeneralizedTime +BitString = declarative_asn1.BitString +TLV = declarative_asn1.Tlv +Null = declarative_asn1.Null diff --git a/source/target-vendors/cp312/cryptography/hazmat/backends/__init__.py b/source/target-vendors/cp312/cryptography/hazmat/backends/__init__.py new file mode 100644 index 0000000..b4400aa --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/backends/__init__.py @@ -0,0 +1,13 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from typing import Any + + +def default_backend() -> Any: + from cryptography.hazmat.backends.openssl.backend import backend + + return backend diff --git a/source/target-vendors/cp312/cryptography/hazmat/backends/openssl/__init__.py b/source/target-vendors/cp312/cryptography/hazmat/backends/openssl/__init__.py new file mode 100644 index 0000000..51b0447 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/backends/openssl/__init__.py @@ -0,0 +1,9 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.hazmat.backends.openssl.backend import backend + +__all__ = ["backend"] diff --git a/source/target-vendors/cp312/cryptography/hazmat/backends/openssl/backend.py b/source/target-vendors/cp312/cryptography/hazmat/backends/openssl/backend.py new file mode 100644 index 0000000..0d02c4f --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/backends/openssl/backend.py @@ -0,0 +1,309 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.hazmat.bindings._rust import openssl as rust_openssl +from cryptography.hazmat.bindings.openssl import binding +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives._asymmetric import AsymmetricPadding +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric import utils as asym_utils +from cryptography.hazmat.primitives.asymmetric.padding import ( + MGF1, + OAEP, + PSS, + PKCS1v15, +) +from cryptography.hazmat.primitives.ciphers import ( + CipherAlgorithm, +) +from cryptography.hazmat.primitives.ciphers.algorithms import ( + AES, +) +from cryptography.hazmat.primitives.ciphers.modes import ( + CBC, + Mode, +) + + +class Backend: + """ + OpenSSL API binding interfaces. + """ + + name = "openssl" + + # TripleDES encryption is disallowed/deprecated throughout 2023 in + # FIPS 140-3. To keep it simple we denylist any use of TripleDES (TDEA). + _fips_ciphers = (AES,) + # Sometimes SHA1 is still permissible. That logic is contained + # within the various *_supported methods. + _fips_hashes = ( + hashes.SHA224, + hashes.SHA256, + hashes.SHA384, + hashes.SHA512, + hashes.SHA512_224, + hashes.SHA512_256, + hashes.SHA3_224, + hashes.SHA3_256, + hashes.SHA3_384, + hashes.SHA3_512, + hashes.SHAKE128, + hashes.SHAKE256, + ) + _fips_ecdh_curves = ( + ec.SECP224R1, + ec.SECP256R1, + ec.SECP384R1, + ec.SECP521R1, + ) + _fips_rsa_min_key_size = 2048 + _fips_rsa_min_public_exponent = 65537 + _fips_dsa_min_modulus = 1 << 2048 + _fips_dh_min_key_size = 2048 + _fips_dh_min_modulus = 1 << _fips_dh_min_key_size + + def __init__(self) -> None: + self._binding = binding.Binding() + self._ffi = self._binding.ffi + self._lib = self._binding.lib + self._fips_enabled = rust_openssl.is_fips_enabled() + + def __repr__(self) -> str: + return ( + f"" + ) + + def openssl_assert(self, ok: bool) -> None: + return binding._openssl_assert(ok) + + def _enable_fips(self) -> None: + # This function enables FIPS mode for OpenSSL 3.0.0 on installs that + # have the FIPS provider installed properly. + rust_openssl.enable_fips(rust_openssl._providers) + assert rust_openssl.is_fips_enabled() + self._fips_enabled = rust_openssl.is_fips_enabled() + + def openssl_version_text(self) -> str: + """ + Friendly string name of the loaded OpenSSL library. This is not + necessarily the same version as it was compiled against. + + Example: OpenSSL 3.2.1 30 Jan 2024 + """ + return rust_openssl.openssl_version_text() + + def openssl_version_number(self) -> int: + return rust_openssl.openssl_version() + + def hash_supported(self, algorithm: hashes.HashAlgorithm) -> bool: + if self._fips_enabled and not isinstance(algorithm, self._fips_hashes): + return False + + return rust_openssl.hashes.hash_supported(algorithm) + + def signature_hash_supported( + self, algorithm: hashes.HashAlgorithm + ) -> bool: + # Dedicated check for hashing algorithm use in message digest for + # signatures, e.g. RSA PKCS#1 v1.5 SHA1 (sha1WithRSAEncryption). + if self._fips_enabled and isinstance(algorithm, hashes.SHA1): + return False + return self.hash_supported(algorithm) + + def scrypt_supported(self) -> bool: + if self._fips_enabled: + return False + else: + return hasattr(rust_openssl.kdf.Scrypt, "derive") + + def argon2_supported(self) -> bool: + if self._fips_enabled: + return False + else: + return hasattr(rust_openssl.kdf.Argon2id, "derive") + + def hmac_supported(self, algorithm: hashes.HashAlgorithm) -> bool: + # FIPS mode still allows SHA1 for HMAC + if self._fips_enabled and isinstance(algorithm, hashes.SHA1): + return True + if rust_openssl.CRYPTOGRAPHY_IS_AWSLC: + return isinstance( + algorithm, + ( + hashes.MD5, + hashes.SHA1, + hashes.SHA224, + hashes.SHA256, + hashes.SHA384, + hashes.SHA512, + hashes.SHA512_224, + hashes.SHA512_256, + hashes.SHA3_224, + hashes.SHA3_256, + hashes.SHA3_384, + hashes.SHA3_512, + ), + ) + return self.hash_supported(algorithm) + + def cipher_supported(self, cipher: CipherAlgorithm, mode: Mode) -> bool: + if self._fips_enabled: + # FIPS mode requires AES. TripleDES is disallowed/deprecated in + # FIPS 140-3. + if not isinstance(cipher, self._fips_ciphers): + return False + + return rust_openssl.ciphers.cipher_supported(cipher, mode) + + def _consume_errors(self) -> list[rust_openssl.OpenSSLError]: + return rust_openssl.capture_error_stack() + + def _oaep_hash_supported(self, algorithm: hashes.HashAlgorithm) -> bool: + if self._fips_enabled and isinstance(algorithm, hashes.SHA1): + return False + + return isinstance( + algorithm, + ( + hashes.SHA1, + hashes.SHA224, + hashes.SHA256, + hashes.SHA384, + hashes.SHA512, + ), + ) + + def rsa_padding_supported(self, padding: AsymmetricPadding) -> bool: + if isinstance(padding, PKCS1v15): + return True + elif isinstance(padding, PSS) and isinstance(padding._mgf, MGF1): + # FIPS 186-4 only allows salt length == digest length for PSS + # It is technically acceptable to set an explicit salt length + # equal to the digest length and this will incorrectly fail, but + # since we don't do that in the tests and this method is + # private, we'll ignore that until we need to do otherwise. + if ( + self._fips_enabled + and padding._salt_length != PSS.DIGEST_LENGTH + ): + return False + return self.hash_supported(padding._mgf._algorithm) + elif isinstance(padding, OAEP) and isinstance(padding._mgf, MGF1): + return self._oaep_hash_supported( + padding._mgf._algorithm + ) and self._oaep_hash_supported(padding._algorithm) + else: + return False + + def rsa_encryption_supported(self, padding: AsymmetricPadding) -> bool: + if self._fips_enabled and isinstance(padding, PKCS1v15): + return False + else: + return self.rsa_padding_supported(padding) + + def dsa_supported(self) -> bool: + return ( + not rust_openssl.CRYPTOGRAPHY_IS_BORINGSSL + and not self._fips_enabled + ) + + def dsa_hash_supported(self, algorithm: hashes.HashAlgorithm) -> bool: + if not self.dsa_supported(): + return False + return self.signature_hash_supported(algorithm) + + def cmac_algorithm_supported(self, algorithm) -> bool: + return self.cipher_supported( + algorithm, CBC(b"\x00" * algorithm.block_size) + ) + + def elliptic_curve_supported(self, curve: ec.EllipticCurve) -> bool: + if self._fips_enabled and not isinstance( + curve, self._fips_ecdh_curves + ): + return False + + return rust_openssl.ec.curve_supported(curve) + + def elliptic_curve_signature_algorithm_supported( + self, + signature_algorithm: ec.EllipticCurveSignatureAlgorithm, + curve: ec.EllipticCurve, + ) -> bool: + # We only support ECDSA right now. + if not isinstance(signature_algorithm, ec.ECDSA): + return False + + return self.elliptic_curve_supported(curve) and ( + isinstance(signature_algorithm.algorithm, asym_utils.Prehashed) + or self.hash_supported(signature_algorithm.algorithm) + ) + + def elliptic_curve_exchange_algorithm_supported( + self, algorithm: ec.ECDH, curve: ec.EllipticCurve + ) -> bool: + return self.elliptic_curve_supported(curve) and isinstance( + algorithm, ec.ECDH + ) + + def dh_supported(self) -> bool: + return not rust_openssl.CRYPTOGRAPHY_IS_BORINGSSL + + def dh_x942_serialization_supported(self) -> bool: + return self._lib.Cryptography_HAS_EVP_PKEY_DHX == 1 + + def x25519_supported(self) -> bool: + return not self._fips_enabled + + def x448_supported(self) -> bool: + if self._fips_enabled: + return False + return ( + not rust_openssl.CRYPTOGRAPHY_IS_LIBRESSL + and not rust_openssl.CRYPTOGRAPHY_IS_BORINGSSL + and not rust_openssl.CRYPTOGRAPHY_IS_AWSLC + ) + + def mlkem_supported(self) -> bool: + return ( + rust_openssl.CRYPTOGRAPHY_IS_AWSLC + or rust_openssl.CRYPTOGRAPHY_IS_BORINGSSL + or rust_openssl.CRYPTOGRAPHY_OPENSSL_350_OR_GREATER + ) + + def mldsa_supported(self) -> bool: + return ( + rust_openssl.CRYPTOGRAPHY_IS_AWSLC + or rust_openssl.CRYPTOGRAPHY_IS_BORINGSSL + or rust_openssl.CRYPTOGRAPHY_OPENSSL_350_OR_GREATER + ) + + def ed25519_supported(self) -> bool: + return True + + def ed448_supported(self) -> bool: + return ( + not rust_openssl.CRYPTOGRAPHY_IS_LIBRESSL + and not rust_openssl.CRYPTOGRAPHY_IS_BORINGSSL + and not rust_openssl.CRYPTOGRAPHY_IS_AWSLC + ) + + def ecdsa_deterministic_supported(self) -> bool: + return ( + rust_openssl.CRYPTOGRAPHY_OPENSSL_320_OR_GREATER + and not self._fips_enabled + ) + + def poly1305_supported(self) -> bool: + if rust_openssl.CRYPTOGRAPHY_IS_AWSLC: + return True + return not self._fips_enabled + + +backend = Backend() diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/__init__.py b/source/target-vendors/cp312/cryptography/hazmat/bindings/__init__.py new file mode 100644 index 0000000..41d7318 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/__init__.py @@ -0,0 +1,5 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust.abi3.so b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust.abi3.so new file mode 100644 index 0000000..fddbd2a Binary files /dev/null and b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust.abi3.so differ diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/__init__.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/__init__.pyi new file mode 100644 index 0000000..c3148f1 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/__init__.pyi @@ -0,0 +1,67 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import typing + +from cryptography.hazmat.primitives import padding +from cryptography.hazmat.primitives._serialization import ( + KeySerializationEncryptionBuilder, +) +from cryptography.utils import Buffer + +class PKCS7PaddingContext(padding.PaddingContext): + def __init__(self, block_size: int) -> None: ... + def update(self, data: Buffer) -> bytes: ... + def finalize(self) -> bytes: ... + +class ANSIX923PaddingContext(padding.PaddingContext): + def __init__(self, block_size: int) -> None: ... + def update(self, data: Buffer) -> bytes: ... + def finalize(self) -> bytes: ... + +class PKCS7UnpaddingContext(padding.PaddingContext): + def __init__(self, block_size: int) -> None: ... + def update(self, data: Buffer) -> bytes: ... + def finalize(self) -> bytes: ... + +class ANSIX923UnpaddingContext(padding.PaddingContext): + def __init__(self, block_size: int) -> None: ... + def update(self, data: Buffer) -> bytes: ... + def finalize(self) -> bytes: ... + +class Encoding: + PEM: typing.ClassVar[Encoding] + DER: typing.ClassVar[Encoding] + OpenSSH: typing.ClassVar[Encoding] + Raw: typing.ClassVar[Encoding] + X962: typing.ClassVar[Encoding] + SMIME: typing.ClassVar[Encoding] + +class PrivateFormat: + PKCS8: typing.ClassVar[PrivateFormat] + TraditionalOpenSSL: typing.ClassVar[PrivateFormat] + Raw: typing.ClassVar[PrivateFormat] + OpenSSH: typing.ClassVar[PrivateFormat] + PKCS12: typing.ClassVar[PrivateFormat] + def encryption_builder(self) -> KeySerializationEncryptionBuilder: ... + +class PublicFormat: + SubjectPublicKeyInfo: typing.ClassVar[PublicFormat] + PKCS1: typing.ClassVar[PublicFormat] + OpenSSH: typing.ClassVar[PublicFormat] + Raw: typing.ClassVar[PublicFormat] + CompressedPoint: typing.ClassVar[PublicFormat] + UncompressedPoint: typing.ClassVar[PublicFormat] + +class ParameterFormat: + PKCS3: typing.ClassVar[ParameterFormat] + +class ObjectIdentifier: + def __init__(self, value: str) -> None: ... + @property + def dotted_string(self) -> str: ... + @property + def _name(self) -> str: ... + +T = typing.TypeVar("T") diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/_openssl.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/_openssl.pyi new file mode 100644 index 0000000..3d4ea4e --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/_openssl.pyi @@ -0,0 +1,8 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import typing + +lib: typing.Any +ffi: typing.Any diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/asn1.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/asn1.pyi new file mode 100644 index 0000000..3b5f208 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/asn1.pyi @@ -0,0 +1,7 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +def decode_dss_signature(signature: bytes) -> tuple[int, int]: ... +def encode_dss_signature(r: int, s: int) -> bytes: ... +def parse_spki_for_data(data: bytes) -> bytes: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/cobblestone.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/cobblestone.pyi new file mode 100644 index 0000000..95e9583 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/cobblestone.pyi @@ -0,0 +1,33 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from cryptography.utils import Buffer + +class Cobblestone128Encryptor: + def __init__(self, key: Buffer, context: Buffer) -> None: ... + @staticmethod + def generate_key() -> bytes: ... + def update(self, data: Buffer) -> bytes: ... + def update_into(self, data: Buffer, buf: Buffer) -> int: ... + def finalize(self) -> bytes: ... + +class Cobblestone128Decryptor: + def __init__(self, key: Buffer, context: Buffer) -> None: ... + def update(self, data: Buffer) -> bytes: ... + def update_into(self, data: Buffer, buf: Buffer) -> int: ... + def finalize(self) -> bytes: ... + +class Cobblestone256Encryptor: + def __init__(self, key: Buffer, context: Buffer) -> None: ... + @staticmethod + def generate_key() -> bytes: ... + def update(self, data: Buffer) -> bytes: ... + def update_into(self, data: Buffer, buf: Buffer) -> int: ... + def finalize(self) -> bytes: ... + +class Cobblestone256Decryptor: + def __init__(self, key: Buffer, context: Buffer) -> None: ... + def update(self, data: Buffer) -> bytes: ... + def update_into(self, data: Buffer, buf: Buffer) -> int: ... + def finalize(self) -> bytes: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/declarative_asn1.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/declarative_asn1.pyi new file mode 100644 index 0000000..66fc065 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/declarative_asn1.pyi @@ -0,0 +1,133 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import datetime +import typing + +T = typing.TypeVar("T") + +def decode_der(cls: type[T], value: bytes) -> T: ... +def encode_der(value: typing.Any) -> bytes: ... +def non_root_python_to_rust(cls: type) -> Type: ... + +# Type is a Rust enum with tuple variants. For now, we express the type +# annotations like this: +class Type: + Sequence: typing.ClassVar[type] + SequenceOf: typing.ClassVar[type] + Set: typing.ClassVar[type] + SetOf: typing.ClassVar[type] + Option: typing.ClassVar[type] + Choice: typing.ClassVar[type] + ValueSet: typing.ClassVar[type] + PyBool: typing.ClassVar[type] + PyInt: typing.ClassVar[type] + PyBytes: typing.ClassVar[type] + PyStr: typing.ClassVar[type] + +class Annotation: + default: typing.Any | None + encoding: Encoding | None + size: Size | None + def __new__( + cls, + default: typing.Any | None = None, + encoding: Encoding | None = None, + size: Size | None = None, + ) -> Annotation: ... + def is_empty(self) -> bool: ... + +# Encoding is a Rust enum with tuple variants. For now, we express the type +# annotations like this: +class Encoding: + Implicit: typing.ClassVar[type] + Explicit: typing.ClassVar[type] + +class Size: + min: int + max: int | None + + def __new__(cls, min: int, max: int | None) -> Size: ... + @staticmethod + def exact(n: int) -> Size: ... + +class AnnotatedType: + inner: Type + annotation: Annotation + + def __new__(cls, inner: Type, annotation: Annotation) -> AnnotatedType: ... + +class AnnotatedTypeObject: + annotated_type: AnnotatedType + value: typing.Any + + def __new__( + cls, annotated_type: AnnotatedType, value: typing.Any + ) -> AnnotatedTypeObject: ... + +class Variant: + python_class: type + ann_type: AnnotatedType + tag_name: str | None + + def __new__( + cls, + python_class: type, + ann_type: AnnotatedType, + tag_name: str | None, + ) -> Variant: ... + +class PrintableString: + def __new__(cls, inner: str) -> PrintableString: ... + def __repr__(self) -> str: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + def as_str(self) -> str: ... + +class IA5String: + def __new__(cls, inner: str) -> IA5String: ... + def __repr__(self) -> str: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + def as_str(self) -> str: ... + +class UTCTime: + def __new__(cls, inner: datetime.datetime) -> UTCTime: ... + def __repr__(self) -> str: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + def as_datetime(self) -> datetime.datetime: ... + +class GeneralizedTime: + def __new__(cls, inner: datetime.datetime) -> GeneralizedTime: ... + def __repr__(self) -> str: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + def as_datetime(self) -> datetime.datetime: ... + +class BitString: + def __new__(cls, data: bytes, padding_bits: int) -> BitString: ... + def __repr__(self) -> str: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + def as_bytes(self) -> bytes: ... + def padding_bits(self) -> int: ... + +class Tlv: + @property + def tag_bytes(self) -> bytes: ... + @property + def data(self) -> memoryview: ... + def parse(self, cls: type): ... + +class SetOf(typing.Generic[T]): + def __new__(cls, inner: list[T]) -> SetOf[T]: ... + def as_list(self) -> list[T]: ... + def __eq__(self, other: object) -> bool: ... + def __repr__(self) -> str: ... + +class Null: + def __new__(cls) -> Null: ... + def __repr__(self) -> str: ... + def __eq__(self, other: object) -> bool: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/exceptions.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/exceptions.pyi new file mode 100644 index 0000000..09f46b1 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/exceptions.pyi @@ -0,0 +1,17 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +class _Reasons: + BACKEND_MISSING_INTERFACE: _Reasons + UNSUPPORTED_HASH: _Reasons + UNSUPPORTED_CIPHER: _Reasons + UNSUPPORTED_PADDING: _Reasons + UNSUPPORTED_MGF: _Reasons + UNSUPPORTED_PUBLIC_KEY_ALGORITHM: _Reasons + UNSUPPORTED_ELLIPTIC_CURVE: _Reasons + UNSUPPORTED_SERIALIZATION: _Reasons + UNSUPPORTED_X509: _Reasons + UNSUPPORTED_EXCHANGE_ALGORITHM: _Reasons + UNSUPPORTED_DIFFIE_HELLMAN: _Reasons + UNSUPPORTED_MAC: _Reasons diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/ocsp.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/ocsp.pyi new file mode 100644 index 0000000..103e96c --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/ocsp.pyi @@ -0,0 +1,117 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import datetime +from collections.abc import Iterator + +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes +from cryptography.x509 import ocsp + +class OCSPRequest: + @property + def issuer_key_hash(self) -> bytes: ... + @property + def issuer_name_hash(self) -> bytes: ... + @property + def hash_algorithm(self) -> hashes.HashAlgorithm: ... + @property + def serial_number(self) -> int: ... + def public_bytes(self, encoding: serialization.Encoding) -> bytes: ... + @property + def extensions(self) -> x509.Extensions: ... + +class OCSPResponse: + @property + def responses(self) -> Iterator[OCSPSingleResponse]: ... + @property + def response_status(self) -> ocsp.OCSPResponseStatus: ... + @property + def signature_algorithm_oid(self) -> x509.ObjectIdentifier: ... + @property + def signature_hash_algorithm( + self, + ) -> hashes.HashAlgorithm | None: ... + @property + def signature(self) -> bytes: ... + @property + def tbs_response_bytes(self) -> bytes: ... + @property + def certificates(self) -> list[x509.Certificate]: ... + @property + def responder_key_hash(self) -> bytes | None: ... + @property + def responder_name(self) -> x509.Name | None: ... + @property + def produced_at(self) -> datetime.datetime: ... + @property + def produced_at_utc(self) -> datetime.datetime: ... + @property + def certificate_status(self) -> ocsp.OCSPCertStatus: ... + @property + def revocation_time(self) -> datetime.datetime | None: ... + @property + def revocation_time_utc(self) -> datetime.datetime | None: ... + @property + def revocation_reason(self) -> x509.ReasonFlags | None: ... + @property + def this_update(self) -> datetime.datetime: ... + @property + def this_update_utc(self) -> datetime.datetime: ... + @property + def next_update(self) -> datetime.datetime | None: ... + @property + def next_update_utc(self) -> datetime.datetime | None: ... + @property + def issuer_key_hash(self) -> bytes: ... + @property + def issuer_name_hash(self) -> bytes: ... + @property + def hash_algorithm(self) -> hashes.HashAlgorithm: ... + @property + def serial_number(self) -> int: ... + @property + def extensions(self) -> x509.Extensions: ... + @property + def single_extensions(self) -> x509.Extensions: ... + def public_bytes(self, encoding: serialization.Encoding) -> bytes: ... + +class OCSPSingleResponse: + @property + def certificate_status(self) -> ocsp.OCSPCertStatus: ... + @property + def revocation_time(self) -> datetime.datetime | None: ... + @property + def revocation_time_utc(self) -> datetime.datetime | None: ... + @property + def revocation_reason(self) -> x509.ReasonFlags | None: ... + @property + def this_update(self) -> datetime.datetime: ... + @property + def this_update_utc(self) -> datetime.datetime: ... + @property + def next_update(self) -> datetime.datetime | None: ... + @property + def next_update_utc(self) -> datetime.datetime | None: ... + @property + def issuer_key_hash(self) -> bytes: ... + @property + def issuer_name_hash(self) -> bytes: ... + @property + def hash_algorithm(self) -> hashes.HashAlgorithm: ... + @property + def serial_number(self) -> int: ... + +def load_der_ocsp_request(data: bytes) -> ocsp.OCSPRequest: ... +def load_der_ocsp_response(data: bytes) -> ocsp.OCSPResponse: ... +def create_ocsp_request( + builder: ocsp.OCSPRequestBuilder, +) -> ocsp.OCSPRequest: ... +def create_ocsp_response( + status: ocsp.OCSPResponseStatus, + builder: ocsp.OCSPResponseBuilder | None, + private_key: PrivateKeyTypes | None, + hash_algorithm: hashes.HashAlgorithm | None, +) -> ocsp.OCSPResponse: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/__init__.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/__init__.pyi new file mode 100644 index 0000000..e41ba78 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/__init__.pyi @@ -0,0 +1,82 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import typing + +from cryptography.hazmat.bindings._rust.openssl import ( + aead, + ciphers, + cmac, + dh, + dsa, + ec, + ed448, + ed25519, + hashes, + hmac, + hpke, + kdf, + keys, + keywrap, + mldsa, + mlkem, + poly1305, + rsa, + x448, + x25519, +) + +__all__ = [ + "aead", + "ciphers", + "cmac", + "dh", + "dsa", + "ec", + "ed448", + "ed25519", + "hashes", + "hmac", + "hpke", + "kdf", + "keys", + "keywrap", + "mldsa", + "mlkem", + "openssl_version", + "openssl_version_text", + "poly1305", + "raise_openssl_error", + "rsa", + "x448", + "x25519", +] + +CRYPTOGRAPHY_IS_LIBRESSL: bool +CRYPTOGRAPHY_IS_BORINGSSL: bool +CRYPTOGRAPHY_IS_AWSLC: bool +CRYPTOGRAPHY_OPENSSL_309_OR_GREATER: bool +CRYPTOGRAPHY_OPENSSL_320_OR_GREATER: bool +CRYPTOGRAPHY_OPENSSL_330_OR_GREATER: bool +CRYPTOGRAPHY_OPENSSL_350_OR_GREATER: bool + +class Providers: ... + +_legacy_provider_loaded: bool +_providers: Providers + +def openssl_version() -> int: ... +def openssl_version_text() -> str: ... +def raise_openssl_error() -> typing.NoReturn: ... +def capture_error_stack() -> list[OpenSSLError]: ... +def is_fips_enabled() -> bool: ... +def enable_fips(providers: Providers) -> None: ... + +class OpenSSLError: + @property + def lib(self) -> int: ... + @property + def reason(self) -> int: ... + @property + def reason_text(self) -> bytes: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/aead.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/aead.pyi new file mode 100644 index 0000000..eb44608 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/aead.pyi @@ -0,0 +1,189 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from collections.abc import Sequence + +from cryptography.utils import Buffer + +class AESGCM: + def __init__(self, key: Buffer) -> None: ... + @staticmethod + def generate_key(bit_length: int) -> bytes: ... + def encrypt( + self, + nonce: Buffer, + data: Buffer, + associated_data: Buffer | None, + ) -> bytes: ... + def decrypt( + self, + nonce: Buffer, + data: Buffer, + associated_data: Buffer | None, + ) -> bytes: ... + def encrypt_into( + self, + nonce: Buffer, + data: Buffer, + associated_data: Buffer | None, + buf: Buffer, + ) -> int: ... + def decrypt_into( + self, + nonce: Buffer, + data: Buffer, + associated_data: Buffer | None, + buf: Buffer, + ) -> int: ... + +class ChaCha20Poly1305: + def __init__(self, key: Buffer) -> None: ... + @staticmethod + def generate_key() -> bytes: ... + def encrypt( + self, + nonce: Buffer, + data: Buffer, + associated_data: Buffer | None, + ) -> bytes: ... + def encrypt_into( + self, + nonce: Buffer, + data: Buffer, + associated_data: Buffer | None, + buf: Buffer, + ) -> int: ... + def decrypt( + self, + nonce: Buffer, + data: Buffer, + associated_data: Buffer | None, + ) -> bytes: ... + def decrypt_into( + self, + nonce: Buffer, + data: Buffer, + associated_data: Buffer | None, + buf: Buffer, + ) -> int: ... + +class AESCCM: + def __init__(self, key: Buffer, tag_length: int = 16) -> None: ... + @staticmethod + def generate_key(bit_length: int) -> bytes: ... + def encrypt( + self, + nonce: Buffer, + data: Buffer, + associated_data: Buffer | None, + ) -> bytes: ... + def encrypt_into( + self, + nonce: Buffer, + data: Buffer, + associated_data: Buffer | None, + buf: Buffer, + ) -> int: ... + def decrypt( + self, + nonce: Buffer, + data: Buffer, + associated_data: Buffer | None, + ) -> bytes: ... + def decrypt_into( + self, + nonce: Buffer, + data: Buffer, + associated_data: Buffer | None, + buf: Buffer, + ) -> int: ... + +class AESSIV: + def __init__(self, key: Buffer) -> None: ... + @staticmethod + def generate_key(bit_length: int) -> bytes: ... + def encrypt( + self, + data: Buffer, + associated_data: Sequence[Buffer] | None, + ) -> bytes: ... + def encrypt_into( + self, + data: Buffer, + associated_data: Sequence[Buffer] | None, + buf: Buffer, + ) -> int: ... + def decrypt( + self, + data: Buffer, + associated_data: Sequence[Buffer] | None, + ) -> bytes: ... + def decrypt_into( + self, + data: Buffer, + associated_data: Sequence[Buffer] | None, + buf: Buffer, + ) -> int: ... + +class AESOCB3: + def __init__(self, key: Buffer) -> None: ... + @staticmethod + def generate_key(bit_length: int) -> bytes: ... + def encrypt( + self, + nonce: Buffer, + data: Buffer, + associated_data: Buffer | None, + ) -> bytes: ... + def encrypt_into( + self, + nonce: Buffer, + data: Buffer, + associated_data: Buffer | None, + buf: Buffer, + ) -> int: ... + def decrypt( + self, + nonce: Buffer, + data: Buffer, + associated_data: Buffer | None, + ) -> bytes: ... + def decrypt_into( + self, + nonce: Buffer, + data: Buffer, + associated_data: Buffer | None, + buf: Buffer, + ) -> int: ... + +class AESGCMSIV: + def __init__(self, key: Buffer) -> None: ... + @staticmethod + def generate_key(bit_length: int) -> bytes: ... + def encrypt( + self, + nonce: Buffer, + data: Buffer, + associated_data: Buffer | None, + ) -> bytes: ... + def encrypt_into( + self, + nonce: Buffer, + data: Buffer, + associated_data: Buffer | None, + buf: Buffer, + ) -> int: ... + def decrypt( + self, + nonce: Buffer, + data: Buffer, + associated_data: Buffer | None, + ) -> bytes: ... + def decrypt_into( + self, + nonce: Buffer, + data: Buffer, + associated_data: Buffer | None, + buf: Buffer, + ) -> int: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/ciphers.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/ciphers.pyi new file mode 100644 index 0000000..a48fb01 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/ciphers.pyi @@ -0,0 +1,38 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import typing + +from cryptography.hazmat.primitives import ciphers +from cryptography.hazmat.primitives.ciphers import modes + +@typing.overload +def create_encryption_ctx( + algorithm: ciphers.CipherAlgorithm, mode: modes.ModeWithAuthenticationTag +) -> ciphers.AEADEncryptionContext: ... +@typing.overload +def create_encryption_ctx( + algorithm: ciphers.CipherAlgorithm, mode: modes.Mode | None +) -> ciphers.CipherContext: ... +@typing.overload +def create_decryption_ctx( + algorithm: ciphers.CipherAlgorithm, mode: modes.ModeWithAuthenticationTag +) -> ciphers.AEADDecryptionContext: ... +@typing.overload +def create_decryption_ctx( + algorithm: ciphers.CipherAlgorithm, mode: modes.Mode | None +) -> ciphers.CipherContext: ... +def cipher_supported( + algorithm: ciphers.CipherAlgorithm, mode: modes.Mode +) -> bool: ... +def _advance( + ctx: ciphers.AEADEncryptionContext | ciphers.AEADDecryptionContext, n: int +) -> None: ... +def _advance_aad( + ctx: ciphers.AEADEncryptionContext | ciphers.AEADDecryptionContext, n: int +) -> None: ... + +class CipherContext: ... +class AEADEncryptionContext: ... +class AEADDecryptionContext: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/cmac.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/cmac.pyi new file mode 100644 index 0000000..9c03508 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/cmac.pyi @@ -0,0 +1,18 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import typing + +from cryptography.hazmat.primitives import ciphers + +class CMAC: + def __init__( + self, + algorithm: ciphers.BlockCipherAlgorithm, + backend: typing.Any = None, + ) -> None: ... + def update(self, data: bytes) -> None: ... + def finalize(self) -> bytes: ... + def verify(self, signature: bytes) -> None: ... + def copy(self) -> CMAC: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/dh.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/dh.pyi new file mode 100644 index 0000000..08733d7 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/dh.pyi @@ -0,0 +1,51 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import typing + +from cryptography.hazmat.primitives.asymmetric import dh + +MIN_MODULUS_SIZE: int + +class DHPrivateKey: ... +class DHPublicKey: ... +class DHParameters: ... + +class DHPrivateNumbers: + def __init__(self, x: int, public_numbers: DHPublicNumbers) -> None: ... + def private_key(self, backend: typing.Any = None) -> dh.DHPrivateKey: ... + @property + def x(self) -> int: ... + @property + def public_numbers(self) -> DHPublicNumbers: ... + +class DHPublicNumbers: + def __init__( + self, y: int, parameter_numbers: DHParameterNumbers + ) -> None: ... + def public_key(self, backend: typing.Any = None) -> dh.DHPublicKey: ... + @property + def y(self) -> int: ... + @property + def parameter_numbers(self) -> DHParameterNumbers: ... + +class DHParameterNumbers: + def __init__(self, p: int, g: int, q: int | None = None) -> None: ... + def parameters(self, backend: typing.Any = None) -> dh.DHParameters: ... + @property + def p(self) -> int: ... + @property + def g(self) -> int: ... + @property + def q(self) -> int | None: ... + +def generate_parameters( + generator: int, key_size: int, backend: typing.Any = None +) -> dh.DHParameters: ... +def from_pem_parameters( + data: bytes, backend: typing.Any = None +) -> dh.DHParameters: ... +def from_der_parameters( + data: bytes, backend: typing.Any = None +) -> dh.DHParameters: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/dsa.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/dsa.pyi new file mode 100644 index 0000000..0922a4c --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/dsa.pyi @@ -0,0 +1,41 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import typing + +from cryptography.hazmat.primitives.asymmetric import dsa + +class DSAPrivateKey: ... +class DSAPublicKey: ... +class DSAParameters: ... + +class DSAPrivateNumbers: + def __init__(self, x: int, public_numbers: DSAPublicNumbers) -> None: ... + @property + def x(self) -> int: ... + @property + def public_numbers(self) -> DSAPublicNumbers: ... + def private_key(self, backend: typing.Any = None) -> dsa.DSAPrivateKey: ... + +class DSAPublicNumbers: + def __init__( + self, y: int, parameter_numbers: DSAParameterNumbers + ) -> None: ... + @property + def y(self) -> int: ... + @property + def parameter_numbers(self) -> DSAParameterNumbers: ... + def public_key(self, backend: typing.Any = None) -> dsa.DSAPublicKey: ... + +class DSAParameterNumbers: + def __init__(self, p: int, q: int, g: int) -> None: ... + @property + def p(self) -> int: ... + @property + def q(self) -> int: ... + @property + def g(self) -> int: ... + def parameters(self, backend: typing.Any = None) -> dsa.DSAParameters: ... + +def generate_parameters(key_size: int) -> dsa.DSAParameters: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/ec.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/ec.pyi new file mode 100644 index 0000000..5c3b7bf --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/ec.pyi @@ -0,0 +1,52 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import typing + +from cryptography.hazmat.primitives.asymmetric import ec + +class ECPrivateKey: ... +class ECPublicKey: ... + +class EllipticCurvePrivateNumbers: + def __init__( + self, private_value: int, public_numbers: EllipticCurvePublicNumbers + ) -> None: ... + def private_key( + self, backend: typing.Any = None + ) -> ec.EllipticCurvePrivateKey: ... + @property + def private_value(self) -> int: ... + @property + def public_numbers(self) -> EllipticCurvePublicNumbers: ... + +class EllipticCurvePublicNumbers: + def __init__(self, x: int, y: int, curve: ec.EllipticCurve) -> None: ... + def public_key( + self, backend: typing.Any = None + ) -> ec.EllipticCurvePublicKey: ... + @property + def x(self) -> int: ... + @property + def y(self) -> int: ... + @property + def curve(self) -> ec.EllipticCurve: ... + def __eq__(self, other: object) -> bool: ... + +def curve_supported(curve: ec.EllipticCurve) -> bool: ... +def generate_private_key( + curve: ec.EllipticCurve, backend: typing.Any = None +) -> ec.EllipticCurvePrivateKey: ... +def from_private_numbers( + numbers: ec.EllipticCurvePrivateNumbers, +) -> ec.EllipticCurvePrivateKey: ... +def from_public_numbers( + numbers: ec.EllipticCurvePublicNumbers, +) -> ec.EllipticCurvePublicKey: ... +def from_public_bytes( + curve: ec.EllipticCurve, data: bytes +) -> ec.EllipticCurvePublicKey: ... +def derive_private_key( + private_value: int, curve: ec.EllipticCurve +) -> ec.EllipticCurvePrivateKey: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/ed25519.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/ed25519.pyi new file mode 100644 index 0000000..f85b3d1 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/ed25519.pyi @@ -0,0 +1,13 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from cryptography.hazmat.primitives.asymmetric import ed25519 +from cryptography.utils import Buffer + +class Ed25519PrivateKey: ... +class Ed25519PublicKey: ... + +def generate_key() -> ed25519.Ed25519PrivateKey: ... +def from_private_bytes(data: Buffer) -> ed25519.Ed25519PrivateKey: ... +def from_public_bytes(data: bytes) -> ed25519.Ed25519PublicKey: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/ed448.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/ed448.pyi new file mode 100644 index 0000000..c8ca0ec --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/ed448.pyi @@ -0,0 +1,13 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from cryptography.hazmat.primitives.asymmetric import ed448 +from cryptography.utils import Buffer + +class Ed448PrivateKey: ... +class Ed448PublicKey: ... + +def generate_key() -> ed448.Ed448PrivateKey: ... +def from_private_bytes(data: Buffer) -> ed448.Ed448PrivateKey: ... +def from_public_bytes(data: bytes) -> ed448.Ed448PublicKey: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/hashes.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/hashes.pyi new file mode 100644 index 0000000..106b531 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/hashes.pyi @@ -0,0 +1,30 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import typing + +from cryptography.hazmat.primitives import hashes +from cryptography.utils import Buffer + +class Hash(hashes.HashContext): + def __init__( + self, algorithm: hashes.HashAlgorithm, backend: typing.Any = None + ) -> None: ... + @property + def algorithm(self) -> hashes.HashAlgorithm: ... + def update(self, data: Buffer) -> None: ... + def finalize(self) -> bytes: ... + def copy(self) -> Hash: ... + @staticmethod + def hash(algorithm: hashes.HashAlgorithm, data: Buffer) -> bytes: ... + +def hash_supported(algorithm: hashes.HashAlgorithm) -> bool: ... + +class XOFHash: + def __init__(self, algorithm: hashes.ExtendableOutputFunction) -> None: ... + @property + def algorithm(self) -> hashes.ExtendableOutputFunction: ... + def update(self, data: Buffer) -> None: ... + def squeeze(self, length: int) -> bytes: ... + def copy(self) -> XOFHash: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/hmac.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/hmac.pyi new file mode 100644 index 0000000..3883d1b --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/hmac.pyi @@ -0,0 +1,22 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import typing + +from cryptography.hazmat.primitives import hashes +from cryptography.utils import Buffer + +class HMAC(hashes.HashContext): + def __init__( + self, + key: Buffer, + algorithm: hashes.HashAlgorithm, + backend: typing.Any = None, + ) -> None: ... + @property + def algorithm(self) -> hashes.HashAlgorithm: ... + def update(self, data: Buffer) -> None: ... + def finalize(self) -> bytes: ... + def verify(self, signature: bytes) -> None: ... + def copy(self) -> HMAC: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/hpke.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/hpke.pyi new file mode 100644 index 0000000..7ea1711 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/hpke.pyi @@ -0,0 +1,109 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from cryptography.hazmat.primitives.asymmetric import ec, mlkem, x25519 +from cryptography.utils import Buffer + +class KEM: + X25519: KEM + P256: KEM + P384: KEM + P521: KEM + MLKEM768: KEM + MLKEM1024: KEM + MLKEM768_X25519: KEM + MLKEM1024_P384: KEM + def enc_length(self) -> int: ... + +class KDF: + HKDF_SHA256: KDF + HKDF_SHA384: KDF + HKDF_SHA512: KDF + SHAKE128: KDF + SHAKE256: KDF + +class AEAD: + AES_128_GCM: AEAD + AES_256_GCM: AEAD + CHACHA20_POLY1305: AEAD + +class MLKEM768X25519PrivateKey: + def __init__( + self, + mlkem_key: mlkem.MLKEM768PrivateKey, + x25519_key: x25519.X25519PrivateKey, + ) -> None: ... + def public_key(self) -> MLKEM768X25519PublicKey: ... + +class MLKEM768X25519PublicKey: + def __init__( + self, + mlkem_key: mlkem.MLKEM768PublicKey, + x25519_key: x25519.X25519PublicKey, + ) -> None: ... + +class MLKEM1024P384PrivateKey: + def __init__( + self, + mlkem_key: mlkem.MLKEM1024PrivateKey, + p384_key: ec.EllipticCurvePrivateKey, + ) -> None: ... + def public_key(self) -> MLKEM1024P384PublicKey: ... + +class MLKEM1024P384PublicKey: + def __init__( + self, + mlkem_key: mlkem.MLKEM1024PublicKey, + p384_key: ec.EllipticCurvePublicKey, + ) -> None: ... + +class Suite: + def __init__(self, kem: KEM, kdf: KDF, aead: AEAD) -> None: ... + def encrypt( + self, + plaintext: Buffer, + public_key: x25519.X25519PublicKey + | ec.EllipticCurvePublicKey + | mlkem.MLKEM768PublicKey + | mlkem.MLKEM1024PublicKey + | MLKEM768X25519PublicKey + | MLKEM1024P384PublicKey, + info: Buffer | None = None, + ) -> bytes: ... + def decrypt( + self, + ciphertext: Buffer, + private_key: x25519.X25519PrivateKey + | ec.EllipticCurvePrivateKey + | mlkem.MLKEM768PrivateKey + | mlkem.MLKEM1024PrivateKey + | MLKEM768X25519PrivateKey + | MLKEM1024P384PrivateKey, + info: Buffer | None = None, + ) -> bytes: ... + +def _encrypt_with_aad( + suite: Suite, + plaintext: Buffer, + public_key: x25519.X25519PublicKey + | ec.EllipticCurvePublicKey + | mlkem.MLKEM768PublicKey + | mlkem.MLKEM1024PublicKey + | MLKEM768X25519PublicKey + | MLKEM1024P384PublicKey, + info: Buffer | None = None, + aad: Buffer | None = None, +) -> bytes: ... +def _decrypt_with_aad( + suite: Suite, + ciphertext: Buffer, + private_key: x25519.X25519PrivateKey + | ec.EllipticCurvePrivateKey + | mlkem.MLKEM768PrivateKey + | mlkem.MLKEM1024PrivateKey + | MLKEM768X25519PrivateKey + | MLKEM1024P384PrivateKey, + info: Buffer | None = None, + aad: Buffer | None = None, +) -> bytes: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi new file mode 100644 index 0000000..bb37b96 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi @@ -0,0 +1,205 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import typing + +from cryptography.hazmat.primitives.hashes import HashAlgorithm +from cryptography.hazmat.primitives.kdf.kbkdf import CounterLocation, Mode +from cryptography.utils import Buffer + +class PBKDF2HMAC: + def __init__( + self, + algorithm: HashAlgorithm, + length: int, + salt: bytes, + iterations: int, + backend: typing.Any = None, + ) -> None: ... + def derive(self, key_material: Buffer) -> bytes: ... + def derive_into(self, key_material: Buffer, buffer: Buffer) -> int: ... + def verify(self, key_material: bytes, expected_key: bytes) -> None: ... + +class Scrypt: + def __init__( + self, + salt: bytes, + length: int, + n: int, + r: int, + p: int, + backend: typing.Any = None, + ) -> None: ... + def derive(self, key_material: Buffer) -> bytes: ... + def derive_into(self, key_material: Buffer, buffer: Buffer) -> int: ... + def verify(self, key_material: bytes, expected_key: bytes) -> None: ... + +class Argon2d: + def __init__( + self, + *, + salt: bytes, + length: int, + iterations: int, + lanes: int, + memory_cost: int, + ad: bytes | None = None, + secret: bytes | None = None, + ) -> None: ... + def derive(self, key_material: bytes) -> bytes: ... + def derive_into(self, key_material: bytes, buffer: Buffer) -> int: ... + def verify(self, key_material: bytes, expected_key: bytes) -> None: ... + def derive_phc_encoded(self, key_material: bytes) -> str: ... + @classmethod + def verify_phc_encoded( + cls, key_material: bytes, phc_encoded: str, secret: bytes | None = None + ) -> None: ... + +class Argon2i: + def __init__( + self, + *, + salt: bytes, + length: int, + iterations: int, + lanes: int, + memory_cost: int, + ad: bytes | None = None, + secret: bytes | None = None, + ) -> None: ... + def derive(self, key_material: bytes) -> bytes: ... + def derive_into(self, key_material: bytes, buffer: Buffer) -> int: ... + def verify(self, key_material: bytes, expected_key: bytes) -> None: ... + def derive_phc_encoded(self, key_material: bytes) -> str: ... + @classmethod + def verify_phc_encoded( + cls, key_material: bytes, phc_encoded: str, secret: bytes | None = None + ) -> None: ... + +class Argon2id: + def __init__( + self, + *, + salt: bytes, + length: int, + iterations: int, + lanes: int, + memory_cost: int, + ad: bytes | None = None, + secret: bytes | None = None, + ) -> None: ... + def derive(self, key_material: bytes) -> bytes: ... + def derive_into(self, key_material: bytes, buffer: Buffer) -> int: ... + def verify(self, key_material: bytes, expected_key: bytes) -> None: ... + def derive_phc_encoded(self, key_material: bytes) -> str: ... + @classmethod + def verify_phc_encoded( + cls, key_material: bytes, phc_encoded: str, secret: bytes | None = None + ) -> None: ... + +class HKDF: + def __init__( + self, + algorithm: HashAlgorithm, + length: int, + salt: bytes | None, + info: bytes | None, + backend: typing.Any = None, + ): ... + @staticmethod + def extract( + algorithm: HashAlgorithm, salt: bytes | None, key_material: Buffer + ) -> bytes: ... + def derive(self, key_material: Buffer) -> bytes: ... + def derive_into(self, key_material: Buffer, buffer: Buffer) -> int: ... + def verify(self, key_material: bytes, expected_key: bytes) -> None: ... + +class HKDFExpand: + def __init__( + self, + algorithm: HashAlgorithm, + length: int, + info: bytes | None, + backend: typing.Any = None, + ): ... + def derive(self, key_material: Buffer) -> bytes: ... + def derive_into(self, key_material: Buffer, buffer: Buffer) -> int: ... + def verify(self, key_material: bytes, expected_key: bytes) -> None: ... + +class X963KDF: + def __init__( + self, + algorithm: HashAlgorithm, + length: int, + sharedinfo: bytes | None, + backend: typing.Any = None, + ) -> None: ... + def derive(self, key_material: Buffer) -> bytes: ... + def derive_into(self, key_material: Buffer, buffer: Buffer) -> int: ... + def verify(self, key_material: bytes, expected_key: bytes) -> None: ... + +class ConcatKDFHash: + def __init__( + self, + algorithm: HashAlgorithm, + length: int, + otherinfo: bytes | None, + backend: typing.Any = None, + ) -> None: ... + def derive(self, key_material: Buffer) -> bytes: ... + def derive_into(self, key_material: Buffer, buffer: Buffer) -> int: ... + def verify(self, key_material: bytes, expected_key: bytes) -> None: ... + +class ConcatKDFHMAC: + def __init__( + self, + algorithm: HashAlgorithm, + length: int, + salt: bytes | None, + otherinfo: bytes | None, + backend: typing.Any = None, + ) -> None: ... + def derive(self, key_material: Buffer) -> bytes: ... + def derive_into(self, key_material: Buffer, buffer: Buffer) -> int: ... + def verify(self, key_material: bytes, expected_key: bytes) -> None: ... + +class KBKDFHMAC: + def __init__( + self, + algorithm: HashAlgorithm, + mode: Mode, + length: int, + rlen: int, + llen: int | None, + location: CounterLocation, + label: bytes | None, + context: bytes | None, + fixed: bytes | None, + backend: typing.Any = None, + *, + break_location: int | None = None, + ) -> None: ... + def derive(self, key_material: Buffer) -> bytes: ... + def derive_into(self, key_material: Buffer, buffer: Buffer) -> int: ... + def verify(self, key_material: bytes, expected_key: bytes) -> None: ... + +class KBKDFCMAC: + def __init__( + self, + algorithm: typing.Any, + mode: Mode, + length: int, + rlen: int, + llen: int | None, + location: CounterLocation, + label: bytes | None, + context: bytes | None, + fixed: bytes | None, + backend: typing.Any = None, + *, + break_location: int | None = None, + ) -> None: ... + def derive(self, key_material: Buffer) -> bytes: ... + def derive_into(self, key_material: Buffer, buffer: Buffer) -> int: ... + def verify(self, key_material: bytes, expected_key: bytes) -> None: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/keys.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/keys.pyi new file mode 100644 index 0000000..404057e --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/keys.pyi @@ -0,0 +1,34 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import typing + +from cryptography.hazmat.primitives.asymmetric.types import ( + PrivateKeyTypes, + PublicKeyTypes, +) +from cryptography.utils import Buffer + +def load_der_private_key( + data: Buffer, + password: bytes | None, + backend: typing.Any = None, + *, + unsafe_skip_rsa_key_validation: bool = False, +) -> PrivateKeyTypes: ... +def load_pem_private_key( + data: Buffer, + password: bytes | None, + backend: typing.Any = None, + *, + unsafe_skip_rsa_key_validation: bool = False, +) -> PrivateKeyTypes: ... +def load_der_public_key( + data: bytes, + backend: typing.Any = None, +) -> PublicKeyTypes: ... +def load_pem_public_key( + data: bytes, + backend: typing.Any = None, +) -> PublicKeyTypes: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/keywrap.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/keywrap.pyi new file mode 100644 index 0000000..dd445ea --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/keywrap.pyi @@ -0,0 +1,28 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import typing + +from cryptography.utils import Buffer + +def aes_key_wrap( + wrapping_key: Buffer, + key_to_wrap: Buffer, + backend: typing.Any = None, +) -> bytes: ... +def aes_key_unwrap( + wrapping_key: Buffer, + wrapped_key: Buffer, + backend: typing.Any = None, +) -> bytes: ... +def aes_key_wrap_with_padding( + wrapping_key: Buffer, + key_to_wrap: Buffer, + backend: typing.Any = None, +) -> bytes: ... +def aes_key_unwrap_with_padding( + wrapping_key: Buffer, + wrapped_key: Buffer, + backend: typing.Any = None, +) -> bytes: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/mldsa.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/mldsa.pyi new file mode 100644 index 0000000..cea9cb1 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/mldsa.pyi @@ -0,0 +1,35 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from cryptography.hazmat.primitives.asymmetric import mldsa +from cryptography.utils import Buffer + +class MLDSA44PrivateKey: ... +class MLDSA44PublicKey: ... +class MLDSA65PrivateKey: ... +class MLDSA65PublicKey: ... +class MLDSA87PrivateKey: ... +class MLDSA87PublicKey: ... + +class MLDSAMuHasher: + def __init__( + self, + public_key: mldsa.MLDSA44PublicKey + | mldsa.MLDSA65PublicKey + | mldsa.MLDSA87PublicKey, + context: Buffer | None = None, + ) -> None: ... + def update(self, data: Buffer) -> None: ... + def copy(self) -> MLDSAMuHasher: ... + def finalize(self) -> bytes: ... + +def generate_mldsa44_key() -> mldsa.MLDSA44PrivateKey: ... +def from_mldsa44_public_bytes(data: bytes) -> mldsa.MLDSA44PublicKey: ... +def from_mldsa44_seed_bytes(data: Buffer) -> mldsa.MLDSA44PrivateKey: ... +def generate_mldsa65_key() -> mldsa.MLDSA65PrivateKey: ... +def from_mldsa65_public_bytes(data: bytes) -> mldsa.MLDSA65PublicKey: ... +def from_mldsa65_seed_bytes(data: Buffer) -> mldsa.MLDSA65PrivateKey: ... +def generate_mldsa87_key() -> mldsa.MLDSA87PrivateKey: ... +def from_mldsa87_public_bytes(data: bytes) -> mldsa.MLDSA87PublicKey: ... +def from_mldsa87_seed_bytes(data: Buffer) -> mldsa.MLDSA87PrivateKey: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/mlkem.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/mlkem.pyi new file mode 100644 index 0000000..768a340 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/mlkem.pyi @@ -0,0 +1,18 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from cryptography.hazmat.primitives.asymmetric import mlkem +from cryptography.utils import Buffer + +class MLKEM768PrivateKey: ... +class MLKEM768PublicKey: ... +class MLKEM1024PrivateKey: ... +class MLKEM1024PublicKey: ... + +def generate_mlkem768_key() -> mlkem.MLKEM768PrivateKey: ... +def from_mlkem768_seed_bytes(data: Buffer) -> mlkem.MLKEM768PrivateKey: ... +def from_mlkem768_public_bytes(data: Buffer) -> mlkem.MLKEM768PublicKey: ... +def generate_mlkem1024_key() -> mlkem.MLKEM1024PrivateKey: ... +def from_mlkem1024_seed_bytes(data: Buffer) -> mlkem.MLKEM1024PrivateKey: ... +def from_mlkem1024_public_bytes(data: Buffer) -> mlkem.MLKEM1024PublicKey: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/poly1305.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/poly1305.pyi new file mode 100644 index 0000000..45a2a39 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/poly1305.pyi @@ -0,0 +1,15 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from cryptography.utils import Buffer + +class Poly1305: + def __init__(self, key: Buffer) -> None: ... + @staticmethod + def generate_tag(key: Buffer, data: Buffer) -> bytes: ... + @staticmethod + def verify_tag(key: Buffer, data: Buffer, tag: bytes) -> None: ... + def update(self, data: Buffer) -> None: ... + def finalize(self) -> bytes: ... + def verify(self, tag: bytes) -> None: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/rsa.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/rsa.pyi new file mode 100644 index 0000000..ef7752d --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/rsa.pyi @@ -0,0 +1,55 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import typing + +from cryptography.hazmat.primitives.asymmetric import rsa + +class RSAPrivateKey: ... +class RSAPublicKey: ... + +class RSAPrivateNumbers: + def __init__( + self, + p: int, + q: int, + d: int, + dmp1: int, + dmq1: int, + iqmp: int, + public_numbers: RSAPublicNumbers, + ) -> None: ... + @property + def p(self) -> int: ... + @property + def q(self) -> int: ... + @property + def d(self) -> int: ... + @property + def dmp1(self) -> int: ... + @property + def dmq1(self) -> int: ... + @property + def iqmp(self) -> int: ... + @property + def public_numbers(self) -> RSAPublicNumbers: ... + def private_key( + self, + backend: typing.Any = None, + *, + unsafe_skip_rsa_key_validation: bool = False, + ) -> rsa.RSAPrivateKey: ... + +class RSAPublicNumbers: + def __init__(self, e: int, n: int) -> None: ... + @property + def n(self) -> int: ... + @property + def e(self) -> int: ... + def public_key(self, backend: typing.Any = None) -> rsa.RSAPublicKey: ... + +def generate_private_key( + public_exponent: int, + key_size: int, +) -> rsa.RSAPrivateKey: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/x25519.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/x25519.pyi new file mode 100644 index 0000000..38d2add --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/x25519.pyi @@ -0,0 +1,13 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from cryptography.hazmat.primitives.asymmetric import x25519 +from cryptography.utils import Buffer + +class X25519PrivateKey: ... +class X25519PublicKey: ... + +def generate_key() -> x25519.X25519PrivateKey: ... +def from_private_bytes(data: Buffer) -> x25519.X25519PrivateKey: ... +def from_public_bytes(data: bytes) -> x25519.X25519PublicKey: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/x448.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/x448.pyi new file mode 100644 index 0000000..3ac0980 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/openssl/x448.pyi @@ -0,0 +1,13 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from cryptography.hazmat.primitives.asymmetric import x448 +from cryptography.utils import Buffer + +class X448PrivateKey: ... +class X448PublicKey: ... + +def generate_key() -> x448.X448PrivateKey: ... +def from_private_bytes(data: Buffer) -> x448.X448PrivateKey: ... +def from_public_bytes(data: bytes) -> x448.X448PublicKey: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/pkcs12.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/pkcs12.pyi new file mode 100644 index 0000000..b25becb --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/pkcs12.pyi @@ -0,0 +1,52 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import typing +from collections.abc import Iterable + +from cryptography import x509 +from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes +from cryptography.hazmat.primitives.serialization import ( + KeySerializationEncryption, +) +from cryptography.hazmat.primitives.serialization.pkcs12 import ( + PKCS12KeyAndCertificates, + PKCS12PrivateKeyTypes, +) +from cryptography.utils import Buffer + +class PKCS12Certificate: + def __init__( + self, cert: x509.Certificate, friendly_name: bytes | None + ) -> None: ... + @property + def friendly_name(self) -> bytes | None: ... + @property + def certificate(self) -> x509.Certificate: ... + +def load_key_and_certificates( + data: Buffer, + password: Buffer | None, + backend: typing.Any = None, +) -> tuple[ + PrivateKeyTypes | None, + x509.Certificate | None, + list[x509.Certificate], +]: ... +def load_pkcs12( + data: bytes, + password: bytes | None, + backend: typing.Any = None, +) -> PKCS12KeyAndCertificates: ... +def serialize_java_truststore( + certs: Iterable[PKCS12Certificate], + encryption_algorithm: KeySerializationEncryption, +) -> bytes: ... +def serialize_key_and_certificates( + name: bytes | None, + key: PKCS12PrivateKeyTypes | None, + cert: x509.Certificate | None, + cas: Iterable[x509.Certificate | PKCS12Certificate] | None, + encryption_algorithm: KeySerializationEncryption, +) -> bytes: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/pkcs7.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/pkcs7.pyi new file mode 100644 index 0000000..358b135 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/pkcs7.pyi @@ -0,0 +1,50 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from collections.abc import Iterable + +from cryptography import x509 +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.serialization import pkcs7 + +def serialize_certificates( + certs: list[x509.Certificate], + encoding: serialization.Encoding, +) -> bytes: ... +def encrypt_and_serialize( + builder: pkcs7.PKCS7EnvelopeBuilder, + content_encryption_algorithm: pkcs7.ContentEncryptionAlgorithm, + encoding: serialization.Encoding, + options: Iterable[pkcs7.PKCS7Options], +) -> bytes: ... +def sign_and_serialize( + builder: pkcs7.PKCS7SignatureBuilder, + encoding: serialization.Encoding, + options: Iterable[pkcs7.PKCS7Options], +) -> bytes: ... +def decrypt_der( + data: bytes, + certificate: x509.Certificate, + private_key: rsa.RSAPrivateKey, + options: Iterable[pkcs7.PKCS7Options], +) -> bytes: ... +def decrypt_pem( + data: bytes, + certificate: x509.Certificate, + private_key: rsa.RSAPrivateKey, + options: Iterable[pkcs7.PKCS7Options], +) -> bytes: ... +def decrypt_smime( + data: bytes, + certificate: x509.Certificate, + private_key: rsa.RSAPrivateKey, + options: Iterable[pkcs7.PKCS7Options], +) -> bytes: ... +def load_pem_pkcs7_certificates( + data: bytes, +) -> list[x509.Certificate]: ... +def load_der_pkcs7_certificates( + data: bytes, +) -> list[x509.Certificate]: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/test_support.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/test_support.pyi new file mode 100644 index 0000000..c6c6d0b --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/test_support.pyi @@ -0,0 +1,23 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from cryptography import x509 +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.serialization import pkcs7 +from cryptography.utils import Buffer + +class TestCertificate: + not_after_tag: int + not_before_tag: int + issuer_value_tags: list[int] + subject_value_tags: list[int] + +def test_parse_certificate(data: bytes) -> TestCertificate: ... +def pkcs7_verify( + encoding: serialization.Encoding, + sig: bytes, + msg: Buffer | None, + certs: list[x509.Certificate], + options: list[pkcs7.PKCS7Options], +) -> None: ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/x509.pyi b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/x509.pyi new file mode 100644 index 0000000..4438cc7 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/_rust/x509.pyi @@ -0,0 +1,313 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import datetime +import typing +from collections.abc import Iterator + +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric.ec import ECDSA +from cryptography.hazmat.primitives.asymmetric.padding import PSS, PKCS1v15 +from cryptography.hazmat.primitives.asymmetric.types import ( + CertificateIssuerPublicKeyTypes, + CertificatePublicKeyTypes, + PrivateKeyTypes, +) +from cryptography.x509 import certificate_transparency + +def load_pem_x509_certificate( + data: bytes, backend: typing.Any = None +) -> x509.Certificate: ... +def load_der_x509_certificate( + data: bytes, backend: typing.Any = None +) -> x509.Certificate: ... +def load_pem_x509_certificates( + data: bytes, +) -> list[x509.Certificate]: ... +def load_pem_x509_crl( + data: bytes, backend: typing.Any = None +) -> x509.CertificateRevocationList: ... +def load_der_x509_crl( + data: bytes, backend: typing.Any = None +) -> x509.CertificateRevocationList: ... +def load_pem_x509_csr( + data: bytes, backend: typing.Any = None +) -> x509.CertificateSigningRequest: ... +def load_der_x509_csr( + data: bytes, backend: typing.Any = None +) -> x509.CertificateSigningRequest: ... +def encode_name_bytes(name: x509.Name) -> bytes: ... +def parse_name_bytes(data: bytes) -> x509.Name: ... +def encode_extension_value(extension: x509.ExtensionType) -> bytes: ... +def create_x509_certificate( + builder: x509.CertificateBuilder, + private_key: PrivateKeyTypes | None, + hash_algorithm: hashes.HashAlgorithm | None, + rsa_padding: PKCS1v15 | PSS | None, + ecdsa_deterministic: bool | None, +) -> x509.Certificate: ... +def create_x509_csr( + builder: x509.CertificateSigningRequestBuilder, + private_key: PrivateKeyTypes, + hash_algorithm: hashes.HashAlgorithm | None, + rsa_padding: PKCS1v15 | PSS | None, + ecdsa_deterministic: bool | None, +) -> x509.CertificateSigningRequest: ... +def create_revoked_certificate( + builder: x509.RevokedCertificateBuilder, +) -> x509.RevokedCertificate: ... +def create_x509_crl( + builder: x509.CertificateRevocationListBuilder, + private_key: PrivateKeyTypes, + hash_algorithm: hashes.HashAlgorithm | None, + rsa_padding: PKCS1v15 | PSS | None, + ecdsa_deterministic: bool | None, +) -> x509.CertificateRevocationList: ... + +class Sct: + @property + def version(self) -> certificate_transparency.Version: ... + @property + def log_id(self) -> bytes: ... + @property + def timestamp(self) -> datetime.datetime: ... + @property + def entry_type(self) -> certificate_transparency.LogEntryType: ... + @property + def signature_hash_algorithm(self) -> hashes.HashAlgorithm: ... + @property + def signature_algorithm( + self, + ) -> certificate_transparency.SignatureAlgorithm: ... + @property + def signature(self) -> bytes: ... + @property + def extension_bytes(self) -> bytes: ... + +class Certificate: + def fingerprint(self, algorithm: hashes.HashAlgorithm) -> bytes: ... + @property + def serial_number(self) -> int: ... + @property + def version(self) -> x509.Version: ... + def public_key(self) -> CertificatePublicKeyTypes: ... + @property + def public_key_algorithm_oid(self) -> x509.ObjectIdentifier: ... + @property + def not_valid_before(self) -> datetime.datetime: ... + @property + def not_valid_before_utc(self) -> datetime.datetime: ... + @property + def not_valid_after(self) -> datetime.datetime: ... + @property + def not_valid_after_utc(self) -> datetime.datetime: ... + @property + def issuer(self) -> x509.Name: ... + @property + def subject(self) -> x509.Name: ... + @property + def signature_hash_algorithm( + self, + ) -> hashes.HashAlgorithm | None: ... + @property + def signature_algorithm_oid(self) -> x509.ObjectIdentifier: ... + @property + def signature_algorithm_parameters( + self, + ) -> PSS | PKCS1v15 | ECDSA | None: ... + @property + def extensions(self) -> x509.Extensions: ... + @property + def signature(self) -> bytes: ... + @property + def tbs_certificate_bytes(self) -> bytes: ... + @property + def tbs_precertificate_bytes(self) -> bytes: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + def public_bytes(self, encoding: serialization.Encoding) -> bytes: ... + def verify_directly_issued_by(self, issuer: Certificate) -> None: ... + +class RevokedCertificate: + @property + def serial_number(self) -> int: ... + @property + def revocation_date(self) -> datetime.datetime: ... + @property + def revocation_date_utc(self) -> datetime.datetime: ... + @property + def extensions(self) -> x509.Extensions: ... + +class CertificateRevocationList: + def public_bytes(self, encoding: serialization.Encoding) -> bytes: ... + def fingerprint(self, algorithm: hashes.HashAlgorithm) -> bytes: ... + def get_revoked_certificate_by_serial_number( + self, serial_number: int + ) -> x509.RevokedCertificate | None: ... + @property + def signature_hash_algorithm( + self, + ) -> hashes.HashAlgorithm | None: ... + @property + def signature_algorithm_oid(self) -> x509.ObjectIdentifier: ... + @property + def signature_algorithm_parameters( + self, + ) -> PSS | PKCS1v15 | ECDSA | None: ... + @property + def issuer(self) -> x509.Name: ... + @property + def next_update(self) -> datetime.datetime | None: ... + @property + def next_update_utc(self) -> datetime.datetime | None: ... + @property + def last_update(self) -> datetime.datetime: ... + @property + def last_update_utc(self) -> datetime.datetime: ... + @property + def extensions(self) -> x509.Extensions: ... + @property + def signature(self) -> bytes: ... + @property + def tbs_certlist_bytes(self) -> bytes: ... + def __eq__(self, other: object) -> bool: ... + def __len__(self) -> int: ... + @typing.overload + def __getitem__(self, idx: int) -> x509.RevokedCertificate: ... + @typing.overload + def __getitem__(self, idx: slice) -> list[x509.RevokedCertificate]: ... + def __iter__(self) -> Iterator[x509.RevokedCertificate]: ... + def is_signature_valid( + self, public_key: CertificateIssuerPublicKeyTypes + ) -> bool: ... + +class CertificateSigningRequest: + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + def public_key(self) -> CertificatePublicKeyTypes: ... + @property + def subject(self) -> x509.Name: ... + @property + def signature_hash_algorithm( + self, + ) -> hashes.HashAlgorithm | None: ... + @property + def signature_algorithm_oid(self) -> x509.ObjectIdentifier: ... + @property + def signature_algorithm_parameters( + self, + ) -> PSS | PKCS1v15 | ECDSA | None: ... + @property + def extensions(self) -> x509.Extensions: ... + @property + def attributes(self) -> x509.Attributes: ... + def public_bytes(self, encoding: serialization.Encoding) -> bytes: ... + @property + def signature(self) -> bytes: ... + @property + def tbs_certrequest_bytes(self) -> bytes: ... + @property + def is_signature_valid(self) -> bool: ... + +class PolicyBuilder: + def time(self, time: datetime.datetime) -> PolicyBuilder: ... + def store(self, store: Store) -> PolicyBuilder: ... + def max_chain_depth(self, max_chain_depth: int) -> PolicyBuilder: ... + def extension_policies( + self, *, ca_policy: ExtensionPolicy, ee_policy: ExtensionPolicy + ) -> PolicyBuilder: ... + def build_client_verifier(self) -> ClientVerifier: ... + def build_server_verifier( + self, subject: x509.verification.Subject + ) -> ServerVerifier: ... + +class Policy: + @property + def max_chain_depth(self) -> int: ... + @property + def subject(self) -> x509.verification.Subject | None: ... + @property + def validation_time(self) -> datetime.datetime: ... + @property + def extended_key_usage(self) -> x509.ObjectIdentifier: ... + @property + def minimum_rsa_modulus(self) -> int: ... + +class Criticality: + CRITICAL: Criticality + AGNOSTIC: Criticality + NON_CRITICAL: Criticality + +T = typing.TypeVar("T", contravariant=True, bound=x509.ExtensionType) + +MaybeExtensionValidatorCallback = typing.Callable[ + [ + Policy, + x509.Certificate, + T | None, + ], + None, +] + +PresentExtensionValidatorCallback = typing.Callable[ + [Policy, x509.Certificate, T], + None, +] + +class ExtensionPolicy: + @staticmethod + def permit_all() -> ExtensionPolicy: ... + @staticmethod + def webpki_defaults_ca() -> ExtensionPolicy: ... + @staticmethod + def webpki_defaults_ee() -> ExtensionPolicy: ... + def require_not_present( + self, extension_type: type[x509.ExtensionType] + ) -> ExtensionPolicy: ... + def may_be_present( + self, + extension_type: type[T], + criticality: Criticality, + validator: MaybeExtensionValidatorCallback[T] | None, + ) -> ExtensionPolicy: ... + def require_present( + self, + extension_type: type[T], + criticality: Criticality, + validator: PresentExtensionValidatorCallback[T] | None, + ) -> ExtensionPolicy: ... + +class VerifiedClient: + @property + def subjects(self) -> list[x509.GeneralName] | None: ... + @property + def chain(self) -> list[x509.Certificate]: ... + +class ClientVerifier: + @property + def policy(self) -> Policy: ... + @property + def store(self) -> Store: ... + def verify( + self, + leaf: x509.Certificate, + intermediates: list[x509.Certificate], + ) -> VerifiedClient: ... + +class ServerVerifier: + @property + def policy(self) -> Policy: ... + @property + def store(self) -> Store: ... + def verify( + self, + leaf: x509.Certificate, + intermediates: list[x509.Certificate], + ) -> list[x509.Certificate]: ... + +class Store: + def __init__(self, certs: list[x509.Certificate]) -> None: ... + +class VerificationError(Exception): ... diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/openssl/__init__.py b/source/target-vendors/cp312/cryptography/hazmat/bindings/openssl/__init__.py new file mode 100644 index 0000000..41d7318 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/openssl/__init__.py @@ -0,0 +1,5 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/openssl/_conditional.py b/source/target-vendors/cp312/cryptography/hazmat/bindings/openssl/_conditional.py new file mode 100644 index 0000000..1e447a5 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/openssl/_conditional.py @@ -0,0 +1,199 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + + +def cryptography_has_set_cert_cb() -> list[str]: + return [ + "SSL_CTX_set_cert_cb", + "SSL_set_cert_cb", + ] + + +def cryptography_has_ssl_st() -> list[str]: + return [ + "SSL_ST_BEFORE", + "SSL_ST_OK", + "SSL_ST_INIT", + "SSL_ST_RENEGOTIATE", + ] + + +def cryptography_has_tls_st() -> list[str]: + return [ + "TLS_ST_BEFORE", + "TLS_ST_OK", + ] + + +def cryptography_has_ssl_sigalgs() -> list[str]: + return [ + "SSL_CTX_set1_sigalgs_list", + ] + + +def cryptography_has_psk() -> list[str]: + return [ + "SSL_CTX_use_psk_identity_hint", + "SSL_CTX_set_psk_server_callback", + "SSL_CTX_set_psk_client_callback", + ] + + +def cryptography_has_psk_tlsv13() -> list[str]: + return [ + "SSL_CTX_set_psk_find_session_callback", + "SSL_CTX_set_psk_use_session_callback", + "Cryptography_SSL_SESSION_new", + "SSL_CIPHER_find", + "SSL_SESSION_set1_master_key", + "SSL_SESSION_set_cipher", + "SSL_SESSION_set_protocol_version", + ] + + +def cryptography_has_custom_ext() -> list[str]: + return [ + "SSL_CTX_add_client_custom_ext", + "SSL_CTX_add_server_custom_ext", + "SSL_extension_supported", + ] + + +def cryptography_has_tlsv13_functions() -> list[str]: + return [ + "SSL_CTX_set_ciphersuites", + ] + + +def cryptography_has_tlsv13_hs_functions() -> list[str]: + return [ + "SSL_VERIFY_POST_HANDSHAKE", + "SSL_verify_client_post_handshake", + "SSL_CTX_set_post_handshake_auth", + "SSL_set_post_handshake_auth", + "SSL_SESSION_get_max_early_data", + "SSL_write_early_data", + "SSL_read_early_data", + "SSL_CTX_set_max_early_data", + ] + + +def cryptography_has_ssl_verify_client_post_handshake() -> list[str]: + return [ + "SSL_verify_client_post_handshake", + ] + + +def cryptography_has_engine() -> list[str]: + return [ + "ENGINE_by_id", + "ENGINE_init", + "ENGINE_finish", + "ENGINE_get_default_RAND", + "ENGINE_set_default_RAND", + "ENGINE_unregister_RAND", + "ENGINE_ctrl_cmd", + "ENGINE_free", + "ENGINE_get_name", + "ENGINE_ctrl_cmd_string", + "ENGINE_load_builtin_engines", + "ENGINE_load_private_key", + "ENGINE_load_public_key", + "SSL_CTX_set_client_cert_engine", + ] + + +def cryptography_has_verified_chain() -> list[str]: + return [ + "SSL_get0_verified_chain", + ] + + +def cryptography_has_srtp() -> list[str]: + return [ + "SSL_CTX_set_tlsext_use_srtp", + "SSL_set_tlsext_use_srtp", + "SSL_get_selected_srtp_profile", + ] + + +def cryptography_has_dtls_get_data_mtu() -> list[str]: + return [ + "DTLS_get_data_mtu", + ] + + +def cryptography_has_ssl_cookie() -> list[str]: + return [ + "SSL_OP_COOKIE_EXCHANGE", + "DTLS1_COOKIE_LENGTH", + "DTLSv1_listen", + "SSL_CTX_set_cookie_generate_cb", + "SSL_CTX_set_cookie_verify_cb", + ] + + +def cryptography_has_prime_checks() -> list[str]: + return [ + "BN_prime_checks_for_size", + ] + + +def cryptography_has_unexpected_eof_while_reading() -> list[str]: + return ["SSL_R_UNEXPECTED_EOF_WHILE_READING"] + + +def cryptography_has_ssl_op_ignore_unexpected_eof() -> list[str]: + return [ + "SSL_OP_IGNORE_UNEXPECTED_EOF", + ] + + +def cryptography_has_get_extms_support() -> list[str]: + return ["SSL_get_extms_support"] + + +def cryptography_has_ssl_get0_group_name() -> list[str]: + return ["SSL_get0_group_name"] + + +# This is a mapping of +# {condition: function-returning-names-dependent-on-that-condition} so we can +# loop over them and delete unsupported names at runtime. It will be removed +# when cffi supports #if in cdef. We use functions instead of just a dict of +# lists so we can use coverage to measure which are used. +CONDITIONAL_NAMES = { + "Cryptography_HAS_SET_CERT_CB": cryptography_has_set_cert_cb, + "Cryptography_HAS_SSL_ST": cryptography_has_ssl_st, + "Cryptography_HAS_TLS_ST": cryptography_has_tls_st, + "Cryptography_HAS_SIGALGS": cryptography_has_ssl_sigalgs, + "Cryptography_HAS_PSK": cryptography_has_psk, + "Cryptography_HAS_PSK_TLSv1_3": cryptography_has_psk_tlsv13, + "Cryptography_HAS_CUSTOM_EXT": cryptography_has_custom_ext, + "Cryptography_HAS_TLSv1_3_FUNCTIONS": cryptography_has_tlsv13_functions, + "Cryptography_HAS_TLSv1_3_HS_FUNCTIONS": ( + cryptography_has_tlsv13_hs_functions + ), + "Cryptography_HAS_SSL_VERIFY_CLIENT_POST_HANDSHAKE": ( + cryptography_has_ssl_verify_client_post_handshake + ), + "Cryptography_HAS_ENGINE": cryptography_has_engine, + "Cryptography_HAS_VERIFIED_CHAIN": cryptography_has_verified_chain, + "Cryptography_HAS_SRTP": cryptography_has_srtp, + "Cryptography_HAS_DTLS_GET_DATA_MTU": cryptography_has_dtls_get_data_mtu, + "Cryptography_HAS_SSL_COOKIE": cryptography_has_ssl_cookie, + "Cryptography_HAS_PRIME_CHECKS": cryptography_has_prime_checks, + "Cryptography_HAS_UNEXPECTED_EOF_WHILE_READING": ( + cryptography_has_unexpected_eof_while_reading + ), + "Cryptography_HAS_SSL_OP_IGNORE_UNEXPECTED_EOF": ( + cryptography_has_ssl_op_ignore_unexpected_eof + ), + "Cryptography_HAS_GET_EXTMS_SUPPORT": cryptography_has_get_extms_support, + "Cryptography_HAS_SSL_GET0_GROUP_NAME": ( + cryptography_has_ssl_get0_group_name + ), +} diff --git a/source/target-vendors/cp312/cryptography/hazmat/bindings/openssl/binding.py b/source/target-vendors/cp312/cryptography/hazmat/bindings/openssl/binding.py new file mode 100644 index 0000000..6e13df1 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/bindings/openssl/binding.py @@ -0,0 +1,107 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import threading +import types +import typing +from collections.abc import Callable, Mapping + +import cryptography +from cryptography.exceptions import InternalError +from cryptography.hazmat.bindings._rust import _openssl, openssl +from cryptography.hazmat.bindings.openssl._conditional import CONDITIONAL_NAMES + + +def _openssl_assert(ok: bool) -> None: + if not ok: + errors = openssl.capture_error_stack() + + raise InternalError( + "Unknown OpenSSL error. This error is commonly encountered when " + "another library is not cleaning up the OpenSSL error stack. If " + "you are using cryptography with another library that uses " + "OpenSSL try disabling it before reporting a bug. Otherwise " + "please file an issue at https://github.com/pyca/cryptography/" + "issues with information on how to reproduce " + f"this. ({errors!r})", + errors, + ) + + +def build_conditional_library( + lib: typing.Any, + conditional_names: Mapping[str, Callable[[], list[str]]], +) -> typing.Any: + conditional_lib = types.ModuleType("lib") + conditional_lib._original_lib = lib # type: ignore[attr-defined] + excluded_names = set() + for condition, names_cb in conditional_names.items(): + if not getattr(lib, condition): + excluded_names.update(names_cb()) + + for attr in dir(lib): + if attr not in excluded_names: + setattr(conditional_lib, attr, getattr(lib, attr)) + + return conditional_lib + + +class Binding: + """ + OpenSSL API wrapper. + """ + + lib: typing.ClassVar[typing.Any] = None + ffi: typing.Any = _openssl.ffi + _lib_loaded = False + _init_lock = threading.Lock() + + def __init__(self) -> None: + self._ensure_ffi_initialized() + + @classmethod + def _ensure_ffi_initialized(cls) -> None: + with cls._init_lock: + if not cls._lib_loaded: + cls.lib = build_conditional_library( + _openssl.lib, CONDITIONAL_NAMES + ) + cls._lib_loaded = True + + @classmethod + def init_static_locks(cls) -> None: + cls._ensure_ffi_initialized() + + +def _verify_package_version(version: str) -> None: + # Occasionally we run into situations where the version of the Python + # package does not match the version of the shared object that is loaded. + # This may occur in environments where multiple versions of cryptography + # are installed and available in the python path. To avoid errors cropping + # up later this code checks that the currently imported package and the + # shared object that were loaded have the same version and raise an + # ImportError if they do not + so_package_version = _openssl.ffi.string( + _openssl.lib.CRYPTOGRAPHY_PACKAGE_VERSION + ) + if version.encode("ascii") != so_package_version: + raise ImportError( + "The version of cryptography does not match the loaded " + "shared object. This can happen if you have multiple copies of " + "cryptography installed in your Python path. Please try creating " + "a new virtual environment to resolve this issue. " + f"Loaded python version: {version}, " + f"shared object version: {so_package_version}" + ) + + _openssl_assert( + _openssl.lib.OpenSSL_version_num() == openssl.openssl_version(), + ) + + +_verify_package_version(cryptography.__version__) + +Binding.init_static_locks() diff --git a/source/target-vendors/cp312/cryptography/hazmat/decrepit/__init__.py b/source/target-vendors/cp312/cryptography/hazmat/decrepit/__init__.py new file mode 100644 index 0000000..41d7318 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/decrepit/__init__.py @@ -0,0 +1,5 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations diff --git a/source/target-vendors/cp312/cryptography/hazmat/decrepit/ciphers/__init__.py b/source/target-vendors/cp312/cryptography/hazmat/decrepit/ciphers/__init__.py new file mode 100644 index 0000000..41d7318 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/decrepit/ciphers/__init__.py @@ -0,0 +1,5 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations diff --git a/source/target-vendors/cp312/cryptography/hazmat/decrepit/ciphers/algorithms.py b/source/target-vendors/cp312/cryptography/hazmat/decrepit/ciphers/algorithms.py new file mode 100644 index 0000000..72dad04 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/decrepit/ciphers/algorithms.py @@ -0,0 +1,145 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import warnings + +from cryptography import utils +from cryptography.hazmat.primitives._cipheralgorithm import ( + BlockCipherAlgorithm, + CipherAlgorithm, + _verify_key_size, +) + + +class ARC4(CipherAlgorithm): + name = "RC4" + key_sizes = frozenset([40, 56, 64, 80, 128, 160, 192, 256]) + + def __init__(self, key: bytes): + self.key = _verify_key_size(self, key) + + @property + def key_size(self) -> int: + return len(self.key) * 8 + + +class TripleDES(BlockCipherAlgorithm): + name = "3DES" + block_size = 64 + key_sizes = frozenset([64, 128, 192]) + + def __init__(self, key: bytes): + # Check the key type before the length-based deprecation warnings so + # an invalid key type doesn't trigger a spurious warning. + utils._check_byteslike("key", key) + if len(key) == 8: + warnings.warn( + "Single-key TripleDES (8-byte keys) is deprecated and " + "support will be removed in a future release. Use 24-byte " + "keys instead (e.g., key + key + key).", + utils.DeprecatedIn47, + stacklevel=2, + ) + key = key + key + key + elif len(key) == 16: + warnings.warn( + "Two-key TripleDES (16-byte keys) is deprecated and " + "support will be removed in a future release. Use 24-byte " + "keys instead (e.g., key + key[:8]).", + utils.DeprecatedIn47, + stacklevel=2, + ) + key = key + key[:8] + self.key = _verify_key_size(self, key) + + @property + def key_size(self) -> int: + return len(self.key) * 8 + + +# Not actually supported, marker for tests +class _DES: + key_size = 64 + + +class Blowfish(BlockCipherAlgorithm): + name = "Blowfish" + block_size = 64 + key_sizes = frozenset(range(32, 449, 8)) + + def __init__(self, key: bytes): + self.key = _verify_key_size(self, key) + + @property + def key_size(self) -> int: + return len(self.key) * 8 + + +class CAST5(BlockCipherAlgorithm): + name = "CAST5" + block_size = 64 + key_sizes = frozenset(range(40, 129, 8)) + + def __init__(self, key: bytes): + self.key = _verify_key_size(self, key) + + @property + def key_size(self) -> int: + return len(self.key) * 8 + + +class SEED(BlockCipherAlgorithm): + name = "SEED" + block_size = 128 + key_sizes = frozenset([128]) + + def __init__(self, key: bytes): + self.key = _verify_key_size(self, key) + + @property + def key_size(self) -> int: + return len(self.key) * 8 + + +class IDEA(BlockCipherAlgorithm): + name = "IDEA" + block_size = 64 + key_sizes = frozenset([128]) + + def __init__(self, key: bytes): + self.key = _verify_key_size(self, key) + + @property + def key_size(self) -> int: + return len(self.key) * 8 + + +class Camellia(BlockCipherAlgorithm): + name = "camellia" + block_size = 128 + key_sizes = frozenset([128, 192, 256]) + + def __init__(self, key: bytes): + self.key = _verify_key_size(self, key) + + @property + def key_size(self) -> int: + return len(self.key) * 8 + + +# This class only allows RC2 with a 128-bit key. No support for +# effective key bits or other key sizes is provided. +class RC2(BlockCipherAlgorithm): + name = "RC2" + block_size = 64 + key_sizes = frozenset([128]) + + def __init__(self, key: bytes): + self.key = _verify_key_size(self, key) + + @property + def key_size(self) -> int: + return len(self.key) * 8 diff --git a/source/target-vendors/cp312/cryptography/hazmat/decrepit/ciphers/modes.py b/source/target-vendors/cp312/cryptography/hazmat/decrepit/ciphers/modes.py new file mode 100644 index 0000000..1786bb0 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/decrepit/ciphers/modes.py @@ -0,0 +1,53 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography import utils +from cryptography.hazmat.primitives._modes import ( + ModeWithInitializationVector, + _check_iv_and_key_length, +) + + +class OFB(ModeWithInitializationVector): + name = "OFB" + + def __init__(self, initialization_vector: utils.Buffer): + utils._check_byteslike("initialization_vector", initialization_vector) + self._initialization_vector = initialization_vector + + @property + def initialization_vector(self) -> utils.Buffer: + return self._initialization_vector + + validate_for_algorithm = _check_iv_and_key_length + + +class CFB(ModeWithInitializationVector): + name = "CFB" + + def __init__(self, initialization_vector: utils.Buffer): + utils._check_byteslike("initialization_vector", initialization_vector) + self._initialization_vector = initialization_vector + + @property + def initialization_vector(self) -> utils.Buffer: + return self._initialization_vector + + validate_for_algorithm = _check_iv_and_key_length + + +class CFB8(ModeWithInitializationVector): + name = "CFB8" + + def __init__(self, initialization_vector: utils.Buffer): + utils._check_byteslike("initialization_vector", initialization_vector) + self._initialization_vector = initialization_vector + + @property + def initialization_vector(self) -> utils.Buffer: + return self._initialization_vector + + validate_for_algorithm = _check_iv_and_key_length diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/__init__.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/__init__.py new file mode 100644 index 0000000..41d7318 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/__init__.py @@ -0,0 +1,5 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/_asymmetric.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/_asymmetric.py new file mode 100644 index 0000000..ea55ffd --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/_asymmetric.py @@ -0,0 +1,19 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import abc + +# This exists to break an import cycle. It is normally accessible from the +# asymmetric padding module. + + +class AsymmetricPadding(metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def name(self) -> str: + """ + A string naming this padding (e.g. "PSS", "PKCS1"). + """ diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/_cipheralgorithm.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/_cipheralgorithm.py new file mode 100644 index 0000000..305a9fd --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/_cipheralgorithm.py @@ -0,0 +1,60 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import abc + +from cryptography import utils + +# This exists to break an import cycle. It is normally accessible from the +# ciphers module. + + +class CipherAlgorithm(metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def name(self) -> str: + """ + A string naming this mode (e.g. "AES", "Camellia"). + """ + + @property + @abc.abstractmethod + def key_sizes(self) -> frozenset[int]: + """ + Valid key sizes for this algorithm in bits + """ + + @property + @abc.abstractmethod + def key_size(self) -> int: + """ + The size of the key being used as an integer in bits (e.g. 128, 256). + """ + + +class BlockCipherAlgorithm(CipherAlgorithm): + key: utils.Buffer + + @property + @abc.abstractmethod + def block_size(self) -> int: + """ + The size of a block as an integer in bits (e.g. 64, 128). + """ + + +def _verify_key_size( + algorithm: CipherAlgorithm, key: utils.Buffer +) -> utils.Buffer: + # Verify that the key is instance of bytes + utils._check_byteslike("key", key) + + # Verify that the key size matches the expected key size + if len(key) * 8 not in algorithm.key_sizes: + raise ValueError( + f"Invalid key size ({len(key) * 8}) for {algorithm.name}." + ) + return key diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/_modes.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/_modes.py new file mode 100644 index 0000000..deae8bc --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/_modes.py @@ -0,0 +1,105 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import abc + +from cryptography import utils +from cryptography.exceptions import UnsupportedAlgorithm, _Reasons +from cryptography.hazmat.primitives._cipheralgorithm import ( + BlockCipherAlgorithm, + CipherAlgorithm, +) + + +class Mode(metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def name(self) -> str: + """ + A string naming this mode (e.g. "ECB", "CBC"). + """ + + @abc.abstractmethod + def validate_for_algorithm(self, algorithm: CipherAlgorithm) -> None: + """ + Checks that all the necessary invariants of this (mode, algorithm) + combination are met. + """ + + +class ModeWithInitializationVector(Mode, metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def initialization_vector(self) -> utils.Buffer: + """ + The value of the initialization vector for this mode as bytes. + """ + + +class ModeWithTweak(Mode, metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def tweak(self) -> utils.Buffer: + """ + The value of the tweak for this mode as bytes. + """ + + +class ModeWithNonce(Mode, metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def nonce(self) -> utils.Buffer: + """ + The value of the nonce for this mode as bytes. + """ + + +class ModeWithAuthenticationTag(Mode, metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def tag(self) -> bytes | None: + """ + The value of the tag supplied to the constructor of this mode. + """ + + +def _check_aes_key_length(self: Mode, algorithm: CipherAlgorithm) -> None: + if algorithm.key_size > 256 and algorithm.name == "AES": + raise ValueError( + "Only 128, 192, and 256 bit keys are allowed for this AES mode" + ) + + +def _check_iv_length( + self: ModeWithInitializationVector, algorithm: BlockCipherAlgorithm +) -> None: + iv_len = len(self.initialization_vector) + if iv_len * 8 != algorithm.block_size: + raise ValueError(f"Invalid IV size ({iv_len}) for {self.name}.") + + +def _check_nonce_length( + nonce: utils.Buffer, name: str, algorithm: CipherAlgorithm +) -> None: + if not isinstance(algorithm, BlockCipherAlgorithm): + raise UnsupportedAlgorithm( + f"{name} requires a block cipher algorithm", + _Reasons.UNSUPPORTED_CIPHER, + ) + if len(nonce) * 8 != algorithm.block_size: + raise ValueError(f"Invalid nonce size ({len(nonce)}) for {name}.") + + +def _check_iv_and_key_length( + self: ModeWithInitializationVector, algorithm: CipherAlgorithm +) -> None: + if not isinstance(algorithm, BlockCipherAlgorithm): + raise UnsupportedAlgorithm( + f"{self} requires a block cipher algorithm", + _Reasons.UNSUPPORTED_CIPHER, + ) + _check_aes_key_length(self, algorithm) + _check_iv_length(self, algorithm) diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/_serialization.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/_serialization.py new file mode 100644 index 0000000..9c3c474 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/_serialization.py @@ -0,0 +1,136 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import abc + +from cryptography import utils +from cryptography.hazmat.bindings._rust import Encoding as Encoding +from cryptography.hazmat.bindings._rust import ( + ParameterFormat as ParameterFormat, +) +from cryptography.hazmat.bindings._rust import PrivateFormat as PrivateFormat +from cryptography.hazmat.bindings._rust import PublicFormat as PublicFormat +from cryptography.hazmat.primitives.hashes import HashAlgorithm + +# This exists to break an import cycle. These classes are normally accessible +# from the serialization module. + + +class PBES(utils.Enum): + PBESv1SHA1And3KeyTripleDESCBC = "PBESv1 using SHA1 and 3-Key TripleDES" + PBESv2SHA256AndAES256CBC = "PBESv2 using SHA256 PBKDF2 and AES256 CBC" + + +class KeySerializationEncryption(metaclass=abc.ABCMeta): + pass + + +class BestAvailableEncryption(KeySerializationEncryption): + def __init__(self, password: bytes): + if not isinstance(password, bytes) or len(password) == 0: + raise ValueError("Password must be 1 or more bytes.") + + self.password = password + + +class NoEncryption(KeySerializationEncryption): + pass + + +class KeySerializationEncryptionBuilder: + def __init__( + self, + format: PrivateFormat, + *, + _kdf_rounds: int | None = None, + _hmac_hash: HashAlgorithm | None = None, + _key_cert_algorithm: PBES | None = None, + ) -> None: + self._format = format + + self._kdf_rounds = _kdf_rounds + self._hmac_hash = _hmac_hash + self._key_cert_algorithm = _key_cert_algorithm + + def kdf_rounds(self, rounds: int) -> KeySerializationEncryptionBuilder: + if self._kdf_rounds is not None: + raise ValueError("kdf_rounds already set") + + if not isinstance(rounds, int): + raise TypeError("kdf_rounds must be an integer") + + if rounds < 1: + raise ValueError("kdf_rounds must be a positive integer") + + return KeySerializationEncryptionBuilder( + self._format, + _kdf_rounds=rounds, + _hmac_hash=self._hmac_hash, + _key_cert_algorithm=self._key_cert_algorithm, + ) + + def hmac_hash( + self, algorithm: HashAlgorithm + ) -> KeySerializationEncryptionBuilder: + if self._format is not PrivateFormat.PKCS12: + raise TypeError( + "hmac_hash only supported with PrivateFormat.PKCS12" + ) + + if self._hmac_hash is not None: + raise ValueError("hmac_hash already set") + return KeySerializationEncryptionBuilder( + self._format, + _kdf_rounds=self._kdf_rounds, + _hmac_hash=algorithm, + _key_cert_algorithm=self._key_cert_algorithm, + ) + + def key_cert_algorithm( + self, algorithm: PBES + ) -> KeySerializationEncryptionBuilder: + if self._format is not PrivateFormat.PKCS12: + raise TypeError( + "key_cert_algorithm only supported with PrivateFormat.PKCS12" + ) + if self._key_cert_algorithm is not None: + raise ValueError("key_cert_algorithm already set") + return KeySerializationEncryptionBuilder( + self._format, + _kdf_rounds=self._kdf_rounds, + _hmac_hash=self._hmac_hash, + _key_cert_algorithm=algorithm, + ) + + def build(self, password: bytes) -> KeySerializationEncryption: + if not isinstance(password, bytes) or len(password) == 0: + raise ValueError("Password must be 1 or more bytes.") + + return _KeySerializationEncryption( + self._format, + password, + kdf_rounds=self._kdf_rounds, + hmac_hash=self._hmac_hash, + key_cert_algorithm=self._key_cert_algorithm, + ) + + +class _KeySerializationEncryption(KeySerializationEncryption): + def __init__( + self, + format: PrivateFormat, + password: bytes, + *, + kdf_rounds: int | None, + hmac_hash: HashAlgorithm | None, + key_cert_algorithm: PBES | None, + ): + self._format = format + self.password = password + + self._kdf_rounds = kdf_rounds + self._hmac_hash = hmac_hash + self._key_cert_algorithm = key_cert_algorithm diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/__init__.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/__init__.py new file mode 100644 index 0000000..41d7318 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/__init__.py @@ -0,0 +1,5 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/dh.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/dh.py new file mode 100644 index 0000000..814fdd3 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/dh.py @@ -0,0 +1,253 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import abc + +from cryptography import utils +from cryptography.hazmat.bindings._rust import openssl as rust_openssl +from cryptography.hazmat.primitives import _serialization + +_FFDH_DEPRECATION_MSG = ( + "Diffie-Hellman over finite fields (FFDH) is deprecated and support " + "will be removed in a future release. Use a more modern key exchange " + "algorithm." +) + +generate_parameters = rust_openssl.dh.generate_parameters + + +DHPrivateNumbers = rust_openssl.dh.DHPrivateNumbers +DHPublicNumbers = rust_openssl.dh.DHPublicNumbers +DHParameterNumbers = rust_openssl.dh.DHParameterNumbers + + +class DHParameters(metaclass=abc.ABCMeta): + @abc.abstractmethod + def generate_private_key(self) -> DHPrivateKey: + """ + Generates and returns a DHPrivateKey. + """ + + @abc.abstractmethod + def parameter_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.ParameterFormat, + ) -> bytes: + """ + Returns the parameters serialized as bytes. + """ + + @abc.abstractmethod + def parameter_numbers(self) -> DHParameterNumbers: + """ + Returns a DHParameterNumbers. + """ + + +DHParametersWithSerialization = DHParameters +DHParameters.register(rust_openssl.dh.DHParameters) + + +class DHPublicKey(metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def key_size(self) -> int: + """ + The bit length of the prime modulus. + """ + + @abc.abstractmethod + def parameters(self) -> DHParameters: + """ + The DHParameters object associated with this public key. + """ + + @abc.abstractmethod + def public_numbers(self) -> DHPublicNumbers: + """ + Returns a DHPublicNumbers. + """ + + @abc.abstractmethod + def public_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PublicFormat, + ) -> bytes: + """ + Returns the key serialized as bytes. + """ + + @abc.abstractmethod + def __eq__(self, other: object) -> bool: + """ + Checks equality. + """ + + @abc.abstractmethod + def __copy__(self) -> DHPublicKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> DHPublicKey: + """ + Returns a deep copy. + """ + + +DHPublicKeyWithSerialization = DHPublicKey +DHPublicKey.register(rust_openssl.dh.DHPublicKey) + + +class DHPrivateKey(metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def key_size(self) -> int: + """ + The bit length of the prime modulus. + """ + + @abc.abstractmethod + def public_key(self) -> DHPublicKey: + """ + The DHPublicKey associated with this private key. + """ + + @abc.abstractmethod + def parameters(self) -> DHParameters: + """ + The DHParameters object associated with this private key. + """ + + @abc.abstractmethod + def exchange(self, peer_public_key: DHPublicKey) -> bytes: + """ + Given peer's DHPublicKey, carry out the key exchange and + return shared key as bytes. + """ + + @abc.abstractmethod + def private_numbers(self) -> DHPrivateNumbers: + """ + Returns a DHPrivateNumbers. + """ + + @abc.abstractmethod + def private_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PrivateFormat, + encryption_algorithm: _serialization.KeySerializationEncryption, + ) -> bytes: + """ + Returns the key serialized as bytes. + """ + + @abc.abstractmethod + def __copy__(self) -> DHPrivateKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> DHPrivateKey: + """ + Returns a deep copy. + """ + + +DHPrivateKeyWithSerialization = DHPrivateKey +DHPrivateKey.register(rust_openssl.dh.DHPrivateKey) + +# Aliases that do not emit the deprecation warning on attribute access, for +# internal use (e.g. the unions in +# cryptography.hazmat.primitives.asymmetric.types, which are evaluated at +# import time). +_DHPublicKey = DHPublicKey +_DHPrivateKey = DHPrivateKey + +utils.deprecated( + generate_parameters, + __name__, + _FFDH_DEPRECATION_MSG, + utils.DeprecatedIn50, + name="generate_parameters", +) + +utils.deprecated( + DHPrivateNumbers, + __name__, + _FFDH_DEPRECATION_MSG, + utils.DeprecatedIn50, + name="DHPrivateNumbers", +) + +utils.deprecated( + DHPublicNumbers, + __name__, + _FFDH_DEPRECATION_MSG, + utils.DeprecatedIn50, + name="DHPublicNumbers", +) + +utils.deprecated( + DHParameterNumbers, + __name__, + _FFDH_DEPRECATION_MSG, + utils.DeprecatedIn50, + name="DHParameterNumbers", +) + +utils.deprecated( + DHParameters, + __name__, + _FFDH_DEPRECATION_MSG, + utils.DeprecatedIn50, + name="DHParameters", +) + +utils.deprecated( + DHParameters, + __name__, + _FFDH_DEPRECATION_MSG, + utils.DeprecatedIn50, + name="DHParametersWithSerialization", +) + +utils.deprecated( + DHPublicKey, + __name__, + _FFDH_DEPRECATION_MSG, + utils.DeprecatedIn50, + name="DHPublicKey", +) + +utils.deprecated( + DHPublicKey, + __name__, + _FFDH_DEPRECATION_MSG, + utils.DeprecatedIn50, + name="DHPublicKeyWithSerialization", +) + +utils.deprecated( + DHPrivateKey, + __name__, + _FFDH_DEPRECATION_MSG, + utils.DeprecatedIn50, + name="DHPrivateKey", +) + +utils.deprecated( + DHPrivateKey, + __name__, + _FFDH_DEPRECATION_MSG, + utils.DeprecatedIn50, + name="DHPrivateKeyWithSerialization", +) diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/dsa.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/dsa.py new file mode 100644 index 0000000..f245557 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/dsa.py @@ -0,0 +1,179 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import abc +import typing + +from cryptography.hazmat.bindings._rust import openssl as rust_openssl +from cryptography.hazmat.primitives import _serialization, hashes +from cryptography.hazmat.primitives.asymmetric import utils as asym_utils +from cryptography.utils import Buffer + + +class DSAParameters(metaclass=abc.ABCMeta): + @abc.abstractmethod + def generate_private_key(self) -> DSAPrivateKey: + """ + Generates and returns a DSAPrivateKey. + """ + + @abc.abstractmethod + def parameter_numbers(self) -> DSAParameterNumbers: + """ + Returns a DSAParameterNumbers. + """ + + +DSAParametersWithNumbers = DSAParameters +DSAParameters.register(rust_openssl.dsa.DSAParameters) + + +class DSAPrivateKey(metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def key_size(self) -> int: + """ + The bit length of the prime modulus. + """ + + @abc.abstractmethod + def public_key(self) -> DSAPublicKey: + """ + The DSAPublicKey associated with this private key. + """ + + @abc.abstractmethod + def parameters(self) -> DSAParameters: + """ + The DSAParameters object associated with this private key. + """ + + @abc.abstractmethod + def sign( + self, + data: Buffer, + algorithm: asym_utils.Prehashed | hashes.HashAlgorithm, + ) -> bytes: + """ + Signs the data + """ + + @abc.abstractmethod + def private_numbers(self) -> DSAPrivateNumbers: + """ + Returns a DSAPrivateNumbers. + """ + + @abc.abstractmethod + def private_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PrivateFormat, + encryption_algorithm: _serialization.KeySerializationEncryption, + ) -> bytes: + """ + Returns the key serialized as bytes. + """ + + @abc.abstractmethod + def __copy__(self) -> DSAPrivateKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> DSAPrivateKey: + """ + Returns a deep copy. + """ + + +DSAPrivateKeyWithSerialization = DSAPrivateKey +DSAPrivateKey.register(rust_openssl.dsa.DSAPrivateKey) + + +class DSAPublicKey(metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def key_size(self) -> int: + """ + The bit length of the prime modulus. + """ + + @abc.abstractmethod + def parameters(self) -> DSAParameters: + """ + The DSAParameters object associated with this public key. + """ + + @abc.abstractmethod + def public_numbers(self) -> DSAPublicNumbers: + """ + Returns a DSAPublicNumbers. + """ + + @abc.abstractmethod + def public_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PublicFormat, + ) -> bytes: + """ + Returns the key serialized as bytes. + """ + + @abc.abstractmethod + def verify( + self, + signature: Buffer, + data: Buffer, + algorithm: asym_utils.Prehashed | hashes.HashAlgorithm, + ) -> None: + """ + Verifies the signature of the data. + """ + + @abc.abstractmethod + def __eq__(self, other: object) -> bool: + """ + Checks equality. + """ + + @abc.abstractmethod + def __copy__(self) -> DSAPublicKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> DSAPublicKey: + """ + Returns a deep copy. + """ + + +DSAPublicKeyWithSerialization = DSAPublicKey +DSAPublicKey.register(rust_openssl.dsa.DSAPublicKey) + +DSAPrivateNumbers = rust_openssl.dsa.DSAPrivateNumbers +DSAPublicNumbers = rust_openssl.dsa.DSAPublicNumbers +DSAParameterNumbers = rust_openssl.dsa.DSAParameterNumbers + + +def generate_parameters( + key_size: int, backend: typing.Any = None +) -> DSAParameters: + if key_size not in (1024, 2048, 3072, 4096): + raise ValueError("Key size must be 1024, 2048, 3072, or 4096 bits.") + + return rust_openssl.dsa.generate_parameters(key_size) + + +def generate_private_key( + key_size: int, backend: typing.Any = None +) -> DSAPrivateKey: + parameters = generate_parameters(key_size) + return parameters.generate_private_key() diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/ec.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/ec.py new file mode 100644 index 0000000..39e6751 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/ec.py @@ -0,0 +1,369 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import abc +import typing + +from cryptography import utils +from cryptography.exceptions import UnsupportedAlgorithm, _Reasons +from cryptography.hazmat._oid import ObjectIdentifier +from cryptography.hazmat.bindings._rust import openssl as rust_openssl +from cryptography.hazmat.primitives import _serialization, hashes +from cryptography.hazmat.primitives.asymmetric import utils as asym_utils + + +class EllipticCurveOID: + SECP192R1 = ObjectIdentifier("1.2.840.10045.3.1.1") + SECP224R1 = ObjectIdentifier("1.3.132.0.33") + SECP256K1 = ObjectIdentifier("1.3.132.0.10") + SECP256R1 = ObjectIdentifier("1.2.840.10045.3.1.7") + SECP384R1 = ObjectIdentifier("1.3.132.0.34") + SECP521R1 = ObjectIdentifier("1.3.132.0.35") + BRAINPOOLP256R1 = ObjectIdentifier("1.3.36.3.3.2.8.1.1.7") + BRAINPOOLP384R1 = ObjectIdentifier("1.3.36.3.3.2.8.1.1.11") + BRAINPOOLP512R1 = ObjectIdentifier("1.3.36.3.3.2.8.1.1.13") + + +class EllipticCurve(metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def name(self) -> str: + """ + The name of the curve. e.g. secp256r1. + """ + + @property + @abc.abstractmethod + def key_size(self) -> int: + """ + Bit size of a secret scalar for the curve. + """ + + @property + @abc.abstractmethod + def group_order(self) -> int: + """ + The order of the curve's group. + """ + + +class EllipticCurveSignatureAlgorithm(metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def algorithm( + self, + ) -> asym_utils.Prehashed | hashes.HashAlgorithm: + """ + The digest algorithm used with this signature. + """ + + +class EllipticCurvePrivateKey(metaclass=abc.ABCMeta): + @abc.abstractmethod + def exchange( + self, algorithm: ECDH, peer_public_key: EllipticCurvePublicKey + ) -> bytes: + """ + Performs a key exchange operation using the provided algorithm with the + provided peer's public key. + """ + + @abc.abstractmethod + def public_key(self) -> EllipticCurvePublicKey: + """ + The EllipticCurvePublicKey for this private key. + """ + + @property + @abc.abstractmethod + def curve(self) -> EllipticCurve: + """ + The EllipticCurve that this key is on. + """ + + @property + @abc.abstractmethod + def key_size(self) -> int: + """ + Bit size of a secret scalar for the curve. + """ + + @abc.abstractmethod + def sign( + self, + data: utils.Buffer, + signature_algorithm: EllipticCurveSignatureAlgorithm, + ) -> bytes: + """ + Signs the data + """ + + @abc.abstractmethod + def private_numbers(self) -> EllipticCurvePrivateNumbers: + """ + Returns an EllipticCurvePrivateNumbers. + """ + + @abc.abstractmethod + def private_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PrivateFormat, + encryption_algorithm: _serialization.KeySerializationEncryption, + ) -> bytes: + """ + Returns the key serialized as bytes. + """ + + @abc.abstractmethod + def __copy__(self) -> EllipticCurvePrivateKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> EllipticCurvePrivateKey: + """ + Returns a deep copy. + """ + + +EllipticCurvePrivateKeyWithSerialization = EllipticCurvePrivateKey +EllipticCurvePrivateKey.register(rust_openssl.ec.ECPrivateKey) + + +class EllipticCurvePublicKey(metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def curve(self) -> EllipticCurve: + """ + The EllipticCurve that this key is on. + """ + + @property + @abc.abstractmethod + def key_size(self) -> int: + """ + Bit size of a secret scalar for the curve. + """ + + @abc.abstractmethod + def public_numbers(self) -> EllipticCurvePublicNumbers: + """ + Returns an EllipticCurvePublicNumbers. + """ + + @abc.abstractmethod + def public_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PublicFormat, + ) -> bytes: + """ + Returns the key serialized as bytes. + """ + + @abc.abstractmethod + def verify( + self, + signature: utils.Buffer, + data: utils.Buffer, + signature_algorithm: EllipticCurveSignatureAlgorithm, + ) -> None: + """ + Verifies the signature of the data. + """ + + @classmethod + def from_encoded_point( + cls, curve: EllipticCurve, data: bytes + ) -> EllipticCurvePublicKey: + utils._check_bytes("data", data) + + if len(data) == 0: + raise ValueError("data must not be an empty byte string") + + if data[0] not in [0x02, 0x03, 0x04]: + raise ValueError("Unsupported elliptic curve point type") + + return rust_openssl.ec.from_public_bytes(curve, data) + + @abc.abstractmethod + def __eq__(self, other: object) -> bool: + """ + Checks equality. + """ + + @abc.abstractmethod + def __copy__(self) -> EllipticCurvePublicKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> EllipticCurvePublicKey: + """ + Returns a deep copy. + """ + + +EllipticCurvePublicKeyWithSerialization = EllipticCurvePublicKey +EllipticCurvePublicKey.register(rust_openssl.ec.ECPublicKey) + +EllipticCurvePrivateNumbers = rust_openssl.ec.EllipticCurvePrivateNumbers +EllipticCurvePublicNumbers = rust_openssl.ec.EllipticCurvePublicNumbers + + +class SECP521R1(EllipticCurve): + name = "secp521r1" + key_size = 521 + group_order = 0x1FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409 # noqa: E501 + + +class SECP384R1(EllipticCurve): + name = "secp384r1" + key_size = 384 + group_order = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973 # noqa: E501 + + +class SECP256R1(EllipticCurve): + name = "secp256r1" + key_size = 256 + group_order = ( + 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551 + ) + + +class SECP256K1(EllipticCurve): + name = "secp256k1" + key_size = 256 + group_order = ( + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + ) + + +class SECP224R1(EllipticCurve): + name = "secp224r1" + key_size = 224 + group_order = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF16A2E0B8F03E13DD29455C5C2A3D + + +class SECP192R1(EllipticCurve): + name = "secp192r1" + key_size = 192 + group_order = 0xFFFFFFFFFFFFFFFFFFFFFFFF99DEF836146BC9B1B4D22831 + + +class BrainpoolP256R1(EllipticCurve): + name = "brainpoolP256r1" + key_size = 256 + group_order = ( + 0xA9FB57DBA1EEA9BC3E660A909D838D718C397AA3B561A6F7901E0E82974856A7 + ) + + +class BrainpoolP384R1(EllipticCurve): + name = "brainpoolP384r1" + key_size = 384 + group_order = 0x8CB91E82A3386D280F5D6F7E50E641DF152F7109ED5456B31F166E6CAC0425A7CF3AB6AF6B7FC3103B883202E9046565 # noqa: E501 + + +class BrainpoolP512R1(EllipticCurve): + name = "brainpoolP512r1" + key_size = 512 + group_order = 0xAADD9DB8DBE9C48B3FD4E6AE33C9FC07CB308DB3B3C9D20ED6639CCA70330870553E5C414CA92619418661197FAC10471DB1D381085DDADDB58796829CA90069 # noqa: E501 + + +_CURVE_TYPES: dict[str, EllipticCurve] = { + "prime192v1": SECP192R1(), + "prime256v1": SECP256R1(), + "secp192r1": SECP192R1(), + "secp224r1": SECP224R1(), + "secp256r1": SECP256R1(), + "secp384r1": SECP384R1(), + "secp521r1": SECP521R1(), + "secp256k1": SECP256K1(), + "brainpoolP256r1": BrainpoolP256R1(), + "brainpoolP384r1": BrainpoolP384R1(), + "brainpoolP512r1": BrainpoolP512R1(), +} + + +class ECDSA(EllipticCurveSignatureAlgorithm): + def __init__( + self, + algorithm: asym_utils.Prehashed | hashes.HashAlgorithm, + deterministic_signing: bool = False, + ): + from cryptography.hazmat.backends.openssl.backend import backend + + if ( + deterministic_signing + and not backend.ecdsa_deterministic_supported() + ): + raise UnsupportedAlgorithm( + "ECDSA with deterministic signature (RFC 6979) is not " + "supported by this version of OpenSSL.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM, + ) + self._algorithm = algorithm + self._deterministic_signing = deterministic_signing + + @property + def algorithm( + self, + ) -> asym_utils.Prehashed | hashes.HashAlgorithm: + return self._algorithm + + @property + def deterministic_signing( + self, + ) -> bool: + return self._deterministic_signing + + +generate_private_key = rust_openssl.ec.generate_private_key + + +def derive_private_key( + private_value: int, + curve: EllipticCurve, + backend: typing.Any = None, +) -> EllipticCurvePrivateKey: + if not isinstance(private_value, int): + raise TypeError("private_value must be an integer type.") + + if private_value <= 0: + raise ValueError("private_value must be a positive integer.") + + return rust_openssl.ec.derive_private_key(private_value, curve) + + +class ECDH: + pass + + +_OID_TO_CURVE = { + EllipticCurveOID.SECP192R1: SECP192R1, + EllipticCurveOID.SECP224R1: SECP224R1, + EllipticCurveOID.SECP256K1: SECP256K1, + EllipticCurveOID.SECP256R1: SECP256R1, + EllipticCurveOID.SECP384R1: SECP384R1, + EllipticCurveOID.SECP521R1: SECP521R1, + EllipticCurveOID.BRAINPOOLP256R1: BrainpoolP256R1, + EllipticCurveOID.BRAINPOOLP384R1: BrainpoolP384R1, + EllipticCurveOID.BRAINPOOLP512R1: BrainpoolP512R1, +} + + +def get_curve_for_oid(oid: ObjectIdentifier) -> type[EllipticCurve]: + try: + return _OID_TO_CURVE[oid] + except KeyError: + raise LookupError( + "The provided object identifier has no matching elliptic " + "curve class" + ) diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/ed25519.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/ed25519.py new file mode 100644 index 0000000..70aec5b --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/ed25519.py @@ -0,0 +1,116 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import abc + +from cryptography.hazmat.bindings._rust import openssl as rust_openssl +from cryptography.hazmat.primitives import _serialization +from cryptography.utils import Buffer + + +class Ed25519PublicKey(metaclass=abc.ABCMeta): + @classmethod + def from_public_bytes(cls, data: bytes) -> Ed25519PublicKey: + return rust_openssl.ed25519.from_public_bytes(data) + + @abc.abstractmethod + def public_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PublicFormat, + ) -> bytes: + """ + The serialized bytes of the public key. + """ + + @abc.abstractmethod + def public_bytes_raw(self) -> bytes: + """ + The raw bytes of the public key. + Equivalent to public_bytes(Raw, Raw). + """ + + @abc.abstractmethod + def verify(self, signature: Buffer, data: Buffer) -> None: + """ + Verify the signature. + """ + + @abc.abstractmethod + def __eq__(self, other: object) -> bool: + """ + Checks equality. + """ + + @abc.abstractmethod + def __copy__(self) -> Ed25519PublicKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> Ed25519PublicKey: + """ + Returns a deep copy. + """ + + +Ed25519PublicKey.register(rust_openssl.ed25519.Ed25519PublicKey) + + +class Ed25519PrivateKey(metaclass=abc.ABCMeta): + @classmethod + def generate(cls) -> Ed25519PrivateKey: + return rust_openssl.ed25519.generate_key() + + @classmethod + def from_private_bytes(cls, data: Buffer) -> Ed25519PrivateKey: + return rust_openssl.ed25519.from_private_bytes(data) + + @abc.abstractmethod + def public_key(self) -> Ed25519PublicKey: + """ + The Ed25519PublicKey derived from the private key. + """ + + @abc.abstractmethod + def private_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PrivateFormat, + encryption_algorithm: _serialization.KeySerializationEncryption, + ) -> bytes: + """ + The serialized bytes of the private key. + """ + + @abc.abstractmethod + def private_bytes_raw(self) -> bytes: + """ + The raw bytes of the private key. + Equivalent to private_bytes(Raw, Raw, NoEncryption()). + """ + + @abc.abstractmethod + def sign(self, data: Buffer) -> bytes: + """ + Signs the data. + """ + + @abc.abstractmethod + def __copy__(self) -> Ed25519PrivateKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> Ed25519PrivateKey: + """ + Returns a deep copy. + """ + + +Ed25519PrivateKey.register(rust_openssl.ed25519.Ed25519PrivateKey) diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/ed448.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/ed448.py new file mode 100644 index 0000000..9ecb478 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/ed448.py @@ -0,0 +1,143 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import abc + +from cryptography.exceptions import UnsupportedAlgorithm, _Reasons +from cryptography.hazmat.bindings._rust import openssl as rust_openssl +from cryptography.hazmat.primitives import _serialization +from cryptography.utils import Buffer + + +class Ed448PublicKey(metaclass=abc.ABCMeta): + @classmethod + def from_public_bytes(cls, data: bytes) -> Ed448PublicKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.ed448_supported(): + raise UnsupportedAlgorithm( + "ed448 is not supported by this version of OpenSSL.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM, + ) + + return rust_openssl.ed448.from_public_bytes(data) + + @abc.abstractmethod + def public_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PublicFormat, + ) -> bytes: + """ + The serialized bytes of the public key. + """ + + @abc.abstractmethod + def public_bytes_raw(self) -> bytes: + """ + The raw bytes of the public key. + Equivalent to public_bytes(Raw, Raw). + """ + + @abc.abstractmethod + def verify(self, signature: Buffer, data: Buffer) -> None: + """ + Verify the signature. + """ + + @abc.abstractmethod + def __eq__(self, other: object) -> bool: + """ + Checks equality. + """ + + @abc.abstractmethod + def __copy__(self) -> Ed448PublicKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> Ed448PublicKey: + """ + Returns a deep copy. + """ + + +if hasattr(rust_openssl, "ed448"): + Ed448PublicKey.register(rust_openssl.ed448.Ed448PublicKey) + + +class Ed448PrivateKey(metaclass=abc.ABCMeta): + @classmethod + def generate(cls) -> Ed448PrivateKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.ed448_supported(): + raise UnsupportedAlgorithm( + "ed448 is not supported by this version of OpenSSL.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM, + ) + + return rust_openssl.ed448.generate_key() + + @classmethod + def from_private_bytes(cls, data: Buffer) -> Ed448PrivateKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.ed448_supported(): + raise UnsupportedAlgorithm( + "ed448 is not supported by this version of OpenSSL.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM, + ) + + return rust_openssl.ed448.from_private_bytes(data) + + @abc.abstractmethod + def public_key(self) -> Ed448PublicKey: + """ + The Ed448PublicKey derived from the private key. + """ + + @abc.abstractmethod + def sign(self, data: Buffer) -> bytes: + """ + Signs the data. + """ + + @abc.abstractmethod + def private_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PrivateFormat, + encryption_algorithm: _serialization.KeySerializationEncryption, + ) -> bytes: + """ + The serialized bytes of the private key. + """ + + @abc.abstractmethod + def private_bytes_raw(self) -> bytes: + """ + The raw bytes of the private key. + Equivalent to private_bytes(Raw, Raw, NoEncryption()). + """ + + @abc.abstractmethod + def __copy__(self) -> Ed448PrivateKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> Ed448PrivateKey: + """ + Returns a deep copy. + """ + + +if hasattr(rust_openssl, "x448"): + Ed448PrivateKey.register(rust_openssl.ed448.Ed448PrivateKey) diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/mldsa.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/mldsa.py new file mode 100644 index 0000000..beba90f --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/mldsa.py @@ -0,0 +1,505 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import abc + +from cryptography.exceptions import UnsupportedAlgorithm, _Reasons +from cryptography.hazmat.bindings._rust import openssl as rust_openssl +from cryptography.hazmat.primitives import _serialization +from cryptography.utils import Buffer + + +class MLDSA44PublicKey(metaclass=abc.ABCMeta): + @classmethod + def from_public_bytes(cls, data: bytes) -> MLDSA44PublicKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.mldsa_supported(): + raise UnsupportedAlgorithm( + "ML-DSA-44 is not supported by this backend.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM, + ) + + return rust_openssl.mldsa.from_mldsa44_public_bytes(data) + + @abc.abstractmethod + def public_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PublicFormat, + ) -> bytes: + """ + The serialized bytes of the public key. + """ + + @abc.abstractmethod + def public_bytes_raw(self) -> bytes: + """ + The raw bytes of the public key. + Equivalent to public_bytes(Raw, Raw). + + The public key is 1,312 bytes for MLDSA-44. + """ + + @abc.abstractmethod + def verify( + self, + signature: Buffer, + data: Buffer, + context: Buffer | None = None, + ) -> None: + """ + Verify the signature. + """ + + @abc.abstractmethod + def verify_mu( + self, + signature: Buffer, + mu: Buffer, + ) -> None: + """ + Verify the signature over a precomputed mu (message representative). + + mu must be 64 bytes. + """ + + @abc.abstractmethod + def __eq__(self, other: object) -> bool: + """ + Checks equality. + """ + + @abc.abstractmethod + def __copy__(self) -> MLDSA44PublicKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> MLDSA44PublicKey: + """ + Returns a deep copy. + """ + + +if hasattr(rust_openssl, "mldsa"): + MLDSA44PublicKey.register(rust_openssl.mldsa.MLDSA44PublicKey) + + +class MLDSA44PrivateKey(metaclass=abc.ABCMeta): + @classmethod + def generate(cls) -> MLDSA44PrivateKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.mldsa_supported(): + raise UnsupportedAlgorithm( + "ML-DSA-44 is not supported by this backend.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM, + ) + + return rust_openssl.mldsa.generate_mldsa44_key() + + @classmethod + def from_seed_bytes(cls, data: Buffer) -> MLDSA44PrivateKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.mldsa_supported(): + raise UnsupportedAlgorithm( + "ML-DSA-44 is not supported by this backend.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM, + ) + + return rust_openssl.mldsa.from_mldsa44_seed_bytes(data) + + @abc.abstractmethod + def public_key(self) -> MLDSA44PublicKey: + """ + The MLDSA44PublicKey derived from the private key. + """ + + @abc.abstractmethod + def private_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PrivateFormat, + encryption_algorithm: _serialization.KeySerializationEncryption, + ) -> bytes: + """ + The serialized bytes of the private key. + + This method only returns the serialization of the seed form of the + private key, never the expanded one. + """ + + @abc.abstractmethod + def private_bytes_raw(self) -> bytes: + """ + The raw bytes of the private key. + Equivalent to private_bytes(Raw, Raw, NoEncryption()). + + This method only returns the seed form of the private key (32 bytes). + """ + + @abc.abstractmethod + def sign(self, data: Buffer, context: Buffer | None = None) -> bytes: + """ + Signs the data. + """ + + @abc.abstractmethod + def sign_mu(self, mu: Buffer) -> bytes: + """ + Signs a precomputed mu (message representative). + + mu must be 64 bytes and already incorporates the context, so no + context is accepted here. + """ + + @abc.abstractmethod + def __copy__(self) -> MLDSA44PrivateKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> MLDSA44PrivateKey: + """ + Returns a deep copy. + """ + + +if hasattr(rust_openssl, "mldsa"): + MLDSA44PrivateKey.register(rust_openssl.mldsa.MLDSA44PrivateKey) + + +class MLDSA65PublicKey(metaclass=abc.ABCMeta): + @classmethod + def from_public_bytes(cls, data: bytes) -> MLDSA65PublicKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.mldsa_supported(): + raise UnsupportedAlgorithm( + "ML-DSA-65 is not supported by this backend.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM, + ) + + return rust_openssl.mldsa.from_mldsa65_public_bytes(data) + + @abc.abstractmethod + def public_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PublicFormat, + ) -> bytes: + """ + The serialized bytes of the public key. + """ + + @abc.abstractmethod + def public_bytes_raw(self) -> bytes: + """ + The raw bytes of the public key. + Equivalent to public_bytes(Raw, Raw). + + The public key is 1,952 bytes for MLDSA-65. + """ + + @abc.abstractmethod + def verify( + self, + signature: Buffer, + data: Buffer, + context: Buffer | None = None, + ) -> None: + """ + Verify the signature. + """ + + @abc.abstractmethod + def verify_mu( + self, + signature: Buffer, + mu: Buffer, + ) -> None: + """ + Verify the signature over a precomputed mu (message representative). + + mu must be 64 bytes. + """ + + @abc.abstractmethod + def __eq__(self, other: object) -> bool: + """ + Checks equality. + """ + + @abc.abstractmethod + def __copy__(self) -> MLDSA65PublicKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> MLDSA65PublicKey: + """ + Returns a deep copy. + """ + + +if hasattr(rust_openssl, "mldsa"): + MLDSA65PublicKey.register(rust_openssl.mldsa.MLDSA65PublicKey) + + +class MLDSA65PrivateKey(metaclass=abc.ABCMeta): + @classmethod + def generate(cls) -> MLDSA65PrivateKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.mldsa_supported(): + raise UnsupportedAlgorithm( + "ML-DSA-65 is not supported by this backend.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM, + ) + + return rust_openssl.mldsa.generate_mldsa65_key() + + @classmethod + def from_seed_bytes(cls, data: Buffer) -> MLDSA65PrivateKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.mldsa_supported(): + raise UnsupportedAlgorithm( + "ML-DSA-65 is not supported by this backend.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM, + ) + + return rust_openssl.mldsa.from_mldsa65_seed_bytes(data) + + @abc.abstractmethod + def public_key(self) -> MLDSA65PublicKey: + """ + The MLDSA65PublicKey derived from the private key. + """ + + @abc.abstractmethod + def private_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PrivateFormat, + encryption_algorithm: _serialization.KeySerializationEncryption, + ) -> bytes: + """ + The serialized bytes of the private key. + + This method only returns the serialization of the seed form of the + private key, never the expanded one. + """ + + @abc.abstractmethod + def private_bytes_raw(self) -> bytes: + """ + The raw bytes of the private key. + Equivalent to private_bytes(Raw, Raw, NoEncryption()). + + This method only returns the seed form of the private key (32 bytes). + """ + + @abc.abstractmethod + def sign(self, data: Buffer, context: Buffer | None = None) -> bytes: + """ + Signs the data. + """ + + @abc.abstractmethod + def sign_mu(self, mu: Buffer) -> bytes: + """ + Signs a precomputed mu (message representative). + + mu must be 64 bytes and already incorporates the context, so no + context is accepted here. + """ + + @abc.abstractmethod + def __copy__(self) -> MLDSA65PrivateKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> MLDSA65PrivateKey: + """ + Returns a deep copy. + """ + + +if hasattr(rust_openssl, "mldsa"): + MLDSA65PrivateKey.register(rust_openssl.mldsa.MLDSA65PrivateKey) + + +class MLDSA87PublicKey(metaclass=abc.ABCMeta): + @classmethod + def from_public_bytes(cls, data: bytes) -> MLDSA87PublicKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.mldsa_supported(): + raise UnsupportedAlgorithm( + "ML-DSA-87 is not supported by this backend.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM, + ) + + return rust_openssl.mldsa.from_mldsa87_public_bytes(data) + + @abc.abstractmethod + def public_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PublicFormat, + ) -> bytes: + """ + The serialized bytes of the public key. + """ + + @abc.abstractmethod + def public_bytes_raw(self) -> bytes: + """ + The raw bytes of the public key. + Equivalent to public_bytes(Raw, Raw). + + The public key is 2,592 bytes for MLDSA-87. + """ + + @abc.abstractmethod + def verify( + self, + signature: Buffer, + data: Buffer, + context: Buffer | None = None, + ) -> None: + """ + Verify the signature. + """ + + @abc.abstractmethod + def verify_mu( + self, + signature: Buffer, + mu: Buffer, + ) -> None: + """ + Verify the signature over a precomputed mu (message representative). + + mu must be 64 bytes. + """ + + @abc.abstractmethod + def __eq__(self, other: object) -> bool: + """ + Checks equality. + """ + + @abc.abstractmethod + def __copy__(self) -> MLDSA87PublicKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> MLDSA87PublicKey: + """ + Returns a deep copy. + """ + + +if hasattr(rust_openssl, "mldsa"): + MLDSA87PublicKey.register(rust_openssl.mldsa.MLDSA87PublicKey) + + +class MLDSA87PrivateKey(metaclass=abc.ABCMeta): + @classmethod + def generate(cls) -> MLDSA87PrivateKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.mldsa_supported(): + raise UnsupportedAlgorithm( + "ML-DSA-87 is not supported by this backend.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM, + ) + + return rust_openssl.mldsa.generate_mldsa87_key() + + @classmethod + def from_seed_bytes(cls, data: Buffer) -> MLDSA87PrivateKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.mldsa_supported(): + raise UnsupportedAlgorithm( + "ML-DSA-87 is not supported by this backend.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM, + ) + + return rust_openssl.mldsa.from_mldsa87_seed_bytes(data) + + @abc.abstractmethod + def public_key(self) -> MLDSA87PublicKey: + """ + The MLDSA87PublicKey derived from the private key. + """ + + @abc.abstractmethod + def private_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PrivateFormat, + encryption_algorithm: _serialization.KeySerializationEncryption, + ) -> bytes: + """ + The serialized bytes of the private key. + + This method only returns the serialization of the seed form of the + private key, never the expanded one. + """ + + @abc.abstractmethod + def private_bytes_raw(self) -> bytes: + """ + The raw bytes of the private key. + Equivalent to private_bytes(Raw, Raw, NoEncryption()). + + This method only returns the seed form of the private key (32 bytes). + """ + + @abc.abstractmethod + def sign(self, data: Buffer, context: Buffer | None = None) -> bytes: + """ + Signs the data. + """ + + @abc.abstractmethod + def sign_mu(self, mu: Buffer) -> bytes: + """ + Signs a precomputed mu (message representative). + + mu must be 64 bytes and already incorporates the context, so no + context is accepted here. + """ + + @abc.abstractmethod + def __copy__(self) -> MLDSA87PrivateKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> MLDSA87PrivateKey: + """ + Returns a deep copy. + """ + + +if hasattr(rust_openssl, "mldsa"): + MLDSA87PrivateKey.register(rust_openssl.mldsa.MLDSA87PrivateKey) + MLDSAMuHasher = rust_openssl.mldsa.MLDSAMuHasher diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/mlkem.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/mlkem.py new file mode 100644 index 0000000..64bca11 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/mlkem.py @@ -0,0 +1,278 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import abc + +from cryptography.exceptions import UnsupportedAlgorithm, _Reasons +from cryptography.hazmat.bindings._rust import openssl as rust_openssl +from cryptography.hazmat.primitives import _serialization +from cryptography.utils import Buffer + + +class MLKEM768PublicKey(metaclass=abc.ABCMeta): + @classmethod + def from_public_bytes(cls, data: Buffer) -> MLKEM768PublicKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.mlkem_supported(): + raise UnsupportedAlgorithm( + "ML-KEM-768 is not supported by this backend.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM, + ) + + return rust_openssl.mlkem.from_mlkem768_public_bytes(data) + + @abc.abstractmethod + def encapsulate(self) -> tuple[bytes, bytes]: + """ + Encapsulate: returns (shared_secret, ciphertext). + """ + + @abc.abstractmethod + def public_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PublicFormat, + ) -> bytes: + """ + The serialized bytes of the public key. + """ + + @abc.abstractmethod + def public_bytes_raw(self) -> bytes: + """ + The raw bytes of the public key. + Equivalent to public_bytes(Raw, Raw). + + The public key is 1,184 bytes for ML-KEM-768. + """ + + @abc.abstractmethod + def __eq__(self, other: object) -> bool: + """ + Checks equality. + """ + + @abc.abstractmethod + def __copy__(self) -> MLKEM768PublicKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> MLKEM768PublicKey: + """ + Returns a deep copy. + """ + + +if hasattr(rust_openssl, "mlkem"): + MLKEM768PublicKey.register(rust_openssl.mlkem.MLKEM768PublicKey) + + +class MLKEM768PrivateKey(metaclass=abc.ABCMeta): + @classmethod + def generate(cls) -> MLKEM768PrivateKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.mlkem_supported(): + raise UnsupportedAlgorithm( + "ML-KEM-768 is not supported by this backend.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM, + ) + + return rust_openssl.mlkem.generate_mlkem768_key() + + @classmethod + def from_seed_bytes(cls, data: Buffer) -> MLKEM768PrivateKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.mlkem_supported(): + raise UnsupportedAlgorithm( + "ML-KEM-768 is not supported by this backend.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM, + ) + + return rust_openssl.mlkem.from_mlkem768_seed_bytes(data) + + @abc.abstractmethod + def decapsulate(self, ciphertext: Buffer) -> bytes: + """ + Decapsulate: returns shared_secret. + """ + + @abc.abstractmethod + def public_key(self) -> MLKEM768PublicKey: + """ + The MLKEM768PublicKey derived from this private key. + """ + + @abc.abstractmethod + def private_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PrivateFormat, + encryption_algorithm: _serialization.KeySerializationEncryption, + ) -> bytes: + """ + The serialized bytes of the private key. + """ + + @abc.abstractmethod + def private_bytes_raw(self) -> bytes: + """ + The raw bytes of the private key (64-byte seed). + Equivalent to private_bytes(Raw, Raw, NoEncryption()). + """ + + @abc.abstractmethod + def __copy__(self) -> MLKEM768PrivateKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> MLKEM768PrivateKey: + """ + Returns a deep copy. + """ + + +if hasattr(rust_openssl, "mlkem"): + MLKEM768PrivateKey.register(rust_openssl.mlkem.MLKEM768PrivateKey) + + +class MLKEM1024PublicKey(metaclass=abc.ABCMeta): + @classmethod + def from_public_bytes(cls, data: Buffer) -> MLKEM1024PublicKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.mlkem_supported(): + raise UnsupportedAlgorithm( + "ML-KEM-1024 is not supported by this backend.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM, + ) + + return rust_openssl.mlkem.from_mlkem1024_public_bytes(data) + + @abc.abstractmethod + def encapsulate(self) -> tuple[bytes, bytes]: + """ + Encapsulate: returns (shared_secret, ciphertext). + """ + + @abc.abstractmethod + def public_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PublicFormat, + ) -> bytes: + """ + The serialized bytes of the public key. + """ + + @abc.abstractmethod + def public_bytes_raw(self) -> bytes: + """ + The raw bytes of the public key. + Equivalent to public_bytes(Raw, Raw). + + The public key is 1,568 bytes for ML-KEM-1024. + """ + + @abc.abstractmethod + def __eq__(self, other: object) -> bool: + """ + Checks equality. + """ + + @abc.abstractmethod + def __copy__(self) -> MLKEM1024PublicKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> MLKEM1024PublicKey: + """ + Returns a deep copy. + """ + + +if hasattr(rust_openssl, "mlkem"): + MLKEM1024PublicKey.register(rust_openssl.mlkem.MLKEM1024PublicKey) + + +class MLKEM1024PrivateKey(metaclass=abc.ABCMeta): + @classmethod + def generate(cls) -> MLKEM1024PrivateKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.mlkem_supported(): + raise UnsupportedAlgorithm( + "ML-KEM-1024 is not supported by this backend.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM, + ) + + return rust_openssl.mlkem.generate_mlkem1024_key() + + @classmethod + def from_seed_bytes(cls, data: Buffer) -> MLKEM1024PrivateKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.mlkem_supported(): + raise UnsupportedAlgorithm( + "ML-KEM-1024 is not supported by this backend.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM, + ) + + return rust_openssl.mlkem.from_mlkem1024_seed_bytes(data) + + @abc.abstractmethod + def decapsulate(self, ciphertext: Buffer) -> bytes: + """ + Decapsulate: returns shared_secret. + """ + + @abc.abstractmethod + def public_key(self) -> MLKEM1024PublicKey: + """ + The MLKEM1024PublicKey derived from this private key. + """ + + @abc.abstractmethod + def private_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PrivateFormat, + encryption_algorithm: _serialization.KeySerializationEncryption, + ) -> bytes: + """ + The serialized bytes of the private key. + """ + + @abc.abstractmethod + def private_bytes_raw(self) -> bytes: + """ + The raw bytes of the private key (64-byte seed). + Equivalent to private_bytes(Raw, Raw, NoEncryption()). + """ + + @abc.abstractmethod + def __copy__(self) -> MLKEM1024PrivateKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> MLKEM1024PrivateKey: + """ + Returns a deep copy. + """ + + +if hasattr(rust_openssl, "mlkem"): + MLKEM1024PrivateKey.register(rust_openssl.mlkem.MLKEM1024PrivateKey) diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/padding.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/padding.py new file mode 100644 index 0000000..1fd2873 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/padding.py @@ -0,0 +1,141 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import abc + +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives._asymmetric import ( + AsymmetricPadding as AsymmetricPadding, +) +from cryptography.hazmat.primitives.asymmetric import rsa + + +class PKCS1v15(AsymmetricPadding): + name = "EMSA-PKCS1-v1_5" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, PKCS1v15): + return NotImplemented + + return True + + +class _MaxLength: + "Sentinel value for `MAX_LENGTH`." + + +class _Auto: + "Sentinel value for `AUTO`." + + +class _DigestLength: + "Sentinel value for `DIGEST_LENGTH`." + + +class PSS(AsymmetricPadding): + MAX_LENGTH = _MaxLength() + AUTO = _Auto() + DIGEST_LENGTH = _DigestLength() + name = "EMSA-PSS" + _salt_length: int | _MaxLength | _Auto | _DigestLength + + def __init__( + self, + mgf: MGF, + salt_length: int | _MaxLength | _Auto | _DigestLength, + ) -> None: + self._mgf = mgf + + if not isinstance( + salt_length, (int, _MaxLength, _Auto, _DigestLength) + ): + raise TypeError( + "salt_length must be an integer, MAX_LENGTH, " + "DIGEST_LENGTH, or AUTO" + ) + + if isinstance(salt_length, int) and salt_length < 0: + raise ValueError("salt_length must be zero or greater.") + + self._salt_length = salt_length + + def __eq__(self, other: object) -> bool: + if not isinstance(other, PSS): + return NotImplemented + + return ( + self._mgf == other._mgf and self._salt_length == other._salt_length + ) + + @property + def mgf(self) -> MGF: + return self._mgf + + +class OAEP(AsymmetricPadding): + name = "EME-OAEP" + + def __init__( + self, + mgf: MGF, + algorithm: hashes.HashAlgorithm, + label: bytes | None, + ): + if not isinstance(algorithm, hashes.HashAlgorithm): + raise TypeError("Expected instance of hashes.HashAlgorithm.") + + self._mgf = mgf + self._algorithm = algorithm + self._label = label + + def __eq__(self, other: object) -> bool: + if not isinstance(other, OAEP): + return NotImplemented + + return ( + self._mgf == other._mgf + and self._algorithm == other._algorithm + and self._label == other._label + ) + + @property + def algorithm(self) -> hashes.HashAlgorithm: + return self._algorithm + + @property + def mgf(self) -> MGF: + return self._mgf + + +class MGF(metaclass=abc.ABCMeta): + _algorithm: hashes.HashAlgorithm + + +class MGF1(MGF): + def __init__(self, algorithm: hashes.HashAlgorithm): + if not isinstance(algorithm, hashes.HashAlgorithm): + raise TypeError("Expected instance of hashes.HashAlgorithm.") + + self._algorithm = algorithm + + def __eq__(self, other: object) -> bool: + if not isinstance(other, MGF1): + return NotImplemented + + return self._algorithm == other._algorithm + + +def calculate_max_pss_salt_length( + key: rsa.RSAPrivateKey | rsa.RSAPublicKey, + hash_algorithm: hashes.HashAlgorithm, +) -> int: + if not isinstance(key, (rsa.RSAPrivateKey, rsa.RSAPublicKey)): + raise TypeError("key must be an RSA public or private key") + # bit length - 1 per RFC 3447 + emlen = (key.key_size + 6) // 8 + salt_length = emlen - hash_algorithm.digest_size - 2 + assert salt_length >= 0 + return salt_length diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/rsa.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/rsa.py new file mode 100644 index 0000000..d730ceb --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/rsa.py @@ -0,0 +1,295 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import abc +import random +import typing +from math import gcd, lcm + +from cryptography.hazmat.bindings._rust import openssl as rust_openssl +from cryptography.hazmat.primitives import _serialization, hashes +from cryptography.hazmat.primitives._asymmetric import AsymmetricPadding +from cryptography.hazmat.primitives.asymmetric import utils as asym_utils + + +class RSAPrivateKey(metaclass=abc.ABCMeta): + @abc.abstractmethod + def decrypt(self, ciphertext: bytes, padding: AsymmetricPadding) -> bytes: + """ + Decrypts the provided ciphertext. + """ + + @property + @abc.abstractmethod + def key_size(self) -> int: + """ + The bit length of the public modulus. + """ + + @abc.abstractmethod + def public_key(self) -> RSAPublicKey: + """ + The RSAPublicKey associated with this private key. + """ + + @abc.abstractmethod + def sign( + self, + data: bytes, + padding: AsymmetricPadding, + algorithm: asym_utils.Prehashed + | hashes.HashAlgorithm + | asym_utils.NoDigestInfo, + ) -> bytes: + """ + Signs the data. + """ + + @abc.abstractmethod + def private_numbers(self) -> RSAPrivateNumbers: + """ + Returns an RSAPrivateNumbers. + """ + + @abc.abstractmethod + def private_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PrivateFormat, + encryption_algorithm: _serialization.KeySerializationEncryption, + ) -> bytes: + """ + Returns the key serialized as bytes. + """ + + @abc.abstractmethod + def __copy__(self) -> RSAPrivateKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> RSAPrivateKey: + """ + Returns a deep copy. + """ + + +RSAPrivateKeyWithSerialization = RSAPrivateKey +RSAPrivateKey.register(rust_openssl.rsa.RSAPrivateKey) + + +class RSAPublicKey(metaclass=abc.ABCMeta): + @abc.abstractmethod + def encrypt(self, plaintext: bytes, padding: AsymmetricPadding) -> bytes: + """ + Encrypts the given plaintext. + """ + + @property + @abc.abstractmethod + def key_size(self) -> int: + """ + The bit length of the public modulus. + """ + + @abc.abstractmethod + def public_numbers(self) -> RSAPublicNumbers: + """ + Returns an RSAPublicNumbers + """ + + @abc.abstractmethod + def public_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PublicFormat, + ) -> bytes: + """ + Returns the key serialized as bytes. + """ + + @abc.abstractmethod + def verify( + self, + signature: bytes, + data: bytes, + padding: AsymmetricPadding, + algorithm: asym_utils.Prehashed | hashes.HashAlgorithm, + ) -> None: + """ + Verifies the signature of the data. + """ + + @abc.abstractmethod + def recover_data_from_signature( + self, + signature: bytes, + padding: AsymmetricPadding, + algorithm: hashes.HashAlgorithm | asym_utils.NoDigestInfo | None, + ) -> bytes: + """ + Recovers the original data from the signature. + """ + + @abc.abstractmethod + def __eq__(self, other: object) -> bool: + """ + Checks equality. + """ + + @abc.abstractmethod + def __copy__(self) -> RSAPublicKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> RSAPublicKey: + """ + Returns a deep copy. + """ + + +RSAPublicKeyWithSerialization = RSAPublicKey +RSAPublicKey.register(rust_openssl.rsa.RSAPublicKey) + +RSAPrivateNumbers = rust_openssl.rsa.RSAPrivateNumbers +RSAPublicNumbers = rust_openssl.rsa.RSAPublicNumbers + + +def generate_private_key( + public_exponent: int, + key_size: int, + backend: typing.Any = None, +) -> RSAPrivateKey: + _verify_rsa_parameters(public_exponent, key_size) + return rust_openssl.rsa.generate_private_key(public_exponent, key_size) + + +def _verify_rsa_parameters(public_exponent: int, key_size: int) -> None: + if public_exponent not in (3, 65537): + raise ValueError( + "public_exponent must be either 3 (for legacy compatibility) or " + "65537. Almost everyone should choose 65537 here!" + ) + + if key_size < 1024: + raise ValueError("key_size must be at least 1024-bits.") + + +def _modinv(e: int, m: int) -> int: + """ + Modular Multiplicative Inverse. Returns x such that: (x*e) mod m == 1 + """ + x1, x2 = 1, 0 + a, b = e, m + while b > 0: + q, r = divmod(a, b) + xn = x1 - q * x2 + a, b, x1, x2 = b, r, x2, xn + return x1 % m + + +def rsa_crt_iqmp(p: int, q: int) -> int: + """ + Compute the CRT (q ** -1) % p value from RSA primes p and q. + """ + if p <= 1 or q <= 1: + raise ValueError("Values can't be <= 1") + return _modinv(q, p) + + +def rsa_crt_dmp1(private_exponent: int, p: int) -> int: + """ + Compute the CRT private_exponent % (p - 1) value from the RSA + private_exponent (d) and p. + """ + if private_exponent <= 1 or p <= 1: + raise ValueError("Values can't be <= 1") + return private_exponent % (p - 1) + + +def rsa_crt_dmq1(private_exponent: int, q: int) -> int: + """ + Compute the CRT private_exponent % (q - 1) value from the RSA + private_exponent (d) and q. + """ + if private_exponent <= 1 or q <= 1: + raise ValueError("Values can't be <= 1") + return private_exponent % (q - 1) + + +def rsa_recover_private_exponent(e: int, p: int, q: int) -> int: + """ + Compute the RSA private_exponent (d) given the public exponent (e) + and the RSA primes p and q. + + This uses the Carmichael totient function to generate the + smallest possible working value of the private exponent. + """ + # This lambda_n is the Carmichael totient function. + # The original RSA paper uses the Euler totient function + # here: phi_n = (p - 1) * (q - 1) + # Either version of the private exponent will work, but the + # one generated by the older formulation may be larger + # than necessary. (lambda_n always divides phi_n) + if e <= 1 or p <= 1 or q <= 1: + raise ValueError("Values can't be <= 1") + return _modinv(e, lcm(p - 1, q - 1)) + + +# Controls the number of iterations rsa_recover_prime_factors will perform +# to obtain the prime factors. +_MAX_RECOVERY_ATTEMPTS = 500 + + +def rsa_recover_prime_factors(n: int, e: int, d: int) -> tuple[int, int]: + """ + Compute factors p and q from the private exponent d. We assume that n has + no more than two factors. This function is adapted from code in PyCrypto. + """ + # reject invalid values early + if d <= 1 or e <= 1: + raise ValueError("d, e can't be <= 1") + if 17 != pow(17, e * d, n): + raise ValueError("n, d, e don't match") + # See 8.2.2(i) in Handbook of Applied Cryptography. + ktot = d * e - 1 + # The quantity d*e-1 is a multiple of phi(n), even, + # and can be represented as t*2^s. + t = ktot + while t % 2 == 0: + t = t // 2 + # Cycle through all multiplicative inverses in Zn. + # The algorithm is non-deterministic, but there is a 50% chance + # any candidate a leads to successful factoring. + # See "Digitalized Signatures and Public Key Functions as Intractable + # as Factorization", M. Rabin, 1979 + spotted = False + tries = 0 + while not spotted and tries < _MAX_RECOVERY_ATTEMPTS: + a = random.randint(2, n - 1) + tries += 1 + k = t + # Cycle through all values a^{t*2^i}=a^k + while k < ktot: + cand = pow(a, k, n) + # Check if a^k is a non-trivial root of unity (mod n) + if cand != 1 and cand != (n - 1) and pow(cand, 2, n) == 1: + # We have found a number such that (cand-1)(cand+1)=0 (mod n). + # Either of the terms divides n. + p = gcd(cand + 1, n) + spotted = True + break + k *= 2 + if not spotted: + raise ValueError("Unable to compute factors p and q from exponent d.") + # Found ! + q, r = divmod(n, p) + assert r == 0 + p, q = sorted((p, q), reverse=True) + return (p, q) diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/types.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/types.py new file mode 100644 index 0000000..dbf4b74 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/types.py @@ -0,0 +1,94 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import typing + +from cryptography.hazmat.primitives.asymmetric import ( + dh, + dsa, + ec, + ed448, + ed25519, + mldsa, + mlkem, + rsa, + x448, + x25519, +) + +# Every asymmetric key type. These use the private DH aliases so that +# importing this module doesn't trigger the FFDH deprecation warning. +PublicKeyTypes = typing.Union[ + dh._DHPublicKey, + dsa.DSAPublicKey, + rsa.RSAPublicKey, + ec.EllipticCurvePublicKey, + ed25519.Ed25519PublicKey, + ed448.Ed448PublicKey, + mldsa.MLDSA44PublicKey, + mldsa.MLDSA65PublicKey, + mldsa.MLDSA87PublicKey, + mlkem.MLKEM768PublicKey, + mlkem.MLKEM1024PublicKey, + x25519.X25519PublicKey, + x448.X448PublicKey, +] +# Every asymmetric key type +PrivateKeyTypes = typing.Union[ + dh._DHPrivateKey, + ed25519.Ed25519PrivateKey, + ed448.Ed448PrivateKey, + mldsa.MLDSA44PrivateKey, + mldsa.MLDSA65PrivateKey, + mldsa.MLDSA87PrivateKey, + mlkem.MLKEM768PrivateKey, + mlkem.MLKEM1024PrivateKey, + rsa.RSAPrivateKey, + dsa.DSAPrivateKey, + ec.EllipticCurvePrivateKey, + x25519.X25519PrivateKey, + x448.X448PrivateKey, +] +# Just the key types we allow to be used for x509 signing. This mirrors +# the certificate public key types +CertificateIssuerPrivateKeyTypes = typing.Union[ + ed25519.Ed25519PrivateKey, + ed448.Ed448PrivateKey, + rsa.RSAPrivateKey, + dsa.DSAPrivateKey, + ec.EllipticCurvePrivateKey, + mldsa.MLDSA44PrivateKey, + mldsa.MLDSA65PrivateKey, + mldsa.MLDSA87PrivateKey, +] +# Just the key types we allow to be used for x509 signing. This mirrors +# the certificate private key types +CertificateIssuerPublicKeyTypes = typing.Union[ + dsa.DSAPublicKey, + rsa.RSAPublicKey, + ec.EllipticCurvePublicKey, + ed25519.Ed25519PublicKey, + ed448.Ed448PublicKey, + mldsa.MLDSA44PublicKey, + mldsa.MLDSA65PublicKey, + mldsa.MLDSA87PublicKey, +] +# This type removes DHPublicKey. x448/x25519/mlkem can be a public key +# but cannot be used in signing so they are allowed here. +CertificatePublicKeyTypes = typing.Union[ + dsa.DSAPublicKey, + rsa.RSAPublicKey, + ec.EllipticCurvePublicKey, + ed25519.Ed25519PublicKey, + ed448.Ed448PublicKey, + mldsa.MLDSA44PublicKey, + mldsa.MLDSA65PublicKey, + mldsa.MLDSA87PublicKey, + mlkem.MLKEM768PublicKey, + mlkem.MLKEM1024PublicKey, + x25519.X25519PublicKey, + x448.X448PublicKey, +] diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/utils.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/utils.py new file mode 100644 index 0000000..c01c342 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/utils.py @@ -0,0 +1,28 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.hazmat.bindings._rust import asn1 +from cryptography.hazmat.primitives import hashes + +decode_dss_signature = asn1.decode_dss_signature +encode_dss_signature = asn1.encode_dss_signature + + +class NoDigestInfo: + pass + + +class Prehashed: + def __init__(self, algorithm: hashes.HashAlgorithm): + if not isinstance(algorithm, hashes.HashAlgorithm): + raise TypeError("Expected instance of HashAlgorithm.") + + self._algorithm = algorithm + self._digest_size = algorithm.digest_size + + @property + def digest_size(self) -> int: + return self._digest_size diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/x25519.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/x25519.py new file mode 100644 index 0000000..7498998 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/x25519.py @@ -0,0 +1,134 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import abc + +from cryptography.exceptions import UnsupportedAlgorithm, _Reasons +from cryptography.hazmat.bindings._rust import openssl as rust_openssl +from cryptography.hazmat.primitives import _serialization +from cryptography.utils import Buffer + + +class X25519PublicKey(metaclass=abc.ABCMeta): + @classmethod + def from_public_bytes(cls, data: bytes) -> X25519PublicKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.x25519_supported(): + raise UnsupportedAlgorithm( + "X25519 is not supported by this version of OpenSSL.", + _Reasons.UNSUPPORTED_EXCHANGE_ALGORITHM, + ) + + return rust_openssl.x25519.from_public_bytes(data) + + @abc.abstractmethod + def public_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PublicFormat, + ) -> bytes: + """ + The serialized bytes of the public key. + """ + + @abc.abstractmethod + def public_bytes_raw(self) -> bytes: + """ + The raw bytes of the public key. + Equivalent to public_bytes(Raw, Raw). + """ + + @abc.abstractmethod + def __eq__(self, other: object) -> bool: + """ + Checks equality. + """ + + @abc.abstractmethod + def __copy__(self) -> X25519PublicKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> X25519PublicKey: + """ + Returns a deep copy. + """ + + +X25519PublicKey.register(rust_openssl.x25519.X25519PublicKey) + + +class X25519PrivateKey(metaclass=abc.ABCMeta): + @classmethod + def generate(cls) -> X25519PrivateKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.x25519_supported(): + raise UnsupportedAlgorithm( + "X25519 is not supported by this version of OpenSSL.", + _Reasons.UNSUPPORTED_EXCHANGE_ALGORITHM, + ) + return rust_openssl.x25519.generate_key() + + @classmethod + def from_private_bytes(cls, data: Buffer) -> X25519PrivateKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.x25519_supported(): + raise UnsupportedAlgorithm( + "X25519 is not supported by this version of OpenSSL.", + _Reasons.UNSUPPORTED_EXCHANGE_ALGORITHM, + ) + + return rust_openssl.x25519.from_private_bytes(data) + + @abc.abstractmethod + def public_key(self) -> X25519PublicKey: + """ + Returns the public key associated with this private key + """ + + @abc.abstractmethod + def private_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PrivateFormat, + encryption_algorithm: _serialization.KeySerializationEncryption, + ) -> bytes: + """ + The serialized bytes of the private key. + """ + + @abc.abstractmethod + def private_bytes_raw(self) -> bytes: + """ + The raw bytes of the private key. + Equivalent to private_bytes(Raw, Raw, NoEncryption()). + """ + + @abc.abstractmethod + def exchange(self, peer_public_key: X25519PublicKey) -> bytes: + """ + Performs a key exchange operation using the provided peer's public key. + """ + + @abc.abstractmethod + def __copy__(self) -> X25519PrivateKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> X25519PrivateKey: + """ + Returns a deep copy. + """ + + +X25519PrivateKey.register(rust_openssl.x25519.X25519PrivateKey) diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/x448.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/x448.py new file mode 100644 index 0000000..b9dc826 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/asymmetric/x448.py @@ -0,0 +1,137 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import abc + +from cryptography.exceptions import UnsupportedAlgorithm, _Reasons +from cryptography.hazmat.bindings._rust import openssl as rust_openssl +from cryptography.hazmat.primitives import _serialization +from cryptography.utils import Buffer + + +class X448PublicKey(metaclass=abc.ABCMeta): + @classmethod + def from_public_bytes(cls, data: bytes) -> X448PublicKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.x448_supported(): + raise UnsupportedAlgorithm( + "X448 is not supported by this version of OpenSSL.", + _Reasons.UNSUPPORTED_EXCHANGE_ALGORITHM, + ) + + return rust_openssl.x448.from_public_bytes(data) + + @abc.abstractmethod + def public_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PublicFormat, + ) -> bytes: + """ + The serialized bytes of the public key. + """ + + @abc.abstractmethod + def public_bytes_raw(self) -> bytes: + """ + The raw bytes of the public key. + Equivalent to public_bytes(Raw, Raw). + """ + + @abc.abstractmethod + def __eq__(self, other: object) -> bool: + """ + Checks equality. + """ + + @abc.abstractmethod + def __copy__(self) -> X448PublicKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> X448PublicKey: + """ + Returns a deep copy. + """ + + +if hasattr(rust_openssl, "x448"): + X448PublicKey.register(rust_openssl.x448.X448PublicKey) + + +class X448PrivateKey(metaclass=abc.ABCMeta): + @classmethod + def generate(cls) -> X448PrivateKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.x448_supported(): + raise UnsupportedAlgorithm( + "X448 is not supported by this version of OpenSSL.", + _Reasons.UNSUPPORTED_EXCHANGE_ALGORITHM, + ) + + return rust_openssl.x448.generate_key() + + @classmethod + def from_private_bytes(cls, data: Buffer) -> X448PrivateKey: + from cryptography.hazmat.backends.openssl.backend import backend + + if not backend.x448_supported(): + raise UnsupportedAlgorithm( + "X448 is not supported by this version of OpenSSL.", + _Reasons.UNSUPPORTED_EXCHANGE_ALGORITHM, + ) + + return rust_openssl.x448.from_private_bytes(data) + + @abc.abstractmethod + def public_key(self) -> X448PublicKey: + """ + Returns the public key associated with this private key + """ + + @abc.abstractmethod + def private_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PrivateFormat, + encryption_algorithm: _serialization.KeySerializationEncryption, + ) -> bytes: + """ + The serialized bytes of the private key. + """ + + @abc.abstractmethod + def private_bytes_raw(self) -> bytes: + """ + The raw bytes of the private key. + Equivalent to private_bytes(Raw, Raw, NoEncryption()). + """ + + @abc.abstractmethod + def exchange(self, peer_public_key: X448PublicKey) -> bytes: + """ + Performs a key exchange operation using the provided peer's public key. + """ + + @abc.abstractmethod + def __copy__(self) -> X448PrivateKey: + """ + Returns a copy. + """ + + @abc.abstractmethod + def __deepcopy__(self, memo: dict) -> X448PrivateKey: + """ + Returns a deep copy. + """ + + +if hasattr(rust_openssl, "x448"): + X448PrivateKey.register(rust_openssl.x448.X448PrivateKey) diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/ciphers/__init__.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/ciphers/__init__.py new file mode 100644 index 0000000..10c15d0 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/ciphers/__init__.py @@ -0,0 +1,27 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.hazmat.primitives._cipheralgorithm import ( + BlockCipherAlgorithm, + CipherAlgorithm, +) +from cryptography.hazmat.primitives.ciphers.base import ( + AEADCipherContext, + AEADDecryptionContext, + AEADEncryptionContext, + Cipher, + CipherContext, +) + +__all__ = [ + "AEADCipherContext", + "AEADDecryptionContext", + "AEADEncryptionContext", + "BlockCipherAlgorithm", + "Cipher", + "CipherAlgorithm", + "CipherContext", +] diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/ciphers/aead.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/ciphers/aead.py new file mode 100644 index 0000000..c8a582d --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/ciphers/aead.py @@ -0,0 +1,23 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.hazmat.bindings._rust import openssl as rust_openssl + +__all__ = [ + "AESCCM", + "AESGCM", + "AESGCMSIV", + "AESOCB3", + "AESSIV", + "ChaCha20Poly1305", +] + +AESGCM = rust_openssl.aead.AESGCM +ChaCha20Poly1305 = rust_openssl.aead.ChaCha20Poly1305 +AESCCM = rust_openssl.aead.AESCCM +AESSIV = rust_openssl.aead.AESSIV +AESOCB3 = rust_openssl.aead.AESOCB3 +AESGCMSIV = rust_openssl.aead.AESGCMSIV diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/ciphers/algorithms.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/ciphers/algorithms.py new file mode 100644 index 0000000..6b20dd5 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/ciphers/algorithms.py @@ -0,0 +1,138 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography import utils +from cryptography.hazmat.decrepit.ciphers.algorithms import ( + ARC4 as ARC4, +) +from cryptography.hazmat.decrepit.ciphers.algorithms import ( + CAST5 as CAST5, +) +from cryptography.hazmat.decrepit.ciphers.algorithms import ( + IDEA as IDEA, +) +from cryptography.hazmat.decrepit.ciphers.algorithms import ( + SEED as SEED, +) +from cryptography.hazmat.decrepit.ciphers.algorithms import ( + Blowfish as Blowfish, +) +from cryptography.hazmat.decrepit.ciphers.algorithms import ( + Camellia as Camellia, +) +from cryptography.hazmat.decrepit.ciphers.algorithms import ( + TripleDES as TripleDES, +) +from cryptography.hazmat.primitives._cipheralgorithm import _verify_key_size +from cryptography.hazmat.primitives.ciphers import ( + BlockCipherAlgorithm, + CipherAlgorithm, +) + + +class AES(BlockCipherAlgorithm): + name = "AES" + block_size = 128 + # 512 added to support AES-256-XTS, which uses 512-bit keys + key_sizes = frozenset([128, 192, 256, 512]) + + def __init__(self, key: utils.Buffer): + self.key = _verify_key_size(self, key) + + @property + def key_size(self) -> int: + return len(self.key) * 8 + + +class AES128(BlockCipherAlgorithm): + name = "AES" + block_size = 128 + key_sizes = frozenset([128]) + key_size = 128 + + def __init__(self, key: utils.Buffer): + self.key = _verify_key_size(self, key) + + +class AES256(BlockCipherAlgorithm): + name = "AES" + block_size = 128 + key_sizes = frozenset([256]) + key_size = 256 + + def __init__(self, key: utils.Buffer): + self.key = _verify_key_size(self, key) + + +utils.deprecated( + Camellia, + __name__, + "Camellia has been moved to " + "cryptography.hazmat.decrepit.ciphers.algorithms.Camellia and " + "will be removed from " + "cryptography.hazmat.primitives.ciphers.algorithms in 49.0.0.", + utils.DeprecatedIn43, + name="Camellia", +) + + +utils.deprecated( + ARC4, + __name__, + "ARC4 has been moved to " + "cryptography.hazmat.decrepit.ciphers.algorithms.ARC4 and " + "will be removed from " + "cryptography.hazmat.primitives.ciphers.algorithms in 48.0.0.", + utils.DeprecatedIn43, + name="ARC4", +) + + +utils.deprecated( + TripleDES, + __name__, + "TripleDES has been moved to " + "cryptography.hazmat.decrepit.ciphers.algorithms.TripleDES and " + "will be removed from " + "cryptography.hazmat.primitives.ciphers.algorithms in 48.0.0.", + utils.DeprecatedIn43, + name="TripleDES", +) + + +class ChaCha20(CipherAlgorithm): + name = "ChaCha20" + key_sizes = frozenset([256]) + + def __init__(self, key: utils.Buffer, nonce: utils.Buffer): + self.key = _verify_key_size(self, key) + utils._check_byteslike("nonce", nonce) + + if len(nonce) != 16: + raise ValueError("nonce must be 128-bits (16 bytes)") + + self._nonce = nonce + + @property + def nonce(self) -> utils.Buffer: + return self._nonce + + @property + def key_size(self) -> int: + return len(self.key) * 8 + + +class SM4(BlockCipherAlgorithm): + name = "SM4" + block_size = 128 + key_sizes = frozenset([128]) + + def __init__(self, key: bytes): + self.key = _verify_key_size(self, key) + + @property + def key_size(self) -> int: + return len(self.key) * 8 diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/ciphers/base.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/ciphers/base.py new file mode 100644 index 0000000..24fceea --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/ciphers/base.py @@ -0,0 +1,146 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import abc +import typing + +from cryptography.hazmat.bindings._rust import openssl as rust_openssl +from cryptography.hazmat.primitives._cipheralgorithm import CipherAlgorithm +from cryptography.hazmat.primitives.ciphers import modes +from cryptography.utils import Buffer + + +class CipherContext(metaclass=abc.ABCMeta): + @abc.abstractmethod + def update(self, data: Buffer) -> bytes: + """ + Processes the provided bytes through the cipher and returns the results + as bytes. + """ + + @abc.abstractmethod + def update_into(self, data: Buffer, buf: Buffer) -> int: + """ + Processes the provided bytes and writes the resulting data into the + provided buffer. Returns the number of bytes written. + """ + + @abc.abstractmethod + def finalize(self) -> bytes: + """ + Returns the results of processing the final block as bytes. + """ + + @abc.abstractmethod + def reset_nonce(self, nonce: bytes) -> None: + """ + Resets the nonce for the cipher context to the provided value. + Raises an exception if it does not support reset or if the + provided nonce does not have a valid length. + """ + + +class AEADCipherContext(CipherContext, metaclass=abc.ABCMeta): + @abc.abstractmethod + def authenticate_additional_data(self, data: Buffer) -> None: + """ + Authenticates the provided bytes. + """ + + +class AEADDecryptionContext(AEADCipherContext, metaclass=abc.ABCMeta): + @abc.abstractmethod + def finalize_with_tag(self, tag: bytes) -> bytes: + """ + Returns the results of processing the final block as bytes and allows + delayed passing of the authentication tag. + """ + + +class AEADEncryptionContext(AEADCipherContext, metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def tag(self) -> bytes: + """ + Returns tag bytes. This is only available after encryption is + finalized. + """ + + +Mode = typing.TypeVar( + "Mode", bound=typing.Optional[modes.Mode], covariant=True +) + + +class Cipher(typing.Generic[Mode]): + def __init__( + self, + algorithm: CipherAlgorithm, + mode: Mode, + backend: typing.Any = None, + ) -> None: + if not isinstance(algorithm, CipherAlgorithm): + raise TypeError("Expected interface of CipherAlgorithm.") + + if mode is not None: + # mypy needs this assert to narrow the type from our generic + # type. Maybe it won't some time in the future. + assert isinstance(mode, modes.Mode) + mode.validate_for_algorithm(algorithm) + + self.algorithm = algorithm + self.mode = mode + + @typing.overload + def encryptor( + self: Cipher[modes.ModeWithAuthenticationTag], + ) -> AEADEncryptionContext: ... + + @typing.overload + def encryptor( + self: _CIPHER_TYPE, + ) -> CipherContext: ... + + def encryptor(self): + if isinstance(self.mode, modes.ModeWithAuthenticationTag): + if self.mode.tag is not None: + raise ValueError( + "Authentication tag must be None when encrypting." + ) + + return rust_openssl.ciphers.create_encryption_ctx( + self.algorithm, self.mode + ) + + @typing.overload + def decryptor( + self: Cipher[modes.ModeWithAuthenticationTag], + ) -> AEADDecryptionContext: ... + + @typing.overload + def decryptor( + self: _CIPHER_TYPE, + ) -> CipherContext: ... + + def decryptor(self): + return rust_openssl.ciphers.create_decryption_ctx( + self.algorithm, self.mode + ) + + +_CIPHER_TYPE = Cipher[ + typing.Union[ + modes.ModeWithNonce, + modes.ModeWithTweak, + modes.ECB, + modes.ModeWithInitializationVector, + None, + ] +] + +CipherContext.register(rust_openssl.ciphers.CipherContext) +AEADEncryptionContext.register(rust_openssl.ciphers.AEADEncryptionContext) +AEADDecryptionContext.register(rust_openssl.ciphers.AEADDecryptionContext) diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/ciphers/modes.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/ciphers/modes.py new file mode 100644 index 0000000..c2008e9 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/ciphers/modes.py @@ -0,0 +1,192 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography import utils +from cryptography.exceptions import UnsupportedAlgorithm, _Reasons +from cryptography.hazmat.decrepit.ciphers.modes import CFB as CFB +from cryptography.hazmat.decrepit.ciphers.modes import CFB8 as CFB8 +from cryptography.hazmat.decrepit.ciphers.modes import OFB as OFB +from cryptography.hazmat.primitives._cipheralgorithm import ( + BlockCipherAlgorithm, + CipherAlgorithm, +) +from cryptography.hazmat.primitives._modes import ( + Mode as Mode, +) +from cryptography.hazmat.primitives._modes import ( + ModeWithAuthenticationTag as ModeWithAuthenticationTag, +) +from cryptography.hazmat.primitives._modes import ( + ModeWithInitializationVector as ModeWithInitializationVector, +) +from cryptography.hazmat.primitives._modes import ( + ModeWithNonce as ModeWithNonce, +) +from cryptography.hazmat.primitives._modes import ( + ModeWithTweak as ModeWithTweak, +) +from cryptography.hazmat.primitives._modes import ( + _check_aes_key_length, + _check_iv_and_key_length, + _check_nonce_length, +) +from cryptography.hazmat.primitives.ciphers import algorithms + + +class CBC(ModeWithInitializationVector): + name = "CBC" + + def __init__(self, initialization_vector: utils.Buffer): + utils._check_byteslike("initialization_vector", initialization_vector) + self._initialization_vector = initialization_vector + + @property + def initialization_vector(self) -> utils.Buffer: + return self._initialization_vector + + validate_for_algorithm = _check_iv_and_key_length + + +class XTS(ModeWithTweak): + name = "XTS" + + def __init__(self, tweak: utils.Buffer): + utils._check_byteslike("tweak", tweak) + + if len(tweak) != 16: + raise ValueError("tweak must be 128-bits (16 bytes)") + + self._tweak = tweak + + @property + def tweak(self) -> utils.Buffer: + return self._tweak + + def validate_for_algorithm(self, algorithm: CipherAlgorithm) -> None: + if isinstance(algorithm, (algorithms.AES128, algorithms.AES256)): + raise TypeError( + "The AES128 and AES256 classes do not support XTS, please use " + "the standard AES class instead." + ) + + if algorithm.key_size not in (256, 512): + raise ValueError( + "The XTS specification requires a 256-bit key for AES-128-XTS" + " and 512-bit key for AES-256-XTS" + ) + + +class ECB(Mode): + name = "ECB" + + validate_for_algorithm = _check_aes_key_length + + +class CTR(ModeWithNonce): + name = "CTR" + + def __init__(self, nonce: utils.Buffer): + utils._check_byteslike("nonce", nonce) + self._nonce = nonce + + @property + def nonce(self) -> utils.Buffer: + return self._nonce + + def validate_for_algorithm(self, algorithm: CipherAlgorithm) -> None: + _check_aes_key_length(self, algorithm) + _check_nonce_length(self.nonce, self.name, algorithm) + + +class GCM(ModeWithInitializationVector, ModeWithAuthenticationTag): + name = "GCM" + _MAX_ENCRYPTED_BYTES = (2**39 - 256) // 8 + _MAX_AAD_BYTES = (2**64 - 1) // 8 + + def __init__( + self, + initialization_vector: utils.Buffer, + tag: bytes | None = None, + min_tag_length: int = 16, + ): + # OpenSSL 3.0.0 constrains GCM IVs to [64, 1024] bits inclusive + # This is a sane limit anyway so we'll enforce it here. + utils._check_byteslike("initialization_vector", initialization_vector) + if len(initialization_vector) < 8 or len(initialization_vector) > 128: + raise ValueError( + "initialization_vector must be between 8 and 128 bytes (64 " + "and 1024 bits)." + ) + self._initialization_vector = initialization_vector + if min_tag_length < 4: + raise ValueError("min_tag_length must be >= 4") + if tag is not None: + utils._check_bytes("tag", tag) + if len(tag) < min_tag_length: + raise ValueError( + f"Authentication tag must be {min_tag_length} bytes or " + "longer." + ) + self._tag = tag + self._min_tag_length = min_tag_length + + @property + def tag(self) -> bytes | None: + return self._tag + + @property + def initialization_vector(self) -> utils.Buffer: + return self._initialization_vector + + def validate_for_algorithm(self, algorithm: CipherAlgorithm) -> None: + _check_aes_key_length(self, algorithm) + if not isinstance(algorithm, BlockCipherAlgorithm): + raise UnsupportedAlgorithm( + "GCM requires a block cipher algorithm", + _Reasons.UNSUPPORTED_CIPHER, + ) + block_size_bytes = algorithm.block_size // 8 + if self._tag is not None and len(self._tag) > block_size_bytes: + raise ValueError( + f"Authentication tag cannot be more than {block_size_bytes} " + "bytes." + ) + + +utils.deprecated( + OFB, + __name__, + "OFB has been moved to " + "cryptography.hazmat.decrepit.ciphers.modes.OFB and " + "will be removed from " + "cryptography.hazmat.primitives.ciphers.modes in 49.0.0.", + utils.DeprecatedIn47, + name="OFB", +) + + +utils.deprecated( + CFB, + __name__, + "CFB has been moved to " + "cryptography.hazmat.decrepit.ciphers.modes.CFB and " + "will be removed from " + "cryptography.hazmat.primitives.ciphers.modes in 49.0.0.", + utils.DeprecatedIn47, + name="CFB", +) + + +utils.deprecated( + CFB8, + __name__, + "CFB8 has been moved to " + "cryptography.hazmat.decrepit.ciphers.modes.CFB8 and " + "will be removed from " + "cryptography.hazmat.primitives.ciphers.modes in 49.0.0.", + utils.DeprecatedIn47, + name="CFB8", +) diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/cmac.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/cmac.py new file mode 100644 index 0000000..2c67ce2 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/cmac.py @@ -0,0 +1,10 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.hazmat.bindings._rust import openssl as rust_openssl + +__all__ = ["CMAC"] +CMAC = rust_openssl.cmac.CMAC diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/constant_time.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/constant_time.py new file mode 100644 index 0000000..3975c71 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/constant_time.py @@ -0,0 +1,14 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import hmac + + +def bytes_eq(a: bytes, b: bytes) -> bool: + if not isinstance(a, bytes) or not isinstance(b, bytes): + raise TypeError("a and b must be bytes.") + + return hmac.compare_digest(a, b) diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/hashes.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/hashes.py new file mode 100644 index 0000000..50ed355 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/hashes.py @@ -0,0 +1,357 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import abc +import sys + +from cryptography.hazmat.bindings._rust import openssl as rust_openssl +from cryptography.utils import Buffer + +__all__ = [ + "MD5", + "SHA1", + "SHA3_224", + "SHA3_256", + "SHA3_384", + "SHA3_512", + "SHA224", + "SHA256", + "SHA384", + "SHA512", + "SHA512_224", + "SHA512_256", + "SHAKE128", + "SHAKE256", + "SM3", + "BLAKE2b", + "BLAKE2s", + "ExtendableOutputFunction", + "Hash", + "HashAlgorithm", + "HashContext", + "XOFHash", +] + + +class HashAlgorithm(metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def name(self) -> str: + """ + A string naming this algorithm (e.g. "sha256", "md5"). + """ + + @property + @abc.abstractmethod + def digest_size(self) -> int: + """ + The size of the resulting digest in bytes. + """ + + @property + @abc.abstractmethod + def block_size(self) -> int | None: + """ + The internal block size of the hash function, or None if the hash + function does not use blocks internally (e.g. SHA3). + """ + + +class HashContext(metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def algorithm(self) -> HashAlgorithm: + """ + A HashAlgorithm that will be used by this context. + """ + + @abc.abstractmethod + def update(self, data: Buffer) -> None: + """ + Processes the provided bytes through the hash. + """ + + @abc.abstractmethod + def finalize(self) -> bytes: + """ + Finalizes the hash context and returns the hash digest as bytes. + """ + + @abc.abstractmethod + def copy(self) -> HashContext: + """ + Return a HashContext that is a copy of the current context. + """ + + +Hash = rust_openssl.hashes.Hash +HashContext.register(Hash) + +XOFHash = rust_openssl.hashes.XOFHash + + +class ExtendableOutputFunction(metaclass=abc.ABCMeta): + """ + An interface for extendable output functions. + """ + + +class SHA1(HashAlgorithm): + name = "sha1" + digest_size = 20 + block_size = 64 + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SHA1): + return NotImplemented + + return True + + +class SHA512_224(HashAlgorithm): # noqa: N801 + name = "sha512-224" + digest_size = 28 + block_size = 128 + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SHA512_224): + return NotImplemented + + return True + + +class SHA512_256(HashAlgorithm): # noqa: N801 + name = "sha512-256" + digest_size = 32 + block_size = 128 + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SHA512_256): + return NotImplemented + + return True + + +class SHA224(HashAlgorithm): + name = "sha224" + digest_size = 28 + block_size = 64 + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SHA224): + return NotImplemented + + return True + + +class SHA256(HashAlgorithm): + name = "sha256" + digest_size = 32 + block_size = 64 + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SHA256): + return NotImplemented + + return True + + +class SHA384(HashAlgorithm): + name = "sha384" + digest_size = 48 + block_size = 128 + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SHA384): + return NotImplemented + + return True + + +class SHA512(HashAlgorithm): + name = "sha512" + digest_size = 64 + block_size = 128 + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SHA512): + return NotImplemented + + return True + + +class SHA3_224(HashAlgorithm): # noqa: N801 + name = "sha3-224" + digest_size = 28 + block_size = None + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SHA3_224): + return NotImplemented + + return True + + +class SHA3_256(HashAlgorithm): # noqa: N801 + name = "sha3-256" + digest_size = 32 + block_size = None + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SHA3_256): + return NotImplemented + + return True + + +class SHA3_384(HashAlgorithm): # noqa: N801 + name = "sha3-384" + digest_size = 48 + block_size = None + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SHA3_384): + return NotImplemented + + return True + + +class SHA3_512(HashAlgorithm): # noqa: N801 + name = "sha3-512" + digest_size = 64 + block_size = None + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SHA3_512): + return NotImplemented + + return True + + +class SHAKE128(HashAlgorithm, ExtendableOutputFunction): + name = "shake128" + block_size = None + + def __init__(self, digest_size: int): + if not isinstance(digest_size, int): + raise TypeError("digest_size must be an integer") + + if digest_size < 1: + raise ValueError("digest_size must be a positive integer") + + self._digest_size = digest_size + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SHAKE128): + return NotImplemented + + return self._digest_size == other._digest_size + + @property + def digest_size(self) -> int: + return self._digest_size + + @classmethod + def xof(cls): + return cls(sys.maxsize) + + +class SHAKE256(HashAlgorithm, ExtendableOutputFunction): + name = "shake256" + block_size = None + + def __init__(self, digest_size: int): + if not isinstance(digest_size, int): + raise TypeError("digest_size must be an integer") + + if digest_size < 1: + raise ValueError("digest_size must be a positive integer") + + self._digest_size = digest_size + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SHAKE256): + return NotImplemented + + return self._digest_size == other._digest_size + + @property + def digest_size(self) -> int: + return self._digest_size + + @classmethod + def xof(cls): + return cls(sys.maxsize) + + +class MD5(HashAlgorithm): + name = "md5" + digest_size = 16 + block_size = 64 + + def __eq__(self, other: object) -> bool: + if not isinstance(other, MD5): + return NotImplemented + + return True + + +class BLAKE2b(HashAlgorithm): + name = "blake2b" + _max_digest_size = 64 + _min_digest_size = 1 + block_size = 128 + + def __init__(self, digest_size: int): + if digest_size != 64: + raise ValueError("Digest size must be 64") + + self._digest_size = digest_size + + def __eq__(self, other: object) -> bool: + if not isinstance(other, BLAKE2b): + return NotImplemented + + return self._digest_size == other._digest_size + + @property + def digest_size(self) -> int: + return self._digest_size + + +class BLAKE2s(HashAlgorithm): + name = "blake2s" + block_size = 64 + _max_digest_size = 32 + _min_digest_size = 1 + + def __init__(self, digest_size: int): + if digest_size != 32: + raise ValueError("Digest size must be 32") + + self._digest_size = digest_size + + def __eq__(self, other: object) -> bool: + if not isinstance(other, BLAKE2s): + return NotImplemented + + return self._digest_size == other._digest_size + + @property + def digest_size(self) -> int: + return self._digest_size + + +class SM3(HashAlgorithm): + name = "sm3" + digest_size = 32 + block_size = 64 + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SM3): + return NotImplemented + + return True diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/hmac.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/hmac.py new file mode 100644 index 0000000..a9442d5 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/hmac.py @@ -0,0 +1,13 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.hazmat.bindings._rust import openssl as rust_openssl +from cryptography.hazmat.primitives import hashes + +__all__ = ["HMAC"] + +HMAC = rust_openssl.hmac.HMAC +hashes.HashContext.register(HMAC) diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/hpke.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/hpke.py new file mode 100644 index 0000000..c802808 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/hpke.py @@ -0,0 +1,27 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.hazmat.bindings._rust import openssl as rust_openssl + +AEAD = rust_openssl.hpke.AEAD +KDF = rust_openssl.hpke.KDF +KEM = rust_openssl.hpke.KEM +MLKEM768X25519PrivateKey = rust_openssl.hpke.MLKEM768X25519PrivateKey +MLKEM768X25519PublicKey = rust_openssl.hpke.MLKEM768X25519PublicKey +MLKEM1024P384PrivateKey = rust_openssl.hpke.MLKEM1024P384PrivateKey +MLKEM1024P384PublicKey = rust_openssl.hpke.MLKEM1024P384PublicKey +Suite = rust_openssl.hpke.Suite + +__all__ = [ + "AEAD", + "KDF", + "KEM", + "MLKEM768X25519PrivateKey", + "MLKEM768X25519PublicKey", + "MLKEM1024P384PrivateKey", + "MLKEM1024P384PublicKey", + "Suite", +] diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/__init__.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/__init__.py new file mode 100644 index 0000000..26c45bd --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/__init__.py @@ -0,0 +1,32 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import abc + +from cryptography.utils import Buffer + + +class KeyDerivationFunction(metaclass=abc.ABCMeta): + @abc.abstractmethod + def derive(self, key_material: bytes) -> bytes: + """ + Deterministically generates and returns a new key based on the existing + key material. + """ + + @abc.abstractmethod + def derive_into(self, key_material: bytes, buffer: Buffer) -> None: + """ + Deterministically generates a new key based on the existing key + material and stores it in the provided buffer. + """ + + @abc.abstractmethod + def verify(self, key_material: bytes, expected_key: bytes) -> None: + """ + Checks whether the key generated by the key material matches the + expected derived key. Raises an exception if they do not match. + """ diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/argon2.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/argon2.py new file mode 100644 index 0000000..03e84d4 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/argon2.py @@ -0,0 +1,17 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.hazmat.bindings._rust import openssl as rust_openssl +from cryptography.hazmat.primitives.kdf import KeyDerivationFunction + +Argon2d = rust_openssl.kdf.Argon2d +Argon2i = rust_openssl.kdf.Argon2i +Argon2id = rust_openssl.kdf.Argon2id +KeyDerivationFunction.register(Argon2d) +KeyDerivationFunction.register(Argon2i) +KeyDerivationFunction.register(Argon2id) + +__all__ = ["Argon2d", "Argon2i", "Argon2id"] diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/concatkdf.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/concatkdf.py new file mode 100644 index 0000000..398dc5d --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/concatkdf.py @@ -0,0 +1,16 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.hazmat.bindings._rust import openssl as rust_openssl +from cryptography.hazmat.primitives.kdf import KeyDerivationFunction + +ConcatKDFHash = rust_openssl.kdf.ConcatKDFHash +ConcatKDFHMAC = rust_openssl.kdf.ConcatKDFHMAC + +KeyDerivationFunction.register(ConcatKDFHash) +KeyDerivationFunction.register(ConcatKDFHMAC) + +__all__ = ["ConcatKDFHMAC", "ConcatKDFHash"] diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/hkdf.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/hkdf.py new file mode 100644 index 0000000..1e162d9 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/hkdf.py @@ -0,0 +1,16 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.hazmat.bindings._rust import openssl as rust_openssl +from cryptography.hazmat.primitives.kdf import KeyDerivationFunction + +HKDF = rust_openssl.kdf.HKDF +HKDFExpand = rust_openssl.kdf.HKDFExpand + +KeyDerivationFunction.register(HKDF) +KeyDerivationFunction.register(HKDFExpand) + +__all__ = ["HKDF", "HKDFExpand"] diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/kbkdf.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/kbkdf.py new file mode 100644 index 0000000..e559df8 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/kbkdf.py @@ -0,0 +1,26 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography import utils +from cryptography.hazmat.bindings._rust import openssl as rust_openssl +from cryptography.hazmat.primitives.kdf import KeyDerivationFunction + + +class Mode(utils.Enum): + CounterMode = "ctr" + + +class CounterLocation(utils.Enum): + BeforeFixed = "before_fixed" + AfterFixed = "after_fixed" + MiddleFixed = "middle_fixed" + + +KBKDFHMAC = rust_openssl.kdf.KBKDFHMAC +KeyDerivationFunction.register(KBKDFHMAC) + +KBKDFCMAC = rust_openssl.kdf.KBKDFCMAC +KeyDerivationFunction.register(KBKDFCMAC) diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/pbkdf2.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/pbkdf2.py new file mode 100644 index 0000000..771d80d --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/pbkdf2.py @@ -0,0 +1,13 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.hazmat.bindings._rust import openssl as rust_openssl +from cryptography.hazmat.primitives.kdf import KeyDerivationFunction + +PBKDF2HMAC = rust_openssl.kdf.PBKDF2HMAC +KeyDerivationFunction.register(PBKDF2HMAC) + +__all__ = ["PBKDF2HMAC"] diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/scrypt.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/scrypt.py new file mode 100644 index 0000000..f791cee --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/scrypt.py @@ -0,0 +1,19 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import sys + +from cryptography.hazmat.bindings._rust import openssl as rust_openssl +from cryptography.hazmat.primitives.kdf import KeyDerivationFunction + +# This is used by the scrypt tests to skip tests that require more memory +# than the MEM_LIMIT +_MEM_LIMIT = sys.maxsize // 2 + +Scrypt = rust_openssl.kdf.Scrypt +KeyDerivationFunction.register(Scrypt) + +__all__ = ["Scrypt"] diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/x963kdf.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/x963kdf.py new file mode 100644 index 0000000..8c4e2d3 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/kdf/x963kdf.py @@ -0,0 +1,13 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.hazmat.bindings._rust import openssl as rust_openssl +from cryptography.hazmat.primitives.kdf import KeyDerivationFunction + +X963KDF = rust_openssl.kdf.X963KDF +KeyDerivationFunction.register(X963KDF) + +__all__ = ["X963KDF"] diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/keywrap.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/keywrap.py new file mode 100644 index 0000000..123d97b --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/keywrap.py @@ -0,0 +1,25 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.hazmat.bindings._rust import openssl as rust_openssl + +__all__ = [ + "InvalidUnwrap", + "aes_key_unwrap", + "aes_key_unwrap_with_padding", + "aes_key_wrap", + "aes_key_wrap_with_padding", +] + + +class InvalidUnwrap(Exception): + pass + + +aes_key_wrap = rust_openssl.keywrap.aes_key_wrap +aes_key_unwrap = rust_openssl.keywrap.aes_key_unwrap +aes_key_wrap_with_padding = rust_openssl.keywrap.aes_key_wrap_with_padding +aes_key_unwrap_with_padding = rust_openssl.keywrap.aes_key_unwrap_with_padding diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/padding.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/padding.py new file mode 100644 index 0000000..f9cd1f1 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/padding.py @@ -0,0 +1,69 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import abc + +from cryptography import utils +from cryptography.hazmat.bindings._rust import ( + ANSIX923PaddingContext, + ANSIX923UnpaddingContext, + PKCS7PaddingContext, + PKCS7UnpaddingContext, +) + + +class PaddingContext(metaclass=abc.ABCMeta): + @abc.abstractmethod + def update(self, data: utils.Buffer) -> bytes: + """ + Pads the provided bytes and returns any available data as bytes. + """ + + @abc.abstractmethod + def finalize(self) -> bytes: + """ + Finalize the padding, returns bytes. + """ + + +def _byte_padding_check(block_size: int) -> None: + if not (0 <= block_size <= 2040): + raise ValueError("block_size must be in range(0, 2041).") + + if block_size % 8 != 0: + raise ValueError("block_size must be a multiple of 8.") + + +class PKCS7: + def __init__(self, block_size: int): + _byte_padding_check(block_size) + self.block_size = block_size + + def padder(self) -> PaddingContext: + return PKCS7PaddingContext(self.block_size) + + def unpadder(self) -> PaddingContext: + return PKCS7UnpaddingContext(self.block_size) + + +PaddingContext.register(PKCS7PaddingContext) +PaddingContext.register(PKCS7UnpaddingContext) + + +class ANSIX923: + def __init__(self, block_size: int): + _byte_padding_check(block_size) + self.block_size = block_size + + def padder(self) -> PaddingContext: + return ANSIX923PaddingContext(self.block_size) + + def unpadder(self) -> PaddingContext: + return ANSIX923UnpaddingContext(self.block_size) + + +PaddingContext.register(ANSIX923PaddingContext) +PaddingContext.register(ANSIX923UnpaddingContext) diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/poly1305.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/poly1305.py new file mode 100644 index 0000000..7f5a77a --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/poly1305.py @@ -0,0 +1,11 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.hazmat.bindings._rust import openssl as rust_openssl + +__all__ = ["Poly1305"] + +Poly1305 = rust_openssl.poly1305.Poly1305 diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/serialization/__init__.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/serialization/__init__.py new file mode 100644 index 0000000..40beb21 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/serialization/__init__.py @@ -0,0 +1,85 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography import utils +from cryptography.hazmat.primitives._serialization import ( + BestAvailableEncryption, + Encoding, + KeySerializationEncryption, + NoEncryption, + ParameterFormat, + PrivateFormat, + PublicFormat, + _KeySerializationEncryption, +) +from cryptography.hazmat.primitives.asymmetric.dh import _FFDH_DEPRECATION_MSG +from cryptography.hazmat.primitives.serialization.base import ( + load_der_parameters, + load_der_private_key, + load_der_public_key, + load_pem_parameters, + load_pem_private_key, + load_pem_public_key, +) +from cryptography.hazmat.primitives.serialization.ssh import ( + SSHCertificate, + SSHCertificateBuilder, + SSHCertificateType, + SSHCertPrivateKeyTypes, + SSHCertPublicKeyTypes, + SSHPrivateKeyTypes, + SSHPublicKeyTypes, + load_ssh_private_key, + load_ssh_public_identity, + load_ssh_public_key, + ssh_key_fingerprint, +) + +__all__ = [ + "BestAvailableEncryption", + "Encoding", + "KeySerializationEncryption", + "NoEncryption", + "ParameterFormat", + "PrivateFormat", + "PublicFormat", + "SSHCertPrivateKeyTypes", + "SSHCertPublicKeyTypes", + "SSHCertificate", + "SSHCertificateBuilder", + "SSHCertificateType", + "SSHPrivateKeyTypes", + "SSHPublicKeyTypes", + "_KeySerializationEncryption", + "load_der_parameters", + "load_der_private_key", + "load_der_public_key", + "load_pem_parameters", + "load_pem_private_key", + "load_pem_public_key", + "load_ssh_private_key", + "load_ssh_public_identity", + "load_ssh_public_key", + "ssh_key_fingerprint", +] + +# These can only load FFDH parameters, so the functions themselves are +# deprecated alongside the rest of FFDH. +utils.deprecated( + load_pem_parameters, + __name__, + _FFDH_DEPRECATION_MSG, + utils.DeprecatedIn50, + name="load_pem_parameters", +) + +utils.deprecated( + load_der_parameters, + __name__, + _FFDH_DEPRECATION_MSG, + utils.DeprecatedIn50, + name="load_der_parameters", +) diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/serialization/base.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/serialization/base.py new file mode 100644 index 0000000..f2b503b --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/serialization/base.py @@ -0,0 +1,16 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.hazmat.bindings._rust import openssl as rust_openssl + +load_pem_private_key = rust_openssl.keys.load_pem_private_key +load_der_private_key = rust_openssl.keys.load_der_private_key + +load_pem_public_key = rust_openssl.keys.load_pem_public_key +load_der_public_key = rust_openssl.keys.load_der_public_key + +load_pem_parameters = rust_openssl.dh.from_pem_parameters +load_der_parameters = rust_openssl.dh.from_der_parameters diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/serialization/pkcs12.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/serialization/pkcs12.py new file mode 100644 index 0000000..58884ff --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/serialization/pkcs12.py @@ -0,0 +1,176 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import typing +from collections.abc import Iterable + +from cryptography import x509 +from cryptography.hazmat.bindings._rust import pkcs12 as rust_pkcs12 +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives._serialization import PBES as PBES +from cryptography.hazmat.primitives.asymmetric import ( + dsa, + ec, + ed448, + ed25519, + rsa, +) +from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes + +__all__ = [ + "PBES", + "PKCS12Certificate", + "PKCS12KeyAndCertificates", + "PKCS12PrivateKeyTypes", + "load_key_and_certificates", + "load_pkcs12", + "serialize_java_truststore", + "serialize_key_and_certificates", +] + +PKCS12PrivateKeyTypes = typing.Union[ + rsa.RSAPrivateKey, + dsa.DSAPrivateKey, + ec.EllipticCurvePrivateKey, + ed25519.Ed25519PrivateKey, + ed448.Ed448PrivateKey, +] + + +PKCS12Certificate = rust_pkcs12.PKCS12Certificate + + +class PKCS12KeyAndCertificates: + def __init__( + self, + key: PrivateKeyTypes | None, + cert: PKCS12Certificate | None, + additional_certs: list[PKCS12Certificate], + ): + if key is not None and not isinstance( + key, + ( + rsa.RSAPrivateKey, + dsa.DSAPrivateKey, + ec.EllipticCurvePrivateKey, + ed25519.Ed25519PrivateKey, + ed448.Ed448PrivateKey, + ), + ): + raise TypeError( + "Key must be RSA, DSA, EllipticCurve, ED25519, or ED448" + " private key, or None." + ) + if cert is not None and not isinstance(cert, PKCS12Certificate): + raise TypeError("cert must be a PKCS12Certificate object or None") + if not all( + isinstance(add_cert, PKCS12Certificate) + for add_cert in additional_certs + ): + raise TypeError( + "all values in additional_certs must be PKCS12Certificate" + " objects" + ) + self._key = key + self._cert = cert + self._additional_certs = additional_certs + + @property + def key(self) -> PrivateKeyTypes | None: + return self._key + + @property + def cert(self) -> PKCS12Certificate | None: + return self._cert + + @property + def additional_certs(self) -> list[PKCS12Certificate]: + return self._additional_certs + + def __eq__(self, other: object) -> bool: + if not isinstance(other, PKCS12KeyAndCertificates): + return NotImplemented + + return ( + self.key == other.key + and self.cert == other.cert + and self.additional_certs == other.additional_certs + ) + + def __hash__(self) -> int: + return hash((self.key, self.cert, tuple(self.additional_certs))) + + def __repr__(self) -> str: + fmt = ( + "" + ) + return fmt.format(self.key, self.cert, self.additional_certs) + + +load_key_and_certificates = rust_pkcs12.load_key_and_certificates +load_pkcs12 = rust_pkcs12.load_pkcs12 + + +_PKCS12CATypes = typing.Union[ + x509.Certificate, + PKCS12Certificate, +] + + +def serialize_java_truststore( + certs: Iterable[PKCS12Certificate], + encryption_algorithm: serialization.KeySerializationEncryption, +) -> bytes: + if not certs: + raise ValueError("You must supply at least one cert") + + if not isinstance( + encryption_algorithm, serialization.KeySerializationEncryption + ): + raise TypeError( + "Key encryption algorithm must be a " + "KeySerializationEncryption instance" + ) + + return rust_pkcs12.serialize_java_truststore(certs, encryption_algorithm) + + +def serialize_key_and_certificates( + name: bytes | None, + key: PKCS12PrivateKeyTypes | None, + cert: x509.Certificate | None, + cas: Iterable[_PKCS12CATypes] | None, + encryption_algorithm: serialization.KeySerializationEncryption, +) -> bytes: + if key is not None and not isinstance( + key, + ( + rsa.RSAPrivateKey, + dsa.DSAPrivateKey, + ec.EllipticCurvePrivateKey, + ed25519.Ed25519PrivateKey, + ed448.Ed448PrivateKey, + ), + ): + raise TypeError( + "Key must be RSA, DSA, EllipticCurve, ED25519, or ED448" + " private key, or None." + ) + + if not isinstance( + encryption_algorithm, serialization.KeySerializationEncryption + ): + raise TypeError( + "Key encryption algorithm must be a " + "KeySerializationEncryption instance" + ) + + if key is None and cert is None and not cas: + raise ValueError("You must supply at least one of key, cert, or cas") + + return rust_pkcs12.serialize_key_and_certificates( + name, key, cert, cas, encryption_algorithm + ) diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/serialization/pkcs7.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/serialization/pkcs7.py new file mode 100644 index 0000000..76b667a --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/serialization/pkcs7.py @@ -0,0 +1,412 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import email.base64mime +import email.generator +import email.message +import email.policy +import io +import typing +from collections.abc import Iterable + +from cryptography import utils, x509 +from cryptography.exceptions import UnsupportedAlgorithm, _Reasons +from cryptography.hazmat.bindings._rust import pkcs7 as rust_pkcs7 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa +from cryptography.hazmat.primitives.ciphers import ( + algorithms, +) +from cryptography.utils import _check_byteslike + +load_pem_pkcs7_certificates = rust_pkcs7.load_pem_pkcs7_certificates + +load_der_pkcs7_certificates = rust_pkcs7.load_der_pkcs7_certificates + +serialize_certificates = rust_pkcs7.serialize_certificates + +PKCS7HashTypes = typing.Union[ + hashes.SHA224, + hashes.SHA256, + hashes.SHA384, + hashes.SHA512, +] + +PKCS7PrivateKeyTypes = typing.Union[ + rsa.RSAPrivateKey, ec.EllipticCurvePrivateKey +] + +ContentEncryptionAlgorithm = typing.Union[ + type[algorithms.AES128], type[algorithms.AES256] +] + + +class PKCS7Options(utils.Enum): + Text = "Add text/plain MIME type" + Binary = "Don't translate input data into canonical MIME format" + DetachedSignature = "Don't embed data in the PKCS7 structure" + NoCapabilities = "Don't embed SMIME capabilities" + NoAttributes = "Don't embed authenticatedAttributes" + NoCerts = "Don't embed signer certificate" + + +class PKCS7SignatureBuilder: + def __init__( + self, + data: utils.Buffer | None = None, + signers: list[ + tuple[ + x509.Certificate, + PKCS7PrivateKeyTypes, + PKCS7HashTypes, + padding.PSS | padding.PKCS1v15 | None, + ] + ] = [], + additional_certs: list[x509.Certificate] = [], + ): + self._data = data + self._signers = signers + self._additional_certs = additional_certs + + def set_data(self, data: utils.Buffer) -> PKCS7SignatureBuilder: + _check_byteslike("data", data) + if self._data is not None: + raise ValueError("data may only be set once") + + return PKCS7SignatureBuilder(data, self._signers) + + def add_signer( + self, + certificate: x509.Certificate, + private_key: PKCS7PrivateKeyTypes, + hash_algorithm: PKCS7HashTypes, + *, + rsa_padding: padding.PSS | padding.PKCS1v15 | None = None, + ) -> PKCS7SignatureBuilder: + if not isinstance( + hash_algorithm, + ( + hashes.SHA224, + hashes.SHA256, + hashes.SHA384, + hashes.SHA512, + ), + ): + raise TypeError( + "hash_algorithm must be one of hashes.SHA224, " + "SHA256, SHA384, or SHA512" + ) + if not isinstance(certificate, x509.Certificate): + raise TypeError("certificate must be a x509.Certificate") + + if not isinstance( + private_key, (rsa.RSAPrivateKey, ec.EllipticCurvePrivateKey) + ): + raise TypeError("Only RSA & EC keys are supported at this time.") + + if rsa_padding is not None: + if not isinstance(rsa_padding, (padding.PSS, padding.PKCS1v15)): + raise TypeError("Padding must be PSS or PKCS1v15") + if not isinstance(private_key, rsa.RSAPrivateKey): + raise TypeError("Padding is only supported for RSA keys") + + return PKCS7SignatureBuilder( + self._data, + [ + *self._signers, + (certificate, private_key, hash_algorithm, rsa_padding), + ], + ) + + def add_certificate( + self, certificate: x509.Certificate + ) -> PKCS7SignatureBuilder: + if not isinstance(certificate, x509.Certificate): + raise TypeError("certificate must be a x509.Certificate") + + return PKCS7SignatureBuilder( + self._data, self._signers, [*self._additional_certs, certificate] + ) + + def sign( + self, + encoding: serialization.Encoding, + options: Iterable[PKCS7Options], + backend: typing.Any = None, + ) -> bytes: + if len(self._signers) == 0: + raise ValueError("Must have at least one signer") + if self._data is None: + raise ValueError("You must add data to sign") + options = list(options) + if not all(isinstance(x, PKCS7Options) for x in options): + raise ValueError("options must be from the PKCS7Options enum") + if encoding not in ( + serialization.Encoding.PEM, + serialization.Encoding.DER, + serialization.Encoding.SMIME, + ): + raise ValueError( + "Must be PEM, DER, or SMIME from the Encoding enum" + ) + + # Text is a meaningless option unless it is accompanied by + # DetachedSignature + if ( + PKCS7Options.Text in options + and PKCS7Options.DetachedSignature not in options + ): + raise ValueError( + "When passing the Text option you must also pass " + "DetachedSignature" + ) + + if PKCS7Options.Text in options and encoding in ( + serialization.Encoding.DER, + serialization.Encoding.PEM, + ): + raise ValueError( + "The Text option is only available for SMIME serialization" + ) + + # No attributes implies no capabilities so we'll error if you try to + # pass both. + if ( + PKCS7Options.NoAttributes in options + and PKCS7Options.NoCapabilities in options + ): + raise ValueError( + "NoAttributes is a superset of NoCapabilities. Do not pass " + "both values." + ) + + return rust_pkcs7.sign_and_serialize(self, encoding, options) + + +class PKCS7EnvelopeBuilder: + def __init__( + self, + *, + _data: bytes | None = None, + _recipients: list[x509.Certificate] | None = None, + _content_encryption_algorithm: ContentEncryptionAlgorithm + | None = None, + ): + from cryptography.hazmat.backends.openssl.backend import ( + backend as ossl, + ) + + if not ossl.rsa_encryption_supported(padding=padding.PKCS1v15()): + raise UnsupportedAlgorithm( + "RSA with PKCS1 v1.5 padding is not supported by this version" + " of OpenSSL.", + _Reasons.UNSUPPORTED_PADDING, + ) + self._data = _data + self._recipients = _recipients if _recipients is not None else [] + self._content_encryption_algorithm = _content_encryption_algorithm + + def set_data(self, data: bytes) -> PKCS7EnvelopeBuilder: + _check_byteslike("data", data) + if self._data is not None: + raise ValueError("data may only be set once") + + return PKCS7EnvelopeBuilder( + _data=data, + _recipients=self._recipients, + _content_encryption_algorithm=self._content_encryption_algorithm, + ) + + def add_recipient( + self, + certificate: x509.Certificate, + ) -> PKCS7EnvelopeBuilder: + if not isinstance(certificate, x509.Certificate): + raise TypeError("certificate must be a x509.Certificate") + + if not isinstance(certificate.public_key(), rsa.RSAPublicKey): + raise TypeError("Only RSA keys are supported at this time.") + + return PKCS7EnvelopeBuilder( + _data=self._data, + _recipients=[ + *self._recipients, + certificate, + ], + _content_encryption_algorithm=self._content_encryption_algorithm, + ) + + def set_content_encryption_algorithm( + self, content_encryption_algorithm: ContentEncryptionAlgorithm + ) -> PKCS7EnvelopeBuilder: + if self._content_encryption_algorithm is not None: + raise ValueError("Content encryption algo may only be set once") + if content_encryption_algorithm not in { + algorithms.AES128, + algorithms.AES256, + }: + raise TypeError("Only AES128 and AES256 are supported") + + return PKCS7EnvelopeBuilder( + _data=self._data, + _recipients=self._recipients, + _content_encryption_algorithm=content_encryption_algorithm, + ) + + def encrypt( + self, + encoding: serialization.Encoding, + options: Iterable[PKCS7Options], + ) -> bytes: + if len(self._recipients) == 0: + raise ValueError("Must have at least one recipient") + if self._data is None: + raise ValueError("You must add data to encrypt") + + # The default content encryption algorithm is AES-128-CBC, which the + # S/MIME v3.2 RFC specifies as MUST support (https://datatracker.ietf.org/doc/html/rfc5751#section-2.7) + # however rest of S/MIME v3.2 is not currently supported + content_encryption_algorithm = ( + self._content_encryption_algorithm or algorithms.AES128 + ) + + options = list(options) + if not all(isinstance(x, PKCS7Options) for x in options): + raise ValueError("options must be from the PKCS7Options enum") + if encoding not in ( + serialization.Encoding.PEM, + serialization.Encoding.DER, + serialization.Encoding.SMIME, + ): + raise ValueError( + "Must be PEM, DER, or SMIME from the Encoding enum" + ) + + # Only allow options that make sense for encryption + if any( + opt not in [PKCS7Options.Text, PKCS7Options.Binary] + for opt in options + ): + raise ValueError( + "Only the following options are supported for encryption: " + "Text, Binary" + ) + elif PKCS7Options.Text in options and PKCS7Options.Binary in options: + # OpenSSL accepts both options at the same time, but ignores Text. + # We fail defensively to avoid unexpected outputs. + raise ValueError( + "Cannot use Binary and Text options at the same time" + ) + + return rust_pkcs7.encrypt_and_serialize( + self, content_encryption_algorithm, encoding, options + ) + + +pkcs7_decrypt_der = rust_pkcs7.decrypt_der +pkcs7_decrypt_pem = rust_pkcs7.decrypt_pem +pkcs7_decrypt_smime = rust_pkcs7.decrypt_smime + + +def _smime_signed_encode( + data: bytes, signature: bytes, micalg: str, text_mode: bool +) -> bytes: + # This function works pretty hard to replicate what OpenSSL does + # precisely. For good and for ill. + + m = email.message.Message() + m.add_header("MIME-Version", "1.0") + m.add_header( + "Content-Type", + "multipart/signed", + protocol="application/x-pkcs7-signature", + micalg=micalg, + ) + + m.preamble = "This is an S/MIME signed message\n" + + msg_part = OpenSSLMimePart() + msg_part.set_payload(data) + if text_mode: + msg_part.add_header("Content-Type", "text/plain") + m.attach(msg_part) + + sig_part = email.message.MIMEPart() + sig_part.add_header( + "Content-Type", "application/x-pkcs7-signature", name="smime.p7s" + ) + sig_part.add_header("Content-Transfer-Encoding", "base64") + sig_part.add_header( + "Content-Disposition", "attachment", filename="smime.p7s" + ) + sig_part.set_payload( + email.base64mime.body_encode(signature, maxlinelen=65) + ) + del sig_part["MIME-Version"] + m.attach(sig_part) + + fp = io.BytesIO() + g = email.generator.BytesGenerator( + fp, + maxheaderlen=0, + mangle_from_=False, + policy=m.policy.clone(linesep="\r\n"), + ) + g.flatten(m) + return fp.getvalue() + + +def _smime_enveloped_encode(data: bytes) -> bytes: + m = email.message.Message() + m.add_header("MIME-Version", "1.0") + m.add_header("Content-Disposition", "attachment", filename="smime.p7m") + m.add_header( + "Content-Type", + "application/pkcs7-mime", + smime_type="enveloped-data", + name="smime.p7m", + ) + m.add_header("Content-Transfer-Encoding", "base64") + + m.set_payload(email.base64mime.body_encode(data, maxlinelen=65)) + + return m.as_bytes(policy=m.policy.clone(linesep="\n", max_line_length=0)) + + +def _smime_enveloped_decode(data: bytes) -> bytes: + m = email.message_from_bytes(data) + if m.get_content_type() not in { + "application/x-pkcs7-mime", + "application/pkcs7-mime", + }: + raise ValueError("Not an S/MIME enveloped message") + return bytes(m.get_payload(decode=True)) + + +def _smime_remove_text_headers(data: bytes) -> bytes: + m = email.message_from_bytes(data) + # Using get() instead of get_content_type() since it has None as default, + # where the latter has "text/plain". Both methods are case-insensitive. + content_type = m.get("content-type") + if content_type is None: + raise ValueError( + "Decrypted MIME data has no 'Content-Type' header. " + "Please remove the 'Text' option to parse it manually." + ) + if "text/plain" not in content_type: + raise ValueError( + f"Decrypted MIME data content type is '{content_type}', not " + "'text/plain'. Remove the 'Text' option to parse it manually." + ) + return bytes(m.get_payload(decode=True)) + + +class OpenSSLMimePart(email.message.MIMEPart): + # A MIMEPart subclass that replicates OpenSSL's behavior of not including + # a newline if there are no headers. + def _write_headers(self, generator) -> None: + if list(self.raw_items()): + generator._write_headers(self) diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/serialization/ssh.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/serialization/ssh.py new file mode 100644 index 0000000..230e147 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/serialization/ssh.py @@ -0,0 +1,1615 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import binascii +import enum +import os +import re +import typing +import warnings +from dataclasses import dataclass + +from cryptography import utils +from cryptography.exceptions import UnsupportedAlgorithm +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ( + dsa, + ec, + ed25519, + padding, + rsa, +) +from cryptography.hazmat.primitives.asymmetric import utils as asym_utils +from cryptography.hazmat.primitives.ciphers import ( + AEADDecryptionContext, + Cipher, + algorithms, + modes, +) +from cryptography.hazmat.primitives.serialization import ( + Encoding, + KeySerializationEncryption, + NoEncryption, + PrivateFormat, + PublicFormat, + _KeySerializationEncryption, +) + +try: + from bcrypt import kdf as _bcrypt_kdf + + _bcrypt_supported = True +except ImportError: + _bcrypt_supported = False + + def _bcrypt_kdf( + password: bytes, + salt: bytes, + desired_key_bytes: int, + rounds: int, + ignore_few_rounds: bool = False, + ) -> bytes: + raise UnsupportedAlgorithm("Need bcrypt module") + + +_SSH_ED25519 = b"ssh-ed25519" +_SSH_RSA = b"ssh-rsa" +_SSH_DSA = b"ssh-dss" +_ECDSA_NISTP256 = b"ecdsa-sha2-nistp256" +_ECDSA_NISTP384 = b"ecdsa-sha2-nistp384" +_ECDSA_NISTP521 = b"ecdsa-sha2-nistp521" +_CERT_SUFFIX = b"-cert-v01@openssh.com" + +# U2F application string suffixed pubkey +_SK_SSH_ED25519 = b"sk-ssh-ed25519@openssh.com" +_SK_SSH_ECDSA_NISTP256 = b"sk-ecdsa-sha2-nistp256@openssh.com" + +# These are not key types, only algorithms, so they cannot appear +# as a public key type +_SSH_RSA_SHA256 = b"rsa-sha2-256" +_SSH_RSA_SHA512 = b"rsa-sha2-512" + +_SSH_PUBKEY_RC = re.compile(rb"\A(\S+)[ \t]+(\S+)") +_SK_MAGIC = b"openssh-key-v1\0" +_SK_START = b"-----BEGIN OPENSSH PRIVATE KEY-----" +_SK_END = b"-----END OPENSSH PRIVATE KEY-----" +_BCRYPT = b"bcrypt" +_NONE = b"none" +_DEFAULT_CIPHER = b"aes256-ctr" +_DEFAULT_ROUNDS = 16 + +# re is only way to work on bytes-like data +_PEM_RC = re.compile(_SK_START + b"(.*?)" + _SK_END, re.DOTALL) + +# padding for max blocksize +_PADDING = memoryview(bytearray(range(1, 1 + 16))) + + +@dataclass +class _SSHCipher: + alg: type[algorithms.AES] + key_len: int + mode: type[modes.CTR] | type[modes.CBC] | type[modes.GCM] + block_len: int + iv_len: int + tag_len: int | None + is_aead: bool + + +# ciphers that are actually used in key wrapping +_SSH_CIPHERS: dict[bytes, _SSHCipher] = { + b"aes256-ctr": _SSHCipher( + alg=algorithms.AES, + key_len=32, + mode=modes.CTR, + block_len=16, + iv_len=16, + tag_len=None, + is_aead=False, + ), + b"aes256-cbc": _SSHCipher( + alg=algorithms.AES, + key_len=32, + mode=modes.CBC, + block_len=16, + iv_len=16, + tag_len=None, + is_aead=False, + ), + b"aes256-gcm@openssh.com": _SSHCipher( + alg=algorithms.AES, + key_len=32, + mode=modes.GCM, + block_len=16, + iv_len=12, + tag_len=16, + is_aead=True, + ), +} + +# map local curve name to key type +_ECDSA_KEY_TYPE = { + "secp256r1": _ECDSA_NISTP256, + "secp384r1": _ECDSA_NISTP384, + "secp521r1": _ECDSA_NISTP521, +} + + +def _get_ssh_key_type(key: SSHPrivateKeyTypes | SSHPublicKeyTypes) -> bytes: + if isinstance(key, ec.EllipticCurvePrivateKey): + key_type = _ecdsa_key_type(key.public_key()) + elif isinstance(key, ec.EllipticCurvePublicKey): + key_type = _ecdsa_key_type(key) + elif isinstance(key, (rsa.RSAPrivateKey, rsa.RSAPublicKey)): + key_type = _SSH_RSA + elif isinstance(key, (dsa.DSAPrivateKey, dsa.DSAPublicKey)): + key_type = _SSH_DSA + elif isinstance( + key, (ed25519.Ed25519PrivateKey, ed25519.Ed25519PublicKey) + ): + key_type = _SSH_ED25519 + else: + raise ValueError("Unsupported key type") + + return key_type + + +def _ecdsa_key_type(public_key: ec.EllipticCurvePublicKey) -> bytes: + """Return SSH key_type and curve_name for private key.""" + curve = public_key.curve + if curve.name not in _ECDSA_KEY_TYPE: + raise ValueError( + f"Unsupported curve for ssh private key: {curve.name!r}" + ) + return _ECDSA_KEY_TYPE[curve.name] + + +def _check_block_size(data: utils.Buffer, block_len: int) -> None: + """Require data to be full blocks""" + if not data or len(data) % block_len != 0: + raise ValueError("Corrupt data: missing padding") + + +def _check_empty(data: utils.Buffer) -> None: + """All data should have been parsed.""" + if data: + raise ValueError("Corrupt data: unparsed data") + + +def _init_cipher( + ciphername: bytes, + password: bytes | None, + salt: bytes, + rounds: int, +) -> Cipher[modes.CBC | modes.CTR | modes.GCM]: + """Generate key + iv and return cipher.""" + if not password: + raise TypeError( + "Key is password-protected, but password was not provided." + ) + + ciph = _SSH_CIPHERS[ciphername] + seed = _bcrypt_kdf( + password, salt, ciph.key_len + ciph.iv_len, rounds, True + ) + return Cipher( + ciph.alg(seed[: ciph.key_len]), + ciph.mode(seed[ciph.key_len :]), + ) + + +def _get_u32(data: memoryview) -> tuple[int, memoryview]: + """Uint32""" + if len(data) < 4: + raise ValueError("Invalid data") + return int.from_bytes(data[:4], byteorder="big"), data[4:] + + +def _get_u64(data: memoryview) -> tuple[int, memoryview]: + """Uint64""" + if len(data) < 8: + raise ValueError("Invalid data") + return int.from_bytes(data[:8], byteorder="big"), data[8:] + + +def _get_sshstr(data: memoryview) -> tuple[memoryview, memoryview]: + """Bytes with u32 length prefix""" + n, data = _get_u32(data) + if n > len(data): + raise ValueError("Invalid data") + return data[:n], data[n:] + + +def _get_mpint(data: memoryview) -> tuple[int, memoryview]: + """Big integer.""" + val, data = _get_sshstr(data) + if val and val[0] > 0x7F: + raise ValueError("Invalid data") + return int.from_bytes(val, "big"), data + + +def _to_mpint(val: int) -> bytes: + """Storage format for signed bigint.""" + if val < 0: + raise ValueError("negative mpint not allowed") + if not val: + return b"" + nbytes = (val.bit_length() + 8) // 8 + return utils.int_to_bytes(val, nbytes) + + +class _FragList: + """Build recursive structure without data copy.""" + + flist: list[utils.Buffer] + + def __init__(self, init: list[utils.Buffer] | None = None) -> None: + self.flist = [] + if init: + self.flist.extend(init) + + def put_raw(self, val: utils.Buffer) -> None: + """Add plain bytes""" + self.flist.append(val) + + def put_u32(self, val: int) -> None: + """Big-endian uint32""" + self.flist.append(val.to_bytes(length=4, byteorder="big")) + + def put_u64(self, val: int) -> None: + """Big-endian uint64""" + self.flist.append(val.to_bytes(length=8, byteorder="big")) + + def put_sshstr(self, val: bytes | _FragList) -> None: + """Bytes prefixed with u32 length""" + if isinstance(val, (bytes, memoryview, bytearray)): + self.put_u32(len(val)) + self.flist.append(val) + else: + self.put_u32(val.size()) + self.flist.extend(val.flist) + + def put_mpint(self, val: int) -> None: + """Big-endian bigint prefixed with u32 length""" + self.put_sshstr(_to_mpint(val)) + + def size(self) -> int: + """Current number of bytes""" + return sum(map(len, self.flist)) + + def render(self, dstbuf: memoryview, pos: int = 0) -> int: + """Write into bytearray""" + for frag in self.flist: + flen = len(frag) + start, pos = pos, pos + flen + dstbuf[start:pos] = frag + return pos + + def tobytes(self) -> bytes: + """Return as bytes""" + buf = memoryview(bytearray(self.size())) + self.render(buf) + return buf.tobytes() + + +class _SSHFormatRSA: + """Format for RSA keys. + + Public: + mpint e, n + Private: + mpint n, e, d, iqmp, p, q + """ + + def get_public( + self, data: memoryview + ) -> tuple[tuple[int, int], memoryview]: + """RSA public fields""" + e, data = _get_mpint(data) + n, data = _get_mpint(data) + return (e, n), data + + def load_public( + self, data: memoryview + ) -> tuple[rsa.RSAPublicKey, memoryview]: + """Make RSA public key from data.""" + (e, n), data = self.get_public(data) + public_numbers = rsa.RSAPublicNumbers(e, n) + public_key = public_numbers.public_key() + return public_key, data + + def load_private( + self, data: memoryview, pubfields, unsafe_skip_rsa_key_validation: bool + ) -> tuple[rsa.RSAPrivateKey, memoryview]: + """Make RSA private key from data.""" + n, data = _get_mpint(data) + e, data = _get_mpint(data) + d, data = _get_mpint(data) + iqmp, data = _get_mpint(data) + p, data = _get_mpint(data) + q, data = _get_mpint(data) + + if (e, n) != pubfields: + raise ValueError("Corrupt data: rsa field mismatch") + dmp1 = rsa.rsa_crt_dmp1(d, p) + dmq1 = rsa.rsa_crt_dmq1(d, q) + public_numbers = rsa.RSAPublicNumbers(e, n) + private_numbers = rsa.RSAPrivateNumbers( + p, q, d, dmp1, dmq1, iqmp, public_numbers + ) + private_key = private_numbers.private_key( + unsafe_skip_rsa_key_validation=unsafe_skip_rsa_key_validation + ) + return private_key, data + + def encode_public( + self, public_key: rsa.RSAPublicKey, f_pub: _FragList + ) -> None: + """Write RSA public key""" + pubn = public_key.public_numbers() + f_pub.put_mpint(pubn.e) + f_pub.put_mpint(pubn.n) + + def encode_private( + self, private_key: rsa.RSAPrivateKey, f_priv: _FragList + ) -> None: + """Write RSA private key""" + private_numbers = private_key.private_numbers() + public_numbers = private_numbers.public_numbers + + f_priv.put_mpint(public_numbers.n) + f_priv.put_mpint(public_numbers.e) + + f_priv.put_mpint(private_numbers.d) + f_priv.put_mpint(private_numbers.iqmp) + f_priv.put_mpint(private_numbers.p) + f_priv.put_mpint(private_numbers.q) + + +class _SSHFormatDSA: + """Format for DSA keys. + + Public: + mpint p, q, g, y + Private: + mpint p, q, g, y, x + """ + + def get_public(self, data: memoryview) -> tuple[tuple, memoryview]: + """DSA public fields""" + p, data = _get_mpint(data) + q, data = _get_mpint(data) + g, data = _get_mpint(data) + y, data = _get_mpint(data) + return (p, q, g, y), data + + def load_public( + self, data: memoryview + ) -> tuple[dsa.DSAPublicKey, memoryview]: + """Make DSA public key from data.""" + (p, q, g, y), data = self.get_public(data) + parameter_numbers = dsa.DSAParameterNumbers(p, q, g) + public_numbers = dsa.DSAPublicNumbers(y, parameter_numbers) + self._validate(public_numbers) + public_key = public_numbers.public_key() + return public_key, data + + def load_private( + self, data: memoryview, pubfields, unsafe_skip_rsa_key_validation: bool + ) -> tuple[dsa.DSAPrivateKey, memoryview]: + """Make DSA private key from data.""" + (p, q, g, y), data = self.get_public(data) + x, data = _get_mpint(data) + + if (p, q, g, y) != pubfields: + raise ValueError("Corrupt data: dsa field mismatch") + parameter_numbers = dsa.DSAParameterNumbers(p, q, g) + public_numbers = dsa.DSAPublicNumbers(y, parameter_numbers) + self._validate(public_numbers) + private_numbers = dsa.DSAPrivateNumbers(x, public_numbers) + private_key = private_numbers.private_key() + return private_key, data + + def encode_public( + self, public_key: dsa.DSAPublicKey, f_pub: _FragList + ) -> None: + """Write DSA public key""" + public_numbers = public_key.public_numbers() + parameter_numbers = public_numbers.parameter_numbers + self._validate(public_numbers) + + f_pub.put_mpint(parameter_numbers.p) + f_pub.put_mpint(parameter_numbers.q) + f_pub.put_mpint(parameter_numbers.g) + f_pub.put_mpint(public_numbers.y) + + def encode_private( + self, private_key: dsa.DSAPrivateKey, f_priv: _FragList + ) -> None: + """Write DSA private key""" + self.encode_public(private_key.public_key(), f_priv) + f_priv.put_mpint(private_key.private_numbers().x) + + def _validate(self, public_numbers: dsa.DSAPublicNumbers) -> None: + parameter_numbers = public_numbers.parameter_numbers + if parameter_numbers.p.bit_length() != 1024: + raise ValueError("SSH supports only 1024 bit DSA keys") + + +class _SSHFormatECDSA: + """Format for ECDSA keys. + + Public: + str curve + bytes point + Private: + str curve + bytes point + mpint secret + """ + + def __init__(self, ssh_curve_name: bytes, curve: ec.EllipticCurve): + self.ssh_curve_name = ssh_curve_name + self.curve = curve + + def get_public( + self, data: memoryview + ) -> tuple[tuple[memoryview, memoryview], memoryview]: + """ECDSA public fields""" + curve, data = _get_sshstr(data) + point, data = _get_sshstr(data) + if curve != self.ssh_curve_name: + raise ValueError("Curve name mismatch") + if len(point) == 0: + raise ValueError("Invalid EC point: empty data") + if point[0] != 4: + raise NotImplementedError("Need uncompressed point") + return (curve, point), data + + def load_public( + self, data: memoryview + ) -> tuple[ec.EllipticCurvePublicKey, memoryview]: + """Make ECDSA public key from data.""" + (_, point), data = self.get_public(data) + public_key = ec.EllipticCurvePublicKey.from_encoded_point( + self.curve, point.tobytes() + ) + return public_key, data + + def load_private( + self, data: memoryview, pubfields, unsafe_skip_rsa_key_validation: bool + ) -> tuple[ec.EllipticCurvePrivateKey, memoryview]: + """Make ECDSA private key from data.""" + (curve_name, point), data = self.get_public(data) + secret, data = _get_mpint(data) + + if (curve_name, point) != pubfields: + raise ValueError("Corrupt data: ecdsa field mismatch") + private_key = ec.derive_private_key(secret, self.curve) + return private_key, data + + def encode_public( + self, public_key: ec.EllipticCurvePublicKey, f_pub: _FragList + ) -> None: + """Write ECDSA public key""" + point = public_key.public_bytes( + Encoding.X962, PublicFormat.UncompressedPoint + ) + f_pub.put_sshstr(self.ssh_curve_name) + f_pub.put_sshstr(point) + + def encode_private( + self, private_key: ec.EllipticCurvePrivateKey, f_priv: _FragList + ) -> None: + """Write ECDSA private key""" + public_key = private_key.public_key() + private_numbers = private_key.private_numbers() + + self.encode_public(public_key, f_priv) + f_priv.put_mpint(private_numbers.private_value) + + +class _SSHFormatEd25519: + """Format for Ed25519 keys. + + Public: + bytes point + Private: + bytes point + bytes secret_and_point + """ + + def get_public( + self, data: memoryview + ) -> tuple[tuple[memoryview], memoryview]: + """Ed25519 public fields""" + point, data = _get_sshstr(data) + return (point,), data + + def load_public( + self, data: memoryview + ) -> tuple[ed25519.Ed25519PublicKey, memoryview]: + """Make Ed25519 public key from data.""" + (point,), data = self.get_public(data) + public_key = ed25519.Ed25519PublicKey.from_public_bytes( + point.tobytes() + ) + return public_key, data + + def load_private( + self, data: memoryview, pubfields, unsafe_skip_rsa_key_validation: bool + ) -> tuple[ed25519.Ed25519PrivateKey, memoryview]: + """Make Ed25519 private key from data.""" + (point,), data = self.get_public(data) + keypair, data = _get_sshstr(data) + + secret = keypair[:32] + point2 = keypair[32:] + if point != point2 or (point,) != pubfields: + raise ValueError("Corrupt data: ed25519 field mismatch") + private_key = ed25519.Ed25519PrivateKey.from_private_bytes(secret) + return private_key, data + + def encode_public( + self, public_key: ed25519.Ed25519PublicKey, f_pub: _FragList + ) -> None: + """Write Ed25519 public key""" + raw_public_key = public_key.public_bytes( + Encoding.Raw, PublicFormat.Raw + ) + f_pub.put_sshstr(raw_public_key) + + def encode_private( + self, private_key: ed25519.Ed25519PrivateKey, f_priv: _FragList + ) -> None: + """Write Ed25519 private key""" + public_key = private_key.public_key() + raw_private_key = private_key.private_bytes( + Encoding.Raw, PrivateFormat.Raw, NoEncryption() + ) + raw_public_key = public_key.public_bytes( + Encoding.Raw, PublicFormat.Raw + ) + f_keypair = _FragList([raw_private_key, raw_public_key]) + + self.encode_public(public_key, f_priv) + f_priv.put_sshstr(f_keypair) + + +def load_application(data) -> tuple[memoryview, memoryview]: + """ + U2F application strings + """ + application, data = _get_sshstr(data) + if not application.tobytes().startswith(b"ssh:"): + raise ValueError( + "U2F application string does not start with b'ssh:' " + f"({application})" + ) + return application, data + + +class _SSHFormatSKEd25519: + """ + The format of a sk-ssh-ed25519@openssh.com public key is: + + string "sk-ssh-ed25519@openssh.com" + string public key + string application (user-specified, but typically "ssh:") + """ + + def load_public( + self, data: memoryview + ) -> tuple[ed25519.Ed25519PublicKey, memoryview]: + """Make Ed25519 public key from data.""" + public_key, data = _lookup_kformat(_SSH_ED25519).load_public(data) + _, data = load_application(data) + return public_key, data + + def get_public(self, data: memoryview) -> typing.NoReturn: + # Confusingly `get_public` is an entry point used by private key + # loading. + raise UnsupportedAlgorithm( + "sk-ssh-ed25519 private keys cannot be loaded" + ) + + +class _SSHFormatSKECDSA: + """ + The format of a sk-ecdsa-sha2-nistp256@openssh.com public key is: + + string "sk-ecdsa-sha2-nistp256@openssh.com" + string curve name + ec_point Q + string application (user-specified, but typically "ssh:") + """ + + def load_public( + self, data: memoryview + ) -> tuple[ec.EllipticCurvePublicKey, memoryview]: + """Make ECDSA public key from data.""" + public_key, data = _lookup_kformat(_ECDSA_NISTP256).load_public(data) + _, data = load_application(data) + return public_key, data + + def get_public(self, data: memoryview) -> typing.NoReturn: + # Confusingly `get_public` is an entry point used by private key + # loading. + raise UnsupportedAlgorithm( + "sk-ecdsa-sha2-nistp256 private keys cannot be loaded" + ) + + +_KEY_FORMATS = { + _SSH_RSA: _SSHFormatRSA(), + _SSH_DSA: _SSHFormatDSA(), + _SSH_ED25519: _SSHFormatEd25519(), + _ECDSA_NISTP256: _SSHFormatECDSA(b"nistp256", ec.SECP256R1()), + _ECDSA_NISTP384: _SSHFormatECDSA(b"nistp384", ec.SECP384R1()), + _ECDSA_NISTP521: _SSHFormatECDSA(b"nistp521", ec.SECP521R1()), + _SK_SSH_ED25519: _SSHFormatSKEd25519(), + _SK_SSH_ECDSA_NISTP256: _SSHFormatSKECDSA(), +} + + +def _lookup_kformat(key_type: utils.Buffer): + """Return valid format or throw error""" + if not isinstance(key_type, bytes): + key_type = memoryview(key_type).tobytes() + if key_type in _KEY_FORMATS: + return _KEY_FORMATS[key_type] + raise UnsupportedAlgorithm(f"Unsupported key type: {key_type!r}") + + +SSHPrivateKeyTypes = typing.Union[ + ec.EllipticCurvePrivateKey, + rsa.RSAPrivateKey, + dsa.DSAPrivateKey, + ed25519.Ed25519PrivateKey, +] + + +def load_ssh_private_key( + data: utils.Buffer, + password: bytes | None, + backend: typing.Any = None, + *, + unsafe_skip_rsa_key_validation: bool = False, +) -> SSHPrivateKeyTypes: + """Load private key from OpenSSH custom encoding.""" + utils._check_byteslike("data", data) + if password is not None: + utils._check_bytes("password", password) + + m = _PEM_RC.search(data) + if not m: + raise ValueError("Not OpenSSH private key format") + p1 = m.start(1) + p2 = m.end(1) + data = binascii.a2b_base64(memoryview(data)[p1:p2]) + if not data.startswith(_SK_MAGIC): + raise ValueError("Not OpenSSH private key format") + data = memoryview(data)[len(_SK_MAGIC) :] + + # parse header + ciphername, data = _get_sshstr(data) + kdfname, data = _get_sshstr(data) + kdfoptions, data = _get_sshstr(data) + nkeys, data = _get_u32(data) + if nkeys != 1: + raise ValueError("Only one key supported") + + # load public key data + pubdata, data = _get_sshstr(data) + pub_key_type, pubdata = _get_sshstr(pubdata) + kformat = _lookup_kformat(pub_key_type) + pubfields, pubdata = kformat.get_public(pubdata) + _check_empty(pubdata) + + if ciphername != _NONE or kdfname != _NONE: + ciphername_bytes = ciphername.tobytes() + if ciphername_bytes not in _SSH_CIPHERS: + raise UnsupportedAlgorithm( + f"Unsupported cipher: {ciphername_bytes!r}" + ) + if kdfname != _BCRYPT: + raise UnsupportedAlgorithm(f"Unsupported KDF: {kdfname!r}") + blklen = _SSH_CIPHERS[ciphername_bytes].block_len + tag_len = _SSH_CIPHERS[ciphername_bytes].tag_len + # load secret data + edata, data = _get_sshstr(data) + # see https://bugzilla.mindrot.org/show_bug.cgi?id=3553 for + # information about how OpenSSH handles AEAD tags + if _SSH_CIPHERS[ciphername_bytes].is_aead: + tag = bytes(data) + if len(tag) != tag_len: + raise ValueError("Corrupt data: invalid tag length for cipher") + else: + _check_empty(data) + _check_block_size(edata, blklen) + salt, kbuf = _get_sshstr(kdfoptions) + rounds, kbuf = _get_u32(kbuf) + _check_empty(kbuf) + ciph = _init_cipher(ciphername_bytes, password, salt.tobytes(), rounds) + dec = ciph.decryptor() + edata = memoryview(dec.update(edata)) + if _SSH_CIPHERS[ciphername_bytes].is_aead: + assert isinstance(dec, AEADDecryptionContext) + _check_empty(dec.finalize_with_tag(tag)) + else: + # _check_block_size requires data to be a full block so there + # should be no output from finalize + _check_empty(dec.finalize()) + else: + if password: + raise TypeError( + "Password was given but private key is not encrypted." + ) + # load secret data + edata, data = _get_sshstr(data) + _check_empty(data) + blklen = 8 + _check_block_size(edata, blklen) + ck1, edata = _get_u32(edata) + ck2, edata = _get_u32(edata) + if ck1 != ck2: + raise ValueError("Corrupt data: broken checksum") + + # load per-key struct + key_type, edata = _get_sshstr(edata) + if key_type != pub_key_type: + raise ValueError("Corrupt data: key type mismatch") + private_key, edata = kformat.load_private( + edata, + pubfields, + unsafe_skip_rsa_key_validation=unsafe_skip_rsa_key_validation, + ) + # We don't use the comment + _, edata = _get_sshstr(edata) + + # yes, SSH does padding check *after* all other parsing is done. + # need to follow as it writes zero-byte padding too. + if edata != _PADDING[: len(edata)]: + raise ValueError("Corrupt data: invalid padding") + + if isinstance(private_key, dsa.DSAPrivateKey): + warnings.warn( + "SSH DSA keys are deprecated and will be removed in a future " + "release.", + utils.DeprecatedIn40, + stacklevel=2, + ) + + return private_key + + +def _serialize_ssh_private_key( + private_key: SSHPrivateKeyTypes, + password: bytes, + encryption_algorithm: KeySerializationEncryption, +) -> bytes: + """Serialize private key with OpenSSH custom encoding.""" + utils._check_bytes("password", password) + if isinstance(private_key, dsa.DSAPrivateKey): + warnings.warn( + "SSH DSA key support is deprecated and will be " + "removed in a future release", + utils.DeprecatedIn40, + stacklevel=4, + ) + + key_type = _get_ssh_key_type(private_key) + kformat = _lookup_kformat(key_type) + + # setup parameters + f_kdfoptions = _FragList() + if password: + ciphername = _DEFAULT_CIPHER + blklen = _SSH_CIPHERS[ciphername].block_len + kdfname = _BCRYPT + rounds = _DEFAULT_ROUNDS + if ( + isinstance(encryption_algorithm, _KeySerializationEncryption) + and encryption_algorithm._kdf_rounds is not None + ): + rounds = encryption_algorithm._kdf_rounds + salt = os.urandom(16) + f_kdfoptions.put_sshstr(salt) + f_kdfoptions.put_u32(rounds) + ciph = _init_cipher(ciphername, password, salt, rounds) + else: + ciphername = kdfname = _NONE + blklen = 8 + ciph = None + nkeys = 1 + checkval = os.urandom(4) + comment = b"" + + # encode public and private parts together + f_public_key = _FragList() + f_public_key.put_sshstr(key_type) + kformat.encode_public(private_key.public_key(), f_public_key) + + f_secrets = _FragList([checkval, checkval]) + f_secrets.put_sshstr(key_type) + kformat.encode_private(private_key, f_secrets) + f_secrets.put_sshstr(comment) + f_secrets.put_raw(_PADDING[: blklen - (f_secrets.size() % blklen)]) + + # top-level structure + f_main = _FragList() + f_main.put_raw(_SK_MAGIC) + f_main.put_sshstr(ciphername) + f_main.put_sshstr(kdfname) + f_main.put_sshstr(f_kdfoptions) + f_main.put_u32(nkeys) + f_main.put_sshstr(f_public_key) + f_main.put_sshstr(f_secrets) + + # copy result info bytearray + slen = f_secrets.size() + mlen = f_main.size() + buf = memoryview(bytearray(mlen + blklen)) + f_main.render(buf) + ofs = mlen - slen + + # encrypt in-place + if ciph is not None: + ciph.encryptor().update_into(buf[ofs:mlen], buf[ofs:]) + + return bytes(buf[:mlen]) + + +SSHPublicKeyTypes = typing.Union[ + ec.EllipticCurvePublicKey, + rsa.RSAPublicKey, + dsa.DSAPublicKey, + ed25519.Ed25519PublicKey, +] + +SSHCertPublicKeyTypes = typing.Union[ + ec.EllipticCurvePublicKey, + rsa.RSAPublicKey, + ed25519.Ed25519PublicKey, +] + + +class SSHCertificateType(enum.Enum): + USER = 1 + HOST = 2 + + +class SSHCertificate: + def __init__( + self, + _nonce: memoryview, + _public_key: SSHPublicKeyTypes, + _serial: int, + _cctype: int, + _key_id: memoryview, + _valid_principals: list[bytes], + _valid_after: int, + _valid_before: int, + _critical_options: dict[bytes, bytes], + _extensions: dict[bytes, bytes], + _sig_type: memoryview, + _sig_key: memoryview, + _inner_sig_type: memoryview, + _signature: memoryview, + _tbs_cert_body: memoryview, + _cert_key_type: bytes, + _cert_body: memoryview, + ): + self._nonce = _nonce + self._public_key = _public_key + self._serial = _serial + try: + self._type = SSHCertificateType(_cctype) + except ValueError: + raise ValueError("Invalid certificate type") + self._key_id = _key_id + self._valid_principals = _valid_principals + self._valid_after = _valid_after + self._valid_before = _valid_before + self._critical_options = _critical_options + self._extensions = _extensions + self._sig_type = _sig_type + self._sig_key = _sig_key + self._inner_sig_type = _inner_sig_type + self._signature = _signature + self._cert_key_type = _cert_key_type + self._cert_body = _cert_body + self._tbs_cert_body = _tbs_cert_body + + @property + def nonce(self) -> bytes: + return bytes(self._nonce) + + def public_key(self) -> SSHCertPublicKeyTypes: + # make mypy happy until we remove DSA support entirely and + # the underlying union won't have a disallowed type + return typing.cast(SSHCertPublicKeyTypes, self._public_key) + + @property + def serial(self) -> int: + return self._serial + + @property + def type(self) -> SSHCertificateType: + return self._type + + @property + def key_id(self) -> bytes: + return bytes(self._key_id) + + @property + def valid_principals(self) -> list[bytes]: + return self._valid_principals + + @property + def valid_before(self) -> int: + return self._valid_before + + @property + def valid_after(self) -> int: + return self._valid_after + + @property + def critical_options(self) -> dict[bytes, bytes]: + return self._critical_options + + @property + def extensions(self) -> dict[bytes, bytes]: + return self._extensions + + def signature_key(self) -> SSHCertPublicKeyTypes: + sigformat = _lookup_kformat(self._sig_type) + signature_key, sigkey_rest = sigformat.load_public(self._sig_key) + _check_empty(sigkey_rest) + return signature_key + + def public_bytes(self) -> bytes: + return ( + bytes(self._cert_key_type) + + b" " + + binascii.b2a_base64(bytes(self._cert_body), newline=False) + ) + + def verify_cert_signature(self) -> None: + signature_key = self.signature_key() + if isinstance(signature_key, ed25519.Ed25519PublicKey): + signature_key.verify( + bytes(self._signature), bytes(self._tbs_cert_body) + ) + elif isinstance(signature_key, ec.EllipticCurvePublicKey): + # The signature is encoded as a pair of big-endian integers + r, data = _get_mpint(self._signature) + s, data = _get_mpint(data) + _check_empty(data) + computed_sig = asym_utils.encode_dss_signature(r, s) + hash_alg = _get_ec_hash_alg(signature_key.curve) + signature_key.verify( + computed_sig, bytes(self._tbs_cert_body), ec.ECDSA(hash_alg) + ) + else: + assert isinstance(signature_key, rsa.RSAPublicKey) + if self._inner_sig_type == _SSH_RSA: + hash_alg = hashes.SHA1() + elif self._inner_sig_type == _SSH_RSA_SHA256: + hash_alg = hashes.SHA256() + else: + assert self._inner_sig_type == _SSH_RSA_SHA512 + hash_alg = hashes.SHA512() + signature_key.verify( + bytes(self._signature), + bytes(self._tbs_cert_body), + padding.PKCS1v15(), + hash_alg, + ) + + +def _get_ec_hash_alg(curve: ec.EllipticCurve) -> hashes.HashAlgorithm: + if isinstance(curve, ec.SECP256R1): + return hashes.SHA256() + elif isinstance(curve, ec.SECP384R1): + return hashes.SHA384() + else: + assert isinstance(curve, ec.SECP521R1) + return hashes.SHA512() + + +def _load_ssh_public_identity( + data: utils.Buffer, + _legacy_dsa_allowed=False, +) -> SSHCertificate | SSHPublicKeyTypes: + utils._check_byteslike("data", data) + + m = _SSH_PUBKEY_RC.match(data) + if not m: + raise ValueError("Invalid line format") + key_type = orig_key_type = m.group(1) + key_body = m.group(2) + with_cert = False + if key_type.endswith(_CERT_SUFFIX): + with_cert = True + key_type = key_type[: -len(_CERT_SUFFIX)] + if key_type == _SSH_DSA and not _legacy_dsa_allowed: + raise UnsupportedAlgorithm( + "DSA keys aren't supported in SSH certificates" + ) + kformat = _lookup_kformat(key_type) + + try: + rest = memoryview(binascii.a2b_base64(key_body)) + except (TypeError, binascii.Error): + raise ValueError("Invalid format") + + if with_cert: + cert_body = rest + inner_key_type, rest = _get_sshstr(rest) + if inner_key_type != orig_key_type: + raise ValueError("Invalid key format") + if with_cert: + nonce, rest = _get_sshstr(rest) + public_key, rest = kformat.load_public(rest) + if with_cert: + serial, rest = _get_u64(rest) + cctype, rest = _get_u32(rest) + key_id, rest = _get_sshstr(rest) + principals, rest = _get_sshstr(rest) + valid_principals = [] + while principals: + principal, principals = _get_sshstr(principals) + valid_principals.append(bytes(principal)) + valid_after, rest = _get_u64(rest) + valid_before, rest = _get_u64(rest) + crit_options, rest = _get_sshstr(rest) + critical_options = _parse_exts_opts(crit_options) + exts, rest = _get_sshstr(rest) + extensions = _parse_exts_opts(exts) + # Get the reserved field, which is unused. + _, rest = _get_sshstr(rest) + sig_key_raw, rest = _get_sshstr(rest) + sig_type, sig_key = _get_sshstr(sig_key_raw) + if sig_type == _SSH_DSA and not _legacy_dsa_allowed: + raise UnsupportedAlgorithm( + "DSA signatures aren't supported in SSH certificates" + ) + # Get the entire cert body and subtract the signature + tbs_cert_body = cert_body[: -len(rest)] + signature_raw, rest = _get_sshstr(rest) + _check_empty(rest) + inner_sig_type, sig_rest = _get_sshstr(signature_raw) + # RSA certs can have multiple algorithm types + if ( + sig_type == _SSH_RSA + and inner_sig_type + not in [_SSH_RSA_SHA256, _SSH_RSA_SHA512, _SSH_RSA] + ) or (sig_type != _SSH_RSA and inner_sig_type != sig_type): + raise ValueError("Signature key type does not match") + signature, sig_rest = _get_sshstr(sig_rest) + _check_empty(sig_rest) + return SSHCertificate( + nonce, + public_key, + serial, + cctype, + key_id, + valid_principals, + valid_after, + valid_before, + critical_options, + extensions, + sig_type, + sig_key, + inner_sig_type, + signature, + tbs_cert_body, + orig_key_type, + cert_body, + ) + else: + _check_empty(rest) + return public_key + + +def load_ssh_public_identity( + data: utils.Buffer, +) -> SSHCertificate | SSHPublicKeyTypes: + return _load_ssh_public_identity(data) + + +def _parse_exts_opts(exts_opts: memoryview) -> dict[bytes, bytes]: + result: dict[bytes, bytes] = {} + last_name = None + while exts_opts: + name, exts_opts = _get_sshstr(exts_opts) + bname: bytes = bytes(name) + if bname in result: + raise ValueError("Duplicate name") + if last_name is not None and bname < last_name: + raise ValueError("Fields not lexically sorted") + value, exts_opts = _get_sshstr(exts_opts) + if len(value) > 0: + value, extra = _get_sshstr(value) + if len(extra) > 0: + raise ValueError("Unexpected extra data after value") + result[bname] = bytes(value) + last_name = bname + return result + + +def ssh_key_fingerprint( + key: SSHPublicKeyTypes, + hash_algorithm: hashes.MD5 | hashes.SHA1 | hashes.SHA256, +) -> bytes: + if not isinstance( + hash_algorithm, + (hashes.MD5, hashes.SHA1, hashes.SHA256), + ): + raise TypeError("hash_algorithm must be either MD5, SHA1, or SHA256") + + key_type = _get_ssh_key_type(key) + kformat = _lookup_kformat(key_type) + + f_pub = _FragList() + f_pub.put_sshstr(key_type) + kformat.encode_public(key, f_pub) + + ssh_binary_data = f_pub.tobytes() + + # Hash the binary data + hash_obj = hashes.Hash(hash_algorithm) + hash_obj.update(ssh_binary_data) + return hash_obj.finalize() + + +def load_ssh_public_key( + data: utils.Buffer, backend: typing.Any = None +) -> SSHPublicKeyTypes: + cert_or_key = _load_ssh_public_identity(data, _legacy_dsa_allowed=True) + public_key: SSHPublicKeyTypes + if isinstance(cert_or_key, SSHCertificate): + public_key = cert_or_key.public_key() + else: + public_key = cert_or_key + + if isinstance(public_key, dsa.DSAPublicKey): + warnings.warn( + "SSH DSA keys are deprecated and will be removed in a future " + "release.", + utils.DeprecatedIn40, + stacklevel=2, + ) + return public_key + + +def serialize_ssh_public_key(public_key: SSHPublicKeyTypes) -> bytes: + """One-line public key format for OpenSSH""" + if isinstance(public_key, dsa.DSAPublicKey): + warnings.warn( + "SSH DSA key support is deprecated and will be " + "removed in a future release", + utils.DeprecatedIn40, + stacklevel=4, + ) + key_type = _get_ssh_key_type(public_key) + kformat = _lookup_kformat(key_type) + + f_pub = _FragList() + f_pub.put_sshstr(key_type) + kformat.encode_public(public_key, f_pub) + + pub = binascii.b2a_base64(f_pub.tobytes()).strip() + return b"".join([key_type, b" ", pub]) + + +SSHCertPrivateKeyTypes = typing.Union[ + ec.EllipticCurvePrivateKey, + rsa.RSAPrivateKey, + ed25519.Ed25519PrivateKey, +] + + +# This is an undocumented limit enforced in the openssh codebase for sshd and +# ssh-keygen, but it is undefined in the ssh certificates spec. +_SSHKEY_CERT_MAX_PRINCIPALS = 256 + + +class SSHCertificateBuilder: + def __init__( + self, + _public_key: SSHCertPublicKeyTypes | None = None, + _serial: int | None = None, + _type: SSHCertificateType | None = None, + _key_id: bytes | None = None, + _valid_principals: list[bytes] = [], + _valid_for_all_principals: bool = False, + _valid_before: int | None = None, + _valid_after: int | None = None, + _critical_options: list[tuple[bytes, bytes]] = [], + _extensions: list[tuple[bytes, bytes]] = [], + ): + self._public_key = _public_key + self._serial = _serial + self._type = _type + self._key_id = _key_id + self._valid_principals = _valid_principals + self._valid_for_all_principals = _valid_for_all_principals + self._valid_before = _valid_before + self._valid_after = _valid_after + self._critical_options = _critical_options + self._extensions = _extensions + + def public_key( + self, public_key: SSHCertPublicKeyTypes + ) -> SSHCertificateBuilder: + if not isinstance( + public_key, + ( + ec.EllipticCurvePublicKey, + rsa.RSAPublicKey, + ed25519.Ed25519PublicKey, + ), + ): + raise TypeError("Unsupported key type") + if self._public_key is not None: + raise ValueError("public_key already set") + + return SSHCertificateBuilder( + _public_key=public_key, + _serial=self._serial, + _type=self._type, + _key_id=self._key_id, + _valid_principals=self._valid_principals, + _valid_for_all_principals=self._valid_for_all_principals, + _valid_before=self._valid_before, + _valid_after=self._valid_after, + _critical_options=self._critical_options, + _extensions=self._extensions, + ) + + def serial(self, serial: int) -> SSHCertificateBuilder: + if not isinstance(serial, int): + raise TypeError("serial must be an integer") + if not 0 <= serial < 2**64: + raise ValueError("serial must be between 0 and 2**64") + if self._serial is not None: + raise ValueError("serial already set") + + return SSHCertificateBuilder( + _public_key=self._public_key, + _serial=serial, + _type=self._type, + _key_id=self._key_id, + _valid_principals=self._valid_principals, + _valid_for_all_principals=self._valid_for_all_principals, + _valid_before=self._valid_before, + _valid_after=self._valid_after, + _critical_options=self._critical_options, + _extensions=self._extensions, + ) + + def type(self, type: SSHCertificateType) -> SSHCertificateBuilder: + if not isinstance(type, SSHCertificateType): + raise TypeError("type must be an SSHCertificateType") + if self._type is not None: + raise ValueError("type already set") + + return SSHCertificateBuilder( + _public_key=self._public_key, + _serial=self._serial, + _type=type, + _key_id=self._key_id, + _valid_principals=self._valid_principals, + _valid_for_all_principals=self._valid_for_all_principals, + _valid_before=self._valid_before, + _valid_after=self._valid_after, + _critical_options=self._critical_options, + _extensions=self._extensions, + ) + + def key_id(self, key_id: bytes) -> SSHCertificateBuilder: + if not isinstance(key_id, bytes): + raise TypeError("key_id must be bytes") + if self._key_id is not None: + raise ValueError("key_id already set") + + return SSHCertificateBuilder( + _public_key=self._public_key, + _serial=self._serial, + _type=self._type, + _key_id=key_id, + _valid_principals=self._valid_principals, + _valid_for_all_principals=self._valid_for_all_principals, + _valid_before=self._valid_before, + _valid_after=self._valid_after, + _critical_options=self._critical_options, + _extensions=self._extensions, + ) + + def valid_principals( + self, valid_principals: list[bytes] + ) -> SSHCertificateBuilder: + if self._valid_for_all_principals: + raise ValueError( + "Principals can't be set because the cert is valid " + "for all principals" + ) + if ( + not all(isinstance(x, bytes) for x in valid_principals) + or not valid_principals + ): + raise TypeError( + "principals must be a list of bytes and can't be empty" + ) + if self._valid_principals: + raise ValueError("valid_principals already set") + + if len(valid_principals) > _SSHKEY_CERT_MAX_PRINCIPALS: + raise ValueError( + "Reached or exceeded the maximum number of valid_principals" + ) + + return SSHCertificateBuilder( + _public_key=self._public_key, + _serial=self._serial, + _type=self._type, + _key_id=self._key_id, + _valid_principals=valid_principals, + _valid_for_all_principals=self._valid_for_all_principals, + _valid_before=self._valid_before, + _valid_after=self._valid_after, + _critical_options=self._critical_options, + _extensions=self._extensions, + ) + + def valid_for_all_principals(self): + if self._valid_principals: + raise ValueError( + "valid_principals already set, can't set " + "valid_for_all_principals" + ) + if self._valid_for_all_principals: + raise ValueError("valid_for_all_principals already set") + + return SSHCertificateBuilder( + _public_key=self._public_key, + _serial=self._serial, + _type=self._type, + _key_id=self._key_id, + _valid_principals=self._valid_principals, + _valid_for_all_principals=True, + _valid_before=self._valid_before, + _valid_after=self._valid_after, + _critical_options=self._critical_options, + _extensions=self._extensions, + ) + + def valid_before(self, valid_before: int | float) -> SSHCertificateBuilder: + if not isinstance(valid_before, (int, float)): + raise TypeError("valid_before must be an int or float") + valid_before = int(valid_before) + if valid_before < 0 or valid_before >= 2**64: + raise ValueError("valid_before must [0, 2**64)") + if self._valid_before is not None: + raise ValueError("valid_before already set") + + return SSHCertificateBuilder( + _public_key=self._public_key, + _serial=self._serial, + _type=self._type, + _key_id=self._key_id, + _valid_principals=self._valid_principals, + _valid_for_all_principals=self._valid_for_all_principals, + _valid_before=valid_before, + _valid_after=self._valid_after, + _critical_options=self._critical_options, + _extensions=self._extensions, + ) + + def valid_after(self, valid_after: int | float) -> SSHCertificateBuilder: + if not isinstance(valid_after, (int, float)): + raise TypeError("valid_after must be an int or float") + valid_after = int(valid_after) + if valid_after < 0 or valid_after >= 2**64: + raise ValueError("valid_after must [0, 2**64)") + if self._valid_after is not None: + raise ValueError("valid_after already set") + + return SSHCertificateBuilder( + _public_key=self._public_key, + _serial=self._serial, + _type=self._type, + _key_id=self._key_id, + _valid_principals=self._valid_principals, + _valid_for_all_principals=self._valid_for_all_principals, + _valid_before=self._valid_before, + _valid_after=valid_after, + _critical_options=self._critical_options, + _extensions=self._extensions, + ) + + def add_critical_option( + self, name: bytes, value: bytes + ) -> SSHCertificateBuilder: + if not isinstance(name, bytes) or not isinstance(value, bytes): + raise TypeError("name and value must be bytes") + # This is O(n**2) + if name in [name for name, _ in self._critical_options]: + raise ValueError("Duplicate critical option name") + + return SSHCertificateBuilder( + _public_key=self._public_key, + _serial=self._serial, + _type=self._type, + _key_id=self._key_id, + _valid_principals=self._valid_principals, + _valid_for_all_principals=self._valid_for_all_principals, + _valid_before=self._valid_before, + _valid_after=self._valid_after, + _critical_options=[*self._critical_options, (name, value)], + _extensions=self._extensions, + ) + + def add_extension( + self, name: bytes, value: bytes + ) -> SSHCertificateBuilder: + if not isinstance(name, bytes) or not isinstance(value, bytes): + raise TypeError("name and value must be bytes") + # This is O(n**2) + if name in [name for name, _ in self._extensions]: + raise ValueError("Duplicate extension name") + + return SSHCertificateBuilder( + _public_key=self._public_key, + _serial=self._serial, + _type=self._type, + _key_id=self._key_id, + _valid_principals=self._valid_principals, + _valid_for_all_principals=self._valid_for_all_principals, + _valid_before=self._valid_before, + _valid_after=self._valid_after, + _critical_options=self._critical_options, + _extensions=[*self._extensions, (name, value)], + ) + + def sign(self, private_key: SSHCertPrivateKeyTypes) -> SSHCertificate: + if not isinstance( + private_key, + ( + ec.EllipticCurvePrivateKey, + rsa.RSAPrivateKey, + ed25519.Ed25519PrivateKey, + ), + ): + raise TypeError("Unsupported private key type") + + if self._public_key is None: + raise ValueError("public_key must be set") + + # Not required + serial = 0 if self._serial is None else self._serial + + if self._type is None: + raise ValueError("type must be set") + + # Not required + key_id = b"" if self._key_id is None else self._key_id + + # A zero length list is valid, but means the certificate + # is valid for any principal of the specified type. We require + # the user to explicitly set valid_for_all_principals to get + # that behavior. + if not self._valid_principals and not self._valid_for_all_principals: + raise ValueError( + "valid_principals must be set if valid_for_all_principals " + "is False" + ) + + if self._valid_before is None: + raise ValueError("valid_before must be set") + + if self._valid_after is None: + raise ValueError("valid_after must be set") + + if self._valid_after > self._valid_before: + raise ValueError("valid_after must be earlier than valid_before") + + # lexically sort our byte strings + self._critical_options.sort(key=lambda x: x[0]) + self._extensions.sort(key=lambda x: x[0]) + + key_type = _get_ssh_key_type(self._public_key) + cert_prefix = key_type + _CERT_SUFFIX + + # Marshal the bytes to be signed + nonce = os.urandom(32) + kformat = _lookup_kformat(key_type) + f = _FragList() + f.put_sshstr(cert_prefix) + f.put_sshstr(nonce) + kformat.encode_public(self._public_key, f) + f.put_u64(serial) + f.put_u32(self._type.value) + f.put_sshstr(key_id) + fprincipals = _FragList() + for p in self._valid_principals: + fprincipals.put_sshstr(p) + f.put_sshstr(fprincipals.tobytes()) + f.put_u64(self._valid_after) + f.put_u64(self._valid_before) + fcrit = _FragList() + for name, value in self._critical_options: + fcrit.put_sshstr(name) + if len(value) > 0: + foptval = _FragList() + foptval.put_sshstr(value) + fcrit.put_sshstr(foptval.tobytes()) + else: + fcrit.put_sshstr(value) + f.put_sshstr(fcrit.tobytes()) + fext = _FragList() + for name, value in self._extensions: + fext.put_sshstr(name) + if len(value) > 0: + fextval = _FragList() + fextval.put_sshstr(value) + fext.put_sshstr(fextval.tobytes()) + else: + fext.put_sshstr(value) + f.put_sshstr(fext.tobytes()) + f.put_sshstr(b"") # RESERVED FIELD + # encode CA public key + ca_type = _get_ssh_key_type(private_key) + caformat = _lookup_kformat(ca_type) + caf = _FragList() + caf.put_sshstr(ca_type) + caformat.encode_public(private_key.public_key(), caf) + f.put_sshstr(caf.tobytes()) + # Sigs according to the rules defined for the CA's public key + # (RFC4253 section 6.6 for ssh-rsa, RFC5656 for ECDSA, + # and RFC8032 for Ed25519). + if isinstance(private_key, ed25519.Ed25519PrivateKey): + signature = private_key.sign(f.tobytes()) + fsig = _FragList() + fsig.put_sshstr(ca_type) + fsig.put_sshstr(signature) + f.put_sshstr(fsig.tobytes()) + elif isinstance(private_key, ec.EllipticCurvePrivateKey): + hash_alg = _get_ec_hash_alg(private_key.curve) + signature = private_key.sign(f.tobytes(), ec.ECDSA(hash_alg)) + r, s = asym_utils.decode_dss_signature(signature) + fsig = _FragList() + fsig.put_sshstr(ca_type) + fsigblob = _FragList() + fsigblob.put_mpint(r) + fsigblob.put_mpint(s) + fsig.put_sshstr(fsigblob.tobytes()) + f.put_sshstr(fsig.tobytes()) + + else: + assert isinstance(private_key, rsa.RSAPrivateKey) + # Just like Golang, we're going to use SHA512 for RSA + # https://cs.opensource.google/go/x/crypto/+/refs/tags/ + # v0.4.0:ssh/certs.go;l=445 + # RFC 8332 defines SHA256 and 512 as options + fsig = _FragList() + fsig.put_sshstr(_SSH_RSA_SHA512) + signature = private_key.sign( + f.tobytes(), padding.PKCS1v15(), hashes.SHA512() + ) + fsig.put_sshstr(signature) + f.put_sshstr(fsig.tobytes()) + + cert_data = binascii.b2a_base64(f.tobytes()).strip() + # load_ssh_public_identity returns a union, but this is + # guaranteed to be an SSHCertificate, so we cast to make + # mypy happy. + return typing.cast( + SSHCertificate, + load_ssh_public_identity(b"".join([cert_prefix, b" ", cert_data])), + ) diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/twofactor/__init__.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/twofactor/__init__.py new file mode 100644 index 0000000..c1af423 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/twofactor/__init__.py @@ -0,0 +1,9 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + + +class InvalidToken(Exception): + pass diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/twofactor/hotp.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/twofactor/hotp.py new file mode 100644 index 0000000..21fb000 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/twofactor/hotp.py @@ -0,0 +1,101 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import base64 +import typing +from urllib.parse import quote, urlencode + +from cryptography.hazmat.primitives import constant_time, hmac +from cryptography.hazmat.primitives.hashes import SHA1, SHA256, SHA512 +from cryptography.hazmat.primitives.twofactor import InvalidToken +from cryptography.utils import Buffer + +HOTPHashTypes = typing.Union[SHA1, SHA256, SHA512] + + +def _generate_uri( + hotp: HOTP, + type_name: str, + account_name: str, + issuer: str | None, + extra_parameters: list[tuple[str, int]], +) -> str: + parameters = [ + ("digits", hotp._length), + ("secret", base64.b32encode(hotp._key)), + ("algorithm", hotp._algorithm.name.upper()), + ] + + if issuer is not None: + parameters.append(("issuer", issuer)) + + parameters.extend(extra_parameters) + + label = ( + f"{quote(issuer)}:{quote(account_name)}" + if issuer + else quote(account_name) + ) + return f"otpauth://{type_name}/{label}?{urlencode(parameters)}" + + +class HOTP: + def __init__( + self, + key: Buffer, + length: int, + algorithm: HOTPHashTypes, + backend: typing.Any = None, + enforce_key_length: bool = True, + ) -> None: + if len(key) < 16 and enforce_key_length is True: + raise ValueError("Key length has to be at least 128 bits.") + + if not isinstance(length, int): + raise TypeError("Length parameter must be an integer type.") + + if length < 6 or length > 8: + raise ValueError("Length of HOTP has to be between 6 and 8.") + + if not isinstance(algorithm, (SHA1, SHA256, SHA512)): + raise TypeError("Algorithm must be SHA1, SHA256 or SHA512.") + + self._key = key + self._length = length + self._algorithm = algorithm + + def generate(self, counter: int) -> bytes: + if not isinstance(counter, int): + raise TypeError("Counter parameter must be an integer type.") + + truncated_value = self._dynamic_truncate(counter) + hotp = truncated_value % (10**self._length) + return "{0:0{1}}".format(hotp, self._length).encode() + + def verify(self, hotp: bytes, counter: int) -> None: + if not constant_time.bytes_eq(self.generate(counter), hotp): + raise InvalidToken("Supplied HOTP value does not match.") + + def _dynamic_truncate(self, counter: int) -> int: + ctx = hmac.HMAC(self._key, self._algorithm) + + try: + ctx.update(counter.to_bytes(length=8, byteorder="big")) + except OverflowError: + raise ValueError(f"Counter must be between 0 and {2**64 - 1}.") + + hmac_value = ctx.finalize() + + offset = hmac_value[len(hmac_value) - 1] & 0b1111 + p = hmac_value[offset : offset + 4] + return int.from_bytes(p, byteorder="big") & 0x7FFFFFFF + + def get_provisioning_uri( + self, account_name: str, counter: int, issuer: str | None + ) -> str: + return _generate_uri( + self, "hotp", account_name, issuer, [("counter", int(counter))] + ) diff --git a/source/target-vendors/cp312/cryptography/hazmat/primitives/twofactor/totp.py b/source/target-vendors/cp312/cryptography/hazmat/primitives/twofactor/totp.py new file mode 100644 index 0000000..10c725c --- /dev/null +++ b/source/target-vendors/cp312/cryptography/hazmat/primitives/twofactor/totp.py @@ -0,0 +1,56 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import typing + +from cryptography.hazmat.primitives import constant_time +from cryptography.hazmat.primitives.twofactor import InvalidToken +from cryptography.hazmat.primitives.twofactor.hotp import ( + HOTP, + HOTPHashTypes, + _generate_uri, +) +from cryptography.utils import Buffer + + +class TOTP: + def __init__( + self, + key: Buffer, + length: int, + algorithm: HOTPHashTypes, + time_step: int, + backend: typing.Any = None, + enforce_key_length: bool = True, + ): + self._time_step = time_step + self._hotp = HOTP( + key, length, algorithm, enforce_key_length=enforce_key_length + ) + + def generate(self, time: int | float) -> bytes: + if not isinstance(time, (int, float)): + raise TypeError( + "Time parameter must be an integer type or float type." + ) + + counter = int(time / self._time_step) + return self._hotp.generate(counter) + + def verify(self, totp: bytes, time: int) -> None: + if not constant_time.bytes_eq(self.generate(time), totp): + raise InvalidToken("Supplied TOTP value does not match.") + + def get_provisioning_uri( + self, account_name: str, issuer: str | None + ) -> str: + return _generate_uri( + self._hotp, + "totp", + account_name, + issuer, + [("period", int(self._time_step))], + ) diff --git a/source/target-vendors/cp312/cryptography/py.typed b/source/target-vendors/cp312/cryptography/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/cryptography/utils.py b/source/target-vendors/cp312/cryptography/utils.py new file mode 100644 index 0000000..90bd42d --- /dev/null +++ b/source/target-vendors/cp312/cryptography/utils.py @@ -0,0 +1,136 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import enum +import sys +import types +import typing +import warnings +from collections.abc import Sequence + + +# We use a UserWarning subclass, instead of DeprecationWarning, because CPython +# decided deprecation warnings should be invisible by default. +class CryptographyDeprecationWarning(UserWarning): + pass + + +# Several APIs were deprecated with no specific end-of-life date because of the +# ubiquity of their use. They should not be removed until we agree on when that +# cycle ends. +DeprecatedIn36 = CryptographyDeprecationWarning +DeprecatedIn40 = CryptographyDeprecationWarning +DeprecatedIn41 = CryptographyDeprecationWarning +DeprecatedIn42 = CryptographyDeprecationWarning +DeprecatedIn43 = CryptographyDeprecationWarning +DeprecatedIn47 = CryptographyDeprecationWarning +DeprecatedIn50 = CryptographyDeprecationWarning + + +# If you're wondering why we don't use `Buffer`, it's because `Buffer` would +# be more accurately named: Bufferable. It means something which has an +# `__buffer__`. Which means you can't actually treat the result as a buffer +# (and do things like take a `len()`). +Buffer = typing.Union[bytes, bytearray, memoryview] + + +def _check_bytes(name: str, value: bytes) -> None: + if not isinstance(value, bytes): + raise TypeError(f"{name} must be bytes") + + +def _check_byteslike(name: str, value: Buffer) -> None: + if isinstance(value, (bytes, bytearray, memoryview)): + return + try: + memoryview(value) + except TypeError: + raise TypeError(f"{name} must be bytes-like") + + +def int_to_bytes(integer: int, length: int | None = None) -> bytes: + if length == 0: + raise ValueError("length argument can't be 0") + return integer.to_bytes( + length or (integer.bit_length() + 7) // 8 or 1, "big" + ) + + +class InterfaceNotImplemented(Exception): + pass + + +class _DeprecatedValue: + def __init__(self, value: object, message: str, warning_class): + self.value = value + self.message = message + self.warning_class = warning_class + + +class _ModuleWithDeprecations(types.ModuleType): + def __init__(self, module: types.ModuleType): + super().__init__(module.__name__) + self.__dict__["_module"] = module + + def __getattr__(self, name: str) -> typing.Any: + obj = getattr(self._module, name) + if isinstance(obj, _DeprecatedValue): + warnings.warn(obj.message, obj.warning_class, stacklevel=2) + obj = obj.value + else: + # Cache non-deprecated attributes in our own `__dict__` so that + # subsequent lookups are ordinary module attribute accesses and + # don't pay for this `__getattr__` (which would otherwise defeat + # CPython's LOAD_ATTR module caching for every attribute of the + # module). `__setattr__` and `__delattr__` keep the cache + # coherent. + self.__dict__[name] = obj + return obj + + def __setattr__(self, attr: str, value: object) -> None: + if isinstance(value, _DeprecatedValue): + self.__dict__.pop(attr, None) + else: + self.__dict__[attr] = value + setattr(self._module, attr, value) + + def __delattr__(self, attr: str) -> None: + obj = getattr(self._module, attr) + if isinstance(obj, _DeprecatedValue): + warnings.warn(obj.message, obj.warning_class, stacklevel=2) + + self.__dict__.pop(attr, None) + delattr(self._module, attr) + + def __dir__(self) -> Sequence[str]: + return ["_module", *dir(self._module)] + + +def deprecated( + value: object, + module_name: str, + message: str, + warning_class: type[Warning], + name: str | None = None, +) -> _DeprecatedValue: + module = sys.modules[module_name] + if not isinstance(module, _ModuleWithDeprecations): + sys.modules[module_name] = module = _ModuleWithDeprecations(module) + dv = _DeprecatedValue(value, message, warning_class) + # Maintain backwards compatibility with `name is None` for pyOpenSSL. + if name is not None: + setattr(module, name, dv) + return dv + + +# Python 3.10 changed representation of enums. We use well-defined object +# representation and string representation from Python 3.9. +class Enum(enum.Enum): + def __repr__(self) -> str: + return f"<{self.__class__.__name__}.{self._name_}: {self._value_!r}>" + + def __str__(self) -> str: + return f"{self.__class__.__name__}.{self._name_}" diff --git a/source/target-vendors/cp312/cryptography/x509/__init__.py b/source/target-vendors/cp312/cryptography/x509/__init__.py new file mode 100644 index 0000000..6442b00 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/x509/__init__.py @@ -0,0 +1,270 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.x509 import certificate_transparency, oid, verification +from cryptography.x509.base import ( + Attribute, + AttributeNotFound, + Attributes, + Certificate, + CertificateBuilder, + CertificateRevocationList, + CertificateRevocationListBuilder, + CertificateSigningRequest, + CertificateSigningRequestBuilder, + InvalidVersion, + RevokedCertificate, + RevokedCertificateBuilder, + Version, + load_der_x509_certificate, + load_der_x509_crl, + load_der_x509_csr, + load_pem_x509_certificate, + load_pem_x509_certificates, + load_pem_x509_crl, + load_pem_x509_csr, + random_serial_number, +) +from cryptography.x509.extensions import ( + AccessDescription, + Admission, + Admissions, + AuthorityInformationAccess, + AuthorityKeyIdentifier, + BasicConstraints, + CertificateIssuer, + CertificatePolicies, + CRLDistributionPoints, + CRLNumber, + CRLReason, + DeltaCRLIndicator, + DistributionPoint, + DuplicateExtension, + ExtendedKeyUsage, + Extension, + ExtensionNotFound, + Extensions, + ExtensionType, + FreshestCRL, + GeneralNames, + InhibitAnyPolicy, + InvalidityDate, + IssuerAlternativeName, + IssuingDistributionPoint, + KeyUsage, + MSCertificateTemplate, + NameConstraints, + NamingAuthority, + NoticeReference, + OCSPAcceptableResponses, + OCSPNoCheck, + OCSPNonce, + PolicyConstraints, + PolicyInformation, + PrecertificateSignedCertificateTimestamps, + PrecertPoison, + PrivateKeyUsagePeriod, + ProfessionInfo, + ReasonFlags, + SignedCertificateTimestamps, + SubjectAlternativeName, + SubjectInformationAccess, + SubjectKeyIdentifier, + TLSFeature, + TLSFeatureType, + UnrecognizedExtension, + UserNotice, +) +from cryptography.x509.general_name import ( + DirectoryName, + DNSName, + GeneralName, + IPAddress, + OtherName, + RegisteredID, + RFC822Name, + UniformResourceIdentifier, + UnsupportedGeneralNameType, +) +from cryptography.x509.name import ( + Name, + NameAttribute, + RelativeDistinguishedName, +) +from cryptography.x509.oid import ( + AuthorityInformationAccessOID, + CertificatePoliciesOID, + CRLEntryExtensionOID, + ExtendedKeyUsageOID, + ExtensionOID, + NameOID, + ObjectIdentifier, + PublicKeyAlgorithmOID, + SignatureAlgorithmOID, +) + +OID_AUTHORITY_INFORMATION_ACCESS = ExtensionOID.AUTHORITY_INFORMATION_ACCESS +OID_AUTHORITY_KEY_IDENTIFIER = ExtensionOID.AUTHORITY_KEY_IDENTIFIER +OID_BASIC_CONSTRAINTS = ExtensionOID.BASIC_CONSTRAINTS +OID_CERTIFICATE_POLICIES = ExtensionOID.CERTIFICATE_POLICIES +OID_CRL_DISTRIBUTION_POINTS = ExtensionOID.CRL_DISTRIBUTION_POINTS +OID_EXTENDED_KEY_USAGE = ExtensionOID.EXTENDED_KEY_USAGE +OID_FRESHEST_CRL = ExtensionOID.FRESHEST_CRL +OID_INHIBIT_ANY_POLICY = ExtensionOID.INHIBIT_ANY_POLICY +OID_ISSUER_ALTERNATIVE_NAME = ExtensionOID.ISSUER_ALTERNATIVE_NAME +OID_KEY_USAGE = ExtensionOID.KEY_USAGE +OID_PRIVATE_KEY_USAGE_PERIOD = ExtensionOID.PRIVATE_KEY_USAGE_PERIOD +OID_NAME_CONSTRAINTS = ExtensionOID.NAME_CONSTRAINTS +OID_OCSP_NO_CHECK = ExtensionOID.OCSP_NO_CHECK +OID_POLICY_CONSTRAINTS = ExtensionOID.POLICY_CONSTRAINTS +OID_POLICY_MAPPINGS = ExtensionOID.POLICY_MAPPINGS +OID_SUBJECT_ALTERNATIVE_NAME = ExtensionOID.SUBJECT_ALTERNATIVE_NAME +OID_SUBJECT_DIRECTORY_ATTRIBUTES = ExtensionOID.SUBJECT_DIRECTORY_ATTRIBUTES +OID_SUBJECT_INFORMATION_ACCESS = ExtensionOID.SUBJECT_INFORMATION_ACCESS +OID_SUBJECT_KEY_IDENTIFIER = ExtensionOID.SUBJECT_KEY_IDENTIFIER + +OID_DSA_WITH_SHA1 = SignatureAlgorithmOID.DSA_WITH_SHA1 +OID_DSA_WITH_SHA224 = SignatureAlgorithmOID.DSA_WITH_SHA224 +OID_DSA_WITH_SHA256 = SignatureAlgorithmOID.DSA_WITH_SHA256 +OID_ECDSA_WITH_SHA1 = SignatureAlgorithmOID.ECDSA_WITH_SHA1 +OID_ECDSA_WITH_SHA224 = SignatureAlgorithmOID.ECDSA_WITH_SHA224 +OID_ECDSA_WITH_SHA256 = SignatureAlgorithmOID.ECDSA_WITH_SHA256 +OID_ECDSA_WITH_SHA384 = SignatureAlgorithmOID.ECDSA_WITH_SHA384 +OID_ECDSA_WITH_SHA512 = SignatureAlgorithmOID.ECDSA_WITH_SHA512 +OID_RSA_WITH_MD5 = SignatureAlgorithmOID.RSA_WITH_MD5 +OID_RSA_WITH_SHA1 = SignatureAlgorithmOID.RSA_WITH_SHA1 +OID_RSA_WITH_SHA224 = SignatureAlgorithmOID.RSA_WITH_SHA224 +OID_RSA_WITH_SHA256 = SignatureAlgorithmOID.RSA_WITH_SHA256 +OID_RSA_WITH_SHA384 = SignatureAlgorithmOID.RSA_WITH_SHA384 +OID_RSA_WITH_SHA512 = SignatureAlgorithmOID.RSA_WITH_SHA512 +OID_RSASSA_PSS = SignatureAlgorithmOID.RSASSA_PSS + +OID_COMMON_NAME = NameOID.COMMON_NAME +OID_COUNTRY_NAME = NameOID.COUNTRY_NAME +OID_DOMAIN_COMPONENT = NameOID.DOMAIN_COMPONENT +OID_DN_QUALIFIER = NameOID.DN_QUALIFIER +OID_EMAIL_ADDRESS = NameOID.EMAIL_ADDRESS +OID_GENERATION_QUALIFIER = NameOID.GENERATION_QUALIFIER +OID_GIVEN_NAME = NameOID.GIVEN_NAME +OID_LOCALITY_NAME = NameOID.LOCALITY_NAME +OID_ORGANIZATIONAL_UNIT_NAME = NameOID.ORGANIZATIONAL_UNIT_NAME +OID_ORGANIZATION_NAME = NameOID.ORGANIZATION_NAME +OID_PSEUDONYM = NameOID.PSEUDONYM +OID_SERIAL_NUMBER = NameOID.SERIAL_NUMBER +OID_STATE_OR_PROVINCE_NAME = NameOID.STATE_OR_PROVINCE_NAME +OID_SURNAME = NameOID.SURNAME +OID_TITLE = NameOID.TITLE + +OID_CLIENT_AUTH = ExtendedKeyUsageOID.CLIENT_AUTH +OID_CODE_SIGNING = ExtendedKeyUsageOID.CODE_SIGNING +OID_EMAIL_PROTECTION = ExtendedKeyUsageOID.EMAIL_PROTECTION +OID_OCSP_SIGNING = ExtendedKeyUsageOID.OCSP_SIGNING +OID_SERVER_AUTH = ExtendedKeyUsageOID.SERVER_AUTH +OID_TIME_STAMPING = ExtendedKeyUsageOID.TIME_STAMPING + +OID_ANY_POLICY = CertificatePoliciesOID.ANY_POLICY +OID_CPS_QUALIFIER = CertificatePoliciesOID.CPS_QUALIFIER +OID_CPS_USER_NOTICE = CertificatePoliciesOID.CPS_USER_NOTICE + +OID_CERTIFICATE_ISSUER = CRLEntryExtensionOID.CERTIFICATE_ISSUER +OID_CRL_REASON = CRLEntryExtensionOID.CRL_REASON +OID_INVALIDITY_DATE = CRLEntryExtensionOID.INVALIDITY_DATE + +OID_CA_ISSUERS = AuthorityInformationAccessOID.CA_ISSUERS +OID_OCSP = AuthorityInformationAccessOID.OCSP + +__all__ = [ + "OID_CA_ISSUERS", + "OID_OCSP", + "AccessDescription", + "Admission", + "Admissions", + "Attribute", + "AttributeNotFound", + "Attributes", + "AuthorityInformationAccess", + "AuthorityKeyIdentifier", + "BasicConstraints", + "CRLDistributionPoints", + "CRLNumber", + "CRLReason", + "Certificate", + "CertificateBuilder", + "CertificateIssuer", + "CertificatePolicies", + "CertificateRevocationList", + "CertificateRevocationListBuilder", + "CertificateSigningRequest", + "CertificateSigningRequestBuilder", + "DNSName", + "DeltaCRLIndicator", + "DirectoryName", + "DistributionPoint", + "DuplicateExtension", + "ExtendedKeyUsage", + "Extension", + "ExtensionNotFound", + "ExtensionType", + "Extensions", + "FreshestCRL", + "GeneralName", + "GeneralNames", + "IPAddress", + "InhibitAnyPolicy", + "InvalidVersion", + "InvalidityDate", + "IssuerAlternativeName", + "IssuingDistributionPoint", + "KeyUsage", + "MSCertificateTemplate", + "Name", + "NameAttribute", + "NameConstraints", + "NameOID", + "NamingAuthority", + "NoticeReference", + "OCSPAcceptableResponses", + "OCSPNoCheck", + "OCSPNonce", + "ObjectIdentifier", + "OtherName", + "PolicyConstraints", + "PolicyInformation", + "PrecertPoison", + "PrecertificateSignedCertificateTimestamps", + "PrivateKeyUsagePeriod", + "ProfessionInfo", + "PublicKeyAlgorithmOID", + "RFC822Name", + "ReasonFlags", + "RegisteredID", + "RelativeDistinguishedName", + "RevokedCertificate", + "RevokedCertificateBuilder", + "SignatureAlgorithmOID", + "SignedCertificateTimestamps", + "SubjectAlternativeName", + "SubjectInformationAccess", + "SubjectKeyIdentifier", + "TLSFeature", + "TLSFeatureType", + "UniformResourceIdentifier", + "UnrecognizedExtension", + "UnsupportedGeneralNameType", + "UserNotice", + "Version", + "certificate_transparency", + "load_der_x509_certificate", + "load_der_x509_crl", + "load_der_x509_csr", + "load_pem_x509_certificate", + "load_pem_x509_certificates", + "load_pem_x509_crl", + "load_pem_x509_csr", + "oid", + "random_serial_number", + "verification", +] diff --git a/source/target-vendors/cp312/cryptography/x509/base.py b/source/target-vendors/cp312/cryptography/x509/base.py new file mode 100644 index 0000000..3ded5dc --- /dev/null +++ b/source/target-vendors/cp312/cryptography/x509/base.py @@ -0,0 +1,834 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import datetime +import os +import typing +from collections.abc import Iterable + +from cryptography import utils +from cryptography.hazmat.bindings._rust import x509 as rust_x509 +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ( + dsa, + ec, + ed448, + ed25519, + mldsa, + mlkem, + padding, + rsa, + x448, + x25519, +) +from cryptography.hazmat.primitives.asymmetric.types import ( + CertificateIssuerPrivateKeyTypes, + CertificatePublicKeyTypes, +) +from cryptography.x509.extensions import ( + Extension, + ExtensionType, + _make_sequence_methods, +) +from cryptography.x509.name import Name, _ASN1Type +from cryptography.x509.oid import ObjectIdentifier + +_EARLIEST_UTC_TIME = datetime.datetime(1950, 1, 1) + +# This must be kept in sync with sign.rs's list of allowable types in +# identify_hash_type +_AllowedHashTypes = typing.Union[ + hashes.SHA224, + hashes.SHA256, + hashes.SHA384, + hashes.SHA512, + hashes.SHA3_224, + hashes.SHA3_256, + hashes.SHA3_384, + hashes.SHA3_512, +] + + +class AttributeNotFound(Exception): + def __init__(self, msg: str, oid: ObjectIdentifier) -> None: + super().__init__(msg) + self.oid = oid + + +def _reject_duplicate_extension( + extension: Extension[ExtensionType], + extensions: list[Extension[ExtensionType]], +) -> None: + # This is quadratic in the number of extensions + for e in extensions: + if e.oid == extension.oid: + raise ValueError("This extension has already been set.") + + +def _reject_duplicate_attribute( + oid: ObjectIdentifier, + attributes: list[tuple[ObjectIdentifier, bytes, int | None]], +) -> None: + # This is quadratic in the number of attributes + for attr_oid, _, _ in attributes: + if attr_oid == oid: + raise ValueError("This attribute has already been set.") + + +def _convert_to_naive_utc_time(time: datetime.datetime) -> datetime.datetime: + """Normalizes a datetime to a naive datetime in UTC. + + time -- datetime to normalize. Assumed to be in UTC if not timezone + aware. + """ + if time.tzinfo is not None: + offset = time.utcoffset() + offset = offset if offset else datetime.timedelta() + return time.replace(tzinfo=None) - offset + else: + return time + + +class Attribute: + def __init__( + self, + oid: ObjectIdentifier, + value: bytes, + _type: int = _ASN1Type.UTF8String.value, + ) -> None: + self._oid = oid + self._value = value + self._type = _type + + @property + def oid(self) -> ObjectIdentifier: + return self._oid + + @property + def value(self) -> bytes: + return self._value + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Attribute): + return NotImplemented + + return ( + self.oid == other.oid + and self.value == other.value + and self._type == other._type + ) + + def __hash__(self) -> int: + return hash((self.oid, self.value, self._type)) + + +class Attributes: + def __init__( + self, + attributes: Iterable[Attribute], + ) -> None: + self._attributes = list(attributes) + + __len__, __iter__, __getitem__ = _make_sequence_methods("_attributes") + + def __repr__(self) -> str: + return f"" + + def get_attribute_for_oid(self, oid: ObjectIdentifier) -> Attribute: + for attr in self: + if attr.oid == oid: + return attr + + raise AttributeNotFound(f"No {oid} attribute was found", oid) + + +class Version(utils.Enum): + v1 = 0 + v3 = 2 + + +class InvalidVersion(Exception): + def __init__(self, msg: str, parsed_version: int) -> None: + super().__init__(msg) + self.parsed_version = parsed_version + + +Certificate = rust_x509.Certificate +RevokedCertificate = rust_x509.RevokedCertificate + + +CertificateRevocationList = rust_x509.CertificateRevocationList +CertificateSigningRequest = rust_x509.CertificateSigningRequest + + +load_pem_x509_certificate = rust_x509.load_pem_x509_certificate +load_der_x509_certificate = rust_x509.load_der_x509_certificate + +load_pem_x509_certificates = rust_x509.load_pem_x509_certificates + +load_pem_x509_csr = rust_x509.load_pem_x509_csr +load_der_x509_csr = rust_x509.load_der_x509_csr + +load_pem_x509_crl = rust_x509.load_pem_x509_crl +load_der_x509_crl = rust_x509.load_der_x509_crl + + +class CertificateSigningRequestBuilder: + def __init__( + self, + subject_name: Name | None = None, + extensions: list[Extension[ExtensionType]] = [], + attributes: list[tuple[ObjectIdentifier, bytes, int | None]] = [], + ): + """ + Creates an empty X.509 certificate request (v1). + """ + self._subject_name = subject_name + self._extensions = extensions + self._attributes = attributes + + def subject_name(self, name: Name) -> CertificateSigningRequestBuilder: + """ + Sets the certificate requestor's distinguished name. + """ + if not isinstance(name, Name): + raise TypeError("Expecting x509.Name object.") + if self._subject_name is not None: + raise ValueError("The subject name may only be set once.") + return CertificateSigningRequestBuilder( + name, self._extensions, self._attributes + ) + + def add_extension( + self, extval: ExtensionType, critical: bool + ) -> CertificateSigningRequestBuilder: + """ + Adds an X.509 extension to the certificate request. + """ + if not isinstance(extval, ExtensionType): + raise TypeError("extension must be an ExtensionType") + + extension = Extension(extval.oid, critical, extval) + _reject_duplicate_extension(extension, self._extensions) + + return CertificateSigningRequestBuilder( + self._subject_name, + [*self._extensions, extension], + self._attributes, + ) + + def add_attribute( + self, + oid: ObjectIdentifier, + value: bytes, + *, + _tag: _ASN1Type | None = None, + ) -> CertificateSigningRequestBuilder: + """ + Adds an X.509 attribute with an OID and associated value. + """ + if not isinstance(oid, ObjectIdentifier): + raise TypeError("oid must be an ObjectIdentifier") + + if not isinstance(value, bytes): + raise TypeError("value must be bytes") + + if _tag is not None and not isinstance(_tag, _ASN1Type): + raise TypeError("tag must be _ASN1Type") + + _reject_duplicate_attribute(oid, self._attributes) + + if _tag is not None: + tag = _tag.value + else: + tag = None + + return CertificateSigningRequestBuilder( + self._subject_name, + self._extensions, + [*self._attributes, (oid, value, tag)], + ) + + def sign( + self, + private_key: CertificateIssuerPrivateKeyTypes, + algorithm: _AllowedHashTypes | None, + backend: typing.Any = None, + *, + rsa_padding: padding.PSS | padding.PKCS1v15 | None = None, + ecdsa_deterministic: bool | None = None, + ) -> CertificateSigningRequest: + """ + Signs the request using the requestor's private key. + """ + if self._subject_name is None: + raise ValueError("A CertificateSigningRequest must have a subject") + + if rsa_padding is not None: + if not isinstance(rsa_padding, (padding.PSS, padding.PKCS1v15)): + raise TypeError("Padding must be PSS or PKCS1v15") + if not isinstance(private_key, rsa.RSAPrivateKey): + raise TypeError("Padding is only supported for RSA keys") + + if ecdsa_deterministic is not None: + if not isinstance(private_key, ec.EllipticCurvePrivateKey): + raise TypeError( + "Deterministic ECDSA is only supported for EC keys" + ) + + return rust_x509.create_x509_csr( + self, + private_key, + algorithm, + rsa_padding, + ecdsa_deterministic, + ) + + +class CertificateBuilder: + _extensions: list[Extension[ExtensionType]] + + def __init__( + self, + issuer_name: Name | None = None, + subject_name: Name | None = None, + public_key: CertificatePublicKeyTypes | None = None, + serial_number: int | None = None, + not_valid_before: datetime.datetime | None = None, + not_valid_after: datetime.datetime | None = None, + extensions: list[Extension[ExtensionType]] = [], + public_key_rsa_padding: type[padding.PSS] | None = None, + ) -> None: + self._version = Version.v3 + self._issuer_name = issuer_name + self._subject_name = subject_name + self._public_key = public_key + self._serial_number = serial_number + self._not_valid_before = not_valid_before + self._not_valid_after = not_valid_after + self._extensions = extensions + self._public_key_rsa_padding = public_key_rsa_padding + + def issuer_name(self, name: Name) -> CertificateBuilder: + """ + Sets the CA's distinguished name. + """ + if not isinstance(name, Name): + raise TypeError("Expecting x509.Name object.") + if self._issuer_name is not None: + raise ValueError("The issuer name may only be set once.") + return CertificateBuilder( + name, + self._subject_name, + self._public_key, + self._serial_number, + self._not_valid_before, + self._not_valid_after, + self._extensions, + self._public_key_rsa_padding, + ) + + def subject_name(self, name: Name) -> CertificateBuilder: + """ + Sets the requestor's distinguished name. + """ + if not isinstance(name, Name): + raise TypeError("Expecting x509.Name object.") + if self._subject_name is not None: + raise ValueError("The subject name may only be set once.") + return CertificateBuilder( + self._issuer_name, + name, + self._public_key, + self._serial_number, + self._not_valid_before, + self._not_valid_after, + self._extensions, + self._public_key_rsa_padding, + ) + + def public_key( + self, + key: CertificatePublicKeyTypes, + *, + rsa_padding: type[padding.PSS] | None = None, + ) -> CertificateBuilder: + """ + Sets the requestor's public key (as found in the signing request). + """ + if not isinstance( + key, + ( + dsa.DSAPublicKey, + rsa.RSAPublicKey, + ec.EllipticCurvePublicKey, + ed25519.Ed25519PublicKey, + ed448.Ed448PublicKey, + mldsa.MLDSA44PublicKey, + mldsa.MLDSA65PublicKey, + mldsa.MLDSA87PublicKey, + mlkem.MLKEM768PublicKey, + mlkem.MLKEM1024PublicKey, + x25519.X25519PublicKey, + x448.X448PublicKey, + ), + ): + raise TypeError( + "Expecting one of DSAPublicKey, RSAPublicKey," + " EllipticCurvePublicKey, Ed25519PublicKey," + " Ed448PublicKey, MLDSA44PublicKey, MLDSA65PublicKey," + " MLDSA87PublicKey, MLKEM768PublicKey, MLKEM1024PublicKey," + " X25519PublicKey or X448PublicKey." + ) + if rsa_padding is not None: + if rsa_padding is not padding.PSS: + raise TypeError( + "rsa_padding must be the PSS class, not an instance" + ) + if not isinstance(key, rsa.RSAPublicKey): + raise TypeError( + "rsa_padding is only supported with RSA public keys" + ) + if self._public_key is not None: + raise ValueError("The public key may only be set once.") + return CertificateBuilder( + self._issuer_name, + self._subject_name, + key, + self._serial_number, + self._not_valid_before, + self._not_valid_after, + self._extensions, + rsa_padding, + ) + + def serial_number(self, number: int) -> CertificateBuilder: + """ + Sets the certificate serial number. + """ + if not isinstance(number, int): + raise TypeError("Serial number must be of integral type.") + if self._serial_number is not None: + raise ValueError("The serial number may only be set once.") + if number <= 0: + raise ValueError("The serial number should be positive.") + + # ASN.1 integers are always signed, so most significant bit must be + # zero. + if number.bit_length() >= 160: # As defined in RFC 5280 + raise ValueError( + "The serial number should not be more than 159 bits." + ) + return CertificateBuilder( + self._issuer_name, + self._subject_name, + self._public_key, + number, + self._not_valid_before, + self._not_valid_after, + self._extensions, + self._public_key_rsa_padding, + ) + + def not_valid_before(self, time: datetime.datetime) -> CertificateBuilder: + """ + Sets the certificate activation time. + """ + if not isinstance(time, datetime.datetime): + raise TypeError("Expecting datetime object.") + if self._not_valid_before is not None: + raise ValueError("The not valid before may only be set once.") + time = _convert_to_naive_utc_time(time) + if time < _EARLIEST_UTC_TIME: + raise ValueError( + "The not valid before date must be on or after" + " 1950 January 1)." + ) + if self._not_valid_after is not None and time > self._not_valid_after: + raise ValueError( + "The not valid before date must be before the not valid after " + "date." + ) + return CertificateBuilder( + self._issuer_name, + self._subject_name, + self._public_key, + self._serial_number, + time, + self._not_valid_after, + self._extensions, + self._public_key_rsa_padding, + ) + + def not_valid_after(self, time: datetime.datetime) -> CertificateBuilder: + """ + Sets the certificate expiration time. + """ + if not isinstance(time, datetime.datetime): + raise TypeError("Expecting datetime object.") + if self._not_valid_after is not None: + raise ValueError("The not valid after may only be set once.") + time = _convert_to_naive_utc_time(time) + if time < _EARLIEST_UTC_TIME: + raise ValueError( + "The not valid after date must be on or after 1950 January 1." + ) + if ( + self._not_valid_before is not None + and time < self._not_valid_before + ): + raise ValueError( + "The not valid after date must be after the not valid before " + "date." + ) + return CertificateBuilder( + self._issuer_name, + self._subject_name, + self._public_key, + self._serial_number, + self._not_valid_before, + time, + self._extensions, + self._public_key_rsa_padding, + ) + + def add_extension( + self, extval: ExtensionType, critical: bool + ) -> CertificateBuilder: + """ + Adds an X.509 extension to the certificate. + """ + if not isinstance(extval, ExtensionType): + raise TypeError("extension must be an ExtensionType") + + extension = Extension(extval.oid, critical, extval) + _reject_duplicate_extension(extension, self._extensions) + + return CertificateBuilder( + self._issuer_name, + self._subject_name, + self._public_key, + self._serial_number, + self._not_valid_before, + self._not_valid_after, + [*self._extensions, extension], + self._public_key_rsa_padding, + ) + + def sign( + self, + private_key: CertificateIssuerPrivateKeyTypes, + algorithm: _AllowedHashTypes | None, + backend: typing.Any = None, + *, + rsa_padding: padding.PSS | padding.PKCS1v15 | None = None, + ecdsa_deterministic: bool | None = None, + ) -> Certificate: + """ + Signs the certificate using the CA's private key. + """ + if self._subject_name is None: + raise ValueError("A certificate must have a subject name") + + if self._issuer_name is None: + raise ValueError("A certificate must have an issuer name") + + if self._serial_number is None: + raise ValueError("A certificate must have a serial number") + + if self._not_valid_before is None: + raise ValueError("A certificate must have a not valid before time") + + if self._not_valid_after is None: + raise ValueError("A certificate must have a not valid after time") + + if self._public_key is None: + raise ValueError("A certificate must have a public key") + + if private_key is None: + raise TypeError("Need private key to sign certificate") + + if rsa_padding is not None: + if not isinstance(rsa_padding, (padding.PSS, padding.PKCS1v15)): + raise TypeError("Padding must be PSS or PKCS1v15") + if not isinstance(private_key, rsa.RSAPrivateKey): + raise TypeError("Padding is only supported for RSA keys") + + if ecdsa_deterministic is not None: + if not isinstance(private_key, ec.EllipticCurvePrivateKey): + raise TypeError( + "Deterministic ECDSA is only supported for EC keys" + ) + + return rust_x509.create_x509_certificate( + self, + private_key, + algorithm, + rsa_padding, + ecdsa_deterministic, + ) + + def create_unsigned(self) -> Certificate: + """ + Creates an unsigned certificate, per RFC 9925. + """ + if self._subject_name is None: + raise ValueError("A certificate must have a subject name") + + if self._issuer_name is None: + raise ValueError("A certificate must have an issuer name") + + if self._serial_number is None: + raise ValueError("A certificate must have a serial number") + + if self._not_valid_before is None: + raise ValueError("A certificate must have a not valid before time") + + if self._not_valid_after is None: + raise ValueError("A certificate must have a not valid after time") + + if self._public_key is None: + raise ValueError("A certificate must have a public key") + + return rust_x509.create_x509_certificate( + self, + private_key=None, + hash_algorithm=None, + rsa_padding=None, + ecdsa_deterministic=None, + ) + + +class CertificateRevocationListBuilder: + _extensions: list[Extension[ExtensionType]] + _revoked_certificates: list[RevokedCertificate] + + def __init__( + self, + issuer_name: Name | None = None, + last_update: datetime.datetime | None = None, + next_update: datetime.datetime | None = None, + extensions: list[Extension[ExtensionType]] = [], + revoked_certificates: list[RevokedCertificate] = [], + ): + self._issuer_name = issuer_name + self._last_update = last_update + self._next_update = next_update + self._extensions = extensions + self._revoked_certificates = revoked_certificates + + def issuer_name( + self, issuer_name: Name + ) -> CertificateRevocationListBuilder: + if not isinstance(issuer_name, Name): + raise TypeError("Expecting x509.Name object.") + if self._issuer_name is not None: + raise ValueError("The issuer name may only be set once.") + return CertificateRevocationListBuilder( + issuer_name, + self._last_update, + self._next_update, + self._extensions, + self._revoked_certificates, + ) + + def last_update( + self, last_update: datetime.datetime + ) -> CertificateRevocationListBuilder: + if not isinstance(last_update, datetime.datetime): + raise TypeError("Expecting datetime object.") + if self._last_update is not None: + raise ValueError("Last update may only be set once.") + last_update = _convert_to_naive_utc_time(last_update) + if last_update < _EARLIEST_UTC_TIME: + raise ValueError( + "The last update date must be on or after 1950 January 1." + ) + if self._next_update is not None and last_update > self._next_update: + raise ValueError( + "The last update date must be before the next update date." + ) + return CertificateRevocationListBuilder( + self._issuer_name, + last_update, + self._next_update, + self._extensions, + self._revoked_certificates, + ) + + def next_update( + self, next_update: datetime.datetime + ) -> CertificateRevocationListBuilder: + if not isinstance(next_update, datetime.datetime): + raise TypeError("Expecting datetime object.") + if self._next_update is not None: + raise ValueError("Last update may only be set once.") + next_update = _convert_to_naive_utc_time(next_update) + if next_update < _EARLIEST_UTC_TIME: + raise ValueError( + "The last update date must be on or after 1950 January 1." + ) + if self._last_update is not None and next_update < self._last_update: + raise ValueError( + "The next update date must be after the last update date." + ) + return CertificateRevocationListBuilder( + self._issuer_name, + self._last_update, + next_update, + self._extensions, + self._revoked_certificates, + ) + + def add_extension( + self, extval: ExtensionType, critical: bool + ) -> CertificateRevocationListBuilder: + """ + Adds an X.509 extension to the certificate revocation list. + """ + if not isinstance(extval, ExtensionType): + raise TypeError("extension must be an ExtensionType") + + extension = Extension(extval.oid, critical, extval) + _reject_duplicate_extension(extension, self._extensions) + return CertificateRevocationListBuilder( + self._issuer_name, + self._last_update, + self._next_update, + [*self._extensions, extension], + self._revoked_certificates, + ) + + def add_revoked_certificate( + self, revoked_certificate: RevokedCertificate + ) -> CertificateRevocationListBuilder: + """ + Adds a revoked certificate to the CRL. + """ + if not isinstance(revoked_certificate, RevokedCertificate): + raise TypeError("Must be an instance of RevokedCertificate") + + return CertificateRevocationListBuilder( + self._issuer_name, + self._last_update, + self._next_update, + self._extensions, + [*self._revoked_certificates, revoked_certificate], + ) + + def sign( + self, + private_key: CertificateIssuerPrivateKeyTypes, + algorithm: _AllowedHashTypes | None, + backend: typing.Any = None, + *, + rsa_padding: padding.PSS | padding.PKCS1v15 | None = None, + ecdsa_deterministic: bool | None = None, + ) -> CertificateRevocationList: + if self._issuer_name is None: + raise ValueError("A CRL must have an issuer name") + + if self._last_update is None: + raise ValueError("A CRL must have a last update time") + + if self._next_update is None: + raise ValueError("A CRL must have a next update time") + + if rsa_padding is not None: + if not isinstance(rsa_padding, (padding.PSS, padding.PKCS1v15)): + raise TypeError("Padding must be PSS or PKCS1v15") + if not isinstance(private_key, rsa.RSAPrivateKey): + raise TypeError("Padding is only supported for RSA keys") + + if ecdsa_deterministic is not None: + if not isinstance(private_key, ec.EllipticCurvePrivateKey): + raise TypeError( + "Deterministic ECDSA is only supported for EC keys" + ) + + return rust_x509.create_x509_crl( + self, + private_key, + algorithm, + rsa_padding, + ecdsa_deterministic, + ) + + +class RevokedCertificateBuilder: + def __init__( + self, + serial_number: int | None = None, + revocation_date: datetime.datetime | None = None, + extensions: list[Extension[ExtensionType]] = [], + ): + self._serial_number = serial_number + self._revocation_date = revocation_date + self._extensions = extensions + + def serial_number(self, number: int) -> RevokedCertificateBuilder: + if not isinstance(number, int): + raise TypeError("Serial number must be of integral type.") + if self._serial_number is not None: + raise ValueError("The serial number may only be set once.") + if number <= 0: + raise ValueError("The serial number should be positive") + + # ASN.1 integers are always signed, so most significant bit must be + # zero. + if number.bit_length() >= 160: # As defined in RFC 5280 + raise ValueError( + "The serial number should not be more than 159 bits." + ) + return RevokedCertificateBuilder( + number, self._revocation_date, self._extensions + ) + + def revocation_date( + self, time: datetime.datetime + ) -> RevokedCertificateBuilder: + if not isinstance(time, datetime.datetime): + raise TypeError("Expecting datetime object.") + if self._revocation_date is not None: + raise ValueError("The revocation date may only be set once.") + time = _convert_to_naive_utc_time(time) + if time < _EARLIEST_UTC_TIME: + raise ValueError( + "The revocation date must be on or after 1950 January 1." + ) + return RevokedCertificateBuilder( + self._serial_number, time, self._extensions + ) + + def add_extension( + self, extval: ExtensionType, critical: bool + ) -> RevokedCertificateBuilder: + if not isinstance(extval, ExtensionType): + raise TypeError("extension must be an ExtensionType") + + extension = Extension(extval.oid, critical, extval) + _reject_duplicate_extension(extension, self._extensions) + return RevokedCertificateBuilder( + self._serial_number, + self._revocation_date, + [*self._extensions, extension], + ) + + def build(self, backend: typing.Any = None) -> RevokedCertificate: + if self._serial_number is None: + raise ValueError("A revoked certificate must have a serial number") + if self._revocation_date is None: + raise ValueError( + "A revoked certificate must have a revocation date" + ) + return rust_x509.create_revoked_certificate(self) + + +def random_serial_number() -> int: + return int.from_bytes(os.urandom(20), "big") >> 1 diff --git a/source/target-vendors/cp312/cryptography/x509/certificate_transparency.py b/source/target-vendors/cp312/cryptography/x509/certificate_transparency.py new file mode 100644 index 0000000..fb66cc6 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/x509/certificate_transparency.py @@ -0,0 +1,35 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography import utils +from cryptography.hazmat.bindings._rust import x509 as rust_x509 + + +class LogEntryType(utils.Enum): + X509_CERTIFICATE = 0 + PRE_CERTIFICATE = 1 + + +class Version(utils.Enum): + v1 = 0 + + +class SignatureAlgorithm(utils.Enum): + """ + Signature algorithms that are valid for SCTs. + + These are exactly the same as SignatureAlgorithm in RFC 5246 (TLS 1.2). + + See: + """ + + ANONYMOUS = 0 + RSA = 1 + DSA = 2 + ECDSA = 3 + + +SignedCertificateTimestamp = rust_x509.Sct diff --git a/source/target-vendors/cp312/cryptography/x509/extensions.py b/source/target-vendors/cp312/cryptography/x509/extensions.py new file mode 100644 index 0000000..dc7728e --- /dev/null +++ b/source/target-vendors/cp312/cryptography/x509/extensions.py @@ -0,0 +1,2533 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import abc +import datetime +import hashlib +import ipaddress +import typing +from collections.abc import Iterable, Iterator + +from cryptography import utils +from cryptography.hazmat.bindings._rust import asn1 +from cryptography.hazmat.bindings._rust import x509 as rust_x509 +from cryptography.hazmat.primitives import _serialization, constant_time +from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey +from cryptography.hazmat.primitives.asymmetric.types import ( + CertificateIssuerPublicKeyTypes, + CertificatePublicKeyTypes, +) +from cryptography.x509.certificate_transparency import ( + SignedCertificateTimestamp, +) +from cryptography.x509.general_name import ( + DirectoryName, + DNSName, + GeneralName, + IPAddress, + OtherName, + RegisteredID, + RFC822Name, + UniformResourceIdentifier, + _IPAddressTypes, +) +from cryptography.x509.name import Name, RelativeDistinguishedName +from cryptography.x509.oid import ( + CRLEntryExtensionOID, + ExtensionOID, + ObjectIdentifier, + OCSPExtensionOID, +) + +ExtensionTypeVar = typing.TypeVar( + "ExtensionTypeVar", bound="ExtensionType", covariant=True +) + + +def _key_identifier_from_public_key( + public_key: CertificatePublicKeyTypes, +) -> bytes: + if isinstance(public_key, RSAPublicKey): + data = public_key.public_bytes( + _serialization.Encoding.DER, + _serialization.PublicFormat.PKCS1, + ) + elif isinstance(public_key, EllipticCurvePublicKey): + data = public_key.public_bytes( + _serialization.Encoding.X962, + _serialization.PublicFormat.UncompressedPoint, + ) + else: + # This is a very slow way to do this. + serialized = public_key.public_bytes( + _serialization.Encoding.DER, + _serialization.PublicFormat.SubjectPublicKeyInfo, + ) + data = asn1.parse_spki_for_data(serialized) + + return hashlib.sha1(data).digest() + + +def _make_sequence_methods(field_name: str): + def len_method(self) -> int: + return len(getattr(self, field_name)) + + def iter_method(self): + return iter(getattr(self, field_name)) + + def getitem_method(self, idx): + return getattr(self, field_name)[idx] + + return len_method, iter_method, getitem_method + + +class DuplicateExtension(Exception): + def __init__(self, msg: str, oid: ObjectIdentifier) -> None: + super().__init__(msg) + self.oid = oid + + +class ExtensionNotFound(Exception): + def __init__(self, msg: str, oid: ObjectIdentifier) -> None: + super().__init__(msg) + self.oid = oid + + +class ExtensionType(metaclass=abc.ABCMeta): + oid: typing.ClassVar[ObjectIdentifier] + + def public_bytes(self) -> bytes: + """ + Serializes the extension type to DER. + """ + raise NotImplementedError( + f"public_bytes is not implemented for extension type {self!r}" + ) + + +class Extensions: + def __init__(self, extensions: Iterable[Extension[ExtensionType]]) -> None: + self._extensions = list(extensions) + + def get_extension_for_oid( + self, oid: ObjectIdentifier + ) -> Extension[ExtensionType]: + for ext in self: + if ext.oid == oid: + return ext + + raise ExtensionNotFound(f"No {oid} extension was found", oid) + + def get_extension_for_class( + self, extclass: type[ExtensionTypeVar] + ) -> Extension[ExtensionTypeVar]: + if extclass is UnrecognizedExtension: + raise TypeError( + "UnrecognizedExtension can't be used with " + "get_extension_for_class because more than one instance of the" + " class may be present." + ) + + for ext in self: + if isinstance(ext.value, extclass): + return ext + + raise ExtensionNotFound( + f"No {extclass} extension was found", extclass.oid + ) + + __len__, __iter__, __getitem__ = _make_sequence_methods("_extensions") + + def __repr__(self) -> str: + return f"" + + +class CRLNumber(ExtensionType): + oid = ExtensionOID.CRL_NUMBER + + def __init__(self, crl_number: int) -> None: + if not isinstance(crl_number, int): + raise TypeError("crl_number must be an integer") + + self._crl_number = crl_number + + def __eq__(self, other: object) -> bool: + if not isinstance(other, CRLNumber): + return NotImplemented + + return self.crl_number == other.crl_number + + def __hash__(self) -> int: + return hash(self.crl_number) + + def __repr__(self) -> str: + return f"" + + @property + def crl_number(self) -> int: + return self._crl_number + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class AuthorityKeyIdentifier(ExtensionType): + oid = ExtensionOID.AUTHORITY_KEY_IDENTIFIER + + def __init__( + self, + key_identifier: bytes | None, + authority_cert_issuer: Iterable[GeneralName] | None, + authority_cert_serial_number: int | None, + ) -> None: + if (authority_cert_issuer is None) != ( + authority_cert_serial_number is None + ): + raise ValueError( + "authority_cert_issuer and authority_cert_serial_number " + "must both be present or both None" + ) + + if authority_cert_issuer is not None: + authority_cert_issuer = list(authority_cert_issuer) + if not all( + isinstance(x, GeneralName) for x in authority_cert_issuer + ): + raise TypeError( + "authority_cert_issuer must be a list of GeneralName " + "objects" + ) + + if authority_cert_serial_number is not None and not isinstance( + authority_cert_serial_number, int + ): + raise TypeError("authority_cert_serial_number must be an integer") + + self._key_identifier = key_identifier + self._authority_cert_issuer = authority_cert_issuer + self._authority_cert_serial_number = authority_cert_serial_number + + # This takes a subset of CertificatePublicKeyTypes because an issuer + # cannot have an X25519/X448 key. This introduces some unfortunate + # asymmetry that requires typing users to explicitly + # narrow their type, but we should make this accurate and not just + # convenient. + @classmethod + def from_issuer_public_key( + cls, public_key: CertificateIssuerPublicKeyTypes + ) -> AuthorityKeyIdentifier: + digest = _key_identifier_from_public_key(public_key) + return cls( + key_identifier=digest, + authority_cert_issuer=None, + authority_cert_serial_number=None, + ) + + @classmethod + def from_issuer_subject_key_identifier( + cls, ski: SubjectKeyIdentifier + ) -> AuthorityKeyIdentifier: + return cls( + key_identifier=ski.digest, + authority_cert_issuer=None, + authority_cert_serial_number=None, + ) + + def __repr__(self) -> str: + return ( + f"" + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, AuthorityKeyIdentifier): + return NotImplemented + + return ( + self.key_identifier == other.key_identifier + and self.authority_cert_issuer == other.authority_cert_issuer + and self.authority_cert_serial_number + == other.authority_cert_serial_number + ) + + def __hash__(self) -> int: + if self.authority_cert_issuer is None: + aci = None + else: + aci = tuple(self.authority_cert_issuer) + return hash( + (self.key_identifier, aci, self.authority_cert_serial_number) + ) + + @property + def key_identifier(self) -> bytes | None: + return self._key_identifier + + @property + def authority_cert_issuer( + self, + ) -> list[GeneralName] | None: + return self._authority_cert_issuer + + @property + def authority_cert_serial_number(self) -> int | None: + return self._authority_cert_serial_number + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class SubjectKeyIdentifier(ExtensionType): + oid = ExtensionOID.SUBJECT_KEY_IDENTIFIER + + def __init__(self, digest: bytes) -> None: + self._digest = digest + + @classmethod + def from_public_key( + cls, public_key: CertificatePublicKeyTypes + ) -> SubjectKeyIdentifier: + return cls(_key_identifier_from_public_key(public_key)) + + @property + def digest(self) -> bytes: + return self._digest + + @property + def key_identifier(self) -> bytes: + return self._digest + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SubjectKeyIdentifier): + return NotImplemented + + return constant_time.bytes_eq(self.digest, other.digest) + + def __hash__(self) -> int: + return hash(self.digest) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class AuthorityInformationAccess(ExtensionType): + oid = ExtensionOID.AUTHORITY_INFORMATION_ACCESS + + def __init__(self, descriptions: Iterable[AccessDescription]) -> None: + descriptions = list(descriptions) + if not all(isinstance(x, AccessDescription) for x in descriptions): + raise TypeError( + "Every item in the descriptions list must be an " + "AccessDescription" + ) + + self._descriptions = descriptions + + __len__, __iter__, __getitem__ = _make_sequence_methods("_descriptions") + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, AuthorityInformationAccess): + return NotImplemented + + return self._descriptions == other._descriptions + + def __hash__(self) -> int: + return hash(tuple(self._descriptions)) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class SubjectInformationAccess(ExtensionType): + oid = ExtensionOID.SUBJECT_INFORMATION_ACCESS + + def __init__(self, descriptions: Iterable[AccessDescription]) -> None: + descriptions = list(descriptions) + if not all(isinstance(x, AccessDescription) for x in descriptions): + raise TypeError( + "Every item in the descriptions list must be an " + "AccessDescription" + ) + + self._descriptions = descriptions + + __len__, __iter__, __getitem__ = _make_sequence_methods("_descriptions") + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SubjectInformationAccess): + return NotImplemented + + return self._descriptions == other._descriptions + + def __hash__(self) -> int: + return hash(tuple(self._descriptions)) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class AccessDescription: + def __init__( + self, access_method: ObjectIdentifier, access_location: GeneralName + ) -> None: + if not isinstance(access_method, ObjectIdentifier): + raise TypeError("access_method must be an ObjectIdentifier") + + if not isinstance(access_location, GeneralName): + raise TypeError("access_location must be a GeneralName") + + self._access_method = access_method + self._access_location = access_location + + def __repr__(self) -> str: + return ( + f"" + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, AccessDescription): + return NotImplemented + + return ( + self.access_method == other.access_method + and self.access_location == other.access_location + ) + + def __hash__(self) -> int: + return hash((self.access_method, self.access_location)) + + @property + def access_method(self) -> ObjectIdentifier: + return self._access_method + + @property + def access_location(self) -> GeneralName: + return self._access_location + + +class BasicConstraints(ExtensionType): + oid = ExtensionOID.BASIC_CONSTRAINTS + + def __init__(self, ca: bool, path_length: int | None) -> None: + if not isinstance(ca, bool): + raise TypeError("ca must be a boolean value") + + if path_length is not None and not ca: + raise ValueError("path_length must be None when ca is False") + + if path_length is not None and ( + not isinstance(path_length, int) or path_length < 0 + ): + raise TypeError( + "path_length must be a non-negative integer or None" + ) + + self._ca = ca + self._path_length = path_length + + @property + def ca(self) -> bool: + return self._ca + + @property + def path_length(self) -> int | None: + return self._path_length + + def __repr__(self) -> str: + return ( + f"" + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, BasicConstraints): + return NotImplemented + + return self.ca == other.ca and self.path_length == other.path_length + + def __hash__(self) -> int: + return hash((self.ca, self.path_length)) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class DeltaCRLIndicator(ExtensionType): + oid = ExtensionOID.DELTA_CRL_INDICATOR + + def __init__(self, crl_number: int) -> None: + if not isinstance(crl_number, int): + raise TypeError("crl_number must be an integer") + + self._crl_number = crl_number + + @property + def crl_number(self) -> int: + return self._crl_number + + def __eq__(self, other: object) -> bool: + if not isinstance(other, DeltaCRLIndicator): + return NotImplemented + + return self.crl_number == other.crl_number + + def __hash__(self) -> int: + return hash(self.crl_number) + + def __repr__(self) -> str: + return f"" + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class CRLDistributionPoints(ExtensionType): + oid = ExtensionOID.CRL_DISTRIBUTION_POINTS + + def __init__( + self, distribution_points: Iterable[DistributionPoint] + ) -> None: + distribution_points = list(distribution_points) + if not all( + isinstance(x, DistributionPoint) for x in distribution_points + ): + raise TypeError( + "distribution_points must be a list of DistributionPoint " + "objects" + ) + + self._distribution_points = distribution_points + + __len__, __iter__, __getitem__ = _make_sequence_methods( + "_distribution_points" + ) + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, CRLDistributionPoints): + return NotImplemented + + return self._distribution_points == other._distribution_points + + def __hash__(self) -> int: + return hash(tuple(self._distribution_points)) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class FreshestCRL(ExtensionType): + oid = ExtensionOID.FRESHEST_CRL + + def __init__( + self, distribution_points: Iterable[DistributionPoint] + ) -> None: + distribution_points = list(distribution_points) + if not all( + isinstance(x, DistributionPoint) for x in distribution_points + ): + raise TypeError( + "distribution_points must be a list of DistributionPoint " + "objects" + ) + + self._distribution_points = distribution_points + + __len__, __iter__, __getitem__ = _make_sequence_methods( + "_distribution_points" + ) + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, FreshestCRL): + return NotImplemented + + return self._distribution_points == other._distribution_points + + def __hash__(self) -> int: + return hash(tuple(self._distribution_points)) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class DistributionPoint: + def __init__( + self, + full_name: Iterable[GeneralName] | None, + relative_name: RelativeDistinguishedName | None, + reasons: frozenset[ReasonFlags] | None, + crl_issuer: Iterable[GeneralName] | None, + ) -> None: + if full_name and relative_name: + raise ValueError( + "You cannot provide both full_name and relative_name, at " + "least one must be None." + ) + if not full_name and not relative_name and not crl_issuer: + raise ValueError( + "Either full_name, relative_name or crl_issuer must be " + "provided." + ) + + if full_name is not None: + full_name = list(full_name) + if not all(isinstance(x, GeneralName) for x in full_name): + raise TypeError( + "full_name must be a list of GeneralName objects" + ) + + if relative_name: + if not isinstance(relative_name, RelativeDistinguishedName): + raise TypeError( + "relative_name must be a RelativeDistinguishedName" + ) + + if crl_issuer is not None: + crl_issuer = list(crl_issuer) + if not all(isinstance(x, GeneralName) for x in crl_issuer): + raise TypeError( + "crl_issuer must be None or a list of general names" + ) + + if reasons and ( + not isinstance(reasons, frozenset) + or not all(isinstance(x, ReasonFlags) for x in reasons) + ): + raise TypeError("reasons must be None or frozenset of ReasonFlags") + + if reasons and ( + ReasonFlags.unspecified in reasons + or ReasonFlags.remove_from_crl in reasons + ): + raise ValueError( + "unspecified and remove_from_crl are not valid reasons in a " + "DistributionPoint" + ) + + self._full_name = full_name + self._relative_name = relative_name + self._reasons = reasons + self._crl_issuer = crl_issuer + + def __repr__(self) -> str: + return ( + "".format(self) + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, DistributionPoint): + return NotImplemented + + return ( + self.full_name == other.full_name + and self.relative_name == other.relative_name + and self.reasons == other.reasons + and self.crl_issuer == other.crl_issuer + ) + + def __hash__(self) -> int: + if self.full_name is not None: + fn: tuple[GeneralName, ...] | None = tuple(self.full_name) + else: + fn = None + + if self.crl_issuer is not None: + crl_issuer: tuple[GeneralName, ...] | None = tuple(self.crl_issuer) + else: + crl_issuer = None + + return hash((fn, self.relative_name, self.reasons, crl_issuer)) + + @property + def full_name(self) -> list[GeneralName] | None: + return self._full_name + + @property + def relative_name(self) -> RelativeDistinguishedName | None: + return self._relative_name + + @property + def reasons(self) -> frozenset[ReasonFlags] | None: + return self._reasons + + @property + def crl_issuer(self) -> list[GeneralName] | None: + return self._crl_issuer + + +class ReasonFlags(utils.Enum): + unspecified = "unspecified" + key_compromise = "keyCompromise" + ca_compromise = "cACompromise" + affiliation_changed = "affiliationChanged" + superseded = "superseded" + cessation_of_operation = "cessationOfOperation" + certificate_hold = "certificateHold" + privilege_withdrawn = "privilegeWithdrawn" + aa_compromise = "aACompromise" + remove_from_crl = "removeFromCRL" + + +# These are distribution point bit string mappings. Not to be confused with +# CRLReason reason flags bit string mappings. +# ReasonFlags ::= BIT STRING { +# unused (0), +# keyCompromise (1), +# cACompromise (2), +# affiliationChanged (3), +# superseded (4), +# cessationOfOperation (5), +# certificateHold (6), +# privilegeWithdrawn (7), +# aACompromise (8) } +_REASON_BIT_MAPPING = { + 1: ReasonFlags.key_compromise, + 2: ReasonFlags.ca_compromise, + 3: ReasonFlags.affiliation_changed, + 4: ReasonFlags.superseded, + 5: ReasonFlags.cessation_of_operation, + 6: ReasonFlags.certificate_hold, + 7: ReasonFlags.privilege_withdrawn, + 8: ReasonFlags.aa_compromise, +} + +_CRLREASONFLAGS = { + ReasonFlags.key_compromise: 1, + ReasonFlags.ca_compromise: 2, + ReasonFlags.affiliation_changed: 3, + ReasonFlags.superseded: 4, + ReasonFlags.cessation_of_operation: 5, + ReasonFlags.certificate_hold: 6, + ReasonFlags.privilege_withdrawn: 7, + ReasonFlags.aa_compromise: 8, +} + +# CRLReason ::= ENUMERATED { +# unspecified (0), +# keyCompromise (1), +# cACompromise (2), +# affiliationChanged (3), +# superseded (4), +# cessationOfOperation (5), +# certificateHold (6), +# -- value 7 is not used +# removeFromCRL (8), +# privilegeWithdrawn (9), +# aACompromise (10) } +_CRL_ENTRY_REASON_ENUM_TO_CODE = { + ReasonFlags.unspecified: 0, + ReasonFlags.key_compromise: 1, + ReasonFlags.ca_compromise: 2, + ReasonFlags.affiliation_changed: 3, + ReasonFlags.superseded: 4, + ReasonFlags.cessation_of_operation: 5, + ReasonFlags.certificate_hold: 6, + ReasonFlags.remove_from_crl: 8, + ReasonFlags.privilege_withdrawn: 9, + ReasonFlags.aa_compromise: 10, +} + + +class PolicyConstraints(ExtensionType): + oid = ExtensionOID.POLICY_CONSTRAINTS + + def __init__( + self, + require_explicit_policy: int | None, + inhibit_policy_mapping: int | None, + ) -> None: + if require_explicit_policy is not None and not isinstance( + require_explicit_policy, int + ): + raise TypeError( + "require_explicit_policy must be a non-negative integer or " + "None" + ) + + if inhibit_policy_mapping is not None and not isinstance( + inhibit_policy_mapping, int + ): + raise TypeError( + "inhibit_policy_mapping must be a non-negative integer or None" + ) + + if inhibit_policy_mapping is None and require_explicit_policy is None: + raise ValueError( + "At least one of require_explicit_policy and " + "inhibit_policy_mapping must not be None" + ) + + self._require_explicit_policy = require_explicit_policy + self._inhibit_policy_mapping = inhibit_policy_mapping + + def __repr__(self) -> str: + return ( + "".format(self) + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, PolicyConstraints): + return NotImplemented + + return ( + self.require_explicit_policy == other.require_explicit_policy + and self.inhibit_policy_mapping == other.inhibit_policy_mapping + ) + + def __hash__(self) -> int: + return hash( + (self.require_explicit_policy, self.inhibit_policy_mapping) + ) + + @property + def require_explicit_policy(self) -> int | None: + return self._require_explicit_policy + + @property + def inhibit_policy_mapping(self) -> int | None: + return self._inhibit_policy_mapping + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class CertificatePolicies(ExtensionType): + oid = ExtensionOID.CERTIFICATE_POLICIES + + def __init__(self, policies: Iterable[PolicyInformation]) -> None: + policies = list(policies) + if not all(isinstance(x, PolicyInformation) for x in policies): + raise TypeError( + "Every item in the policies list must be a PolicyInformation" + ) + + self._policies = policies + + __len__, __iter__, __getitem__ = _make_sequence_methods("_policies") + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, CertificatePolicies): + return NotImplemented + + return self._policies == other._policies + + def __hash__(self) -> int: + return hash(tuple(self._policies)) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class PolicyInformation: + def __init__( + self, + policy_identifier: ObjectIdentifier, + policy_qualifiers: Iterable[str | UserNotice] | None, + ) -> None: + if not isinstance(policy_identifier, ObjectIdentifier): + raise TypeError("policy_identifier must be an ObjectIdentifier") + + self._policy_identifier = policy_identifier + + if policy_qualifiers is not None: + policy_qualifiers = list(policy_qualifiers) + if not all( + isinstance(x, (str, UserNotice)) for x in policy_qualifiers + ): + raise TypeError( + "policy_qualifiers must be a list of strings and/or " + "UserNotice objects or None" + ) + + self._policy_qualifiers = policy_qualifiers + + def __repr__(self) -> str: + return ( + f"" + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, PolicyInformation): + return NotImplemented + + return ( + self.policy_identifier == other.policy_identifier + and self.policy_qualifiers == other.policy_qualifiers + ) + + def __hash__(self) -> int: + if self.policy_qualifiers is not None: + pq = tuple(self.policy_qualifiers) + else: + pq = None + + return hash((self.policy_identifier, pq)) + + @property + def policy_identifier(self) -> ObjectIdentifier: + return self._policy_identifier + + @property + def policy_qualifiers( + self, + ) -> list[str | UserNotice] | None: + return self._policy_qualifiers + + +class UserNotice: + def __init__( + self, + notice_reference: NoticeReference | None, + explicit_text: str | None, + ) -> None: + if notice_reference and not isinstance( + notice_reference, NoticeReference + ): + raise TypeError( + "notice_reference must be None or a NoticeReference" + ) + + self._notice_reference = notice_reference + self._explicit_text = explicit_text + + def __repr__(self) -> str: + return ( + f"" + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, UserNotice): + return NotImplemented + + return ( + self.notice_reference == other.notice_reference + and self.explicit_text == other.explicit_text + ) + + def __hash__(self) -> int: + return hash((self.notice_reference, self.explicit_text)) + + @property + def notice_reference(self) -> NoticeReference | None: + return self._notice_reference + + @property + def explicit_text(self) -> str | None: + return self._explicit_text + + +class NoticeReference: + def __init__( + self, + organization: str | None, + notice_numbers: Iterable[int], + ) -> None: + self._organization = organization + notice_numbers = list(notice_numbers) + if not all(isinstance(x, int) for x in notice_numbers): + raise TypeError("notice_numbers must be a list of integers") + + self._notice_numbers = notice_numbers + + def __repr__(self) -> str: + return ( + f"" + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, NoticeReference): + return NotImplemented + + return ( + self.organization == other.organization + and self.notice_numbers == other.notice_numbers + ) + + def __hash__(self) -> int: + return hash((self.organization, tuple(self.notice_numbers))) + + @property + def organization(self) -> str | None: + return self._organization + + @property + def notice_numbers(self) -> list[int]: + return self._notice_numbers + + +class ExtendedKeyUsage(ExtensionType): + oid = ExtensionOID.EXTENDED_KEY_USAGE + + def __init__(self, usages: Iterable[ObjectIdentifier]) -> None: + usages = list(usages) + if not all(isinstance(x, ObjectIdentifier) for x in usages): + raise TypeError( + "Every item in the usages list must be an ObjectIdentifier" + ) + + self._usages = usages + + __len__, __iter__, __getitem__ = _make_sequence_methods("_usages") + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, ExtendedKeyUsage): + return NotImplemented + + return self._usages == other._usages + + def __hash__(self) -> int: + return hash(tuple(self._usages)) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class OCSPNoCheck(ExtensionType): + oid = ExtensionOID.OCSP_NO_CHECK + + def __eq__(self, other: object) -> bool: + if not isinstance(other, OCSPNoCheck): + return NotImplemented + + return True + + def __hash__(self) -> int: + return hash(OCSPNoCheck) + + def __repr__(self) -> str: + return "" + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class PrecertPoison(ExtensionType): + oid = ExtensionOID.PRECERT_POISON + + def __eq__(self, other: object) -> bool: + if not isinstance(other, PrecertPoison): + return NotImplemented + + return True + + def __hash__(self) -> int: + return hash(PrecertPoison) + + def __repr__(self) -> str: + return "" + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class TLSFeature(ExtensionType): + oid = ExtensionOID.TLS_FEATURE + + def __init__(self, features: Iterable[TLSFeatureType]) -> None: + features = list(features) + if ( + not all(isinstance(x, TLSFeatureType) for x in features) + or len(features) == 0 + ): + raise TypeError( + "features must be a list of elements from the TLSFeatureType " + "enum" + ) + + self._features = features + + __len__, __iter__, __getitem__ = _make_sequence_methods("_features") + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, TLSFeature): + return NotImplemented + + return self._features == other._features + + def __hash__(self) -> int: + return hash(tuple(self._features)) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class TLSFeatureType(utils.Enum): + # status_request is defined in RFC 6066 and is used for what is commonly + # called OCSP Must-Staple when present in the TLS Feature extension in an + # X.509 certificate. + status_request = 5 + # status_request_v2 is defined in RFC 6961 and allows multiple OCSP + # responses to be provided. It is not currently in use by clients or + # servers. + status_request_v2 = 17 + + +_TLS_FEATURE_TYPE_TO_ENUM = {x.value: x for x in TLSFeatureType} + + +class InhibitAnyPolicy(ExtensionType): + oid = ExtensionOID.INHIBIT_ANY_POLICY + + def __init__(self, skip_certs: int) -> None: + if not isinstance(skip_certs, int): + raise TypeError("skip_certs must be an integer") + + if skip_certs < 0: + raise ValueError("skip_certs must be a non-negative integer") + + self._skip_certs = skip_certs + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, InhibitAnyPolicy): + return NotImplemented + + return self.skip_certs == other.skip_certs + + def __hash__(self) -> int: + return hash(self.skip_certs) + + @property + def skip_certs(self) -> int: + return self._skip_certs + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class KeyUsage(ExtensionType): + oid = ExtensionOID.KEY_USAGE + + def __init__( + self, + digital_signature: bool, + content_commitment: bool, + key_encipherment: bool, + data_encipherment: bool, + key_agreement: bool, + key_cert_sign: bool, + crl_sign: bool, + encipher_only: bool, + decipher_only: bool, + ) -> None: + if not key_agreement and (encipher_only or decipher_only): + raise ValueError( + "encipher_only and decipher_only can only be true when " + "key_agreement is true" + ) + + self._digital_signature = digital_signature + self._content_commitment = content_commitment + self._key_encipherment = key_encipherment + self._data_encipherment = data_encipherment + self._key_agreement = key_agreement + self._key_cert_sign = key_cert_sign + self._crl_sign = crl_sign + self._encipher_only = encipher_only + self._decipher_only = decipher_only + + @property + def digital_signature(self) -> bool: + return self._digital_signature + + @property + def content_commitment(self) -> bool: + return self._content_commitment + + @property + def key_encipherment(self) -> bool: + return self._key_encipherment + + @property + def data_encipherment(self) -> bool: + return self._data_encipherment + + @property + def key_agreement(self) -> bool: + return self._key_agreement + + @property + def key_cert_sign(self) -> bool: + return self._key_cert_sign + + @property + def crl_sign(self) -> bool: + return self._crl_sign + + @property + def encipher_only(self) -> bool: + if not self.key_agreement: + raise ValueError( + "encipher_only is undefined unless key_agreement is true" + ) + else: + return self._encipher_only + + @property + def decipher_only(self) -> bool: + if not self.key_agreement: + raise ValueError( + "decipher_only is undefined unless key_agreement is true" + ) + else: + return self._decipher_only + + def __repr__(self) -> str: + try: + encipher_only = self.encipher_only + decipher_only = self.decipher_only + except ValueError: + # Users found None confusing because even though encipher/decipher + # have no meaning unless key_agreement is true, to construct an + # instance of the class you still need to pass False. + encipher_only = False + decipher_only = False + + return ( + f"" + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, KeyUsage): + return NotImplemented + + return ( + self.digital_signature == other.digital_signature + and self.content_commitment == other.content_commitment + and self.key_encipherment == other.key_encipherment + and self.data_encipherment == other.data_encipherment + and self.key_agreement == other.key_agreement + and self.key_cert_sign == other.key_cert_sign + and self.crl_sign == other.crl_sign + and self._encipher_only == other._encipher_only + and self._decipher_only == other._decipher_only + ) + + def __hash__(self) -> int: + return hash( + ( + self.digital_signature, + self.content_commitment, + self.key_encipherment, + self.data_encipherment, + self.key_agreement, + self.key_cert_sign, + self.crl_sign, + self._encipher_only, + self._decipher_only, + ) + ) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class PrivateKeyUsagePeriod(ExtensionType): + oid = ExtensionOID.PRIVATE_KEY_USAGE_PERIOD + + def __init__( + self, + not_before: datetime.datetime | None, + not_after: datetime.datetime | None, + ) -> None: + if ( + not isinstance(not_before, datetime.datetime) + and not_before is not None + ): + raise TypeError("not_before must be a datetime.datetime or None") + + if ( + not isinstance(not_after, datetime.datetime) + and not_after is not None + ): + raise TypeError("not_after must be a datetime.datetime or None") + + if not_before is None and not_after is None: + raise ValueError( + "At least one of not_before and not_after must not be None" + ) + + if ( + not_before is not None + and not_after is not None + and not_before > not_after + ): + raise ValueError("not_before must be before not_after") + + self._not_before = not_before + self._not_after = not_after + + @property + def not_before(self) -> datetime.datetime | None: + return self._not_before + + @property + def not_after(self) -> datetime.datetime | None: + return self._not_after + + def __repr__(self) -> str: + return ( + f"" + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, PrivateKeyUsagePeriod): + return NotImplemented + + return ( + self.not_before == other.not_before + and self.not_after == other.not_after + ) + + def __hash__(self) -> int: + return hash((self.not_before, self.not_after)) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class NameConstraints(ExtensionType): + oid = ExtensionOID.NAME_CONSTRAINTS + + def __init__( + self, + permitted_subtrees: Iterable[GeneralName] | None, + excluded_subtrees: Iterable[GeneralName] | None, + ) -> None: + if permitted_subtrees is not None: + permitted_subtrees = list(permitted_subtrees) + if not permitted_subtrees: + raise ValueError( + "permitted_subtrees must be a non-empty list or None" + ) + if not all(isinstance(x, GeneralName) for x in permitted_subtrees): + raise TypeError( + "permitted_subtrees must be a list of GeneralName objects " + "or None" + ) + + self._validate_tree(permitted_subtrees) + + if excluded_subtrees is not None: + excluded_subtrees = list(excluded_subtrees) + if not excluded_subtrees: + raise ValueError( + "excluded_subtrees must be a non-empty list or None" + ) + if not all(isinstance(x, GeneralName) for x in excluded_subtrees): + raise TypeError( + "excluded_subtrees must be a list of GeneralName objects " + "or None" + ) + + self._validate_tree(excluded_subtrees) + + if permitted_subtrees is None and excluded_subtrees is None: + raise ValueError( + "At least one of permitted_subtrees and excluded_subtrees " + "must not be None" + ) + + self._permitted_subtrees = permitted_subtrees + self._excluded_subtrees = excluded_subtrees + + def __eq__(self, other: object) -> bool: + if not isinstance(other, NameConstraints): + return NotImplemented + + return ( + self.excluded_subtrees == other.excluded_subtrees + and self.permitted_subtrees == other.permitted_subtrees + ) + + def _validate_tree(self, tree: Iterable[GeneralName]) -> None: + self._validate_ip_name(tree) + self._validate_dns_name(tree) + + def _validate_ip_name(self, tree: Iterable[GeneralName]) -> None: + if any( + isinstance(name, IPAddress) + and not isinstance( + name.value, (ipaddress.IPv4Network, ipaddress.IPv6Network) + ) + for name in tree + ): + raise TypeError( + "IPAddress name constraints must be an IPv4Network or" + " IPv6Network object" + ) + + def _validate_dns_name(self, tree: Iterable[GeneralName]) -> None: + if any( + isinstance(name, DNSName) and "*" in name.value for name in tree + ): + raise ValueError( + "DNSName name constraints must not contain the '*' wildcard" + " character" + ) + + def __repr__(self) -> str: + return ( + f"" + ) + + def __hash__(self) -> int: + if self.permitted_subtrees is not None: + ps: tuple[GeneralName, ...] | None = tuple(self.permitted_subtrees) + else: + ps = None + + if self.excluded_subtrees is not None: + es: tuple[GeneralName, ...] | None = tuple(self.excluded_subtrees) + else: + es = None + + return hash((ps, es)) + + @property + def permitted_subtrees( + self, + ) -> list[GeneralName] | None: + return self._permitted_subtrees + + @property + def excluded_subtrees( + self, + ) -> list[GeneralName] | None: + return self._excluded_subtrees + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class Extension(typing.Generic[ExtensionTypeVar]): + def __init__( + self, oid: ObjectIdentifier, critical: bool, value: ExtensionTypeVar + ) -> None: + if not isinstance(oid, ObjectIdentifier): + raise TypeError( + "oid argument must be an ObjectIdentifier instance." + ) + + if not isinstance(critical, bool): + raise TypeError("critical must be a boolean value") + + self._oid = oid + self._critical = critical + self._value = value + + @property + def oid(self) -> ObjectIdentifier: + return self._oid + + @property + def critical(self) -> bool: + return self._critical + + @property + def value(self) -> ExtensionTypeVar: + return self._value + + def __repr__(self) -> str: + return ( + f"" + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Extension): + return NotImplemented + + return ( + self.oid == other.oid + and self.critical == other.critical + and self.value == other.value + ) + + def __hash__(self) -> int: + return hash((self.oid, self.critical, self.value)) + + +class GeneralNames: + def __init__(self, general_names: Iterable[GeneralName]) -> None: + general_names = list(general_names) + if not all(isinstance(x, GeneralName) for x in general_names): + raise TypeError( + "Every item in the general_names list must be an " + "object conforming to the GeneralName interface" + ) + + self._general_names = general_names + + __len__, __iter__, __getitem__ = _make_sequence_methods("_general_names") + + @typing.overload + def get_values_for_type( + self, + type: type[DNSName] + | type[UniformResourceIdentifier] + | type[RFC822Name], + ) -> list[str]: ... + + @typing.overload + def get_values_for_type( + self, + type: type[DirectoryName], + ) -> list[Name]: ... + + @typing.overload + def get_values_for_type( + self, + type: type[RegisteredID], + ) -> list[ObjectIdentifier]: ... + + @typing.overload + def get_values_for_type( + self, type: type[IPAddress] + ) -> list[_IPAddressTypes]: ... + + @typing.overload + def get_values_for_type( + self, type: type[OtherName] + ) -> list[OtherName]: ... + + def get_values_for_type( + self, + type: type[DNSName] + | type[DirectoryName] + | type[IPAddress] + | type[OtherName] + | type[RFC822Name] + | type[RegisteredID] + | type[UniformResourceIdentifier], + ) -> ( + list[_IPAddressTypes] + | list[str] + | list[OtherName] + | list[Name] + | list[ObjectIdentifier] + ): + # Return the value of each GeneralName, except for OtherName instances + # which we return directly because it has two important properties not + # just one value. + objs = (i for i in self if isinstance(i, type)) + if type != OtherName: + return [i.value for i in objs] # type: ignore[return-value,unused-ignore] + return list(objs) # type: ignore[return-value,unused-ignore] + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, GeneralNames): + return NotImplemented + + return self._general_names == other._general_names + + def __hash__(self) -> int: + return hash(tuple(self._general_names)) + + +class SubjectAlternativeName(ExtensionType): + oid = ExtensionOID.SUBJECT_ALTERNATIVE_NAME + + def __init__(self, general_names: Iterable[GeneralName]) -> None: + self._general_names = GeneralNames(general_names) + + __len__, __iter__, __getitem__ = _make_sequence_methods("_general_names") + + @typing.overload + def get_values_for_type( + self, + type: type[DNSName] + | type[UniformResourceIdentifier] + | type[RFC822Name], + ) -> list[str]: ... + + @typing.overload + def get_values_for_type( + self, + type: type[DirectoryName], + ) -> list[Name]: ... + + @typing.overload + def get_values_for_type( + self, + type: type[RegisteredID], + ) -> list[ObjectIdentifier]: ... + + @typing.overload + def get_values_for_type( + self, type: type[IPAddress] + ) -> list[_IPAddressTypes]: ... + + @typing.overload + def get_values_for_type( + self, type: type[OtherName] + ) -> list[OtherName]: ... + + def get_values_for_type( + self, + type: type[DNSName] + | type[DirectoryName] + | type[IPAddress] + | type[OtherName] + | type[RFC822Name] + | type[RegisteredID] + | type[UniformResourceIdentifier], + ) -> ( + list[_IPAddressTypes] + | list[str] + | list[OtherName] + | list[Name] + | list[ObjectIdentifier] + ): + return self._general_names.get_values_for_type(type) + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SubjectAlternativeName): + return NotImplemented + + return self._general_names == other._general_names + + def __hash__(self) -> int: + return hash(self._general_names) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class IssuerAlternativeName(ExtensionType): + oid = ExtensionOID.ISSUER_ALTERNATIVE_NAME + + def __init__(self, general_names: Iterable[GeneralName]) -> None: + self._general_names = GeneralNames(general_names) + + __len__, __iter__, __getitem__ = _make_sequence_methods("_general_names") + + @typing.overload + def get_values_for_type( + self, + type: type[DNSName] + | type[UniformResourceIdentifier] + | type[RFC822Name], + ) -> list[str]: ... + + @typing.overload + def get_values_for_type( + self, + type: type[DirectoryName], + ) -> list[Name]: ... + + @typing.overload + def get_values_for_type( + self, + type: type[RegisteredID], + ) -> list[ObjectIdentifier]: ... + + @typing.overload + def get_values_for_type( + self, type: type[IPAddress] + ) -> list[_IPAddressTypes]: ... + + @typing.overload + def get_values_for_type( + self, type: type[OtherName] + ) -> list[OtherName]: ... + + def get_values_for_type( + self, + type: type[DNSName] + | type[DirectoryName] + | type[IPAddress] + | type[OtherName] + | type[RFC822Name] + | type[RegisteredID] + | type[UniformResourceIdentifier], + ) -> ( + list[_IPAddressTypes] + | list[str] + | list[OtherName] + | list[Name] + | list[ObjectIdentifier] + ): + return self._general_names.get_values_for_type(type) + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, IssuerAlternativeName): + return NotImplemented + + return self._general_names == other._general_names + + def __hash__(self) -> int: + return hash(self._general_names) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class CertificateIssuer(ExtensionType): + oid = CRLEntryExtensionOID.CERTIFICATE_ISSUER + + def __init__(self, general_names: Iterable[GeneralName]) -> None: + self._general_names = GeneralNames(general_names) + + __len__, __iter__, __getitem__ = _make_sequence_methods("_general_names") + + @typing.overload + def get_values_for_type( + self, + type: type[DNSName] + | type[UniformResourceIdentifier] + | type[RFC822Name], + ) -> list[str]: ... + + @typing.overload + def get_values_for_type( + self, + type: type[DirectoryName], + ) -> list[Name]: ... + + @typing.overload + def get_values_for_type( + self, + type: type[RegisteredID], + ) -> list[ObjectIdentifier]: ... + + @typing.overload + def get_values_for_type( + self, type: type[IPAddress] + ) -> list[_IPAddressTypes]: ... + + @typing.overload + def get_values_for_type( + self, type: type[OtherName] + ) -> list[OtherName]: ... + + def get_values_for_type( + self, + type: type[DNSName] + | type[DirectoryName] + | type[IPAddress] + | type[OtherName] + | type[RFC822Name] + | type[RegisteredID] + | type[UniformResourceIdentifier], + ) -> ( + list[_IPAddressTypes] + | list[str] + | list[OtherName] + | list[Name] + | list[ObjectIdentifier] + ): + return self._general_names.get_values_for_type(type) + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, CertificateIssuer): + return NotImplemented + + return self._general_names == other._general_names + + def __hash__(self) -> int: + return hash(self._general_names) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class CRLReason(ExtensionType): + oid = CRLEntryExtensionOID.CRL_REASON + + def __init__(self, reason: ReasonFlags) -> None: + if not isinstance(reason, ReasonFlags): + raise TypeError("reason must be an element from ReasonFlags") + + self._reason = reason + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, CRLReason): + return NotImplemented + + return self.reason == other.reason + + def __hash__(self) -> int: + return hash(self.reason) + + @property + def reason(self) -> ReasonFlags: + return self._reason + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class InvalidityDate(ExtensionType): + oid = CRLEntryExtensionOID.INVALIDITY_DATE + + def __init__(self, invalidity_date: datetime.datetime) -> None: + if not isinstance(invalidity_date, datetime.datetime): + raise TypeError("invalidity_date must be a datetime.datetime") + + self._invalidity_date = invalidity_date + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, InvalidityDate): + return NotImplemented + + return self.invalidity_date == other.invalidity_date + + def __hash__(self) -> int: + return hash(self.invalidity_date) + + @property + def invalidity_date(self) -> datetime.datetime: + return self._invalidity_date + + @property + def invalidity_date_utc(self) -> datetime.datetime: + if self._invalidity_date.tzinfo is None: + return self._invalidity_date.replace(tzinfo=datetime.timezone.utc) + else: + return self._invalidity_date.astimezone(tz=datetime.timezone.utc) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class PrecertificateSignedCertificateTimestamps(ExtensionType): + oid = ExtensionOID.PRECERT_SIGNED_CERTIFICATE_TIMESTAMPS + + def __init__( + self, + signed_certificate_timestamps: Iterable[SignedCertificateTimestamp], + ) -> None: + signed_certificate_timestamps = list(signed_certificate_timestamps) + if not all( + isinstance(sct, SignedCertificateTimestamp) + for sct in signed_certificate_timestamps + ): + raise TypeError( + "Every item in the signed_certificate_timestamps list must be " + "a SignedCertificateTimestamp" + ) + self._signed_certificate_timestamps = signed_certificate_timestamps + + __len__, __iter__, __getitem__ = _make_sequence_methods( + "_signed_certificate_timestamps" + ) + + def __repr__(self) -> str: + return f"" + + def __hash__(self) -> int: + return hash(tuple(self._signed_certificate_timestamps)) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, PrecertificateSignedCertificateTimestamps): + return NotImplemented + + return ( + self._signed_certificate_timestamps + == other._signed_certificate_timestamps + ) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class SignedCertificateTimestamps(ExtensionType): + oid = ExtensionOID.SIGNED_CERTIFICATE_TIMESTAMPS + + def __init__( + self, + signed_certificate_timestamps: Iterable[SignedCertificateTimestamp], + ) -> None: + signed_certificate_timestamps = list(signed_certificate_timestamps) + if not all( + isinstance(sct, SignedCertificateTimestamp) + for sct in signed_certificate_timestamps + ): + raise TypeError( + "Every item in the signed_certificate_timestamps list must be " + "a SignedCertificateTimestamp" + ) + self._signed_certificate_timestamps = signed_certificate_timestamps + + __len__, __iter__, __getitem__ = _make_sequence_methods( + "_signed_certificate_timestamps" + ) + + def __repr__(self) -> str: + return f"" + + def __hash__(self) -> int: + return hash(tuple(self._signed_certificate_timestamps)) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SignedCertificateTimestamps): + return NotImplemented + + return ( + self._signed_certificate_timestamps + == other._signed_certificate_timestamps + ) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class OCSPNonce(ExtensionType): + oid = OCSPExtensionOID.NONCE + + def __init__(self, nonce: bytes) -> None: + if not isinstance(nonce, bytes): + raise TypeError("nonce must be bytes") + + self._nonce = nonce + + def __eq__(self, other: object) -> bool: + if not isinstance(other, OCSPNonce): + return NotImplemented + + return self.nonce == other.nonce + + def __hash__(self) -> int: + return hash(self.nonce) + + def __repr__(self) -> str: + return f"" + + @property + def nonce(self) -> bytes: + return self._nonce + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class OCSPAcceptableResponses(ExtensionType): + oid = OCSPExtensionOID.ACCEPTABLE_RESPONSES + + def __init__(self, responses: Iterable[ObjectIdentifier]) -> None: + responses = list(responses) + if any(not isinstance(r, ObjectIdentifier) for r in responses): + raise TypeError("All responses must be ObjectIdentifiers") + + self._responses = responses + + def __eq__(self, other: object) -> bool: + if not isinstance(other, OCSPAcceptableResponses): + return NotImplemented + + return self._responses == other._responses + + def __hash__(self) -> int: + return hash(tuple(self._responses)) + + def __repr__(self) -> str: + return f"" + + def __iter__(self) -> Iterator[ObjectIdentifier]: + return iter(self._responses) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class IssuingDistributionPoint(ExtensionType): + oid = ExtensionOID.ISSUING_DISTRIBUTION_POINT + + def __init__( + self, + full_name: Iterable[GeneralName] | None, + relative_name: RelativeDistinguishedName | None, + only_contains_user_certs: bool, + only_contains_ca_certs: bool, + only_some_reasons: frozenset[ReasonFlags] | None, + indirect_crl: bool, + only_contains_attribute_certs: bool, + ) -> None: + if full_name is not None: + full_name = list(full_name) + + if only_some_reasons and ( + not isinstance(only_some_reasons, frozenset) + or not all(isinstance(x, ReasonFlags) for x in only_some_reasons) + ): + raise TypeError( + "only_some_reasons must be None or frozenset of ReasonFlags" + ) + + if only_some_reasons and ( + ReasonFlags.unspecified in only_some_reasons + or ReasonFlags.remove_from_crl in only_some_reasons + ): + raise ValueError( + "unspecified and remove_from_crl are not valid reasons in an " + "IssuingDistributionPoint" + ) + + if not ( + isinstance(only_contains_user_certs, bool) + and isinstance(only_contains_ca_certs, bool) + and isinstance(indirect_crl, bool) + and isinstance(only_contains_attribute_certs, bool) + ): + raise TypeError( + "only_contains_user_certs, only_contains_ca_certs, " + "indirect_crl and only_contains_attribute_certs " + "must all be boolean." + ) + + # Per RFC5280 Section 5.2.5, the Issuing Distribution Point extension + # in a CRL can have only one of onlyContainsUserCerts, + # onlyContainsCACerts, onlyContainsAttributeCerts set to TRUE. + crl_constraints = [ + only_contains_user_certs, + only_contains_ca_certs, + only_contains_attribute_certs, + ] + + if len([x for x in crl_constraints if x]) > 1: + raise ValueError( + "Only one of the following can be set to True: " + "only_contains_user_certs, only_contains_ca_certs, " + "only_contains_attribute_certs" + ) + + if not any( + [ + only_contains_user_certs, + only_contains_ca_certs, + indirect_crl, + only_contains_attribute_certs, + full_name, + relative_name, + only_some_reasons, + ] + ): + raise ValueError( + "Cannot create empty extension: " + "if only_contains_user_certs, only_contains_ca_certs, " + "indirect_crl, and only_contains_attribute_certs are all False" + ", then either full_name, relative_name, or only_some_reasons " + "must have a value." + ) + + self._only_contains_user_certs = only_contains_user_certs + self._only_contains_ca_certs = only_contains_ca_certs + self._indirect_crl = indirect_crl + self._only_contains_attribute_certs = only_contains_attribute_certs + self._only_some_reasons = only_some_reasons + self._full_name = full_name + self._relative_name = relative_name + + def __repr__(self) -> str: + return ( + f"" + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, IssuingDistributionPoint): + return NotImplemented + + return ( + self.full_name == other.full_name + and self.relative_name == other.relative_name + and self.only_contains_user_certs == other.only_contains_user_certs + and self.only_contains_ca_certs == other.only_contains_ca_certs + and self.only_some_reasons == other.only_some_reasons + and self.indirect_crl == other.indirect_crl + and self.only_contains_attribute_certs + == other.only_contains_attribute_certs + ) + + def __hash__(self) -> int: + if self.full_name is not None: + full_name: tuple[GeneralName, ...] | None = tuple(self.full_name) + else: + full_name = None + + return hash( + ( + full_name, + self.relative_name, + self.only_contains_user_certs, + self.only_contains_ca_certs, + self.only_some_reasons, + self.indirect_crl, + self.only_contains_attribute_certs, + ) + ) + + @property + def full_name(self) -> list[GeneralName] | None: + return self._full_name + + @property + def relative_name(self) -> RelativeDistinguishedName | None: + return self._relative_name + + @property + def only_contains_user_certs(self) -> bool: + return self._only_contains_user_certs + + @property + def only_contains_ca_certs(self) -> bool: + return self._only_contains_ca_certs + + @property + def only_some_reasons( + self, + ) -> frozenset[ReasonFlags] | None: + return self._only_some_reasons + + @property + def indirect_crl(self) -> bool: + return self._indirect_crl + + @property + def only_contains_attribute_certs(self) -> bool: + return self._only_contains_attribute_certs + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class MSCertificateTemplate(ExtensionType): + oid = ExtensionOID.MS_CERTIFICATE_TEMPLATE + + def __init__( + self, + template_id: ObjectIdentifier, + major_version: int | None, + minor_version: int | None, + ) -> None: + if not isinstance(template_id, ObjectIdentifier): + raise TypeError("oid must be an ObjectIdentifier") + self._template_id = template_id + if ( + major_version is not None and not isinstance(major_version, int) + ) or ( + minor_version is not None and not isinstance(minor_version, int) + ): + raise TypeError( + "major_version and minor_version must be integers or None" + ) + self._major_version = major_version + self._minor_version = minor_version + + @property + def template_id(self) -> ObjectIdentifier: + return self._template_id + + @property + def major_version(self) -> int | None: + return self._major_version + + @property + def minor_version(self) -> int | None: + return self._minor_version + + def __repr__(self) -> str: + return ( + f"" + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, MSCertificateTemplate): + return NotImplemented + + return ( + self.template_id == other.template_id + and self.major_version == other.major_version + and self.minor_version == other.minor_version + ) + + def __hash__(self) -> int: + return hash((self.template_id, self.major_version, self.minor_version)) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class NamingAuthority: + def __init__( + self, + id: ObjectIdentifier | None, + url: str | None, + text: str | None, + ) -> None: + if id is not None and not isinstance(id, ObjectIdentifier): + raise TypeError("id must be an ObjectIdentifier") + + if url is not None and not isinstance(url, str): + raise TypeError("url must be a str") + + if text is not None and not isinstance(text, str): + raise TypeError("text must be a str") + + self._id = id + self._url = url + self._text = text + + @property + def id(self) -> ObjectIdentifier | None: + return self._id + + @property + def url(self) -> str | None: + return self._url + + @property + def text(self) -> str | None: + return self._text + + def __repr__(self) -> str: + return ( + f"" + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, NamingAuthority): + return NotImplemented + + return ( + self.id == other.id + and self.url == other.url + and self.text == other.text + ) + + def __hash__(self) -> int: + return hash( + ( + self.id, + self.url, + self.text, + ) + ) + + +class ProfessionInfo: + def __init__( + self, + naming_authority: NamingAuthority | None, + profession_items: Iterable[str], + profession_oids: Iterable[ObjectIdentifier] | None, + registration_number: str | None, + add_profession_info: bytes | None, + ) -> None: + if naming_authority is not None and not isinstance( + naming_authority, NamingAuthority + ): + raise TypeError("naming_authority must be a NamingAuthority") + + profession_items = list(profession_items) + if not all(isinstance(item, str) for item in profession_items): + raise TypeError( + "Every item in the profession_items list must be a str" + ) + + if profession_oids is not None: + profession_oids = list(profession_oids) + if not all( + isinstance(oid, ObjectIdentifier) for oid in profession_oids + ): + raise TypeError( + "Every item in the profession_oids list must be an " + "ObjectIdentifier" + ) + + if registration_number is not None and not isinstance( + registration_number, str + ): + raise TypeError("registration_number must be a str") + + if add_profession_info is not None and not isinstance( + add_profession_info, bytes + ): + raise TypeError("add_profession_info must be bytes") + + self._naming_authority = naming_authority + self._profession_items = profession_items + self._profession_oids = profession_oids + self._registration_number = registration_number + self._add_profession_info = add_profession_info + + @property + def naming_authority(self) -> NamingAuthority | None: + return self._naming_authority + + @property + def profession_items(self) -> list[str]: + return self._profession_items + + @property + def profession_oids(self) -> list[ObjectIdentifier] | None: + return self._profession_oids + + @property + def registration_number(self) -> str | None: + return self._registration_number + + @property + def add_profession_info(self) -> bytes | None: + return self._add_profession_info + + def __repr__(self) -> str: + return ( + f"" + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, ProfessionInfo): + return NotImplemented + + return ( + self.naming_authority == other.naming_authority + and self.profession_items == other.profession_items + and self.profession_oids == other.profession_oids + and self.registration_number == other.registration_number + and self.add_profession_info == other.add_profession_info + ) + + def __hash__(self) -> int: + if self.profession_oids is not None: + profession_oids = tuple(self.profession_oids) + else: + profession_oids = None + return hash( + ( + self.naming_authority, + tuple(self.profession_items), + profession_oids, + self.registration_number, + self.add_profession_info, + ) + ) + + +class Admission: + def __init__( + self, + admission_authority: GeneralName | None, + naming_authority: NamingAuthority | None, + profession_infos: Iterable[ProfessionInfo], + ) -> None: + if admission_authority is not None and not isinstance( + admission_authority, GeneralName + ): + raise TypeError("admission_authority must be a GeneralName") + + if naming_authority is not None and not isinstance( + naming_authority, NamingAuthority + ): + raise TypeError("naming_authority must be a NamingAuthority") + + profession_infos = list(profession_infos) + if not all( + isinstance(info, ProfessionInfo) for info in profession_infos + ): + raise TypeError( + "Every item in the profession_infos list must be a " + "ProfessionInfo" + ) + + self._admission_authority = admission_authority + self._naming_authority = naming_authority + self._profession_infos = profession_infos + + @property + def admission_authority(self) -> GeneralName | None: + return self._admission_authority + + @property + def naming_authority(self) -> NamingAuthority | None: + return self._naming_authority + + @property + def profession_infos(self) -> list[ProfessionInfo]: + return self._profession_infos + + def __repr__(self) -> str: + return ( + f"" + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Admission): + return NotImplemented + + return ( + self.admission_authority == other.admission_authority + and self.naming_authority == other.naming_authority + and self.profession_infos == other.profession_infos + ) + + def __hash__(self) -> int: + return hash( + ( + self.admission_authority, + self.naming_authority, + tuple(self.profession_infos), + ) + ) + + +class Admissions(ExtensionType): + oid = ExtensionOID.ADMISSIONS + + def __init__( + self, + authority: GeneralName | None, + admissions: Iterable[Admission], + ) -> None: + if authority is not None and not isinstance(authority, GeneralName): + raise TypeError("authority must be a GeneralName") + + admissions = list(admissions) + if not all( + isinstance(admission, Admission) for admission in admissions + ): + raise TypeError( + "Every item in the contents_of_admissions list must be an " + "Admission" + ) + + self._authority = authority + self._admissions = admissions + + __len__, __iter__, __getitem__ = _make_sequence_methods("_admissions") + + @property + def authority(self) -> GeneralName | None: + return self._authority + + def __repr__(self) -> str: + return ( + f"" + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Admissions): + return NotImplemented + + return ( + self.authority == other.authority + and self._admissions == other._admissions + ) + + def __hash__(self) -> int: + return hash((self.authority, tuple(self._admissions))) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + +class UnrecognizedExtension(ExtensionType): + def __init__(self, oid: ObjectIdentifier, value: bytes) -> None: + if not isinstance(oid, ObjectIdentifier): + raise TypeError("oid must be an ObjectIdentifier") + self._oid = oid + self._value = value + + @property + def oid(self) -> ObjectIdentifier: # type: ignore[override] + return self._oid + + @property + def value(self) -> bytes: + return self._value + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, UnrecognizedExtension): + return NotImplemented + + return self.oid == other.oid and self.value == other.value + + def __hash__(self) -> int: + return hash((self.oid, self.value)) + + def public_bytes(self) -> bytes: + return self.value diff --git a/source/target-vendors/cp312/cryptography/x509/general_name.py b/source/target-vendors/cp312/cryptography/x509/general_name.py new file mode 100644 index 0000000..672f287 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/x509/general_name.py @@ -0,0 +1,281 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import abc +import ipaddress +import typing +from email.utils import parseaddr + +from cryptography.x509.name import Name +from cryptography.x509.oid import ObjectIdentifier + +_IPAddressTypes = typing.Union[ + ipaddress.IPv4Address, + ipaddress.IPv6Address, + ipaddress.IPv4Network, + ipaddress.IPv6Network, +] + + +class UnsupportedGeneralNameType(Exception): + pass + + +class GeneralName(metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def value(self) -> typing.Any: + """ + Return the value of the object + """ + + +class RFC822Name(GeneralName): + def __init__(self, value: str) -> None: + if isinstance(value, str): + try: + value.encode("ascii") + except UnicodeEncodeError: + raise ValueError( + "RFC822Name values should be passed as an A-label string. " + "This means unicode characters should be encoded via " + "a library like idna." + ) + else: + raise TypeError("value must be string") + + name, address = parseaddr(value) + if name or not address: + # parseaddr has found a name (e.g. Name ) or the entire + # value is an empty string. + raise ValueError("Invalid rfc822name value") + + self._value = value + + @property + def value(self) -> str: + return self._value + + @classmethod + def _init_without_validation(cls, value: str) -> RFC822Name: + instance = cls.__new__(cls) + instance._value = value + return instance + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, RFC822Name): + return NotImplemented + + return self.value == other.value + + def __hash__(self) -> int: + return hash(self.value) + + +class DNSName(GeneralName): + def __init__(self, value: str) -> None: + if isinstance(value, str): + try: + value.encode("ascii") + except UnicodeEncodeError: + raise ValueError( + "DNSName values should be passed as an A-label string. " + "This means unicode characters should be encoded via " + "a library like idna." + ) + else: + raise TypeError("value must be string") + + self._value = value + + @property + def value(self) -> str: + return self._value + + @classmethod + def _init_without_validation(cls, value: str) -> DNSName: + instance = cls.__new__(cls) + instance._value = value + return instance + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, DNSName): + return NotImplemented + + return self.value == other.value + + def __hash__(self) -> int: + return hash(self.value) + + +class UniformResourceIdentifier(GeneralName): + def __init__(self, value: str) -> None: + if isinstance(value, str): + try: + value.encode("ascii") + except UnicodeEncodeError: + raise ValueError( + "URI values should be passed as an A-label string. " + "This means unicode characters should be encoded via " + "a library like idna." + ) + else: + raise TypeError("value must be string") + + self._value = value + + @property + def value(self) -> str: + return self._value + + @classmethod + def _init_without_validation(cls, value: str) -> UniformResourceIdentifier: + instance = cls.__new__(cls) + instance._value = value + return instance + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, UniformResourceIdentifier): + return NotImplemented + + return self.value == other.value + + def __hash__(self) -> int: + return hash(self.value) + + +class DirectoryName(GeneralName): + def __init__(self, value: Name) -> None: + if not isinstance(value, Name): + raise TypeError("value must be a Name") + + self._value = value + + @property + def value(self) -> Name: + return self._value + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, DirectoryName): + return NotImplemented + + return self.value == other.value + + def __hash__(self) -> int: + return hash(self.value) + + +class RegisteredID(GeneralName): + def __init__(self, value: ObjectIdentifier) -> None: + if not isinstance(value, ObjectIdentifier): + raise TypeError("value must be an ObjectIdentifier") + + self._value = value + + @property + def value(self) -> ObjectIdentifier: + return self._value + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, RegisteredID): + return NotImplemented + + return self.value == other.value + + def __hash__(self) -> int: + return hash(self.value) + + +class IPAddress(GeneralName): + def __init__(self, value: _IPAddressTypes) -> None: + if not isinstance( + value, + ( + ipaddress.IPv4Address, + ipaddress.IPv6Address, + ipaddress.IPv4Network, + ipaddress.IPv6Network, + ), + ): + raise TypeError( + "value must be an instance of ipaddress.IPv4Address, " + "ipaddress.IPv6Address, ipaddress.IPv4Network, or " + "ipaddress.IPv6Network" + ) + + self._value = value + + @property + def value(self) -> _IPAddressTypes: + return self._value + + def _packed(self) -> bytes: + if isinstance( + self.value, (ipaddress.IPv4Address, ipaddress.IPv6Address) + ): + return self.value.packed + else: + return ( + self.value.network_address.packed + self.value.netmask.packed + ) + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, IPAddress): + return NotImplemented + + return self.value == other.value + + def __hash__(self) -> int: + return hash(self.value) + + +class OtherName(GeneralName): + def __init__(self, type_id: ObjectIdentifier, value: bytes) -> None: + if not isinstance(type_id, ObjectIdentifier): + raise TypeError("type_id must be an ObjectIdentifier") + if not isinstance(value, bytes): + raise TypeError("value must be a binary string") + + self._type_id = type_id + self._value = value + + @property + def type_id(self) -> ObjectIdentifier: + return self._type_id + + @property + def value(self) -> bytes: + return self._value + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, OtherName): + return NotImplemented + + return self.type_id == other.type_id and self.value == other.value + + def __hash__(self) -> int: + return hash((self.type_id, self.value)) diff --git a/source/target-vendors/cp312/cryptography/x509/name.py b/source/target-vendors/cp312/cryptography/x509/name.py new file mode 100644 index 0000000..2e54edc --- /dev/null +++ b/source/target-vendors/cp312/cryptography/x509/name.py @@ -0,0 +1,489 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import binascii +import re +import sys +import typing +import warnings +from collections.abc import Iterable, Iterator + +from cryptography import utils +from cryptography.hazmat.bindings._rust import x509 as rust_x509 +from cryptography.x509.oid import NameOID, ObjectIdentifier + + +class _ASN1Type(utils.Enum): + BitString = 3 + OctetString = 4 + UTF8String = 12 + NumericString = 18 + PrintableString = 19 + T61String = 20 + IA5String = 22 + UTCTime = 23 + GeneralizedTime = 24 + VisibleString = 26 + UniversalString = 28 + BMPString = 30 + + +_ASN1_TYPE_TO_ENUM = {i.value: i for i in _ASN1Type} +_NAMEOID_DEFAULT_TYPE: dict[ObjectIdentifier, _ASN1Type] = { + NameOID.COUNTRY_NAME: _ASN1Type.PrintableString, + NameOID.JURISDICTION_COUNTRY_NAME: _ASN1Type.PrintableString, + NameOID.SERIAL_NUMBER: _ASN1Type.PrintableString, + NameOID.DN_QUALIFIER: _ASN1Type.PrintableString, + NameOID.EMAIL_ADDRESS: _ASN1Type.IA5String, + NameOID.DOMAIN_COMPONENT: _ASN1Type.IA5String, +} + +# Type alias +_OidNameMap = typing.Mapping[ObjectIdentifier, str] +_NameOidMap = typing.Mapping[str, ObjectIdentifier] + +#: Short attribute names from RFC 4514: +#: https://tools.ietf.org/html/rfc4514#page-7 +_NAMEOID_TO_NAME: _OidNameMap = { + NameOID.COMMON_NAME: "CN", + NameOID.LOCALITY_NAME: "L", + NameOID.STATE_OR_PROVINCE_NAME: "ST", + NameOID.ORGANIZATION_NAME: "O", + NameOID.ORGANIZATIONAL_UNIT_NAME: "OU", + NameOID.COUNTRY_NAME: "C", + NameOID.STREET_ADDRESS: "STREET", + NameOID.DOMAIN_COMPONENT: "DC", + NameOID.USER_ID: "UID", +} +_NAME_TO_NAMEOID = {v: k for k, v in _NAMEOID_TO_NAME.items()} + +_NAMEOID_LENGTH_LIMIT = { + NameOID.COUNTRY_NAME: (2, 2), + NameOID.JURISDICTION_COUNTRY_NAME: (2, 2), + NameOID.COMMON_NAME: (1, 64), +} + + +def _escape_dn_value(val: str | bytes) -> str: + """Escape special characters in RFC4514 Distinguished Name value.""" + + if not val: + return "" + + # RFC 4514 Section 2.4 defines the value as being the # (U+0023) character + # followed by the hexadecimal encoding of the octets. + if isinstance(val, bytes): + return "#" + binascii.hexlify(val).decode("utf8") + + # See https://tools.ietf.org/html/rfc4514#section-2.4 + val = val.replace("\\", "\\\\") + val = val.replace('"', '\\"') + val = val.replace("+", "\\+") + val = val.replace(",", "\\,") + val = val.replace(";", "\\;") + val = val.replace("<", "\\<") + val = val.replace(">", "\\>") + val = val.replace("\0", "\\00") + + if val[0] == "#" or (val[0] == " " and len(val) > 1): + val = "\\" + val + if val[-1] == " ": + val = val[:-1] + "\\ " + + return val + + +def _unescape_dn_value(val: str) -> str: + if not val: + return "" + + # See https://tools.ietf.org/html/rfc4514#section-3 + + # special = escaped / SPACE / SHARP / EQUALS + # escaped = DQUOTE / PLUS / COMMA / SEMI / LANGLE / RANGLE + def sub(m): + val = m.group(0) + # Special character escape + if len(val) == 2: + return val[1:] + + # Unicode string of hex + return binascii.unhexlify(val.replace("\\", "")).decode() + + return _RFC4514NameParser._PAIR_MULTI_RE.sub(sub, val) + + +NameAttributeValueType = typing.TypeVar( + "NameAttributeValueType", + typing.Union[str, bytes], + str, + bytes, + covariant=True, +) + + +class NameAttribute(typing.Generic[NameAttributeValueType]): + def __init__( + self, + oid: ObjectIdentifier, + value: NameAttributeValueType, + _type: _ASN1Type | None = None, + *, + _validate: bool = True, + ) -> None: + if not isinstance(oid, ObjectIdentifier): + raise TypeError( + "oid argument must be an ObjectIdentifier instance." + ) + if _type == _ASN1Type.BitString: + if oid != NameOID.X500_UNIQUE_IDENTIFIER: + raise TypeError( + "oid must be X500_UNIQUE_IDENTIFIER for BitString type." + ) + if not isinstance(value, bytes): + raise TypeError("value must be bytes for BitString") + elif not isinstance(value, str): + raise TypeError("value argument must be a str") + + length_limits = _NAMEOID_LENGTH_LIMIT.get(oid) + if length_limits is not None: + min_length, max_length = length_limits + assert isinstance(value, str) + c_len = len(value.encode("utf8")) + if c_len < min_length or c_len > max_length: + msg = ( + f"Attribute's length must be >= {min_length} and " + f"<= {max_length}, but it was {c_len}" + ) + if _validate is True: + raise ValueError(msg) + else: + warnings.warn(msg, stacklevel=2) + + # The appropriate ASN1 string type varies by OID and is defined across + # multiple RFCs including 2459, 3280, and 5280. In general UTF8String + # is preferred (2459), but 3280 and 5280 specify several OIDs with + # alternate types. This means when we see the sentinel value we need + # to look up whether the OID has a non-UTF8 type. If it does, set it + # to that. Otherwise, UTF8! + if _type is None: + _type = _NAMEOID_DEFAULT_TYPE.get(oid, _ASN1Type.UTF8String) + + if not isinstance(_type, _ASN1Type): + raise TypeError("_type must be from the _ASN1Type enum") + + self._oid = oid + self._value: NameAttributeValueType = value + self._type: _ASN1Type = _type + + @property + def oid(self) -> ObjectIdentifier: + return self._oid + + @property + def value(self) -> NameAttributeValueType: + return self._value + + @property + def rfc4514_attribute_name(self) -> str: + """ + The short attribute name (for example "CN") if available, + otherwise the OID dotted string. + """ + return _NAMEOID_TO_NAME.get(self.oid, self.oid.dotted_string) + + def rfc4514_string( + self, attr_name_overrides: _OidNameMap | None = None + ) -> str: + """ + Format as RFC4514 Distinguished Name string. + + Use short attribute name if available, otherwise fall back to OID + dotted string. + """ + attr_name = ( + attr_name_overrides.get(self.oid) if attr_name_overrides else None + ) + if attr_name is None: + attr_name = self.rfc4514_attribute_name + + return f"{attr_name}={_escape_dn_value(self.value)}" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, NameAttribute): + return NotImplemented + + return self.oid == other.oid and self.value == other.value + + def __hash__(self) -> int: + return hash((self.oid, self.value)) + + def __repr__(self) -> str: + return f"" + + +class RelativeDistinguishedName: + def __init__(self, attributes: Iterable[NameAttribute[str | bytes]]): + attributes = list(attributes) + if not attributes: + raise ValueError("a relative distinguished name cannot be empty") + if not all(isinstance(x, NameAttribute) for x in attributes): + raise TypeError("attributes must be an iterable of NameAttribute") + + # Keep list and frozenset to preserve attribute order where it matters + self._attributes = attributes + self._attribute_set = frozenset(attributes) + + if len(self._attribute_set) != len(attributes): + raise ValueError("duplicate attributes are not allowed") + + def get_attributes_for_oid( + self, + oid: ObjectIdentifier, + ) -> list[NameAttribute[str | bytes]]: + return [i for i in self if i.oid == oid] + + def rfc4514_string( + self, attr_name_overrides: _OidNameMap | None = None + ) -> str: + """ + Format as RFC4514 Distinguished Name string. + + Within each RDN, attributes are joined by '+', although that is rarely + used in certificates. + """ + return "+".join( + attr.rfc4514_string(attr_name_overrides) + for attr in self._attributes + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, RelativeDistinguishedName): + return NotImplemented + + return self._attribute_set == other._attribute_set + + def __hash__(self) -> int: + return hash(self._attribute_set) + + def __iter__(self) -> Iterator[NameAttribute[str | bytes]]: + return iter(self._attributes) + + def __len__(self) -> int: + return len(self._attributes) + + def __repr__(self) -> str: + return f"" + + +class Name: + @typing.overload + def __init__( + self, attributes: Iterable[NameAttribute[str | bytes]] + ) -> None: ... + + @typing.overload + def __init__( + self, attributes: Iterable[RelativeDistinguishedName] + ) -> None: ... + + def __init__( + self, + attributes: Iterable[ + NameAttribute[str | bytes] | RelativeDistinguishedName + ], + ) -> None: + attributes = list(attributes) + if all(isinstance(x, NameAttribute) for x in attributes): + self._attributes = [ + RelativeDistinguishedName([typing.cast(NameAttribute, x)]) + for x in attributes + ] + elif all(isinstance(x, RelativeDistinguishedName) for x in attributes): + self._attributes = typing.cast( + list[RelativeDistinguishedName], attributes + ) + else: + raise TypeError( + "attributes must be a list of NameAttribute" + " or a list RelativeDistinguishedName" + ) + + @classmethod + def from_rfc4514_string( + cls, + data: str, + attr_name_overrides: _NameOidMap | None = None, + ) -> Name: + return _RFC4514NameParser(data, attr_name_overrides or {}).parse() + + @classmethod + def from_bytes(cls, data: bytes) -> Name: + """ + Parse a DER encoded X.509 name (the inverse of ``public_bytes``). + """ + return rust_x509.parse_name_bytes(data) + + def rfc4514_string( + self, attr_name_overrides: _OidNameMap | None = None + ) -> str: + """ + Format as RFC4514 Distinguished Name string. + For example 'CN=foobar.com,O=Foo Corp,C=US' + + An X.509 name is a two-level structure: a list of sets of attributes. + Each list element is separated by ',' and within each list element, set + elements are separated by '+'. The latter is almost never used in + real world certificates. According to RFC4514 section 2.1 the + RDNSequence must be reversed when converting to string representation. + """ + return ",".join( + attr.rfc4514_string(attr_name_overrides) + for attr in reversed(self._attributes) + ) + + def get_attributes_for_oid( + self, + oid: ObjectIdentifier, + ) -> list[NameAttribute[str | bytes]]: + return [i for i in self if i.oid == oid] + + @property + def rdns(self) -> list[RelativeDistinguishedName]: + return self._attributes + + def public_bytes(self, backend: typing.Any = None) -> bytes: + return rust_x509.encode_name_bytes(self) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Name): + return NotImplemented + + return self._attributes == other._attributes + + def __hash__(self) -> int: + # TODO: this is relatively expensive, if this looks like a bottleneck + # for you, consider optimizing! + return hash(tuple(self._attributes)) + + def __iter__(self) -> Iterator[NameAttribute[str | bytes]]: + for rdn in self._attributes: + yield from rdn + + def __len__(self) -> int: + return sum(len(rdn) for rdn in self._attributes) + + def __repr__(self) -> str: + return f"" + + +class _RFC4514NameParser: + _OID_RE = re.compile(r"(0|([1-9]\d*))(\.(0|([1-9]\d*)))+") + _DESCR_RE = re.compile(r"[a-zA-Z][a-zA-Z\d-]*") + + _ESCAPE_SPECIAL = r"[\\ #=\"\+,;<>]" + _ESCAPE_HEX = r"[\da-zA-Z]{2}" + _PAIR = rf"\\({_ESCAPE_SPECIAL}|{_ESCAPE_HEX})" + _PAIR_MULTI_RE = re.compile(rf"(\\{_ESCAPE_SPECIAL})|((\\{_ESCAPE_HEX})+)") + _LUTF1 = r"[\x01-\x1f\x21\x24-\x2A\x2D-\x3A\x3D\x3F-\x5B\x5D-\x7F]" + _SUTF1 = r"[\x01-\x21\x23-\x2A\x2D-\x3A\x3D\x3F-\x5B\x5D-\x7F]" + _TUTF1 = r"[\x01-\x1F\x21\x23-\x2A\x2D-\x3A\x3D\x3F-\x5B\x5D-\x7F]" + _UTFMB = rf"[\x80-{chr(sys.maxunicode)}]" + _LEADCHAR = rf"{_LUTF1}|{_UTFMB}" + _STRINGCHAR = rf"{_SUTF1}|{_UTFMB}" + _TRAILCHAR = rf"{_TUTF1}|{_UTFMB}" + _STRING_RE = re.compile( + rf""" + ( + ({_LEADCHAR}|{_PAIR}) + ( + ({_STRINGCHAR}|{_PAIR})* + ({_TRAILCHAR}|{_PAIR}) + )? + )? + """, + re.VERBOSE, + ) + _HEXSTRING_RE = re.compile(r"#([\da-zA-Z]{2})+") + + def __init__(self, data: str, attr_name_overrides: _NameOidMap) -> None: + self._data = data + self._idx = 0 + + self._attr_name_overrides = attr_name_overrides + + def _has_data(self) -> bool: + return self._idx < len(self._data) + + def _peek(self) -> str | None: + if self._has_data(): + return self._data[self._idx] + return None + + def _read_char(self, ch: str) -> None: + if self._peek() != ch: + raise ValueError + self._idx += 1 + + def _read_re(self, pat) -> str: + match = pat.match(self._data, pos=self._idx) + if match is None: + raise ValueError + val = match.group() + self._idx += len(val) + return val + + def parse(self) -> Name: + """ + Parses the `data` string and converts it to a Name. + + According to RFC4514 section 2.1 the RDNSequence must be + reversed when converting to string representation. So, when + we parse it, we need to reverse again to get the RDNs on the + correct order. + """ + + if not self._has_data(): + return Name([]) + + rdns = [self._parse_rdn()] + + while self._has_data(): + self._read_char(",") + rdns.append(self._parse_rdn()) + + return Name(reversed(rdns)) + + def _parse_rdn(self) -> RelativeDistinguishedName: + nas = [self._parse_na()] + while self._peek() == "+": + self._read_char("+") + nas.append(self._parse_na()) + + return RelativeDistinguishedName(nas) + + def _parse_na(self) -> NameAttribute[str]: + try: + oid_value = self._read_re(self._OID_RE) + except ValueError: + name = self._read_re(self._DESCR_RE) + oid = self._attr_name_overrides.get( + name, _NAME_TO_NAMEOID.get(name) + ) + if oid is None: + raise ValueError + else: + oid = ObjectIdentifier(oid_value) + + self._read_char("=") + if self._peek() == "#": + value = self._read_re(self._HEXSTRING_RE) + value = binascii.unhexlify(value[1:]).decode() + else: + raw_value = self._read_re(self._STRING_RE) + value = _unescape_dn_value(raw_value) + + return NameAttribute(oid, value) diff --git a/source/target-vendors/cp312/cryptography/x509/ocsp.py b/source/target-vendors/cp312/cryptography/x509/ocsp.py new file mode 100644 index 0000000..f61ed80 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/x509/ocsp.py @@ -0,0 +1,379 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import datetime +from collections.abc import Iterable + +from cryptography import utils, x509 +from cryptography.hazmat.bindings._rust import ocsp +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric.types import ( + CertificateIssuerPrivateKeyTypes, +) +from cryptography.x509.base import _reject_duplicate_extension + + +class OCSPResponderEncoding(utils.Enum): + HASH = "By Hash" + NAME = "By Name" + + +class OCSPResponseStatus(utils.Enum): + SUCCESSFUL = 0 + MALFORMED_REQUEST = 1 + INTERNAL_ERROR = 2 + TRY_LATER = 3 + SIG_REQUIRED = 5 + UNAUTHORIZED = 6 + + +_ALLOWED_HASHES = ( + hashes.SHA1, + hashes.SHA224, + hashes.SHA256, + hashes.SHA384, + hashes.SHA512, +) + + +def _verify_algorithm(algorithm: hashes.HashAlgorithm) -> None: + if not isinstance(algorithm, _ALLOWED_HASHES): + raise ValueError( + "Algorithm must be SHA1, SHA224, SHA256, SHA384, or SHA512" + ) + + +class OCSPCertStatus(utils.Enum): + GOOD = 0 + REVOKED = 1 + UNKNOWN = 2 + + +class _SingleResponse: + def __init__( + self, + resp: tuple[x509.Certificate, x509.Certificate] | None, + resp_hash: tuple[bytes, bytes, int] | None, + algorithm: hashes.HashAlgorithm, + cert_status: OCSPCertStatus, + this_update: datetime.datetime, + next_update: datetime.datetime | None, + revocation_time: datetime.datetime | None, + revocation_reason: x509.ReasonFlags | None, + ): + _verify_algorithm(algorithm) + if not isinstance(this_update, datetime.datetime): + raise TypeError("this_update must be a datetime object") + if next_update is not None and not isinstance( + next_update, datetime.datetime + ): + raise TypeError("next_update must be a datetime object or None") + + self._resp = resp + self._resp_hash = resp_hash + self._algorithm = algorithm + self._this_update = this_update + self._next_update = next_update + + if not isinstance(cert_status, OCSPCertStatus): + raise TypeError( + "cert_status must be an item from the OCSPCertStatus enum" + ) + if cert_status is not OCSPCertStatus.REVOKED: + if revocation_time is not None: + raise ValueError( + "revocation_time can only be provided if the certificate " + "is revoked" + ) + if revocation_reason is not None: + raise ValueError( + "revocation_reason can only be provided if the certificate" + " is revoked" + ) + else: + if not isinstance(revocation_time, datetime.datetime): + raise TypeError("revocation_time must be a datetime object") + + if revocation_reason is not None and not isinstance( + revocation_reason, x509.ReasonFlags + ): + raise TypeError( + "revocation_reason must be an item from the ReasonFlags " + "enum or None" + ) + + self._cert_status = cert_status + self._revocation_time = revocation_time + self._revocation_reason = revocation_reason + + +OCSPRequest = ocsp.OCSPRequest +OCSPResponse = ocsp.OCSPResponse +OCSPSingleResponse = ocsp.OCSPSingleResponse + + +class OCSPRequestBuilder: + def __init__( + self, + request: tuple[ + x509.Certificate, x509.Certificate, hashes.HashAlgorithm + ] + | None = None, + request_hash: tuple[bytes, bytes, int, hashes.HashAlgorithm] + | None = None, + extensions: list[x509.Extension[x509.ExtensionType]] = [], + ) -> None: + self._request = request + self._request_hash = request_hash + self._extensions = extensions + + def add_certificate( + self, + cert: x509.Certificate, + issuer: x509.Certificate, + algorithm: hashes.HashAlgorithm, + ) -> OCSPRequestBuilder: + if self._request is not None or self._request_hash is not None: + raise ValueError("Only one certificate can be added to a request") + + _verify_algorithm(algorithm) + if not isinstance(cert, x509.Certificate) or not isinstance( + issuer, x509.Certificate + ): + raise TypeError("cert and issuer must be a Certificate") + + return OCSPRequestBuilder( + (cert, issuer, algorithm), self._request_hash, self._extensions + ) + + def add_certificate_by_hash( + self, + issuer_name_hash: bytes, + issuer_key_hash: bytes, + serial_number: int, + algorithm: hashes.HashAlgorithm, + ) -> OCSPRequestBuilder: + if self._request is not None or self._request_hash is not None: + raise ValueError("Only one certificate can be added to a request") + + if not isinstance(serial_number, int): + raise TypeError("serial_number must be an integer") + + _verify_algorithm(algorithm) + utils._check_bytes("issuer_name_hash", issuer_name_hash) + utils._check_bytes("issuer_key_hash", issuer_key_hash) + if algorithm.digest_size != len( + issuer_name_hash + ) or algorithm.digest_size != len(issuer_key_hash): + raise ValueError( + "issuer_name_hash and issuer_key_hash must be the same length " + "as the digest size of the algorithm" + ) + + return OCSPRequestBuilder( + self._request, + (issuer_name_hash, issuer_key_hash, serial_number, algorithm), + self._extensions, + ) + + def add_extension( + self, extval: x509.ExtensionType, critical: bool + ) -> OCSPRequestBuilder: + if not isinstance(extval, x509.ExtensionType): + raise TypeError("extension must be an ExtensionType") + + extension = x509.Extension(extval.oid, critical, extval) + _reject_duplicate_extension(extension, self._extensions) + + return OCSPRequestBuilder( + self._request, self._request_hash, [*self._extensions, extension] + ) + + def build(self) -> OCSPRequest: + if self._request is None and self._request_hash is None: + raise ValueError("You must add a certificate before building") + + return ocsp.create_ocsp_request(self) + + +class OCSPResponseBuilder: + def __init__( + self, + response: _SingleResponse | None = None, + responder_id: tuple[x509.Certificate, OCSPResponderEncoding] + | None = None, + certs: list[x509.Certificate] | None = None, + extensions: list[x509.Extension[x509.ExtensionType]] = [], + ): + self._response = response + self._responder_id = responder_id + self._certs = certs + self._extensions = extensions + + def add_response( + self, + cert: x509.Certificate, + issuer: x509.Certificate, + algorithm: hashes.HashAlgorithm, + cert_status: OCSPCertStatus, + this_update: datetime.datetime, + next_update: datetime.datetime | None, + revocation_time: datetime.datetime | None, + revocation_reason: x509.ReasonFlags | None, + ) -> OCSPResponseBuilder: + if self._response is not None: + raise ValueError("Only one response per OCSPResponse.") + + if not isinstance(cert, x509.Certificate) or not isinstance( + issuer, x509.Certificate + ): + raise TypeError("cert and issuer must be a Certificate") + + singleresp = _SingleResponse( + (cert, issuer), + None, + algorithm, + cert_status, + this_update, + next_update, + revocation_time, + revocation_reason, + ) + return OCSPResponseBuilder( + singleresp, + self._responder_id, + self._certs, + self._extensions, + ) + + def add_response_by_hash( + self, + issuer_name_hash: bytes, + issuer_key_hash: bytes, + serial_number: int, + algorithm: hashes.HashAlgorithm, + cert_status: OCSPCertStatus, + this_update: datetime.datetime, + next_update: datetime.datetime | None, + revocation_time: datetime.datetime | None, + revocation_reason: x509.ReasonFlags | None, + ) -> OCSPResponseBuilder: + if self._response is not None: + raise ValueError("Only one response per OCSPResponse.") + + if not isinstance(serial_number, int): + raise TypeError("serial_number must be an integer") + + utils._check_bytes("issuer_name_hash", issuer_name_hash) + utils._check_bytes("issuer_key_hash", issuer_key_hash) + _verify_algorithm(algorithm) + if algorithm.digest_size != len( + issuer_name_hash + ) or algorithm.digest_size != len(issuer_key_hash): + raise ValueError( + "issuer_name_hash and issuer_key_hash must be the same length " + "as the digest size of the algorithm" + ) + + singleresp = _SingleResponse( + None, + (issuer_name_hash, issuer_key_hash, serial_number), + algorithm, + cert_status, + this_update, + next_update, + revocation_time, + revocation_reason, + ) + return OCSPResponseBuilder( + singleresp, + self._responder_id, + self._certs, + self._extensions, + ) + + def responder_id( + self, encoding: OCSPResponderEncoding, responder_cert: x509.Certificate + ) -> OCSPResponseBuilder: + if self._responder_id is not None: + raise ValueError("responder_id can only be set once") + if not isinstance(responder_cert, x509.Certificate): + raise TypeError("responder_cert must be a Certificate") + if not isinstance(encoding, OCSPResponderEncoding): + raise TypeError( + "encoding must be an element from OCSPResponderEncoding" + ) + + return OCSPResponseBuilder( + self._response, + (responder_cert, encoding), + self._certs, + self._extensions, + ) + + def certificates( + self, certs: Iterable[x509.Certificate] + ) -> OCSPResponseBuilder: + if self._certs is not None: + raise ValueError("certificates may only be set once") + certs = list(certs) + if len(certs) == 0: + raise ValueError("certs must not be an empty list") + if not all(isinstance(x, x509.Certificate) for x in certs): + raise TypeError("certs must be a list of Certificates") + return OCSPResponseBuilder( + self._response, + self._responder_id, + certs, + self._extensions, + ) + + def add_extension( + self, extval: x509.ExtensionType, critical: bool + ) -> OCSPResponseBuilder: + if not isinstance(extval, x509.ExtensionType): + raise TypeError("extension must be an ExtensionType") + + extension = x509.Extension(extval.oid, critical, extval) + _reject_duplicate_extension(extension, self._extensions) + + return OCSPResponseBuilder( + self._response, + self._responder_id, + self._certs, + [*self._extensions, extension], + ) + + def sign( + self, + private_key: CertificateIssuerPrivateKeyTypes, + algorithm: hashes.HashAlgorithm | None, + ) -> OCSPResponse: + if self._response is None: + raise ValueError("You must add a response before signing") + if self._responder_id is None: + raise ValueError("You must add a responder_id before signing") + + return ocsp.create_ocsp_response( + OCSPResponseStatus.SUCCESSFUL, self, private_key, algorithm + ) + + @classmethod + def build_unsuccessful( + cls, response_status: OCSPResponseStatus + ) -> OCSPResponse: + if not isinstance(response_status, OCSPResponseStatus): + raise TypeError( + "response_status must be an item from OCSPResponseStatus" + ) + if response_status is OCSPResponseStatus.SUCCESSFUL: + raise ValueError("response_status cannot be SUCCESSFUL") + + return ocsp.create_ocsp_response(response_status, None, None, None) + + +load_der_ocsp_request = ocsp.load_der_ocsp_request +load_der_ocsp_response = ocsp.load_der_ocsp_response diff --git a/source/target-vendors/cp312/cryptography/x509/oid.py b/source/target-vendors/cp312/cryptography/x509/oid.py new file mode 100644 index 0000000..520fc7a --- /dev/null +++ b/source/target-vendors/cp312/cryptography/x509/oid.py @@ -0,0 +1,37 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +from cryptography.hazmat._oid import ( + AttributeOID, + AuthorityInformationAccessOID, + CertificatePoliciesOID, + CRLEntryExtensionOID, + ExtendedKeyUsageOID, + ExtensionOID, + NameOID, + ObjectIdentifier, + OCSPExtensionOID, + OtherNameFormOID, + PublicKeyAlgorithmOID, + SignatureAlgorithmOID, + SubjectInformationAccessOID, +) + +__all__ = [ + "AttributeOID", + "AuthorityInformationAccessOID", + "CRLEntryExtensionOID", + "CertificatePoliciesOID", + "ExtendedKeyUsageOID", + "ExtensionOID", + "NameOID", + "OCSPExtensionOID", + "ObjectIdentifier", + "OtherNameFormOID", + "PublicKeyAlgorithmOID", + "SignatureAlgorithmOID", + "SubjectInformationAccessOID", +] diff --git a/source/target-vendors/cp312/cryptography/x509/verification.py b/source/target-vendors/cp312/cryptography/x509/verification.py new file mode 100644 index 0000000..2db4324 --- /dev/null +++ b/source/target-vendors/cp312/cryptography/x509/verification.py @@ -0,0 +1,34 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import annotations + +import typing + +from cryptography.hazmat.bindings._rust import x509 as rust_x509 +from cryptography.x509.general_name import DNSName, IPAddress + +__all__ = [ + "ClientVerifier", + "Criticality", + "ExtensionPolicy", + "Policy", + "PolicyBuilder", + "ServerVerifier", + "Store", + "Subject", + "VerificationError", + "VerifiedClient", +] + +Store = rust_x509.Store +Subject = typing.Union[DNSName, IPAddress] +VerifiedClient = rust_x509.VerifiedClient +ClientVerifier = rust_x509.ClientVerifier +ServerVerifier = rust_x509.ServerVerifier +PolicyBuilder = rust_x509.PolicyBuilder +Policy = rust_x509.Policy +ExtensionPolicy = rust_x509.ExtensionPolicy +Criticality = rust_x509.Criticality +VerificationError = rust_x509.VerificationError diff --git a/source/target-vendors/cp312/dotenv/__init__.py b/source/target-vendors/cp312/dotenv/__init__.py new file mode 100644 index 0000000..dde24a0 --- /dev/null +++ b/source/target-vendors/cp312/dotenv/__init__.py @@ -0,0 +1,51 @@ +from typing import Any, Optional + +from .main import dotenv_values, find_dotenv, get_key, load_dotenv, set_key, unset_key + + +def load_ipython_extension(ipython: Any) -> None: + from .ipython import load_ipython_extension + + load_ipython_extension(ipython) + + +def get_cli_string( + path: Optional[str] = None, + action: Optional[str] = None, + key: Optional[str] = None, + value: Optional[str] = None, + quote: Optional[str] = None, +): + """Returns a string suitable for running as a shell script. + + Useful for converting a arguments passed to a fabric task + to be passed to a `local` or `run` command. + """ + command = ["dotenv"] + if quote: + command.append(f"-q {quote}") + if path: + command.append(f"-f {path}") + if action: + command.append(action) + if key: + command.append(key) + if value: + if " " in value: + command.append(f'"{value}"') + else: + command.append(value) + + return " ".join(command).strip() + + +__all__ = [ + "get_cli_string", + "load_dotenv", + "dotenv_values", + "get_key", + "set_key", + "unset_key", + "find_dotenv", + "load_ipython_extension", +] diff --git a/source/target-vendors/cp312/dotenv/__main__.py b/source/target-vendors/cp312/dotenv/__main__.py new file mode 100644 index 0000000..3977f55 --- /dev/null +++ b/source/target-vendors/cp312/dotenv/__main__.py @@ -0,0 +1,6 @@ +"""Entry point for cli, enables execution with `python -m dotenv`""" + +from .cli import cli + +if __name__ == "__main__": + cli() diff --git a/source/target-vendors/cp312/dotenv/cli.py b/source/target-vendors/cp312/dotenv/cli.py new file mode 100644 index 0000000..79613e2 --- /dev/null +++ b/source/target-vendors/cp312/dotenv/cli.py @@ -0,0 +1,247 @@ +import json +import os +import shlex +import sys +from contextlib import contextmanager +from typing import IO, Any, Dict, Iterator, List, Optional + +if sys.platform == "win32": + from subprocess import Popen + +try: + import click +except ImportError: + sys.stderr.write( + "It seems python-dotenv is not installed with cli option. \n" + 'Run pip install "python-dotenv[cli]" to fix this.' + ) + sys.exit(1) + +from .main import dotenv_values, set_key, unset_key +from .version import __version__ + + +def enumerate_env() -> Optional[str]: + """ + Return a path for the ${pwd}/.env file. + + If pwd does not exist, return None. + """ + try: + cwd = os.getcwd() + except FileNotFoundError: + return None + path = os.path.join(cwd, ".env") + return path + + +@click.group() +@click.option( + "-f", + "--file", + default=enumerate_env(), + type=click.Path(file_okay=True), + help="Location of the .env file, defaults to .env file in current working directory.", +) +@click.option( + "-q", + "--quote", + default="always", + type=click.Choice(["always", "never", "auto"]), + help="Whether to quote or not the variable values. Default mode is always. This does not affect parsing.", +) +@click.option( + "-e", + "--export", + default=False, + type=click.BOOL, + help="Whether to write the dot file as an executable bash script.", +) +@click.version_option(version=__version__) +@click.pass_context +def cli(ctx: click.Context, file: Any, quote: Any, export: Any) -> None: + """This script is used to set, get or unset values from a .env file.""" + ctx.obj = {"QUOTE": quote, "EXPORT": export, "FILE": file} + + +@contextmanager +def stream_file(path: os.PathLike) -> Iterator[IO[str]]: + """ + Open a file and yield the corresponding (decoded) stream. + + Exits with error code 2 if the file cannot be opened. + """ + + try: + with open(path) as stream: + yield stream + except OSError as exc: + print(f"Error opening env file: {exc}", file=sys.stderr) + sys.exit(2) + + +@cli.command(name="list") +@click.pass_context +@click.option( + "--format", + "output_format", + default="simple", + type=click.Choice(["simple", "json", "shell", "export"]), + help="The format in which to display the list. Default format is simple, " + "which displays name=value without quotes.", +) +def list_values(ctx: click.Context, output_format: str) -> None: + """Display all the stored key/value.""" + file = ctx.obj["FILE"] + + with stream_file(file) as stream: + values = dotenv_values(stream=stream) + + if output_format == "json": + click.echo(json.dumps(values, indent=2, sort_keys=True)) + else: + prefix = "export " if output_format == "export" else "" + for k in sorted(values): + v = values[k] + if v is not None: + if output_format in ("export", "shell"): + v = shlex.quote(v) + click.echo(f"{prefix}{k}={v}") + + +@cli.command(name="set") +@click.pass_context +@click.argument("key", required=True) +@click.argument("value", required=True) +def set_value(ctx: click.Context, key: Any, value: Any) -> None: + """ + Store the given key/value. + + This doesn't follow symlinks, to avoid accidentally modifying a file at a + potentially untrusted path. + """ + + file = ctx.obj["FILE"] + quote = ctx.obj["QUOTE"] + export = ctx.obj["EXPORT"] + success, key, value = set_key(file, key, value, quote, export) + if success: + click.echo(f"{key}={value}") + else: + sys.exit(1) + + +@cli.command() +@click.pass_context +@click.argument("key", required=True) +def get(ctx: click.Context, key: Any) -> None: + """Retrieve the value for the given key.""" + file = ctx.obj["FILE"] + + with stream_file(file) as stream: + values = dotenv_values(stream=stream) + + stored_value = values.get(key) + if stored_value: + click.echo(stored_value) + else: + sys.exit(1) + + +@cli.command() +@click.pass_context +@click.argument("key", required=True) +def unset(ctx: click.Context, key: Any) -> None: + """ + Removes the given key. + + This doesn't follow symlinks, to avoid accidentally modifying a file at a + potentially untrusted path. + """ + file = ctx.obj["FILE"] + quote = ctx.obj["QUOTE"] + success, key = unset_key(file, key, quote) + if success: + click.echo(f"Successfully removed {key}") + else: + sys.exit(1) + + +@cli.command( + context_settings={ + "allow_extra_args": True, + "allow_interspersed_args": False, + "ignore_unknown_options": True, + } +) +@click.pass_context +@click.option( + "--override/--no-override", + default=True, + help="Override variables from the environment file with those from the .env file.", +) +@click.argument("commandline", nargs=-1, type=click.UNPROCESSED) +def run(ctx: click.Context, override: bool, commandline: tuple[str, ...]) -> None: + """Run command with environment variables present.""" + file = ctx.obj["FILE"] + if not os.path.isfile(file): + raise click.BadParameter( + f"Invalid value for '-f' \"{file}\" does not exist.", ctx=ctx + ) + dotenv_as_dict = { + k: v + for (k, v) in dotenv_values(file).items() + if v is not None and (override or k not in os.environ) + } + + if not commandline: + click.echo("No command given.") + sys.exit(1) + + run_command([*commandline, *ctx.args], dotenv_as_dict) + + +def run_command(command: List[str], env: Dict[str, str]) -> None: + """Replace the current process with the specified command. + + Replaces the current process with the specified command and the variables from `env` + added in the current environment variables. + + Parameters + ---------- + command: List[str] + The command and it's parameters + env: Dict + The additional environment variables + + Returns + ------- + None + This function does not return any value. It replaces the current process with the new one. + + """ + # copy the current environment variables and add the vales from + # `env` + cmd_env = os.environ.copy() + cmd_env.update(env) + + if sys.platform == "win32": + # execvpe on Windows returns control immediately + # rather than once the command has finished. + try: + p = Popen( + command, universal_newlines=True, bufsize=0, shell=False, env=cmd_env + ) + except FileNotFoundError: + print(f"Command not found: {command[0]}", file=sys.stderr) + sys.exit(1) + + _, _ = p.communicate() + + sys.exit(p.returncode) + else: + try: + os.execvpe(command[0], args=command, env=cmd_env) + except FileNotFoundError: + print(f"Command not found: {command[0]}", file=sys.stderr) + sys.exit(1) diff --git a/source/target-vendors/cp312/dotenv/ipython.py b/source/target-vendors/cp312/dotenv/ipython.py new file mode 100644 index 0000000..4e7edbb --- /dev/null +++ b/source/target-vendors/cp312/dotenv/ipython.py @@ -0,0 +1,50 @@ +from IPython.core.magic import Magics, line_magic, magics_class # type: ignore +from IPython.core.magic_arguments import ( + argument, + magic_arguments, + parse_argstring, +) # type: ignore + +from .main import find_dotenv, load_dotenv + + +@magics_class +class IPythonDotEnv(Magics): + @magic_arguments() + @argument( + "-o", + "--override", + action="store_true", + help="Indicate to override existing variables", + ) + @argument( + "-v", + "--verbose", + action="store_true", + help="Indicate function calls to be verbose", + ) + @argument( + "dotenv_path", + nargs="?", + type=str, + default=".env", + help="Search in increasingly higher folders for the `dotenv_path`", + ) + @line_magic + def dotenv(self, line): + args = parse_argstring(self.dotenv, line) + # Locate the .env file + dotenv_path = args.dotenv_path + try: + dotenv_path = find_dotenv(dotenv_path, True, True) + except IOError: + print("cannot find .env file") + return + + # Load the .env file + load_dotenv(dotenv_path, verbose=args.verbose, override=args.override) + + +def load_ipython_extension(ipython): + """Register the %dotenv magic.""" + ipython.register_magics(IPythonDotEnv) diff --git a/source/target-vendors/cp312/dotenv/main.py b/source/target-vendors/cp312/dotenv/main.py new file mode 100644 index 0000000..3123690 --- /dev/null +++ b/source/target-vendors/cp312/dotenv/main.py @@ -0,0 +1,487 @@ +import io +import logging +import os +import pathlib +import stat +import sys +import tempfile +from collections import OrderedDict +from contextlib import contextmanager +from typing import IO, Dict, Iterable, Iterator, Mapping, Optional, Tuple, Union + +from .parser import Binding, parse_stream +from .variables import parse_variables + +# A type alias for a string path to be used for the paths in this file. +# These paths may flow to `open()` and `os.replace()`. +StrPath = Union[str, "os.PathLike[str]"] + +logger = logging.getLogger(__name__) + + +def _load_dotenv_disabled() -> bool: + """ + Determine if dotenv loading has been disabled. + """ + if "PYTHON_DOTENV_DISABLED" not in os.environ: + return False + value = os.environ["PYTHON_DOTENV_DISABLED"].casefold() + return value in {"1", "true", "t", "yes", "y"} + + +def with_warn_for_invalid_lines(mappings: Iterator[Binding]) -> Iterator[Binding]: + for mapping in mappings: + if mapping.error: + logger.warning( + "python-dotenv could not parse statement starting at line %s", + mapping.original.line, + ) + yield mapping + + +class DotEnv: + def __init__( + self, + dotenv_path: Optional[StrPath], + stream: Optional[IO[str]] = None, + verbose: bool = False, + encoding: Optional[str] = None, + interpolate: bool = True, + override: bool = True, + ) -> None: + self.dotenv_path: Optional[StrPath] = dotenv_path + self.stream: Optional[IO[str]] = stream + self._dict: Optional[Dict[str, Optional[str]]] = None + self.verbose: bool = verbose + self.encoding: Optional[str] = encoding + self.interpolate: bool = interpolate + self.override: bool = override + + @contextmanager + def _get_stream(self) -> Iterator[IO[str]]: + if self.dotenv_path and _is_file_or_fifo(self.dotenv_path): + with open(self.dotenv_path, encoding=self.encoding) as stream: + yield stream + elif self.stream is not None: + yield self.stream + else: + if self.verbose: + logger.info( + "python-dotenv could not find configuration file %s.", + self.dotenv_path or ".env", + ) + yield io.StringIO("") + + def dict(self) -> Dict[str, Optional[str]]: + """Return dotenv as dict""" + if self._dict is not None: + return self._dict + + raw_values = self.parse() + + if self.interpolate: + self._dict = OrderedDict( + resolve_variables(raw_values, override=self.override) + ) + else: + self._dict = OrderedDict(raw_values) + + return self._dict + + def parse(self) -> Iterator[Tuple[str, Optional[str]]]: + with self._get_stream() as stream: + for mapping in with_warn_for_invalid_lines(parse_stream(stream)): + if mapping.key is not None: + yield mapping.key, mapping.value + + def set_as_environment_variables(self) -> bool: + """ + Load the current dotenv as system environment variable. + """ + if not self.dict(): + return False + + for k, v in self.dict().items(): + if k in os.environ and not self.override: + continue + if v is not None: + os.environ[k] = v + + return True + + def get(self, key: str) -> Optional[str]: + """ """ + data = self.dict() + + if key in data: + return data[key] + + if self.verbose: + logger.warning("Key %s not found in %s.", key, self.dotenv_path) + + return None + + +def get_key( + dotenv_path: StrPath, + key_to_get: str, + encoding: Optional[str] = "utf-8", +) -> Optional[str]: + """ + Get the value of a given key from the given .env. + + Returns `None` if the key isn't found or doesn't have a value. + """ + return DotEnv(dotenv_path, verbose=True, encoding=encoding).get(key_to_get) + + +@contextmanager +def rewrite( + path: StrPath, + encoding: Optional[str], + follow_symlinks: bool = False, +) -> Iterator[Tuple[IO[str], IO[str]]]: + if follow_symlinks: + path = os.path.realpath(path) + + try: + source: IO[str] = open(path, encoding=encoding) + try: + path_stat = os.lstat(path) + original_mode: Optional[int] = ( + stat.S_IMODE(path_stat.st_mode) + if stat.S_ISREG(path_stat.st_mode) + else None + ) + except BaseException: + source.close() + raise + except FileNotFoundError: + source = io.StringIO("") + original_mode = None + + with tempfile.NamedTemporaryFile( + mode="w", + encoding=encoding, + delete=False, + prefix=".tmp_", + dir=os.path.dirname(os.path.abspath(path)), + ) as dest: + dest_path = pathlib.Path(dest.name) + error = None + + try: + with source: + yield (source, dest) + except BaseException as err: + error = err + + if error is None: + try: + if original_mode is not None: + os.chmod(dest_path, original_mode) + + os.replace(dest_path, path) + except BaseException: + dest_path.unlink(missing_ok=True) + raise + else: + dest_path.unlink(missing_ok=True) + raise error from None + + +def set_key( + dotenv_path: StrPath, + key_to_set: str, + value_to_set: str, + quote_mode: str = "always", + export: bool = False, + encoding: Optional[str] = "utf-8", + follow_symlinks: bool = False, +) -> Tuple[Optional[bool], str, str]: + """ + Adds or Updates a key/value to the given .env + + The target .env file is created if it doesn't exist. + + This function doesn't follow symlinks by default, to avoid accidentally + modifying a file at a potentially untrusted path. If you don't need this + protection and need symlinks to be followed, use `follow_symlinks`. + """ + if quote_mode not in ("always", "auto", "never"): + raise ValueError(f"Unknown quote_mode: {quote_mode}") + + quote = quote_mode == "always" or ( + quote_mode == "auto" and not value_to_set.isalnum() + ) + + if quote: + # The single-quoted-value parser decodes `\\` and `\'`, so both have to + # be escaped here for the value to survive a write/read round-trip. + # Backslashes first, otherwise the backslash added by the quote + # escaping would be escaped in turn. + escaped = value_to_set.replace("\\", "\\\\").replace("'", "\\'") + value_out = f"'{escaped}'" + else: + value_out = value_to_set + if export: + line_out = f"export {key_to_set}={value_out}\n" + else: + line_out = f"{key_to_set}={value_out}\n" + + with rewrite(dotenv_path, encoding=encoding, follow_symlinks=follow_symlinks) as ( + source, + dest, + ): + replaced = False + missing_newline = False + for mapping in with_warn_for_invalid_lines(parse_stream(source)): + if mapping.key == key_to_set: + dest.write(line_out) + replaced = True + else: + dest.write(mapping.original.string) + missing_newline = not mapping.original.string.endswith("\n") + if not replaced: + if missing_newline: + dest.write("\n") + dest.write(line_out) + + return True, key_to_set, value_to_set + + +def unset_key( + dotenv_path: StrPath, + key_to_unset: str, + quote_mode: str = "always", + encoding: Optional[str] = "utf-8", + follow_symlinks: bool = False, +) -> Tuple[Optional[bool], str]: + """ + Removes a given key from the given `.env` file. + + If the .env path given doesn't exist, fails. + If the given key doesn't exist in the .env, fails. + + This function doesn't follow symlinks by default, to avoid accidentally + modifying a file at a potentially untrusted path. If you don't need this + protection and need symlinks to be followed, use `follow_symlinks`. + """ + if not os.path.exists(dotenv_path): + logger.warning("Can't delete from %s - it doesn't exist.", dotenv_path) + return None, key_to_unset + + removed = False + with rewrite(dotenv_path, encoding=encoding, follow_symlinks=follow_symlinks) as ( + source, + dest, + ): + for mapping in with_warn_for_invalid_lines(parse_stream(source)): + if mapping.key == key_to_unset: + removed = True + else: + dest.write(mapping.original.string) + + if not removed: + logger.warning( + "Key %s not removed from %s - key doesn't exist.", key_to_unset, dotenv_path + ) + return None, key_to_unset + + return removed, key_to_unset + + +def resolve_variables( + values: Iterable[Tuple[str, Optional[str]]], + override: bool, +) -> Mapping[str, Optional[str]]: + new_values: Dict[str, Optional[str]] = {} + + for name, value in values: + if value is None: + result = None + else: + atoms = parse_variables(value) + env: Dict[str, Optional[str]] = {} + if override: + env.update(os.environ) # type: ignore + env.update(new_values) + else: + env.update(new_values) + env.update(os.environ) # type: ignore + result = "".join(atom.resolve(env) for atom in atoms) + + new_values[name] = result + + return new_values + + +def _walk_to_root(path: str) -> Iterator[str]: + """ + Yield directories starting from the given directory up to the root + """ + if not os.path.exists(path): + raise IOError("Starting path not found") + + if os.path.isfile(path): + path = os.path.dirname(path) + + last_dir = None + current_dir = os.path.abspath(path) + while last_dir != current_dir: + yield current_dir + parent_dir = os.path.abspath(os.path.join(current_dir, os.path.pardir)) + last_dir, current_dir = current_dir, parent_dir + + +def find_dotenv( + filename: str = ".env", + raise_error_if_not_found: bool = False, + usecwd: bool = False, +) -> str: + """ + Search in increasingly higher folders for the given file + + Returns path to the file if found, or an empty string otherwise + """ + + def _is_interactive(): + """Decide whether this is running in a REPL or IPython notebook""" + if hasattr(sys, "ps1") or hasattr(sys, "ps2"): + return True + try: + main = __import__("__main__", None, None, fromlist=["__file__"]) + except ModuleNotFoundError: + return False + return not hasattr(main, "__file__") + + def _is_debugger(): + return sys.gettrace() is not None + + if usecwd or _is_interactive() or _is_debugger() or getattr(sys, "frozen", False): + # Should work without __file__, e.g. in REPL or IPython notebook. + path = os.getcwd() + else: + # will work for .py files + frame = sys._getframe() + current_file = __file__ + + while frame.f_code.co_filename == current_file or not os.path.exists( + frame.f_code.co_filename + ): + assert frame.f_back is not None + frame = frame.f_back + frame_filename = frame.f_code.co_filename + path = os.path.dirname(os.path.abspath(frame_filename)) + + for dirname in _walk_to_root(path): + check_path = os.path.join(dirname, filename) + if _is_file_or_fifo(check_path): + return check_path + + if raise_error_if_not_found: + raise IOError("File not found") + + return "" + + +def load_dotenv( + dotenv_path: Optional[StrPath] = None, + stream: Optional[IO[str]] = None, + verbose: bool = False, + override: bool = False, + interpolate: bool = True, + encoding: Optional[str] = "utf-8", +) -> bool: + """Parse a .env file and then load all the variables found as environment variables. + + Parameters: + dotenv_path: Absolute or relative path to .env file. + stream: Text stream (such as `io.StringIO`) with .env content, used if + `dotenv_path` is `None`. + verbose: Whether to output a warning the .env file is missing. + override: Whether to override the system environment variables with the variables + from the `.env` file. + interpolate: Whether to interpolate variables using POSIX variable expansion. + encoding: Encoding to be used to read the file. + Returns: + Bool: True if at least one environment variable is set else False + + If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the + .env file with its default parameters. If you need to change the default parameters + of `find_dotenv()`, you can explicitly call `find_dotenv()` and pass the result + to this function as `dotenv_path`. + + If the environment variable `PYTHON_DOTENV_DISABLED` is set to a truthy value, + .env loading is disabled. + """ + if _load_dotenv_disabled(): + logger.debug( + "python-dotenv: .env loading disabled by PYTHON_DOTENV_DISABLED environment variable" + ) + return False + + if dotenv_path is None and stream is None: + dotenv_path = find_dotenv() + + dotenv = DotEnv( + dotenv_path=dotenv_path, + stream=stream, + verbose=verbose, + interpolate=interpolate, + override=override, + encoding=encoding, + ) + return dotenv.set_as_environment_variables() + + +def dotenv_values( + dotenv_path: Optional[StrPath] = None, + stream: Optional[IO[str]] = None, + verbose: bool = False, + interpolate: bool = True, + encoding: Optional[str] = "utf-8", +) -> Dict[str, Optional[str]]: + """ + Parse a .env file and return its content as a dict. + + The returned dict will have `None` values for keys without values in the .env file. + For example, `foo=bar` results in `{"foo": "bar"}` whereas `foo` alone results in + `{"foo": None}` + + Parameters: + dotenv_path: Absolute or relative path to the .env file. + stream: `StringIO` object with .env content, used if `dotenv_path` is `None`. + verbose: Whether to output a warning if the .env file is missing. + interpolate: Whether to interpolate variables using POSIX variable expansion. + encoding: Encoding to be used to read the file. + + If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the + .env file. + """ + if dotenv_path is None and stream is None: + dotenv_path = find_dotenv() + + return DotEnv( + dotenv_path=dotenv_path, + stream=stream, + verbose=verbose, + interpolate=interpolate, + override=True, + encoding=encoding, + ).dict() + + +def _is_file_or_fifo(path: StrPath) -> bool: + """ + Return True if `path` exists and is either a regular file or a FIFO. + """ + if os.path.isfile(path): + return True + + try: + st = os.stat(path) + except (FileNotFoundError, OSError): + return False + + return stat.S_ISFIFO(st.st_mode) diff --git a/source/target-vendors/cp312/dotenv/parser.py b/source/target-vendors/cp312/dotenv/parser.py new file mode 100644 index 0000000..02648ab --- /dev/null +++ b/source/target-vendors/cp312/dotenv/parser.py @@ -0,0 +1,184 @@ +import codecs +import re +from typing import ( + IO, + Iterator, + Match, + NamedTuple, + Optional, + Pattern, + Sequence, +) + + +def make_regex(string: str, extra_flags: int = 0) -> Pattern[str]: + return re.compile(string, re.UNICODE | extra_flags) + + +_newline = make_regex(r"(\r\n|\n|\r)") +_multiline_whitespace = make_regex(r"\s*", extra_flags=re.MULTILINE) +_whitespace = make_regex(r"[^\S\r\n]*") +_export = make_regex(r"(?:export[^\S\r\n]+)?") +_single_quoted_key = make_regex(r"'([^']+)'") +_unquoted_key = make_regex(r"([^=\#\s]+)") +_equal_sign = make_regex(r"(=[^\S\r\n]*)") +# A backslash always escapes the character after it, so that an escaped +# backslash (`\\`) is not mistaken for the start of an escaped quote. +_single_quoted_value = make_regex(r"'((?:\\.|[^'\\])*)'", extra_flags=re.DOTALL) +_double_quoted_value = make_regex(r'"((?:\\.|[^"\\])*)"', extra_flags=re.DOTALL) +_unquoted_value = make_regex(r"([^\r\n]*)") +_comment = make_regex(r"(?:[^\S\r\n]*#[^\r\n]*)?") +_end_of_line = make_regex(r"[^\S\r\n]*(?:\r\n|\n|\r|$)") +_rest_of_line = make_regex(r"[^\r\n]*(?:\r|\n|\r\n)?") +_double_quote_escapes = make_regex(r"\\[\\'\"abfnrtv]") +_single_quote_escapes = make_regex(r"\\[\\']") + + +class Original(NamedTuple): + string: str + line: int + + +class Binding(NamedTuple): + key: Optional[str] + value: Optional[str] + original: Original + error: bool + + +class Position: + def __init__(self, chars: int, line: int) -> None: + self.chars = chars + self.line = line + + @classmethod + def start(cls) -> "Position": + return cls(chars=0, line=1) + + def set(self, other: "Position") -> None: + self.chars = other.chars + self.line = other.line + + def advance(self, string: str) -> None: + self.chars += len(string) + self.line += len(re.findall(_newline, string)) + + +class Error(Exception): + pass + + +class Reader: + def __init__(self, stream: IO[str]) -> None: + self.string = stream.read().removeprefix("\ufeff") + self.position = Position.start() + self.mark = Position.start() + + def has_next(self) -> bool: + return self.position.chars < len(self.string) + + def set_mark(self) -> None: + self.mark.set(self.position) + + def get_marked(self) -> Original: + return Original( + string=self.string[self.mark.chars : self.position.chars], + line=self.mark.line, + ) + + def peek(self, count: int) -> str: + return self.string[self.position.chars : self.position.chars + count] + + def read(self, count: int) -> str: + result = self.string[self.position.chars : self.position.chars + count] + if len(result) < count: + raise Error("read: End of string") + self.position.advance(result) + return result + + def read_regex(self, regex: Pattern[str]) -> Sequence[str]: + match = regex.match(self.string, self.position.chars) + if match is None: + raise Error("read_regex: Pattern not found") + self.position.advance(self.string[match.start() : match.end()]) + return match.groups() + + +def decode_escapes(regex: Pattern[str], string: str) -> str: + def decode_match(match: Match[str]) -> str: + return codecs.decode(match.group(0), "unicode-escape") # type: ignore + + return regex.sub(decode_match, string) + + +def parse_key(reader: Reader) -> Optional[str]: + char = reader.peek(1) + if char == "#": + return None + elif char == "'": + (key,) = reader.read_regex(_single_quoted_key) + else: + (key,) = reader.read_regex(_unquoted_key) + return key + + +def parse_unquoted_value(reader: Reader) -> str: + (part,) = reader.read_regex(_unquoted_value) + return re.sub(r"\s+#.*", "", part).rstrip() + + +def parse_value(reader: Reader) -> str: + char = reader.peek(1) + if char == "'": + (value,) = reader.read_regex(_single_quoted_value) + return decode_escapes(_single_quote_escapes, value) + elif char == '"': + (value,) = reader.read_regex(_double_quoted_value) + return decode_escapes(_double_quote_escapes, value) + elif char in ("", "\n", "\r"): + return "" + else: + return parse_unquoted_value(reader) + + +def parse_binding(reader: Reader) -> Binding: + reader.set_mark() + try: + reader.read_regex(_multiline_whitespace) + if not reader.has_next(): + return Binding( + key=None, + value=None, + original=reader.get_marked(), + error=False, + ) + reader.read_regex(_export) + key = parse_key(reader) + reader.read_regex(_whitespace) + if reader.peek(1) == "=": + reader.read_regex(_equal_sign) + value: Optional[str] = parse_value(reader) + else: + value = None + reader.read_regex(_comment) + reader.read_regex(_end_of_line) + return Binding( + key=key, + value=value, + original=reader.get_marked(), + error=False, + ) + except Error: + reader.read_regex(_rest_of_line) + return Binding( + key=None, + value=None, + original=reader.get_marked(), + error=True, + ) + + +def parse_stream(stream: IO[str]) -> Iterator[Binding]: + reader = Reader(stream) + while reader.has_next(): + yield parse_binding(reader) diff --git a/source/target-vendors/cp312/dotenv/py.typed b/source/target-vendors/cp312/dotenv/py.typed new file mode 100644 index 0000000..7632ecf --- /dev/null +++ b/source/target-vendors/cp312/dotenv/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561 diff --git a/source/target-vendors/cp312/dotenv/variables.py b/source/target-vendors/cp312/dotenv/variables.py new file mode 100644 index 0000000..667f2f2 --- /dev/null +++ b/source/target-vendors/cp312/dotenv/variables.py @@ -0,0 +1,86 @@ +import re +from abc import ABCMeta, abstractmethod +from typing import Iterator, Mapping, Optional, Pattern + +_posix_variable: Pattern[str] = re.compile( + r""" + \$\{ + (?P[^\}:]*) + (?::- + (?P[^\}]*) + )? + \} + """, + re.VERBOSE, +) + + +class Atom(metaclass=ABCMeta): + def __ne__(self, other: object) -> bool: + result = self.__eq__(other) + if result is NotImplemented: + return NotImplemented + return not result + + @abstractmethod + def resolve(self, env: Mapping[str, Optional[str]]) -> str: ... + + +class Literal(Atom): + def __init__(self, value: str) -> None: + self.value = value + + def __repr__(self) -> str: + return f"Literal(value={self.value})" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, self.__class__): + return NotImplemented + return self.value == other.value + + def __hash__(self) -> int: + return hash((self.__class__, self.value)) + + def resolve(self, env: Mapping[str, Optional[str]]) -> str: + return self.value + + +class Variable(Atom): + def __init__(self, name: str, default: Optional[str]) -> None: + self.name = name + self.default = default + + def __repr__(self) -> str: + return f"Variable(name={self.name}, default={self.default})" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, self.__class__): + return NotImplemented + return (self.name, self.default) == (other.name, other.default) + + def __hash__(self) -> int: + return hash((self.__class__, self.name, self.default)) + + def resolve(self, env: Mapping[str, Optional[str]]) -> str: + default = self.default if self.default is not None else "" + result = env.get(self.name, default) + return result if result is not None else "" + + +def parse_variables(value: str) -> Iterator[Atom]: + cursor = 0 + + for match in _posix_variable.finditer(value): + (start, end) = match.span() + name = match["name"] + default = match["default"] + + if start > cursor: + yield Literal(value=value[cursor:start]) + + yield Variable(name=name, default=default) + cursor = end + + length = len(value) + if cursor < length: + yield Literal(value=value[cursor:length]) diff --git a/source/target-vendors/cp312/dotenv/version.py b/source/target-vendors/cp312/dotenv/version.py new file mode 100644 index 0000000..10aa336 --- /dev/null +++ b/source/target-vendors/cp312/dotenv/version.py @@ -0,0 +1 @@ +__version__ = "1.2.3" diff --git a/source/target-vendors/cp312/h11-0.16.0.dist-info/METADATA b/source/target-vendors/cp312/h11-0.16.0.dist-info/METADATA new file mode 100644 index 0000000..8a2f639 --- /dev/null +++ b/source/target-vendors/cp312/h11-0.16.0.dist-info/METADATA @@ -0,0 +1,202 @@ +Metadata-Version: 2.4 +Name: h11 +Version: 0.16.0 +Summary: A pure-Python, bring-your-own-I/O implementation of HTTP/1.1 +Home-page: https://github.com/python-hyper/h11 +Author: Nathaniel J. Smith +Author-email: njs@pobox.com +License: MIT +Classifier: Development Status :: 3 - Alpha +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Topic :: Internet :: WWW/HTTP +Classifier: Topic :: System :: Networking +Requires-Python: >=3.8 +License-File: LICENSE.txt +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: home-page +Dynamic: license +Dynamic: license-file +Dynamic: requires-python +Dynamic: summary + +h11 +=== + +.. image:: https://travis-ci.org/python-hyper/h11.svg?branch=master + :target: https://travis-ci.org/python-hyper/h11 + :alt: Automated test status + +.. image:: https://codecov.io/gh/python-hyper/h11/branch/master/graph/badge.svg + :target: https://codecov.io/gh/python-hyper/h11 + :alt: Test coverage + +.. image:: https://readthedocs.org/projects/h11/badge/?version=latest + :target: http://h11.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status + +This is a little HTTP/1.1 library written from scratch in Python, +heavily inspired by `hyper-h2 `_. + +It's a "bring-your-own-I/O" library; h11 contains no IO code +whatsoever. This means you can hook h11 up to your favorite network +API, and that could be anything you want: synchronous, threaded, +asynchronous, or your own implementation of `RFC 6214 +`_ -- h11 won't judge you. +(Compare this to the current state of the art, where every time a `new +network API `_ comes along then someone +gets to start over reimplementing the entire HTTP protocol from +scratch.) Cory Benfield made an `excellent blog post describing the +benefits of this approach +`_, or if you like video +then here's his `PyCon 2016 talk on the same theme +`_. + +This also means that h11 is not immediately useful out of the box: +it's a toolkit for building programs that speak HTTP, not something +that could directly replace ``requests`` or ``twisted.web`` or +whatever. But h11 makes it much easier to implement something like +``requests`` or ``twisted.web``. + +At a high level, working with h11 goes like this: + +1) First, create an ``h11.Connection`` object to track the state of a + single HTTP/1.1 connection. + +2) When you read data off the network, pass it to + ``conn.receive_data(...)``; you'll get back a list of objects + representing high-level HTTP "events". + +3) When you want to send a high-level HTTP event, create the + corresponding "event" object and pass it to ``conn.send(...)``; + this will give you back some bytes that you can then push out + through the network. + +For example, a client might instantiate and then send a +``h11.Request`` object, then zero or more ``h11.Data`` objects for the +request body (e.g., if this is a POST), and then a +``h11.EndOfMessage`` to indicate the end of the message. Then the +server would then send back a ``h11.Response``, some ``h11.Data``, and +its own ``h11.EndOfMessage``. If either side violates the protocol, +you'll get a ``h11.ProtocolError`` exception. + +h11 is suitable for implementing both servers and clients, and has a +pleasantly symmetric API: the events you send as a client are exactly +the ones that you receive as a server and vice-versa. + +`Here's an example of a tiny HTTP client +`_ + +It also has `a fine manual `_. + +FAQ +--- + +*Whyyyyy?* + +I wanted to play with HTTP in `Curio +`__ and `Trio +`__, which at the time didn't have any +HTTP libraries. So I thought, no big deal, Python has, like, a dozen +different implementations of HTTP, surely I can find one that's +reusable. I didn't find one, but I did find Cory's call-to-arms +blog-post. So I figured, well, fine, if I have to implement HTTP from +scratch, at least I can make sure no-one *else* has to ever again. + +*Should I use it?* + +Maybe. You should be aware that it's a very young project. But, it's +feature complete and has an exhaustive test-suite and complete docs, +so the next step is for people to try using it and see how it goes +:-). If you do then please let us know -- if nothing else we'll want +to talk to you before making any incompatible changes! + +*What are the features/limitations?* + +Roughly speaking, it's trying to be a robust, complete, and non-hacky +implementation of the first "chapter" of the HTTP/1.1 spec: `RFC 7230: +HTTP/1.1 Message Syntax and Routing +`_. That is, it mostly focuses on +implementing HTTP at the level of taking bytes on and off the wire, +and the headers related to that, and tries to be anal about spec +conformance. It doesn't know about higher-level concerns like URL +routing, conditional GETs, cross-origin cookie policies, or content +negotiation. But it does know how to take care of framing, +cross-version differences in keep-alive handling, and the "obsolete +line folding" rule, so you can focus your energies on the hard / +interesting parts for your application, and it tries to support the +full specification in the sense that any useful HTTP/1.1 conformant +application should be able to use h11. + +It's pure Python, and has no dependencies outside of the standard +library. + +It has a test suite with 100.0% coverage for both statements and +branches. + +Currently it supports Python 3 (testing on 3.8-3.12) and PyPy 3. +The last Python 2-compatible version was h11 0.11.x. +(Originally it had a Cython wrapper for `http-parser +`_ and a beautiful nested state +machine implemented with ``yield from`` to postprocess the output. But +I had to take these out -- the new *parser* needs fewer lines-of-code +than the old *parser wrapper*, is written in pure Python, uses no +exotic language syntax, and has more features. It's sad, really; that +old state machine was really slick. I just need a few sentences here +to mourn that.) + +I don't know how fast it is. I haven't benchmarked or profiled it yet, +so it's probably got a few pointless hot spots, and I've been trying +to err on the side of simplicity and robustness instead of +micro-optimization. But at the architectural level I tried hard to +avoid fundamentally bad decisions, e.g., I believe that all the +parsing algorithms remain linear-time even in the face of pathological +input like slowloris, and there are no byte-by-byte loops. (I also +believe that it maintains bounded memory usage in the face of +arbitrary/pathological input.) + +The whole library is ~800 lines-of-code. You can read and understand +the whole thing in less than an hour. Most of the energy invested in +this so far has been spent on trying to keep things simple by +minimizing special-cases and ad hoc state manipulation; even though it +is now quite small and simple, I'm still annoyed that I haven't +figured out how to make it even smaller and simpler. (Unfortunately, +HTTP does not lend itself to simplicity.) + +The API is ~feature complete and I don't expect the general outlines +to change much, but you can't judge an API's ergonomics until you +actually document and use it, so I'd expect some changes in the +details. + +*How do I try it?* + +.. code-block:: sh + + $ pip install h11 + $ git clone git@github.com:python-hyper/h11 + $ cd h11/examples + $ python basic-client.py + +and go from there. + +*License?* + +MIT + +*Code of conduct?* + +Contributors are requested to follow our `code of conduct +`_ in +all project spaces. diff --git a/source/target-vendors/cp312/h11-0.16.0.dist-info/RECORD b/source/target-vendors/cp312/h11-0.16.0.dist-info/RECORD new file mode 100644 index 0000000..0ec9e3e --- /dev/null +++ b/source/target-vendors/cp312/h11-0.16.0.dist-info/RECORD @@ -0,0 +1,17 @@ +h11/__init__.py,sha256=iO1KzkSO42yZ6ffg-VMgbx_ZVTWGUY00nRYEWn-s3kY,1507 +h11/_abnf.py,sha256=ybixr0xsupnkA6GFAyMubuXF6Tc1lb_hF890NgCsfNc,4815 +h11/_connection.py,sha256=k9YRVf6koZqbttBW36xSWaJpWdZwa-xQVU9AHEo9DuI,26863 +h11/_events.py,sha256=I97aXoal1Wu7dkL548BANBUCkOIbe-x5CioYA9IBY14,11792 +h11/_headers.py,sha256=P7D-lBNxHwdLZPLimmYwrPG-9ZkjElvvJZJdZAgSP-4,10412 +h11/_readers.py,sha256=a4RypORUCC3d0q_kxPuBIM7jTD8iLt5X91TH0FsduN4,8590 +h11/_receivebuffer.py,sha256=xrspsdsNgWFxRfQcTXxR8RrdjRXXTK0Io5cQYWpJ1Ws,5252 +h11/_state.py,sha256=_5LG_BGR8FCcFQeBPH-TMHgm_-B-EUcWCnQof_9XjFE,13231 +h11/_util.py,sha256=LWkkjXyJaFlAy6Lt39w73UStklFT5ovcvo0TkY7RYuk,4888 +h11/_version.py,sha256=GVSsbPSPDcOuF6ptfIiXnVJoaEm3ygXbMnqlr_Giahw,686 +h11/_writers.py,sha256=oFKm6PtjeHfbj4RLX7VB7KDc1gIY53gXG3_HR9ltmTA,5081 +h11/py.typed,sha256=sow9soTwP9T_gEAQSVh7Gb8855h04Nwmhs2We-JRgZM,7 +h11-0.16.0.dist-info/licenses/LICENSE.txt,sha256=N9tbuFkm2yikJ6JYZ_ELEjIAOuob5pzLhRE4rbjm82E,1124 +h11-0.16.0.dist-info/METADATA,sha256=KPMmCYrAn8unm48YD5YIfIQf4kViFct7hyqcfVzRnWQ,8348 +h11-0.16.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91 +h11-0.16.0.dist-info/top_level.txt,sha256=F7dC4jl3zeh8TGHEPaWJrMbeuoWbS379Gwdi-Yvdcis,4 +h11-0.16.0.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/h11-0.16.0.dist-info/WHEEL b/source/target-vendors/cp312/h11-0.16.0.dist-info/WHEEL new file mode 100644 index 0000000..1eb3c49 --- /dev/null +++ b/source/target-vendors/cp312/h11-0.16.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (78.1.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/source/target-vendors/cp312/h11-0.16.0.dist-info/licenses/LICENSE.txt b/source/target-vendors/cp312/h11-0.16.0.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000..8f080ea --- /dev/null +++ b/source/target-vendors/cp312/h11-0.16.0.dist-info/licenses/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2016 Nathaniel J. Smith and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/source/target-vendors/cp312/h11-0.16.0.dist-info/top_level.txt b/source/target-vendors/cp312/h11-0.16.0.dist-info/top_level.txt new file mode 100644 index 0000000..0d24def --- /dev/null +++ b/source/target-vendors/cp312/h11-0.16.0.dist-info/top_level.txt @@ -0,0 +1 @@ +h11 diff --git a/source/target-vendors/cp312/h11/__init__.py b/source/target-vendors/cp312/h11/__init__.py new file mode 100644 index 0000000..989e92c --- /dev/null +++ b/source/target-vendors/cp312/h11/__init__.py @@ -0,0 +1,62 @@ +# A highish-level implementation of the HTTP/1.1 wire protocol (RFC 7230), +# containing no networking code at all, loosely modelled on hyper-h2's generic +# implementation of HTTP/2 (and in particular the h2.connection.H2Connection +# class). There's still a bunch of subtle details you need to get right if you +# want to make this actually useful, because it doesn't implement all the +# semantics to check that what you're asking to write to the wire is sensible, +# but at least it gets you out of dealing with the wire itself. + +from h11._connection import Connection, NEED_DATA, PAUSED +from h11._events import ( + ConnectionClosed, + Data, + EndOfMessage, + Event, + InformationalResponse, + Request, + Response, +) +from h11._state import ( + CLIENT, + CLOSED, + DONE, + ERROR, + IDLE, + MIGHT_SWITCH_PROTOCOL, + MUST_CLOSE, + SEND_BODY, + SEND_RESPONSE, + SERVER, + SWITCHED_PROTOCOL, +) +from h11._util import LocalProtocolError, ProtocolError, RemoteProtocolError +from h11._version import __version__ + +PRODUCT_ID = "python-h11/" + __version__ + + +__all__ = ( + "Connection", + "NEED_DATA", + "PAUSED", + "ConnectionClosed", + "Data", + "EndOfMessage", + "Event", + "InformationalResponse", + "Request", + "Response", + "CLIENT", + "CLOSED", + "DONE", + "ERROR", + "IDLE", + "MUST_CLOSE", + "SEND_BODY", + "SEND_RESPONSE", + "SERVER", + "SWITCHED_PROTOCOL", + "ProtocolError", + "LocalProtocolError", + "RemoteProtocolError", +) diff --git a/source/target-vendors/cp312/h11/_abnf.py b/source/target-vendors/cp312/h11/_abnf.py new file mode 100644 index 0000000..933587f --- /dev/null +++ b/source/target-vendors/cp312/h11/_abnf.py @@ -0,0 +1,132 @@ +# We use native strings for all the re patterns, to take advantage of string +# formatting, and then convert to bytestrings when compiling the final re +# objects. + +# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#whitespace +# OWS = *( SP / HTAB ) +# ; optional whitespace +OWS = r"[ \t]*" + +# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#rule.token.separators +# token = 1*tchar +# +# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" +# / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" +# / DIGIT / ALPHA +# ; any VCHAR, except delimiters +token = r"[-!#$%&'*+.^_`|~0-9a-zA-Z]+" + +# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#header.fields +# field-name = token +field_name = token + +# The standard says: +# +# field-value = *( field-content / obs-fold ) +# field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] +# field-vchar = VCHAR / obs-text +# obs-fold = CRLF 1*( SP / HTAB ) +# ; obsolete line folding +# ; see Section 3.2.4 +# +# https://tools.ietf.org/html/rfc5234#appendix-B.1 +# +# VCHAR = %x21-7E +# ; visible (printing) characters +# +# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#rule.quoted-string +# obs-text = %x80-FF +# +# However, the standard definition of field-content is WRONG! It disallows +# fields containing a single visible character surrounded by whitespace, +# e.g. "foo a bar". +# +# See: https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4189 +# +# So our definition of field_content attempts to fix it up... +# +# Also, we allow lots of control characters, because apparently people assume +# that they're legal in practice (e.g., google analytics makes cookies with +# \x01 in them!): +# https://github.com/python-hyper/h11/issues/57 +# We still don't allow NUL or whitespace, because those are often treated as +# meta-characters and letting them through can lead to nasty issues like SSRF. +vchar = r"[\x21-\x7e]" +vchar_or_obs_text = r"[^\x00\s]" +field_vchar = vchar_or_obs_text +field_content = r"{field_vchar}+(?:[ \t]+{field_vchar}+)*".format(**globals()) + +# We handle obs-fold at a different level, and our fixed-up field_content +# already grows to swallow the whole value, so ? instead of * +field_value = r"({field_content})?".format(**globals()) + +# header-field = field-name ":" OWS field-value OWS +header_field = ( + r"(?P{field_name})" + r":" + r"{OWS}" + r"(?P{field_value})" + r"{OWS}".format(**globals()) +) + +# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#request.line +# +# request-line = method SP request-target SP HTTP-version CRLF +# method = token +# HTTP-version = HTTP-name "/" DIGIT "." DIGIT +# HTTP-name = %x48.54.54.50 ; "HTTP", case-sensitive +# +# request-target is complicated (see RFC 7230 sec 5.3) -- could be path, full +# URL, host+port (for connect), or even "*", but in any case we are guaranteed +# that it contists of the visible printing characters. +method = token +request_target = r"{vchar}+".format(**globals()) +http_version = r"HTTP/(?P[0-9]\.[0-9])" +request_line = ( + r"(?P{method})" + r" " + r"(?P{request_target})" + r" " + r"{http_version}".format(**globals()) +) + +# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#status.line +# +# status-line = HTTP-version SP status-code SP reason-phrase CRLF +# status-code = 3DIGIT +# reason-phrase = *( HTAB / SP / VCHAR / obs-text ) +status_code = r"[0-9]{3}" +reason_phrase = r"([ \t]|{vchar_or_obs_text})*".format(**globals()) +status_line = ( + r"{http_version}" + r" " + r"(?P{status_code})" + # However, there are apparently a few too many servers out there that just + # leave out the reason phrase: + # https://github.com/scrapy/scrapy/issues/345#issuecomment-281756036 + # https://github.com/seanmonstar/httparse/issues/29 + # so make it optional. ?: is a non-capturing group. + r"(?: (?P{reason_phrase}))?".format(**globals()) +) + +HEXDIG = r"[0-9A-Fa-f]" +# Actually +# +# chunk-size = 1*HEXDIG +# +# but we impose an upper-limit to avoid ridiculosity. len(str(2**64)) == 20 +chunk_size = r"({HEXDIG}){{1,20}}".format(**globals()) +# Actually +# +# chunk-ext = *( ";" chunk-ext-name [ "=" chunk-ext-val ] ) +# +# but we aren't parsing the things so we don't really care. +chunk_ext = r";.*" +chunk_header = ( + r"(?P{chunk_size})" + r"(?P{chunk_ext})?" + r"{OWS}\r\n".format( + **globals() + ) # Even though the specification does not allow for extra whitespaces, + # we are lenient with trailing whitespaces because some servers on the wild use it. +) diff --git a/source/target-vendors/cp312/h11/_connection.py b/source/target-vendors/cp312/h11/_connection.py new file mode 100644 index 0000000..e37d82a --- /dev/null +++ b/source/target-vendors/cp312/h11/_connection.py @@ -0,0 +1,659 @@ +# This contains the main Connection class. Everything in h11 revolves around +# this. +from typing import ( + Any, + Callable, + cast, + Dict, + List, + Optional, + overload, + Tuple, + Type, + Union, +) + +from ._events import ( + ConnectionClosed, + Data, + EndOfMessage, + Event, + InformationalResponse, + Request, + Response, +) +from ._headers import get_comma_header, has_expect_100_continue, set_comma_header +from ._readers import READERS, ReadersType +from ._receivebuffer import ReceiveBuffer +from ._state import ( + _SWITCH_CONNECT, + _SWITCH_UPGRADE, + CLIENT, + ConnectionState, + DONE, + ERROR, + MIGHT_SWITCH_PROTOCOL, + SEND_BODY, + SERVER, + SWITCHED_PROTOCOL, +) +from ._util import ( # Import the internal things we need + LocalProtocolError, + RemoteProtocolError, + Sentinel, +) +from ._writers import WRITERS, WritersType + +# Everything in __all__ gets re-exported as part of the h11 public API. +__all__ = ["Connection", "NEED_DATA", "PAUSED"] + + +class NEED_DATA(Sentinel, metaclass=Sentinel): + pass + + +class PAUSED(Sentinel, metaclass=Sentinel): + pass + + +# If we ever have this much buffered without it making a complete parseable +# event, we error out. The only time we really buffer is when reading the +# request/response line + headers together, so this is effectively the limit on +# the size of that. +# +# Some precedents for defaults: +# - node.js: 80 * 1024 +# - tomcat: 8 * 1024 +# - IIS: 16 * 1024 +# - Apache: <8 KiB per line> +DEFAULT_MAX_INCOMPLETE_EVENT_SIZE = 16 * 1024 + + +# RFC 7230's rules for connection lifecycles: +# - If either side says they want to close the connection, then the connection +# must close. +# - HTTP/1.1 defaults to keep-alive unless someone says Connection: close +# - HTTP/1.0 defaults to close unless both sides say Connection: keep-alive +# (and even this is a mess -- e.g. if you're implementing a proxy then +# sending Connection: keep-alive is forbidden). +# +# We simplify life by simply not supporting keep-alive with HTTP/1.0 peers. So +# our rule is: +# - If someone says Connection: close, we will close +# - If someone uses HTTP/1.0, we will close. +def _keep_alive(event: Union[Request, Response]) -> bool: + connection = get_comma_header(event.headers, b"connection") + if b"close" in connection: + return False + if getattr(event, "http_version", b"1.1") < b"1.1": + return False + return True + + +def _body_framing( + request_method: bytes, event: Union[Request, Response] +) -> Tuple[str, Union[Tuple[()], Tuple[int]]]: + # Called when we enter SEND_BODY to figure out framing information for + # this body. + # + # These are the only two events that can trigger a SEND_BODY state: + assert type(event) in (Request, Response) + # Returns one of: + # + # ("content-length", count) + # ("chunked", ()) + # ("http/1.0", ()) + # + # which are (lookup key, *args) for constructing body reader/writer + # objects. + # + # Reference: https://tools.ietf.org/html/rfc7230#section-3.3.3 + # + # Step 1: some responses always have an empty body, regardless of what the + # headers say. + if type(event) is Response: + if ( + event.status_code in (204, 304) + or request_method == b"HEAD" + or (request_method == b"CONNECT" and 200 <= event.status_code < 300) + ): + return ("content-length", (0,)) + # Section 3.3.3 also lists another case -- responses with status_code + # < 200. For us these are InformationalResponses, not Responses, so + # they can't get into this function in the first place. + assert event.status_code >= 200 + + # Step 2: check for Transfer-Encoding (T-E beats C-L): + transfer_encodings = get_comma_header(event.headers, b"transfer-encoding") + if transfer_encodings: + assert transfer_encodings == [b"chunked"] + return ("chunked", ()) + + # Step 3: check for Content-Length + content_lengths = get_comma_header(event.headers, b"content-length") + if content_lengths: + return ("content-length", (int(content_lengths[0]),)) + + # Step 4: no applicable headers; fallback/default depends on type + if type(event) is Request: + return ("content-length", (0,)) + else: + return ("http/1.0", ()) + + +################################################################ +# +# The main Connection class +# +################################################################ + + +class Connection: + """An object encapsulating the state of an HTTP connection. + + Args: + our_role: If you're implementing a client, pass :data:`h11.CLIENT`. If + you're implementing a server, pass :data:`h11.SERVER`. + + max_incomplete_event_size (int): + The maximum number of bytes we're willing to buffer of an + incomplete event. In practice this mostly sets a limit on the + maximum size of the request/response line + headers. If this is + exceeded, then :meth:`next_event` will raise + :exc:`RemoteProtocolError`. + + """ + + def __init__( + self, + our_role: Type[Sentinel], + max_incomplete_event_size: int = DEFAULT_MAX_INCOMPLETE_EVENT_SIZE, + ) -> None: + self._max_incomplete_event_size = max_incomplete_event_size + # State and role tracking + if our_role not in (CLIENT, SERVER): + raise ValueError(f"expected CLIENT or SERVER, not {our_role!r}") + self.our_role = our_role + self.their_role: Type[Sentinel] + if our_role is CLIENT: + self.their_role = SERVER + else: + self.their_role = CLIENT + self._cstate = ConnectionState() + + # Callables for converting data->events or vice-versa given the + # current state + self._writer = self._get_io_object(self.our_role, None, WRITERS) + self._reader = self._get_io_object(self.their_role, None, READERS) + + # Holds any unprocessed received data + self._receive_buffer = ReceiveBuffer() + # If this is true, then it indicates that the incoming connection was + # closed *after* the end of whatever's in self._receive_buffer: + self._receive_buffer_closed = False + + # Extra bits of state that don't fit into the state machine. + # + # These two are only used to interpret framing headers for figuring + # out how to read/write response bodies. their_http_version is also + # made available as a convenient public API. + self.their_http_version: Optional[bytes] = None + self._request_method: Optional[bytes] = None + # This is pure flow-control and doesn't at all affect the set of legal + # transitions, so no need to bother ConnectionState with it: + self.client_is_waiting_for_100_continue = False + + @property + def states(self) -> Dict[Type[Sentinel], Type[Sentinel]]: + """A dictionary like:: + + {CLIENT: , SERVER: } + + See :ref:`state-machine` for details. + + """ + return dict(self._cstate.states) + + @property + def our_state(self) -> Type[Sentinel]: + """The current state of whichever role we are playing. See + :ref:`state-machine` for details. + """ + return self._cstate.states[self.our_role] + + @property + def their_state(self) -> Type[Sentinel]: + """The current state of whichever role we are NOT playing. See + :ref:`state-machine` for details. + """ + return self._cstate.states[self.their_role] + + @property + def they_are_waiting_for_100_continue(self) -> bool: + return self.their_role is CLIENT and self.client_is_waiting_for_100_continue + + def start_next_cycle(self) -> None: + """Attempt to reset our connection state for a new request/response + cycle. + + If both client and server are in :data:`DONE` state, then resets them + both to :data:`IDLE` state in preparation for a new request/response + cycle on this same connection. Otherwise, raises a + :exc:`LocalProtocolError`. + + See :ref:`keepalive-and-pipelining`. + + """ + old_states = dict(self._cstate.states) + self._cstate.start_next_cycle() + self._request_method = None + # self.their_http_version gets left alone, since it presumably lasts + # beyond a single request/response cycle + assert not self.client_is_waiting_for_100_continue + self._respond_to_state_changes(old_states) + + def _process_error(self, role: Type[Sentinel]) -> None: + old_states = dict(self._cstate.states) + self._cstate.process_error(role) + self._respond_to_state_changes(old_states) + + def _server_switch_event(self, event: Event) -> Optional[Type[Sentinel]]: + if type(event) is InformationalResponse and event.status_code == 101: + return _SWITCH_UPGRADE + if type(event) is Response: + if ( + _SWITCH_CONNECT in self._cstate.pending_switch_proposals + and 200 <= event.status_code < 300 + ): + return _SWITCH_CONNECT + return None + + # All events go through here + def _process_event(self, role: Type[Sentinel], event: Event) -> None: + # First, pass the event through the state machine to make sure it + # succeeds. + old_states = dict(self._cstate.states) + if role is CLIENT and type(event) is Request: + if event.method == b"CONNECT": + self._cstate.process_client_switch_proposal(_SWITCH_CONNECT) + if get_comma_header(event.headers, b"upgrade"): + self._cstate.process_client_switch_proposal(_SWITCH_UPGRADE) + server_switch_event = None + if role is SERVER: + server_switch_event = self._server_switch_event(event) + self._cstate.process_event(role, type(event), server_switch_event) + + # Then perform the updates triggered by it. + + if type(event) is Request: + self._request_method = event.method + + if role is self.their_role and type(event) in ( + Request, + Response, + InformationalResponse, + ): + event = cast(Union[Request, Response, InformationalResponse], event) + self.their_http_version = event.http_version + + # Keep alive handling + # + # RFC 7230 doesn't really say what one should do if Connection: close + # shows up on a 1xx InformationalResponse. I think the idea is that + # this is not supposed to happen. In any case, if it does happen, we + # ignore it. + if type(event) in (Request, Response) and not _keep_alive( + cast(Union[Request, Response], event) + ): + self._cstate.process_keep_alive_disabled() + + # 100-continue + if type(event) is Request and has_expect_100_continue(event): + self.client_is_waiting_for_100_continue = True + if type(event) in (InformationalResponse, Response): + self.client_is_waiting_for_100_continue = False + if role is CLIENT and type(event) in (Data, EndOfMessage): + self.client_is_waiting_for_100_continue = False + + self._respond_to_state_changes(old_states, event) + + def _get_io_object( + self, + role: Type[Sentinel], + event: Optional[Event], + io_dict: Union[ReadersType, WritersType], + ) -> Optional[Callable[..., Any]]: + # event may be None; it's only used when entering SEND_BODY + state = self._cstate.states[role] + if state is SEND_BODY: + # Special case: the io_dict has a dict of reader/writer factories + # that depend on the request/response framing. + framing_type, args = _body_framing( + cast(bytes, self._request_method), cast(Union[Request, Response], event) + ) + return io_dict[SEND_BODY][framing_type](*args) # type: ignore[index] + else: + # General case: the io_dict just has the appropriate reader/writer + # for this state + return io_dict.get((role, state)) # type: ignore[return-value] + + # This must be called after any action that might have caused + # self._cstate.states to change. + def _respond_to_state_changes( + self, + old_states: Dict[Type[Sentinel], Type[Sentinel]], + event: Optional[Event] = None, + ) -> None: + # Update reader/writer + if self.our_state != old_states[self.our_role]: + self._writer = self._get_io_object(self.our_role, event, WRITERS) + if self.their_state != old_states[self.their_role]: + self._reader = self._get_io_object(self.their_role, event, READERS) + + @property + def trailing_data(self) -> Tuple[bytes, bool]: + """Data that has been received, but not yet processed, represented as + a tuple with two elements, where the first is a byte-string containing + the unprocessed data itself, and the second is a bool that is True if + the receive connection was closed. + + See :ref:`switching-protocols` for discussion of why you'd want this. + """ + return (bytes(self._receive_buffer), self._receive_buffer_closed) + + def receive_data(self, data: bytes) -> None: + """Add data to our internal receive buffer. + + This does not actually do any processing on the data, just stores + it. To trigger processing, you have to call :meth:`next_event`. + + Args: + data (:term:`bytes-like object`): + The new data that was just received. + + Special case: If *data* is an empty byte-string like ``b""``, + then this indicates that the remote side has closed the + connection (end of file). Normally this is convenient, because + standard Python APIs like :meth:`file.read` or + :meth:`socket.recv` use ``b""`` to indicate end-of-file, while + other failures to read are indicated using other mechanisms + like raising :exc:`TimeoutError`. When using such an API you + can just blindly pass through whatever you get from ``read`` + to :meth:`receive_data`, and everything will work. + + But, if you have an API where reading an empty string is a + valid non-EOF condition, then you need to be aware of this and + make sure to check for such strings and avoid passing them to + :meth:`receive_data`. + + Returns: + Nothing, but after calling this you should call :meth:`next_event` + to parse the newly received data. + + Raises: + RuntimeError: + Raised if you pass an empty *data*, indicating EOF, and then + pass a non-empty *data*, indicating more data that somehow + arrived after the EOF. + + (Calling ``receive_data(b"")`` multiple times is fine, + and equivalent to calling it once.) + + """ + if data: + if self._receive_buffer_closed: + raise RuntimeError("received close, then received more data?") + self._receive_buffer += data + else: + self._receive_buffer_closed = True + + def _extract_next_receive_event( + self, + ) -> Union[Event, Type[NEED_DATA], Type[PAUSED]]: + state = self.their_state + # We don't pause immediately when they enter DONE, because even in + # DONE state we can still process a ConnectionClosed() event. But + # if we have data in our buffer, then we definitely aren't getting + # a ConnectionClosed() immediately and we need to pause. + if state is DONE and self._receive_buffer: + return PAUSED + if state is MIGHT_SWITCH_PROTOCOL or state is SWITCHED_PROTOCOL: + return PAUSED + assert self._reader is not None + event = self._reader(self._receive_buffer) + if event is None: + if not self._receive_buffer and self._receive_buffer_closed: + # In some unusual cases (basically just HTTP/1.0 bodies), EOF + # triggers an actual protocol event; in that case, we want to + # return that event, and then the state will change and we'll + # get called again to generate the actual ConnectionClosed(). + if hasattr(self._reader, "read_eof"): + event = self._reader.read_eof() + else: + event = ConnectionClosed() + if event is None: + event = NEED_DATA + return event # type: ignore[no-any-return] + + def next_event(self) -> Union[Event, Type[NEED_DATA], Type[PAUSED]]: + """Parse the next event out of our receive buffer, update our internal + state, and return it. + + This is a mutating operation -- think of it like calling :func:`next` + on an iterator. + + Returns: + : One of three things: + + 1) An event object -- see :ref:`events`. + + 2) The special constant :data:`NEED_DATA`, which indicates that + you need to read more data from your socket and pass it to + :meth:`receive_data` before this method will be able to return + any more events. + + 3) The special constant :data:`PAUSED`, which indicates that we + are not in a state where we can process incoming data (usually + because the peer has finished their part of the current + request/response cycle, and you have not yet called + :meth:`start_next_cycle`). See :ref:`flow-control` for details. + + Raises: + RemoteProtocolError: + The peer has misbehaved. You should close the connection + (possibly after sending some kind of 4xx response). + + Once this method returns :class:`ConnectionClosed` once, then all + subsequent calls will also return :class:`ConnectionClosed`. + + If this method raises any exception besides :exc:`RemoteProtocolError` + then that's a bug -- if it happens please file a bug report! + + If this method raises any exception then it also sets + :attr:`Connection.their_state` to :data:`ERROR` -- see + :ref:`error-handling` for discussion. + + """ + + if self.their_state is ERROR: + raise RemoteProtocolError("Can't receive data when peer state is ERROR") + try: + event = self._extract_next_receive_event() + if event not in [NEED_DATA, PAUSED]: + self._process_event(self.their_role, cast(Event, event)) + if event is NEED_DATA: + if len(self._receive_buffer) > self._max_incomplete_event_size: + # 431 is "Request header fields too large" which is pretty + # much the only situation where we can get here + raise RemoteProtocolError( + "Receive buffer too long", error_status_hint=431 + ) + if self._receive_buffer_closed: + # We're still trying to complete some event, but that's + # never going to happen because no more data is coming + raise RemoteProtocolError("peer unexpectedly closed connection") + return event + except BaseException as exc: + self._process_error(self.their_role) + if isinstance(exc, LocalProtocolError): + exc._reraise_as_remote_protocol_error() + else: + raise + + @overload + def send(self, event: ConnectionClosed) -> None: + ... + + @overload + def send( + self, event: Union[Request, InformationalResponse, Response, Data, EndOfMessage] + ) -> bytes: + ... + + @overload + def send(self, event: Event) -> Optional[bytes]: + ... + + def send(self, event: Event) -> Optional[bytes]: + """Convert a high-level event into bytes that can be sent to the peer, + while updating our internal state machine. + + Args: + event: The :ref:`event ` to send. + + Returns: + If ``type(event) is ConnectionClosed``, then returns + ``None``. Otherwise, returns a :term:`bytes-like object`. + + Raises: + LocalProtocolError: + Sending this event at this time would violate our + understanding of the HTTP/1.1 protocol. + + If this method raises any exception then it also sets + :attr:`Connection.our_state` to :data:`ERROR` -- see + :ref:`error-handling` for discussion. + + """ + data_list = self.send_with_data_passthrough(event) + if data_list is None: + return None + else: + return b"".join(data_list) + + def send_with_data_passthrough(self, event: Event) -> Optional[List[bytes]]: + """Identical to :meth:`send`, except that in situations where + :meth:`send` returns a single :term:`bytes-like object`, this instead + returns a list of them -- and when sending a :class:`Data` event, this + list is guaranteed to contain the exact object you passed in as + :attr:`Data.data`. See :ref:`sendfile` for discussion. + + """ + if self.our_state is ERROR: + raise LocalProtocolError("Can't send data when our state is ERROR") + try: + if type(event) is Response: + event = self._clean_up_response_headers_for_sending(event) + # We want to call _process_event before calling the writer, + # because if someone tries to do something invalid then this will + # give a sensible error message, while our writers all just assume + # they will only receive valid events. But, _process_event might + # change self._writer. So we have to do a little dance: + writer = self._writer + self._process_event(self.our_role, event) + if type(event) is ConnectionClosed: + return None + else: + # In any situation where writer is None, process_event should + # have raised ProtocolError + assert writer is not None + data_list: List[bytes] = [] + writer(event, data_list.append) + return data_list + except: + self._process_error(self.our_role) + raise + + def send_failed(self) -> None: + """Notify the state machine that we failed to send the data it gave + us. + + This causes :attr:`Connection.our_state` to immediately become + :data:`ERROR` -- see :ref:`error-handling` for discussion. + + """ + self._process_error(self.our_role) + + # When sending a Response, we take responsibility for a few things: + # + # - Sometimes you MUST set Connection: close. We take care of those + # times. (You can also set it yourself if you want, and if you do then + # we'll respect that and close the connection at the right time. But you + # don't have to worry about that unless you want to.) + # + # - The user has to set Content-Length if they want it. Otherwise, for + # responses that have bodies (e.g. not HEAD), then we will automatically + # select the right mechanism for streaming a body of unknown length, + # which depends on depending on the peer's HTTP version. + # + # This function's *only* responsibility is making sure headers are set up + # right -- everything downstream just looks at the headers. There are no + # side channels. + def _clean_up_response_headers_for_sending(self, response: Response) -> Response: + assert type(response) is Response + + headers = response.headers + need_close = False + + # HEAD requests need some special handling: they always act like they + # have Content-Length: 0, and that's how _body_framing treats + # them. But their headers are supposed to match what we would send if + # the request was a GET. (Technically there is one deviation allowed: + # we're allowed to leave out the framing headers -- see + # https://tools.ietf.org/html/rfc7231#section-4.3.2 . But it's just as + # easy to get them right.) + method_for_choosing_headers = cast(bytes, self._request_method) + if method_for_choosing_headers == b"HEAD": + method_for_choosing_headers = b"GET" + framing_type, _ = _body_framing(method_for_choosing_headers, response) + if framing_type in ("chunked", "http/1.0"): + # This response has a body of unknown length. + # If our peer is HTTP/1.1, we use Transfer-Encoding: chunked + # If our peer is HTTP/1.0, we use no framing headers, and close the + # connection afterwards. + # + # Make sure to clear Content-Length (in principle user could have + # set both and then we ignored Content-Length b/c + # Transfer-Encoding overwrote it -- this would be naughty of them, + # but the HTTP spec says that if our peer does this then we have + # to fix it instead of erroring out, so we'll accord the user the + # same respect). + headers = set_comma_header(headers, b"content-length", []) + if self.their_http_version is None or self.their_http_version < b"1.1": + # Either we never got a valid request and are sending back an + # error (their_http_version is None), so we assume the worst; + # or else we did get a valid HTTP/1.0 request, so we know that + # they don't understand chunked encoding. + headers = set_comma_header(headers, b"transfer-encoding", []) + # This is actually redundant ATM, since currently we + # unconditionally disable keep-alive when talking to HTTP/1.0 + # peers. But let's be defensive just in case we add + # Connection: keep-alive support later: + if self._request_method != b"HEAD": + need_close = True + else: + headers = set_comma_header(headers, b"transfer-encoding", [b"chunked"]) + + if not self._cstate.keep_alive or need_close: + # Make sure Connection: close is set + connection = set(get_comma_header(headers, b"connection")) + connection.discard(b"keep-alive") + connection.add(b"close") + headers = set_comma_header(headers, b"connection", sorted(connection)) + + return Response( + headers=headers, + status_code=response.status_code, + http_version=response.http_version, + reason=response.reason, + ) diff --git a/source/target-vendors/cp312/h11/_events.py b/source/target-vendors/cp312/h11/_events.py new file mode 100644 index 0000000..ca1c3ad --- /dev/null +++ b/source/target-vendors/cp312/h11/_events.py @@ -0,0 +1,369 @@ +# High level events that make up HTTP/1.1 conversations. Loosely inspired by +# the corresponding events in hyper-h2: +# +# http://python-hyper.org/h2/en/stable/api.html#events +# +# Don't subclass these. Stuff will break. + +import re +from abc import ABC +from dataclasses import dataclass +from typing import List, Tuple, Union + +from ._abnf import method, request_target +from ._headers import Headers, normalize_and_validate +from ._util import bytesify, LocalProtocolError, validate + +# Everything in __all__ gets re-exported as part of the h11 public API. +__all__ = [ + "Event", + "Request", + "InformationalResponse", + "Response", + "Data", + "EndOfMessage", + "ConnectionClosed", +] + +method_re = re.compile(method.encode("ascii")) +request_target_re = re.compile(request_target.encode("ascii")) + + +class Event(ABC): + """ + Base class for h11 events. + """ + + __slots__ = () + + +@dataclass(init=False, frozen=True) +class Request(Event): + """The beginning of an HTTP request. + + Fields: + + .. attribute:: method + + An HTTP method, e.g. ``b"GET"`` or ``b"POST"``. Always a byte + string. :term:`Bytes-like objects ` and native + strings containing only ascii characters will be automatically + converted to byte strings. + + .. attribute:: target + + The target of an HTTP request, e.g. ``b"/index.html"``, or one of the + more exotic formats described in `RFC 7320, section 5.3 + `_. Always a byte + string. :term:`Bytes-like objects ` and native + strings containing only ascii characters will be automatically + converted to byte strings. + + .. attribute:: headers + + Request headers, represented as a list of (name, value) pairs. See + :ref:`the header normalization rules ` for details. + + .. attribute:: http_version + + The HTTP protocol version, represented as a byte string like + ``b"1.1"``. See :ref:`the HTTP version normalization rules + ` for details. + + """ + + __slots__ = ("method", "headers", "target", "http_version") + + method: bytes + headers: Headers + target: bytes + http_version: bytes + + def __init__( + self, + *, + method: Union[bytes, str], + headers: Union[Headers, List[Tuple[bytes, bytes]], List[Tuple[str, str]]], + target: Union[bytes, str], + http_version: Union[bytes, str] = b"1.1", + _parsed: bool = False, + ) -> None: + super().__init__() + if isinstance(headers, Headers): + object.__setattr__(self, "headers", headers) + else: + object.__setattr__( + self, "headers", normalize_and_validate(headers, _parsed=_parsed) + ) + if not _parsed: + object.__setattr__(self, "method", bytesify(method)) + object.__setattr__(self, "target", bytesify(target)) + object.__setattr__(self, "http_version", bytesify(http_version)) + else: + object.__setattr__(self, "method", method) + object.__setattr__(self, "target", target) + object.__setattr__(self, "http_version", http_version) + + # "A server MUST respond with a 400 (Bad Request) status code to any + # HTTP/1.1 request message that lacks a Host header field and to any + # request message that contains more than one Host header field or a + # Host header field with an invalid field-value." + # -- https://tools.ietf.org/html/rfc7230#section-5.4 + host_count = 0 + for name, value in self.headers: + if name == b"host": + host_count += 1 + if self.http_version == b"1.1" and host_count == 0: + raise LocalProtocolError("Missing mandatory Host: header") + if host_count > 1: + raise LocalProtocolError("Found multiple Host: headers") + + validate(method_re, self.method, "Illegal method characters") + validate(request_target_re, self.target, "Illegal target characters") + + # This is an unhashable type. + __hash__ = None # type: ignore + + +@dataclass(init=False, frozen=True) +class _ResponseBase(Event): + __slots__ = ("headers", "http_version", "reason", "status_code") + + headers: Headers + http_version: bytes + reason: bytes + status_code: int + + def __init__( + self, + *, + headers: Union[Headers, List[Tuple[bytes, bytes]], List[Tuple[str, str]]], + status_code: int, + http_version: Union[bytes, str] = b"1.1", + reason: Union[bytes, str] = b"", + _parsed: bool = False, + ) -> None: + super().__init__() + if isinstance(headers, Headers): + object.__setattr__(self, "headers", headers) + else: + object.__setattr__( + self, "headers", normalize_and_validate(headers, _parsed=_parsed) + ) + if not _parsed: + object.__setattr__(self, "reason", bytesify(reason)) + object.__setattr__(self, "http_version", bytesify(http_version)) + if not isinstance(status_code, int): + raise LocalProtocolError("status code must be integer") + # Because IntEnum objects are instances of int, but aren't + # duck-compatible (sigh), see gh-72. + object.__setattr__(self, "status_code", int(status_code)) + else: + object.__setattr__(self, "reason", reason) + object.__setattr__(self, "http_version", http_version) + object.__setattr__(self, "status_code", status_code) + + self.__post_init__() + + def __post_init__(self) -> None: + pass + + # This is an unhashable type. + __hash__ = None # type: ignore + + +@dataclass(init=False, frozen=True) +class InformationalResponse(_ResponseBase): + """An HTTP informational response. + + Fields: + + .. attribute:: status_code + + The status code of this response, as an integer. For an + :class:`InformationalResponse`, this is always in the range [100, + 200). + + .. attribute:: headers + + Request headers, represented as a list of (name, value) pairs. See + :ref:`the header normalization rules ` for + details. + + .. attribute:: http_version + + The HTTP protocol version, represented as a byte string like + ``b"1.1"``. See :ref:`the HTTP version normalization rules + ` for details. + + .. attribute:: reason + + The reason phrase of this response, as a byte string. For example: + ``b"OK"``, or ``b"Not Found"``. + + """ + + def __post_init__(self) -> None: + if not (100 <= self.status_code < 200): + raise LocalProtocolError( + "InformationalResponse status_code should be in range " + "[100, 200), not {}".format(self.status_code) + ) + + # This is an unhashable type. + __hash__ = None # type: ignore + + +@dataclass(init=False, frozen=True) +class Response(_ResponseBase): + """The beginning of an HTTP response. + + Fields: + + .. attribute:: status_code + + The status code of this response, as an integer. For an + :class:`Response`, this is always in the range [200, + 1000). + + .. attribute:: headers + + Request headers, represented as a list of (name, value) pairs. See + :ref:`the header normalization rules ` for details. + + .. attribute:: http_version + + The HTTP protocol version, represented as a byte string like + ``b"1.1"``. See :ref:`the HTTP version normalization rules + ` for details. + + .. attribute:: reason + + The reason phrase of this response, as a byte string. For example: + ``b"OK"``, or ``b"Not Found"``. + + """ + + def __post_init__(self) -> None: + if not (200 <= self.status_code < 1000): + raise LocalProtocolError( + "Response status_code should be in range [200, 1000), not {}".format( + self.status_code + ) + ) + + # This is an unhashable type. + __hash__ = None # type: ignore + + +@dataclass(init=False, frozen=True) +class Data(Event): + """Part of an HTTP message body. + + Fields: + + .. attribute:: data + + A :term:`bytes-like object` containing part of a message body. Or, if + using the ``combine=False`` argument to :meth:`Connection.send`, then + any object that your socket writing code knows what to do with, and for + which calling :func:`len` returns the number of bytes that will be + written -- see :ref:`sendfile` for details. + + .. attribute:: chunk_start + + A marker that indicates whether this data object is from the start of a + chunked transfer encoding chunk. This field is ignored when when a Data + event is provided to :meth:`Connection.send`: it is only valid on + events emitted from :meth:`Connection.next_event`. You probably + shouldn't use this attribute at all; see + :ref:`chunk-delimiters-are-bad` for details. + + .. attribute:: chunk_end + + A marker that indicates whether this data object is the last for a + given chunked transfer encoding chunk. This field is ignored when when + a Data event is provided to :meth:`Connection.send`: it is only valid + on events emitted from :meth:`Connection.next_event`. You probably + shouldn't use this attribute at all; see + :ref:`chunk-delimiters-are-bad` for details. + + """ + + __slots__ = ("data", "chunk_start", "chunk_end") + + data: bytes + chunk_start: bool + chunk_end: bool + + def __init__( + self, data: bytes, chunk_start: bool = False, chunk_end: bool = False + ) -> None: + object.__setattr__(self, "data", data) + object.__setattr__(self, "chunk_start", chunk_start) + object.__setattr__(self, "chunk_end", chunk_end) + + # This is an unhashable type. + __hash__ = None # type: ignore + + +# XX FIXME: "A recipient MUST ignore (or consider as an error) any fields that +# are forbidden to be sent in a trailer, since processing them as if they were +# present in the header section might bypass external security filters." +# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#chunked.trailer.part +# Unfortunately, the list of forbidden fields is long and vague :-/ +@dataclass(init=False, frozen=True) +class EndOfMessage(Event): + """The end of an HTTP message. + + Fields: + + .. attribute:: headers + + Default value: ``[]`` + + Any trailing headers attached to this message, represented as a list of + (name, value) pairs. See :ref:`the header normalization rules + ` for details. + + Must be empty unless ``Transfer-Encoding: chunked`` is in use. + + """ + + __slots__ = ("headers",) + + headers: Headers + + def __init__( + self, + *, + headers: Union[ + Headers, List[Tuple[bytes, bytes]], List[Tuple[str, str]], None + ] = None, + _parsed: bool = False, + ) -> None: + super().__init__() + if headers is None: + headers = Headers([]) + elif not isinstance(headers, Headers): + headers = normalize_and_validate(headers, _parsed=_parsed) + + object.__setattr__(self, "headers", headers) + + # This is an unhashable type. + __hash__ = None # type: ignore + + +@dataclass(frozen=True) +class ConnectionClosed(Event): + """This event indicates that the sender has closed their outgoing + connection. + + Note that this does not necessarily mean that they can't *receive* further + data, because TCP connections are composed to two one-way channels which + can be closed independently. See :ref:`closing` for details. + + No fields. + """ + + pass diff --git a/source/target-vendors/cp312/h11/_headers.py b/source/target-vendors/cp312/h11/_headers.py new file mode 100644 index 0000000..31da3e2 --- /dev/null +++ b/source/target-vendors/cp312/h11/_headers.py @@ -0,0 +1,282 @@ +import re +from typing import AnyStr, cast, List, overload, Sequence, Tuple, TYPE_CHECKING, Union + +from ._abnf import field_name, field_value +from ._util import bytesify, LocalProtocolError, validate + +if TYPE_CHECKING: + from ._events import Request + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal # type: ignore + +CONTENT_LENGTH_MAX_DIGITS = 20 # allow up to 1 billion TB - 1 + + +# Facts +# ----- +# +# Headers are: +# keys: case-insensitive ascii +# values: mixture of ascii and raw bytes +# +# "Historically, HTTP has allowed field content with text in the ISO-8859-1 +# charset [ISO-8859-1], supporting other charsets only through use of +# [RFC2047] encoding. In practice, most HTTP header field values use only a +# subset of the US-ASCII charset [USASCII]. Newly defined header fields SHOULD +# limit their field values to US-ASCII octets. A recipient SHOULD treat other +# octets in field content (obs-text) as opaque data." +# And it deprecates all non-ascii values +# +# Leading/trailing whitespace in header names is forbidden +# +# Values get leading/trailing whitespace stripped +# +# Content-Disposition actually needs to contain unicode semantically; to +# accomplish this it has a terrifically weird way of encoding the filename +# itself as ascii (and even this still has lots of cross-browser +# incompatibilities) +# +# Order is important: +# "a proxy MUST NOT change the order of these field values when forwarding a +# message" +# (and there are several headers where the order indicates a preference) +# +# Multiple occurences of the same header: +# "A sender MUST NOT generate multiple header fields with the same field name +# in a message unless either the entire field value for that header field is +# defined as a comma-separated list [or the header is Set-Cookie which gets a +# special exception]" - RFC 7230. (cookies are in RFC 6265) +# +# So every header aside from Set-Cookie can be merged by b", ".join if it +# occurs repeatedly. But, of course, they can't necessarily be split by +# .split(b","), because quoting. +# +# Given all this mess (case insensitive, duplicates allowed, order is +# important, ...), there doesn't appear to be any standard way to handle +# headers in Python -- they're almost like dicts, but... actually just +# aren't. For now we punt and just use a super simple representation: headers +# are a list of pairs +# +# [(name1, value1), (name2, value2), ...] +# +# where all entries are bytestrings, names are lowercase and have no +# leading/trailing whitespace, and values are bytestrings with no +# leading/trailing whitespace. Searching and updating are done via naive O(n) +# methods. +# +# Maybe a dict-of-lists would be better? + +_content_length_re = re.compile(rb"[0-9]+") +_field_name_re = re.compile(field_name.encode("ascii")) +_field_value_re = re.compile(field_value.encode("ascii")) + + +class Headers(Sequence[Tuple[bytes, bytes]]): + """ + A list-like interface that allows iterating over headers as byte-pairs + of (lowercased-name, value). + + Internally we actually store the representation as three-tuples, + including both the raw original casing, in order to preserve casing + over-the-wire, and the lowercased name, for case-insensitive comparisions. + + r = Request( + method="GET", + target="/", + headers=[("Host", "example.org"), ("Connection", "keep-alive")], + http_version="1.1", + ) + assert r.headers == [ + (b"host", b"example.org"), + (b"connection", b"keep-alive") + ] + assert r.headers.raw_items() == [ + (b"Host", b"example.org"), + (b"Connection", b"keep-alive") + ] + """ + + __slots__ = "_full_items" + + def __init__(self, full_items: List[Tuple[bytes, bytes, bytes]]) -> None: + self._full_items = full_items + + def __bool__(self) -> bool: + return bool(self._full_items) + + def __eq__(self, other: object) -> bool: + return list(self) == list(other) # type: ignore + + def __len__(self) -> int: + return len(self._full_items) + + def __repr__(self) -> str: + return "" % repr(list(self)) + + def __getitem__(self, idx: int) -> Tuple[bytes, bytes]: # type: ignore[override] + _, name, value = self._full_items[idx] + return (name, value) + + def raw_items(self) -> List[Tuple[bytes, bytes]]: + return [(raw_name, value) for raw_name, _, value in self._full_items] + + +HeaderTypes = Union[ + List[Tuple[bytes, bytes]], + List[Tuple[bytes, str]], + List[Tuple[str, bytes]], + List[Tuple[str, str]], +] + + +@overload +def normalize_and_validate(headers: Headers, _parsed: Literal[True]) -> Headers: + ... + + +@overload +def normalize_and_validate(headers: HeaderTypes, _parsed: Literal[False]) -> Headers: + ... + + +@overload +def normalize_and_validate( + headers: Union[Headers, HeaderTypes], _parsed: bool = False +) -> Headers: + ... + + +def normalize_and_validate( + headers: Union[Headers, HeaderTypes], _parsed: bool = False +) -> Headers: + new_headers = [] + seen_content_length = None + saw_transfer_encoding = False + for name, value in headers: + # For headers coming out of the parser, we can safely skip some steps, + # because it always returns bytes and has already run these regexes + # over the data: + if not _parsed: + name = bytesify(name) + value = bytesify(value) + validate(_field_name_re, name, "Illegal header name {!r}", name) + validate(_field_value_re, value, "Illegal header value {!r}", value) + assert isinstance(name, bytes) + assert isinstance(value, bytes) + + raw_name = name + name = name.lower() + if name == b"content-length": + lengths = {length.strip() for length in value.split(b",")} + if len(lengths) != 1: + raise LocalProtocolError("conflicting Content-Length headers") + value = lengths.pop() + validate(_content_length_re, value, "bad Content-Length") + if len(value) > CONTENT_LENGTH_MAX_DIGITS: + raise LocalProtocolError("bad Content-Length") + if seen_content_length is None: + seen_content_length = value + new_headers.append((raw_name, name, value)) + elif seen_content_length != value: + raise LocalProtocolError("conflicting Content-Length headers") + elif name == b"transfer-encoding": + # "A server that receives a request message with a transfer coding + # it does not understand SHOULD respond with 501 (Not + # Implemented)." + # https://tools.ietf.org/html/rfc7230#section-3.3.1 + if saw_transfer_encoding: + raise LocalProtocolError( + "multiple Transfer-Encoding headers", error_status_hint=501 + ) + # "All transfer-coding names are case-insensitive" + # -- https://tools.ietf.org/html/rfc7230#section-4 + value = value.lower() + if value != b"chunked": + raise LocalProtocolError( + "Only Transfer-Encoding: chunked is supported", + error_status_hint=501, + ) + saw_transfer_encoding = True + new_headers.append((raw_name, name, value)) + else: + new_headers.append((raw_name, name, value)) + return Headers(new_headers) + + +def get_comma_header(headers: Headers, name: bytes) -> List[bytes]: + # Should only be used for headers whose value is a list of + # comma-separated, case-insensitive values. + # + # The header name `name` is expected to be lower-case bytes. + # + # Connection: meets these criteria (including cast insensitivity). + # + # Content-Length: technically is just a single value (1*DIGIT), but the + # standard makes reference to implementations that do multiple values, and + # using this doesn't hurt. Ditto, case insensitivity doesn't things either + # way. + # + # Transfer-Encoding: is more complex (allows for quoted strings), so + # splitting on , is actually wrong. For example, this is legal: + # + # Transfer-Encoding: foo; options="1,2", chunked + # + # and should be parsed as + # + # foo; options="1,2" + # chunked + # + # but this naive function will parse it as + # + # foo; options="1 + # 2" + # chunked + # + # However, this is okay because the only thing we are going to do with + # any Transfer-Encoding is reject ones that aren't just "chunked", so + # both of these will be treated the same anyway. + # + # Expect: the only legal value is the literal string + # "100-continue". Splitting on commas is harmless. Case insensitive. + # + out: List[bytes] = [] + for _, found_name, found_raw_value in headers._full_items: + if found_name == name: + found_raw_value = found_raw_value.lower() + for found_split_value in found_raw_value.split(b","): + found_split_value = found_split_value.strip() + if found_split_value: + out.append(found_split_value) + return out + + +def set_comma_header(headers: Headers, name: bytes, new_values: List[bytes]) -> Headers: + # The header name `name` is expected to be lower-case bytes. + # + # Note that when we store the header we use title casing for the header + # names, in order to match the conventional HTTP header style. + # + # Simply calling `.title()` is a blunt approach, but it's correct + # here given the cases where we're using `set_comma_header`... + # + # Connection, Content-Length, Transfer-Encoding. + new_headers: List[Tuple[bytes, bytes]] = [] + for found_raw_name, found_name, found_raw_value in headers._full_items: + if found_name != name: + new_headers.append((found_raw_name, found_raw_value)) + for new_value in new_values: + new_headers.append((name.title(), new_value)) + return normalize_and_validate(new_headers) + + +def has_expect_100_continue(request: "Request") -> bool: + # https://tools.ietf.org/html/rfc7231#section-5.1.1 + # "A server that receives a 100-continue expectation in an HTTP/1.0 request + # MUST ignore that expectation." + if request.http_version < b"1.1": + return False + expect = get_comma_header(request.headers, b"expect") + return b"100-continue" in expect diff --git a/source/target-vendors/cp312/h11/_readers.py b/source/target-vendors/cp312/h11/_readers.py new file mode 100644 index 0000000..576804c --- /dev/null +++ b/source/target-vendors/cp312/h11/_readers.py @@ -0,0 +1,250 @@ +# Code to read HTTP data +# +# Strategy: each reader is a callable which takes a ReceiveBuffer object, and +# either: +# 1) consumes some of it and returns an Event +# 2) raises a LocalProtocolError (for consistency -- e.g. we call validate() +# and it might raise a LocalProtocolError, so simpler just to always use +# this) +# 3) returns None, meaning "I need more data" +# +# If they have a .read_eof attribute, then this will be called if an EOF is +# received -- but this is optional. Either way, the actual ConnectionClosed +# event will be generated afterwards. +# +# READERS is a dict describing how to pick a reader. It maps states to either: +# - a reader +# - or, for body readers, a dict of per-framing reader factories + +import re +from typing import Any, Callable, Dict, Iterable, NoReturn, Optional, Tuple, Type, Union + +from ._abnf import chunk_header, header_field, request_line, status_line +from ._events import Data, EndOfMessage, InformationalResponse, Request, Response +from ._receivebuffer import ReceiveBuffer +from ._state import ( + CLIENT, + CLOSED, + DONE, + IDLE, + MUST_CLOSE, + SEND_BODY, + SEND_RESPONSE, + SERVER, +) +from ._util import LocalProtocolError, RemoteProtocolError, Sentinel, validate + +__all__ = ["READERS"] + +header_field_re = re.compile(header_field.encode("ascii")) +obs_fold_re = re.compile(rb"[ \t]+") + + +def _obsolete_line_fold(lines: Iterable[bytes]) -> Iterable[bytes]: + it = iter(lines) + last: Optional[bytes] = None + for line in it: + match = obs_fold_re.match(line) + if match: + if last is None: + raise LocalProtocolError("continuation line at start of headers") + if not isinstance(last, bytearray): + # Cast to a mutable type, avoiding copy on append to ensure O(n) time + last = bytearray(last) + last += b" " + last += line[match.end() :] + else: + if last is not None: + yield last + last = line + if last is not None: + yield last + + +def _decode_header_lines( + lines: Iterable[bytes], +) -> Iterable[Tuple[bytes, bytes]]: + for line in _obsolete_line_fold(lines): + matches = validate(header_field_re, line, "illegal header line: {!r}", line) + yield (matches["field_name"], matches["field_value"]) + + +request_line_re = re.compile(request_line.encode("ascii")) + + +def maybe_read_from_IDLE_client(buf: ReceiveBuffer) -> Optional[Request]: + lines = buf.maybe_extract_lines() + if lines is None: + if buf.is_next_line_obviously_invalid_request_line(): + raise LocalProtocolError("illegal request line") + return None + if not lines: + raise LocalProtocolError("no request line received") + matches = validate( + request_line_re, lines[0], "illegal request line: {!r}", lines[0] + ) + return Request( + headers=list(_decode_header_lines(lines[1:])), _parsed=True, **matches + ) + + +status_line_re = re.compile(status_line.encode("ascii")) + + +def maybe_read_from_SEND_RESPONSE_server( + buf: ReceiveBuffer, +) -> Union[InformationalResponse, Response, None]: + lines = buf.maybe_extract_lines() + if lines is None: + if buf.is_next_line_obviously_invalid_request_line(): + raise LocalProtocolError("illegal request line") + return None + if not lines: + raise LocalProtocolError("no response line received") + matches = validate(status_line_re, lines[0], "illegal status line: {!r}", lines[0]) + http_version = ( + b"1.1" if matches["http_version"] is None else matches["http_version"] + ) + reason = b"" if matches["reason"] is None else matches["reason"] + status_code = int(matches["status_code"]) + class_: Union[Type[InformationalResponse], Type[Response]] = ( + InformationalResponse if status_code < 200 else Response + ) + return class_( + headers=list(_decode_header_lines(lines[1:])), + _parsed=True, + status_code=status_code, + reason=reason, + http_version=http_version, + ) + + +class ContentLengthReader: + def __init__(self, length: int) -> None: + self._length = length + self._remaining = length + + def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]: + if self._remaining == 0: + return EndOfMessage() + data = buf.maybe_extract_at_most(self._remaining) + if data is None: + return None + self._remaining -= len(data) + return Data(data=data) + + def read_eof(self) -> NoReturn: + raise RemoteProtocolError( + "peer closed connection without sending complete message body " + "(received {} bytes, expected {})".format( + self._length - self._remaining, self._length + ) + ) + + +chunk_header_re = re.compile(chunk_header.encode("ascii")) + + +class ChunkedReader: + def __init__(self) -> None: + self._bytes_in_chunk = 0 + # After reading a chunk, we have to throw away the trailing \r\n. + # This tracks the bytes that we need to match and throw away. + self._bytes_to_discard = b"" + self._reading_trailer = False + + def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]: + if self._reading_trailer: + lines = buf.maybe_extract_lines() + if lines is None: + return None + return EndOfMessage(headers=list(_decode_header_lines(lines))) + if self._bytes_to_discard: + data = buf.maybe_extract_at_most(len(self._bytes_to_discard)) + if data is None: + return None + if data != self._bytes_to_discard[: len(data)]: + raise LocalProtocolError( + f"malformed chunk footer: {data!r} (expected {self._bytes_to_discard!r})" + ) + self._bytes_to_discard = self._bytes_to_discard[len(data) :] + if self._bytes_to_discard: + return None + # else, fall through and read some more + assert self._bytes_to_discard == b"" + if self._bytes_in_chunk == 0: + # We need to refill our chunk count + chunk_header = buf.maybe_extract_next_line() + if chunk_header is None: + return None + matches = validate( + chunk_header_re, + chunk_header, + "illegal chunk header: {!r}", + chunk_header, + ) + # XX FIXME: we discard chunk extensions. Does anyone care? + self._bytes_in_chunk = int(matches["chunk_size"], base=16) + if self._bytes_in_chunk == 0: + self._reading_trailer = True + return self(buf) + chunk_start = True + else: + chunk_start = False + assert self._bytes_in_chunk > 0 + data = buf.maybe_extract_at_most(self._bytes_in_chunk) + if data is None: + return None + self._bytes_in_chunk -= len(data) + if self._bytes_in_chunk == 0: + self._bytes_to_discard = b"\r\n" + chunk_end = True + else: + chunk_end = False + return Data(data=data, chunk_start=chunk_start, chunk_end=chunk_end) + + def read_eof(self) -> NoReturn: + raise RemoteProtocolError( + "peer closed connection without sending complete message body " + "(incomplete chunked read)" + ) + + +class Http10Reader: + def __call__(self, buf: ReceiveBuffer) -> Optional[Data]: + data = buf.maybe_extract_at_most(999999999) + if data is None: + return None + return Data(data=data) + + def read_eof(self) -> EndOfMessage: + return EndOfMessage() + + +def expect_nothing(buf: ReceiveBuffer) -> None: + if buf: + raise LocalProtocolError("Got data when expecting EOF") + return None + + +ReadersType = Dict[ + Union[Type[Sentinel], Tuple[Type[Sentinel], Type[Sentinel]]], + Union[Callable[..., Any], Dict[str, Callable[..., Any]]], +] + +READERS: ReadersType = { + (CLIENT, IDLE): maybe_read_from_IDLE_client, + (SERVER, IDLE): maybe_read_from_SEND_RESPONSE_server, + (SERVER, SEND_RESPONSE): maybe_read_from_SEND_RESPONSE_server, + (CLIENT, DONE): expect_nothing, + (CLIENT, MUST_CLOSE): expect_nothing, + (CLIENT, CLOSED): expect_nothing, + (SERVER, DONE): expect_nothing, + (SERVER, MUST_CLOSE): expect_nothing, + (SERVER, CLOSED): expect_nothing, + SEND_BODY: { + "chunked": ChunkedReader, + "content-length": ContentLengthReader, + "http/1.0": Http10Reader, + }, +} diff --git a/source/target-vendors/cp312/h11/_receivebuffer.py b/source/target-vendors/cp312/h11/_receivebuffer.py new file mode 100644 index 0000000..e5c4e08 --- /dev/null +++ b/source/target-vendors/cp312/h11/_receivebuffer.py @@ -0,0 +1,153 @@ +import re +import sys +from typing import List, Optional, Union + +__all__ = ["ReceiveBuffer"] + + +# Operations we want to support: +# - find next \r\n or \r\n\r\n (\n or \n\n are also acceptable), +# or wait until there is one +# - read at-most-N bytes +# Goals: +# - on average, do this fast +# - worst case, do this in O(n) where n is the number of bytes processed +# Plan: +# - store bytearray, offset, how far we've searched for a separator token +# - use the how-far-we've-searched data to avoid rescanning +# - while doing a stream of uninterrupted processing, advance offset instead +# of constantly copying +# WARNING: +# - I haven't benchmarked or profiled any of this yet. +# +# Note that starting in Python 3.4, deleting the initial n bytes from a +# bytearray is amortized O(n), thanks to some excellent work by Antoine +# Martin: +# +# https://bugs.python.org/issue19087 +# +# This means that if we only supported 3.4+, we could get rid of the code here +# involving self._start and self.compress, because it's doing exactly the same +# thing that bytearray now does internally. +# +# BUT unfortunately, we still support 2.7, and reading short segments out of a +# long buffer MUST be O(bytes read) to avoid DoS issues, so we can't actually +# delete this code. Yet: +# +# https://pythonclock.org/ +# +# (Two things to double-check first though: make sure PyPy also has the +# optimization, and benchmark to make sure it's a win, since we do have a +# slightly clever thing where we delay calling compress() until we've +# processed a whole event, which could in theory be slightly more efficient +# than the internal bytearray support.) +blank_line_regex = re.compile(b"\n\r?\n", re.MULTILINE) + + +class ReceiveBuffer: + def __init__(self) -> None: + self._data = bytearray() + self._next_line_search = 0 + self._multiple_lines_search = 0 + + def __iadd__(self, byteslike: Union[bytes, bytearray]) -> "ReceiveBuffer": + self._data += byteslike + return self + + def __bool__(self) -> bool: + return bool(len(self)) + + def __len__(self) -> int: + return len(self._data) + + # for @property unprocessed_data + def __bytes__(self) -> bytes: + return bytes(self._data) + + def _extract(self, count: int) -> bytearray: + # extracting an initial slice of the data buffer and return it + out = self._data[:count] + del self._data[:count] + + self._next_line_search = 0 + self._multiple_lines_search = 0 + + return out + + def maybe_extract_at_most(self, count: int) -> Optional[bytearray]: + """ + Extract a fixed number of bytes from the buffer. + """ + out = self._data[:count] + if not out: + return None + + return self._extract(count) + + def maybe_extract_next_line(self) -> Optional[bytearray]: + """ + Extract the first line, if it is completed in the buffer. + """ + # Only search in buffer space that we've not already looked at. + search_start_index = max(0, self._next_line_search - 1) + partial_idx = self._data.find(b"\r\n", search_start_index) + + if partial_idx == -1: + self._next_line_search = len(self._data) + return None + + # + 2 is to compensate len(b"\r\n") + idx = partial_idx + 2 + + return self._extract(idx) + + def maybe_extract_lines(self) -> Optional[List[bytearray]]: + """ + Extract everything up to the first blank line, and return a list of lines. + """ + # Handle the case where we have an immediate empty line. + if self._data[:1] == b"\n": + self._extract(1) + return [] + + if self._data[:2] == b"\r\n": + self._extract(2) + return [] + + # Only search in buffer space that we've not already looked at. + match = blank_line_regex.search(self._data, self._multiple_lines_search) + if match is None: + self._multiple_lines_search = max(0, len(self._data) - 2) + return None + + # Truncate the buffer and return it. + idx = match.span(0)[-1] + out = self._extract(idx) + lines = out.split(b"\n") + + for line in lines: + if line.endswith(b"\r"): + del line[-1] + + assert lines[-2] == lines[-1] == b"" + + del lines[-2:] + + return lines + + # In theory we should wait until `\r\n` before starting to validate + # incoming data. However it's interesting to detect (very) invalid data + # early given they might not even contain `\r\n` at all (hence only + # timeout will get rid of them). + # This is not a 100% effective detection but more of a cheap sanity check + # allowing for early abort in some useful cases. + # This is especially interesting when peer is messing up with HTTPS and + # sent us a TLS stream where we were expecting plain HTTP given all + # versions of TLS so far start handshake with a 0x16 message type code. + def is_next_line_obviously_invalid_request_line(self) -> bool: + try: + # HTTP header line must not contain non-printable characters + # and should not start with a space + return self._data[0] < 0x21 + except IndexError: + return False diff --git a/source/target-vendors/cp312/h11/_state.py b/source/target-vendors/cp312/h11/_state.py new file mode 100644 index 0000000..3ad444b --- /dev/null +++ b/source/target-vendors/cp312/h11/_state.py @@ -0,0 +1,365 @@ +################################################################ +# The core state machine +################################################################ +# +# Rule 1: everything that affects the state machine and state transitions must +# live here in this file. As much as possible goes into the table-based +# representation, but for the bits that don't quite fit, the actual code and +# state must nonetheless live here. +# +# Rule 2: this file does not know about what role we're playing; it only knows +# about HTTP request/response cycles in the abstract. This ensures that we +# don't cheat and apply different rules to local and remote parties. +# +# +# Theory of operation +# =================== +# +# Possibly the simplest way to think about this is that we actually have 5 +# different state machines here. Yes, 5. These are: +# +# 1) The client state, with its complicated automaton (see the docs) +# 2) The server state, with its complicated automaton (see the docs) +# 3) The keep-alive state, with possible states {True, False} +# 4) The SWITCH_CONNECT state, with possible states {False, True} +# 5) The SWITCH_UPGRADE state, with possible states {False, True} +# +# For (3)-(5), the first state listed is the initial state. +# +# (1)-(3) are stored explicitly in member variables. The last +# two are stored implicitly in the pending_switch_proposals set as: +# (state of 4) == (_SWITCH_CONNECT in pending_switch_proposals) +# (state of 5) == (_SWITCH_UPGRADE in pending_switch_proposals) +# +# And each of these machines has two different kinds of transitions: +# +# a) Event-triggered +# b) State-triggered +# +# Event triggered is the obvious thing that you'd think it is: some event +# happens, and if it's the right event at the right time then a transition +# happens. But there are somewhat complicated rules for which machines can +# "see" which events. (As a rule of thumb, if a machine "sees" an event, this +# means two things: the event can affect the machine, and if the machine is +# not in a state where it expects that event then it's an error.) These rules +# are: +# +# 1) The client machine sees all h11.events objects emitted by the client. +# +# 2) The server machine sees all h11.events objects emitted by the server. +# +# It also sees the client's Request event. +# +# And sometimes, server events are annotated with a _SWITCH_* event. For +# example, we can have a (Response, _SWITCH_CONNECT) event, which is +# different from a regular Response event. +# +# 3) The keep-alive machine sees the process_keep_alive_disabled() event +# (which is derived from Request/Response events), and this event +# transitions it from True -> False, or from False -> False. There's no way +# to transition back. +# +# 4&5) The _SWITCH_* machines transition from False->True when we get a +# Request that proposes the relevant type of switch (via +# process_client_switch_proposals), and they go from True->False when we +# get a Response that has no _SWITCH_* annotation. +# +# So that's event-triggered transitions. +# +# State-triggered transitions are less standard. What they do here is couple +# the machines together. The way this works is, when certain *joint* +# configurations of states are achieved, then we automatically transition to a +# new *joint* state. So, for example, if we're ever in a joint state with +# +# client: DONE +# keep-alive: False +# +# then the client state immediately transitions to: +# +# client: MUST_CLOSE +# +# This is fundamentally different from an event-based transition, because it +# doesn't matter how we arrived at the {client: DONE, keep-alive: False} state +# -- maybe the client transitioned SEND_BODY -> DONE, or keep-alive +# transitioned True -> False. Either way, once this precondition is satisfied, +# this transition is immediately triggered. +# +# What if two conflicting state-based transitions get enabled at the same +# time? In practice there's only one case where this arises (client DONE -> +# MIGHT_SWITCH_PROTOCOL versus DONE -> MUST_CLOSE), and we resolve it by +# explicitly prioritizing the DONE -> MIGHT_SWITCH_PROTOCOL transition. +# +# Implementation +# -------------- +# +# The event-triggered transitions for the server and client machines are all +# stored explicitly in a table. Ditto for the state-triggered transitions that +# involve just the server and client state. +# +# The transitions for the other machines, and the state-triggered transitions +# that involve the other machines, are written out as explicit Python code. +# +# It'd be nice if there were some cleaner way to do all this. This isn't +# *too* terrible, but I feel like it could probably be better. +# +# WARNING +# ------- +# +# The script that generates the state machine diagrams for the docs knows how +# to read out the EVENT_TRIGGERED_TRANSITIONS and STATE_TRIGGERED_TRANSITIONS +# tables. But it can't automatically read the transitions that are written +# directly in Python code. So if you touch those, you need to also update the +# script to keep it in sync! +from typing import cast, Dict, Optional, Set, Tuple, Type, Union + +from ._events import * +from ._util import LocalProtocolError, Sentinel + +# Everything in __all__ gets re-exported as part of the h11 public API. +__all__ = [ + "CLIENT", + "SERVER", + "IDLE", + "SEND_RESPONSE", + "SEND_BODY", + "DONE", + "MUST_CLOSE", + "CLOSED", + "MIGHT_SWITCH_PROTOCOL", + "SWITCHED_PROTOCOL", + "ERROR", +] + + +class CLIENT(Sentinel, metaclass=Sentinel): + pass + + +class SERVER(Sentinel, metaclass=Sentinel): + pass + + +# States +class IDLE(Sentinel, metaclass=Sentinel): + pass + + +class SEND_RESPONSE(Sentinel, metaclass=Sentinel): + pass + + +class SEND_BODY(Sentinel, metaclass=Sentinel): + pass + + +class DONE(Sentinel, metaclass=Sentinel): + pass + + +class MUST_CLOSE(Sentinel, metaclass=Sentinel): + pass + + +class CLOSED(Sentinel, metaclass=Sentinel): + pass + + +class ERROR(Sentinel, metaclass=Sentinel): + pass + + +# Switch types +class MIGHT_SWITCH_PROTOCOL(Sentinel, metaclass=Sentinel): + pass + + +class SWITCHED_PROTOCOL(Sentinel, metaclass=Sentinel): + pass + + +class _SWITCH_UPGRADE(Sentinel, metaclass=Sentinel): + pass + + +class _SWITCH_CONNECT(Sentinel, metaclass=Sentinel): + pass + + +EventTransitionType = Dict[ + Type[Sentinel], + Dict[ + Type[Sentinel], + Dict[Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]], Type[Sentinel]], + ], +] + +EVENT_TRIGGERED_TRANSITIONS: EventTransitionType = { + CLIENT: { + IDLE: {Request: SEND_BODY, ConnectionClosed: CLOSED}, + SEND_BODY: {Data: SEND_BODY, EndOfMessage: DONE}, + DONE: {ConnectionClosed: CLOSED}, + MUST_CLOSE: {ConnectionClosed: CLOSED}, + CLOSED: {ConnectionClosed: CLOSED}, + MIGHT_SWITCH_PROTOCOL: {}, + SWITCHED_PROTOCOL: {}, + ERROR: {}, + }, + SERVER: { + IDLE: { + ConnectionClosed: CLOSED, + Response: SEND_BODY, + # Special case: server sees client Request events, in this form + (Request, CLIENT): SEND_RESPONSE, + }, + SEND_RESPONSE: { + InformationalResponse: SEND_RESPONSE, + Response: SEND_BODY, + (InformationalResponse, _SWITCH_UPGRADE): SWITCHED_PROTOCOL, + (Response, _SWITCH_CONNECT): SWITCHED_PROTOCOL, + }, + SEND_BODY: {Data: SEND_BODY, EndOfMessage: DONE}, + DONE: {ConnectionClosed: CLOSED}, + MUST_CLOSE: {ConnectionClosed: CLOSED}, + CLOSED: {ConnectionClosed: CLOSED}, + SWITCHED_PROTOCOL: {}, + ERROR: {}, + }, +} + +StateTransitionType = Dict[ + Tuple[Type[Sentinel], Type[Sentinel]], Dict[Type[Sentinel], Type[Sentinel]] +] + +# NB: there are also some special-case state-triggered transitions hard-coded +# into _fire_state_triggered_transitions below. +STATE_TRIGGERED_TRANSITIONS: StateTransitionType = { + # (Client state, Server state) -> new states + # Protocol negotiation + (MIGHT_SWITCH_PROTOCOL, SWITCHED_PROTOCOL): {CLIENT: SWITCHED_PROTOCOL}, + # Socket shutdown + (CLOSED, DONE): {SERVER: MUST_CLOSE}, + (CLOSED, IDLE): {SERVER: MUST_CLOSE}, + (ERROR, DONE): {SERVER: MUST_CLOSE}, + (DONE, CLOSED): {CLIENT: MUST_CLOSE}, + (IDLE, CLOSED): {CLIENT: MUST_CLOSE}, + (DONE, ERROR): {CLIENT: MUST_CLOSE}, +} + + +class ConnectionState: + def __init__(self) -> None: + # Extra bits of state that don't quite fit into the state model. + + # If this is False then it enables the automatic DONE -> MUST_CLOSE + # transition. Don't set this directly; call .keep_alive_disabled() + self.keep_alive = True + + # This is a subset of {UPGRADE, CONNECT}, containing the proposals + # made by the client for switching protocols. + self.pending_switch_proposals: Set[Type[Sentinel]] = set() + + self.states: Dict[Type[Sentinel], Type[Sentinel]] = {CLIENT: IDLE, SERVER: IDLE} + + def process_error(self, role: Type[Sentinel]) -> None: + self.states[role] = ERROR + self._fire_state_triggered_transitions() + + def process_keep_alive_disabled(self) -> None: + self.keep_alive = False + self._fire_state_triggered_transitions() + + def process_client_switch_proposal(self, switch_event: Type[Sentinel]) -> None: + self.pending_switch_proposals.add(switch_event) + self._fire_state_triggered_transitions() + + def process_event( + self, + role: Type[Sentinel], + event_type: Type[Event], + server_switch_event: Optional[Type[Sentinel]] = None, + ) -> None: + _event_type: Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]] = event_type + if server_switch_event is not None: + assert role is SERVER + if server_switch_event not in self.pending_switch_proposals: + raise LocalProtocolError( + "Received server _SWITCH_UPGRADE event without a pending proposal" + ) + _event_type = (event_type, server_switch_event) + if server_switch_event is None and _event_type is Response: + self.pending_switch_proposals = set() + self._fire_event_triggered_transitions(role, _event_type) + # Special case: the server state does get to see Request + # events. + if _event_type is Request: + assert role is CLIENT + self._fire_event_triggered_transitions(SERVER, (Request, CLIENT)) + self._fire_state_triggered_transitions() + + def _fire_event_triggered_transitions( + self, + role: Type[Sentinel], + event_type: Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]], + ) -> None: + state = self.states[role] + try: + new_state = EVENT_TRIGGERED_TRANSITIONS[role][state][event_type] + except KeyError: + event_type = cast(Type[Event], event_type) + raise LocalProtocolError( + "can't handle event type {} when role={} and state={}".format( + event_type.__name__, role, self.states[role] + ) + ) from None + self.states[role] = new_state + + def _fire_state_triggered_transitions(self) -> None: + # We apply these rules repeatedly until converging on a fixed point + while True: + start_states = dict(self.states) + + # It could happen that both these special-case transitions are + # enabled at the same time: + # + # DONE -> MIGHT_SWITCH_PROTOCOL + # DONE -> MUST_CLOSE + # + # For example, this will always be true of a HTTP/1.0 client + # requesting CONNECT. If this happens, the protocol switch takes + # priority. From there the client will either go to + # SWITCHED_PROTOCOL, in which case it's none of our business when + # they close the connection, or else the server will deny the + # request, in which case the client will go back to DONE and then + # from there to MUST_CLOSE. + if self.pending_switch_proposals: + if self.states[CLIENT] is DONE: + self.states[CLIENT] = MIGHT_SWITCH_PROTOCOL + + if not self.pending_switch_proposals: + if self.states[CLIENT] is MIGHT_SWITCH_PROTOCOL: + self.states[CLIENT] = DONE + + if not self.keep_alive: + for role in (CLIENT, SERVER): + if self.states[role] is DONE: + self.states[role] = MUST_CLOSE + + # Tabular state-triggered transitions + joint_state = (self.states[CLIENT], self.states[SERVER]) + changes = STATE_TRIGGERED_TRANSITIONS.get(joint_state, {}) + self.states.update(changes) + + if self.states == start_states: + # Fixed point reached + return + + def start_next_cycle(self) -> None: + if self.states != {CLIENT: DONE, SERVER: DONE}: + raise LocalProtocolError( + f"not in a reusable state. self.states={self.states}" + ) + # Can't reach DONE/DONE with any of these active, but still, let's be + # sure. + assert self.keep_alive + assert not self.pending_switch_proposals + self.states = {CLIENT: IDLE, SERVER: IDLE} diff --git a/source/target-vendors/cp312/h11/_util.py b/source/target-vendors/cp312/h11/_util.py new file mode 100644 index 0000000..6718445 --- /dev/null +++ b/source/target-vendors/cp312/h11/_util.py @@ -0,0 +1,135 @@ +from typing import Any, Dict, NoReturn, Pattern, Tuple, Type, TypeVar, Union + +__all__ = [ + "ProtocolError", + "LocalProtocolError", + "RemoteProtocolError", + "validate", + "bytesify", +] + + +class ProtocolError(Exception): + """Exception indicating a violation of the HTTP/1.1 protocol. + + This as an abstract base class, with two concrete base classes: + :exc:`LocalProtocolError`, which indicates that you tried to do something + that HTTP/1.1 says is illegal, and :exc:`RemoteProtocolError`, which + indicates that the remote peer tried to do something that HTTP/1.1 says is + illegal. See :ref:`error-handling` for details. + + In addition to the normal :exc:`Exception` features, it has one attribute: + + .. attribute:: error_status_hint + + This gives a suggestion as to what status code a server might use if + this error occurred as part of a request. + + For a :exc:`RemoteProtocolError`, this is useful as a suggestion for + how you might want to respond to a misbehaving peer, if you're + implementing a server. + + For a :exc:`LocalProtocolError`, this can be taken as a suggestion for + how your peer might have responded to *you* if h11 had allowed you to + continue. + + The default is 400 Bad Request, a generic catch-all for protocol + violations. + + """ + + def __init__(self, msg: str, error_status_hint: int = 400) -> None: + if type(self) is ProtocolError: + raise TypeError("tried to directly instantiate ProtocolError") + Exception.__init__(self, msg) + self.error_status_hint = error_status_hint + + +# Strategy: there are a number of public APIs where a LocalProtocolError can +# be raised (send(), all the different event constructors, ...), and only one +# public API where RemoteProtocolError can be raised +# (receive_data()). Therefore we always raise LocalProtocolError internally, +# and then receive_data will translate this into a RemoteProtocolError. +# +# Internally: +# LocalProtocolError is the generic "ProtocolError". +# Externally: +# LocalProtocolError is for local errors and RemoteProtocolError is for +# remote errors. +class LocalProtocolError(ProtocolError): + def _reraise_as_remote_protocol_error(self) -> NoReturn: + # After catching a LocalProtocolError, use this method to re-raise it + # as a RemoteProtocolError. This method must be called from inside an + # except: block. + # + # An easy way to get an equivalent RemoteProtocolError is just to + # modify 'self' in place. + self.__class__ = RemoteProtocolError # type: ignore + # But the re-raising is somewhat non-trivial -- you might think that + # now that we've modified the in-flight exception object, that just + # doing 'raise' to re-raise it would be enough. But it turns out that + # this doesn't work, because Python tracks the exception type + # (exc_info[0]) separately from the exception object (exc_info[1]), + # and we only modified the latter. So we really do need to re-raise + # the new type explicitly. + # On py3, the traceback is part of the exception object, so our + # in-place modification preserved it and we can just re-raise: + raise self + + +class RemoteProtocolError(ProtocolError): + pass + + +def validate( + regex: Pattern[bytes], data: bytes, msg: str = "malformed data", *format_args: Any +) -> Dict[str, bytes]: + match = regex.fullmatch(data) + if not match: + if format_args: + msg = msg.format(*format_args) + raise LocalProtocolError(msg) + return match.groupdict() + + +# Sentinel values +# +# - Inherit identity-based comparison and hashing from object +# - Have a nice repr +# - Have a *bonus property*: type(sentinel) is sentinel +# +# The bonus property is useful if you want to take the return value from +# next_event() and do some sort of dispatch based on type(event). + +_T_Sentinel = TypeVar("_T_Sentinel", bound="Sentinel") + + +class Sentinel(type): + def __new__( + cls: Type[_T_Sentinel], + name: str, + bases: Tuple[type, ...], + namespace: Dict[str, Any], + **kwds: Any + ) -> _T_Sentinel: + assert bases == (Sentinel,) + v = super().__new__(cls, name, bases, namespace, **kwds) + v.__class__ = v # type: ignore + return v + + def __repr__(self) -> str: + return self.__name__ + + +# Used for methods, request targets, HTTP versions, header names, and header +# values. Accepts ascii-strings, or bytes/bytearray/memoryview/..., and always +# returns bytes. +def bytesify(s: Union[bytes, bytearray, memoryview, int, str]) -> bytes: + # Fast-path: + if type(s) is bytes: + return s + if isinstance(s, str): + s = s.encode("ascii") + if isinstance(s, int): + raise TypeError("expected bytes-like object, not int") + return bytes(s) diff --git a/source/target-vendors/cp312/h11/_version.py b/source/target-vendors/cp312/h11/_version.py new file mode 100644 index 0000000..76e7327 --- /dev/null +++ b/source/target-vendors/cp312/h11/_version.py @@ -0,0 +1,16 @@ +# This file must be kept very simple, because it is consumed from several +# places -- it is imported by h11/__init__.py, execfile'd by setup.py, etc. + +# We use a simple scheme: +# 1.0.0 -> 1.0.0+dev -> 1.1.0 -> 1.1.0+dev +# where the +dev versions are never released into the wild, they're just what +# we stick into the VCS in between releases. +# +# This is compatible with PEP 440: +# http://legacy.python.org/dev/peps/pep-0440/ +# via the use of the "local suffix" "+dev", which is disallowed on index +# servers and causes 1.0.0+dev to sort after plain 1.0.0, which is what we +# want. (Contrast with the special suffix 1.0.0.dev, which sorts *before* +# 1.0.0.) + +__version__ = "0.16.0" diff --git a/source/target-vendors/cp312/h11/_writers.py b/source/target-vendors/cp312/h11/_writers.py new file mode 100644 index 0000000..939cdb9 --- /dev/null +++ b/source/target-vendors/cp312/h11/_writers.py @@ -0,0 +1,145 @@ +# Code to read HTTP data +# +# Strategy: each writer takes an event + a write-some-bytes function, which is +# calls. +# +# WRITERS is a dict describing how to pick a reader. It maps states to either: +# - a writer +# - or, for body writers, a dict of framin-dependent writer factories + +from typing import Any, Callable, Dict, List, Tuple, Type, Union + +from ._events import Data, EndOfMessage, Event, InformationalResponse, Request, Response +from ._headers import Headers +from ._state import CLIENT, IDLE, SEND_BODY, SEND_RESPONSE, SERVER +from ._util import LocalProtocolError, Sentinel + +__all__ = ["WRITERS"] + +Writer = Callable[[bytes], Any] + + +def write_headers(headers: Headers, write: Writer) -> None: + # "Since the Host field-value is critical information for handling a + # request, a user agent SHOULD generate Host as the first header field + # following the request-line." - RFC 7230 + raw_items = headers._full_items + for raw_name, name, value in raw_items: + if name == b"host": + write(b"%s: %s\r\n" % (raw_name, value)) + for raw_name, name, value in raw_items: + if name != b"host": + write(b"%s: %s\r\n" % (raw_name, value)) + write(b"\r\n") + + +def write_request(request: Request, write: Writer) -> None: + if request.http_version != b"1.1": + raise LocalProtocolError("I only send HTTP/1.1") + write(b"%s %s HTTP/1.1\r\n" % (request.method, request.target)) + write_headers(request.headers, write) + + +# Shared between InformationalResponse and Response +def write_any_response( + response: Union[InformationalResponse, Response], write: Writer +) -> None: + if response.http_version != b"1.1": + raise LocalProtocolError("I only send HTTP/1.1") + status_bytes = str(response.status_code).encode("ascii") + # We don't bother sending ascii status messages like "OK"; they're + # optional and ignored by the protocol. (But the space after the numeric + # status code is mandatory.) + # + # XX FIXME: could at least make an effort to pull out the status message + # from stdlib's http.HTTPStatus table. Or maybe just steal their enums + # (either by import or copy/paste). We already accept them as status codes + # since they're of type IntEnum < int. + write(b"HTTP/1.1 %s %s\r\n" % (status_bytes, response.reason)) + write_headers(response.headers, write) + + +class BodyWriter: + def __call__(self, event: Event, write: Writer) -> None: + if type(event) is Data: + self.send_data(event.data, write) + elif type(event) is EndOfMessage: + self.send_eom(event.headers, write) + else: # pragma: no cover + assert False + + def send_data(self, data: bytes, write: Writer) -> None: + pass + + def send_eom(self, headers: Headers, write: Writer) -> None: + pass + + +# +# These are all careful not to do anything to 'data' except call len(data) and +# write(data). This allows us to transparently pass-through funny objects, +# like placeholder objects referring to files on disk that will be sent via +# sendfile(2). +# +class ContentLengthWriter(BodyWriter): + def __init__(self, length: int) -> None: + self._length = length + + def send_data(self, data: bytes, write: Writer) -> None: + self._length -= len(data) + if self._length < 0: + raise LocalProtocolError("Too much data for declared Content-Length") + write(data) + + def send_eom(self, headers: Headers, write: Writer) -> None: + if self._length != 0: + raise LocalProtocolError("Too little data for declared Content-Length") + if headers: + raise LocalProtocolError("Content-Length and trailers don't mix") + + +class ChunkedWriter(BodyWriter): + def send_data(self, data: bytes, write: Writer) -> None: + # if we encoded 0-length data in the naive way, it would look like an + # end-of-message. + if not data: + return + write(b"%x\r\n" % len(data)) + write(data) + write(b"\r\n") + + def send_eom(self, headers: Headers, write: Writer) -> None: + write(b"0\r\n") + write_headers(headers, write) + + +class Http10Writer(BodyWriter): + def send_data(self, data: bytes, write: Writer) -> None: + write(data) + + def send_eom(self, headers: Headers, write: Writer) -> None: + if headers: + raise LocalProtocolError("can't send trailers to HTTP/1.0 client") + # no need to close the socket ourselves, that will be taken care of by + # Connection: close machinery + + +WritersType = Dict[ + Union[Tuple[Type[Sentinel], Type[Sentinel]], Type[Sentinel]], + Union[ + Dict[str, Type[BodyWriter]], + Callable[[Union[InformationalResponse, Response], Writer], None], + Callable[[Request, Writer], None], + ], +] + +WRITERS: WritersType = { + (CLIENT, IDLE): write_request, + (SERVER, IDLE): write_any_response, + (SERVER, SEND_RESPONSE): write_any_response, + SEND_BODY: { + "chunked": ChunkedWriter, + "content-length": ContentLengthWriter, + "http/1.0": Http10Writer, + }, +} diff --git a/source/target-vendors/cp312/h11/py.typed b/source/target-vendors/cp312/h11/py.typed new file mode 100644 index 0000000..f5642f7 --- /dev/null +++ b/source/target-vendors/cp312/h11/py.typed @@ -0,0 +1 @@ +Marker diff --git a/source/target-vendors/cp312/httpcore-1.0.9.dist-info/METADATA b/source/target-vendors/cp312/httpcore-1.0.9.dist-info/METADATA new file mode 100644 index 0000000..8056834 --- /dev/null +++ b/source/target-vendors/cp312/httpcore-1.0.9.dist-info/METADATA @@ -0,0 +1,625 @@ +Metadata-Version: 2.4 +Name: httpcore +Version: 1.0.9 +Summary: A minimal low-level HTTP client. +Project-URL: Documentation, https://www.encode.io/httpcore +Project-URL: Homepage, https://www.encode.io/httpcore/ +Project-URL: Source, https://github.com/encode/httpcore +Author-email: Tom Christie +License-Expression: BSD-3-Clause +License-File: LICENSE.md +Classifier: Development Status :: 3 - Alpha +Classifier: Environment :: Web Environment +Classifier: Framework :: AsyncIO +Classifier: Framework :: Trio +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Topic :: Internet :: WWW/HTTP +Requires-Python: >=3.8 +Requires-Dist: certifi +Requires-Dist: h11>=0.16 +Provides-Extra: asyncio +Requires-Dist: anyio<5.0,>=4.0; extra == 'asyncio' +Provides-Extra: http2 +Requires-Dist: h2<5,>=3; extra == 'http2' +Provides-Extra: socks +Requires-Dist: socksio==1.*; extra == 'socks' +Provides-Extra: trio +Requires-Dist: trio<1.0,>=0.22.0; extra == 'trio' +Description-Content-Type: text/markdown + +# HTTP Core + +[![Test Suite](https://github.com/encode/httpcore/workflows/Test%20Suite/badge.svg)](https://github.com/encode/httpcore/actions) +[![Package version](https://badge.fury.io/py/httpcore.svg)](https://pypi.org/project/httpcore/) + +> *Do one thing, and do it well.* + +The HTTP Core package provides a minimal low-level HTTP client, which does +one thing only. Sending HTTP requests. + +It does not provide any high level model abstractions over the API, +does not handle redirects, multipart uploads, building authentication headers, +transparent HTTP caching, URL parsing, session cookie handling, +content or charset decoding, handling JSON, environment based configuration +defaults, or any of that Jazz. + +Some things HTTP Core does do: + +* Sending HTTP requests. +* Thread-safe / task-safe connection pooling. +* HTTP(S) proxy & SOCKS proxy support. +* Supports HTTP/1.1 and HTTP/2. +* Provides both sync and async interfaces. +* Async backend support for `asyncio` and `trio`. + +## Requirements + +Python 3.8+ + +## Installation + +For HTTP/1.1 only support, install with: + +```shell +$ pip install httpcore +``` + +There are also a number of optional extras available... + +```shell +$ pip install httpcore['asyncio,trio,http2,socks'] +``` + +## Sending requests + +Send an HTTP request: + +```python +import httpcore + +response = httpcore.request("GET", "https://www.example.com/") + +print(response) +# +print(response.status) +# 200 +print(response.headers) +# [(b'Accept-Ranges', b'bytes'), (b'Age', b'557328'), (b'Cache-Control', b'max-age=604800'), ...] +print(response.content) +# b'\n\n\nExample Domain\n\n\n ...' +``` + +The top-level `httpcore.request()` function is provided for convenience. In practice whenever you're working with `httpcore` you'll want to use the connection pooling functionality that it provides. + +```python +import httpcore + +http = httpcore.ConnectionPool() +response = http.request("GET", "https://www.example.com/") +``` + +Once you're ready to get going, [head over to the documentation](https://www.encode.io/httpcore/). + +## Motivation + +You *probably* don't want to be using HTTP Core directly. It might make sense if +you're writing something like a proxy service in Python, and you just want +something at the lowest possible level, but more typically you'll want to use +a higher level client library, such as `httpx`. + +The motivation for `httpcore` is: + +* To provide a reusable low-level client library, that other packages can then build on top of. +* To provide a *really clear interface split* between the networking code and client logic, + so that each is easier to understand and reason about in isolation. + +## Dependencies + +The `httpcore` package has the following dependencies... + +* `h11` +* `certifi` + +And the following optional extras... + +* `anyio` - Required by `pip install httpcore['asyncio']`. +* `trio` - Required by `pip install httpcore['trio']`. +* `h2` - Required by `pip install httpcore['http2']`. +* `socksio` - Required by `pip install httpcore['socks']`. + +## Versioning + +We use [SEMVER for our versioning policy](https://semver.org/). + +For changes between package versions please see our [project changelog](CHANGELOG.md). + +We recommend pinning your requirements either the most current major version, or a more specific version range: + +```python +pip install 'httpcore==1.*' +``` +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## Version 1.0.9 (April 24th, 2025) + +- Resolve https://github.com/advisories/GHSA-vqfr-h8mv-ghfj with h11 dependency update. (#1008) + +## Version 1.0.8 (April 11th, 2025) + +- Fix `AttributeError` when importing on Python 3.14. (#1005) + +## Version 1.0.7 (November 15th, 2024) + +- Support `proxy=…` configuration on `ConnectionPool()`. (#974) + +## Version 1.0.6 (October 1st, 2024) + +- Relax `trio` dependency pinning. (#956) +- Handle `trio` raising `NotImplementedError` on unsupported platforms. (#955) +- Handle mapping `ssl.SSLError` to `httpcore.ConnectError`. (#918) + +## 1.0.5 (March 27th, 2024) + +- Handle `EndOfStream` exception for anyio backend. (#899) +- Allow trio `0.25.*` series in package dependancies. (#903) + +## 1.0.4 (February 21st, 2024) + +- Add `target` request extension. (#888) +- Fix support for connection `Upgrade` and `CONNECT` when some data in the stream has been read. (#882) + +## 1.0.3 (February 13th, 2024) + +- Fix support for async cancellations. (#880) +- Fix trace extension when used with socks proxy. (#849) +- Fix SSL context for connections using the "wss" scheme (#869) + +## 1.0.2 (November 10th, 2023) + +- Fix `float("inf")` timeouts in `Event.wait` function. (#846) + +## 1.0.1 (November 3rd, 2023) + +- Fix pool timeout to account for the total time spent retrying. (#823) +- Raise a neater RuntimeError when the correct async deps are not installed. (#826) +- Add support for synchronous TLS-in-TLS streams. (#840) + +## 1.0.0 (October 6th, 2023) + +From version 1.0 our async support is now optional, as the package has minimal dependencies by default. + +For async support use either `pip install 'httpcore[asyncio]'` or `pip install 'httpcore[trio]'`. + +The project versioning policy is now explicitly governed by SEMVER. See https://semver.org/. + +- Async support becomes fully optional. (#809) +- Add support for Python 3.12. (#807) + +## 0.18.0 (September 8th, 2023) + +- Add support for HTTPS proxies. (#745, #786) +- Drop Python 3.7 support. (#727) +- Handle `sni_hostname` extension with SOCKS proxy. (#774) +- Handle HTTP/1.1 half-closed connections gracefully. (#641) +- Change the type of `Extensions` from `Mapping[Str, Any]` to `MutableMapping[Str, Any]`. (#762) + +## 0.17.3 (July 5th, 2023) + +- Support async cancellations, ensuring that the connection pool is left in a clean state when cancellations occur. (#726) +- The networking backend interface has [been added to the public API](https://www.encode.io/httpcore/network-backends). Some classes which were previously private implementation detail are now part of the top-level public API. (#699) +- Graceful handling of HTTP/2 GoAway frames, with requests being transparently retried on a new connection. (#730) +- Add exceptions when a synchronous `trace callback` is passed to an asynchronous request or an asynchronous `trace callback` is passed to a synchronous request. (#717) +- Drop Python 3.7 support. (#727) + +## 0.17.2 (May 23th, 2023) + +- Add `socket_options` argument to `ConnectionPool` and `HTTProxy` classes. (#668) +- Improve logging with per-module logger names. (#690) +- Add `sni_hostname` request extension. (#696) +- Resolve race condition during import of `anyio` package. (#692) +- Enable TCP_NODELAY for all synchronous sockets. (#651) + +## 0.17.1 (May 17th, 2023) + +- If 'retries' is set, then allow retries if an SSL handshake error occurs. (#669) +- Improve correctness of tracebacks on network exceptions, by raising properly chained exceptions. (#678) +- Prevent connection-hanging behaviour when HTTP/2 connections are closed by a server-sent 'GoAway' frame. (#679) +- Fix edge-case exception when removing requests from the connection pool. (#680) +- Fix pool timeout edge-case. (#688) + +## 0.17.0 (March 16th, 2023) + +- Add DEBUG level logging. (#648) +- Respect HTTP/2 max concurrent streams when settings updates are sent by server. (#652) +- Increase the allowable HTTP header size to 100kB. (#647) +- Add `retries` option to SOCKS proxy classes. (#643) + +## 0.16.3 (December 20th, 2022) + +- Allow `ws` and `wss` schemes. Allows us to properly support websocket upgrade connections. (#625) +- Forwarding HTTP proxies use a connection-per-remote-host. Required by some proxy implementations. (#637) +- Don't raise `RuntimeError` when closing a connection pool with active connections. Removes some error cases when cancellations are used. (#631) +- Lazy import `anyio`, so that it's no longer a hard dependancy, and isn't imported if unused. (#639) + +## 0.16.2 (November 25th, 2022) + +- Revert 'Fix async cancellation behaviour', which introduced race conditions. (#627) +- Raise `RuntimeError` if attempting to us UNIX domain sockets on Windows. (#619) + +## 0.16.1 (November 17th, 2022) + +- Fix HTTP/1.1 interim informational responses, such as "100 Continue". (#605) + +## 0.16.0 (October 11th, 2022) + +- Support HTTP/1.1 informational responses. (#581) +- Fix async cancellation behaviour. (#580) +- Support `h11` 0.14. (#579) + +## 0.15.0 (May 17th, 2022) + +- Drop Python 3.6 support (#535) +- Ensure HTTP proxy CONNECT requests include `timeout` configuration. (#506) +- Switch to explicit `typing.Optional` for type hints. (#513) +- For `trio` map OSError exceptions to `ConnectError`. (#543) + +## 0.14.7 (February 4th, 2022) + +- Requests which raise a PoolTimeout need to be removed from the pool queue. (#502) +- Fix AttributeError that happened when Socks5Connection were terminated. (#501) + +## 0.14.6 (February 1st, 2022) + +- Fix SOCKS support for `http://` URLs. (#492) +- Resolve race condition around exceptions during streaming a response. (#491) + +## 0.14.5 (January 18th, 2022) + +- SOCKS proxy support. (#478) +- Add proxy_auth argument to HTTPProxy. (#481) +- Improve error message on 'RemoteProtocolError' exception when server disconnects without sending a response. (#479) + +## 0.14.4 (January 5th, 2022) + +- Support HTTP/2 on HTTPS tunnelling proxies. (#468) +- Fix proxy headers missing on HTTP forwarding. (#456) +- Only instantiate SSL context if required. (#457) +- More robust HTTP/2 handling. (#253, #439, #440, #441) + +## 0.14.3 (November 17th, 2021) + +- Fix race condition when removing closed connections from the pool. (#437) + +## 0.14.2 (November 16th, 2021) + +- Failed connections no longer remain in the pool. (Pull #433) + +## 0.14.1 (November 12th, 2021) + +- `max_connections` becomes optional. (Pull #429) +- `certifi` is now included in the install dependancies. (Pull #428) +- `h2` is now strictly optional. (Pull #428) + +## 0.14.0 (November 11th, 2021) + +The 0.14 release is a complete reworking of `httpcore`, comprehensively addressing some underlying issues in the connection pooling, as well as substantially redesigning the API to be more user friendly. + +Some of the lower-level API design also makes the components more easily testable in isolation, and the package now has 100% test coverage. + +See [discussion #419](https://github.com/encode/httpcore/discussions/419) for a little more background. + +There's some other neat bits in there too, such as the "trace" extension, which gives a hook into inspecting the internal events that occur during the request/response cycle. This extension is needed for the HTTPX cli, in order to... + +* Log the point at which the connection is established, and the IP/port on which it is made. +* Determine if the outgoing request should log as HTTP/1.1 or HTTP/2, rather than having to assume it's HTTP/2 if the --http2 flag was passed. (Which may not actually be true.) +* Log SSL version info / certificate info. + +Note that `curio` support is not currently available in 0.14.0. If you're using `httpcore` with `curio` please get in touch, so we can assess if we ought to prioritize it as a feature or not. + +## 0.13.7 (September 13th, 2021) + +- Fix broken error messaging when URL scheme is missing, or a non HTTP(S) scheme is used. (Pull #403) + +## 0.13.6 (June 15th, 2021) + +### Fixed + +- Close sockets when read or write timeouts occur. (Pull #365) + +## 0.13.5 (June 14th, 2021) + +### Fixed + +- Resolved niggles with AnyIO EOF behaviours. (Pull #358, #362) + +## 0.13.4 (June 9th, 2021) + +### Added + +- Improved error messaging when URL scheme is missing, or a non HTTP(S) scheme is used. (Pull #354) + +### Fixed + +- Switched to `anyio` as the default backend implementation when running with `asyncio`. Resolves some awkward [TLS timeout issues](https://github.com/encode/httpx/discussions/1511). + +## 0.13.3 (May 6th, 2021) + +### Added + +- Support HTTP/2 prior knowledge, using `httpcore.SyncConnectionPool(http1=False)`. (Pull #333) + +### Fixed + +- Handle cases where environment does not provide `select.poll` support. (Pull #331) + +## 0.13.2 (April 29th, 2021) + +### Added + +- Improve error message for specific case of `RemoteProtocolError` where server disconnects without sending a response. (Pull #313) + +## 0.13.1 (April 28th, 2021) + +### Fixed + +- More resiliant testing for closed connections. (Pull #311) +- Don't raise exceptions on ungraceful connection closes. (Pull #310) + +## 0.13.0 (April 21st, 2021) + +The 0.13 release updates the core API in order to match the HTTPX Transport API, +introduced in HTTPX 0.18 onwards. + +An example of making requests with the new interface is: + +```python +with httpcore.SyncConnectionPool() as http: + status_code, headers, stream, extensions = http.handle_request( + method=b'GET', + url=(b'https', b'example.org', 443, b'/'), + headers=[(b'host', b'example.org'), (b'user-agent', b'httpcore')] + stream=httpcore.ByteStream(b''), + extensions={} + ) + body = stream.read() + print(status_code, body) +``` + +### Changed + +- The `.request()` method is now `handle_request()`. (Pull #296) +- The `.arequest()` method is now `.handle_async_request()`. (Pull #296) +- The `headers` argument is no longer optional. (Pull #296) +- The `stream` argument is no longer optional. (Pull #296) +- The `ext` argument is now named `extensions`, and is no longer optional. (Pull #296) +- The `"reason"` extension keyword is now named `"reason_phrase"`. (Pull #296) +- The `"reason_phrase"` and `"http_version"` extensions now use byte strings for their values. (Pull #296) +- The `httpcore.PlainByteStream()` class becomes `httpcore.ByteStream()`. (Pull #296) + +### Added + +- Streams now support a `.read()` interface. (Pull #296) + +### Fixed + +- Task cancellation no longer leaks connections from the connection pool. (Pull #305) + +## 0.12.3 (December 7th, 2020) + +### Fixed + +- Abort SSL connections on close rather than waiting for remote EOF when using `asyncio`. (Pull #167) +- Fix exception raised in case of connect timeouts when using the `anyio` backend. (Pull #236) +- Fix `Host` header precedence for `:authority` in HTTP/2. (Pull #241, #243) +- Handle extra edge case when detecting for socket readability when using `asyncio`. (Pull #242, #244) +- Fix `asyncio` SSL warning when using proxy tunneling. (Pull #249) + +## 0.12.2 (November 20th, 2020) + +### Fixed + +- Properly wrap connect errors on the asyncio backend. (Pull #235) +- Fix `ImportError` occurring on Python 3.9 when using the HTTP/1.1 sync client in a multithreaded context. (Pull #237) + +## 0.12.1 (November 7th, 2020) + +### Added + +- Add connect retries. (Pull #221) + +### Fixed + +- Tweak detection of dropped connections, resolving an issue with open files limits on Linux. (Pull #185) +- Avoid leaking connections when establishing an HTTP tunnel to a proxy has failed. (Pull #223) +- Properly wrap OS errors when using `trio`. (Pull #225) + +## 0.12.0 (October 6th, 2020) + +### Changed + +- HTTP header casing is now preserved, rather than always sent in lowercase. (#216 and python-hyper/h11#104) + +### Added + +- Add Python 3.9 to officially supported versions. + +### Fixed + +- Gracefully handle a stdlib asyncio bug when a connection is closed while it is in a paused-for-reading state. (#201) + +## 0.11.1 (September 28nd, 2020) + +### Fixed + +- Add await to async semaphore release() coroutine (#197) +- Drop incorrect curio classifier (#192) + +## 0.11.0 (September 22nd, 2020) + +The Transport API with 0.11.0 has a couple of significant changes. + +Firstly we've moved changed the request interface in order to allow extensions, which will later enable us to support features +such as trailing headers, HTTP/2 server push, and CONNECT/Upgrade connections. + +The interface changes from: + +```python +def request(method, url, headers, stream, timeout): + return (http_version, status_code, reason, headers, stream) +``` + +To instead including an optional dictionary of extensions on the request and response: + +```python +def request(method, url, headers, stream, ext): + return (status_code, headers, stream, ext) +``` + +Having an open-ended extensions point will allow us to add later support for various optional features, that wouldn't otherwise be supported without these API changes. + +In particular: + +* Trailing headers support. +* HTTP/2 Server Push +* sendfile. +* Exposing raw connection on CONNECT, Upgrade, HTTP/2 bi-di streaming. +* Exposing debug information out of the API, including template name, template context. + +Currently extensions are limited to: + +* request: `timeout` - Optional. Timeout dictionary. +* response: `http_version` - Optional. Include the HTTP version used on the response. +* response: `reason` - Optional. Include the reason phrase used on the response. Only valid with HTTP/1.*. + +See https://github.com/encode/httpx/issues/1274#issuecomment-694884553 for the history behind this. + +Secondly, the async version of `request` is now namespaced as `arequest`. + +This allows concrete transports to support both sync and async implementations on the same class. + +### Added + +- Add curio support. (Pull #168) +- Add anyio support, with `backend="anyio"`. (Pull #169) + +### Changed + +- Update the Transport API to use 'ext' for optional extensions. (Pull #190) +- Update the Transport API to use `.request` and `.arequest` so implementations can support both sync and async. (Pull #189) + +## 0.10.2 (August 20th, 2020) + +### Added + +- Added Unix Domain Socket support. (Pull #139) + +### Fixed + +- Always include the port on proxy CONNECT requests. (Pull #154) +- Fix `max_keepalive_connections` configuration. (Pull #153) +- Fixes behaviour in HTTP/1.1 where server disconnects can be used to signal the end of the response body. (Pull #164) + +## 0.10.1 (August 7th, 2020) + +- Include `max_keepalive_connections` on `AsyncHTTPProxy`/`SyncHTTPProxy` classes. + +## 0.10.0 (August 7th, 2020) + +The most notable change in the 0.10.0 release is that HTTP/2 support is now fully optional. + +Use either `pip install httpcore` for HTTP/1.1 support only, or `pip install httpcore[http2]` for HTTP/1.1 and HTTP/2 support. + +### Added + +- HTTP/2 support becomes optional. (Pull #121, #130) +- Add `local_address=...` support. (Pull #100, #134) +- Add `PlainByteStream`, `IteratorByteStream`, `AsyncIteratorByteStream`. The `AsyncByteSteam` and `SyncByteStream` classes are now pure interface classes. (#133) +- Add `LocalProtocolError`, `RemoteProtocolError` exceptions. (Pull #129) +- Add `UnsupportedProtocol` exception. (Pull #128) +- Add `.get_connection_info()` method. (Pull #102, #137) +- Add better TRACE logs. (Pull #101) + +### Changed + +- `max_keepalive` is deprecated in favour of `max_keepalive_connections`. (Pull #140) + +### Fixed + +- Improve handling of server disconnects. (Pull #112) + +## 0.9.1 (May 27th, 2020) + +### Fixed + +- Proper host resolution for sync case, including IPv6 support. (Pull #97) +- Close outstanding connections when connection pool is closed. (Pull #98) + +## 0.9.0 (May 21th, 2020) + +### Changed + +- URL port becomes an `Optional[int]` instead of `int`. (Pull #92) + +### Fixed + +- Honor HTTP/2 max concurrent streams settings. (Pull #89, #90) +- Remove incorrect debug log. (Pull #83) + +## 0.8.4 (May 11th, 2020) + +### Added + +- Logging via HTTPCORE_LOG_LEVEL and HTTPX_LOG_LEVEL environment variables +and TRACE level logging. (Pull #79) + +### Fixed + +- Reuse of connections on HTTP/2 in close concurrency situations. (Pull #81) + +## 0.8.3 (May 6rd, 2020) + +### Fixed + +- Include `Host` and `Accept` headers on proxy "CONNECT" requests. +- De-duplicate any headers also contained in proxy_headers. +- HTTP/2 flag not being passed down to proxy connections. + +## 0.8.2 (May 3rd, 2020) + +### Fixed + +- Fix connections using proxy forwarding requests not being added to the +connection pool properly. (Pull #70) + +## 0.8.1 (April 30th, 2020) + +### Changed + +- Allow inherintance of both `httpcore.AsyncByteStream`, `httpcore.SyncByteStream` without type conflicts. + +## 0.8.0 (April 30th, 2020) + +### Fixed + +- Fixed tunnel proxy support. + +### Added + +- New `TimeoutException` base class. + +## 0.7.0 (March 5th, 2020) + +- First integration with HTTPX. diff --git a/source/target-vendors/cp312/httpcore-1.0.9.dist-info/RECORD b/source/target-vendors/cp312/httpcore-1.0.9.dist-info/RECORD new file mode 100644 index 0000000..d9a731a --- /dev/null +++ b/source/target-vendors/cp312/httpcore-1.0.9.dist-info/RECORD @@ -0,0 +1,36 @@ +httpcore/__init__.py,sha256=9kT_kqChCCJUTHww24ZmR_ezcdbpRYWksD-gYNzkZP8,3445 +httpcore/_api.py,sha256=unZmeDschBWCGCPCwkS3Wot9euK6bg_kKxLtGTxw214,3146 +httpcore/_exceptions.py,sha256=looCKga3_YVYu3s-d3L9RMPRJyhsY7fiuuGxvkOD0c0,1184 +httpcore/_models.py,sha256=IO2CcXcdpovRcLTdGFGB6RyBZdEm2h_TOmoCc4rEKho,17623 +httpcore/_ssl.py,sha256=srqmSNU4iOUvWF-SrJvb8G_YEbHFELOXQOwdDIBTS9c,187 +httpcore/_synchronization.py,sha256=zSi13mAColBnknjZBknUC6hKNDQT4C6ijnezZ-r0T2s,9434 +httpcore/_trace.py,sha256=ck6ZoIzYTkdNAIfq5MGeKqBXDtqjOX-qfYwmZFbrGco,3952 +httpcore/_utils.py,sha256=_RLgXYOAYC350ikALV59GZ68IJrdocRZxPs9PjmzdFY,1537 +httpcore/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +httpcore/_async/__init__.py,sha256=EWdl2v4thnAHzJpqjU4h2a8DUiGAvNiWrkii9pfhTf0,1221 +httpcore/_async/connection.py,sha256=6OcPXqMEfc0BU38_-iHUNDd1vKSTc2UVT09XqNb_BOk,8449 +httpcore/_async/connection_pool.py,sha256=DOIQ2s2ZCf9qfwxhzMprTPLqCL8OxGXiKF6qRHxvVyY,17307 +httpcore/_async/http11.py,sha256=-qM9bV7PjSQF5vxs37-eUXOIFwbIjPcZbNliuX9TtBw,13880 +httpcore/_async/http2.py,sha256=azX1fcmtXaIwjputFlZ4vd92J8xwjGOa9ax9QIv4394,23936 +httpcore/_async/http_proxy.py,sha256=2zVkrlv-Ds-rWGaqaXlrhEJiAQFPo23BT3Gq_sWoBXU,14701 +httpcore/_async/interfaces.py,sha256=jTiaWL83pgpGC9ziv90ZfwaKNMmHwmOalzaKiuTxATo,4455 +httpcore/_async/socks_proxy.py,sha256=lLKgLlggPfhFlqi0ODeBkOWvt9CghBBUyqsnsU1tx6Q,13841 +httpcore/_backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +httpcore/_backends/anyio.py,sha256=x8PgEhXRC8bVqsdzk_YJx8Y6d9Tub06CuUSwnbmtqoY,5252 +httpcore/_backends/auto.py,sha256=zO136PKZmsaTDK-HRk84eA-MUg8_2wJf4NvmK432Aio,1662 +httpcore/_backends/base.py,sha256=aShgRdZnMmRhFWHetjumlM73f8Kz1YOAyCUP_4kHslA,3042 +httpcore/_backends/mock.py,sha256=er9T436uSe7NLrfiLa4x6Nuqg5ivQ693CxWYCWsgbH4,4077 +httpcore/_backends/sync.py,sha256=bhE4d9iK9Umxdsdsgm2EfKnXaBms2WggGYU-7jmUujU,7977 +httpcore/_backends/trio.py,sha256=LHu4_Mr5MswQmmT3yE4oLgf9b_JJfeVS4BjDxeJc7Ro,5996 +httpcore/_sync/__init__.py,sha256=JBDIgXt5la1LCJ1sLQeKhjKFpLnpNr8Svs6z2ni3fgg,1141 +httpcore/_sync/connection.py,sha256=9exGOb3PB-Mp2T1-sckSeL2t-tJ_9-NXomV8ihmWCgU,8238 +httpcore/_sync/connection_pool.py,sha256=a-T8LTsUxc7r0Ww1atfHSDoWPjQ0fA8Ul7S3-F0Mj70,16955 +httpcore/_sync/http11.py,sha256=IFobD1Md5JFlJGKWnh1_Q3epikUryI8qo09v8MiJIEA,13476 +httpcore/_sync/http2.py,sha256=AxU4yhcq68Bn5vqdJYtiXKYUj7nvhYbxz3v4rT4xnvA,23400 +httpcore/_sync/http_proxy.py,sha256=_al_6crKuEZu2wyvu493RZImJdBJnj5oGKNjLOJL2Zo,14463 +httpcore/_sync/interfaces.py,sha256=snXON42vUDHO5JBJvo8D4VWk2Wat44z2OXXHDrjbl94,4344 +httpcore/_sync/socks_proxy.py,sha256=zegZW9Snqj2_992DFJa8_CppOVBkVL4AgwduRkStakQ,13614 +httpcore-1.0.9.dist-info/METADATA,sha256=_i1P2mGZEol4d54M8n88BFxTGGP83Zh-rMdPOhjUHCE,21529 +httpcore-1.0.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87 +httpcore-1.0.9.dist-info/licenses/LICENSE.md,sha256=_ctZFUx0y6uhahEkL3dAvqnyPW_rVUeRfYxflKgDkqU,1518 +httpcore-1.0.9.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/httpcore-1.0.9.dist-info/WHEEL b/source/target-vendors/cp312/httpcore-1.0.9.dist-info/WHEEL new file mode 100644 index 0000000..12228d4 --- /dev/null +++ b/source/target-vendors/cp312/httpcore-1.0.9.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.27.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/source/target-vendors/cp312/httpcore-1.0.9.dist-info/licenses/LICENSE.md b/source/target-vendors/cp312/httpcore-1.0.9.dist-info/licenses/LICENSE.md new file mode 100644 index 0000000..311b2b5 --- /dev/null +++ b/source/target-vendors/cp312/httpcore-1.0.9.dist-info/licenses/LICENSE.md @@ -0,0 +1,27 @@ +Copyright © 2020, [Encode OSS Ltd](https://www.encode.io/). +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/source/target-vendors/cp312/httpcore/__init__.py b/source/target-vendors/cp312/httpcore/__init__.py new file mode 100644 index 0000000..9a92dc4 --- /dev/null +++ b/source/target-vendors/cp312/httpcore/__init__.py @@ -0,0 +1,141 @@ +from ._api import request, stream +from ._async import ( + AsyncConnectionInterface, + AsyncConnectionPool, + AsyncHTTP2Connection, + AsyncHTTP11Connection, + AsyncHTTPConnection, + AsyncHTTPProxy, + AsyncSOCKSProxy, +) +from ._backends.base import ( + SOCKET_OPTION, + AsyncNetworkBackend, + AsyncNetworkStream, + NetworkBackend, + NetworkStream, +) +from ._backends.mock import AsyncMockBackend, AsyncMockStream, MockBackend, MockStream +from ._backends.sync import SyncBackend +from ._exceptions import ( + ConnectError, + ConnectionNotAvailable, + ConnectTimeout, + LocalProtocolError, + NetworkError, + PoolTimeout, + ProtocolError, + ProxyError, + ReadError, + ReadTimeout, + RemoteProtocolError, + TimeoutException, + UnsupportedProtocol, + WriteError, + WriteTimeout, +) +from ._models import URL, Origin, Proxy, Request, Response +from ._ssl import default_ssl_context +from ._sync import ( + ConnectionInterface, + ConnectionPool, + HTTP2Connection, + HTTP11Connection, + HTTPConnection, + HTTPProxy, + SOCKSProxy, +) + +# The 'httpcore.AnyIOBackend' class is conditional on 'anyio' being installed. +try: + from ._backends.anyio import AnyIOBackend +except ImportError: # pragma: nocover + + class AnyIOBackend: # type: ignore + def __init__(self, *args, **kwargs): # type: ignore + msg = ( + "Attempted to use 'httpcore.AnyIOBackend' but 'anyio' is not installed." + ) + raise RuntimeError(msg) + + +# The 'httpcore.TrioBackend' class is conditional on 'trio' being installed. +try: + from ._backends.trio import TrioBackend +except ImportError: # pragma: nocover + + class TrioBackend: # type: ignore + def __init__(self, *args, **kwargs): # type: ignore + msg = "Attempted to use 'httpcore.TrioBackend' but 'trio' is not installed." + raise RuntimeError(msg) + + +__all__ = [ + # top-level requests + "request", + "stream", + # models + "Origin", + "URL", + "Request", + "Response", + "Proxy", + # async + "AsyncHTTPConnection", + "AsyncConnectionPool", + "AsyncHTTPProxy", + "AsyncHTTP11Connection", + "AsyncHTTP2Connection", + "AsyncConnectionInterface", + "AsyncSOCKSProxy", + # sync + "HTTPConnection", + "ConnectionPool", + "HTTPProxy", + "HTTP11Connection", + "HTTP2Connection", + "ConnectionInterface", + "SOCKSProxy", + # network backends, implementations + "SyncBackend", + "AnyIOBackend", + "TrioBackend", + # network backends, mock implementations + "AsyncMockBackend", + "AsyncMockStream", + "MockBackend", + "MockStream", + # network backends, interface + "AsyncNetworkStream", + "AsyncNetworkBackend", + "NetworkStream", + "NetworkBackend", + # util + "default_ssl_context", + "SOCKET_OPTION", + # exceptions + "ConnectionNotAvailable", + "ProxyError", + "ProtocolError", + "LocalProtocolError", + "RemoteProtocolError", + "UnsupportedProtocol", + "TimeoutException", + "PoolTimeout", + "ConnectTimeout", + "ReadTimeout", + "WriteTimeout", + "NetworkError", + "ConnectError", + "ReadError", + "WriteError", +] + +__version__ = "1.0.9" + + +__locals = locals() +for __name in __all__: + # Exclude SOCKET_OPTION, it causes AttributeError on Python 3.14 + if not __name.startswith(("__", "SOCKET_OPTION")): + setattr(__locals[__name], "__module__", "httpcore") # noqa diff --git a/source/target-vendors/cp312/httpcore/_api.py b/source/target-vendors/cp312/httpcore/_api.py new file mode 100644 index 0000000..38b961d --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_api.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import contextlib +import typing + +from ._models import URL, Extensions, HeaderTypes, Response +from ._sync.connection_pool import ConnectionPool + + +def request( + method: bytes | str, + url: URL | bytes | str, + *, + headers: HeaderTypes = None, + content: bytes | typing.Iterator[bytes] | None = None, + extensions: Extensions | None = None, +) -> Response: + """ + Sends an HTTP request, returning the response. + + ``` + response = httpcore.request("GET", "https://www.example.com/") + ``` + + Arguments: + method: The HTTP method for the request. Typically one of `"GET"`, + `"OPTIONS"`, `"HEAD"`, `"POST"`, `"PUT"`, `"PATCH"`, or `"DELETE"`. + url: The URL of the HTTP request. Either as an instance of `httpcore.URL`, + or as str/bytes. + headers: The HTTP request headers. Either as a dictionary of str/bytes, + or as a list of two-tuples of str/bytes. + content: The content of the request body. Either as bytes, + or as a bytes iterator. + extensions: A dictionary of optional extra information included on the request. + Possible keys include `"timeout"`. + + Returns: + An instance of `httpcore.Response`. + """ + with ConnectionPool() as pool: + return pool.request( + method=method, + url=url, + headers=headers, + content=content, + extensions=extensions, + ) + + +@contextlib.contextmanager +def stream( + method: bytes | str, + url: URL | bytes | str, + *, + headers: HeaderTypes = None, + content: bytes | typing.Iterator[bytes] | None = None, + extensions: Extensions | None = None, +) -> typing.Iterator[Response]: + """ + Sends an HTTP request, returning the response within a content manager. + + ``` + with httpcore.stream("GET", "https://www.example.com/") as response: + ... + ``` + + When using the `stream()` function, the body of the response will not be + automatically read. If you want to access the response body you should + either use `content = response.read()`, or `for chunk in response.iter_content()`. + + Arguments: + method: The HTTP method for the request. Typically one of `"GET"`, + `"OPTIONS"`, `"HEAD"`, `"POST"`, `"PUT"`, `"PATCH"`, or `"DELETE"`. + url: The URL of the HTTP request. Either as an instance of `httpcore.URL`, + or as str/bytes. + headers: The HTTP request headers. Either as a dictionary of str/bytes, + or as a list of two-tuples of str/bytes. + content: The content of the request body. Either as bytes, + or as a bytes iterator. + extensions: A dictionary of optional extra information included on the request. + Possible keys include `"timeout"`. + + Returns: + An instance of `httpcore.Response`. + """ + with ConnectionPool() as pool: + with pool.stream( + method=method, + url=url, + headers=headers, + content=content, + extensions=extensions, + ) as response: + yield response diff --git a/source/target-vendors/cp312/httpcore/_async/__init__.py b/source/target-vendors/cp312/httpcore/_async/__init__.py new file mode 100644 index 0000000..88dc7f0 --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_async/__init__.py @@ -0,0 +1,39 @@ +from .connection import AsyncHTTPConnection +from .connection_pool import AsyncConnectionPool +from .http11 import AsyncHTTP11Connection +from .http_proxy import AsyncHTTPProxy +from .interfaces import AsyncConnectionInterface + +try: + from .http2 import AsyncHTTP2Connection +except ImportError: # pragma: nocover + + class AsyncHTTP2Connection: # type: ignore + def __init__(self, *args, **kwargs) -> None: # type: ignore + raise RuntimeError( + "Attempted to use http2 support, but the `h2` package is not " + "installed. Use 'pip install httpcore[http2]'." + ) + + +try: + from .socks_proxy import AsyncSOCKSProxy +except ImportError: # pragma: nocover + + class AsyncSOCKSProxy: # type: ignore + def __init__(self, *args, **kwargs) -> None: # type: ignore + raise RuntimeError( + "Attempted to use SOCKS support, but the `socksio` package is not " + "installed. Use 'pip install httpcore[socks]'." + ) + + +__all__ = [ + "AsyncHTTPConnection", + "AsyncConnectionPool", + "AsyncHTTPProxy", + "AsyncHTTP11Connection", + "AsyncHTTP2Connection", + "AsyncConnectionInterface", + "AsyncSOCKSProxy", +] diff --git a/source/target-vendors/cp312/httpcore/_async/connection.py b/source/target-vendors/cp312/httpcore/_async/connection.py new file mode 100644 index 0000000..b42581d --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_async/connection.py @@ -0,0 +1,222 @@ +from __future__ import annotations + +import itertools +import logging +import ssl +import types +import typing + +from .._backends.auto import AutoBackend +from .._backends.base import SOCKET_OPTION, AsyncNetworkBackend, AsyncNetworkStream +from .._exceptions import ConnectError, ConnectTimeout +from .._models import Origin, Request, Response +from .._ssl import default_ssl_context +from .._synchronization import AsyncLock +from .._trace import Trace +from .http11 import AsyncHTTP11Connection +from .interfaces import AsyncConnectionInterface + +RETRIES_BACKOFF_FACTOR = 0.5 # 0s, 0.5s, 1s, 2s, 4s, etc. + + +logger = logging.getLogger("httpcore.connection") + + +def exponential_backoff(factor: float) -> typing.Iterator[float]: + """ + Generate a geometric sequence that has a ratio of 2 and starts with 0. + + For example: + - `factor = 2`: `0, 2, 4, 8, 16, 32, 64, ...` + - `factor = 3`: `0, 3, 6, 12, 24, 48, 96, ...` + """ + yield 0 + for n in itertools.count(): + yield factor * 2**n + + +class AsyncHTTPConnection(AsyncConnectionInterface): + def __init__( + self, + origin: Origin, + ssl_context: ssl.SSLContext | None = None, + keepalive_expiry: float | None = None, + http1: bool = True, + http2: bool = False, + retries: int = 0, + local_address: str | None = None, + uds: str | None = None, + network_backend: AsyncNetworkBackend | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> None: + self._origin = origin + self._ssl_context = ssl_context + self._keepalive_expiry = keepalive_expiry + self._http1 = http1 + self._http2 = http2 + self._retries = retries + self._local_address = local_address + self._uds = uds + + self._network_backend: AsyncNetworkBackend = ( + AutoBackend() if network_backend is None else network_backend + ) + self._connection: AsyncConnectionInterface | None = None + self._connect_failed: bool = False + self._request_lock = AsyncLock() + self._socket_options = socket_options + + async def handle_async_request(self, request: Request) -> Response: + if not self.can_handle_request(request.url.origin): + raise RuntimeError( + f"Attempted to send request to {request.url.origin} on connection to {self._origin}" + ) + + try: + async with self._request_lock: + if self._connection is None: + stream = await self._connect(request) + + ssl_object = stream.get_extra_info("ssl_object") + http2_negotiated = ( + ssl_object is not None + and ssl_object.selected_alpn_protocol() == "h2" + ) + if http2_negotiated or (self._http2 and not self._http1): + from .http2 import AsyncHTTP2Connection + + self._connection = AsyncHTTP2Connection( + origin=self._origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + else: + self._connection = AsyncHTTP11Connection( + origin=self._origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + except BaseException as exc: + self._connect_failed = True + raise exc + + return await self._connection.handle_async_request(request) + + async def _connect(self, request: Request) -> AsyncNetworkStream: + timeouts = request.extensions.get("timeout", {}) + sni_hostname = request.extensions.get("sni_hostname", None) + timeout = timeouts.get("connect", None) + + retries_left = self._retries + delays = exponential_backoff(factor=RETRIES_BACKOFF_FACTOR) + + while True: + try: + if self._uds is None: + kwargs = { + "host": self._origin.host.decode("ascii"), + "port": self._origin.port, + "local_address": self._local_address, + "timeout": timeout, + "socket_options": self._socket_options, + } + async with Trace("connect_tcp", logger, request, kwargs) as trace: + stream = await self._network_backend.connect_tcp(**kwargs) + trace.return_value = stream + else: + kwargs = { + "path": self._uds, + "timeout": timeout, + "socket_options": self._socket_options, + } + async with Trace( + "connect_unix_socket", logger, request, kwargs + ) as trace: + stream = await self._network_backend.connect_unix_socket( + **kwargs + ) + trace.return_value = stream + + if self._origin.scheme in (b"https", b"wss"): + ssl_context = ( + default_ssl_context() + if self._ssl_context is None + else self._ssl_context + ) + alpn_protocols = ["http/1.1", "h2"] if self._http2 else ["http/1.1"] + ssl_context.set_alpn_protocols(alpn_protocols) + + kwargs = { + "ssl_context": ssl_context, + "server_hostname": sni_hostname + or self._origin.host.decode("ascii"), + "timeout": timeout, + } + async with Trace("start_tls", logger, request, kwargs) as trace: + stream = await stream.start_tls(**kwargs) + trace.return_value = stream + return stream + except (ConnectError, ConnectTimeout): + if retries_left <= 0: + raise + retries_left -= 1 + delay = next(delays) + async with Trace("retry", logger, request, kwargs) as trace: + await self._network_backend.sleep(delay) + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._origin + + async def aclose(self) -> None: + if self._connection is not None: + async with Trace("close", logger, None, {}): + await self._connection.aclose() + + def is_available(self) -> bool: + if self._connection is None: + # If HTTP/2 support is enabled, and the resulting connection could + # end up as HTTP/2 then we should indicate the connection as being + # available to service multiple requests. + return ( + self._http2 + and (self._origin.scheme == b"https" or not self._http1) + and not self._connect_failed + ) + return self._connection.is_available() + + def has_expired(self) -> bool: + if self._connection is None: + return self._connect_failed + return self._connection.has_expired() + + def is_idle(self) -> bool: + if self._connection is None: + return self._connect_failed + return self._connection.is_idle() + + def is_closed(self) -> bool: + if self._connection is None: + return self._connect_failed + return self._connection.is_closed() + + def info(self) -> str: + if self._connection is None: + return "CONNECTION FAILED" if self._connect_failed else "CONNECTING" + return self._connection.info() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{self.info()}]>" + + # These context managers are not used in the standard flow, but are + # useful for testing or working with connection instances directly. + + async def __aenter__(self) -> AsyncHTTPConnection: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: types.TracebackType | None = None, + ) -> None: + await self.aclose() diff --git a/source/target-vendors/cp312/httpcore/_async/connection_pool.py b/source/target-vendors/cp312/httpcore/_async/connection_pool.py new file mode 100644 index 0000000..96e973d --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_async/connection_pool.py @@ -0,0 +1,420 @@ +from __future__ import annotations + +import ssl +import sys +import types +import typing + +from .._backends.auto import AutoBackend +from .._backends.base import SOCKET_OPTION, AsyncNetworkBackend +from .._exceptions import ConnectionNotAvailable, UnsupportedProtocol +from .._models import Origin, Proxy, Request, Response +from .._synchronization import AsyncEvent, AsyncShieldCancellation, AsyncThreadLock +from .connection import AsyncHTTPConnection +from .interfaces import AsyncConnectionInterface, AsyncRequestInterface + + +class AsyncPoolRequest: + def __init__(self, request: Request) -> None: + self.request = request + self.connection: AsyncConnectionInterface | None = None + self._connection_acquired = AsyncEvent() + + def assign_to_connection(self, connection: AsyncConnectionInterface | None) -> None: + self.connection = connection + self._connection_acquired.set() + + def clear_connection(self) -> None: + self.connection = None + self._connection_acquired = AsyncEvent() + + async def wait_for_connection( + self, timeout: float | None = None + ) -> AsyncConnectionInterface: + if self.connection is None: + await self._connection_acquired.wait(timeout=timeout) + assert self.connection is not None + return self.connection + + def is_queued(self) -> bool: + return self.connection is None + + +class AsyncConnectionPool(AsyncRequestInterface): + """ + A connection pool for making HTTP requests. + """ + + def __init__( + self, + ssl_context: ssl.SSLContext | None = None, + proxy: Proxy | None = None, + max_connections: int | None = 10, + max_keepalive_connections: int | None = None, + keepalive_expiry: float | None = None, + http1: bool = True, + http2: bool = False, + retries: int = 0, + local_address: str | None = None, + uds: str | None = None, + network_backend: AsyncNetworkBackend | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> None: + """ + A connection pool for making HTTP requests. + + Parameters: + ssl_context: An SSL context to use for verifying connections. + If not specified, the default `httpcore.default_ssl_context()` + will be used. + max_connections: The maximum number of concurrent HTTP connections that + the pool should allow. Any attempt to send a request on a pool that + would exceed this amount will block until a connection is available. + max_keepalive_connections: The maximum number of idle HTTP connections + that will be maintained in the pool. + keepalive_expiry: The duration in seconds that an idle HTTP connection + may be maintained for before being expired from the pool. + http1: A boolean indicating if HTTP/1.1 requests should be supported + by the connection pool. Defaults to True. + http2: A boolean indicating if HTTP/2 requests should be supported by + the connection pool. Defaults to False. + retries: The maximum number of retries when trying to establish a + connection. + local_address: Local address to connect from. Can also be used to connect + using a particular address family. Using `local_address="0.0.0.0"` + will connect using an `AF_INET` address (IPv4), while using + `local_address="::"` will connect using an `AF_INET6` address (IPv6). + uds: Path to a Unix Domain Socket to use instead of TCP sockets. + network_backend: A backend instance to use for handling network I/O. + socket_options: Socket options that have to be included + in the TCP socket when the connection was established. + """ + self._ssl_context = ssl_context + self._proxy = proxy + self._max_connections = ( + sys.maxsize if max_connections is None else max_connections + ) + self._max_keepalive_connections = ( + sys.maxsize + if max_keepalive_connections is None + else max_keepalive_connections + ) + self._max_keepalive_connections = min( + self._max_connections, self._max_keepalive_connections + ) + + self._keepalive_expiry = keepalive_expiry + self._http1 = http1 + self._http2 = http2 + self._retries = retries + self._local_address = local_address + self._uds = uds + + self._network_backend = ( + AutoBackend() if network_backend is None else network_backend + ) + self._socket_options = socket_options + + # The mutable state on a connection pool is the queue of incoming requests, + # and the set of connections that are servicing those requests. + self._connections: list[AsyncConnectionInterface] = [] + self._requests: list[AsyncPoolRequest] = [] + + # We only mutate the state of the connection pool within an 'optional_thread_lock' + # context. This holds a threading lock unless we're running in async mode, + # in which case it is a no-op. + self._optional_thread_lock = AsyncThreadLock() + + def create_connection(self, origin: Origin) -> AsyncConnectionInterface: + if self._proxy is not None: + if self._proxy.url.scheme in (b"socks5", b"socks5h"): + from .socks_proxy import AsyncSocks5Connection + + return AsyncSocks5Connection( + proxy_origin=self._proxy.url.origin, + proxy_auth=self._proxy.auth, + remote_origin=origin, + ssl_context=self._ssl_context, + keepalive_expiry=self._keepalive_expiry, + http1=self._http1, + http2=self._http2, + network_backend=self._network_backend, + ) + elif origin.scheme == b"http": + from .http_proxy import AsyncForwardHTTPConnection + + return AsyncForwardHTTPConnection( + proxy_origin=self._proxy.url.origin, + proxy_headers=self._proxy.headers, + proxy_ssl_context=self._proxy.ssl_context, + remote_origin=origin, + keepalive_expiry=self._keepalive_expiry, + network_backend=self._network_backend, + ) + from .http_proxy import AsyncTunnelHTTPConnection + + return AsyncTunnelHTTPConnection( + proxy_origin=self._proxy.url.origin, + proxy_headers=self._proxy.headers, + proxy_ssl_context=self._proxy.ssl_context, + remote_origin=origin, + ssl_context=self._ssl_context, + keepalive_expiry=self._keepalive_expiry, + http1=self._http1, + http2=self._http2, + network_backend=self._network_backend, + ) + + return AsyncHTTPConnection( + origin=origin, + ssl_context=self._ssl_context, + keepalive_expiry=self._keepalive_expiry, + http1=self._http1, + http2=self._http2, + retries=self._retries, + local_address=self._local_address, + uds=self._uds, + network_backend=self._network_backend, + socket_options=self._socket_options, + ) + + @property + def connections(self) -> list[AsyncConnectionInterface]: + """ + Return a list of the connections currently in the pool. + + For example: + + ```python + >>> pool.connections + [ + , + , + , + ] + ``` + """ + return list(self._connections) + + async def handle_async_request(self, request: Request) -> Response: + """ + Send an HTTP request, and return an HTTP response. + + This is the core implementation that is called into by `.request()` or `.stream()`. + """ + scheme = request.url.scheme.decode() + if scheme == "": + raise UnsupportedProtocol( + "Request URL is missing an 'http://' or 'https://' protocol." + ) + if scheme not in ("http", "https", "ws", "wss"): + raise UnsupportedProtocol( + f"Request URL has an unsupported protocol '{scheme}://'." + ) + + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("pool", None) + + with self._optional_thread_lock: + # Add the incoming request to our request queue. + pool_request = AsyncPoolRequest(request) + self._requests.append(pool_request) + + try: + while True: + with self._optional_thread_lock: + # Assign incoming requests to available connections, + # closing or creating new connections as required. + closing = self._assign_requests_to_connections() + await self._close_connections(closing) + + # Wait until this request has an assigned connection. + connection = await pool_request.wait_for_connection(timeout=timeout) + + try: + # Send the request on the assigned connection. + response = await connection.handle_async_request( + pool_request.request + ) + except ConnectionNotAvailable: + # In some cases a connection may initially be available to + # handle a request, but then become unavailable. + # + # In this case we clear the connection and try again. + pool_request.clear_connection() + else: + break # pragma: nocover + + except BaseException as exc: + with self._optional_thread_lock: + # For any exception or cancellation we remove the request from + # the queue, and then re-assign requests to connections. + self._requests.remove(pool_request) + closing = self._assign_requests_to_connections() + + await self._close_connections(closing) + raise exc from None + + # Return the response. Note that in this case we still have to manage + # the point at which the response is closed. + assert isinstance(response.stream, typing.AsyncIterable) + return Response( + status=response.status, + headers=response.headers, + content=PoolByteStream( + stream=response.stream, pool_request=pool_request, pool=self + ), + extensions=response.extensions, + ) + + def _assign_requests_to_connections(self) -> list[AsyncConnectionInterface]: + """ + Manage the state of the connection pool, assigning incoming + requests to connections as available. + + Called whenever a new request is added or removed from the pool. + + Any closing connections are returned, allowing the I/O for closing + those connections to be handled seperately. + """ + closing_connections = [] + + # First we handle cleaning up any connections that are closed, + # have expired their keep-alive, or surplus idle connections. + for connection in list(self._connections): + if connection.is_closed(): + # log: "removing closed connection" + self._connections.remove(connection) + elif connection.has_expired(): + # log: "closing expired connection" + self._connections.remove(connection) + closing_connections.append(connection) + elif ( + connection.is_idle() + and len([connection.is_idle() for connection in self._connections]) + > self._max_keepalive_connections + ): + # log: "closing idle connection" + self._connections.remove(connection) + closing_connections.append(connection) + + # Assign queued requests to connections. + queued_requests = [request for request in self._requests if request.is_queued()] + for pool_request in queued_requests: + origin = pool_request.request.url.origin + available_connections = [ + connection + for connection in self._connections + if connection.can_handle_request(origin) and connection.is_available() + ] + idle_connections = [ + connection for connection in self._connections if connection.is_idle() + ] + + # There are three cases for how we may be able to handle the request: + # + # 1. There is an existing connection that can handle the request. + # 2. We can create a new connection to handle the request. + # 3. We can close an idle connection and then create a new connection + # to handle the request. + if available_connections: + # log: "reusing existing connection" + connection = available_connections[0] + pool_request.assign_to_connection(connection) + elif len(self._connections) < self._max_connections: + # log: "creating new connection" + connection = self.create_connection(origin) + self._connections.append(connection) + pool_request.assign_to_connection(connection) + elif idle_connections: + # log: "closing idle connection" + connection = idle_connections[0] + self._connections.remove(connection) + closing_connections.append(connection) + # log: "creating new connection" + connection = self.create_connection(origin) + self._connections.append(connection) + pool_request.assign_to_connection(connection) + + return closing_connections + + async def _close_connections(self, closing: list[AsyncConnectionInterface]) -> None: + # Close connections which have been removed from the pool. + with AsyncShieldCancellation(): + for connection in closing: + await connection.aclose() + + async def aclose(self) -> None: + # Explicitly close the connection pool. + # Clears all existing requests and connections. + with self._optional_thread_lock: + closing_connections = list(self._connections) + self._connections = [] + await self._close_connections(closing_connections) + + async def __aenter__(self) -> AsyncConnectionPool: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: types.TracebackType | None = None, + ) -> None: + await self.aclose() + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + with self._optional_thread_lock: + request_is_queued = [request.is_queued() for request in self._requests] + connection_is_idle = [ + connection.is_idle() for connection in self._connections + ] + + num_active_requests = request_is_queued.count(False) + num_queued_requests = request_is_queued.count(True) + num_active_connections = connection_is_idle.count(False) + num_idle_connections = connection_is_idle.count(True) + + requests_info = ( + f"Requests: {num_active_requests} active, {num_queued_requests} queued" + ) + connection_info = ( + f"Connections: {num_active_connections} active, {num_idle_connections} idle" + ) + + return f"<{class_name} [{requests_info} | {connection_info}]>" + + +class PoolByteStream: + def __init__( + self, + stream: typing.AsyncIterable[bytes], + pool_request: AsyncPoolRequest, + pool: AsyncConnectionPool, + ) -> None: + self._stream = stream + self._pool_request = pool_request + self._pool = pool + self._closed = False + + async def __aiter__(self) -> typing.AsyncIterator[bytes]: + try: + async for part in self._stream: + yield part + except BaseException as exc: + await self.aclose() + raise exc from None + + async def aclose(self) -> None: + if not self._closed: + self._closed = True + with AsyncShieldCancellation(): + if hasattr(self._stream, "aclose"): + await self._stream.aclose() + + with self._pool._optional_thread_lock: + self._pool._requests.remove(self._pool_request) + closing = self._pool._assign_requests_to_connections() + + await self._pool._close_connections(closing) diff --git a/source/target-vendors/cp312/httpcore/_async/http11.py b/source/target-vendors/cp312/httpcore/_async/http11.py new file mode 100644 index 0000000..e6d6d70 --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_async/http11.py @@ -0,0 +1,379 @@ +from __future__ import annotations + +import enum +import logging +import ssl +import time +import types +import typing + +import h11 + +from .._backends.base import AsyncNetworkStream +from .._exceptions import ( + ConnectionNotAvailable, + LocalProtocolError, + RemoteProtocolError, + WriteError, + map_exceptions, +) +from .._models import Origin, Request, Response +from .._synchronization import AsyncLock, AsyncShieldCancellation +from .._trace import Trace +from .interfaces import AsyncConnectionInterface + +logger = logging.getLogger("httpcore.http11") + + +# A subset of `h11.Event` types supported by `_send_event` +H11SendEvent = typing.Union[ + h11.Request, + h11.Data, + h11.EndOfMessage, +] + + +class HTTPConnectionState(enum.IntEnum): + NEW = 0 + ACTIVE = 1 + IDLE = 2 + CLOSED = 3 + + +class AsyncHTTP11Connection(AsyncConnectionInterface): + READ_NUM_BYTES = 64 * 1024 + MAX_INCOMPLETE_EVENT_SIZE = 100 * 1024 + + def __init__( + self, + origin: Origin, + stream: AsyncNetworkStream, + keepalive_expiry: float | None = None, + ) -> None: + self._origin = origin + self._network_stream = stream + self._keepalive_expiry: float | None = keepalive_expiry + self._expire_at: float | None = None + self._state = HTTPConnectionState.NEW + self._state_lock = AsyncLock() + self._request_count = 0 + self._h11_state = h11.Connection( + our_role=h11.CLIENT, + max_incomplete_event_size=self.MAX_INCOMPLETE_EVENT_SIZE, + ) + + async def handle_async_request(self, request: Request) -> Response: + if not self.can_handle_request(request.url.origin): + raise RuntimeError( + f"Attempted to send request to {request.url.origin} on connection " + f"to {self._origin}" + ) + + async with self._state_lock: + if self._state in (HTTPConnectionState.NEW, HTTPConnectionState.IDLE): + self._request_count += 1 + self._state = HTTPConnectionState.ACTIVE + self._expire_at = None + else: + raise ConnectionNotAvailable() + + try: + kwargs = {"request": request} + try: + async with Trace( + "send_request_headers", logger, request, kwargs + ) as trace: + await self._send_request_headers(**kwargs) + async with Trace("send_request_body", logger, request, kwargs) as trace: + await self._send_request_body(**kwargs) + except WriteError: + # If we get a write error while we're writing the request, + # then we supress this error and move on to attempting to + # read the response. Servers can sometimes close the request + # pre-emptively and then respond with a well formed HTTP + # error response. + pass + + async with Trace( + "receive_response_headers", logger, request, kwargs + ) as trace: + ( + http_version, + status, + reason_phrase, + headers, + trailing_data, + ) = await self._receive_response_headers(**kwargs) + trace.return_value = ( + http_version, + status, + reason_phrase, + headers, + ) + + network_stream = self._network_stream + + # CONNECT or Upgrade request + if (status == 101) or ( + (request.method == b"CONNECT") and (200 <= status < 300) + ): + network_stream = AsyncHTTP11UpgradeStream(network_stream, trailing_data) + + return Response( + status=status, + headers=headers, + content=HTTP11ConnectionByteStream(self, request), + extensions={ + "http_version": http_version, + "reason_phrase": reason_phrase, + "network_stream": network_stream, + }, + ) + except BaseException as exc: + with AsyncShieldCancellation(): + async with Trace("response_closed", logger, request) as trace: + await self._response_closed() + raise exc + + # Sending the request... + + async def _send_request_headers(self, request: Request) -> None: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("write", None) + + with map_exceptions({h11.LocalProtocolError: LocalProtocolError}): + event = h11.Request( + method=request.method, + target=request.url.target, + headers=request.headers, + ) + await self._send_event(event, timeout=timeout) + + async def _send_request_body(self, request: Request) -> None: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("write", None) + + assert isinstance(request.stream, typing.AsyncIterable) + async for chunk in request.stream: + event = h11.Data(data=chunk) + await self._send_event(event, timeout=timeout) + + await self._send_event(h11.EndOfMessage(), timeout=timeout) + + async def _send_event(self, event: h11.Event, timeout: float | None = None) -> None: + bytes_to_send = self._h11_state.send(event) + if bytes_to_send is not None: + await self._network_stream.write(bytes_to_send, timeout=timeout) + + # Receiving the response... + + async def _receive_response_headers( + self, request: Request + ) -> tuple[bytes, int, bytes, list[tuple[bytes, bytes]], bytes]: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("read", None) + + while True: + event = await self._receive_event(timeout=timeout) + if isinstance(event, h11.Response): + break + if ( + isinstance(event, h11.InformationalResponse) + and event.status_code == 101 + ): + break + + http_version = b"HTTP/" + event.http_version + + # h11 version 0.11+ supports a `raw_items` interface to get the + # raw header casing, rather than the enforced lowercase headers. + headers = event.headers.raw_items() + + trailing_data, _ = self._h11_state.trailing_data + + return http_version, event.status_code, event.reason, headers, trailing_data + + async def _receive_response_body( + self, request: Request + ) -> typing.AsyncIterator[bytes]: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("read", None) + + while True: + event = await self._receive_event(timeout=timeout) + if isinstance(event, h11.Data): + yield bytes(event.data) + elif isinstance(event, (h11.EndOfMessage, h11.PAUSED)): + break + + async def _receive_event( + self, timeout: float | None = None + ) -> h11.Event | type[h11.PAUSED]: + while True: + with map_exceptions({h11.RemoteProtocolError: RemoteProtocolError}): + event = self._h11_state.next_event() + + if event is h11.NEED_DATA: + data = await self._network_stream.read( + self.READ_NUM_BYTES, timeout=timeout + ) + + # If we feed this case through h11 we'll raise an exception like: + # + # httpcore.RemoteProtocolError: can't handle event type + # ConnectionClosed when role=SERVER and state=SEND_RESPONSE + # + # Which is accurate, but not very informative from an end-user + # perspective. Instead we handle this case distinctly and treat + # it as a ConnectError. + if data == b"" and self._h11_state.their_state == h11.SEND_RESPONSE: + msg = "Server disconnected without sending a response." + raise RemoteProtocolError(msg) + + self._h11_state.receive_data(data) + else: + # mypy fails to narrow the type in the above if statement above + return event # type: ignore[return-value] + + async def _response_closed(self) -> None: + async with self._state_lock: + if ( + self._h11_state.our_state is h11.DONE + and self._h11_state.their_state is h11.DONE + ): + self._state = HTTPConnectionState.IDLE + self._h11_state.start_next_cycle() + if self._keepalive_expiry is not None: + now = time.monotonic() + self._expire_at = now + self._keepalive_expiry + else: + await self.aclose() + + # Once the connection is no longer required... + + async def aclose(self) -> None: + # Note that this method unilaterally closes the connection, and does + # not have any kind of locking in place around it. + self._state = HTTPConnectionState.CLOSED + await self._network_stream.aclose() + + # The AsyncConnectionInterface methods provide information about the state of + # the connection, allowing for a connection pooling implementation to + # determine when to reuse and when to close the connection... + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._origin + + def is_available(self) -> bool: + # Note that HTTP/1.1 connections in the "NEW" state are not treated as + # being "available". The control flow which created the connection will + # be able to send an outgoing request, but the connection will not be + # acquired from the connection pool for any other request. + return self._state == HTTPConnectionState.IDLE + + def has_expired(self) -> bool: + now = time.monotonic() + keepalive_expired = self._expire_at is not None and now > self._expire_at + + # If the HTTP connection is idle but the socket is readable, then the + # only valid state is that the socket is about to return b"", indicating + # a server-initiated disconnect. + server_disconnected = ( + self._state == HTTPConnectionState.IDLE + and self._network_stream.get_extra_info("is_readable") + ) + + return keepalive_expired or server_disconnected + + def is_idle(self) -> bool: + return self._state == HTTPConnectionState.IDLE + + def is_closed(self) -> bool: + return self._state == HTTPConnectionState.CLOSED + + def info(self) -> str: + origin = str(self._origin) + return ( + f"{origin!r}, HTTP/1.1, {self._state.name}, " + f"Request Count: {self._request_count}" + ) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + origin = str(self._origin) + return ( + f"<{class_name} [{origin!r}, {self._state.name}, " + f"Request Count: {self._request_count}]>" + ) + + # These context managers are not used in the standard flow, but are + # useful for testing or working with connection instances directly. + + async def __aenter__(self) -> AsyncHTTP11Connection: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: types.TracebackType | None = None, + ) -> None: + await self.aclose() + + +class HTTP11ConnectionByteStream: + def __init__(self, connection: AsyncHTTP11Connection, request: Request) -> None: + self._connection = connection + self._request = request + self._closed = False + + async def __aiter__(self) -> typing.AsyncIterator[bytes]: + kwargs = {"request": self._request} + try: + async with Trace("receive_response_body", logger, self._request, kwargs): + async for chunk in self._connection._receive_response_body(**kwargs): + yield chunk + except BaseException as exc: + # If we get an exception while streaming the response, + # we want to close the response (and possibly the connection) + # before raising that exception. + with AsyncShieldCancellation(): + await self.aclose() + raise exc + + async def aclose(self) -> None: + if not self._closed: + self._closed = True + async with Trace("response_closed", logger, self._request): + await self._connection._response_closed() + + +class AsyncHTTP11UpgradeStream(AsyncNetworkStream): + def __init__(self, stream: AsyncNetworkStream, leading_data: bytes) -> None: + self._stream = stream + self._leading_data = leading_data + + async def read(self, max_bytes: int, timeout: float | None = None) -> bytes: + if self._leading_data: + buffer = self._leading_data[:max_bytes] + self._leading_data = self._leading_data[max_bytes:] + return buffer + else: + return await self._stream.read(max_bytes, timeout) + + async def write(self, buffer: bytes, timeout: float | None = None) -> None: + await self._stream.write(buffer, timeout) + + async def aclose(self) -> None: + await self._stream.aclose() + + async def start_tls( + self, + ssl_context: ssl.SSLContext, + server_hostname: str | None = None, + timeout: float | None = None, + ) -> AsyncNetworkStream: + return await self._stream.start_tls(ssl_context, server_hostname, timeout) + + def get_extra_info(self, info: str) -> typing.Any: + return self._stream.get_extra_info(info) diff --git a/source/target-vendors/cp312/httpcore/_async/http2.py b/source/target-vendors/cp312/httpcore/_async/http2.py new file mode 100644 index 0000000..dbd0bee --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_async/http2.py @@ -0,0 +1,592 @@ +from __future__ import annotations + +import enum +import logging +import time +import types +import typing + +import h2.config +import h2.connection +import h2.events +import h2.exceptions +import h2.settings + +from .._backends.base import AsyncNetworkStream +from .._exceptions import ( + ConnectionNotAvailable, + LocalProtocolError, + RemoteProtocolError, +) +from .._models import Origin, Request, Response +from .._synchronization import AsyncLock, AsyncSemaphore, AsyncShieldCancellation +from .._trace import Trace +from .interfaces import AsyncConnectionInterface + +logger = logging.getLogger("httpcore.http2") + + +def has_body_headers(request: Request) -> bool: + return any( + k.lower() == b"content-length" or k.lower() == b"transfer-encoding" + for k, v in request.headers + ) + + +class HTTPConnectionState(enum.IntEnum): + ACTIVE = 1 + IDLE = 2 + CLOSED = 3 + + +class AsyncHTTP2Connection(AsyncConnectionInterface): + READ_NUM_BYTES = 64 * 1024 + CONFIG = h2.config.H2Configuration(validate_inbound_headers=False) + + def __init__( + self, + origin: Origin, + stream: AsyncNetworkStream, + keepalive_expiry: float | None = None, + ): + self._origin = origin + self._network_stream = stream + self._keepalive_expiry: float | None = keepalive_expiry + self._h2_state = h2.connection.H2Connection(config=self.CONFIG) + self._state = HTTPConnectionState.IDLE + self._expire_at: float | None = None + self._request_count = 0 + self._init_lock = AsyncLock() + self._state_lock = AsyncLock() + self._read_lock = AsyncLock() + self._write_lock = AsyncLock() + self._sent_connection_init = False + self._used_all_stream_ids = False + self._connection_error = False + + # Mapping from stream ID to response stream events. + self._events: dict[ + int, + list[ + h2.events.ResponseReceived + | h2.events.DataReceived + | h2.events.StreamEnded + | h2.events.StreamReset, + ], + ] = {} + + # Connection terminated events are stored as state since + # we need to handle them for all streams. + self._connection_terminated: h2.events.ConnectionTerminated | None = None + + self._read_exception: Exception | None = None + self._write_exception: Exception | None = None + + async def handle_async_request(self, request: Request) -> Response: + if not self.can_handle_request(request.url.origin): + # This cannot occur in normal operation, since the connection pool + # will only send requests on connections that handle them. + # It's in place simply for resilience as a guard against incorrect + # usage, for anyone working directly with httpcore connections. + raise RuntimeError( + f"Attempted to send request to {request.url.origin} on connection " + f"to {self._origin}" + ) + + async with self._state_lock: + if self._state in (HTTPConnectionState.ACTIVE, HTTPConnectionState.IDLE): + self._request_count += 1 + self._expire_at = None + self._state = HTTPConnectionState.ACTIVE + else: + raise ConnectionNotAvailable() + + async with self._init_lock: + if not self._sent_connection_init: + try: + sci_kwargs = {"request": request} + async with Trace( + "send_connection_init", logger, request, sci_kwargs + ): + await self._send_connection_init(**sci_kwargs) + except BaseException as exc: + with AsyncShieldCancellation(): + await self.aclose() + raise exc + + self._sent_connection_init = True + + # Initially start with just 1 until the remote server provides + # its max_concurrent_streams value + self._max_streams = 1 + + local_settings_max_streams = ( + self._h2_state.local_settings.max_concurrent_streams + ) + self._max_streams_semaphore = AsyncSemaphore(local_settings_max_streams) + + for _ in range(local_settings_max_streams - self._max_streams): + await self._max_streams_semaphore.acquire() + + await self._max_streams_semaphore.acquire() + + try: + stream_id = self._h2_state.get_next_available_stream_id() + self._events[stream_id] = [] + except h2.exceptions.NoAvailableStreamIDError: # pragma: nocover + self._used_all_stream_ids = True + self._request_count -= 1 + raise ConnectionNotAvailable() + + try: + kwargs = {"request": request, "stream_id": stream_id} + async with Trace("send_request_headers", logger, request, kwargs): + await self._send_request_headers(request=request, stream_id=stream_id) + async with Trace("send_request_body", logger, request, kwargs): + await self._send_request_body(request=request, stream_id=stream_id) + async with Trace( + "receive_response_headers", logger, request, kwargs + ) as trace: + status, headers = await self._receive_response( + request=request, stream_id=stream_id + ) + trace.return_value = (status, headers) + + return Response( + status=status, + headers=headers, + content=HTTP2ConnectionByteStream(self, request, stream_id=stream_id), + extensions={ + "http_version": b"HTTP/2", + "network_stream": self._network_stream, + "stream_id": stream_id, + }, + ) + except BaseException as exc: # noqa: PIE786 + with AsyncShieldCancellation(): + kwargs = {"stream_id": stream_id} + async with Trace("response_closed", logger, request, kwargs): + await self._response_closed(stream_id=stream_id) + + if isinstance(exc, h2.exceptions.ProtocolError): + # One case where h2 can raise a protocol error is when a + # closed frame has been seen by the state machine. + # + # This happens when one stream is reading, and encounters + # a GOAWAY event. Other flows of control may then raise + # a protocol error at any point they interact with the 'h2_state'. + # + # In this case we'll have stored the event, and should raise + # it as a RemoteProtocolError. + if self._connection_terminated: # pragma: nocover + raise RemoteProtocolError(self._connection_terminated) + # If h2 raises a protocol error in some other state then we + # must somehow have made a protocol violation. + raise LocalProtocolError(exc) # pragma: nocover + + raise exc + + async def _send_connection_init(self, request: Request) -> None: + """ + The HTTP/2 connection requires some initial setup before we can start + using individual request/response streams on it. + """ + # Need to set these manually here instead of manipulating via + # __setitem__() otherwise the H2Connection will emit SettingsUpdate + # frames in addition to sending the undesired defaults. + self._h2_state.local_settings = h2.settings.Settings( + client=True, + initial_values={ + # Disable PUSH_PROMISE frames from the server since we don't do anything + # with them for now. Maybe when we support caching? + h2.settings.SettingCodes.ENABLE_PUSH: 0, + # These two are taken from h2 for safe defaults + h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: 100, + h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE: 65536, + }, + ) + + # Some websites (*cough* Yahoo *cough*) balk at this setting being + # present in the initial handshake since it's not defined in the original + # RFC despite the RFC mandating ignoring settings you don't know about. + del self._h2_state.local_settings[ + h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL + ] + + self._h2_state.initiate_connection() + self._h2_state.increment_flow_control_window(2**24) + await self._write_outgoing_data(request) + + # Sending the request... + + async def _send_request_headers(self, request: Request, stream_id: int) -> None: + """ + Send the request headers to a given stream ID. + """ + end_stream = not has_body_headers(request) + + # In HTTP/2 the ':authority' pseudo-header is used instead of 'Host'. + # In order to gracefully handle HTTP/1.1 and HTTP/2 we always require + # HTTP/1.1 style headers, and map them appropriately if we end up on + # an HTTP/2 connection. + authority = [v for k, v in request.headers if k.lower() == b"host"][0] + + headers = [ + (b":method", request.method), + (b":authority", authority), + (b":scheme", request.url.scheme), + (b":path", request.url.target), + ] + [ + (k.lower(), v) + for k, v in request.headers + if k.lower() + not in ( + b"host", + b"transfer-encoding", + ) + ] + + self._h2_state.send_headers(stream_id, headers, end_stream=end_stream) + self._h2_state.increment_flow_control_window(2**24, stream_id=stream_id) + await self._write_outgoing_data(request) + + async def _send_request_body(self, request: Request, stream_id: int) -> None: + """ + Iterate over the request body sending it to a given stream ID. + """ + if not has_body_headers(request): + return + + assert isinstance(request.stream, typing.AsyncIterable) + async for data in request.stream: + await self._send_stream_data(request, stream_id, data) + await self._send_end_stream(request, stream_id) + + async def _send_stream_data( + self, request: Request, stream_id: int, data: bytes + ) -> None: + """ + Send a single chunk of data in one or more data frames. + """ + while data: + max_flow = await self._wait_for_outgoing_flow(request, stream_id) + chunk_size = min(len(data), max_flow) + chunk, data = data[:chunk_size], data[chunk_size:] + self._h2_state.send_data(stream_id, chunk) + await self._write_outgoing_data(request) + + async def _send_end_stream(self, request: Request, stream_id: int) -> None: + """ + Send an empty data frame on on a given stream ID with the END_STREAM flag set. + """ + self._h2_state.end_stream(stream_id) + await self._write_outgoing_data(request) + + # Receiving the response... + + async def _receive_response( + self, request: Request, stream_id: int + ) -> tuple[int, list[tuple[bytes, bytes]]]: + """ + Return the response status code and headers for a given stream ID. + """ + while True: + event = await self._receive_stream_event(request, stream_id) + if isinstance(event, h2.events.ResponseReceived): + break + + status_code = 200 + headers = [] + assert event.headers is not None + for k, v in event.headers: + if k == b":status": + status_code = int(v.decode("ascii", errors="ignore")) + elif not k.startswith(b":"): + headers.append((k, v)) + + return (status_code, headers) + + async def _receive_response_body( + self, request: Request, stream_id: int + ) -> typing.AsyncIterator[bytes]: + """ + Iterator that returns the bytes of the response body for a given stream ID. + """ + while True: + event = await self._receive_stream_event(request, stream_id) + if isinstance(event, h2.events.DataReceived): + assert event.flow_controlled_length is not None + assert event.data is not None + amount = event.flow_controlled_length + self._h2_state.acknowledge_received_data(amount, stream_id) + await self._write_outgoing_data(request) + yield event.data + elif isinstance(event, h2.events.StreamEnded): + break + + async def _receive_stream_event( + self, request: Request, stream_id: int + ) -> h2.events.ResponseReceived | h2.events.DataReceived | h2.events.StreamEnded: + """ + Return the next available event for a given stream ID. + + Will read more data from the network if required. + """ + while not self._events.get(stream_id): + await self._receive_events(request, stream_id) + event = self._events[stream_id].pop(0) + if isinstance(event, h2.events.StreamReset): + raise RemoteProtocolError(event) + return event + + async def _receive_events( + self, request: Request, stream_id: int | None = None + ) -> None: + """ + Read some data from the network until we see one or more events + for a given stream ID. + """ + async with self._read_lock: + if self._connection_terminated is not None: + last_stream_id = self._connection_terminated.last_stream_id + if stream_id and last_stream_id and stream_id > last_stream_id: + self._request_count -= 1 + raise ConnectionNotAvailable() + raise RemoteProtocolError(self._connection_terminated) + + # This conditional is a bit icky. We don't want to block reading if we've + # actually got an event to return for a given stream. We need to do that + # check *within* the atomic read lock. Though it also need to be optional, + # because when we call it from `_wait_for_outgoing_flow` we *do* want to + # block until we've available flow control, event when we have events + # pending for the stream ID we're attempting to send on. + if stream_id is None or not self._events.get(stream_id): + events = await self._read_incoming_data(request) + for event in events: + if isinstance(event, h2.events.RemoteSettingsChanged): + async with Trace( + "receive_remote_settings", logger, request + ) as trace: + await self._receive_remote_settings_change(event) + trace.return_value = event + + elif isinstance( + event, + ( + h2.events.ResponseReceived, + h2.events.DataReceived, + h2.events.StreamEnded, + h2.events.StreamReset, + ), + ): + if event.stream_id in self._events: + self._events[event.stream_id].append(event) + + elif isinstance(event, h2.events.ConnectionTerminated): + self._connection_terminated = event + + await self._write_outgoing_data(request) + + async def _receive_remote_settings_change( + self, event: h2.events.RemoteSettingsChanged + ) -> None: + max_concurrent_streams = event.changed_settings.get( + h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS + ) + if max_concurrent_streams: + new_max_streams = min( + max_concurrent_streams.new_value, + self._h2_state.local_settings.max_concurrent_streams, + ) + if new_max_streams and new_max_streams != self._max_streams: + while new_max_streams > self._max_streams: + await self._max_streams_semaphore.release() + self._max_streams += 1 + while new_max_streams < self._max_streams: + await self._max_streams_semaphore.acquire() + self._max_streams -= 1 + + async def _response_closed(self, stream_id: int) -> None: + await self._max_streams_semaphore.release() + del self._events[stream_id] + async with self._state_lock: + if self._connection_terminated and not self._events: + await self.aclose() + + elif self._state == HTTPConnectionState.ACTIVE and not self._events: + self._state = HTTPConnectionState.IDLE + if self._keepalive_expiry is not None: + now = time.monotonic() + self._expire_at = now + self._keepalive_expiry + if self._used_all_stream_ids: # pragma: nocover + await self.aclose() + + async def aclose(self) -> None: + # Note that this method unilaterally closes the connection, and does + # not have any kind of locking in place around it. + self._h2_state.close_connection() + self._state = HTTPConnectionState.CLOSED + await self._network_stream.aclose() + + # Wrappers around network read/write operations... + + async def _read_incoming_data(self, request: Request) -> list[h2.events.Event]: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("read", None) + + if self._read_exception is not None: + raise self._read_exception # pragma: nocover + + try: + data = await self._network_stream.read(self.READ_NUM_BYTES, timeout) + if data == b"": + raise RemoteProtocolError("Server disconnected") + except Exception as exc: + # If we get a network error we should: + # + # 1. Save the exception and just raise it immediately on any future reads. + # (For example, this means that a single read timeout or disconnect will + # immediately close all pending streams. Without requiring multiple + # sequential timeouts.) + # 2. Mark the connection as errored, so that we don't accept any other + # incoming requests. + self._read_exception = exc + self._connection_error = True + raise exc + + events: list[h2.events.Event] = self._h2_state.receive_data(data) + + return events + + async def _write_outgoing_data(self, request: Request) -> None: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("write", None) + + async with self._write_lock: + data_to_send = self._h2_state.data_to_send() + + if self._write_exception is not None: + raise self._write_exception # pragma: nocover + + try: + await self._network_stream.write(data_to_send, timeout) + except Exception as exc: # pragma: nocover + # If we get a network error we should: + # + # 1. Save the exception and just raise it immediately on any future write. + # (For example, this means that a single write timeout or disconnect will + # immediately close all pending streams. Without requiring multiple + # sequential timeouts.) + # 2. Mark the connection as errored, so that we don't accept any other + # incoming requests. + self._write_exception = exc + self._connection_error = True + raise exc + + # Flow control... + + async def _wait_for_outgoing_flow(self, request: Request, stream_id: int) -> int: + """ + Returns the maximum allowable outgoing flow for a given stream. + + If the allowable flow is zero, then waits on the network until + WindowUpdated frames have increased the flow rate. + https://tools.ietf.org/html/rfc7540#section-6.9 + """ + local_flow: int = self._h2_state.local_flow_control_window(stream_id) + max_frame_size: int = self._h2_state.max_outbound_frame_size + flow = min(local_flow, max_frame_size) + while flow == 0: + await self._receive_events(request) + local_flow = self._h2_state.local_flow_control_window(stream_id) + max_frame_size = self._h2_state.max_outbound_frame_size + flow = min(local_flow, max_frame_size) + return flow + + # Interface for connection pooling... + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._origin + + def is_available(self) -> bool: + return ( + self._state != HTTPConnectionState.CLOSED + and not self._connection_error + and not self._used_all_stream_ids + and not ( + self._h2_state.state_machine.state + == h2.connection.ConnectionState.CLOSED + ) + ) + + def has_expired(self) -> bool: + now = time.monotonic() + return self._expire_at is not None and now > self._expire_at + + def is_idle(self) -> bool: + return self._state == HTTPConnectionState.IDLE + + def is_closed(self) -> bool: + return self._state == HTTPConnectionState.CLOSED + + def info(self) -> str: + origin = str(self._origin) + return ( + f"{origin!r}, HTTP/2, {self._state.name}, " + f"Request Count: {self._request_count}" + ) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + origin = str(self._origin) + return ( + f"<{class_name} [{origin!r}, {self._state.name}, " + f"Request Count: {self._request_count}]>" + ) + + # These context managers are not used in the standard flow, but are + # useful for testing or working with connection instances directly. + + async def __aenter__(self) -> AsyncHTTP2Connection: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: types.TracebackType | None = None, + ) -> None: + await self.aclose() + + +class HTTP2ConnectionByteStream: + def __init__( + self, connection: AsyncHTTP2Connection, request: Request, stream_id: int + ) -> None: + self._connection = connection + self._request = request + self._stream_id = stream_id + self._closed = False + + async def __aiter__(self) -> typing.AsyncIterator[bytes]: + kwargs = {"request": self._request, "stream_id": self._stream_id} + try: + async with Trace("receive_response_body", logger, self._request, kwargs): + async for chunk in self._connection._receive_response_body( + request=self._request, stream_id=self._stream_id + ): + yield chunk + except BaseException as exc: + # If we get an exception while streaming the response, + # we want to close the response (and possibly the connection) + # before raising that exception. + with AsyncShieldCancellation(): + await self.aclose() + raise exc + + async def aclose(self) -> None: + if not self._closed: + self._closed = True + kwargs = {"stream_id": self._stream_id} + async with Trace("response_closed", logger, self._request, kwargs): + await self._connection._response_closed(stream_id=self._stream_id) diff --git a/source/target-vendors/cp312/httpcore/_async/http_proxy.py b/source/target-vendors/cp312/httpcore/_async/http_proxy.py new file mode 100644 index 0000000..cc9d920 --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_async/http_proxy.py @@ -0,0 +1,367 @@ +from __future__ import annotations + +import base64 +import logging +import ssl +import typing + +from .._backends.base import SOCKET_OPTION, AsyncNetworkBackend +from .._exceptions import ProxyError +from .._models import ( + URL, + Origin, + Request, + Response, + enforce_bytes, + enforce_headers, + enforce_url, +) +from .._ssl import default_ssl_context +from .._synchronization import AsyncLock +from .._trace import Trace +from .connection import AsyncHTTPConnection +from .connection_pool import AsyncConnectionPool +from .http11 import AsyncHTTP11Connection +from .interfaces import AsyncConnectionInterface + +ByteOrStr = typing.Union[bytes, str] +HeadersAsSequence = typing.Sequence[typing.Tuple[ByteOrStr, ByteOrStr]] +HeadersAsMapping = typing.Mapping[ByteOrStr, ByteOrStr] + + +logger = logging.getLogger("httpcore.proxy") + + +def merge_headers( + default_headers: typing.Sequence[tuple[bytes, bytes]] | None = None, + override_headers: typing.Sequence[tuple[bytes, bytes]] | None = None, +) -> list[tuple[bytes, bytes]]: + """ + Append default_headers and override_headers, de-duplicating if a key exists + in both cases. + """ + default_headers = [] if default_headers is None else list(default_headers) + override_headers = [] if override_headers is None else list(override_headers) + has_override = set(key.lower() for key, value in override_headers) + default_headers = [ + (key, value) + for key, value in default_headers + if key.lower() not in has_override + ] + return default_headers + override_headers + + +class AsyncHTTPProxy(AsyncConnectionPool): # pragma: nocover + """ + A connection pool that sends requests via an HTTP proxy. + """ + + def __init__( + self, + proxy_url: URL | bytes | str, + proxy_auth: tuple[bytes | str, bytes | str] | None = None, + proxy_headers: HeadersAsMapping | HeadersAsSequence | None = None, + ssl_context: ssl.SSLContext | None = None, + proxy_ssl_context: ssl.SSLContext | None = None, + max_connections: int | None = 10, + max_keepalive_connections: int | None = None, + keepalive_expiry: float | None = None, + http1: bool = True, + http2: bool = False, + retries: int = 0, + local_address: str | None = None, + uds: str | None = None, + network_backend: AsyncNetworkBackend | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> None: + """ + A connection pool for making HTTP requests. + + Parameters: + proxy_url: The URL to use when connecting to the proxy server. + For example `"http://127.0.0.1:8080/"`. + proxy_auth: Any proxy authentication as a two-tuple of + (username, password). May be either bytes or ascii-only str. + proxy_headers: Any HTTP headers to use for the proxy requests. + For example `{"Proxy-Authorization": "Basic :"}`. + ssl_context: An SSL context to use for verifying connections. + If not specified, the default `httpcore.default_ssl_context()` + will be used. + proxy_ssl_context: The same as `ssl_context`, but for a proxy server rather than a remote origin. + max_connections: The maximum number of concurrent HTTP connections that + the pool should allow. Any attempt to send a request on a pool that + would exceed this amount will block until a connection is available. + max_keepalive_connections: The maximum number of idle HTTP connections + that will be maintained in the pool. + keepalive_expiry: The duration in seconds that an idle HTTP connection + may be maintained for before being expired from the pool. + http1: A boolean indicating if HTTP/1.1 requests should be supported + by the connection pool. Defaults to True. + http2: A boolean indicating if HTTP/2 requests should be supported by + the connection pool. Defaults to False. + retries: The maximum number of retries when trying to establish + a connection. + local_address: Local address to connect from. Can also be used to + connect using a particular address family. Using + `local_address="0.0.0.0"` will connect using an `AF_INET` address + (IPv4), while using `local_address="::"` will connect using an + `AF_INET6` address (IPv6). + uds: Path to a Unix Domain Socket to use instead of TCP sockets. + network_backend: A backend instance to use for handling network I/O. + """ + super().__init__( + ssl_context=ssl_context, + max_connections=max_connections, + max_keepalive_connections=max_keepalive_connections, + keepalive_expiry=keepalive_expiry, + http1=http1, + http2=http2, + network_backend=network_backend, + retries=retries, + local_address=local_address, + uds=uds, + socket_options=socket_options, + ) + + self._proxy_url = enforce_url(proxy_url, name="proxy_url") + if ( + self._proxy_url.scheme == b"http" and proxy_ssl_context is not None + ): # pragma: no cover + raise RuntimeError( + "The `proxy_ssl_context` argument is not allowed for the http scheme" + ) + + self._ssl_context = ssl_context + self._proxy_ssl_context = proxy_ssl_context + self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers") + if proxy_auth is not None: + username = enforce_bytes(proxy_auth[0], name="proxy_auth") + password = enforce_bytes(proxy_auth[1], name="proxy_auth") + userpass = username + b":" + password + authorization = b"Basic " + base64.b64encode(userpass) + self._proxy_headers = [ + (b"Proxy-Authorization", authorization) + ] + self._proxy_headers + + def create_connection(self, origin: Origin) -> AsyncConnectionInterface: + if origin.scheme == b"http": + return AsyncForwardHTTPConnection( + proxy_origin=self._proxy_url.origin, + proxy_headers=self._proxy_headers, + remote_origin=origin, + keepalive_expiry=self._keepalive_expiry, + network_backend=self._network_backend, + proxy_ssl_context=self._proxy_ssl_context, + ) + return AsyncTunnelHTTPConnection( + proxy_origin=self._proxy_url.origin, + proxy_headers=self._proxy_headers, + remote_origin=origin, + ssl_context=self._ssl_context, + proxy_ssl_context=self._proxy_ssl_context, + keepalive_expiry=self._keepalive_expiry, + http1=self._http1, + http2=self._http2, + network_backend=self._network_backend, + ) + + +class AsyncForwardHTTPConnection(AsyncConnectionInterface): + def __init__( + self, + proxy_origin: Origin, + remote_origin: Origin, + proxy_headers: HeadersAsMapping | HeadersAsSequence | None = None, + keepalive_expiry: float | None = None, + network_backend: AsyncNetworkBackend | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + proxy_ssl_context: ssl.SSLContext | None = None, + ) -> None: + self._connection = AsyncHTTPConnection( + origin=proxy_origin, + keepalive_expiry=keepalive_expiry, + network_backend=network_backend, + socket_options=socket_options, + ssl_context=proxy_ssl_context, + ) + self._proxy_origin = proxy_origin + self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers") + self._remote_origin = remote_origin + + async def handle_async_request(self, request: Request) -> Response: + headers = merge_headers(self._proxy_headers, request.headers) + url = URL( + scheme=self._proxy_origin.scheme, + host=self._proxy_origin.host, + port=self._proxy_origin.port, + target=bytes(request.url), + ) + proxy_request = Request( + method=request.method, + url=url, + headers=headers, + content=request.stream, + extensions=request.extensions, + ) + return await self._connection.handle_async_request(proxy_request) + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._remote_origin + + async def aclose(self) -> None: + await self._connection.aclose() + + def info(self) -> str: + return self._connection.info() + + def is_available(self) -> bool: + return self._connection.is_available() + + def has_expired(self) -> bool: + return self._connection.has_expired() + + def is_idle(self) -> bool: + return self._connection.is_idle() + + def is_closed(self) -> bool: + return self._connection.is_closed() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{self.info()}]>" + + +class AsyncTunnelHTTPConnection(AsyncConnectionInterface): + def __init__( + self, + proxy_origin: Origin, + remote_origin: Origin, + ssl_context: ssl.SSLContext | None = None, + proxy_ssl_context: ssl.SSLContext | None = None, + proxy_headers: typing.Sequence[tuple[bytes, bytes]] | None = None, + keepalive_expiry: float | None = None, + http1: bool = True, + http2: bool = False, + network_backend: AsyncNetworkBackend | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> None: + self._connection: AsyncConnectionInterface = AsyncHTTPConnection( + origin=proxy_origin, + keepalive_expiry=keepalive_expiry, + network_backend=network_backend, + socket_options=socket_options, + ssl_context=proxy_ssl_context, + ) + self._proxy_origin = proxy_origin + self._remote_origin = remote_origin + self._ssl_context = ssl_context + self._proxy_ssl_context = proxy_ssl_context + self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers") + self._keepalive_expiry = keepalive_expiry + self._http1 = http1 + self._http2 = http2 + self._connect_lock = AsyncLock() + self._connected = False + + async def handle_async_request(self, request: Request) -> Response: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("connect", None) + + async with self._connect_lock: + if not self._connected: + target = b"%b:%d" % (self._remote_origin.host, self._remote_origin.port) + + connect_url = URL( + scheme=self._proxy_origin.scheme, + host=self._proxy_origin.host, + port=self._proxy_origin.port, + target=target, + ) + connect_headers = merge_headers( + [(b"Host", target), (b"Accept", b"*/*")], self._proxy_headers + ) + connect_request = Request( + method=b"CONNECT", + url=connect_url, + headers=connect_headers, + extensions=request.extensions, + ) + connect_response = await self._connection.handle_async_request( + connect_request + ) + + if connect_response.status < 200 or connect_response.status > 299: + reason_bytes = connect_response.extensions.get("reason_phrase", b"") + reason_str = reason_bytes.decode("ascii", errors="ignore") + msg = "%d %s" % (connect_response.status, reason_str) + await self._connection.aclose() + raise ProxyError(msg) + + stream = connect_response.extensions["network_stream"] + + # Upgrade the stream to SSL + ssl_context = ( + default_ssl_context() + if self._ssl_context is None + else self._ssl_context + ) + alpn_protocols = ["http/1.1", "h2"] if self._http2 else ["http/1.1"] + ssl_context.set_alpn_protocols(alpn_protocols) + + kwargs = { + "ssl_context": ssl_context, + "server_hostname": self._remote_origin.host.decode("ascii"), + "timeout": timeout, + } + async with Trace("start_tls", logger, request, kwargs) as trace: + stream = await stream.start_tls(**kwargs) + trace.return_value = stream + + # Determine if we should be using HTTP/1.1 or HTTP/2 + ssl_object = stream.get_extra_info("ssl_object") + http2_negotiated = ( + ssl_object is not None + and ssl_object.selected_alpn_protocol() == "h2" + ) + + # Create the HTTP/1.1 or HTTP/2 connection + if http2_negotiated or (self._http2 and not self._http1): + from .http2 import AsyncHTTP2Connection + + self._connection = AsyncHTTP2Connection( + origin=self._remote_origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + else: + self._connection = AsyncHTTP11Connection( + origin=self._remote_origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + + self._connected = True + return await self._connection.handle_async_request(request) + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._remote_origin + + async def aclose(self) -> None: + await self._connection.aclose() + + def info(self) -> str: + return self._connection.info() + + def is_available(self) -> bool: + return self._connection.is_available() + + def has_expired(self) -> bool: + return self._connection.has_expired() + + def is_idle(self) -> bool: + return self._connection.is_idle() + + def is_closed(self) -> bool: + return self._connection.is_closed() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{self.info()}]>" diff --git a/source/target-vendors/cp312/httpcore/_async/interfaces.py b/source/target-vendors/cp312/httpcore/_async/interfaces.py new file mode 100644 index 0000000..361583b --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_async/interfaces.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +import contextlib +import typing + +from .._models import ( + URL, + Extensions, + HeaderTypes, + Origin, + Request, + Response, + enforce_bytes, + enforce_headers, + enforce_url, + include_request_headers, +) + + +class AsyncRequestInterface: + async def request( + self, + method: bytes | str, + url: URL | bytes | str, + *, + headers: HeaderTypes = None, + content: bytes | typing.AsyncIterator[bytes] | None = None, + extensions: Extensions | None = None, + ) -> Response: + # Strict type checking on our parameters. + method = enforce_bytes(method, name="method") + url = enforce_url(url, name="url") + headers = enforce_headers(headers, name="headers") + + # Include Host header, and optionally Content-Length or Transfer-Encoding. + headers = include_request_headers(headers, url=url, content=content) + + request = Request( + method=method, + url=url, + headers=headers, + content=content, + extensions=extensions, + ) + response = await self.handle_async_request(request) + try: + await response.aread() + finally: + await response.aclose() + return response + + @contextlib.asynccontextmanager + async def stream( + self, + method: bytes | str, + url: URL | bytes | str, + *, + headers: HeaderTypes = None, + content: bytes | typing.AsyncIterator[bytes] | None = None, + extensions: Extensions | None = None, + ) -> typing.AsyncIterator[Response]: + # Strict type checking on our parameters. + method = enforce_bytes(method, name="method") + url = enforce_url(url, name="url") + headers = enforce_headers(headers, name="headers") + + # Include Host header, and optionally Content-Length or Transfer-Encoding. + headers = include_request_headers(headers, url=url, content=content) + + request = Request( + method=method, + url=url, + headers=headers, + content=content, + extensions=extensions, + ) + response = await self.handle_async_request(request) + try: + yield response + finally: + await response.aclose() + + async def handle_async_request(self, request: Request) -> Response: + raise NotImplementedError() # pragma: nocover + + +class AsyncConnectionInterface(AsyncRequestInterface): + async def aclose(self) -> None: + raise NotImplementedError() # pragma: nocover + + def info(self) -> str: + raise NotImplementedError() # pragma: nocover + + def can_handle_request(self, origin: Origin) -> bool: + raise NotImplementedError() # pragma: nocover + + def is_available(self) -> bool: + """ + Return `True` if the connection is currently able to accept an + outgoing request. + + An HTTP/1.1 connection will only be available if it is currently idle. + + An HTTP/2 connection will be available so long as the stream ID space is + not yet exhausted, and the connection is not in an error state. + + While the connection is being established we may not yet know if it is going + to result in an HTTP/1.1 or HTTP/2 connection. The connection should be + treated as being available, but might ultimately raise `NewConnectionRequired` + required exceptions if multiple requests are attempted over a connection + that ends up being established as HTTP/1.1. + """ + raise NotImplementedError() # pragma: nocover + + def has_expired(self) -> bool: + """ + Return `True` if the connection is in a state where it should be closed. + + This either means that the connection is idle and it has passed the + expiry time on its keep-alive, or that server has sent an EOF. + """ + raise NotImplementedError() # pragma: nocover + + def is_idle(self) -> bool: + """ + Return `True` if the connection is currently idle. + """ + raise NotImplementedError() # pragma: nocover + + def is_closed(self) -> bool: + """ + Return `True` if the connection has been closed. + + Used when a response is closed to determine if the connection may be + returned to the connection pool or not. + """ + raise NotImplementedError() # pragma: nocover diff --git a/source/target-vendors/cp312/httpcore/_async/socks_proxy.py b/source/target-vendors/cp312/httpcore/_async/socks_proxy.py new file mode 100644 index 0000000..b363f55 --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_async/socks_proxy.py @@ -0,0 +1,341 @@ +from __future__ import annotations + +import logging +import ssl + +import socksio + +from .._backends.auto import AutoBackend +from .._backends.base import AsyncNetworkBackend, AsyncNetworkStream +from .._exceptions import ConnectionNotAvailable, ProxyError +from .._models import URL, Origin, Request, Response, enforce_bytes, enforce_url +from .._ssl import default_ssl_context +from .._synchronization import AsyncLock +from .._trace import Trace +from .connection_pool import AsyncConnectionPool +from .http11 import AsyncHTTP11Connection +from .interfaces import AsyncConnectionInterface + +logger = logging.getLogger("httpcore.socks") + + +AUTH_METHODS = { + b"\x00": "NO AUTHENTICATION REQUIRED", + b"\x01": "GSSAPI", + b"\x02": "USERNAME/PASSWORD", + b"\xff": "NO ACCEPTABLE METHODS", +} + +REPLY_CODES = { + b"\x00": "Succeeded", + b"\x01": "General SOCKS server failure", + b"\x02": "Connection not allowed by ruleset", + b"\x03": "Network unreachable", + b"\x04": "Host unreachable", + b"\x05": "Connection refused", + b"\x06": "TTL expired", + b"\x07": "Command not supported", + b"\x08": "Address type not supported", +} + + +async def _init_socks5_connection( + stream: AsyncNetworkStream, + *, + host: bytes, + port: int, + auth: tuple[bytes, bytes] | None = None, +) -> None: + conn = socksio.socks5.SOCKS5Connection() + + # Auth method request + auth_method = ( + socksio.socks5.SOCKS5AuthMethod.NO_AUTH_REQUIRED + if auth is None + else socksio.socks5.SOCKS5AuthMethod.USERNAME_PASSWORD + ) + conn.send(socksio.socks5.SOCKS5AuthMethodsRequest([auth_method])) + outgoing_bytes = conn.data_to_send() + await stream.write(outgoing_bytes) + + # Auth method response + incoming_bytes = await stream.read(max_bytes=4096) + response = conn.receive_data(incoming_bytes) + assert isinstance(response, socksio.socks5.SOCKS5AuthReply) + if response.method != auth_method: + requested = AUTH_METHODS.get(auth_method, "UNKNOWN") + responded = AUTH_METHODS.get(response.method, "UNKNOWN") + raise ProxyError( + f"Requested {requested} from proxy server, but got {responded}." + ) + + if response.method == socksio.socks5.SOCKS5AuthMethod.USERNAME_PASSWORD: + # Username/password request + assert auth is not None + username, password = auth + conn.send(socksio.socks5.SOCKS5UsernamePasswordRequest(username, password)) + outgoing_bytes = conn.data_to_send() + await stream.write(outgoing_bytes) + + # Username/password response + incoming_bytes = await stream.read(max_bytes=4096) + response = conn.receive_data(incoming_bytes) + assert isinstance(response, socksio.socks5.SOCKS5UsernamePasswordReply) + if not response.success: + raise ProxyError("Invalid username/password") + + # Connect request + conn.send( + socksio.socks5.SOCKS5CommandRequest.from_address( + socksio.socks5.SOCKS5Command.CONNECT, (host, port) + ) + ) + outgoing_bytes = conn.data_to_send() + await stream.write(outgoing_bytes) + + # Connect response + incoming_bytes = await stream.read(max_bytes=4096) + response = conn.receive_data(incoming_bytes) + assert isinstance(response, socksio.socks5.SOCKS5Reply) + if response.reply_code != socksio.socks5.SOCKS5ReplyCode.SUCCEEDED: + reply_code = REPLY_CODES.get(response.reply_code, "UNKOWN") + raise ProxyError(f"Proxy Server could not connect: {reply_code}.") + + +class AsyncSOCKSProxy(AsyncConnectionPool): # pragma: nocover + """ + A connection pool that sends requests via an HTTP proxy. + """ + + def __init__( + self, + proxy_url: URL | bytes | str, + proxy_auth: tuple[bytes | str, bytes | str] | None = None, + ssl_context: ssl.SSLContext | None = None, + max_connections: int | None = 10, + max_keepalive_connections: int | None = None, + keepalive_expiry: float | None = None, + http1: bool = True, + http2: bool = False, + retries: int = 0, + network_backend: AsyncNetworkBackend | None = None, + ) -> None: + """ + A connection pool for making HTTP requests. + + Parameters: + proxy_url: The URL to use when connecting to the proxy server. + For example `"http://127.0.0.1:8080/"`. + ssl_context: An SSL context to use for verifying connections. + If not specified, the default `httpcore.default_ssl_context()` + will be used. + max_connections: The maximum number of concurrent HTTP connections that + the pool should allow. Any attempt to send a request on a pool that + would exceed this amount will block until a connection is available. + max_keepalive_connections: The maximum number of idle HTTP connections + that will be maintained in the pool. + keepalive_expiry: The duration in seconds that an idle HTTP connection + may be maintained for before being expired from the pool. + http1: A boolean indicating if HTTP/1.1 requests should be supported + by the connection pool. Defaults to True. + http2: A boolean indicating if HTTP/2 requests should be supported by + the connection pool. Defaults to False. + retries: The maximum number of retries when trying to establish + a connection. + local_address: Local address to connect from. Can also be used to + connect using a particular address family. Using + `local_address="0.0.0.0"` will connect using an `AF_INET` address + (IPv4), while using `local_address="::"` will connect using an + `AF_INET6` address (IPv6). + uds: Path to a Unix Domain Socket to use instead of TCP sockets. + network_backend: A backend instance to use for handling network I/O. + """ + super().__init__( + ssl_context=ssl_context, + max_connections=max_connections, + max_keepalive_connections=max_keepalive_connections, + keepalive_expiry=keepalive_expiry, + http1=http1, + http2=http2, + network_backend=network_backend, + retries=retries, + ) + self._ssl_context = ssl_context + self._proxy_url = enforce_url(proxy_url, name="proxy_url") + if proxy_auth is not None: + username, password = proxy_auth + username_bytes = enforce_bytes(username, name="proxy_auth") + password_bytes = enforce_bytes(password, name="proxy_auth") + self._proxy_auth: tuple[bytes, bytes] | None = ( + username_bytes, + password_bytes, + ) + else: + self._proxy_auth = None + + def create_connection(self, origin: Origin) -> AsyncConnectionInterface: + return AsyncSocks5Connection( + proxy_origin=self._proxy_url.origin, + remote_origin=origin, + proxy_auth=self._proxy_auth, + ssl_context=self._ssl_context, + keepalive_expiry=self._keepalive_expiry, + http1=self._http1, + http2=self._http2, + network_backend=self._network_backend, + ) + + +class AsyncSocks5Connection(AsyncConnectionInterface): + def __init__( + self, + proxy_origin: Origin, + remote_origin: Origin, + proxy_auth: tuple[bytes, bytes] | None = None, + ssl_context: ssl.SSLContext | None = None, + keepalive_expiry: float | None = None, + http1: bool = True, + http2: bool = False, + network_backend: AsyncNetworkBackend | None = None, + ) -> None: + self._proxy_origin = proxy_origin + self._remote_origin = remote_origin + self._proxy_auth = proxy_auth + self._ssl_context = ssl_context + self._keepalive_expiry = keepalive_expiry + self._http1 = http1 + self._http2 = http2 + + self._network_backend: AsyncNetworkBackend = ( + AutoBackend() if network_backend is None else network_backend + ) + self._connect_lock = AsyncLock() + self._connection: AsyncConnectionInterface | None = None + self._connect_failed = False + + async def handle_async_request(self, request: Request) -> Response: + timeouts = request.extensions.get("timeout", {}) + sni_hostname = request.extensions.get("sni_hostname", None) + timeout = timeouts.get("connect", None) + + async with self._connect_lock: + if self._connection is None: + try: + # Connect to the proxy + kwargs = { + "host": self._proxy_origin.host.decode("ascii"), + "port": self._proxy_origin.port, + "timeout": timeout, + } + async with Trace("connect_tcp", logger, request, kwargs) as trace: + stream = await self._network_backend.connect_tcp(**kwargs) + trace.return_value = stream + + # Connect to the remote host using socks5 + kwargs = { + "stream": stream, + "host": self._remote_origin.host.decode("ascii"), + "port": self._remote_origin.port, + "auth": self._proxy_auth, + } + async with Trace( + "setup_socks5_connection", logger, request, kwargs + ) as trace: + await _init_socks5_connection(**kwargs) + trace.return_value = stream + + # Upgrade the stream to SSL + if self._remote_origin.scheme == b"https": + ssl_context = ( + default_ssl_context() + if self._ssl_context is None + else self._ssl_context + ) + alpn_protocols = ( + ["http/1.1", "h2"] if self._http2 else ["http/1.1"] + ) + ssl_context.set_alpn_protocols(alpn_protocols) + + kwargs = { + "ssl_context": ssl_context, + "server_hostname": sni_hostname + or self._remote_origin.host.decode("ascii"), + "timeout": timeout, + } + async with Trace("start_tls", logger, request, kwargs) as trace: + stream = await stream.start_tls(**kwargs) + trace.return_value = stream + + # Determine if we should be using HTTP/1.1 or HTTP/2 + ssl_object = stream.get_extra_info("ssl_object") + http2_negotiated = ( + ssl_object is not None + and ssl_object.selected_alpn_protocol() == "h2" + ) + + # Create the HTTP/1.1 or HTTP/2 connection + if http2_negotiated or ( + self._http2 and not self._http1 + ): # pragma: nocover + from .http2 import AsyncHTTP2Connection + + self._connection = AsyncHTTP2Connection( + origin=self._remote_origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + else: + self._connection = AsyncHTTP11Connection( + origin=self._remote_origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + except Exception as exc: + self._connect_failed = True + raise exc + elif not self._connection.is_available(): # pragma: nocover + raise ConnectionNotAvailable() + + return await self._connection.handle_async_request(request) + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._remote_origin + + async def aclose(self) -> None: + if self._connection is not None: + await self._connection.aclose() + + def is_available(self) -> bool: + if self._connection is None: # pragma: nocover + # If HTTP/2 support is enabled, and the resulting connection could + # end up as HTTP/2 then we should indicate the connection as being + # available to service multiple requests. + return ( + self._http2 + and (self._remote_origin.scheme == b"https" or not self._http1) + and not self._connect_failed + ) + return self._connection.is_available() + + def has_expired(self) -> bool: + if self._connection is None: # pragma: nocover + return self._connect_failed + return self._connection.has_expired() + + def is_idle(self) -> bool: + if self._connection is None: # pragma: nocover + return self._connect_failed + return self._connection.is_idle() + + def is_closed(self) -> bool: + if self._connection is None: # pragma: nocover + return self._connect_failed + return self._connection.is_closed() + + def info(self) -> str: + if self._connection is None: # pragma: nocover + return "CONNECTION FAILED" if self._connect_failed else "CONNECTING" + return self._connection.info() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{self.info()}]>" diff --git a/source/target-vendors/cp312/httpcore/_backends/__init__.py b/source/target-vendors/cp312/httpcore/_backends/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/httpcore/_backends/anyio.py b/source/target-vendors/cp312/httpcore/_backends/anyio.py new file mode 100644 index 0000000..a140095 --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_backends/anyio.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +import ssl +import typing + +import anyio + +from .._exceptions import ( + ConnectError, + ConnectTimeout, + ReadError, + ReadTimeout, + WriteError, + WriteTimeout, + map_exceptions, +) +from .._utils import is_socket_readable +from .base import SOCKET_OPTION, AsyncNetworkBackend, AsyncNetworkStream + + +class AnyIOStream(AsyncNetworkStream): + def __init__(self, stream: anyio.abc.ByteStream) -> None: + self._stream = stream + + async def read(self, max_bytes: int, timeout: float | None = None) -> bytes: + exc_map = { + TimeoutError: ReadTimeout, + anyio.BrokenResourceError: ReadError, + anyio.ClosedResourceError: ReadError, + anyio.EndOfStream: ReadError, + } + with map_exceptions(exc_map): + with anyio.fail_after(timeout): + try: + return await self._stream.receive(max_bytes=max_bytes) + except anyio.EndOfStream: # pragma: nocover + return b"" + + async def write(self, buffer: bytes, timeout: float | None = None) -> None: + if not buffer: + return + + exc_map = { + TimeoutError: WriteTimeout, + anyio.BrokenResourceError: WriteError, + anyio.ClosedResourceError: WriteError, + } + with map_exceptions(exc_map): + with anyio.fail_after(timeout): + await self._stream.send(item=buffer) + + async def aclose(self) -> None: + await self._stream.aclose() + + async def start_tls( + self, + ssl_context: ssl.SSLContext, + server_hostname: str | None = None, + timeout: float | None = None, + ) -> AsyncNetworkStream: + exc_map = { + TimeoutError: ConnectTimeout, + anyio.BrokenResourceError: ConnectError, + anyio.EndOfStream: ConnectError, + ssl.SSLError: ConnectError, + } + with map_exceptions(exc_map): + try: + with anyio.fail_after(timeout): + ssl_stream = await anyio.streams.tls.TLSStream.wrap( + self._stream, + ssl_context=ssl_context, + hostname=server_hostname, + standard_compatible=False, + server_side=False, + ) + except Exception as exc: # pragma: nocover + await self.aclose() + raise exc + return AnyIOStream(ssl_stream) + + def get_extra_info(self, info: str) -> typing.Any: + if info == "ssl_object": + return self._stream.extra(anyio.streams.tls.TLSAttribute.ssl_object, None) + if info == "client_addr": + return self._stream.extra(anyio.abc.SocketAttribute.local_address, None) + if info == "server_addr": + return self._stream.extra(anyio.abc.SocketAttribute.remote_address, None) + if info == "socket": + return self._stream.extra(anyio.abc.SocketAttribute.raw_socket, None) + if info == "is_readable": + sock = self._stream.extra(anyio.abc.SocketAttribute.raw_socket, None) + return is_socket_readable(sock) + return None + + +class AnyIOBackend(AsyncNetworkBackend): + async def connect_tcp( + self, + host: str, + port: int, + timeout: float | None = None, + local_address: str | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> AsyncNetworkStream: # pragma: nocover + if socket_options is None: + socket_options = [] + exc_map = { + TimeoutError: ConnectTimeout, + OSError: ConnectError, + anyio.BrokenResourceError: ConnectError, + } + with map_exceptions(exc_map): + with anyio.fail_after(timeout): + stream: anyio.abc.ByteStream = await anyio.connect_tcp( + remote_host=host, + remote_port=port, + local_host=local_address, + ) + # By default TCP sockets opened in `asyncio` include TCP_NODELAY. + for option in socket_options: + stream._raw_socket.setsockopt(*option) # type: ignore[attr-defined] # pragma: no cover + return AnyIOStream(stream) + + async def connect_unix_socket( + self, + path: str, + timeout: float | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> AsyncNetworkStream: # pragma: nocover + if socket_options is None: + socket_options = [] + exc_map = { + TimeoutError: ConnectTimeout, + OSError: ConnectError, + anyio.BrokenResourceError: ConnectError, + } + with map_exceptions(exc_map): + with anyio.fail_after(timeout): + stream: anyio.abc.ByteStream = await anyio.connect_unix(path) + for option in socket_options: + stream._raw_socket.setsockopt(*option) # type: ignore[attr-defined] # pragma: no cover + return AnyIOStream(stream) + + async def sleep(self, seconds: float) -> None: + await anyio.sleep(seconds) # pragma: nocover diff --git a/source/target-vendors/cp312/httpcore/_backends/auto.py b/source/target-vendors/cp312/httpcore/_backends/auto.py new file mode 100644 index 0000000..49f0e69 --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_backends/auto.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import typing + +from .._synchronization import current_async_library +from .base import SOCKET_OPTION, AsyncNetworkBackend, AsyncNetworkStream + + +class AutoBackend(AsyncNetworkBackend): + async def _init_backend(self) -> None: + if not (hasattr(self, "_backend")): + backend = current_async_library() + if backend == "trio": + from .trio import TrioBackend + + self._backend: AsyncNetworkBackend = TrioBackend() + else: + from .anyio import AnyIOBackend + + self._backend = AnyIOBackend() + + async def connect_tcp( + self, + host: str, + port: int, + timeout: float | None = None, + local_address: str | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> AsyncNetworkStream: + await self._init_backend() + return await self._backend.connect_tcp( + host, + port, + timeout=timeout, + local_address=local_address, + socket_options=socket_options, + ) + + async def connect_unix_socket( + self, + path: str, + timeout: float | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> AsyncNetworkStream: # pragma: nocover + await self._init_backend() + return await self._backend.connect_unix_socket( + path, timeout=timeout, socket_options=socket_options + ) + + async def sleep(self, seconds: float) -> None: # pragma: nocover + await self._init_backend() + return await self._backend.sleep(seconds) diff --git a/source/target-vendors/cp312/httpcore/_backends/base.py b/source/target-vendors/cp312/httpcore/_backends/base.py new file mode 100644 index 0000000..cf55c8b --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_backends/base.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import ssl +import time +import typing + +SOCKET_OPTION = typing.Union[ + typing.Tuple[int, int, int], + typing.Tuple[int, int, typing.Union[bytes, bytearray]], + typing.Tuple[int, int, None, int], +] + + +class NetworkStream: + def read(self, max_bytes: int, timeout: float | None = None) -> bytes: + raise NotImplementedError() # pragma: nocover + + def write(self, buffer: bytes, timeout: float | None = None) -> None: + raise NotImplementedError() # pragma: nocover + + def close(self) -> None: + raise NotImplementedError() # pragma: nocover + + def start_tls( + self, + ssl_context: ssl.SSLContext, + server_hostname: str | None = None, + timeout: float | None = None, + ) -> NetworkStream: + raise NotImplementedError() # pragma: nocover + + def get_extra_info(self, info: str) -> typing.Any: + return None # pragma: nocover + + +class NetworkBackend: + def connect_tcp( + self, + host: str, + port: int, + timeout: float | None = None, + local_address: str | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> NetworkStream: + raise NotImplementedError() # pragma: nocover + + def connect_unix_socket( + self, + path: str, + timeout: float | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> NetworkStream: + raise NotImplementedError() # pragma: nocover + + def sleep(self, seconds: float) -> None: + time.sleep(seconds) # pragma: nocover + + +class AsyncNetworkStream: + async def read(self, max_bytes: int, timeout: float | None = None) -> bytes: + raise NotImplementedError() # pragma: nocover + + async def write(self, buffer: bytes, timeout: float | None = None) -> None: + raise NotImplementedError() # pragma: nocover + + async def aclose(self) -> None: + raise NotImplementedError() # pragma: nocover + + async def start_tls( + self, + ssl_context: ssl.SSLContext, + server_hostname: str | None = None, + timeout: float | None = None, + ) -> AsyncNetworkStream: + raise NotImplementedError() # pragma: nocover + + def get_extra_info(self, info: str) -> typing.Any: + return None # pragma: nocover + + +class AsyncNetworkBackend: + async def connect_tcp( + self, + host: str, + port: int, + timeout: float | None = None, + local_address: str | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> AsyncNetworkStream: + raise NotImplementedError() # pragma: nocover + + async def connect_unix_socket( + self, + path: str, + timeout: float | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> AsyncNetworkStream: + raise NotImplementedError() # pragma: nocover + + async def sleep(self, seconds: float) -> None: + raise NotImplementedError() # pragma: nocover diff --git a/source/target-vendors/cp312/httpcore/_backends/mock.py b/source/target-vendors/cp312/httpcore/_backends/mock.py new file mode 100644 index 0000000..9b6edca --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_backends/mock.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +import ssl +import typing + +from .._exceptions import ReadError +from .base import ( + SOCKET_OPTION, + AsyncNetworkBackend, + AsyncNetworkStream, + NetworkBackend, + NetworkStream, +) + + +class MockSSLObject: + def __init__(self, http2: bool): + self._http2 = http2 + + def selected_alpn_protocol(self) -> str: + return "h2" if self._http2 else "http/1.1" + + +class MockStream(NetworkStream): + def __init__(self, buffer: list[bytes], http2: bool = False) -> None: + self._buffer = buffer + self._http2 = http2 + self._closed = False + + def read(self, max_bytes: int, timeout: float | None = None) -> bytes: + if self._closed: + raise ReadError("Connection closed") + if not self._buffer: + return b"" + return self._buffer.pop(0) + + def write(self, buffer: bytes, timeout: float | None = None) -> None: + pass + + def close(self) -> None: + self._closed = True + + def start_tls( + self, + ssl_context: ssl.SSLContext, + server_hostname: str | None = None, + timeout: float | None = None, + ) -> NetworkStream: + return self + + def get_extra_info(self, info: str) -> typing.Any: + return MockSSLObject(http2=self._http2) if info == "ssl_object" else None + + def __repr__(self) -> str: + return "" + + +class MockBackend(NetworkBackend): + def __init__(self, buffer: list[bytes], http2: bool = False) -> None: + self._buffer = buffer + self._http2 = http2 + + def connect_tcp( + self, + host: str, + port: int, + timeout: float | None = None, + local_address: str | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> NetworkStream: + return MockStream(list(self._buffer), http2=self._http2) + + def connect_unix_socket( + self, + path: str, + timeout: float | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> NetworkStream: + return MockStream(list(self._buffer), http2=self._http2) + + def sleep(self, seconds: float) -> None: + pass + + +class AsyncMockStream(AsyncNetworkStream): + def __init__(self, buffer: list[bytes], http2: bool = False) -> None: + self._buffer = buffer + self._http2 = http2 + self._closed = False + + async def read(self, max_bytes: int, timeout: float | None = None) -> bytes: + if self._closed: + raise ReadError("Connection closed") + if not self._buffer: + return b"" + return self._buffer.pop(0) + + async def write(self, buffer: bytes, timeout: float | None = None) -> None: + pass + + async def aclose(self) -> None: + self._closed = True + + async def start_tls( + self, + ssl_context: ssl.SSLContext, + server_hostname: str | None = None, + timeout: float | None = None, + ) -> AsyncNetworkStream: + return self + + def get_extra_info(self, info: str) -> typing.Any: + return MockSSLObject(http2=self._http2) if info == "ssl_object" else None + + def __repr__(self) -> str: + return "" + + +class AsyncMockBackend(AsyncNetworkBackend): + def __init__(self, buffer: list[bytes], http2: bool = False) -> None: + self._buffer = buffer + self._http2 = http2 + + async def connect_tcp( + self, + host: str, + port: int, + timeout: float | None = None, + local_address: str | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> AsyncNetworkStream: + return AsyncMockStream(list(self._buffer), http2=self._http2) + + async def connect_unix_socket( + self, + path: str, + timeout: float | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> AsyncNetworkStream: + return AsyncMockStream(list(self._buffer), http2=self._http2) + + async def sleep(self, seconds: float) -> None: + pass diff --git a/source/target-vendors/cp312/httpcore/_backends/sync.py b/source/target-vendors/cp312/httpcore/_backends/sync.py new file mode 100644 index 0000000..4018a09 --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_backends/sync.py @@ -0,0 +1,241 @@ +from __future__ import annotations + +import functools +import socket +import ssl +import sys +import typing + +from .._exceptions import ( + ConnectError, + ConnectTimeout, + ExceptionMapping, + ReadError, + ReadTimeout, + WriteError, + WriteTimeout, + map_exceptions, +) +from .._utils import is_socket_readable +from .base import SOCKET_OPTION, NetworkBackend, NetworkStream + + +class TLSinTLSStream(NetworkStream): # pragma: no cover + """ + Because the standard `SSLContext.wrap_socket` method does + not work for `SSLSocket` objects, we need this class + to implement TLS stream using an underlying `SSLObject` + instance in order to support TLS on top of TLS. + """ + + # Defined in RFC 8449 + TLS_RECORD_SIZE = 16384 + + def __init__( + self, + sock: socket.socket, + ssl_context: ssl.SSLContext, + server_hostname: str | None = None, + timeout: float | None = None, + ): + self._sock = sock + self._incoming = ssl.MemoryBIO() + self._outgoing = ssl.MemoryBIO() + + self.ssl_obj = ssl_context.wrap_bio( + incoming=self._incoming, + outgoing=self._outgoing, + server_hostname=server_hostname, + ) + + self._sock.settimeout(timeout) + self._perform_io(self.ssl_obj.do_handshake) + + def _perform_io( + self, + func: typing.Callable[..., typing.Any], + ) -> typing.Any: + ret = None + + while True: + errno = None + try: + ret = func() + except (ssl.SSLWantReadError, ssl.SSLWantWriteError) as e: + errno = e.errno + + self._sock.sendall(self._outgoing.read()) + + if errno == ssl.SSL_ERROR_WANT_READ: + buf = self._sock.recv(self.TLS_RECORD_SIZE) + + if buf: + self._incoming.write(buf) + else: + self._incoming.write_eof() + if errno is None: + return ret + + def read(self, max_bytes: int, timeout: float | None = None) -> bytes: + exc_map: ExceptionMapping = {socket.timeout: ReadTimeout, OSError: ReadError} + with map_exceptions(exc_map): + self._sock.settimeout(timeout) + return typing.cast( + bytes, self._perform_io(functools.partial(self.ssl_obj.read, max_bytes)) + ) + + def write(self, buffer: bytes, timeout: float | None = None) -> None: + exc_map: ExceptionMapping = {socket.timeout: WriteTimeout, OSError: WriteError} + with map_exceptions(exc_map): + self._sock.settimeout(timeout) + while buffer: + nsent = self._perform_io(functools.partial(self.ssl_obj.write, buffer)) + buffer = buffer[nsent:] + + def close(self) -> None: + self._sock.close() + + def start_tls( + self, + ssl_context: ssl.SSLContext, + server_hostname: str | None = None, + timeout: float | None = None, + ) -> NetworkStream: + raise NotImplementedError() + + def get_extra_info(self, info: str) -> typing.Any: + if info == "ssl_object": + return self.ssl_obj + if info == "client_addr": + return self._sock.getsockname() + if info == "server_addr": + return self._sock.getpeername() + if info == "socket": + return self._sock + if info == "is_readable": + return is_socket_readable(self._sock) + return None + + +class SyncStream(NetworkStream): + def __init__(self, sock: socket.socket) -> None: + self._sock = sock + + def read(self, max_bytes: int, timeout: float | None = None) -> bytes: + exc_map: ExceptionMapping = {socket.timeout: ReadTimeout, OSError: ReadError} + with map_exceptions(exc_map): + self._sock.settimeout(timeout) + return self._sock.recv(max_bytes) + + def write(self, buffer: bytes, timeout: float | None = None) -> None: + if not buffer: + return + + exc_map: ExceptionMapping = {socket.timeout: WriteTimeout, OSError: WriteError} + with map_exceptions(exc_map): + while buffer: + self._sock.settimeout(timeout) + n = self._sock.send(buffer) + buffer = buffer[n:] + + def close(self) -> None: + self._sock.close() + + def start_tls( + self, + ssl_context: ssl.SSLContext, + server_hostname: str | None = None, + timeout: float | None = None, + ) -> NetworkStream: + exc_map: ExceptionMapping = { + socket.timeout: ConnectTimeout, + OSError: ConnectError, + } + with map_exceptions(exc_map): + try: + if isinstance(self._sock, ssl.SSLSocket): # pragma: no cover + # If the underlying socket has already been upgraded + # to the TLS layer (i.e. is an instance of SSLSocket), + # we need some additional smarts to support TLS-in-TLS. + return TLSinTLSStream( + self._sock, ssl_context, server_hostname, timeout + ) + else: + self._sock.settimeout(timeout) + sock = ssl_context.wrap_socket( + self._sock, server_hostname=server_hostname + ) + except Exception as exc: # pragma: nocover + self.close() + raise exc + return SyncStream(sock) + + def get_extra_info(self, info: str) -> typing.Any: + if info == "ssl_object" and isinstance(self._sock, ssl.SSLSocket): + return self._sock._sslobj # type: ignore + if info == "client_addr": + return self._sock.getsockname() + if info == "server_addr": + return self._sock.getpeername() + if info == "socket": + return self._sock + if info == "is_readable": + return is_socket_readable(self._sock) + return None + + +class SyncBackend(NetworkBackend): + def connect_tcp( + self, + host: str, + port: int, + timeout: float | None = None, + local_address: str | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> NetworkStream: + # Note that we automatically include `TCP_NODELAY` + # in addition to any other custom socket options. + if socket_options is None: + socket_options = [] # pragma: no cover + address = (host, port) + source_address = None if local_address is None else (local_address, 0) + exc_map: ExceptionMapping = { + socket.timeout: ConnectTimeout, + OSError: ConnectError, + } + + with map_exceptions(exc_map): + sock = socket.create_connection( + address, + timeout, + source_address=source_address, + ) + for option in socket_options: + sock.setsockopt(*option) # pragma: no cover + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + return SyncStream(sock) + + def connect_unix_socket( + self, + path: str, + timeout: float | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> NetworkStream: # pragma: nocover + if sys.platform == "win32": + raise RuntimeError( + "Attempted to connect to a UNIX socket on a Windows system." + ) + if socket_options is None: + socket_options = [] + + exc_map: ExceptionMapping = { + socket.timeout: ConnectTimeout, + OSError: ConnectError, + } + with map_exceptions(exc_map): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + for option in socket_options: + sock.setsockopt(*option) + sock.settimeout(timeout) + sock.connect(path) + return SyncStream(sock) diff --git a/source/target-vendors/cp312/httpcore/_backends/trio.py b/source/target-vendors/cp312/httpcore/_backends/trio.py new file mode 100644 index 0000000..6f53f5f --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_backends/trio.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +import ssl +import typing + +import trio + +from .._exceptions import ( + ConnectError, + ConnectTimeout, + ExceptionMapping, + ReadError, + ReadTimeout, + WriteError, + WriteTimeout, + map_exceptions, +) +from .base import SOCKET_OPTION, AsyncNetworkBackend, AsyncNetworkStream + + +class TrioStream(AsyncNetworkStream): + def __init__(self, stream: trio.abc.Stream) -> None: + self._stream = stream + + async def read(self, max_bytes: int, timeout: float | None = None) -> bytes: + timeout_or_inf = float("inf") if timeout is None else timeout + exc_map: ExceptionMapping = { + trio.TooSlowError: ReadTimeout, + trio.BrokenResourceError: ReadError, + trio.ClosedResourceError: ReadError, + } + with map_exceptions(exc_map): + with trio.fail_after(timeout_or_inf): + data: bytes = await self._stream.receive_some(max_bytes=max_bytes) + return data + + async def write(self, buffer: bytes, timeout: float | None = None) -> None: + if not buffer: + return + + timeout_or_inf = float("inf") if timeout is None else timeout + exc_map: ExceptionMapping = { + trio.TooSlowError: WriteTimeout, + trio.BrokenResourceError: WriteError, + trio.ClosedResourceError: WriteError, + } + with map_exceptions(exc_map): + with trio.fail_after(timeout_or_inf): + await self._stream.send_all(data=buffer) + + async def aclose(self) -> None: + await self._stream.aclose() + + async def start_tls( + self, + ssl_context: ssl.SSLContext, + server_hostname: str | None = None, + timeout: float | None = None, + ) -> AsyncNetworkStream: + timeout_or_inf = float("inf") if timeout is None else timeout + exc_map: ExceptionMapping = { + trio.TooSlowError: ConnectTimeout, + trio.BrokenResourceError: ConnectError, + } + ssl_stream = trio.SSLStream( + self._stream, + ssl_context=ssl_context, + server_hostname=server_hostname, + https_compatible=True, + server_side=False, + ) + with map_exceptions(exc_map): + try: + with trio.fail_after(timeout_or_inf): + await ssl_stream.do_handshake() + except Exception as exc: # pragma: nocover + await self.aclose() + raise exc + return TrioStream(ssl_stream) + + def get_extra_info(self, info: str) -> typing.Any: + if info == "ssl_object" and isinstance(self._stream, trio.SSLStream): + # Type checkers cannot see `_ssl_object` attribute because trio._ssl.SSLStream uses __getattr__/__setattr__. + # Tracked at https://github.com/python-trio/trio/issues/542 + return self._stream._ssl_object # type: ignore[attr-defined] + if info == "client_addr": + return self._get_socket_stream().socket.getsockname() + if info == "server_addr": + return self._get_socket_stream().socket.getpeername() + if info == "socket": + stream = self._stream + while isinstance(stream, trio.SSLStream): + stream = stream.transport_stream + assert isinstance(stream, trio.SocketStream) + return stream.socket + if info == "is_readable": + socket = self.get_extra_info("socket") + return socket.is_readable() + return None + + def _get_socket_stream(self) -> trio.SocketStream: + stream = self._stream + while isinstance(stream, trio.SSLStream): + stream = stream.transport_stream + assert isinstance(stream, trio.SocketStream) + return stream + + +class TrioBackend(AsyncNetworkBackend): + async def connect_tcp( + self, + host: str, + port: int, + timeout: float | None = None, + local_address: str | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> AsyncNetworkStream: + # By default for TCP sockets, trio enables TCP_NODELAY. + # https://trio.readthedocs.io/en/stable/reference-io.html#trio.SocketStream + if socket_options is None: + socket_options = [] # pragma: no cover + timeout_or_inf = float("inf") if timeout is None else timeout + exc_map: ExceptionMapping = { + trio.TooSlowError: ConnectTimeout, + trio.BrokenResourceError: ConnectError, + OSError: ConnectError, + } + with map_exceptions(exc_map): + with trio.fail_after(timeout_or_inf): + stream: trio.abc.Stream = await trio.open_tcp_stream( + host=host, port=port, local_address=local_address + ) + for option in socket_options: + stream.setsockopt(*option) # type: ignore[attr-defined] # pragma: no cover + return TrioStream(stream) + + async def connect_unix_socket( + self, + path: str, + timeout: float | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> AsyncNetworkStream: # pragma: nocover + if socket_options is None: + socket_options = [] + timeout_or_inf = float("inf") if timeout is None else timeout + exc_map: ExceptionMapping = { + trio.TooSlowError: ConnectTimeout, + trio.BrokenResourceError: ConnectError, + OSError: ConnectError, + } + with map_exceptions(exc_map): + with trio.fail_after(timeout_or_inf): + stream: trio.abc.Stream = await trio.open_unix_socket(path) + for option in socket_options: + stream.setsockopt(*option) # type: ignore[attr-defined] # pragma: no cover + return TrioStream(stream) + + async def sleep(self, seconds: float) -> None: + await trio.sleep(seconds) # pragma: nocover diff --git a/source/target-vendors/cp312/httpcore/_exceptions.py b/source/target-vendors/cp312/httpcore/_exceptions.py new file mode 100644 index 0000000..bc28d44 --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_exceptions.py @@ -0,0 +1,81 @@ +import contextlib +import typing + +ExceptionMapping = typing.Mapping[typing.Type[Exception], typing.Type[Exception]] + + +@contextlib.contextmanager +def map_exceptions(map: ExceptionMapping) -> typing.Iterator[None]: + try: + yield + except Exception as exc: # noqa: PIE786 + for from_exc, to_exc in map.items(): + if isinstance(exc, from_exc): + raise to_exc(exc) from exc + raise # pragma: nocover + + +class ConnectionNotAvailable(Exception): + pass + + +class ProxyError(Exception): + pass + + +class UnsupportedProtocol(Exception): + pass + + +class ProtocolError(Exception): + pass + + +class RemoteProtocolError(ProtocolError): + pass + + +class LocalProtocolError(ProtocolError): + pass + + +# Timeout errors + + +class TimeoutException(Exception): + pass + + +class PoolTimeout(TimeoutException): + pass + + +class ConnectTimeout(TimeoutException): + pass + + +class ReadTimeout(TimeoutException): + pass + + +class WriteTimeout(TimeoutException): + pass + + +# Network errors + + +class NetworkError(Exception): + pass + + +class ConnectError(NetworkError): + pass + + +class ReadError(NetworkError): + pass + + +class WriteError(NetworkError): + pass diff --git a/source/target-vendors/cp312/httpcore/_models.py b/source/target-vendors/cp312/httpcore/_models.py new file mode 100644 index 0000000..8a65f13 --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_models.py @@ -0,0 +1,516 @@ +from __future__ import annotations + +import base64 +import ssl +import typing +import urllib.parse + +# Functions for typechecking... + + +ByteOrStr = typing.Union[bytes, str] +HeadersAsSequence = typing.Sequence[typing.Tuple[ByteOrStr, ByteOrStr]] +HeadersAsMapping = typing.Mapping[ByteOrStr, ByteOrStr] +HeaderTypes = typing.Union[HeadersAsSequence, HeadersAsMapping, None] + +Extensions = typing.MutableMapping[str, typing.Any] + + +def enforce_bytes(value: bytes | str, *, name: str) -> bytes: + """ + Any arguments that are ultimately represented as bytes can be specified + either as bytes or as strings. + + However we enforce that any string arguments must only contain characters in + the plain ASCII range. chr(0)...chr(127). If you need to use characters + outside that range then be precise, and use a byte-wise argument. + """ + if isinstance(value, str): + try: + return value.encode("ascii") + except UnicodeEncodeError: + raise TypeError(f"{name} strings may not include unicode characters.") + elif isinstance(value, bytes): + return value + + seen_type = type(value).__name__ + raise TypeError(f"{name} must be bytes or str, but got {seen_type}.") + + +def enforce_url(value: URL | bytes | str, *, name: str) -> URL: + """ + Type check for URL parameters. + """ + if isinstance(value, (bytes, str)): + return URL(value) + elif isinstance(value, URL): + return value + + seen_type = type(value).__name__ + raise TypeError(f"{name} must be a URL, bytes, or str, but got {seen_type}.") + + +def enforce_headers( + value: HeadersAsMapping | HeadersAsSequence | None = None, *, name: str +) -> list[tuple[bytes, bytes]]: + """ + Convienence function that ensure all items in request or response headers + are either bytes or strings in the plain ASCII range. + """ + if value is None: + return [] + elif isinstance(value, typing.Mapping): + return [ + ( + enforce_bytes(k, name="header name"), + enforce_bytes(v, name="header value"), + ) + for k, v in value.items() + ] + elif isinstance(value, typing.Sequence): + return [ + ( + enforce_bytes(k, name="header name"), + enforce_bytes(v, name="header value"), + ) + for k, v in value + ] + + seen_type = type(value).__name__ + raise TypeError( + f"{name} must be a mapping or sequence of two-tuples, but got {seen_type}." + ) + + +def enforce_stream( + value: bytes | typing.Iterable[bytes] | typing.AsyncIterable[bytes] | None, + *, + name: str, +) -> typing.Iterable[bytes] | typing.AsyncIterable[bytes]: + if value is None: + return ByteStream(b"") + elif isinstance(value, bytes): + return ByteStream(value) + return value + + +# * https://tools.ietf.org/html/rfc3986#section-3.2.3 +# * https://url.spec.whatwg.org/#url-miscellaneous +# * https://url.spec.whatwg.org/#scheme-state +DEFAULT_PORTS = { + b"ftp": 21, + b"http": 80, + b"https": 443, + b"ws": 80, + b"wss": 443, +} + + +def include_request_headers( + headers: list[tuple[bytes, bytes]], + *, + url: "URL", + content: None | bytes | typing.Iterable[bytes] | typing.AsyncIterable[bytes], +) -> list[tuple[bytes, bytes]]: + headers_set = set(k.lower() for k, v in headers) + + if b"host" not in headers_set: + default_port = DEFAULT_PORTS.get(url.scheme) + if url.port is None or url.port == default_port: + header_value = url.host + else: + header_value = b"%b:%d" % (url.host, url.port) + headers = [(b"Host", header_value)] + headers + + if ( + content is not None + and b"content-length" not in headers_set + and b"transfer-encoding" not in headers_set + ): + if isinstance(content, bytes): + content_length = str(len(content)).encode("ascii") + headers += [(b"Content-Length", content_length)] + else: + headers += [(b"Transfer-Encoding", b"chunked")] # pragma: nocover + + return headers + + +# Interfaces for byte streams... + + +class ByteStream: + """ + A container for non-streaming content, and that supports both sync and async + stream iteration. + """ + + def __init__(self, content: bytes) -> None: + self._content = content + + def __iter__(self) -> typing.Iterator[bytes]: + yield self._content + + async def __aiter__(self) -> typing.AsyncIterator[bytes]: + yield self._content + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{len(self._content)} bytes]>" + + +class Origin: + def __init__(self, scheme: bytes, host: bytes, port: int) -> None: + self.scheme = scheme + self.host = host + self.port = port + + def __eq__(self, other: typing.Any) -> bool: + return ( + isinstance(other, Origin) + and self.scheme == other.scheme + and self.host == other.host + and self.port == other.port + ) + + def __str__(self) -> str: + scheme = self.scheme.decode("ascii") + host = self.host.decode("ascii") + port = str(self.port) + return f"{scheme}://{host}:{port}" + + +class URL: + """ + Represents the URL against which an HTTP request may be made. + + The URL may either be specified as a plain string, for convienence: + + ```python + url = httpcore.URL("https://www.example.com/") + ``` + + Or be constructed with explicitily pre-parsed components: + + ```python + url = httpcore.URL(scheme=b'https', host=b'www.example.com', port=None, target=b'/') + ``` + + Using this second more explicit style allows integrations that are using + `httpcore` to pass through URLs that have already been parsed in order to use + libraries such as `rfc-3986` rather than relying on the stdlib. It also ensures + that URL parsing is treated identically at both the networking level and at any + higher layers of abstraction. + + The four components are important here, as they allow the URL to be precisely + specified in a pre-parsed format. They also allow certain types of request to + be created that could not otherwise be expressed. + + For example, an HTTP request to `http://www.example.com/` forwarded via a proxy + at `http://localhost:8080`... + + ```python + # Constructs an HTTP request with a complete URL as the target: + # GET https://www.example.com/ HTTP/1.1 + url = httpcore.URL( + scheme=b'http', + host=b'localhost', + port=8080, + target=b'https://www.example.com/' + ) + request = httpcore.Request( + method="GET", + url=url + ) + ``` + + Another example is constructing an `OPTIONS *` request... + + ```python + # Constructs an 'OPTIONS *' HTTP request: + # OPTIONS * HTTP/1.1 + url = httpcore.URL(scheme=b'https', host=b'www.example.com', target=b'*') + request = httpcore.Request(method="OPTIONS", url=url) + ``` + + This kind of request is not possible to formulate with a URL string, + because the `/` delimiter is always used to demark the target from the + host/port portion of the URL. + + For convenience, string-like arguments may be specified either as strings or + as bytes. However, once a request is being issue over-the-wire, the URL + components are always ultimately required to be a bytewise representation. + + In order to avoid any ambiguity over character encodings, when strings are used + as arguments, they must be strictly limited to the ASCII range `chr(0)`-`chr(127)`. + If you require a bytewise representation that is outside this range you must + handle the character encoding directly, and pass a bytes instance. + """ + + def __init__( + self, + url: bytes | str = "", + *, + scheme: bytes | str = b"", + host: bytes | str = b"", + port: int | None = None, + target: bytes | str = b"", + ) -> None: + """ + Parameters: + url: The complete URL as a string or bytes. + scheme: The URL scheme as a string or bytes. + Typically either `"http"` or `"https"`. + host: The URL host as a string or bytes. Such as `"www.example.com"`. + port: The port to connect to. Either an integer or `None`. + target: The target of the HTTP request. Such as `"/items?search=red"`. + """ + if url: + parsed = urllib.parse.urlparse(enforce_bytes(url, name="url")) + self.scheme = parsed.scheme + self.host = parsed.hostname or b"" + self.port = parsed.port + self.target = (parsed.path or b"/") + ( + b"?" + parsed.query if parsed.query else b"" + ) + else: + self.scheme = enforce_bytes(scheme, name="scheme") + self.host = enforce_bytes(host, name="host") + self.port = port + self.target = enforce_bytes(target, name="target") + + @property + def origin(self) -> Origin: + default_port = { + b"http": 80, + b"https": 443, + b"ws": 80, + b"wss": 443, + b"socks5": 1080, + b"socks5h": 1080, + }[self.scheme] + return Origin( + scheme=self.scheme, host=self.host, port=self.port or default_port + ) + + def __eq__(self, other: typing.Any) -> bool: + return ( + isinstance(other, URL) + and other.scheme == self.scheme + and other.host == self.host + and other.port == self.port + and other.target == self.target + ) + + def __bytes__(self) -> bytes: + if self.port is None: + return b"%b://%b%b" % (self.scheme, self.host, self.target) + return b"%b://%b:%d%b" % (self.scheme, self.host, self.port, self.target) + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(scheme={self.scheme!r}, " + f"host={self.host!r}, port={self.port!r}, target={self.target!r})" + ) + + +class Request: + """ + An HTTP request. + """ + + def __init__( + self, + method: bytes | str, + url: URL | bytes | str, + *, + headers: HeaderTypes = None, + content: bytes + | typing.Iterable[bytes] + | typing.AsyncIterable[bytes] + | None = None, + extensions: Extensions | None = None, + ) -> None: + """ + Parameters: + method: The HTTP request method, either as a string or bytes. + For example: `GET`. + url: The request URL, either as a `URL` instance, or as a string or bytes. + For example: `"https://www.example.com".` + headers: The HTTP request headers. + content: The content of the request body. + extensions: A dictionary of optional extra information included on + the request. Possible keys include `"timeout"`, and `"trace"`. + """ + self.method: bytes = enforce_bytes(method, name="method") + self.url: URL = enforce_url(url, name="url") + self.headers: list[tuple[bytes, bytes]] = enforce_headers( + headers, name="headers" + ) + self.stream: typing.Iterable[bytes] | typing.AsyncIterable[bytes] = ( + enforce_stream(content, name="content") + ) + self.extensions = {} if extensions is None else extensions + + if "target" in self.extensions: + self.url = URL( + scheme=self.url.scheme, + host=self.url.host, + port=self.url.port, + target=self.extensions["target"], + ) + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{self.method!r}]>" + + +class Response: + """ + An HTTP response. + """ + + def __init__( + self, + status: int, + *, + headers: HeaderTypes = None, + content: bytes + | typing.Iterable[bytes] + | typing.AsyncIterable[bytes] + | None = None, + extensions: Extensions | None = None, + ) -> None: + """ + Parameters: + status: The HTTP status code of the response. For example `200`. + headers: The HTTP response headers. + content: The content of the response body. + extensions: A dictionary of optional extra information included on + the responseself.Possible keys include `"http_version"`, + `"reason_phrase"`, and `"network_stream"`. + """ + self.status: int = status + self.headers: list[tuple[bytes, bytes]] = enforce_headers( + headers, name="headers" + ) + self.stream: typing.Iterable[bytes] | typing.AsyncIterable[bytes] = ( + enforce_stream(content, name="content") + ) + self.extensions = {} if extensions is None else extensions + + self._stream_consumed = False + + @property + def content(self) -> bytes: + if not hasattr(self, "_content"): + if isinstance(self.stream, typing.Iterable): + raise RuntimeError( + "Attempted to access 'response.content' on a streaming response. " + "Call 'response.read()' first." + ) + else: + raise RuntimeError( + "Attempted to access 'response.content' on a streaming response. " + "Call 'await response.aread()' first." + ) + return self._content + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{self.status}]>" + + # Sync interface... + + def read(self) -> bytes: + if not isinstance(self.stream, typing.Iterable): # pragma: nocover + raise RuntimeError( + "Attempted to read an asynchronous response using 'response.read()'. " + "You should use 'await response.aread()' instead." + ) + if not hasattr(self, "_content"): + self._content = b"".join([part for part in self.iter_stream()]) + return self._content + + def iter_stream(self) -> typing.Iterator[bytes]: + if not isinstance(self.stream, typing.Iterable): # pragma: nocover + raise RuntimeError( + "Attempted to stream an asynchronous response using 'for ... in " + "response.iter_stream()'. " + "You should use 'async for ... in response.aiter_stream()' instead." + ) + if self._stream_consumed: + raise RuntimeError( + "Attempted to call 'for ... in response.iter_stream()' more than once." + ) + self._stream_consumed = True + for chunk in self.stream: + yield chunk + + def close(self) -> None: + if not isinstance(self.stream, typing.Iterable): # pragma: nocover + raise RuntimeError( + "Attempted to close an asynchronous response using 'response.close()'. " + "You should use 'await response.aclose()' instead." + ) + if hasattr(self.stream, "close"): + self.stream.close() + + # Async interface... + + async def aread(self) -> bytes: + if not isinstance(self.stream, typing.AsyncIterable): # pragma: nocover + raise RuntimeError( + "Attempted to read an synchronous response using " + "'await response.aread()'. " + "You should use 'response.read()' instead." + ) + if not hasattr(self, "_content"): + self._content = b"".join([part async for part in self.aiter_stream()]) + return self._content + + async def aiter_stream(self) -> typing.AsyncIterator[bytes]: + if not isinstance(self.stream, typing.AsyncIterable): # pragma: nocover + raise RuntimeError( + "Attempted to stream an synchronous response using 'async for ... in " + "response.aiter_stream()'. " + "You should use 'for ... in response.iter_stream()' instead." + ) + if self._stream_consumed: + raise RuntimeError( + "Attempted to call 'async for ... in response.aiter_stream()' " + "more than once." + ) + self._stream_consumed = True + async for chunk in self.stream: + yield chunk + + async def aclose(self) -> None: + if not isinstance(self.stream, typing.AsyncIterable): # pragma: nocover + raise RuntimeError( + "Attempted to close a synchronous response using " + "'await response.aclose()'. " + "You should use 'response.close()' instead." + ) + if hasattr(self.stream, "aclose"): + await self.stream.aclose() + + +class Proxy: + def __init__( + self, + url: URL | bytes | str, + auth: tuple[bytes | str, bytes | str] | None = None, + headers: HeadersAsMapping | HeadersAsSequence | None = None, + ssl_context: ssl.SSLContext | None = None, + ): + self.url = enforce_url(url, name="url") + self.headers = enforce_headers(headers, name="headers") + self.ssl_context = ssl_context + + if auth is not None: + username = enforce_bytes(auth[0], name="auth") + password = enforce_bytes(auth[1], name="auth") + userpass = username + b":" + password + authorization = b"Basic " + base64.b64encode(userpass) + self.auth: tuple[bytes, bytes] | None = (username, password) + self.headers = [(b"Proxy-Authorization", authorization)] + self.headers + else: + self.auth = None diff --git a/source/target-vendors/cp312/httpcore/_ssl.py b/source/target-vendors/cp312/httpcore/_ssl.py new file mode 100644 index 0000000..c99c5a6 --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_ssl.py @@ -0,0 +1,9 @@ +import ssl + +import certifi + + +def default_ssl_context() -> ssl.SSLContext: + context = ssl.create_default_context() + context.load_verify_locations(certifi.where()) + return context diff --git a/source/target-vendors/cp312/httpcore/_sync/__init__.py b/source/target-vendors/cp312/httpcore/_sync/__init__.py new file mode 100644 index 0000000..b476d76 --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_sync/__init__.py @@ -0,0 +1,39 @@ +from .connection import HTTPConnection +from .connection_pool import ConnectionPool +from .http11 import HTTP11Connection +from .http_proxy import HTTPProxy +from .interfaces import ConnectionInterface + +try: + from .http2 import HTTP2Connection +except ImportError: # pragma: nocover + + class HTTP2Connection: # type: ignore + def __init__(self, *args, **kwargs) -> None: # type: ignore + raise RuntimeError( + "Attempted to use http2 support, but the `h2` package is not " + "installed. Use 'pip install httpcore[http2]'." + ) + + +try: + from .socks_proxy import SOCKSProxy +except ImportError: # pragma: nocover + + class SOCKSProxy: # type: ignore + def __init__(self, *args, **kwargs) -> None: # type: ignore + raise RuntimeError( + "Attempted to use SOCKS support, but the `socksio` package is not " + "installed. Use 'pip install httpcore[socks]'." + ) + + +__all__ = [ + "HTTPConnection", + "ConnectionPool", + "HTTPProxy", + "HTTP11Connection", + "HTTP2Connection", + "ConnectionInterface", + "SOCKSProxy", +] diff --git a/source/target-vendors/cp312/httpcore/_sync/connection.py b/source/target-vendors/cp312/httpcore/_sync/connection.py new file mode 100644 index 0000000..363f8be --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_sync/connection.py @@ -0,0 +1,222 @@ +from __future__ import annotations + +import itertools +import logging +import ssl +import types +import typing + +from .._backends.sync import SyncBackend +from .._backends.base import SOCKET_OPTION, NetworkBackend, NetworkStream +from .._exceptions import ConnectError, ConnectTimeout +from .._models import Origin, Request, Response +from .._ssl import default_ssl_context +from .._synchronization import Lock +from .._trace import Trace +from .http11 import HTTP11Connection +from .interfaces import ConnectionInterface + +RETRIES_BACKOFF_FACTOR = 0.5 # 0s, 0.5s, 1s, 2s, 4s, etc. + + +logger = logging.getLogger("httpcore.connection") + + +def exponential_backoff(factor: float) -> typing.Iterator[float]: + """ + Generate a geometric sequence that has a ratio of 2 and starts with 0. + + For example: + - `factor = 2`: `0, 2, 4, 8, 16, 32, 64, ...` + - `factor = 3`: `0, 3, 6, 12, 24, 48, 96, ...` + """ + yield 0 + for n in itertools.count(): + yield factor * 2**n + + +class HTTPConnection(ConnectionInterface): + def __init__( + self, + origin: Origin, + ssl_context: ssl.SSLContext | None = None, + keepalive_expiry: float | None = None, + http1: bool = True, + http2: bool = False, + retries: int = 0, + local_address: str | None = None, + uds: str | None = None, + network_backend: NetworkBackend | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> None: + self._origin = origin + self._ssl_context = ssl_context + self._keepalive_expiry = keepalive_expiry + self._http1 = http1 + self._http2 = http2 + self._retries = retries + self._local_address = local_address + self._uds = uds + + self._network_backend: NetworkBackend = ( + SyncBackend() if network_backend is None else network_backend + ) + self._connection: ConnectionInterface | None = None + self._connect_failed: bool = False + self._request_lock = Lock() + self._socket_options = socket_options + + def handle_request(self, request: Request) -> Response: + if not self.can_handle_request(request.url.origin): + raise RuntimeError( + f"Attempted to send request to {request.url.origin} on connection to {self._origin}" + ) + + try: + with self._request_lock: + if self._connection is None: + stream = self._connect(request) + + ssl_object = stream.get_extra_info("ssl_object") + http2_negotiated = ( + ssl_object is not None + and ssl_object.selected_alpn_protocol() == "h2" + ) + if http2_negotiated or (self._http2 and not self._http1): + from .http2 import HTTP2Connection + + self._connection = HTTP2Connection( + origin=self._origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + else: + self._connection = HTTP11Connection( + origin=self._origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + except BaseException as exc: + self._connect_failed = True + raise exc + + return self._connection.handle_request(request) + + def _connect(self, request: Request) -> NetworkStream: + timeouts = request.extensions.get("timeout", {}) + sni_hostname = request.extensions.get("sni_hostname", None) + timeout = timeouts.get("connect", None) + + retries_left = self._retries + delays = exponential_backoff(factor=RETRIES_BACKOFF_FACTOR) + + while True: + try: + if self._uds is None: + kwargs = { + "host": self._origin.host.decode("ascii"), + "port": self._origin.port, + "local_address": self._local_address, + "timeout": timeout, + "socket_options": self._socket_options, + } + with Trace("connect_tcp", logger, request, kwargs) as trace: + stream = self._network_backend.connect_tcp(**kwargs) + trace.return_value = stream + else: + kwargs = { + "path": self._uds, + "timeout": timeout, + "socket_options": self._socket_options, + } + with Trace( + "connect_unix_socket", logger, request, kwargs + ) as trace: + stream = self._network_backend.connect_unix_socket( + **kwargs + ) + trace.return_value = stream + + if self._origin.scheme in (b"https", b"wss"): + ssl_context = ( + default_ssl_context() + if self._ssl_context is None + else self._ssl_context + ) + alpn_protocols = ["http/1.1", "h2"] if self._http2 else ["http/1.1"] + ssl_context.set_alpn_protocols(alpn_protocols) + + kwargs = { + "ssl_context": ssl_context, + "server_hostname": sni_hostname + or self._origin.host.decode("ascii"), + "timeout": timeout, + } + with Trace("start_tls", logger, request, kwargs) as trace: + stream = stream.start_tls(**kwargs) + trace.return_value = stream + return stream + except (ConnectError, ConnectTimeout): + if retries_left <= 0: + raise + retries_left -= 1 + delay = next(delays) + with Trace("retry", logger, request, kwargs) as trace: + self._network_backend.sleep(delay) + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._origin + + def close(self) -> None: + if self._connection is not None: + with Trace("close", logger, None, {}): + self._connection.close() + + def is_available(self) -> bool: + if self._connection is None: + # If HTTP/2 support is enabled, and the resulting connection could + # end up as HTTP/2 then we should indicate the connection as being + # available to service multiple requests. + return ( + self._http2 + and (self._origin.scheme == b"https" or not self._http1) + and not self._connect_failed + ) + return self._connection.is_available() + + def has_expired(self) -> bool: + if self._connection is None: + return self._connect_failed + return self._connection.has_expired() + + def is_idle(self) -> bool: + if self._connection is None: + return self._connect_failed + return self._connection.is_idle() + + def is_closed(self) -> bool: + if self._connection is None: + return self._connect_failed + return self._connection.is_closed() + + def info(self) -> str: + if self._connection is None: + return "CONNECTION FAILED" if self._connect_failed else "CONNECTING" + return self._connection.info() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{self.info()}]>" + + # These context managers are not used in the standard flow, but are + # useful for testing or working with connection instances directly. + + def __enter__(self) -> HTTPConnection: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: types.TracebackType | None = None, + ) -> None: + self.close() diff --git a/source/target-vendors/cp312/httpcore/_sync/connection_pool.py b/source/target-vendors/cp312/httpcore/_sync/connection_pool.py new file mode 100644 index 0000000..9ccfa53 --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_sync/connection_pool.py @@ -0,0 +1,420 @@ +from __future__ import annotations + +import ssl +import sys +import types +import typing + +from .._backends.sync import SyncBackend +from .._backends.base import SOCKET_OPTION, NetworkBackend +from .._exceptions import ConnectionNotAvailable, UnsupportedProtocol +from .._models import Origin, Proxy, Request, Response +from .._synchronization import Event, ShieldCancellation, ThreadLock +from .connection import HTTPConnection +from .interfaces import ConnectionInterface, RequestInterface + + +class PoolRequest: + def __init__(self, request: Request) -> None: + self.request = request + self.connection: ConnectionInterface | None = None + self._connection_acquired = Event() + + def assign_to_connection(self, connection: ConnectionInterface | None) -> None: + self.connection = connection + self._connection_acquired.set() + + def clear_connection(self) -> None: + self.connection = None + self._connection_acquired = Event() + + def wait_for_connection( + self, timeout: float | None = None + ) -> ConnectionInterface: + if self.connection is None: + self._connection_acquired.wait(timeout=timeout) + assert self.connection is not None + return self.connection + + def is_queued(self) -> bool: + return self.connection is None + + +class ConnectionPool(RequestInterface): + """ + A connection pool for making HTTP requests. + """ + + def __init__( + self, + ssl_context: ssl.SSLContext | None = None, + proxy: Proxy | None = None, + max_connections: int | None = 10, + max_keepalive_connections: int | None = None, + keepalive_expiry: float | None = None, + http1: bool = True, + http2: bool = False, + retries: int = 0, + local_address: str | None = None, + uds: str | None = None, + network_backend: NetworkBackend | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> None: + """ + A connection pool for making HTTP requests. + + Parameters: + ssl_context: An SSL context to use for verifying connections. + If not specified, the default `httpcore.default_ssl_context()` + will be used. + max_connections: The maximum number of concurrent HTTP connections that + the pool should allow. Any attempt to send a request on a pool that + would exceed this amount will block until a connection is available. + max_keepalive_connections: The maximum number of idle HTTP connections + that will be maintained in the pool. + keepalive_expiry: The duration in seconds that an idle HTTP connection + may be maintained for before being expired from the pool. + http1: A boolean indicating if HTTP/1.1 requests should be supported + by the connection pool. Defaults to True. + http2: A boolean indicating if HTTP/2 requests should be supported by + the connection pool. Defaults to False. + retries: The maximum number of retries when trying to establish a + connection. + local_address: Local address to connect from. Can also be used to connect + using a particular address family. Using `local_address="0.0.0.0"` + will connect using an `AF_INET` address (IPv4), while using + `local_address="::"` will connect using an `AF_INET6` address (IPv6). + uds: Path to a Unix Domain Socket to use instead of TCP sockets. + network_backend: A backend instance to use for handling network I/O. + socket_options: Socket options that have to be included + in the TCP socket when the connection was established. + """ + self._ssl_context = ssl_context + self._proxy = proxy + self._max_connections = ( + sys.maxsize if max_connections is None else max_connections + ) + self._max_keepalive_connections = ( + sys.maxsize + if max_keepalive_connections is None + else max_keepalive_connections + ) + self._max_keepalive_connections = min( + self._max_connections, self._max_keepalive_connections + ) + + self._keepalive_expiry = keepalive_expiry + self._http1 = http1 + self._http2 = http2 + self._retries = retries + self._local_address = local_address + self._uds = uds + + self._network_backend = ( + SyncBackend() if network_backend is None else network_backend + ) + self._socket_options = socket_options + + # The mutable state on a connection pool is the queue of incoming requests, + # and the set of connections that are servicing those requests. + self._connections: list[ConnectionInterface] = [] + self._requests: list[PoolRequest] = [] + + # We only mutate the state of the connection pool within an 'optional_thread_lock' + # context. This holds a threading lock unless we're running in async mode, + # in which case it is a no-op. + self._optional_thread_lock = ThreadLock() + + def create_connection(self, origin: Origin) -> ConnectionInterface: + if self._proxy is not None: + if self._proxy.url.scheme in (b"socks5", b"socks5h"): + from .socks_proxy import Socks5Connection + + return Socks5Connection( + proxy_origin=self._proxy.url.origin, + proxy_auth=self._proxy.auth, + remote_origin=origin, + ssl_context=self._ssl_context, + keepalive_expiry=self._keepalive_expiry, + http1=self._http1, + http2=self._http2, + network_backend=self._network_backend, + ) + elif origin.scheme == b"http": + from .http_proxy import ForwardHTTPConnection + + return ForwardHTTPConnection( + proxy_origin=self._proxy.url.origin, + proxy_headers=self._proxy.headers, + proxy_ssl_context=self._proxy.ssl_context, + remote_origin=origin, + keepalive_expiry=self._keepalive_expiry, + network_backend=self._network_backend, + ) + from .http_proxy import TunnelHTTPConnection + + return TunnelHTTPConnection( + proxy_origin=self._proxy.url.origin, + proxy_headers=self._proxy.headers, + proxy_ssl_context=self._proxy.ssl_context, + remote_origin=origin, + ssl_context=self._ssl_context, + keepalive_expiry=self._keepalive_expiry, + http1=self._http1, + http2=self._http2, + network_backend=self._network_backend, + ) + + return HTTPConnection( + origin=origin, + ssl_context=self._ssl_context, + keepalive_expiry=self._keepalive_expiry, + http1=self._http1, + http2=self._http2, + retries=self._retries, + local_address=self._local_address, + uds=self._uds, + network_backend=self._network_backend, + socket_options=self._socket_options, + ) + + @property + def connections(self) -> list[ConnectionInterface]: + """ + Return a list of the connections currently in the pool. + + For example: + + ```python + >>> pool.connections + [ + , + , + , + ] + ``` + """ + return list(self._connections) + + def handle_request(self, request: Request) -> Response: + """ + Send an HTTP request, and return an HTTP response. + + This is the core implementation that is called into by `.request()` or `.stream()`. + """ + scheme = request.url.scheme.decode() + if scheme == "": + raise UnsupportedProtocol( + "Request URL is missing an 'http://' or 'https://' protocol." + ) + if scheme not in ("http", "https", "ws", "wss"): + raise UnsupportedProtocol( + f"Request URL has an unsupported protocol '{scheme}://'." + ) + + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("pool", None) + + with self._optional_thread_lock: + # Add the incoming request to our request queue. + pool_request = PoolRequest(request) + self._requests.append(pool_request) + + try: + while True: + with self._optional_thread_lock: + # Assign incoming requests to available connections, + # closing or creating new connections as required. + closing = self._assign_requests_to_connections() + self._close_connections(closing) + + # Wait until this request has an assigned connection. + connection = pool_request.wait_for_connection(timeout=timeout) + + try: + # Send the request on the assigned connection. + response = connection.handle_request( + pool_request.request + ) + except ConnectionNotAvailable: + # In some cases a connection may initially be available to + # handle a request, but then become unavailable. + # + # In this case we clear the connection and try again. + pool_request.clear_connection() + else: + break # pragma: nocover + + except BaseException as exc: + with self._optional_thread_lock: + # For any exception or cancellation we remove the request from + # the queue, and then re-assign requests to connections. + self._requests.remove(pool_request) + closing = self._assign_requests_to_connections() + + self._close_connections(closing) + raise exc from None + + # Return the response. Note that in this case we still have to manage + # the point at which the response is closed. + assert isinstance(response.stream, typing.Iterable) + return Response( + status=response.status, + headers=response.headers, + content=PoolByteStream( + stream=response.stream, pool_request=pool_request, pool=self + ), + extensions=response.extensions, + ) + + def _assign_requests_to_connections(self) -> list[ConnectionInterface]: + """ + Manage the state of the connection pool, assigning incoming + requests to connections as available. + + Called whenever a new request is added or removed from the pool. + + Any closing connections are returned, allowing the I/O for closing + those connections to be handled seperately. + """ + closing_connections = [] + + # First we handle cleaning up any connections that are closed, + # have expired their keep-alive, or surplus idle connections. + for connection in list(self._connections): + if connection.is_closed(): + # log: "removing closed connection" + self._connections.remove(connection) + elif connection.has_expired(): + # log: "closing expired connection" + self._connections.remove(connection) + closing_connections.append(connection) + elif ( + connection.is_idle() + and len([connection.is_idle() for connection in self._connections]) + > self._max_keepalive_connections + ): + # log: "closing idle connection" + self._connections.remove(connection) + closing_connections.append(connection) + + # Assign queued requests to connections. + queued_requests = [request for request in self._requests if request.is_queued()] + for pool_request in queued_requests: + origin = pool_request.request.url.origin + available_connections = [ + connection + for connection in self._connections + if connection.can_handle_request(origin) and connection.is_available() + ] + idle_connections = [ + connection for connection in self._connections if connection.is_idle() + ] + + # There are three cases for how we may be able to handle the request: + # + # 1. There is an existing connection that can handle the request. + # 2. We can create a new connection to handle the request. + # 3. We can close an idle connection and then create a new connection + # to handle the request. + if available_connections: + # log: "reusing existing connection" + connection = available_connections[0] + pool_request.assign_to_connection(connection) + elif len(self._connections) < self._max_connections: + # log: "creating new connection" + connection = self.create_connection(origin) + self._connections.append(connection) + pool_request.assign_to_connection(connection) + elif idle_connections: + # log: "closing idle connection" + connection = idle_connections[0] + self._connections.remove(connection) + closing_connections.append(connection) + # log: "creating new connection" + connection = self.create_connection(origin) + self._connections.append(connection) + pool_request.assign_to_connection(connection) + + return closing_connections + + def _close_connections(self, closing: list[ConnectionInterface]) -> None: + # Close connections which have been removed from the pool. + with ShieldCancellation(): + for connection in closing: + connection.close() + + def close(self) -> None: + # Explicitly close the connection pool. + # Clears all existing requests and connections. + with self._optional_thread_lock: + closing_connections = list(self._connections) + self._connections = [] + self._close_connections(closing_connections) + + def __enter__(self) -> ConnectionPool: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: types.TracebackType | None = None, + ) -> None: + self.close() + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + with self._optional_thread_lock: + request_is_queued = [request.is_queued() for request in self._requests] + connection_is_idle = [ + connection.is_idle() for connection in self._connections + ] + + num_active_requests = request_is_queued.count(False) + num_queued_requests = request_is_queued.count(True) + num_active_connections = connection_is_idle.count(False) + num_idle_connections = connection_is_idle.count(True) + + requests_info = ( + f"Requests: {num_active_requests} active, {num_queued_requests} queued" + ) + connection_info = ( + f"Connections: {num_active_connections} active, {num_idle_connections} idle" + ) + + return f"<{class_name} [{requests_info} | {connection_info}]>" + + +class PoolByteStream: + def __init__( + self, + stream: typing.Iterable[bytes], + pool_request: PoolRequest, + pool: ConnectionPool, + ) -> None: + self._stream = stream + self._pool_request = pool_request + self._pool = pool + self._closed = False + + def __iter__(self) -> typing.Iterator[bytes]: + try: + for part in self._stream: + yield part + except BaseException as exc: + self.close() + raise exc from None + + def close(self) -> None: + if not self._closed: + self._closed = True + with ShieldCancellation(): + if hasattr(self._stream, "close"): + self._stream.close() + + with self._pool._optional_thread_lock: + self._pool._requests.remove(self._pool_request) + closing = self._pool._assign_requests_to_connections() + + self._pool._close_connections(closing) diff --git a/source/target-vendors/cp312/httpcore/_sync/http11.py b/source/target-vendors/cp312/httpcore/_sync/http11.py new file mode 100644 index 0000000..ebd3a97 --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_sync/http11.py @@ -0,0 +1,379 @@ +from __future__ import annotations + +import enum +import logging +import ssl +import time +import types +import typing + +import h11 + +from .._backends.base import NetworkStream +from .._exceptions import ( + ConnectionNotAvailable, + LocalProtocolError, + RemoteProtocolError, + WriteError, + map_exceptions, +) +from .._models import Origin, Request, Response +from .._synchronization import Lock, ShieldCancellation +from .._trace import Trace +from .interfaces import ConnectionInterface + +logger = logging.getLogger("httpcore.http11") + + +# A subset of `h11.Event` types supported by `_send_event` +H11SendEvent = typing.Union[ + h11.Request, + h11.Data, + h11.EndOfMessage, +] + + +class HTTPConnectionState(enum.IntEnum): + NEW = 0 + ACTIVE = 1 + IDLE = 2 + CLOSED = 3 + + +class HTTP11Connection(ConnectionInterface): + READ_NUM_BYTES = 64 * 1024 + MAX_INCOMPLETE_EVENT_SIZE = 100 * 1024 + + def __init__( + self, + origin: Origin, + stream: NetworkStream, + keepalive_expiry: float | None = None, + ) -> None: + self._origin = origin + self._network_stream = stream + self._keepalive_expiry: float | None = keepalive_expiry + self._expire_at: float | None = None + self._state = HTTPConnectionState.NEW + self._state_lock = Lock() + self._request_count = 0 + self._h11_state = h11.Connection( + our_role=h11.CLIENT, + max_incomplete_event_size=self.MAX_INCOMPLETE_EVENT_SIZE, + ) + + def handle_request(self, request: Request) -> Response: + if not self.can_handle_request(request.url.origin): + raise RuntimeError( + f"Attempted to send request to {request.url.origin} on connection " + f"to {self._origin}" + ) + + with self._state_lock: + if self._state in (HTTPConnectionState.NEW, HTTPConnectionState.IDLE): + self._request_count += 1 + self._state = HTTPConnectionState.ACTIVE + self._expire_at = None + else: + raise ConnectionNotAvailable() + + try: + kwargs = {"request": request} + try: + with Trace( + "send_request_headers", logger, request, kwargs + ) as trace: + self._send_request_headers(**kwargs) + with Trace("send_request_body", logger, request, kwargs) as trace: + self._send_request_body(**kwargs) + except WriteError: + # If we get a write error while we're writing the request, + # then we supress this error and move on to attempting to + # read the response. Servers can sometimes close the request + # pre-emptively and then respond with a well formed HTTP + # error response. + pass + + with Trace( + "receive_response_headers", logger, request, kwargs + ) as trace: + ( + http_version, + status, + reason_phrase, + headers, + trailing_data, + ) = self._receive_response_headers(**kwargs) + trace.return_value = ( + http_version, + status, + reason_phrase, + headers, + ) + + network_stream = self._network_stream + + # CONNECT or Upgrade request + if (status == 101) or ( + (request.method == b"CONNECT") and (200 <= status < 300) + ): + network_stream = HTTP11UpgradeStream(network_stream, trailing_data) + + return Response( + status=status, + headers=headers, + content=HTTP11ConnectionByteStream(self, request), + extensions={ + "http_version": http_version, + "reason_phrase": reason_phrase, + "network_stream": network_stream, + }, + ) + except BaseException as exc: + with ShieldCancellation(): + with Trace("response_closed", logger, request) as trace: + self._response_closed() + raise exc + + # Sending the request... + + def _send_request_headers(self, request: Request) -> None: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("write", None) + + with map_exceptions({h11.LocalProtocolError: LocalProtocolError}): + event = h11.Request( + method=request.method, + target=request.url.target, + headers=request.headers, + ) + self._send_event(event, timeout=timeout) + + def _send_request_body(self, request: Request) -> None: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("write", None) + + assert isinstance(request.stream, typing.Iterable) + for chunk in request.stream: + event = h11.Data(data=chunk) + self._send_event(event, timeout=timeout) + + self._send_event(h11.EndOfMessage(), timeout=timeout) + + def _send_event(self, event: h11.Event, timeout: float | None = None) -> None: + bytes_to_send = self._h11_state.send(event) + if bytes_to_send is not None: + self._network_stream.write(bytes_to_send, timeout=timeout) + + # Receiving the response... + + def _receive_response_headers( + self, request: Request + ) -> tuple[bytes, int, bytes, list[tuple[bytes, bytes]], bytes]: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("read", None) + + while True: + event = self._receive_event(timeout=timeout) + if isinstance(event, h11.Response): + break + if ( + isinstance(event, h11.InformationalResponse) + and event.status_code == 101 + ): + break + + http_version = b"HTTP/" + event.http_version + + # h11 version 0.11+ supports a `raw_items` interface to get the + # raw header casing, rather than the enforced lowercase headers. + headers = event.headers.raw_items() + + trailing_data, _ = self._h11_state.trailing_data + + return http_version, event.status_code, event.reason, headers, trailing_data + + def _receive_response_body( + self, request: Request + ) -> typing.Iterator[bytes]: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("read", None) + + while True: + event = self._receive_event(timeout=timeout) + if isinstance(event, h11.Data): + yield bytes(event.data) + elif isinstance(event, (h11.EndOfMessage, h11.PAUSED)): + break + + def _receive_event( + self, timeout: float | None = None + ) -> h11.Event | type[h11.PAUSED]: + while True: + with map_exceptions({h11.RemoteProtocolError: RemoteProtocolError}): + event = self._h11_state.next_event() + + if event is h11.NEED_DATA: + data = self._network_stream.read( + self.READ_NUM_BYTES, timeout=timeout + ) + + # If we feed this case through h11 we'll raise an exception like: + # + # httpcore.RemoteProtocolError: can't handle event type + # ConnectionClosed when role=SERVER and state=SEND_RESPONSE + # + # Which is accurate, but not very informative from an end-user + # perspective. Instead we handle this case distinctly and treat + # it as a ConnectError. + if data == b"" and self._h11_state.their_state == h11.SEND_RESPONSE: + msg = "Server disconnected without sending a response." + raise RemoteProtocolError(msg) + + self._h11_state.receive_data(data) + else: + # mypy fails to narrow the type in the above if statement above + return event # type: ignore[return-value] + + def _response_closed(self) -> None: + with self._state_lock: + if ( + self._h11_state.our_state is h11.DONE + and self._h11_state.their_state is h11.DONE + ): + self._state = HTTPConnectionState.IDLE + self._h11_state.start_next_cycle() + if self._keepalive_expiry is not None: + now = time.monotonic() + self._expire_at = now + self._keepalive_expiry + else: + self.close() + + # Once the connection is no longer required... + + def close(self) -> None: + # Note that this method unilaterally closes the connection, and does + # not have any kind of locking in place around it. + self._state = HTTPConnectionState.CLOSED + self._network_stream.close() + + # The ConnectionInterface methods provide information about the state of + # the connection, allowing for a connection pooling implementation to + # determine when to reuse and when to close the connection... + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._origin + + def is_available(self) -> bool: + # Note that HTTP/1.1 connections in the "NEW" state are not treated as + # being "available". The control flow which created the connection will + # be able to send an outgoing request, but the connection will not be + # acquired from the connection pool for any other request. + return self._state == HTTPConnectionState.IDLE + + def has_expired(self) -> bool: + now = time.monotonic() + keepalive_expired = self._expire_at is not None and now > self._expire_at + + # If the HTTP connection is idle but the socket is readable, then the + # only valid state is that the socket is about to return b"", indicating + # a server-initiated disconnect. + server_disconnected = ( + self._state == HTTPConnectionState.IDLE + and self._network_stream.get_extra_info("is_readable") + ) + + return keepalive_expired or server_disconnected + + def is_idle(self) -> bool: + return self._state == HTTPConnectionState.IDLE + + def is_closed(self) -> bool: + return self._state == HTTPConnectionState.CLOSED + + def info(self) -> str: + origin = str(self._origin) + return ( + f"{origin!r}, HTTP/1.1, {self._state.name}, " + f"Request Count: {self._request_count}" + ) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + origin = str(self._origin) + return ( + f"<{class_name} [{origin!r}, {self._state.name}, " + f"Request Count: {self._request_count}]>" + ) + + # These context managers are not used in the standard flow, but are + # useful for testing or working with connection instances directly. + + def __enter__(self) -> HTTP11Connection: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: types.TracebackType | None = None, + ) -> None: + self.close() + + +class HTTP11ConnectionByteStream: + def __init__(self, connection: HTTP11Connection, request: Request) -> None: + self._connection = connection + self._request = request + self._closed = False + + def __iter__(self) -> typing.Iterator[bytes]: + kwargs = {"request": self._request} + try: + with Trace("receive_response_body", logger, self._request, kwargs): + for chunk in self._connection._receive_response_body(**kwargs): + yield chunk + except BaseException as exc: + # If we get an exception while streaming the response, + # we want to close the response (and possibly the connection) + # before raising that exception. + with ShieldCancellation(): + self.close() + raise exc + + def close(self) -> None: + if not self._closed: + self._closed = True + with Trace("response_closed", logger, self._request): + self._connection._response_closed() + + +class HTTP11UpgradeStream(NetworkStream): + def __init__(self, stream: NetworkStream, leading_data: bytes) -> None: + self._stream = stream + self._leading_data = leading_data + + def read(self, max_bytes: int, timeout: float | None = None) -> bytes: + if self._leading_data: + buffer = self._leading_data[:max_bytes] + self._leading_data = self._leading_data[max_bytes:] + return buffer + else: + return self._stream.read(max_bytes, timeout) + + def write(self, buffer: bytes, timeout: float | None = None) -> None: + self._stream.write(buffer, timeout) + + def close(self) -> None: + self._stream.close() + + def start_tls( + self, + ssl_context: ssl.SSLContext, + server_hostname: str | None = None, + timeout: float | None = None, + ) -> NetworkStream: + return self._stream.start_tls(ssl_context, server_hostname, timeout) + + def get_extra_info(self, info: str) -> typing.Any: + return self._stream.get_extra_info(info) diff --git a/source/target-vendors/cp312/httpcore/_sync/http2.py b/source/target-vendors/cp312/httpcore/_sync/http2.py new file mode 100644 index 0000000..ddcc189 --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_sync/http2.py @@ -0,0 +1,592 @@ +from __future__ import annotations + +import enum +import logging +import time +import types +import typing + +import h2.config +import h2.connection +import h2.events +import h2.exceptions +import h2.settings + +from .._backends.base import NetworkStream +from .._exceptions import ( + ConnectionNotAvailable, + LocalProtocolError, + RemoteProtocolError, +) +from .._models import Origin, Request, Response +from .._synchronization import Lock, Semaphore, ShieldCancellation +from .._trace import Trace +from .interfaces import ConnectionInterface + +logger = logging.getLogger("httpcore.http2") + + +def has_body_headers(request: Request) -> bool: + return any( + k.lower() == b"content-length" or k.lower() == b"transfer-encoding" + for k, v in request.headers + ) + + +class HTTPConnectionState(enum.IntEnum): + ACTIVE = 1 + IDLE = 2 + CLOSED = 3 + + +class HTTP2Connection(ConnectionInterface): + READ_NUM_BYTES = 64 * 1024 + CONFIG = h2.config.H2Configuration(validate_inbound_headers=False) + + def __init__( + self, + origin: Origin, + stream: NetworkStream, + keepalive_expiry: float | None = None, + ): + self._origin = origin + self._network_stream = stream + self._keepalive_expiry: float | None = keepalive_expiry + self._h2_state = h2.connection.H2Connection(config=self.CONFIG) + self._state = HTTPConnectionState.IDLE + self._expire_at: float | None = None + self._request_count = 0 + self._init_lock = Lock() + self._state_lock = Lock() + self._read_lock = Lock() + self._write_lock = Lock() + self._sent_connection_init = False + self._used_all_stream_ids = False + self._connection_error = False + + # Mapping from stream ID to response stream events. + self._events: dict[ + int, + list[ + h2.events.ResponseReceived + | h2.events.DataReceived + | h2.events.StreamEnded + | h2.events.StreamReset, + ], + ] = {} + + # Connection terminated events are stored as state since + # we need to handle them for all streams. + self._connection_terminated: h2.events.ConnectionTerminated | None = None + + self._read_exception: Exception | None = None + self._write_exception: Exception | None = None + + def handle_request(self, request: Request) -> Response: + if not self.can_handle_request(request.url.origin): + # This cannot occur in normal operation, since the connection pool + # will only send requests on connections that handle them. + # It's in place simply for resilience as a guard against incorrect + # usage, for anyone working directly with httpcore connections. + raise RuntimeError( + f"Attempted to send request to {request.url.origin} on connection " + f"to {self._origin}" + ) + + with self._state_lock: + if self._state in (HTTPConnectionState.ACTIVE, HTTPConnectionState.IDLE): + self._request_count += 1 + self._expire_at = None + self._state = HTTPConnectionState.ACTIVE + else: + raise ConnectionNotAvailable() + + with self._init_lock: + if not self._sent_connection_init: + try: + sci_kwargs = {"request": request} + with Trace( + "send_connection_init", logger, request, sci_kwargs + ): + self._send_connection_init(**sci_kwargs) + except BaseException as exc: + with ShieldCancellation(): + self.close() + raise exc + + self._sent_connection_init = True + + # Initially start with just 1 until the remote server provides + # its max_concurrent_streams value + self._max_streams = 1 + + local_settings_max_streams = ( + self._h2_state.local_settings.max_concurrent_streams + ) + self._max_streams_semaphore = Semaphore(local_settings_max_streams) + + for _ in range(local_settings_max_streams - self._max_streams): + self._max_streams_semaphore.acquire() + + self._max_streams_semaphore.acquire() + + try: + stream_id = self._h2_state.get_next_available_stream_id() + self._events[stream_id] = [] + except h2.exceptions.NoAvailableStreamIDError: # pragma: nocover + self._used_all_stream_ids = True + self._request_count -= 1 + raise ConnectionNotAvailable() + + try: + kwargs = {"request": request, "stream_id": stream_id} + with Trace("send_request_headers", logger, request, kwargs): + self._send_request_headers(request=request, stream_id=stream_id) + with Trace("send_request_body", logger, request, kwargs): + self._send_request_body(request=request, stream_id=stream_id) + with Trace( + "receive_response_headers", logger, request, kwargs + ) as trace: + status, headers = self._receive_response( + request=request, stream_id=stream_id + ) + trace.return_value = (status, headers) + + return Response( + status=status, + headers=headers, + content=HTTP2ConnectionByteStream(self, request, stream_id=stream_id), + extensions={ + "http_version": b"HTTP/2", + "network_stream": self._network_stream, + "stream_id": stream_id, + }, + ) + except BaseException as exc: # noqa: PIE786 + with ShieldCancellation(): + kwargs = {"stream_id": stream_id} + with Trace("response_closed", logger, request, kwargs): + self._response_closed(stream_id=stream_id) + + if isinstance(exc, h2.exceptions.ProtocolError): + # One case where h2 can raise a protocol error is when a + # closed frame has been seen by the state machine. + # + # This happens when one stream is reading, and encounters + # a GOAWAY event. Other flows of control may then raise + # a protocol error at any point they interact with the 'h2_state'. + # + # In this case we'll have stored the event, and should raise + # it as a RemoteProtocolError. + if self._connection_terminated: # pragma: nocover + raise RemoteProtocolError(self._connection_terminated) + # If h2 raises a protocol error in some other state then we + # must somehow have made a protocol violation. + raise LocalProtocolError(exc) # pragma: nocover + + raise exc + + def _send_connection_init(self, request: Request) -> None: + """ + The HTTP/2 connection requires some initial setup before we can start + using individual request/response streams on it. + """ + # Need to set these manually here instead of manipulating via + # __setitem__() otherwise the H2Connection will emit SettingsUpdate + # frames in addition to sending the undesired defaults. + self._h2_state.local_settings = h2.settings.Settings( + client=True, + initial_values={ + # Disable PUSH_PROMISE frames from the server since we don't do anything + # with them for now. Maybe when we support caching? + h2.settings.SettingCodes.ENABLE_PUSH: 0, + # These two are taken from h2 for safe defaults + h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: 100, + h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE: 65536, + }, + ) + + # Some websites (*cough* Yahoo *cough*) balk at this setting being + # present in the initial handshake since it's not defined in the original + # RFC despite the RFC mandating ignoring settings you don't know about. + del self._h2_state.local_settings[ + h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL + ] + + self._h2_state.initiate_connection() + self._h2_state.increment_flow_control_window(2**24) + self._write_outgoing_data(request) + + # Sending the request... + + def _send_request_headers(self, request: Request, stream_id: int) -> None: + """ + Send the request headers to a given stream ID. + """ + end_stream = not has_body_headers(request) + + # In HTTP/2 the ':authority' pseudo-header is used instead of 'Host'. + # In order to gracefully handle HTTP/1.1 and HTTP/2 we always require + # HTTP/1.1 style headers, and map them appropriately if we end up on + # an HTTP/2 connection. + authority = [v for k, v in request.headers if k.lower() == b"host"][0] + + headers = [ + (b":method", request.method), + (b":authority", authority), + (b":scheme", request.url.scheme), + (b":path", request.url.target), + ] + [ + (k.lower(), v) + for k, v in request.headers + if k.lower() + not in ( + b"host", + b"transfer-encoding", + ) + ] + + self._h2_state.send_headers(stream_id, headers, end_stream=end_stream) + self._h2_state.increment_flow_control_window(2**24, stream_id=stream_id) + self._write_outgoing_data(request) + + def _send_request_body(self, request: Request, stream_id: int) -> None: + """ + Iterate over the request body sending it to a given stream ID. + """ + if not has_body_headers(request): + return + + assert isinstance(request.stream, typing.Iterable) + for data in request.stream: + self._send_stream_data(request, stream_id, data) + self._send_end_stream(request, stream_id) + + def _send_stream_data( + self, request: Request, stream_id: int, data: bytes + ) -> None: + """ + Send a single chunk of data in one or more data frames. + """ + while data: + max_flow = self._wait_for_outgoing_flow(request, stream_id) + chunk_size = min(len(data), max_flow) + chunk, data = data[:chunk_size], data[chunk_size:] + self._h2_state.send_data(stream_id, chunk) + self._write_outgoing_data(request) + + def _send_end_stream(self, request: Request, stream_id: int) -> None: + """ + Send an empty data frame on on a given stream ID with the END_STREAM flag set. + """ + self._h2_state.end_stream(stream_id) + self._write_outgoing_data(request) + + # Receiving the response... + + def _receive_response( + self, request: Request, stream_id: int + ) -> tuple[int, list[tuple[bytes, bytes]]]: + """ + Return the response status code and headers for a given stream ID. + """ + while True: + event = self._receive_stream_event(request, stream_id) + if isinstance(event, h2.events.ResponseReceived): + break + + status_code = 200 + headers = [] + assert event.headers is not None + for k, v in event.headers: + if k == b":status": + status_code = int(v.decode("ascii", errors="ignore")) + elif not k.startswith(b":"): + headers.append((k, v)) + + return (status_code, headers) + + def _receive_response_body( + self, request: Request, stream_id: int + ) -> typing.Iterator[bytes]: + """ + Iterator that returns the bytes of the response body for a given stream ID. + """ + while True: + event = self._receive_stream_event(request, stream_id) + if isinstance(event, h2.events.DataReceived): + assert event.flow_controlled_length is not None + assert event.data is not None + amount = event.flow_controlled_length + self._h2_state.acknowledge_received_data(amount, stream_id) + self._write_outgoing_data(request) + yield event.data + elif isinstance(event, h2.events.StreamEnded): + break + + def _receive_stream_event( + self, request: Request, stream_id: int + ) -> h2.events.ResponseReceived | h2.events.DataReceived | h2.events.StreamEnded: + """ + Return the next available event for a given stream ID. + + Will read more data from the network if required. + """ + while not self._events.get(stream_id): + self._receive_events(request, stream_id) + event = self._events[stream_id].pop(0) + if isinstance(event, h2.events.StreamReset): + raise RemoteProtocolError(event) + return event + + def _receive_events( + self, request: Request, stream_id: int | None = None + ) -> None: + """ + Read some data from the network until we see one or more events + for a given stream ID. + """ + with self._read_lock: + if self._connection_terminated is not None: + last_stream_id = self._connection_terminated.last_stream_id + if stream_id and last_stream_id and stream_id > last_stream_id: + self._request_count -= 1 + raise ConnectionNotAvailable() + raise RemoteProtocolError(self._connection_terminated) + + # This conditional is a bit icky. We don't want to block reading if we've + # actually got an event to return for a given stream. We need to do that + # check *within* the atomic read lock. Though it also need to be optional, + # because when we call it from `_wait_for_outgoing_flow` we *do* want to + # block until we've available flow control, event when we have events + # pending for the stream ID we're attempting to send on. + if stream_id is None or not self._events.get(stream_id): + events = self._read_incoming_data(request) + for event in events: + if isinstance(event, h2.events.RemoteSettingsChanged): + with Trace( + "receive_remote_settings", logger, request + ) as trace: + self._receive_remote_settings_change(event) + trace.return_value = event + + elif isinstance( + event, + ( + h2.events.ResponseReceived, + h2.events.DataReceived, + h2.events.StreamEnded, + h2.events.StreamReset, + ), + ): + if event.stream_id in self._events: + self._events[event.stream_id].append(event) + + elif isinstance(event, h2.events.ConnectionTerminated): + self._connection_terminated = event + + self._write_outgoing_data(request) + + def _receive_remote_settings_change( + self, event: h2.events.RemoteSettingsChanged + ) -> None: + max_concurrent_streams = event.changed_settings.get( + h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS + ) + if max_concurrent_streams: + new_max_streams = min( + max_concurrent_streams.new_value, + self._h2_state.local_settings.max_concurrent_streams, + ) + if new_max_streams and new_max_streams != self._max_streams: + while new_max_streams > self._max_streams: + self._max_streams_semaphore.release() + self._max_streams += 1 + while new_max_streams < self._max_streams: + self._max_streams_semaphore.acquire() + self._max_streams -= 1 + + def _response_closed(self, stream_id: int) -> None: + self._max_streams_semaphore.release() + del self._events[stream_id] + with self._state_lock: + if self._connection_terminated and not self._events: + self.close() + + elif self._state == HTTPConnectionState.ACTIVE and not self._events: + self._state = HTTPConnectionState.IDLE + if self._keepalive_expiry is not None: + now = time.monotonic() + self._expire_at = now + self._keepalive_expiry + if self._used_all_stream_ids: # pragma: nocover + self.close() + + def close(self) -> None: + # Note that this method unilaterally closes the connection, and does + # not have any kind of locking in place around it. + self._h2_state.close_connection() + self._state = HTTPConnectionState.CLOSED + self._network_stream.close() + + # Wrappers around network read/write operations... + + def _read_incoming_data(self, request: Request) -> list[h2.events.Event]: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("read", None) + + if self._read_exception is not None: + raise self._read_exception # pragma: nocover + + try: + data = self._network_stream.read(self.READ_NUM_BYTES, timeout) + if data == b"": + raise RemoteProtocolError("Server disconnected") + except Exception as exc: + # If we get a network error we should: + # + # 1. Save the exception and just raise it immediately on any future reads. + # (For example, this means that a single read timeout or disconnect will + # immediately close all pending streams. Without requiring multiple + # sequential timeouts.) + # 2. Mark the connection as errored, so that we don't accept any other + # incoming requests. + self._read_exception = exc + self._connection_error = True + raise exc + + events: list[h2.events.Event] = self._h2_state.receive_data(data) + + return events + + def _write_outgoing_data(self, request: Request) -> None: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("write", None) + + with self._write_lock: + data_to_send = self._h2_state.data_to_send() + + if self._write_exception is not None: + raise self._write_exception # pragma: nocover + + try: + self._network_stream.write(data_to_send, timeout) + except Exception as exc: # pragma: nocover + # If we get a network error we should: + # + # 1. Save the exception and just raise it immediately on any future write. + # (For example, this means that a single write timeout or disconnect will + # immediately close all pending streams. Without requiring multiple + # sequential timeouts.) + # 2. Mark the connection as errored, so that we don't accept any other + # incoming requests. + self._write_exception = exc + self._connection_error = True + raise exc + + # Flow control... + + def _wait_for_outgoing_flow(self, request: Request, stream_id: int) -> int: + """ + Returns the maximum allowable outgoing flow for a given stream. + + If the allowable flow is zero, then waits on the network until + WindowUpdated frames have increased the flow rate. + https://tools.ietf.org/html/rfc7540#section-6.9 + """ + local_flow: int = self._h2_state.local_flow_control_window(stream_id) + max_frame_size: int = self._h2_state.max_outbound_frame_size + flow = min(local_flow, max_frame_size) + while flow == 0: + self._receive_events(request) + local_flow = self._h2_state.local_flow_control_window(stream_id) + max_frame_size = self._h2_state.max_outbound_frame_size + flow = min(local_flow, max_frame_size) + return flow + + # Interface for connection pooling... + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._origin + + def is_available(self) -> bool: + return ( + self._state != HTTPConnectionState.CLOSED + and not self._connection_error + and not self._used_all_stream_ids + and not ( + self._h2_state.state_machine.state + == h2.connection.ConnectionState.CLOSED + ) + ) + + def has_expired(self) -> bool: + now = time.monotonic() + return self._expire_at is not None and now > self._expire_at + + def is_idle(self) -> bool: + return self._state == HTTPConnectionState.IDLE + + def is_closed(self) -> bool: + return self._state == HTTPConnectionState.CLOSED + + def info(self) -> str: + origin = str(self._origin) + return ( + f"{origin!r}, HTTP/2, {self._state.name}, " + f"Request Count: {self._request_count}" + ) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + origin = str(self._origin) + return ( + f"<{class_name} [{origin!r}, {self._state.name}, " + f"Request Count: {self._request_count}]>" + ) + + # These context managers are not used in the standard flow, but are + # useful for testing or working with connection instances directly. + + def __enter__(self) -> HTTP2Connection: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: types.TracebackType | None = None, + ) -> None: + self.close() + + +class HTTP2ConnectionByteStream: + def __init__( + self, connection: HTTP2Connection, request: Request, stream_id: int + ) -> None: + self._connection = connection + self._request = request + self._stream_id = stream_id + self._closed = False + + def __iter__(self) -> typing.Iterator[bytes]: + kwargs = {"request": self._request, "stream_id": self._stream_id} + try: + with Trace("receive_response_body", logger, self._request, kwargs): + for chunk in self._connection._receive_response_body( + request=self._request, stream_id=self._stream_id + ): + yield chunk + except BaseException as exc: + # If we get an exception while streaming the response, + # we want to close the response (and possibly the connection) + # before raising that exception. + with ShieldCancellation(): + self.close() + raise exc + + def close(self) -> None: + if not self._closed: + self._closed = True + kwargs = {"stream_id": self._stream_id} + with Trace("response_closed", logger, self._request, kwargs): + self._connection._response_closed(stream_id=self._stream_id) diff --git a/source/target-vendors/cp312/httpcore/_sync/http_proxy.py b/source/target-vendors/cp312/httpcore/_sync/http_proxy.py new file mode 100644 index 0000000..ecca88f --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_sync/http_proxy.py @@ -0,0 +1,367 @@ +from __future__ import annotations + +import base64 +import logging +import ssl +import typing + +from .._backends.base import SOCKET_OPTION, NetworkBackend +from .._exceptions import ProxyError +from .._models import ( + URL, + Origin, + Request, + Response, + enforce_bytes, + enforce_headers, + enforce_url, +) +from .._ssl import default_ssl_context +from .._synchronization import Lock +from .._trace import Trace +from .connection import HTTPConnection +from .connection_pool import ConnectionPool +from .http11 import HTTP11Connection +from .interfaces import ConnectionInterface + +ByteOrStr = typing.Union[bytes, str] +HeadersAsSequence = typing.Sequence[typing.Tuple[ByteOrStr, ByteOrStr]] +HeadersAsMapping = typing.Mapping[ByteOrStr, ByteOrStr] + + +logger = logging.getLogger("httpcore.proxy") + + +def merge_headers( + default_headers: typing.Sequence[tuple[bytes, bytes]] | None = None, + override_headers: typing.Sequence[tuple[bytes, bytes]] | None = None, +) -> list[tuple[bytes, bytes]]: + """ + Append default_headers and override_headers, de-duplicating if a key exists + in both cases. + """ + default_headers = [] if default_headers is None else list(default_headers) + override_headers = [] if override_headers is None else list(override_headers) + has_override = set(key.lower() for key, value in override_headers) + default_headers = [ + (key, value) + for key, value in default_headers + if key.lower() not in has_override + ] + return default_headers + override_headers + + +class HTTPProxy(ConnectionPool): # pragma: nocover + """ + A connection pool that sends requests via an HTTP proxy. + """ + + def __init__( + self, + proxy_url: URL | bytes | str, + proxy_auth: tuple[bytes | str, bytes | str] | None = None, + proxy_headers: HeadersAsMapping | HeadersAsSequence | None = None, + ssl_context: ssl.SSLContext | None = None, + proxy_ssl_context: ssl.SSLContext | None = None, + max_connections: int | None = 10, + max_keepalive_connections: int | None = None, + keepalive_expiry: float | None = None, + http1: bool = True, + http2: bool = False, + retries: int = 0, + local_address: str | None = None, + uds: str | None = None, + network_backend: NetworkBackend | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> None: + """ + A connection pool for making HTTP requests. + + Parameters: + proxy_url: The URL to use when connecting to the proxy server. + For example `"http://127.0.0.1:8080/"`. + proxy_auth: Any proxy authentication as a two-tuple of + (username, password). May be either bytes or ascii-only str. + proxy_headers: Any HTTP headers to use for the proxy requests. + For example `{"Proxy-Authorization": "Basic :"}`. + ssl_context: An SSL context to use for verifying connections. + If not specified, the default `httpcore.default_ssl_context()` + will be used. + proxy_ssl_context: The same as `ssl_context`, but for a proxy server rather than a remote origin. + max_connections: The maximum number of concurrent HTTP connections that + the pool should allow. Any attempt to send a request on a pool that + would exceed this amount will block until a connection is available. + max_keepalive_connections: The maximum number of idle HTTP connections + that will be maintained in the pool. + keepalive_expiry: The duration in seconds that an idle HTTP connection + may be maintained for before being expired from the pool. + http1: A boolean indicating if HTTP/1.1 requests should be supported + by the connection pool. Defaults to True. + http2: A boolean indicating if HTTP/2 requests should be supported by + the connection pool. Defaults to False. + retries: The maximum number of retries when trying to establish + a connection. + local_address: Local address to connect from. Can also be used to + connect using a particular address family. Using + `local_address="0.0.0.0"` will connect using an `AF_INET` address + (IPv4), while using `local_address="::"` will connect using an + `AF_INET6` address (IPv6). + uds: Path to a Unix Domain Socket to use instead of TCP sockets. + network_backend: A backend instance to use for handling network I/O. + """ + super().__init__( + ssl_context=ssl_context, + max_connections=max_connections, + max_keepalive_connections=max_keepalive_connections, + keepalive_expiry=keepalive_expiry, + http1=http1, + http2=http2, + network_backend=network_backend, + retries=retries, + local_address=local_address, + uds=uds, + socket_options=socket_options, + ) + + self._proxy_url = enforce_url(proxy_url, name="proxy_url") + if ( + self._proxy_url.scheme == b"http" and proxy_ssl_context is not None + ): # pragma: no cover + raise RuntimeError( + "The `proxy_ssl_context` argument is not allowed for the http scheme" + ) + + self._ssl_context = ssl_context + self._proxy_ssl_context = proxy_ssl_context + self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers") + if proxy_auth is not None: + username = enforce_bytes(proxy_auth[0], name="proxy_auth") + password = enforce_bytes(proxy_auth[1], name="proxy_auth") + userpass = username + b":" + password + authorization = b"Basic " + base64.b64encode(userpass) + self._proxy_headers = [ + (b"Proxy-Authorization", authorization) + ] + self._proxy_headers + + def create_connection(self, origin: Origin) -> ConnectionInterface: + if origin.scheme == b"http": + return ForwardHTTPConnection( + proxy_origin=self._proxy_url.origin, + proxy_headers=self._proxy_headers, + remote_origin=origin, + keepalive_expiry=self._keepalive_expiry, + network_backend=self._network_backend, + proxy_ssl_context=self._proxy_ssl_context, + ) + return TunnelHTTPConnection( + proxy_origin=self._proxy_url.origin, + proxy_headers=self._proxy_headers, + remote_origin=origin, + ssl_context=self._ssl_context, + proxy_ssl_context=self._proxy_ssl_context, + keepalive_expiry=self._keepalive_expiry, + http1=self._http1, + http2=self._http2, + network_backend=self._network_backend, + ) + + +class ForwardHTTPConnection(ConnectionInterface): + def __init__( + self, + proxy_origin: Origin, + remote_origin: Origin, + proxy_headers: HeadersAsMapping | HeadersAsSequence | None = None, + keepalive_expiry: float | None = None, + network_backend: NetworkBackend | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + proxy_ssl_context: ssl.SSLContext | None = None, + ) -> None: + self._connection = HTTPConnection( + origin=proxy_origin, + keepalive_expiry=keepalive_expiry, + network_backend=network_backend, + socket_options=socket_options, + ssl_context=proxy_ssl_context, + ) + self._proxy_origin = proxy_origin + self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers") + self._remote_origin = remote_origin + + def handle_request(self, request: Request) -> Response: + headers = merge_headers(self._proxy_headers, request.headers) + url = URL( + scheme=self._proxy_origin.scheme, + host=self._proxy_origin.host, + port=self._proxy_origin.port, + target=bytes(request.url), + ) + proxy_request = Request( + method=request.method, + url=url, + headers=headers, + content=request.stream, + extensions=request.extensions, + ) + return self._connection.handle_request(proxy_request) + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._remote_origin + + def close(self) -> None: + self._connection.close() + + def info(self) -> str: + return self._connection.info() + + def is_available(self) -> bool: + return self._connection.is_available() + + def has_expired(self) -> bool: + return self._connection.has_expired() + + def is_idle(self) -> bool: + return self._connection.is_idle() + + def is_closed(self) -> bool: + return self._connection.is_closed() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{self.info()}]>" + + +class TunnelHTTPConnection(ConnectionInterface): + def __init__( + self, + proxy_origin: Origin, + remote_origin: Origin, + ssl_context: ssl.SSLContext | None = None, + proxy_ssl_context: ssl.SSLContext | None = None, + proxy_headers: typing.Sequence[tuple[bytes, bytes]] | None = None, + keepalive_expiry: float | None = None, + http1: bool = True, + http2: bool = False, + network_backend: NetworkBackend | None = None, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> None: + self._connection: ConnectionInterface = HTTPConnection( + origin=proxy_origin, + keepalive_expiry=keepalive_expiry, + network_backend=network_backend, + socket_options=socket_options, + ssl_context=proxy_ssl_context, + ) + self._proxy_origin = proxy_origin + self._remote_origin = remote_origin + self._ssl_context = ssl_context + self._proxy_ssl_context = proxy_ssl_context + self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers") + self._keepalive_expiry = keepalive_expiry + self._http1 = http1 + self._http2 = http2 + self._connect_lock = Lock() + self._connected = False + + def handle_request(self, request: Request) -> Response: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("connect", None) + + with self._connect_lock: + if not self._connected: + target = b"%b:%d" % (self._remote_origin.host, self._remote_origin.port) + + connect_url = URL( + scheme=self._proxy_origin.scheme, + host=self._proxy_origin.host, + port=self._proxy_origin.port, + target=target, + ) + connect_headers = merge_headers( + [(b"Host", target), (b"Accept", b"*/*")], self._proxy_headers + ) + connect_request = Request( + method=b"CONNECT", + url=connect_url, + headers=connect_headers, + extensions=request.extensions, + ) + connect_response = self._connection.handle_request( + connect_request + ) + + if connect_response.status < 200 or connect_response.status > 299: + reason_bytes = connect_response.extensions.get("reason_phrase", b"") + reason_str = reason_bytes.decode("ascii", errors="ignore") + msg = "%d %s" % (connect_response.status, reason_str) + self._connection.close() + raise ProxyError(msg) + + stream = connect_response.extensions["network_stream"] + + # Upgrade the stream to SSL + ssl_context = ( + default_ssl_context() + if self._ssl_context is None + else self._ssl_context + ) + alpn_protocols = ["http/1.1", "h2"] if self._http2 else ["http/1.1"] + ssl_context.set_alpn_protocols(alpn_protocols) + + kwargs = { + "ssl_context": ssl_context, + "server_hostname": self._remote_origin.host.decode("ascii"), + "timeout": timeout, + } + with Trace("start_tls", logger, request, kwargs) as trace: + stream = stream.start_tls(**kwargs) + trace.return_value = stream + + # Determine if we should be using HTTP/1.1 or HTTP/2 + ssl_object = stream.get_extra_info("ssl_object") + http2_negotiated = ( + ssl_object is not None + and ssl_object.selected_alpn_protocol() == "h2" + ) + + # Create the HTTP/1.1 or HTTP/2 connection + if http2_negotiated or (self._http2 and not self._http1): + from .http2 import HTTP2Connection + + self._connection = HTTP2Connection( + origin=self._remote_origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + else: + self._connection = HTTP11Connection( + origin=self._remote_origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + + self._connected = True + return self._connection.handle_request(request) + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._remote_origin + + def close(self) -> None: + self._connection.close() + + def info(self) -> str: + return self._connection.info() + + def is_available(self) -> bool: + return self._connection.is_available() + + def has_expired(self) -> bool: + return self._connection.has_expired() + + def is_idle(self) -> bool: + return self._connection.is_idle() + + def is_closed(self) -> bool: + return self._connection.is_closed() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{self.info()}]>" diff --git a/source/target-vendors/cp312/httpcore/_sync/interfaces.py b/source/target-vendors/cp312/httpcore/_sync/interfaces.py new file mode 100644 index 0000000..e673d4c --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_sync/interfaces.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +import contextlib +import typing + +from .._models import ( + URL, + Extensions, + HeaderTypes, + Origin, + Request, + Response, + enforce_bytes, + enforce_headers, + enforce_url, + include_request_headers, +) + + +class RequestInterface: + def request( + self, + method: bytes | str, + url: URL | bytes | str, + *, + headers: HeaderTypes = None, + content: bytes | typing.Iterator[bytes] | None = None, + extensions: Extensions | None = None, + ) -> Response: + # Strict type checking on our parameters. + method = enforce_bytes(method, name="method") + url = enforce_url(url, name="url") + headers = enforce_headers(headers, name="headers") + + # Include Host header, and optionally Content-Length or Transfer-Encoding. + headers = include_request_headers(headers, url=url, content=content) + + request = Request( + method=method, + url=url, + headers=headers, + content=content, + extensions=extensions, + ) + response = self.handle_request(request) + try: + response.read() + finally: + response.close() + return response + + @contextlib.contextmanager + def stream( + self, + method: bytes | str, + url: URL | bytes | str, + *, + headers: HeaderTypes = None, + content: bytes | typing.Iterator[bytes] | None = None, + extensions: Extensions | None = None, + ) -> typing.Iterator[Response]: + # Strict type checking on our parameters. + method = enforce_bytes(method, name="method") + url = enforce_url(url, name="url") + headers = enforce_headers(headers, name="headers") + + # Include Host header, and optionally Content-Length or Transfer-Encoding. + headers = include_request_headers(headers, url=url, content=content) + + request = Request( + method=method, + url=url, + headers=headers, + content=content, + extensions=extensions, + ) + response = self.handle_request(request) + try: + yield response + finally: + response.close() + + def handle_request(self, request: Request) -> Response: + raise NotImplementedError() # pragma: nocover + + +class ConnectionInterface(RequestInterface): + def close(self) -> None: + raise NotImplementedError() # pragma: nocover + + def info(self) -> str: + raise NotImplementedError() # pragma: nocover + + def can_handle_request(self, origin: Origin) -> bool: + raise NotImplementedError() # pragma: nocover + + def is_available(self) -> bool: + """ + Return `True` if the connection is currently able to accept an + outgoing request. + + An HTTP/1.1 connection will only be available if it is currently idle. + + An HTTP/2 connection will be available so long as the stream ID space is + not yet exhausted, and the connection is not in an error state. + + While the connection is being established we may not yet know if it is going + to result in an HTTP/1.1 or HTTP/2 connection. The connection should be + treated as being available, but might ultimately raise `NewConnectionRequired` + required exceptions if multiple requests are attempted over a connection + that ends up being established as HTTP/1.1. + """ + raise NotImplementedError() # pragma: nocover + + def has_expired(self) -> bool: + """ + Return `True` if the connection is in a state where it should be closed. + + This either means that the connection is idle and it has passed the + expiry time on its keep-alive, or that server has sent an EOF. + """ + raise NotImplementedError() # pragma: nocover + + def is_idle(self) -> bool: + """ + Return `True` if the connection is currently idle. + """ + raise NotImplementedError() # pragma: nocover + + def is_closed(self) -> bool: + """ + Return `True` if the connection has been closed. + + Used when a response is closed to determine if the connection may be + returned to the connection pool or not. + """ + raise NotImplementedError() # pragma: nocover diff --git a/source/target-vendors/cp312/httpcore/_sync/socks_proxy.py b/source/target-vendors/cp312/httpcore/_sync/socks_proxy.py new file mode 100644 index 0000000..0ca96dd --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_sync/socks_proxy.py @@ -0,0 +1,341 @@ +from __future__ import annotations + +import logging +import ssl + +import socksio + +from .._backends.sync import SyncBackend +from .._backends.base import NetworkBackend, NetworkStream +from .._exceptions import ConnectionNotAvailable, ProxyError +from .._models import URL, Origin, Request, Response, enforce_bytes, enforce_url +from .._ssl import default_ssl_context +from .._synchronization import Lock +from .._trace import Trace +from .connection_pool import ConnectionPool +from .http11 import HTTP11Connection +from .interfaces import ConnectionInterface + +logger = logging.getLogger("httpcore.socks") + + +AUTH_METHODS = { + b"\x00": "NO AUTHENTICATION REQUIRED", + b"\x01": "GSSAPI", + b"\x02": "USERNAME/PASSWORD", + b"\xff": "NO ACCEPTABLE METHODS", +} + +REPLY_CODES = { + b"\x00": "Succeeded", + b"\x01": "General SOCKS server failure", + b"\x02": "Connection not allowed by ruleset", + b"\x03": "Network unreachable", + b"\x04": "Host unreachable", + b"\x05": "Connection refused", + b"\x06": "TTL expired", + b"\x07": "Command not supported", + b"\x08": "Address type not supported", +} + + +def _init_socks5_connection( + stream: NetworkStream, + *, + host: bytes, + port: int, + auth: tuple[bytes, bytes] | None = None, +) -> None: + conn = socksio.socks5.SOCKS5Connection() + + # Auth method request + auth_method = ( + socksio.socks5.SOCKS5AuthMethod.NO_AUTH_REQUIRED + if auth is None + else socksio.socks5.SOCKS5AuthMethod.USERNAME_PASSWORD + ) + conn.send(socksio.socks5.SOCKS5AuthMethodsRequest([auth_method])) + outgoing_bytes = conn.data_to_send() + stream.write(outgoing_bytes) + + # Auth method response + incoming_bytes = stream.read(max_bytes=4096) + response = conn.receive_data(incoming_bytes) + assert isinstance(response, socksio.socks5.SOCKS5AuthReply) + if response.method != auth_method: + requested = AUTH_METHODS.get(auth_method, "UNKNOWN") + responded = AUTH_METHODS.get(response.method, "UNKNOWN") + raise ProxyError( + f"Requested {requested} from proxy server, but got {responded}." + ) + + if response.method == socksio.socks5.SOCKS5AuthMethod.USERNAME_PASSWORD: + # Username/password request + assert auth is not None + username, password = auth + conn.send(socksio.socks5.SOCKS5UsernamePasswordRequest(username, password)) + outgoing_bytes = conn.data_to_send() + stream.write(outgoing_bytes) + + # Username/password response + incoming_bytes = stream.read(max_bytes=4096) + response = conn.receive_data(incoming_bytes) + assert isinstance(response, socksio.socks5.SOCKS5UsernamePasswordReply) + if not response.success: + raise ProxyError("Invalid username/password") + + # Connect request + conn.send( + socksio.socks5.SOCKS5CommandRequest.from_address( + socksio.socks5.SOCKS5Command.CONNECT, (host, port) + ) + ) + outgoing_bytes = conn.data_to_send() + stream.write(outgoing_bytes) + + # Connect response + incoming_bytes = stream.read(max_bytes=4096) + response = conn.receive_data(incoming_bytes) + assert isinstance(response, socksio.socks5.SOCKS5Reply) + if response.reply_code != socksio.socks5.SOCKS5ReplyCode.SUCCEEDED: + reply_code = REPLY_CODES.get(response.reply_code, "UNKOWN") + raise ProxyError(f"Proxy Server could not connect: {reply_code}.") + + +class SOCKSProxy(ConnectionPool): # pragma: nocover + """ + A connection pool that sends requests via an HTTP proxy. + """ + + def __init__( + self, + proxy_url: URL | bytes | str, + proxy_auth: tuple[bytes | str, bytes | str] | None = None, + ssl_context: ssl.SSLContext | None = None, + max_connections: int | None = 10, + max_keepalive_connections: int | None = None, + keepalive_expiry: float | None = None, + http1: bool = True, + http2: bool = False, + retries: int = 0, + network_backend: NetworkBackend | None = None, + ) -> None: + """ + A connection pool for making HTTP requests. + + Parameters: + proxy_url: The URL to use when connecting to the proxy server. + For example `"http://127.0.0.1:8080/"`. + ssl_context: An SSL context to use for verifying connections. + If not specified, the default `httpcore.default_ssl_context()` + will be used. + max_connections: The maximum number of concurrent HTTP connections that + the pool should allow. Any attempt to send a request on a pool that + would exceed this amount will block until a connection is available. + max_keepalive_connections: The maximum number of idle HTTP connections + that will be maintained in the pool. + keepalive_expiry: The duration in seconds that an idle HTTP connection + may be maintained for before being expired from the pool. + http1: A boolean indicating if HTTP/1.1 requests should be supported + by the connection pool. Defaults to True. + http2: A boolean indicating if HTTP/2 requests should be supported by + the connection pool. Defaults to False. + retries: The maximum number of retries when trying to establish + a connection. + local_address: Local address to connect from. Can also be used to + connect using a particular address family. Using + `local_address="0.0.0.0"` will connect using an `AF_INET` address + (IPv4), while using `local_address="::"` will connect using an + `AF_INET6` address (IPv6). + uds: Path to a Unix Domain Socket to use instead of TCP sockets. + network_backend: A backend instance to use for handling network I/O. + """ + super().__init__( + ssl_context=ssl_context, + max_connections=max_connections, + max_keepalive_connections=max_keepalive_connections, + keepalive_expiry=keepalive_expiry, + http1=http1, + http2=http2, + network_backend=network_backend, + retries=retries, + ) + self._ssl_context = ssl_context + self._proxy_url = enforce_url(proxy_url, name="proxy_url") + if proxy_auth is not None: + username, password = proxy_auth + username_bytes = enforce_bytes(username, name="proxy_auth") + password_bytes = enforce_bytes(password, name="proxy_auth") + self._proxy_auth: tuple[bytes, bytes] | None = ( + username_bytes, + password_bytes, + ) + else: + self._proxy_auth = None + + def create_connection(self, origin: Origin) -> ConnectionInterface: + return Socks5Connection( + proxy_origin=self._proxy_url.origin, + remote_origin=origin, + proxy_auth=self._proxy_auth, + ssl_context=self._ssl_context, + keepalive_expiry=self._keepalive_expiry, + http1=self._http1, + http2=self._http2, + network_backend=self._network_backend, + ) + + +class Socks5Connection(ConnectionInterface): + def __init__( + self, + proxy_origin: Origin, + remote_origin: Origin, + proxy_auth: tuple[bytes, bytes] | None = None, + ssl_context: ssl.SSLContext | None = None, + keepalive_expiry: float | None = None, + http1: bool = True, + http2: bool = False, + network_backend: NetworkBackend | None = None, + ) -> None: + self._proxy_origin = proxy_origin + self._remote_origin = remote_origin + self._proxy_auth = proxy_auth + self._ssl_context = ssl_context + self._keepalive_expiry = keepalive_expiry + self._http1 = http1 + self._http2 = http2 + + self._network_backend: NetworkBackend = ( + SyncBackend() if network_backend is None else network_backend + ) + self._connect_lock = Lock() + self._connection: ConnectionInterface | None = None + self._connect_failed = False + + def handle_request(self, request: Request) -> Response: + timeouts = request.extensions.get("timeout", {}) + sni_hostname = request.extensions.get("sni_hostname", None) + timeout = timeouts.get("connect", None) + + with self._connect_lock: + if self._connection is None: + try: + # Connect to the proxy + kwargs = { + "host": self._proxy_origin.host.decode("ascii"), + "port": self._proxy_origin.port, + "timeout": timeout, + } + with Trace("connect_tcp", logger, request, kwargs) as trace: + stream = self._network_backend.connect_tcp(**kwargs) + trace.return_value = stream + + # Connect to the remote host using socks5 + kwargs = { + "stream": stream, + "host": self._remote_origin.host.decode("ascii"), + "port": self._remote_origin.port, + "auth": self._proxy_auth, + } + with Trace( + "setup_socks5_connection", logger, request, kwargs + ) as trace: + _init_socks5_connection(**kwargs) + trace.return_value = stream + + # Upgrade the stream to SSL + if self._remote_origin.scheme == b"https": + ssl_context = ( + default_ssl_context() + if self._ssl_context is None + else self._ssl_context + ) + alpn_protocols = ( + ["http/1.1", "h2"] if self._http2 else ["http/1.1"] + ) + ssl_context.set_alpn_protocols(alpn_protocols) + + kwargs = { + "ssl_context": ssl_context, + "server_hostname": sni_hostname + or self._remote_origin.host.decode("ascii"), + "timeout": timeout, + } + with Trace("start_tls", logger, request, kwargs) as trace: + stream = stream.start_tls(**kwargs) + trace.return_value = stream + + # Determine if we should be using HTTP/1.1 or HTTP/2 + ssl_object = stream.get_extra_info("ssl_object") + http2_negotiated = ( + ssl_object is not None + and ssl_object.selected_alpn_protocol() == "h2" + ) + + # Create the HTTP/1.1 or HTTP/2 connection + if http2_negotiated or ( + self._http2 and not self._http1 + ): # pragma: nocover + from .http2 import HTTP2Connection + + self._connection = HTTP2Connection( + origin=self._remote_origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + else: + self._connection = HTTP11Connection( + origin=self._remote_origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + except Exception as exc: + self._connect_failed = True + raise exc + elif not self._connection.is_available(): # pragma: nocover + raise ConnectionNotAvailable() + + return self._connection.handle_request(request) + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._remote_origin + + def close(self) -> None: + if self._connection is not None: + self._connection.close() + + def is_available(self) -> bool: + if self._connection is None: # pragma: nocover + # If HTTP/2 support is enabled, and the resulting connection could + # end up as HTTP/2 then we should indicate the connection as being + # available to service multiple requests. + return ( + self._http2 + and (self._remote_origin.scheme == b"https" or not self._http1) + and not self._connect_failed + ) + return self._connection.is_available() + + def has_expired(self) -> bool: + if self._connection is None: # pragma: nocover + return self._connect_failed + return self._connection.has_expired() + + def is_idle(self) -> bool: + if self._connection is None: # pragma: nocover + return self._connect_failed + return self._connection.is_idle() + + def is_closed(self) -> bool: + if self._connection is None: # pragma: nocover + return self._connect_failed + return self._connection.is_closed() + + def info(self) -> str: + if self._connection is None: # pragma: nocover + return "CONNECTION FAILED" if self._connect_failed else "CONNECTING" + return self._connection.info() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{self.info()}]>" diff --git a/source/target-vendors/cp312/httpcore/_synchronization.py b/source/target-vendors/cp312/httpcore/_synchronization.py new file mode 100644 index 0000000..2ecc9e9 --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_synchronization.py @@ -0,0 +1,318 @@ +from __future__ import annotations + +import threading +import types + +from ._exceptions import ExceptionMapping, PoolTimeout, map_exceptions + +# Our async synchronization primatives use either 'anyio' or 'trio' depending +# on if they're running under asyncio or trio. + +try: + import trio +except (ImportError, NotImplementedError): # pragma: nocover + trio = None # type: ignore + +try: + import anyio +except ImportError: # pragma: nocover + anyio = None # type: ignore + + +def current_async_library() -> str: + # Determine if we're running under trio or asyncio. + # See https://sniffio.readthedocs.io/en/latest/ + try: + import sniffio + except ImportError: # pragma: nocover + environment = "asyncio" + else: + environment = sniffio.current_async_library() + + if environment not in ("asyncio", "trio"): # pragma: nocover + raise RuntimeError("Running under an unsupported async environment.") + + if environment == "asyncio" and anyio is None: # pragma: nocover + raise RuntimeError( + "Running with asyncio requires installation of 'httpcore[asyncio]'." + ) + + if environment == "trio" and trio is None: # pragma: nocover + raise RuntimeError( + "Running with trio requires installation of 'httpcore[trio]'." + ) + + return environment + + +class AsyncLock: + """ + This is a standard lock. + + In the sync case `Lock` provides thread locking. + In the async case `AsyncLock` provides async locking. + """ + + def __init__(self) -> None: + self._backend = "" + + def setup(self) -> None: + """ + Detect if we're running under 'asyncio' or 'trio' and create + a lock with the correct implementation. + """ + self._backend = current_async_library() + if self._backend == "trio": + self._trio_lock = trio.Lock() + elif self._backend == "asyncio": + self._anyio_lock = anyio.Lock() + + async def __aenter__(self) -> AsyncLock: + if not self._backend: + self.setup() + + if self._backend == "trio": + await self._trio_lock.acquire() + elif self._backend == "asyncio": + await self._anyio_lock.acquire() + + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: types.TracebackType | None = None, + ) -> None: + if self._backend == "trio": + self._trio_lock.release() + elif self._backend == "asyncio": + self._anyio_lock.release() + + +class AsyncThreadLock: + """ + This is a threading-only lock for no-I/O contexts. + + In the sync case `ThreadLock` provides thread locking. + In the async case `AsyncThreadLock` is a no-op. + """ + + def __enter__(self) -> AsyncThreadLock: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: types.TracebackType | None = None, + ) -> None: + pass + + +class AsyncEvent: + def __init__(self) -> None: + self._backend = "" + + def setup(self) -> None: + """ + Detect if we're running under 'asyncio' or 'trio' and create + a lock with the correct implementation. + """ + self._backend = current_async_library() + if self._backend == "trio": + self._trio_event = trio.Event() + elif self._backend == "asyncio": + self._anyio_event = anyio.Event() + + def set(self) -> None: + if not self._backend: + self.setup() + + if self._backend == "trio": + self._trio_event.set() + elif self._backend == "asyncio": + self._anyio_event.set() + + async def wait(self, timeout: float | None = None) -> None: + if not self._backend: + self.setup() + + if self._backend == "trio": + trio_exc_map: ExceptionMapping = {trio.TooSlowError: PoolTimeout} + timeout_or_inf = float("inf") if timeout is None else timeout + with map_exceptions(trio_exc_map): + with trio.fail_after(timeout_or_inf): + await self._trio_event.wait() + elif self._backend == "asyncio": + anyio_exc_map: ExceptionMapping = {TimeoutError: PoolTimeout} + with map_exceptions(anyio_exc_map): + with anyio.fail_after(timeout): + await self._anyio_event.wait() + + +class AsyncSemaphore: + def __init__(self, bound: int) -> None: + self._bound = bound + self._backend = "" + + def setup(self) -> None: + """ + Detect if we're running under 'asyncio' or 'trio' and create + a semaphore with the correct implementation. + """ + self._backend = current_async_library() + if self._backend == "trio": + self._trio_semaphore = trio.Semaphore( + initial_value=self._bound, max_value=self._bound + ) + elif self._backend == "asyncio": + self._anyio_semaphore = anyio.Semaphore( + initial_value=self._bound, max_value=self._bound + ) + + async def acquire(self) -> None: + if not self._backend: + self.setup() + + if self._backend == "trio": + await self._trio_semaphore.acquire() + elif self._backend == "asyncio": + await self._anyio_semaphore.acquire() + + async def release(self) -> None: + if self._backend == "trio": + self._trio_semaphore.release() + elif self._backend == "asyncio": + self._anyio_semaphore.release() + + +class AsyncShieldCancellation: + # For certain portions of our codebase where we're dealing with + # closing connections during exception handling we want to shield + # the operation from being cancelled. + # + # with AsyncShieldCancellation(): + # ... # clean-up operations, shielded from cancellation. + + def __init__(self) -> None: + """ + Detect if we're running under 'asyncio' or 'trio' and create + a shielded scope with the correct implementation. + """ + self._backend = current_async_library() + + if self._backend == "trio": + self._trio_shield = trio.CancelScope(shield=True) + elif self._backend == "asyncio": + self._anyio_shield = anyio.CancelScope(shield=True) + + def __enter__(self) -> AsyncShieldCancellation: + if self._backend == "trio": + self._trio_shield.__enter__() + elif self._backend == "asyncio": + self._anyio_shield.__enter__() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: types.TracebackType | None = None, + ) -> None: + if self._backend == "trio": + self._trio_shield.__exit__(exc_type, exc_value, traceback) + elif self._backend == "asyncio": + self._anyio_shield.__exit__(exc_type, exc_value, traceback) + + +# Our thread-based synchronization primitives... + + +class Lock: + """ + This is a standard lock. + + In the sync case `Lock` provides thread locking. + In the async case `AsyncLock` provides async locking. + """ + + def __init__(self) -> None: + self._lock = threading.Lock() + + def __enter__(self) -> Lock: + self._lock.acquire() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: types.TracebackType | None = None, + ) -> None: + self._lock.release() + + +class ThreadLock: + """ + This is a threading-only lock for no-I/O contexts. + + In the sync case `ThreadLock` provides thread locking. + In the async case `AsyncThreadLock` is a no-op. + """ + + def __init__(self) -> None: + self._lock = threading.Lock() + + def __enter__(self) -> ThreadLock: + self._lock.acquire() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: types.TracebackType | None = None, + ) -> None: + self._lock.release() + + +class Event: + def __init__(self) -> None: + self._event = threading.Event() + + def set(self) -> None: + self._event.set() + + def wait(self, timeout: float | None = None) -> None: + if timeout == float("inf"): # pragma: no cover + timeout = None + if not self._event.wait(timeout=timeout): + raise PoolTimeout() # pragma: nocover + + +class Semaphore: + def __init__(self, bound: int) -> None: + self._semaphore = threading.Semaphore(value=bound) + + def acquire(self) -> None: + self._semaphore.acquire() + + def release(self) -> None: + self._semaphore.release() + + +class ShieldCancellation: + # Thread-synchronous codebases don't support cancellation semantics. + # We have this class because we need to mirror the async and sync + # cases within our package, but it's just a no-op. + def __enter__(self) -> ShieldCancellation: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: types.TracebackType | None = None, + ) -> None: + pass diff --git a/source/target-vendors/cp312/httpcore/_trace.py b/source/target-vendors/cp312/httpcore/_trace.py new file mode 100644 index 0000000..5f1cd7c --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_trace.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +import inspect +import logging +import types +import typing + +from ._models import Request + + +class Trace: + def __init__( + self, + name: str, + logger: logging.Logger, + request: Request | None = None, + kwargs: dict[str, typing.Any] | None = None, + ) -> None: + self.name = name + self.logger = logger + self.trace_extension = ( + None if request is None else request.extensions.get("trace") + ) + self.debug = self.logger.isEnabledFor(logging.DEBUG) + self.kwargs = kwargs or {} + self.return_value: typing.Any = None + self.should_trace = self.debug or self.trace_extension is not None + self.prefix = self.logger.name.split(".")[-1] + + def trace(self, name: str, info: dict[str, typing.Any]) -> None: + if self.trace_extension is not None: + prefix_and_name = f"{self.prefix}.{name}" + ret = self.trace_extension(prefix_and_name, info) + if inspect.iscoroutine(ret): # pragma: no cover + raise TypeError( + "If you are using a synchronous interface, " + "the callback of the `trace` extension should " + "be a normal function instead of an asynchronous function." + ) + + if self.debug: + if not info or "return_value" in info and info["return_value"] is None: + message = name + else: + args = " ".join([f"{key}={value!r}" for key, value in info.items()]) + message = f"{name} {args}" + self.logger.debug(message) + + def __enter__(self) -> Trace: + if self.should_trace: + info = self.kwargs + self.trace(f"{self.name}.started", info) + return self + + def __exit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: types.TracebackType | None = None, + ) -> None: + if self.should_trace: + if exc_value is None: + info = {"return_value": self.return_value} + self.trace(f"{self.name}.complete", info) + else: + info = {"exception": exc_value} + self.trace(f"{self.name}.failed", info) + + async def atrace(self, name: str, info: dict[str, typing.Any]) -> None: + if self.trace_extension is not None: + prefix_and_name = f"{self.prefix}.{name}" + coro = self.trace_extension(prefix_and_name, info) + if not inspect.iscoroutine(coro): # pragma: no cover + raise TypeError( + "If you're using an asynchronous interface, " + "the callback of the `trace` extension should " + "be an asynchronous function rather than a normal function." + ) + await coro + + if self.debug: + if not info or "return_value" in info and info["return_value"] is None: + message = name + else: + args = " ".join([f"{key}={value!r}" for key, value in info.items()]) + message = f"{name} {args}" + self.logger.debug(message) + + async def __aenter__(self) -> Trace: + if self.should_trace: + info = self.kwargs + await self.atrace(f"{self.name}.started", info) + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: types.TracebackType | None = None, + ) -> None: + if self.should_trace: + if exc_value is None: + info = {"return_value": self.return_value} + await self.atrace(f"{self.name}.complete", info) + else: + info = {"exception": exc_value} + await self.atrace(f"{self.name}.failed", info) diff --git a/source/target-vendors/cp312/httpcore/_utils.py b/source/target-vendors/cp312/httpcore/_utils.py new file mode 100644 index 0000000..c44ff93 --- /dev/null +++ b/source/target-vendors/cp312/httpcore/_utils.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import select +import socket +import sys + + +def is_socket_readable(sock: socket.socket | None) -> bool: + """ + Return whether a socket, as identifed by its file descriptor, is readable. + "A socket is readable" means that the read buffer isn't empty, i.e. that calling + .recv() on it would immediately return some data. + """ + # NOTE: we want check for readability without actually attempting to read, because + # we don't want to block forever if it's not readable. + + # In the case that the socket no longer exists, or cannot return a file + # descriptor, we treat it as being readable, as if it the next read operation + # on it is ready to return the terminating `b""`. + sock_fd = None if sock is None else sock.fileno() + if sock_fd is None or sock_fd < 0: # pragma: nocover + return True + + # The implementation below was stolen from: + # https://github.com/python-trio/trio/blob/20ee2b1b7376db637435d80e266212a35837ddcc/trio/_socket.py#L471-L478 + # See also: https://github.com/encode/httpcore/pull/193#issuecomment-703129316 + + # Use select.select on Windows, and when poll is unavailable and select.poll + # everywhere else. (E.g. When eventlet is in use. See #327) + if ( + sys.platform == "win32" or getattr(select, "poll", None) is None + ): # pragma: nocover + rready, _, _ = select.select([sock_fd], [], [], 0) + return bool(rready) + p = select.poll() + p.register(sock_fd, select.POLLIN) + return bool(p.poll(0)) diff --git a/source/target-vendors/cp312/httpcore/py.typed b/source/target-vendors/cp312/httpcore/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/httpx-0.28.1.dist-info/METADATA b/source/target-vendors/cp312/httpx-0.28.1.dist-info/METADATA new file mode 100644 index 0000000..b0d2b19 --- /dev/null +++ b/source/target-vendors/cp312/httpx-0.28.1.dist-info/METADATA @@ -0,0 +1,203 @@ +Metadata-Version: 2.3 +Name: httpx +Version: 0.28.1 +Summary: The next generation HTTP client. +Project-URL: Changelog, https://github.com/encode/httpx/blob/master/CHANGELOG.md +Project-URL: Documentation, https://www.python-httpx.org +Project-URL: Homepage, https://github.com/encode/httpx +Project-URL: Source, https://github.com/encode/httpx +Author-email: Tom Christie +License: BSD-3-Clause +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Web Environment +Classifier: Framework :: AsyncIO +Classifier: Framework :: Trio +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Topic :: Internet :: WWW/HTTP +Requires-Python: >=3.8 +Requires-Dist: anyio +Requires-Dist: certifi +Requires-Dist: httpcore==1.* +Requires-Dist: idna +Provides-Extra: brotli +Requires-Dist: brotli; (platform_python_implementation == 'CPython') and extra == 'brotli' +Requires-Dist: brotlicffi; (platform_python_implementation != 'CPython') and extra == 'brotli' +Provides-Extra: cli +Requires-Dist: click==8.*; extra == 'cli' +Requires-Dist: pygments==2.*; extra == 'cli' +Requires-Dist: rich<14,>=10; extra == 'cli' +Provides-Extra: http2 +Requires-Dist: h2<5,>=3; extra == 'http2' +Provides-Extra: socks +Requires-Dist: socksio==1.*; extra == 'socks' +Provides-Extra: zstd +Requires-Dist: zstandard>=0.18.0; extra == 'zstd' +Description-Content-Type: text/markdown + +

+ HTTPX +

+ +

HTTPX - A next-generation HTTP client for Python.

+ +

+ + Test Suite + + + Package version + +

+ +HTTPX is a fully featured HTTP client library for Python 3. It includes **an integrated command line client**, has support for both **HTTP/1.1 and HTTP/2**, and provides both **sync and async APIs**. + +--- + +Install HTTPX using pip: + +```shell +$ pip install httpx +``` + +Now, let's get started: + +```pycon +>>> import httpx +>>> r = httpx.get('https://www.example.org/') +>>> r + +>>> r.status_code +200 +>>> r.headers['content-type'] +'text/html; charset=UTF-8' +>>> r.text +'\n\n\nExample Domain...' +``` + +Or, using the command-line client. + +```shell +$ pip install 'httpx[cli]' # The command line client is an optional dependency. +``` + +Which now allows us to use HTTPX directly from the command-line... + +

+ httpx --help +

+ +Sending a request... + +

+ httpx http://httpbin.org/json +

+ +## Features + +HTTPX builds on the well-established usability of `requests`, and gives you: + +* A broadly [requests-compatible API](https://www.python-httpx.org/compatibility/). +* An integrated command-line client. +* HTTP/1.1 [and HTTP/2 support](https://www.python-httpx.org/http2/). +* Standard synchronous interface, but with [async support if you need it](https://www.python-httpx.org/async/). +* Ability to make requests directly to [WSGI applications](https://www.python-httpx.org/advanced/transports/#wsgi-transport) or [ASGI applications](https://www.python-httpx.org/advanced/transports/#asgi-transport). +* Strict timeouts everywhere. +* Fully type annotated. +* 100% test coverage. + +Plus all the standard features of `requests`... + +* International Domains and URLs +* Keep-Alive & Connection Pooling +* Sessions with Cookie Persistence +* Browser-style SSL Verification +* Basic/Digest Authentication +* Elegant Key/Value Cookies +* Automatic Decompression +* Automatic Content Decoding +* Unicode Response Bodies +* Multipart File Uploads +* HTTP(S) Proxy Support +* Connection Timeouts +* Streaming Downloads +* .netrc Support +* Chunked Requests + +## Installation + +Install with pip: + +```shell +$ pip install httpx +``` + +Or, to include the optional HTTP/2 support, use: + +```shell +$ pip install httpx[http2] +``` + +HTTPX requires Python 3.8+. + +## Documentation + +Project documentation is available at [https://www.python-httpx.org/](https://www.python-httpx.org/). + +For a run-through of all the basics, head over to the [QuickStart](https://www.python-httpx.org/quickstart/). + +For more advanced topics, see the [Advanced Usage](https://www.python-httpx.org/advanced/) section, the [async support](https://www.python-httpx.org/async/) section, or the [HTTP/2](https://www.python-httpx.org/http2/) section. + +The [Developer Interface](https://www.python-httpx.org/api/) provides a comprehensive API reference. + +To find out about tools that integrate with HTTPX, see [Third Party Packages](https://www.python-httpx.org/third_party_packages/). + +## Contribute + +If you want to contribute with HTTPX check out the [Contributing Guide](https://www.python-httpx.org/contributing/) to learn how to start. + +## Dependencies + +The HTTPX project relies on these excellent libraries: + +* `httpcore` - The underlying transport implementation for `httpx`. + * `h11` - HTTP/1.1 support. +* `certifi` - SSL certificates. +* `idna` - Internationalized domain name support. +* `sniffio` - Async library autodetection. + +As well as these optional installs: + +* `h2` - HTTP/2 support. *(Optional, with `httpx[http2]`)* +* `socksio` - SOCKS proxy support. *(Optional, with `httpx[socks]`)* +* `rich` - Rich terminal support. *(Optional, with `httpx[cli]`)* +* `click` - Command line client support. *(Optional, with `httpx[cli]`)* +* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional, with `httpx[brotli]`)* +* `zstandard` - Decoding for "zstd" compressed responses. *(Optional, with `httpx[zstd]`)* + +A huge amount of credit is due to `requests` for the API layout that +much of this work follows, as well as to `urllib3` for plenty of design +inspiration around the lower-level networking details. + +--- + +

HTTPX is BSD licensed code.
Designed & crafted with care.

— 🦋 —

+ +## Release Information + +### Fixed + +* Reintroduced supposedly-private `URLTypes` shortcut. (#2673) + + +--- + +[Full changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md) diff --git a/source/target-vendors/cp312/httpx-0.28.1.dist-info/RECORD b/source/target-vendors/cp312/httpx-0.28.1.dist-info/RECORD new file mode 100644 index 0000000..eecd0e7 --- /dev/null +++ b/source/target-vendors/cp312/httpx-0.28.1.dist-info/RECORD @@ -0,0 +1,29 @@ +httpx/__init__.py,sha256=CsaZe6yZj0rHg6322AWKWHGTMVr9txgEfD5P3_Rrz60,2171 +httpx/__version__.py,sha256=LoUyYeOXTieGzuP_64UL0wxdtxjuu_QbOvE7NOg-IqU,108 +httpx/_api.py,sha256=r_Zgs4jIpcPJLqK5dbbSayqo_iVMKFaxZCd-oOHxLEs,11743 +httpx/_auth.py,sha256=Yr3QwaUSK17rGYx-7j-FdicFIzz4Y9FFV-1F4-7RXX4,11891 +httpx/_client.py,sha256=xD-UG67-WMkeltAAOeGGj-cZ2RRTAm19sWRxlFY7_40,65714 +httpx/_config.py,sha256=pPp2U-wicfcKsF-KYRE1LYdt3e6ERGeIoXZ8Gjo3LWc,8547 +httpx/_content.py,sha256=LGGzrJTR3OvN4Mb1GVVNLXkXJH-6oKlwAttO9p5w_yg,8161 +httpx/_decoders.py,sha256=p0dX8I0NEHexs3UGp4SsZutiMhsXrrWl6-GnqVb0iKM,12041 +httpx/_exceptions.py,sha256=bxW7fxzgVMAdNTbwT0Vnq04gJDW1_gI_GFiQPuMyjL0,8527 +httpx/_main.py,sha256=Cg9GMabiTT_swaDfUgIRitSwxLRMSwUDOm7LdSGqlA4,15626 +httpx/_models.py,sha256=4__Guyv1gLxuZChwim8kfQNiIOcJ9acreFOSurvZfms,44700 +httpx/_multipart.py,sha256=KOHEZZl6oohg9mPaKyyu345qq1rJLg35TUG3YAzXB3Y,9843 +httpx/_status_codes.py,sha256=DYn-2ufBgMeXy5s8x3_TB7wjAuAAMewTakPrm5rXEsc,5639 +httpx/_types.py,sha256=Jyh41GQq7AOev8IOWKDAg7zCbvHAfufmW5g_PiTtErY,2965 +httpx/_urlparse.py,sha256=ZAmH47ONfkxrrj-PPYhGeiHjb6AjKCS-ANWIN4OL_KY,18546 +httpx/_urls.py,sha256=dX99VR1DSOHpgo9Aq7PzYO4FKdxqKjwyNp8grf8dHN0,21550 +httpx/_utils.py,sha256=_TVeqAKvxJkKHdz7dFeb4s0LZqQXgeFkXSgfiHBK_1o,8285 +httpx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +httpx/_transports/__init__.py,sha256=GbUoBSAOp7z-l-9j5YhMhR3DMIcn6FVLhj072O3Nnno,275 +httpx/_transports/asgi.py,sha256=HRfiDYMPt4wQH2gFgHZg4c-i3sblo6bL5GTqcET-xz8,5501 +httpx/_transports/base.py,sha256=kZS_VMbViYfF570pogUCJ1bulz-ybfL51Pqs9yktebU,2523 +httpx/_transports/default.py,sha256=AzeaRUyVwCccTyyNJexDf0n1dFfzzydpdIQgvw7PLnk,13983 +httpx/_transports/mock.py,sha256=PTo0d567RITXxGrki6kN7_67wwAxfwiMDcuXJiZCjEo,1232 +httpx/_transports/wsgi.py,sha256=NcPX3Xap_EwCFZWO_OaSyQNuInCYx1QMNbO8GAei6jY,4825 +httpx-0.28.1.dist-info/METADATA,sha256=_rubD48-gNV8gZnDBPNcQzboWB0dGNeYPJJ2a4J5OyU,7052 +httpx-0.28.1.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87 +httpx-0.28.1.dist-info/entry_points.txt,sha256=2lVkdQmxLA1pNMgSN2eV89o90HCZezhmNwsy6ryKDSA,37 +httpx-0.28.1.dist-info/licenses/LICENSE.md,sha256=TsWdVE8StfU5o6cW_TIaxYzNgDC0ZSIfLIgCAM3yjY0,1508 +httpx-0.28.1.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/httpx-0.28.1.dist-info/WHEEL b/source/target-vendors/cp312/httpx-0.28.1.dist-info/WHEEL new file mode 100644 index 0000000..21aaa72 --- /dev/null +++ b/source/target-vendors/cp312/httpx-0.28.1.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.26.3 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/source/target-vendors/cp312/httpx-0.28.1.dist-info/entry_points.txt b/source/target-vendors/cp312/httpx-0.28.1.dist-info/entry_points.txt new file mode 100644 index 0000000..8ae9600 --- /dev/null +++ b/source/target-vendors/cp312/httpx-0.28.1.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +httpx = httpx:main diff --git a/source/target-vendors/cp312/httpx-0.28.1.dist-info/licenses/LICENSE.md b/source/target-vendors/cp312/httpx-0.28.1.dist-info/licenses/LICENSE.md new file mode 100644 index 0000000..ab79d16 --- /dev/null +++ b/source/target-vendors/cp312/httpx-0.28.1.dist-info/licenses/LICENSE.md @@ -0,0 +1,12 @@ +Copyright © 2019, [Encode OSS Ltd](https://www.encode.io/). +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/source/target-vendors/cp312/httpx/__init__.py b/source/target-vendors/cp312/httpx/__init__.py new file mode 100644 index 0000000..e9addde --- /dev/null +++ b/source/target-vendors/cp312/httpx/__init__.py @@ -0,0 +1,105 @@ +from .__version__ import __description__, __title__, __version__ +from ._api import * +from ._auth import * +from ._client import * +from ._config import * +from ._content import * +from ._exceptions import * +from ._models import * +from ._status_codes import * +from ._transports import * +from ._types import * +from ._urls import * + +try: + from ._main import main +except ImportError: # pragma: no cover + + def main() -> None: # type: ignore + import sys + + print( + "The httpx command line client could not run because the required " + "dependencies were not installed.\nMake sure you've installed " + "everything with: pip install 'httpx[cli]'" + ) + sys.exit(1) + + +__all__ = [ + "__description__", + "__title__", + "__version__", + "ASGITransport", + "AsyncBaseTransport", + "AsyncByteStream", + "AsyncClient", + "AsyncHTTPTransport", + "Auth", + "BaseTransport", + "BasicAuth", + "ByteStream", + "Client", + "CloseError", + "codes", + "ConnectError", + "ConnectTimeout", + "CookieConflict", + "Cookies", + "create_ssl_context", + "DecodingError", + "delete", + "DigestAuth", + "get", + "head", + "Headers", + "HTTPError", + "HTTPStatusError", + "HTTPTransport", + "InvalidURL", + "Limits", + "LocalProtocolError", + "main", + "MockTransport", + "NetRCAuth", + "NetworkError", + "options", + "patch", + "PoolTimeout", + "post", + "ProtocolError", + "Proxy", + "ProxyError", + "put", + "QueryParams", + "ReadError", + "ReadTimeout", + "RemoteProtocolError", + "request", + "Request", + "RequestError", + "RequestNotRead", + "Response", + "ResponseNotRead", + "stream", + "StreamClosed", + "StreamConsumed", + "StreamError", + "SyncByteStream", + "Timeout", + "TimeoutException", + "TooManyRedirects", + "TransportError", + "UnsupportedProtocol", + "URL", + "USE_CLIENT_DEFAULT", + "WriteError", + "WriteTimeout", + "WSGITransport", +] + + +__locals = locals() +for __name in __all__: + if not __name.startswith("__"): + setattr(__locals[__name], "__module__", "httpx") # noqa diff --git a/source/target-vendors/cp312/httpx/__version__.py b/source/target-vendors/cp312/httpx/__version__.py new file mode 100644 index 0000000..801bfac --- /dev/null +++ b/source/target-vendors/cp312/httpx/__version__.py @@ -0,0 +1,3 @@ +__title__ = "httpx" +__description__ = "A next generation HTTP client, for Python 3." +__version__ = "0.28.1" diff --git a/source/target-vendors/cp312/httpx/_api.py b/source/target-vendors/cp312/httpx/_api.py new file mode 100644 index 0000000..c3cda1e --- /dev/null +++ b/source/target-vendors/cp312/httpx/_api.py @@ -0,0 +1,438 @@ +from __future__ import annotations + +import typing +from contextlib import contextmanager + +from ._client import Client +from ._config import DEFAULT_TIMEOUT_CONFIG +from ._models import Response +from ._types import ( + AuthTypes, + CookieTypes, + HeaderTypes, + ProxyTypes, + QueryParamTypes, + RequestContent, + RequestData, + RequestFiles, + TimeoutTypes, +) +from ._urls import URL + +if typing.TYPE_CHECKING: + import ssl # pragma: no cover + + +__all__ = [ + "delete", + "get", + "head", + "options", + "patch", + "post", + "put", + "request", + "stream", +] + + +def request( + method: str, + url: URL | str, + *, + params: QueryParamTypes | None = None, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | None = None, + proxy: ProxyTypes | None = None, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + follow_redirects: bool = False, + verify: ssl.SSLContext | str | bool = True, + trust_env: bool = True, +) -> Response: + """ + Sends an HTTP request. + + **Parameters:** + + * **method** - HTTP method for the new `Request` object: `GET`, `OPTIONS`, + `HEAD`, `POST`, `PUT`, `PATCH`, or `DELETE`. + * **url** - URL for the new `Request` object. + * **params** - *(optional)* Query parameters to include in the URL, as a + string, dictionary, or sequence of two-tuples. + * **content** - *(optional)* Binary content to include in the body of the + request, as bytes or a byte iterator. + * **data** - *(optional)* Form data to include in the body of the request, + as a dictionary. + * **files** - *(optional)* A dictionary of upload files to include in the + body of the request. + * **json** - *(optional)* A JSON serializable object to include in the body + of the request. + * **headers** - *(optional)* Dictionary of HTTP headers to include in the + request. + * **cookies** - *(optional)* Dictionary of Cookie items to include in the + request. + * **auth** - *(optional)* An authentication class to use when sending the + request. + * **proxy** - *(optional)* A proxy URL where all the traffic should be routed. + * **timeout** - *(optional)* The timeout configuration to use when sending + the request. + * **follow_redirects** - *(optional)* Enables or disables HTTP redirects. + * **verify** - *(optional)* Either `True` to use an SSL context with the + default CA bundle, `False` to disable verification, or an instance of + `ssl.SSLContext` to use a custom context. + * **trust_env** - *(optional)* Enables or disables usage of environment + variables for configuration. + + **Returns:** `Response` + + Usage: + + ``` + >>> import httpx + >>> response = httpx.request('GET', 'https://httpbin.org/get') + >>> response + + ``` + """ + with Client( + cookies=cookies, + proxy=proxy, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) as client: + return client.request( + method=method, + url=url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + auth=auth, + follow_redirects=follow_redirects, + ) + + +@contextmanager +def stream( + method: str, + url: URL | str, + *, + params: QueryParamTypes | None = None, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | None = None, + proxy: ProxyTypes | None = None, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + follow_redirects: bool = False, + verify: ssl.SSLContext | str | bool = True, + trust_env: bool = True, +) -> typing.Iterator[Response]: + """ + Alternative to `httpx.request()` that streams the response body + instead of loading it into memory at once. + + **Parameters**: See `httpx.request`. + + See also: [Streaming Responses][0] + + [0]: /quickstart#streaming-responses + """ + with Client( + cookies=cookies, + proxy=proxy, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) as client: + with client.stream( + method=method, + url=url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + auth=auth, + follow_redirects=follow_redirects, + ) as response: + yield response + + +def get( + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | None = None, + proxy: ProxyTypes | None = None, + follow_redirects: bool = False, + verify: ssl.SSLContext | str | bool = True, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + trust_env: bool = True, +) -> Response: + """ + Sends a `GET` request. + + **Parameters**: See `httpx.request`. + + Note that the `data`, `files`, `json` and `content` parameters are not available + on this function, as `GET` requests should not include a request body. + """ + return request( + "GET", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + proxy=proxy, + follow_redirects=follow_redirects, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) + + +def options( + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | None = None, + proxy: ProxyTypes | None = None, + follow_redirects: bool = False, + verify: ssl.SSLContext | str | bool = True, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + trust_env: bool = True, +) -> Response: + """ + Sends an `OPTIONS` request. + + **Parameters**: See `httpx.request`. + + Note that the `data`, `files`, `json` and `content` parameters are not available + on this function, as `OPTIONS` requests should not include a request body. + """ + return request( + "OPTIONS", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + proxy=proxy, + follow_redirects=follow_redirects, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) + + +def head( + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | None = None, + proxy: ProxyTypes | None = None, + follow_redirects: bool = False, + verify: ssl.SSLContext | str | bool = True, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + trust_env: bool = True, +) -> Response: + """ + Sends a `HEAD` request. + + **Parameters**: See `httpx.request`. + + Note that the `data`, `files`, `json` and `content` parameters are not available + on this function, as `HEAD` requests should not include a request body. + """ + return request( + "HEAD", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + proxy=proxy, + follow_redirects=follow_redirects, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) + + +def post( + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | None = None, + proxy: ProxyTypes | None = None, + follow_redirects: bool = False, + verify: ssl.SSLContext | str | bool = True, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + trust_env: bool = True, +) -> Response: + """ + Sends a `POST` request. + + **Parameters**: See `httpx.request`. + """ + return request( + "POST", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + proxy=proxy, + follow_redirects=follow_redirects, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) + + +def put( + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | None = None, + proxy: ProxyTypes | None = None, + follow_redirects: bool = False, + verify: ssl.SSLContext | str | bool = True, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + trust_env: bool = True, +) -> Response: + """ + Sends a `PUT` request. + + **Parameters**: See `httpx.request`. + """ + return request( + "PUT", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + proxy=proxy, + follow_redirects=follow_redirects, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) + + +def patch( + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | None = None, + proxy: ProxyTypes | None = None, + follow_redirects: bool = False, + verify: ssl.SSLContext | str | bool = True, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + trust_env: bool = True, +) -> Response: + """ + Sends a `PATCH` request. + + **Parameters**: See `httpx.request`. + """ + return request( + "PATCH", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + proxy=proxy, + follow_redirects=follow_redirects, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) + + +def delete( + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | None = None, + proxy: ProxyTypes | None = None, + follow_redirects: bool = False, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + verify: ssl.SSLContext | str | bool = True, + trust_env: bool = True, +) -> Response: + """ + Sends a `DELETE` request. + + **Parameters**: See `httpx.request`. + + Note that the `data`, `files`, `json` and `content` parameters are not available + on this function, as `DELETE` requests should not include a request body. + """ + return request( + "DELETE", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + proxy=proxy, + follow_redirects=follow_redirects, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) diff --git a/source/target-vendors/cp312/httpx/_auth.py b/source/target-vendors/cp312/httpx/_auth.py new file mode 100644 index 0000000..b03971a --- /dev/null +++ b/source/target-vendors/cp312/httpx/_auth.py @@ -0,0 +1,348 @@ +from __future__ import annotations + +import hashlib +import os +import re +import time +import typing +from base64 import b64encode +from urllib.request import parse_http_list + +from ._exceptions import ProtocolError +from ._models import Cookies, Request, Response +from ._utils import to_bytes, to_str, unquote + +if typing.TYPE_CHECKING: # pragma: no cover + from hashlib import _Hash + + +__all__ = ["Auth", "BasicAuth", "DigestAuth", "NetRCAuth"] + + +class Auth: + """ + Base class for all authentication schemes. + + To implement a custom authentication scheme, subclass `Auth` and override + the `.auth_flow()` method. + + If the authentication scheme does I/O such as disk access or network calls, or uses + synchronization primitives such as locks, you should override `.sync_auth_flow()` + and/or `.async_auth_flow()` instead of `.auth_flow()` to provide specialized + implementations that will be used by `Client` and `AsyncClient` respectively. + """ + + requires_request_body = False + requires_response_body = False + + def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]: + """ + Execute the authentication flow. + + To dispatch a request, `yield` it: + + ``` + yield request + ``` + + The client will `.send()` the response back into the flow generator. You can + access it like so: + + ``` + response = yield request + ``` + + A `return` (or reaching the end of the generator) will result in the + client returning the last response obtained from the server. + + You can dispatch as many requests as is necessary. + """ + yield request + + def sync_auth_flow( + self, request: Request + ) -> typing.Generator[Request, Response, None]: + """ + Execute the authentication flow synchronously. + + By default, this defers to `.auth_flow()`. You should override this method + when the authentication scheme does I/O and/or uses concurrency primitives. + """ + if self.requires_request_body: + request.read() + + flow = self.auth_flow(request) + request = next(flow) + + while True: + response = yield request + if self.requires_response_body: + response.read() + + try: + request = flow.send(response) + except StopIteration: + break + + async def async_auth_flow( + self, request: Request + ) -> typing.AsyncGenerator[Request, Response]: + """ + Execute the authentication flow asynchronously. + + By default, this defers to `.auth_flow()`. You should override this method + when the authentication scheme does I/O and/or uses concurrency primitives. + """ + if self.requires_request_body: + await request.aread() + + flow = self.auth_flow(request) + request = next(flow) + + while True: + response = yield request + if self.requires_response_body: + await response.aread() + + try: + request = flow.send(response) + except StopIteration: + break + + +class FunctionAuth(Auth): + """ + Allows the 'auth' argument to be passed as a simple callable function, + that takes the request, and returns a new, modified request. + """ + + def __init__(self, func: typing.Callable[[Request], Request]) -> None: + self._func = func + + def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]: + yield self._func(request) + + +class BasicAuth(Auth): + """ + Allows the 'auth' argument to be passed as a (username, password) pair, + and uses HTTP Basic authentication. + """ + + def __init__(self, username: str | bytes, password: str | bytes) -> None: + self._auth_header = self._build_auth_header(username, password) + + def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]: + request.headers["Authorization"] = self._auth_header + yield request + + def _build_auth_header(self, username: str | bytes, password: str | bytes) -> str: + userpass = b":".join((to_bytes(username), to_bytes(password))) + token = b64encode(userpass).decode() + return f"Basic {token}" + + +class NetRCAuth(Auth): + """ + Use a 'netrc' file to lookup basic auth credentials based on the url host. + """ + + def __init__(self, file: str | None = None) -> None: + # Lazily import 'netrc'. + # There's no need for us to load this module unless 'NetRCAuth' is being used. + import netrc + + self._netrc_info = netrc.netrc(file) + + def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]: + auth_info = self._netrc_info.authenticators(request.url.host) + if auth_info is None or not auth_info[2]: + # The netrc file did not have authentication credentials for this host. + yield request + else: + # Build a basic auth header with credentials from the netrc file. + request.headers["Authorization"] = self._build_auth_header( + username=auth_info[0], password=auth_info[2] + ) + yield request + + def _build_auth_header(self, username: str | bytes, password: str | bytes) -> str: + userpass = b":".join((to_bytes(username), to_bytes(password))) + token = b64encode(userpass).decode() + return f"Basic {token}" + + +class DigestAuth(Auth): + _ALGORITHM_TO_HASH_FUNCTION: dict[str, typing.Callable[[bytes], _Hash]] = { + "MD5": hashlib.md5, + "MD5-SESS": hashlib.md5, + "SHA": hashlib.sha1, + "SHA-SESS": hashlib.sha1, + "SHA-256": hashlib.sha256, + "SHA-256-SESS": hashlib.sha256, + "SHA-512": hashlib.sha512, + "SHA-512-SESS": hashlib.sha512, + } + + def __init__(self, username: str | bytes, password: str | bytes) -> None: + self._username = to_bytes(username) + self._password = to_bytes(password) + self._last_challenge: _DigestAuthChallenge | None = None + self._nonce_count = 1 + + def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]: + if self._last_challenge: + request.headers["Authorization"] = self._build_auth_header( + request, self._last_challenge + ) + + response = yield request + + if response.status_code != 401 or "www-authenticate" not in response.headers: + # If the response is not a 401 then we don't + # need to build an authenticated request. + return + + for auth_header in response.headers.get_list("www-authenticate"): + if auth_header.lower().startswith("digest "): + break + else: + # If the response does not include a 'WWW-Authenticate: Digest ...' + # header, then we don't need to build an authenticated request. + return + + self._last_challenge = self._parse_challenge(request, response, auth_header) + self._nonce_count = 1 + + request.headers["Authorization"] = self._build_auth_header( + request, self._last_challenge + ) + if response.cookies: + Cookies(response.cookies).set_cookie_header(request=request) + yield request + + def _parse_challenge( + self, request: Request, response: Response, auth_header: str + ) -> _DigestAuthChallenge: + """ + Returns a challenge from a Digest WWW-Authenticate header. + These take the form of: + `Digest realm="realm@host.com",qop="auth,auth-int",nonce="abc",opaque="xyz"` + """ + scheme, _, fields = auth_header.partition(" ") + + # This method should only ever have been called with a Digest auth header. + assert scheme.lower() == "digest" + + header_dict: dict[str, str] = {} + for field in parse_http_list(fields): + key, value = field.strip().split("=", 1) + header_dict[key] = unquote(value) + + try: + realm = header_dict["realm"].encode() + nonce = header_dict["nonce"].encode() + algorithm = header_dict.get("algorithm", "MD5") + opaque = header_dict["opaque"].encode() if "opaque" in header_dict else None + qop = header_dict["qop"].encode() if "qop" in header_dict else None + return _DigestAuthChallenge( + realm=realm, nonce=nonce, algorithm=algorithm, opaque=opaque, qop=qop + ) + except KeyError as exc: + message = "Malformed Digest WWW-Authenticate header" + raise ProtocolError(message, request=request) from exc + + def _build_auth_header( + self, request: Request, challenge: _DigestAuthChallenge + ) -> str: + hash_func = self._ALGORITHM_TO_HASH_FUNCTION[challenge.algorithm.upper()] + + def digest(data: bytes) -> bytes: + return hash_func(data).hexdigest().encode() + + A1 = b":".join((self._username, challenge.realm, self._password)) + + path = request.url.raw_path + A2 = b":".join((request.method.encode(), path)) + # TODO: implement auth-int + HA2 = digest(A2) + + nc_value = b"%08x" % self._nonce_count + cnonce = self._get_client_nonce(self._nonce_count, challenge.nonce) + self._nonce_count += 1 + + HA1 = digest(A1) + if challenge.algorithm.lower().endswith("-sess"): + HA1 = digest(b":".join((HA1, challenge.nonce, cnonce))) + + qop = self._resolve_qop(challenge.qop, request=request) + if qop is None: + # Following RFC 2069 + digest_data = [HA1, challenge.nonce, HA2] + else: + # Following RFC 2617/7616 + digest_data = [HA1, challenge.nonce, nc_value, cnonce, qop, HA2] + + format_args = { + "username": self._username, + "realm": challenge.realm, + "nonce": challenge.nonce, + "uri": path, + "response": digest(b":".join(digest_data)), + "algorithm": challenge.algorithm.encode(), + } + if challenge.opaque: + format_args["opaque"] = challenge.opaque + if qop: + format_args["qop"] = b"auth" + format_args["nc"] = nc_value + format_args["cnonce"] = cnonce + + return "Digest " + self._get_header_value(format_args) + + def _get_client_nonce(self, nonce_count: int, nonce: bytes) -> bytes: + s = str(nonce_count).encode() + s += nonce + s += time.ctime().encode() + s += os.urandom(8) + + return hashlib.sha1(s).hexdigest()[:16].encode() + + def _get_header_value(self, header_fields: dict[str, bytes]) -> str: + NON_QUOTED_FIELDS = ("algorithm", "qop", "nc") + QUOTED_TEMPLATE = '{}="{}"' + NON_QUOTED_TEMPLATE = "{}={}" + + header_value = "" + for i, (field, value) in enumerate(header_fields.items()): + if i > 0: + header_value += ", " + template = ( + QUOTED_TEMPLATE + if field not in NON_QUOTED_FIELDS + else NON_QUOTED_TEMPLATE + ) + header_value += template.format(field, to_str(value)) + + return header_value + + def _resolve_qop(self, qop: bytes | None, request: Request) -> bytes | None: + if qop is None: + return None + qops = re.split(b", ?", qop) + if b"auth" in qops: + return b"auth" + + if qops == [b"auth-int"]: + raise NotImplementedError("Digest auth-int support is not yet implemented") + + message = f'Unexpected qop value "{qop!r}" in digest auth' + raise ProtocolError(message, request=request) + + +class _DigestAuthChallenge(typing.NamedTuple): + realm: bytes + nonce: bytes + algorithm: str + opaque: bytes | None + qop: bytes | None diff --git a/source/target-vendors/cp312/httpx/_client.py b/source/target-vendors/cp312/httpx/_client.py new file mode 100644 index 0000000..2249231 --- /dev/null +++ b/source/target-vendors/cp312/httpx/_client.py @@ -0,0 +1,2019 @@ +from __future__ import annotations + +import datetime +import enum +import logging +import time +import typing +import warnings +from contextlib import asynccontextmanager, contextmanager +from types import TracebackType + +from .__version__ import __version__ +from ._auth import Auth, BasicAuth, FunctionAuth +from ._config import ( + DEFAULT_LIMITS, + DEFAULT_MAX_REDIRECTS, + DEFAULT_TIMEOUT_CONFIG, + Limits, + Proxy, + Timeout, +) +from ._decoders import SUPPORTED_DECODERS +from ._exceptions import ( + InvalidURL, + RemoteProtocolError, + TooManyRedirects, + request_context, +) +from ._models import Cookies, Headers, Request, Response +from ._status_codes import codes +from ._transports.base import AsyncBaseTransport, BaseTransport +from ._transports.default import AsyncHTTPTransport, HTTPTransport +from ._types import ( + AsyncByteStream, + AuthTypes, + CertTypes, + CookieTypes, + HeaderTypes, + ProxyTypes, + QueryParamTypes, + RequestContent, + RequestData, + RequestExtensions, + RequestFiles, + SyncByteStream, + TimeoutTypes, +) +from ._urls import URL, QueryParams +from ._utils import URLPattern, get_environment_proxies + +if typing.TYPE_CHECKING: + import ssl # pragma: no cover + +__all__ = ["USE_CLIENT_DEFAULT", "AsyncClient", "Client"] + +# The type annotation for @classmethod and context managers here follows PEP 484 +# https://www.python.org/dev/peps/pep-0484/#annotating-instance-and-class-methods +T = typing.TypeVar("T", bound="Client") +U = typing.TypeVar("U", bound="AsyncClient") + + +def _is_https_redirect(url: URL, location: URL) -> bool: + """ + Return 'True' if 'location' is a HTTPS upgrade of 'url' + """ + if url.host != location.host: + return False + + return ( + url.scheme == "http" + and _port_or_default(url) == 80 + and location.scheme == "https" + and _port_or_default(location) == 443 + ) + + +def _port_or_default(url: URL) -> int | None: + if url.port is not None: + return url.port + return {"http": 80, "https": 443}.get(url.scheme) + + +def _same_origin(url: URL, other: URL) -> bool: + """ + Return 'True' if the given URLs share the same origin. + """ + return ( + url.scheme == other.scheme + and url.host == other.host + and _port_or_default(url) == _port_or_default(other) + ) + + +class UseClientDefault: + """ + For some parameters such as `auth=...` and `timeout=...` we need to be able + to indicate the default "unset" state, in a way that is distinctly different + to using `None`. + + The default "unset" state indicates that whatever default is set on the + client should be used. This is different to setting `None`, which + explicitly disables the parameter, possibly overriding a client default. + + For example we use `timeout=USE_CLIENT_DEFAULT` in the `request()` signature. + Omitting the `timeout` parameter will send a request using whatever default + timeout has been configured on the client. Including `timeout=None` will + ensure no timeout is used. + + Note that user code shouldn't need to use the `USE_CLIENT_DEFAULT` constant, + but it is used internally when a parameter is not included. + """ + + +USE_CLIENT_DEFAULT = UseClientDefault() + + +logger = logging.getLogger("httpx") + +USER_AGENT = f"python-httpx/{__version__}" +ACCEPT_ENCODING = ", ".join( + [key for key in SUPPORTED_DECODERS.keys() if key != "identity"] +) + + +class ClientState(enum.Enum): + # UNOPENED: + # The client has been instantiated, but has not been used to send a request, + # or been opened by entering the context of a `with` block. + UNOPENED = 1 + # OPENED: + # The client has either sent a request, or is within a `with` block. + OPENED = 2 + # CLOSED: + # The client has either exited the `with` block, or `close()` has + # been called explicitly. + CLOSED = 3 + + +class BoundSyncStream(SyncByteStream): + """ + A byte stream that is bound to a given response instance, and that + ensures the `response.elapsed` is set once the response is closed. + """ + + def __init__( + self, stream: SyncByteStream, response: Response, start: float + ) -> None: + self._stream = stream + self._response = response + self._start = start + + def __iter__(self) -> typing.Iterator[bytes]: + for chunk in self._stream: + yield chunk + + def close(self) -> None: + elapsed = time.perf_counter() - self._start + self._response.elapsed = datetime.timedelta(seconds=elapsed) + self._stream.close() + + +class BoundAsyncStream(AsyncByteStream): + """ + An async byte stream that is bound to a given response instance, and that + ensures the `response.elapsed` is set once the response is closed. + """ + + def __init__( + self, stream: AsyncByteStream, response: Response, start: float + ) -> None: + self._stream = stream + self._response = response + self._start = start + + async def __aiter__(self) -> typing.AsyncIterator[bytes]: + async for chunk in self._stream: + yield chunk + + async def aclose(self) -> None: + elapsed = time.perf_counter() - self._start + self._response.elapsed = datetime.timedelta(seconds=elapsed) + await self._stream.aclose() + + +EventHook = typing.Callable[..., typing.Any] + + +class BaseClient: + def __init__( + self, + *, + auth: AuthTypes | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + follow_redirects: bool = False, + max_redirects: int = DEFAULT_MAX_REDIRECTS, + event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, + base_url: URL | str = "", + trust_env: bool = True, + default_encoding: str | typing.Callable[[bytes], str] = "utf-8", + ) -> None: + event_hooks = {} if event_hooks is None else event_hooks + + self._base_url = self._enforce_trailing_slash(URL(base_url)) + + self._auth = self._build_auth(auth) + self._params = QueryParams(params) + self.headers = Headers(headers) + self._cookies = Cookies(cookies) + self._timeout = Timeout(timeout) + self.follow_redirects = follow_redirects + self.max_redirects = max_redirects + self._event_hooks = { + "request": list(event_hooks.get("request", [])), + "response": list(event_hooks.get("response", [])), + } + self._trust_env = trust_env + self._default_encoding = default_encoding + self._state = ClientState.UNOPENED + + @property + def is_closed(self) -> bool: + """ + Check if the client being closed + """ + return self._state == ClientState.CLOSED + + @property + def trust_env(self) -> bool: + return self._trust_env + + def _enforce_trailing_slash(self, url: URL) -> URL: + if url.raw_path.endswith(b"/"): + return url + return url.copy_with(raw_path=url.raw_path + b"/") + + def _get_proxy_map( + self, proxy: ProxyTypes | None, allow_env_proxies: bool + ) -> dict[str, Proxy | None]: + if proxy is None: + if allow_env_proxies: + return { + key: None if url is None else Proxy(url=url) + for key, url in get_environment_proxies().items() + } + return {} + else: + proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy + return {"all://": proxy} + + @property + def timeout(self) -> Timeout: + return self._timeout + + @timeout.setter + def timeout(self, timeout: TimeoutTypes) -> None: + self._timeout = Timeout(timeout) + + @property + def event_hooks(self) -> dict[str, list[EventHook]]: + return self._event_hooks + + @event_hooks.setter + def event_hooks(self, event_hooks: dict[str, list[EventHook]]) -> None: + self._event_hooks = { + "request": list(event_hooks.get("request", [])), + "response": list(event_hooks.get("response", [])), + } + + @property + def auth(self) -> Auth | None: + """ + Authentication class used when none is passed at the request-level. + + See also [Authentication][0]. + + [0]: /quickstart/#authentication + """ + return self._auth + + @auth.setter + def auth(self, auth: AuthTypes) -> None: + self._auth = self._build_auth(auth) + + @property + def base_url(self) -> URL: + """ + Base URL to use when sending requests with relative URLs. + """ + return self._base_url + + @base_url.setter + def base_url(self, url: URL | str) -> None: + self._base_url = self._enforce_trailing_slash(URL(url)) + + @property + def headers(self) -> Headers: + """ + HTTP headers to include when sending requests. + """ + return self._headers + + @headers.setter + def headers(self, headers: HeaderTypes) -> None: + client_headers = Headers( + { + b"Accept": b"*/*", + b"Accept-Encoding": ACCEPT_ENCODING.encode("ascii"), + b"Connection": b"keep-alive", + b"User-Agent": USER_AGENT.encode("ascii"), + } + ) + client_headers.update(headers) + self._headers = client_headers + + @property + def cookies(self) -> Cookies: + """ + Cookie values to include when sending requests. + """ + return self._cookies + + @cookies.setter + def cookies(self, cookies: CookieTypes) -> None: + self._cookies = Cookies(cookies) + + @property + def params(self) -> QueryParams: + """ + Query parameters to include in the URL when sending requests. + """ + return self._params + + @params.setter + def params(self, params: QueryParamTypes) -> None: + self._params = QueryParams(params) + + def build_request( + self, + method: str, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Request: + """ + Build and return a request instance. + + * The `params`, `headers` and `cookies` arguments + are merged with any values set on the client. + * The `url` argument is merged with any `base_url` set on the client. + + See also: [Request instances][0] + + [0]: /advanced/clients/#request-instances + """ + url = self._merge_url(url) + headers = self._merge_headers(headers) + cookies = self._merge_cookies(cookies) + params = self._merge_queryparams(params) + extensions = {} if extensions is None else extensions + if "timeout" not in extensions: + timeout = ( + self.timeout + if isinstance(timeout, UseClientDefault) + else Timeout(timeout) + ) + extensions = dict(**extensions, timeout=timeout.as_dict()) + return Request( + method, + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + extensions=extensions, + ) + + def _merge_url(self, url: URL | str) -> URL: + """ + Merge a URL argument together with any 'base_url' on the client, + to create the URL used for the outgoing request. + """ + merge_url = URL(url) + if merge_url.is_relative_url: + # To merge URLs we always append to the base URL. To get this + # behaviour correct we always ensure the base URL ends in a '/' + # separator, and strip any leading '/' from the merge URL. + # + # So, eg... + # + # >>> client = Client(base_url="https://www.example.com/subpath") + # >>> client.base_url + # URL('https://www.example.com/subpath/') + # >>> client.build_request("GET", "/path").url + # URL('https://www.example.com/subpath/path') + merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/") + return self.base_url.copy_with(raw_path=merge_raw_path) + return merge_url + + def _merge_cookies(self, cookies: CookieTypes | None = None) -> CookieTypes | None: + """ + Merge a cookies argument together with any cookies on the client, + to create the cookies used for the outgoing request. + """ + if cookies or self.cookies: + merged_cookies = Cookies(self.cookies) + merged_cookies.update(cookies) + return merged_cookies + return cookies + + def _merge_headers(self, headers: HeaderTypes | None = None) -> HeaderTypes | None: + """ + Merge a headers argument together with any headers on the client, + to create the headers used for the outgoing request. + """ + merged_headers = Headers(self.headers) + merged_headers.update(headers) + return merged_headers + + def _merge_queryparams( + self, params: QueryParamTypes | None = None + ) -> QueryParamTypes | None: + """ + Merge a queryparams argument together with any queryparams on the client, + to create the queryparams used for the outgoing request. + """ + if params or self.params: + merged_queryparams = QueryParams(self.params) + return merged_queryparams.merge(params) + return params + + def _build_auth(self, auth: AuthTypes | None) -> Auth | None: + if auth is None: + return None + elif isinstance(auth, tuple): + return BasicAuth(username=auth[0], password=auth[1]) + elif isinstance(auth, Auth): + return auth + elif callable(auth): + return FunctionAuth(func=auth) + else: + raise TypeError(f'Invalid "auth" argument: {auth!r}') + + def _build_request_auth( + self, + request: Request, + auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, + ) -> Auth: + auth = ( + self._auth if isinstance(auth, UseClientDefault) else self._build_auth(auth) + ) + + if auth is not None: + return auth + + username, password = request.url.username, request.url.password + if username or password: + return BasicAuth(username=username, password=password) + + return Auth() + + def _build_redirect_request(self, request: Request, response: Response) -> Request: + """ + Given a request and a redirect response, return a new request that + should be used to effect the redirect. + """ + method = self._redirect_method(request, response) + url = self._redirect_url(request, response) + headers = self._redirect_headers(request, url, method) + stream = self._redirect_stream(request, method) + cookies = Cookies(self.cookies) + return Request( + method=method, + url=url, + headers=headers, + cookies=cookies, + stream=stream, + extensions=request.extensions, + ) + + def _redirect_method(self, request: Request, response: Response) -> str: + """ + When being redirected we may want to change the method of the request + based on certain specs or browser behavior. + """ + method = request.method + + # https://tools.ietf.org/html/rfc7231#section-6.4.4 + if response.status_code == codes.SEE_OTHER and method != "HEAD": + method = "GET" + + # Do what the browsers do, despite standards... + # Turn 302s into GETs. + if response.status_code == codes.FOUND and method != "HEAD": + method = "GET" + + # If a POST is responded to with a 301, turn it into a GET. + # This bizarre behaviour is explained in 'requests' issue 1704. + if response.status_code == codes.MOVED_PERMANENTLY and method == "POST": + method = "GET" + + return method + + def _redirect_url(self, request: Request, response: Response) -> URL: + """ + Return the URL for the redirect to follow. + """ + location = response.headers["Location"] + + try: + url = URL(location) + except InvalidURL as exc: + raise RemoteProtocolError( + f"Invalid URL in location header: {exc}.", request=request + ) from None + + # Handle malformed 'Location' headers that are "absolute" form, have no host. + # See: https://github.com/encode/httpx/issues/771 + if url.scheme and not url.host: + url = url.copy_with(host=request.url.host) + + # Facilitate relative 'Location' headers, as allowed by RFC 7231. + # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource') + if url.is_relative_url: + url = request.url.join(url) + + # Attach previous fragment if needed (RFC 7231 7.1.2) + if request.url.fragment and not url.fragment: + url = url.copy_with(fragment=request.url.fragment) + + return url + + def _redirect_headers(self, request: Request, url: URL, method: str) -> Headers: + """ + Return the headers that should be used for the redirect request. + """ + headers = Headers(request.headers) + + if not _same_origin(url, request.url): + if not _is_https_redirect(request.url, url): + # Strip Authorization headers when responses are redirected + # away from the origin. (Except for direct HTTP to HTTPS redirects.) + headers.pop("Authorization", None) + + # Update the Host header. + headers["Host"] = url.netloc.decode("ascii") + + if method != request.method and method == "GET": + # If we've switch to a 'GET' request, then strip any headers which + # are only relevant to the request body. + headers.pop("Content-Length", None) + headers.pop("Transfer-Encoding", None) + + # We should use the client cookie store to determine any cookie header, + # rather than whatever was on the original outgoing request. + headers.pop("Cookie", None) + + return headers + + def _redirect_stream( + self, request: Request, method: str + ) -> SyncByteStream | AsyncByteStream | None: + """ + Return the body that should be used for the redirect request. + """ + if method != request.method and method == "GET": + return None + + return request.stream + + def _set_timeout(self, request: Request) -> None: + if "timeout" not in request.extensions: + timeout = ( + self.timeout + if isinstance(self.timeout, UseClientDefault) + else Timeout(self.timeout) + ) + request.extensions = dict(**request.extensions, timeout=timeout.as_dict()) + + +class Client(BaseClient): + """ + An HTTP client, with connection pooling, HTTP/2, redirects, cookie persistence, etc. + + It can be shared between threads. + + Usage: + + ```python + >>> client = httpx.Client() + >>> response = client.get('https://example.org') + ``` + + **Parameters:** + + * **auth** - *(optional)* An authentication class to use when sending + requests. + * **params** - *(optional)* Query parameters to include in request URLs, as + a string, dictionary, or sequence of two-tuples. + * **headers** - *(optional)* Dictionary of HTTP headers to include when + sending requests. + * **cookies** - *(optional)* Dictionary of Cookie items to include when + sending requests. + * **verify** - *(optional)* Either `True` to use an SSL context with the + default CA bundle, `False` to disable verification, or an instance of + `ssl.SSLContext` to use a custom context. + * **http2** - *(optional)* A boolean indicating if HTTP/2 support should be + enabled. Defaults to `False`. + * **proxy** - *(optional)* A proxy URL where all the traffic should be routed. + * **timeout** - *(optional)* The timeout configuration to use when sending + requests. + * **limits** - *(optional)* The limits configuration to use. + * **max_redirects** - *(optional)* The maximum number of redirect responses + that should be followed. + * **base_url** - *(optional)* A URL to use as the base when building + request URLs. + * **transport** - *(optional)* A transport class to use for sending requests + over the network. + * **trust_env** - *(optional)* Enables or disables usage of environment + variables for configuration. + * **default_encoding** - *(optional)* The default encoding to use for decoding + response text, if no charset information is included in a response Content-Type + header. Set to a callable for automatic character set detection. Default: "utf-8". + """ + + def __init__( + self, + *, + auth: AuthTypes | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, + http1: bool = True, + http2: bool = False, + proxy: ProxyTypes | None = None, + mounts: None | (typing.Mapping[str, BaseTransport | None]) = None, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + follow_redirects: bool = False, + limits: Limits = DEFAULT_LIMITS, + max_redirects: int = DEFAULT_MAX_REDIRECTS, + event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, + base_url: URL | str = "", + transport: BaseTransport | None = None, + default_encoding: str | typing.Callable[[bytes], str] = "utf-8", + ) -> None: + super().__init__( + auth=auth, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + follow_redirects=follow_redirects, + max_redirects=max_redirects, + event_hooks=event_hooks, + base_url=base_url, + trust_env=trust_env, + default_encoding=default_encoding, + ) + + if http2: + try: + import h2 # noqa + except ImportError: # pragma: no cover + raise ImportError( + "Using http2=True, but the 'h2' package is not installed. " + "Make sure to install httpx using `pip install httpx[http2]`." + ) from None + + allow_env_proxies = trust_env and transport is None + proxy_map = self._get_proxy_map(proxy, allow_env_proxies) + + self._transport = self._init_transport( + verify=verify, + cert=cert, + trust_env=trust_env, + http1=http1, + http2=http2, + limits=limits, + transport=transport, + ) + self._mounts: dict[URLPattern, BaseTransport | None] = { + URLPattern(key): None + if proxy is None + else self._init_proxy_transport( + proxy, + verify=verify, + cert=cert, + trust_env=trust_env, + http1=http1, + http2=http2, + limits=limits, + ) + for key, proxy in proxy_map.items() + } + if mounts is not None: + self._mounts.update( + {URLPattern(key): transport for key, transport in mounts.items()} + ) + + self._mounts = dict(sorted(self._mounts.items())) + + def _init_transport( + self, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, + http1: bool = True, + http2: bool = False, + limits: Limits = DEFAULT_LIMITS, + transport: BaseTransport | None = None, + ) -> BaseTransport: + if transport is not None: + return transport + + return HTTPTransport( + verify=verify, + cert=cert, + trust_env=trust_env, + http1=http1, + http2=http2, + limits=limits, + ) + + def _init_proxy_transport( + self, + proxy: Proxy, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, + http1: bool = True, + http2: bool = False, + limits: Limits = DEFAULT_LIMITS, + ) -> BaseTransport: + return HTTPTransport( + verify=verify, + cert=cert, + trust_env=trust_env, + http1=http1, + http2=http2, + limits=limits, + proxy=proxy, + ) + + def _transport_for_url(self, url: URL) -> BaseTransport: + """ + Returns the transport instance that should be used for a given URL. + This will either be the standard connection pool, or a proxy. + """ + for pattern, transport in self._mounts.items(): + if pattern.matches(url): + return self._transport if transport is None else transport + + return self._transport + + def request( + self, + method: str, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Build and send a request. + + Equivalent to: + + ```python + request = client.build_request(...) + response = client.send(request, ...) + ``` + + See `Client.build_request()`, `Client.send()` and + [Merging of configuration][0] for how the various parameters + are merged with client-level configuration. + + [0]: /advanced/clients/#merging-of-configuration + """ + if cookies is not None: + message = ( + "Setting per-request cookies=<...> is being deprecated, because " + "the expected behaviour on cookie persistence is ambiguous. Set " + "cookies directly on the client instance instead." + ) + warnings.warn(message, DeprecationWarning, stacklevel=2) + + request = self.build_request( + method=method, + url=url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + extensions=extensions, + ) + return self.send(request, auth=auth, follow_redirects=follow_redirects) + + @contextmanager + def stream( + self, + method: str, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> typing.Iterator[Response]: + """ + Alternative to `httpx.request()` that streams the response body + instead of loading it into memory at once. + + **Parameters**: See `httpx.request`. + + See also: [Streaming Responses][0] + + [0]: /quickstart#streaming-responses + """ + request = self.build_request( + method=method, + url=url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + extensions=extensions, + ) + response = self.send( + request=request, + auth=auth, + follow_redirects=follow_redirects, + stream=True, + ) + try: + yield response + finally: + response.close() + + def send( + self, + request: Request, + *, + stream: bool = False, + auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + ) -> Response: + """ + Send a request. + + The request is sent as-is, unmodified. + + Typically you'll want to build one with `Client.build_request()` + so that any client-level configuration is merged into the request, + but passing an explicit `httpx.Request()` is supported as well. + + See also: [Request instances][0] + + [0]: /advanced/clients/#request-instances + """ + if self._state == ClientState.CLOSED: + raise RuntimeError("Cannot send a request, as the client has been closed.") + + self._state = ClientState.OPENED + follow_redirects = ( + self.follow_redirects + if isinstance(follow_redirects, UseClientDefault) + else follow_redirects + ) + + self._set_timeout(request) + + auth = self._build_request_auth(request, auth) + + response = self._send_handling_auth( + request, + auth=auth, + follow_redirects=follow_redirects, + history=[], + ) + try: + if not stream: + response.read() + + return response + + except BaseException as exc: + response.close() + raise exc + + def _send_handling_auth( + self, + request: Request, + auth: Auth, + follow_redirects: bool, + history: list[Response], + ) -> Response: + auth_flow = auth.sync_auth_flow(request) + try: + request = next(auth_flow) + + while True: + response = self._send_handling_redirects( + request, + follow_redirects=follow_redirects, + history=history, + ) + try: + try: + next_request = auth_flow.send(response) + except StopIteration: + return response + + response.history = list(history) + response.read() + request = next_request + history.append(response) + + except BaseException as exc: + response.close() + raise exc + finally: + auth_flow.close() + + def _send_handling_redirects( + self, + request: Request, + follow_redirects: bool, + history: list[Response], + ) -> Response: + while True: + if len(history) > self.max_redirects: + raise TooManyRedirects( + "Exceeded maximum allowed redirects.", request=request + ) + + for hook in self._event_hooks["request"]: + hook(request) + + response = self._send_single_request(request) + try: + for hook in self._event_hooks["response"]: + hook(response) + response.history = list(history) + + if not response.has_redirect_location: + return response + + request = self._build_redirect_request(request, response) + history = history + [response] + + if follow_redirects: + response.read() + else: + response.next_request = request + return response + + except BaseException as exc: + response.close() + raise exc + + def _send_single_request(self, request: Request) -> Response: + """ + Sends a single request, without handling any redirections. + """ + transport = self._transport_for_url(request.url) + start = time.perf_counter() + + if not isinstance(request.stream, SyncByteStream): + raise RuntimeError( + "Attempted to send an async request with a sync Client instance." + ) + + with request_context(request=request): + response = transport.handle_request(request) + + assert isinstance(response.stream, SyncByteStream) + + response.request = request + response.stream = BoundSyncStream( + response.stream, response=response, start=start + ) + self.cookies.extract_cookies(response) + response.default_encoding = self._default_encoding + + logger.info( + 'HTTP Request: %s %s "%s %d %s"', + request.method, + request.url, + response.http_version, + response.status_code, + response.reason_phrase, + ) + + return response + + def get( + self, + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `GET` request. + + **Parameters**: See `httpx.request`. + """ + return self.request( + "GET", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def options( + self, + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send an `OPTIONS` request. + + **Parameters**: See `httpx.request`. + """ + return self.request( + "OPTIONS", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def head( + self, + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `HEAD` request. + + **Parameters**: See `httpx.request`. + """ + return self.request( + "HEAD", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def post( + self, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `POST` request. + + **Parameters**: See `httpx.request`. + """ + return self.request( + "POST", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def put( + self, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `PUT` request. + + **Parameters**: See `httpx.request`. + """ + return self.request( + "PUT", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def patch( + self, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `PATCH` request. + + **Parameters**: See `httpx.request`. + """ + return self.request( + "PATCH", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def delete( + self, + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `DELETE` request. + + **Parameters**: See `httpx.request`. + """ + return self.request( + "DELETE", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def close(self) -> None: + """ + Close transport and proxies. + """ + if self._state != ClientState.CLOSED: + self._state = ClientState.CLOSED + + self._transport.close() + for transport in self._mounts.values(): + if transport is not None: + transport.close() + + def __enter__(self: T) -> T: + if self._state != ClientState.UNOPENED: + msg = { + ClientState.OPENED: "Cannot open a client instance more than once.", + ClientState.CLOSED: ( + "Cannot reopen a client instance, once it has been closed." + ), + }[self._state] + raise RuntimeError(msg) + + self._state = ClientState.OPENED + + self._transport.__enter__() + for transport in self._mounts.values(): + if transport is not None: + transport.__enter__() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: TracebackType | None = None, + ) -> None: + self._state = ClientState.CLOSED + + self._transport.__exit__(exc_type, exc_value, traceback) + for transport in self._mounts.values(): + if transport is not None: + transport.__exit__(exc_type, exc_value, traceback) + + +class AsyncClient(BaseClient): + """ + An asynchronous HTTP client, with connection pooling, HTTP/2, redirects, + cookie persistence, etc. + + It can be shared between tasks. + + Usage: + + ```python + >>> async with httpx.AsyncClient() as client: + >>> response = await client.get('https://example.org') + ``` + + **Parameters:** + + * **auth** - *(optional)* An authentication class to use when sending + requests. + * **params** - *(optional)* Query parameters to include in request URLs, as + a string, dictionary, or sequence of two-tuples. + * **headers** - *(optional)* Dictionary of HTTP headers to include when + sending requests. + * **cookies** - *(optional)* Dictionary of Cookie items to include when + sending requests. + * **verify** - *(optional)* Either `True` to use an SSL context with the + default CA bundle, `False` to disable verification, or an instance of + `ssl.SSLContext` to use a custom context. + * **http2** - *(optional)* A boolean indicating if HTTP/2 support should be + enabled. Defaults to `False`. + * **proxy** - *(optional)* A proxy URL where all the traffic should be routed. + * **timeout** - *(optional)* The timeout configuration to use when sending + requests. + * **limits** - *(optional)* The limits configuration to use. + * **max_redirects** - *(optional)* The maximum number of redirect responses + that should be followed. + * **base_url** - *(optional)* A URL to use as the base when building + request URLs. + * **transport** - *(optional)* A transport class to use for sending requests + over the network. + * **trust_env** - *(optional)* Enables or disables usage of environment + variables for configuration. + * **default_encoding** - *(optional)* The default encoding to use for decoding + response text, if no charset information is included in a response Content-Type + header. Set to a callable for automatic character set detection. Default: "utf-8". + """ + + def __init__( + self, + *, + auth: AuthTypes | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + http1: bool = True, + http2: bool = False, + proxy: ProxyTypes | None = None, + mounts: None | (typing.Mapping[str, AsyncBaseTransport | None]) = None, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + follow_redirects: bool = False, + limits: Limits = DEFAULT_LIMITS, + max_redirects: int = DEFAULT_MAX_REDIRECTS, + event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, + base_url: URL | str = "", + transport: AsyncBaseTransport | None = None, + trust_env: bool = True, + default_encoding: str | typing.Callable[[bytes], str] = "utf-8", + ) -> None: + super().__init__( + auth=auth, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + follow_redirects=follow_redirects, + max_redirects=max_redirects, + event_hooks=event_hooks, + base_url=base_url, + trust_env=trust_env, + default_encoding=default_encoding, + ) + + if http2: + try: + import h2 # noqa + except ImportError: # pragma: no cover + raise ImportError( + "Using http2=True, but the 'h2' package is not installed. " + "Make sure to install httpx using `pip install httpx[http2]`." + ) from None + + allow_env_proxies = trust_env and transport is None + proxy_map = self._get_proxy_map(proxy, allow_env_proxies) + + self._transport = self._init_transport( + verify=verify, + cert=cert, + trust_env=trust_env, + http1=http1, + http2=http2, + limits=limits, + transport=transport, + ) + + self._mounts: dict[URLPattern, AsyncBaseTransport | None] = { + URLPattern(key): None + if proxy is None + else self._init_proxy_transport( + proxy, + verify=verify, + cert=cert, + trust_env=trust_env, + http1=http1, + http2=http2, + limits=limits, + ) + for key, proxy in proxy_map.items() + } + if mounts is not None: + self._mounts.update( + {URLPattern(key): transport for key, transport in mounts.items()} + ) + self._mounts = dict(sorted(self._mounts.items())) + + def _init_transport( + self, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, + http1: bool = True, + http2: bool = False, + limits: Limits = DEFAULT_LIMITS, + transport: AsyncBaseTransport | None = None, + ) -> AsyncBaseTransport: + if transport is not None: + return transport + + return AsyncHTTPTransport( + verify=verify, + cert=cert, + trust_env=trust_env, + http1=http1, + http2=http2, + limits=limits, + ) + + def _init_proxy_transport( + self, + proxy: Proxy, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, + http1: bool = True, + http2: bool = False, + limits: Limits = DEFAULT_LIMITS, + ) -> AsyncBaseTransport: + return AsyncHTTPTransport( + verify=verify, + cert=cert, + trust_env=trust_env, + http1=http1, + http2=http2, + limits=limits, + proxy=proxy, + ) + + def _transport_for_url(self, url: URL) -> AsyncBaseTransport: + """ + Returns the transport instance that should be used for a given URL. + This will either be the standard connection pool, or a proxy. + """ + for pattern, transport in self._mounts.items(): + if pattern.matches(url): + return self._transport if transport is None else transport + + return self._transport + + async def request( + self, + method: str, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Build and send a request. + + Equivalent to: + + ```python + request = client.build_request(...) + response = await client.send(request, ...) + ``` + + See `AsyncClient.build_request()`, `AsyncClient.send()` + and [Merging of configuration][0] for how the various parameters + are merged with client-level configuration. + + [0]: /advanced/clients/#merging-of-configuration + """ + + if cookies is not None: # pragma: no cover + message = ( + "Setting per-request cookies=<...> is being deprecated, because " + "the expected behaviour on cookie persistence is ambiguous. Set " + "cookies directly on the client instance instead." + ) + warnings.warn(message, DeprecationWarning, stacklevel=2) + + request = self.build_request( + method=method, + url=url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + extensions=extensions, + ) + return await self.send(request, auth=auth, follow_redirects=follow_redirects) + + @asynccontextmanager + async def stream( + self, + method: str, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> typing.AsyncIterator[Response]: + """ + Alternative to `httpx.request()` that streams the response body + instead of loading it into memory at once. + + **Parameters**: See `httpx.request`. + + See also: [Streaming Responses][0] + + [0]: /quickstart#streaming-responses + """ + request = self.build_request( + method=method, + url=url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + extensions=extensions, + ) + response = await self.send( + request=request, + auth=auth, + follow_redirects=follow_redirects, + stream=True, + ) + try: + yield response + finally: + await response.aclose() + + async def send( + self, + request: Request, + *, + stream: bool = False, + auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + ) -> Response: + """ + Send a request. + + The request is sent as-is, unmodified. + + Typically you'll want to build one with `AsyncClient.build_request()` + so that any client-level configuration is merged into the request, + but passing an explicit `httpx.Request()` is supported as well. + + See also: [Request instances][0] + + [0]: /advanced/clients/#request-instances + """ + if self._state == ClientState.CLOSED: + raise RuntimeError("Cannot send a request, as the client has been closed.") + + self._state = ClientState.OPENED + follow_redirects = ( + self.follow_redirects + if isinstance(follow_redirects, UseClientDefault) + else follow_redirects + ) + + self._set_timeout(request) + + auth = self._build_request_auth(request, auth) + + response = await self._send_handling_auth( + request, + auth=auth, + follow_redirects=follow_redirects, + history=[], + ) + try: + if not stream: + await response.aread() + + return response + + except BaseException as exc: + await response.aclose() + raise exc + + async def _send_handling_auth( + self, + request: Request, + auth: Auth, + follow_redirects: bool, + history: list[Response], + ) -> Response: + auth_flow = auth.async_auth_flow(request) + try: + request = await auth_flow.__anext__() + + while True: + response = await self._send_handling_redirects( + request, + follow_redirects=follow_redirects, + history=history, + ) + try: + try: + next_request = await auth_flow.asend(response) + except StopAsyncIteration: + return response + + response.history = list(history) + await response.aread() + request = next_request + history.append(response) + + except BaseException as exc: + await response.aclose() + raise exc + finally: + await auth_flow.aclose() + + async def _send_handling_redirects( + self, + request: Request, + follow_redirects: bool, + history: list[Response], + ) -> Response: + while True: + if len(history) > self.max_redirects: + raise TooManyRedirects( + "Exceeded maximum allowed redirects.", request=request + ) + + for hook in self._event_hooks["request"]: + await hook(request) + + response = await self._send_single_request(request) + try: + for hook in self._event_hooks["response"]: + await hook(response) + + response.history = list(history) + + if not response.has_redirect_location: + return response + + request = self._build_redirect_request(request, response) + history = history + [response] + + if follow_redirects: + await response.aread() + else: + response.next_request = request + return response + + except BaseException as exc: + await response.aclose() + raise exc + + async def _send_single_request(self, request: Request) -> Response: + """ + Sends a single request, without handling any redirections. + """ + transport = self._transport_for_url(request.url) + start = time.perf_counter() + + if not isinstance(request.stream, AsyncByteStream): + raise RuntimeError( + "Attempted to send an sync request with an AsyncClient instance." + ) + + with request_context(request=request): + response = await transport.handle_async_request(request) + + assert isinstance(response.stream, AsyncByteStream) + response.request = request + response.stream = BoundAsyncStream( + response.stream, response=response, start=start + ) + self.cookies.extract_cookies(response) + response.default_encoding = self._default_encoding + + logger.info( + 'HTTP Request: %s %s "%s %d %s"', + request.method, + request.url, + response.http_version, + response.status_code, + response.reason_phrase, + ) + + return response + + async def get( + self, + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `GET` request. + + **Parameters**: See `httpx.request`. + """ + return await self.request( + "GET", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + async def options( + self, + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send an `OPTIONS` request. + + **Parameters**: See `httpx.request`. + """ + return await self.request( + "OPTIONS", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + async def head( + self, + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `HEAD` request. + + **Parameters**: See `httpx.request`. + """ + return await self.request( + "HEAD", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + async def post( + self, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `POST` request. + + **Parameters**: See `httpx.request`. + """ + return await self.request( + "POST", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + async def put( + self, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `PUT` request. + + **Parameters**: See `httpx.request`. + """ + return await self.request( + "PUT", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + async def patch( + self, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `PATCH` request. + + **Parameters**: See `httpx.request`. + """ + return await self.request( + "PATCH", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + async def delete( + self, + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `DELETE` request. + + **Parameters**: See `httpx.request`. + """ + return await self.request( + "DELETE", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + async def aclose(self) -> None: + """ + Close transport and proxies. + """ + if self._state != ClientState.CLOSED: + self._state = ClientState.CLOSED + + await self._transport.aclose() + for proxy in self._mounts.values(): + if proxy is not None: + await proxy.aclose() + + async def __aenter__(self: U) -> U: + if self._state != ClientState.UNOPENED: + msg = { + ClientState.OPENED: "Cannot open a client instance more than once.", + ClientState.CLOSED: ( + "Cannot reopen a client instance, once it has been closed." + ), + }[self._state] + raise RuntimeError(msg) + + self._state = ClientState.OPENED + + await self._transport.__aenter__() + for proxy in self._mounts.values(): + if proxy is not None: + await proxy.__aenter__() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: TracebackType | None = None, + ) -> None: + self._state = ClientState.CLOSED + + await self._transport.__aexit__(exc_type, exc_value, traceback) + for proxy in self._mounts.values(): + if proxy is not None: + await proxy.__aexit__(exc_type, exc_value, traceback) diff --git a/source/target-vendors/cp312/httpx/_config.py b/source/target-vendors/cp312/httpx/_config.py new file mode 100644 index 0000000..467a6c9 --- /dev/null +++ b/source/target-vendors/cp312/httpx/_config.py @@ -0,0 +1,248 @@ +from __future__ import annotations + +import os +import typing + +from ._models import Headers +from ._types import CertTypes, HeaderTypes, TimeoutTypes +from ._urls import URL + +if typing.TYPE_CHECKING: + import ssl # pragma: no cover + +__all__ = ["Limits", "Proxy", "Timeout", "create_ssl_context"] + + +class UnsetType: + pass # pragma: no cover + + +UNSET = UnsetType() + + +def create_ssl_context( + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, +) -> ssl.SSLContext: + import ssl + import warnings + + import certifi + + if verify is True: + if trust_env and os.environ.get("SSL_CERT_FILE"): # pragma: nocover + ctx = ssl.create_default_context(cafile=os.environ["SSL_CERT_FILE"]) + elif trust_env and os.environ.get("SSL_CERT_DIR"): # pragma: nocover + ctx = ssl.create_default_context(capath=os.environ["SSL_CERT_DIR"]) + else: + # Default case... + ctx = ssl.create_default_context(cafile=certifi.where()) + elif verify is False: + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + elif isinstance(verify, str): # pragma: nocover + message = ( + "`verify=` is deprecated. " + "Use `verify=ssl.create_default_context(cafile=...)` " + "or `verify=ssl.create_default_context(capath=...)` instead." + ) + warnings.warn(message, DeprecationWarning) + if os.path.isdir(verify): + return ssl.create_default_context(capath=verify) + return ssl.create_default_context(cafile=verify) + else: + ctx = verify + + if cert: # pragma: nocover + message = ( + "`cert=...` is deprecated. Use `verify=` instead," + "with `.load_cert_chain()` to configure the certificate chain." + ) + warnings.warn(message, DeprecationWarning) + if isinstance(cert, str): + ctx.load_cert_chain(cert) + else: + ctx.load_cert_chain(*cert) + + return ctx + + +class Timeout: + """ + Timeout configuration. + + **Usage**: + + Timeout(None) # No timeouts. + Timeout(5.0) # 5s timeout on all operations. + Timeout(None, connect=5.0) # 5s timeout on connect, no other timeouts. + Timeout(5.0, connect=10.0) # 10s timeout on connect. 5s timeout elsewhere. + Timeout(5.0, pool=None) # No timeout on acquiring connection from pool. + # 5s timeout elsewhere. + """ + + def __init__( + self, + timeout: TimeoutTypes | UnsetType = UNSET, + *, + connect: None | float | UnsetType = UNSET, + read: None | float | UnsetType = UNSET, + write: None | float | UnsetType = UNSET, + pool: None | float | UnsetType = UNSET, + ) -> None: + if isinstance(timeout, Timeout): + # Passed as a single explicit Timeout. + assert connect is UNSET + assert read is UNSET + assert write is UNSET + assert pool is UNSET + self.connect = timeout.connect # type: typing.Optional[float] + self.read = timeout.read # type: typing.Optional[float] + self.write = timeout.write # type: typing.Optional[float] + self.pool = timeout.pool # type: typing.Optional[float] + elif isinstance(timeout, tuple): + # Passed as a tuple. + self.connect = timeout[0] + self.read = timeout[1] + self.write = None if len(timeout) < 3 else timeout[2] + self.pool = None if len(timeout) < 4 else timeout[3] + elif not ( + isinstance(connect, UnsetType) + or isinstance(read, UnsetType) + or isinstance(write, UnsetType) + or isinstance(pool, UnsetType) + ): + self.connect = connect + self.read = read + self.write = write + self.pool = pool + else: + if isinstance(timeout, UnsetType): + raise ValueError( + "httpx.Timeout must either include a default, or set all " + "four parameters explicitly." + ) + self.connect = timeout if isinstance(connect, UnsetType) else connect + self.read = timeout if isinstance(read, UnsetType) else read + self.write = timeout if isinstance(write, UnsetType) else write + self.pool = timeout if isinstance(pool, UnsetType) else pool + + def as_dict(self) -> dict[str, float | None]: + return { + "connect": self.connect, + "read": self.read, + "write": self.write, + "pool": self.pool, + } + + def __eq__(self, other: typing.Any) -> bool: + return ( + isinstance(other, self.__class__) + and self.connect == other.connect + and self.read == other.read + and self.write == other.write + and self.pool == other.pool + ) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + if len({self.connect, self.read, self.write, self.pool}) == 1: + return f"{class_name}(timeout={self.connect})" + return ( + f"{class_name}(connect={self.connect}, " + f"read={self.read}, write={self.write}, pool={self.pool})" + ) + + +class Limits: + """ + Configuration for limits to various client behaviors. + + **Parameters:** + + * **max_connections** - The maximum number of concurrent connections that may be + established. + * **max_keepalive_connections** - Allow the connection pool to maintain + keep-alive connections below this point. Should be less than or equal + to `max_connections`. + * **keepalive_expiry** - Time limit on idle keep-alive connections in seconds. + """ + + def __init__( + self, + *, + max_connections: int | None = None, + max_keepalive_connections: int | None = None, + keepalive_expiry: float | None = 5.0, + ) -> None: + self.max_connections = max_connections + self.max_keepalive_connections = max_keepalive_connections + self.keepalive_expiry = keepalive_expiry + + def __eq__(self, other: typing.Any) -> bool: + return ( + isinstance(other, self.__class__) + and self.max_connections == other.max_connections + and self.max_keepalive_connections == other.max_keepalive_connections + and self.keepalive_expiry == other.keepalive_expiry + ) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + return ( + f"{class_name}(max_connections={self.max_connections}, " + f"max_keepalive_connections={self.max_keepalive_connections}, " + f"keepalive_expiry={self.keepalive_expiry})" + ) + + +class Proxy: + def __init__( + self, + url: URL | str, + *, + ssl_context: ssl.SSLContext | None = None, + auth: tuple[str, str] | None = None, + headers: HeaderTypes | None = None, + ) -> None: + url = URL(url) + headers = Headers(headers) + + if url.scheme not in ("http", "https", "socks5", "socks5h"): + raise ValueError(f"Unknown scheme for proxy URL {url!r}") + + if url.username or url.password: + # Remove any auth credentials from the URL. + auth = (url.username, url.password) + url = url.copy_with(username=None, password=None) + + self.url = url + self.auth = auth + self.headers = headers + self.ssl_context = ssl_context + + @property + def raw_auth(self) -> tuple[bytes, bytes] | None: + # The proxy authentication as raw bytes. + return ( + None + if self.auth is None + else (self.auth[0].encode("utf-8"), self.auth[1].encode("utf-8")) + ) + + def __repr__(self) -> str: + # The authentication is represented with the password component masked. + auth = (self.auth[0], "********") if self.auth else None + + # Build a nice concise representation. + url_str = f"{str(self.url)!r}" + auth_str = f", auth={auth!r}" if auth else "" + headers_str = f", headers={dict(self.headers)!r}" if self.headers else "" + return f"Proxy({url_str}{auth_str}{headers_str})" + + +DEFAULT_TIMEOUT_CONFIG = Timeout(timeout=5.0) +DEFAULT_LIMITS = Limits(max_connections=100, max_keepalive_connections=20) +DEFAULT_MAX_REDIRECTS = 20 diff --git a/source/target-vendors/cp312/httpx/_content.py b/source/target-vendors/cp312/httpx/_content.py new file mode 100644 index 0000000..6f479a0 --- /dev/null +++ b/source/target-vendors/cp312/httpx/_content.py @@ -0,0 +1,240 @@ +from __future__ import annotations + +import inspect +import warnings +from json import dumps as json_dumps +from typing import ( + Any, + AsyncIterable, + AsyncIterator, + Iterable, + Iterator, + Mapping, +) +from urllib.parse import urlencode + +from ._exceptions import StreamClosed, StreamConsumed +from ._multipart import MultipartStream +from ._types import ( + AsyncByteStream, + RequestContent, + RequestData, + RequestFiles, + ResponseContent, + SyncByteStream, +) +from ._utils import peek_filelike_length, primitive_value_to_str + +__all__ = ["ByteStream"] + + +class ByteStream(AsyncByteStream, SyncByteStream): + def __init__(self, stream: bytes) -> None: + self._stream = stream + + def __iter__(self) -> Iterator[bytes]: + yield self._stream + + async def __aiter__(self) -> AsyncIterator[bytes]: + yield self._stream + + +class IteratorByteStream(SyncByteStream): + CHUNK_SIZE = 65_536 + + def __init__(self, stream: Iterable[bytes]) -> None: + self._stream = stream + self._is_stream_consumed = False + self._is_generator = inspect.isgenerator(stream) + + def __iter__(self) -> Iterator[bytes]: + if self._is_stream_consumed and self._is_generator: + raise StreamConsumed() + + self._is_stream_consumed = True + if hasattr(self._stream, "read"): + # File-like interfaces should use 'read' directly. + chunk = self._stream.read(self.CHUNK_SIZE) + while chunk: + yield chunk + chunk = self._stream.read(self.CHUNK_SIZE) + else: + # Otherwise iterate. + for part in self._stream: + yield part + + +class AsyncIteratorByteStream(AsyncByteStream): + CHUNK_SIZE = 65_536 + + def __init__(self, stream: AsyncIterable[bytes]) -> None: + self._stream = stream + self._is_stream_consumed = False + self._is_generator = inspect.isasyncgen(stream) + + async def __aiter__(self) -> AsyncIterator[bytes]: + if self._is_stream_consumed and self._is_generator: + raise StreamConsumed() + + self._is_stream_consumed = True + if hasattr(self._stream, "aread"): + # File-like interfaces should use 'aread' directly. + chunk = await self._stream.aread(self.CHUNK_SIZE) + while chunk: + yield chunk + chunk = await self._stream.aread(self.CHUNK_SIZE) + else: + # Otherwise iterate. + async for part in self._stream: + yield part + + +class UnattachedStream(AsyncByteStream, SyncByteStream): + """ + If a request or response is serialized using pickle, then it is no longer + attached to a stream for I/O purposes. Any stream operations should result + in `httpx.StreamClosed`. + """ + + def __iter__(self) -> Iterator[bytes]: + raise StreamClosed() + + async def __aiter__(self) -> AsyncIterator[bytes]: + raise StreamClosed() + yield b"" # pragma: no cover + + +def encode_content( + content: str | bytes | Iterable[bytes] | AsyncIterable[bytes], +) -> tuple[dict[str, str], SyncByteStream | AsyncByteStream]: + if isinstance(content, (bytes, str)): + body = content.encode("utf-8") if isinstance(content, str) else content + content_length = len(body) + headers = {"Content-Length": str(content_length)} if body else {} + return headers, ByteStream(body) + + elif isinstance(content, Iterable) and not isinstance(content, dict): + # `not isinstance(content, dict)` is a bit oddly specific, but it + # catches a case that's easy for users to make in error, and would + # otherwise pass through here, like any other bytes-iterable, + # because `dict` happens to be iterable. See issue #2491. + content_length_or_none = peek_filelike_length(content) + + if content_length_or_none is None: + headers = {"Transfer-Encoding": "chunked"} + else: + headers = {"Content-Length": str(content_length_or_none)} + return headers, IteratorByteStream(content) # type: ignore + + elif isinstance(content, AsyncIterable): + headers = {"Transfer-Encoding": "chunked"} + return headers, AsyncIteratorByteStream(content) + + raise TypeError(f"Unexpected type for 'content', {type(content)!r}") + + +def encode_urlencoded_data( + data: RequestData, +) -> tuple[dict[str, str], ByteStream]: + plain_data = [] + for key, value in data.items(): + if isinstance(value, (list, tuple)): + plain_data.extend([(key, primitive_value_to_str(item)) for item in value]) + else: + plain_data.append((key, primitive_value_to_str(value))) + body = urlencode(plain_data, doseq=True).encode("utf-8") + content_length = str(len(body)) + content_type = "application/x-www-form-urlencoded" + headers = {"Content-Length": content_length, "Content-Type": content_type} + return headers, ByteStream(body) + + +def encode_multipart_data( + data: RequestData, files: RequestFiles, boundary: bytes | None +) -> tuple[dict[str, str], MultipartStream]: + multipart = MultipartStream(data=data, files=files, boundary=boundary) + headers = multipart.get_headers() + return headers, multipart + + +def encode_text(text: str) -> tuple[dict[str, str], ByteStream]: + body = text.encode("utf-8") + content_length = str(len(body)) + content_type = "text/plain; charset=utf-8" + headers = {"Content-Length": content_length, "Content-Type": content_type} + return headers, ByteStream(body) + + +def encode_html(html: str) -> tuple[dict[str, str], ByteStream]: + body = html.encode("utf-8") + content_length = str(len(body)) + content_type = "text/html; charset=utf-8" + headers = {"Content-Length": content_length, "Content-Type": content_type} + return headers, ByteStream(body) + + +def encode_json(json: Any) -> tuple[dict[str, str], ByteStream]: + body = json_dumps( + json, ensure_ascii=False, separators=(",", ":"), allow_nan=False + ).encode("utf-8") + content_length = str(len(body)) + content_type = "application/json" + headers = {"Content-Length": content_length, "Content-Type": content_type} + return headers, ByteStream(body) + + +def encode_request( + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: Any | None = None, + boundary: bytes | None = None, +) -> tuple[dict[str, str], SyncByteStream | AsyncByteStream]: + """ + Handles encoding the given `content`, `data`, `files`, and `json`, + returning a two-tuple of (, ). + """ + if data is not None and not isinstance(data, Mapping): + # We prefer to separate `content=` + # for raw request content, and `data=
` for url encoded or + # multipart form content. + # + # However for compat with requests, we *do* still support + # `data=` usages. We deal with that case here, treating it + # as if `content=<...>` had been supplied instead. + message = "Use 'content=<...>' to upload raw bytes/text content." + warnings.warn(message, DeprecationWarning, stacklevel=2) + return encode_content(data) + + if content is not None: + return encode_content(content) + elif files: + return encode_multipart_data(data or {}, files, boundary) + elif data: + return encode_urlencoded_data(data) + elif json is not None: + return encode_json(json) + + return {}, ByteStream(b"") + + +def encode_response( + content: ResponseContent | None = None, + text: str | None = None, + html: str | None = None, + json: Any | None = None, +) -> tuple[dict[str, str], SyncByteStream | AsyncByteStream]: + """ + Handles encoding the given `content`, returning a two-tuple of + (, ). + """ + if content is not None: + return encode_content(content) + elif text is not None: + return encode_text(text) + elif html is not None: + return encode_html(html) + elif json is not None: + return encode_json(json) + + return {}, ByteStream(b"") diff --git a/source/target-vendors/cp312/httpx/_decoders.py b/source/target-vendors/cp312/httpx/_decoders.py new file mode 100644 index 0000000..899dfad --- /dev/null +++ b/source/target-vendors/cp312/httpx/_decoders.py @@ -0,0 +1,393 @@ +""" +Handlers for Content-Encoding. + +See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding +""" + +from __future__ import annotations + +import codecs +import io +import typing +import zlib + +from ._exceptions import DecodingError + +# Brotli support is optional +try: + # The C bindings in `brotli` are recommended for CPython. + import brotli +except ImportError: # pragma: no cover + try: + # The CFFI bindings in `brotlicffi` are recommended for PyPy + # and other environments. + import brotlicffi as brotli + except ImportError: + brotli = None + + +# Zstandard support is optional +try: + import zstandard +except ImportError: # pragma: no cover + zstandard = None # type: ignore + + +class ContentDecoder: + def decode(self, data: bytes) -> bytes: + raise NotImplementedError() # pragma: no cover + + def flush(self) -> bytes: + raise NotImplementedError() # pragma: no cover + + +class IdentityDecoder(ContentDecoder): + """ + Handle unencoded data. + """ + + def decode(self, data: bytes) -> bytes: + return data + + def flush(self) -> bytes: + return b"" + + +class DeflateDecoder(ContentDecoder): + """ + Handle 'deflate' decoding. + + See: https://stackoverflow.com/questions/1838699 + """ + + def __init__(self) -> None: + self.first_attempt = True + self.decompressor = zlib.decompressobj() + + def decode(self, data: bytes) -> bytes: + was_first_attempt = self.first_attempt + self.first_attempt = False + try: + return self.decompressor.decompress(data) + except zlib.error as exc: + if was_first_attempt: + self.decompressor = zlib.decompressobj(-zlib.MAX_WBITS) + return self.decode(data) + raise DecodingError(str(exc)) from exc + + def flush(self) -> bytes: + try: + return self.decompressor.flush() + except zlib.error as exc: # pragma: no cover + raise DecodingError(str(exc)) from exc + + +class GZipDecoder(ContentDecoder): + """ + Handle 'gzip' decoding. + + See: https://stackoverflow.com/questions/1838699 + """ + + def __init__(self) -> None: + self.decompressor = zlib.decompressobj(zlib.MAX_WBITS | 16) + + def decode(self, data: bytes) -> bytes: + try: + return self.decompressor.decompress(data) + except zlib.error as exc: + raise DecodingError(str(exc)) from exc + + def flush(self) -> bytes: + try: + return self.decompressor.flush() + except zlib.error as exc: # pragma: no cover + raise DecodingError(str(exc)) from exc + + +class BrotliDecoder(ContentDecoder): + """ + Handle 'brotli' decoding. + + Requires `pip install brotlipy`. See: https://brotlipy.readthedocs.io/ + or `pip install brotli`. See https://github.com/google/brotli + Supports both 'brotlipy' and 'Brotli' packages since they share an import + name. The top branches are for 'brotlipy' and bottom branches for 'Brotli' + """ + + def __init__(self) -> None: + if brotli is None: # pragma: no cover + raise ImportError( + "Using 'BrotliDecoder', but neither of the 'brotlicffi' or 'brotli' " + "packages have been installed. " + "Make sure to install httpx using `pip install httpx[brotli]`." + ) from None + + self.decompressor = brotli.Decompressor() + self.seen_data = False + self._decompress: typing.Callable[[bytes], bytes] + if hasattr(self.decompressor, "decompress"): + # The 'brotlicffi' package. + self._decompress = self.decompressor.decompress # pragma: no cover + else: + # The 'brotli' package. + self._decompress = self.decompressor.process # pragma: no cover + + def decode(self, data: bytes) -> bytes: + if not data: + return b"" + self.seen_data = True + try: + return self._decompress(data) + except brotli.error as exc: + raise DecodingError(str(exc)) from exc + + def flush(self) -> bytes: + if not self.seen_data: + return b"" + try: + if hasattr(self.decompressor, "finish"): + # Only available in the 'brotlicffi' package. + + # As the decompressor decompresses eagerly, this + # will never actually emit any data. However, it will potentially throw + # errors if a truncated or damaged data stream has been used. + self.decompressor.finish() # pragma: no cover + return b"" + except brotli.error as exc: # pragma: no cover + raise DecodingError(str(exc)) from exc + + +class ZStandardDecoder(ContentDecoder): + """ + Handle 'zstd' RFC 8878 decoding. + + Requires `pip install zstandard`. + Can be installed as a dependency of httpx using `pip install httpx[zstd]`. + """ + + # inspired by the ZstdDecoder implementation in urllib3 + def __init__(self) -> None: + if zstandard is None: # pragma: no cover + raise ImportError( + "Using 'ZStandardDecoder', ..." + "Make sure to install httpx using `pip install httpx[zstd]`." + ) from None + + self.decompressor = zstandard.ZstdDecompressor().decompressobj() + self.seen_data = False + + def decode(self, data: bytes) -> bytes: + assert zstandard is not None + self.seen_data = True + output = io.BytesIO() + try: + output.write(self.decompressor.decompress(data)) + while self.decompressor.eof and self.decompressor.unused_data: + unused_data = self.decompressor.unused_data + self.decompressor = zstandard.ZstdDecompressor().decompressobj() + output.write(self.decompressor.decompress(unused_data)) + except zstandard.ZstdError as exc: + raise DecodingError(str(exc)) from exc + return output.getvalue() + + def flush(self) -> bytes: + if not self.seen_data: + return b"" + ret = self.decompressor.flush() # note: this is a no-op + if not self.decompressor.eof: + raise DecodingError("Zstandard data is incomplete") # pragma: no cover + return bytes(ret) + + +class MultiDecoder(ContentDecoder): + """ + Handle the case where multiple encodings have been applied. + """ + + def __init__(self, children: typing.Sequence[ContentDecoder]) -> None: + """ + 'children' should be a sequence of decoders in the order in which + each was applied. + """ + # Note that we reverse the order for decoding. + self.children = list(reversed(children)) + + def decode(self, data: bytes) -> bytes: + for child in self.children: + data = child.decode(data) + return data + + def flush(self) -> bytes: + data = b"" + for child in self.children: + data = child.decode(data) + child.flush() + return data + + +class ByteChunker: + """ + Handles returning byte content in fixed-size chunks. + """ + + def __init__(self, chunk_size: int | None = None) -> None: + self._buffer = io.BytesIO() + self._chunk_size = chunk_size + + def decode(self, content: bytes) -> list[bytes]: + if self._chunk_size is None: + return [content] if content else [] + + self._buffer.write(content) + if self._buffer.tell() >= self._chunk_size: + value = self._buffer.getvalue() + chunks = [ + value[i : i + self._chunk_size] + for i in range(0, len(value), self._chunk_size) + ] + if len(chunks[-1]) == self._chunk_size: + self._buffer.seek(0) + self._buffer.truncate() + return chunks + else: + self._buffer.seek(0) + self._buffer.write(chunks[-1]) + self._buffer.truncate() + return chunks[:-1] + else: + return [] + + def flush(self) -> list[bytes]: + value = self._buffer.getvalue() + self._buffer.seek(0) + self._buffer.truncate() + return [value] if value else [] + + +class TextChunker: + """ + Handles returning text content in fixed-size chunks. + """ + + def __init__(self, chunk_size: int | None = None) -> None: + self._buffer = io.StringIO() + self._chunk_size = chunk_size + + def decode(self, content: str) -> list[str]: + if self._chunk_size is None: + return [content] if content else [] + + self._buffer.write(content) + if self._buffer.tell() >= self._chunk_size: + value = self._buffer.getvalue() + chunks = [ + value[i : i + self._chunk_size] + for i in range(0, len(value), self._chunk_size) + ] + if len(chunks[-1]) == self._chunk_size: + self._buffer.seek(0) + self._buffer.truncate() + return chunks + else: + self._buffer.seek(0) + self._buffer.write(chunks[-1]) + self._buffer.truncate() + return chunks[:-1] + else: + return [] + + def flush(self) -> list[str]: + value = self._buffer.getvalue() + self._buffer.seek(0) + self._buffer.truncate() + return [value] if value else [] + + +class TextDecoder: + """ + Handles incrementally decoding bytes into text + """ + + def __init__(self, encoding: str = "utf-8") -> None: + self.decoder = codecs.getincrementaldecoder(encoding)(errors="replace") + + def decode(self, data: bytes) -> str: + return self.decoder.decode(data) + + def flush(self) -> str: + return self.decoder.decode(b"", True) + + +class LineDecoder: + """ + Handles incrementally reading lines from text. + + Has the same behaviour as the stdllib splitlines, + but handling the input iteratively. + """ + + def __init__(self) -> None: + self.buffer: list[str] = [] + self.trailing_cr: bool = False + + def decode(self, text: str) -> list[str]: + # See https://docs.python.org/3/library/stdtypes.html#str.splitlines + NEWLINE_CHARS = "\n\r\x0b\x0c\x1c\x1d\x1e\x85\u2028\u2029" + + # We always push a trailing `\r` into the next decode iteration. + if self.trailing_cr: + text = "\r" + text + self.trailing_cr = False + if text.endswith("\r"): + self.trailing_cr = True + text = text[:-1] + + if not text: + # NOTE: the edge case input of empty text doesn't occur in practice, + # because other httpx internals filter out this value + return [] # pragma: no cover + + trailing_newline = text[-1] in NEWLINE_CHARS + lines = text.splitlines() + + if len(lines) == 1 and not trailing_newline: + # No new lines, buffer the input and continue. + self.buffer.append(lines[0]) + return [] + + if self.buffer: + # Include any existing buffer in the first portion of the + # splitlines result. + lines = ["".join(self.buffer) + lines[0]] + lines[1:] + self.buffer = [] + + if not trailing_newline: + # If the last segment of splitlines is not newline terminated, + # then drop it from our output and start a new buffer. + self.buffer = [lines.pop()] + + return lines + + def flush(self) -> list[str]: + if not self.buffer and not self.trailing_cr: + return [] + + lines = ["".join(self.buffer)] + self.buffer = [] + self.trailing_cr = False + return lines + + +SUPPORTED_DECODERS = { + "identity": IdentityDecoder, + "gzip": GZipDecoder, + "deflate": DeflateDecoder, + "br": BrotliDecoder, + "zstd": ZStandardDecoder, +} + + +if brotli is None: + SUPPORTED_DECODERS.pop("br") # pragma: no cover +if zstandard is None: + SUPPORTED_DECODERS.pop("zstd") # pragma: no cover diff --git a/source/target-vendors/cp312/httpx/_exceptions.py b/source/target-vendors/cp312/httpx/_exceptions.py new file mode 100644 index 0000000..77f45a6 --- /dev/null +++ b/source/target-vendors/cp312/httpx/_exceptions.py @@ -0,0 +1,379 @@ +""" +Our exception hierarchy: + +* HTTPError + x RequestError + + TransportError + - TimeoutException + · ConnectTimeout + · ReadTimeout + · WriteTimeout + · PoolTimeout + - NetworkError + · ConnectError + · ReadError + · WriteError + · CloseError + - ProtocolError + · LocalProtocolError + · RemoteProtocolError + - ProxyError + - UnsupportedProtocol + + DecodingError + + TooManyRedirects + x HTTPStatusError +* InvalidURL +* CookieConflict +* StreamError + x StreamConsumed + x StreamClosed + x ResponseNotRead + x RequestNotRead +""" + +from __future__ import annotations + +import contextlib +import typing + +if typing.TYPE_CHECKING: + from ._models import Request, Response # pragma: no cover + +__all__ = [ + "CloseError", + "ConnectError", + "ConnectTimeout", + "CookieConflict", + "DecodingError", + "HTTPError", + "HTTPStatusError", + "InvalidURL", + "LocalProtocolError", + "NetworkError", + "PoolTimeout", + "ProtocolError", + "ProxyError", + "ReadError", + "ReadTimeout", + "RemoteProtocolError", + "RequestError", + "RequestNotRead", + "ResponseNotRead", + "StreamClosed", + "StreamConsumed", + "StreamError", + "TimeoutException", + "TooManyRedirects", + "TransportError", + "UnsupportedProtocol", + "WriteError", + "WriteTimeout", +] + + +class HTTPError(Exception): + """ + Base class for `RequestError` and `HTTPStatusError`. + + Useful for `try...except` blocks when issuing a request, + and then calling `.raise_for_status()`. + + For example: + + ``` + try: + response = httpx.get("https://www.example.com") + response.raise_for_status() + except httpx.HTTPError as exc: + print(f"HTTP Exception for {exc.request.url} - {exc}") + ``` + """ + + def __init__(self, message: str) -> None: + super().__init__(message) + self._request: Request | None = None + + @property + def request(self) -> Request: + if self._request is None: + raise RuntimeError("The .request property has not been set.") + return self._request + + @request.setter + def request(self, request: Request) -> None: + self._request = request + + +class RequestError(HTTPError): + """ + Base class for all exceptions that may occur when issuing a `.request()`. + """ + + def __init__(self, message: str, *, request: Request | None = None) -> None: + super().__init__(message) + # At the point an exception is raised we won't typically have a request + # instance to associate it with. + # + # The 'request_context' context manager is used within the Client and + # Response methods in order to ensure that any raised exceptions + # have a `.request` property set on them. + self._request = request + + +class TransportError(RequestError): + """ + Base class for all exceptions that occur at the level of the Transport API. + """ + + +# Timeout exceptions... + + +class TimeoutException(TransportError): + """ + The base class for timeout errors. + + An operation has timed out. + """ + + +class ConnectTimeout(TimeoutException): + """ + Timed out while connecting to the host. + """ + + +class ReadTimeout(TimeoutException): + """ + Timed out while receiving data from the host. + """ + + +class WriteTimeout(TimeoutException): + """ + Timed out while sending data to the host. + """ + + +class PoolTimeout(TimeoutException): + """ + Timed out waiting to acquire a connection from the pool. + """ + + +# Core networking exceptions... + + +class NetworkError(TransportError): + """ + The base class for network-related errors. + + An error occurred while interacting with the network. + """ + + +class ReadError(NetworkError): + """ + Failed to receive data from the network. + """ + + +class WriteError(NetworkError): + """ + Failed to send data through the network. + """ + + +class ConnectError(NetworkError): + """ + Failed to establish a connection. + """ + + +class CloseError(NetworkError): + """ + Failed to close a connection. + """ + + +# Other transport exceptions... + + +class ProxyError(TransportError): + """ + An error occurred while establishing a proxy connection. + """ + + +class UnsupportedProtocol(TransportError): + """ + Attempted to make a request to an unsupported protocol. + + For example issuing a request to `ftp://www.example.com`. + """ + + +class ProtocolError(TransportError): + """ + The protocol was violated. + """ + + +class LocalProtocolError(ProtocolError): + """ + A protocol was violated by the client. + + For example if the user instantiated a `Request` instance explicitly, + failed to include the mandatory `Host:` header, and then issued it directly + using `client.send()`. + """ + + +class RemoteProtocolError(ProtocolError): + """ + The protocol was violated by the server. + + For example, returning malformed HTTP. + """ + + +# Other request exceptions... + + +class DecodingError(RequestError): + """ + Decoding of the response failed, due to a malformed encoding. + """ + + +class TooManyRedirects(RequestError): + """ + Too many redirects. + """ + + +# Client errors + + +class HTTPStatusError(HTTPError): + """ + The response had an error HTTP status of 4xx or 5xx. + + May be raised when calling `response.raise_for_status()` + """ + + def __init__(self, message: str, *, request: Request, response: Response) -> None: + super().__init__(message) + self.request = request + self.response = response + + +class InvalidURL(Exception): + """ + URL is improperly formed or cannot be parsed. + """ + + def __init__(self, message: str) -> None: + super().__init__(message) + + +class CookieConflict(Exception): + """ + Attempted to lookup a cookie by name, but multiple cookies existed. + + Can occur when calling `response.cookies.get(...)`. + """ + + def __init__(self, message: str) -> None: + super().__init__(message) + + +# Stream exceptions... + +# These may occur as the result of a programming error, by accessing +# the request/response stream in an invalid manner. + + +class StreamError(RuntimeError): + """ + The base class for stream exceptions. + + The developer made an error in accessing the request stream in + an invalid way. + """ + + def __init__(self, message: str) -> None: + super().__init__(message) + + +class StreamConsumed(StreamError): + """ + Attempted to read or stream content, but the content has already + been streamed. + """ + + def __init__(self) -> None: + message = ( + "Attempted to read or stream some content, but the content has " + "already been streamed. For requests, this could be due to passing " + "a generator as request content, and then receiving a redirect " + "response or a secondary request as part of an authentication flow." + "For responses, this could be due to attempting to stream the response " + "content more than once." + ) + super().__init__(message) + + +class StreamClosed(StreamError): + """ + Attempted to read or stream response content, but the request has been + closed. + """ + + def __init__(self) -> None: + message = ( + "Attempted to read or stream content, but the stream has " "been closed." + ) + super().__init__(message) + + +class ResponseNotRead(StreamError): + """ + Attempted to access streaming response content, without having called `read()`. + """ + + def __init__(self) -> None: + message = ( + "Attempted to access streaming response content," + " without having called `read()`." + ) + super().__init__(message) + + +class RequestNotRead(StreamError): + """ + Attempted to access streaming request content, without having called `read()`. + """ + + def __init__(self) -> None: + message = ( + "Attempted to access streaming request content," + " without having called `read()`." + ) + super().__init__(message) + + +@contextlib.contextmanager +def request_context( + request: Request | None = None, +) -> typing.Iterator[None]: + """ + A context manager that can be used to attach the given request context + to any `RequestError` exceptions that are raised within the block. + """ + try: + yield + except RequestError as exc: + if request is not None: + exc.request = request + raise exc diff --git a/source/target-vendors/cp312/httpx/_main.py b/source/target-vendors/cp312/httpx/_main.py new file mode 100644 index 0000000..cffa4bb --- /dev/null +++ b/source/target-vendors/cp312/httpx/_main.py @@ -0,0 +1,506 @@ +from __future__ import annotations + +import functools +import json +import sys +import typing + +import click +import pygments.lexers +import pygments.util +import rich.console +import rich.markup +import rich.progress +import rich.syntax +import rich.table + +from ._client import Client +from ._exceptions import RequestError +from ._models import Response +from ._status_codes import codes + +if typing.TYPE_CHECKING: + import httpcore # pragma: no cover + + +def print_help() -> None: + console = rich.console.Console() + + console.print("[bold]HTTPX :butterfly:", justify="center") + console.print() + console.print("A next generation HTTP client.", justify="center") + console.print() + console.print( + "Usage: [bold]httpx[/bold] [cyan] [OPTIONS][/cyan] ", justify="left" + ) + console.print() + + table = rich.table.Table.grid(padding=1, pad_edge=True) + table.add_column("Parameter", no_wrap=True, justify="left", style="bold") + table.add_column("Description") + table.add_row( + "-m, --method [cyan]METHOD", + "Request method, such as GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD.\n" + "[Default: GET, or POST if a request body is included]", + ) + table.add_row( + "-p, --params [cyan] ...", + "Query parameters to include in the request URL.", + ) + table.add_row( + "-c, --content [cyan]TEXT", "Byte content to include in the request body." + ) + table.add_row( + "-d, --data [cyan] ...", "Form data to include in the request body." + ) + table.add_row( + "-f, --files [cyan] ...", + "Form files to include in the request body.", + ) + table.add_row("-j, --json [cyan]TEXT", "JSON data to include in the request body.") + table.add_row( + "-h, --headers [cyan] ...", + "Include additional HTTP headers in the request.", + ) + table.add_row( + "--cookies [cyan] ...", "Cookies to include in the request." + ) + table.add_row( + "--auth [cyan]", + "Username and password to include in the request. Specify '-' for the password" + " to use a password prompt. Note that using --verbose/-v will expose" + " the Authorization header, including the password encoding" + " in a trivially reversible format.", + ) + + table.add_row( + "--proxy [cyan]URL", + "Send the request via a proxy. Should be the URL giving the proxy address.", + ) + + table.add_row( + "--timeout [cyan]FLOAT", + "Timeout value to use for network operations, such as establishing the" + " connection, reading some data, etc... [Default: 5.0]", + ) + + table.add_row("--follow-redirects", "Automatically follow redirects.") + table.add_row("--no-verify", "Disable SSL verification.") + table.add_row( + "--http2", "Send the request using HTTP/2, if the remote server supports it." + ) + + table.add_row( + "--download [cyan]FILE", + "Save the response content as a file, rather than displaying it.", + ) + + table.add_row("-v, --verbose", "Verbose output. Show request as well as response.") + table.add_row("--help", "Show this message and exit.") + console.print(table) + + +def get_lexer_for_response(response: Response) -> str: + content_type = response.headers.get("Content-Type") + if content_type is not None: + mime_type, _, _ = content_type.partition(";") + try: + return typing.cast( + str, pygments.lexers.get_lexer_for_mimetype(mime_type.strip()).name + ) + except pygments.util.ClassNotFound: # pragma: no cover + pass + return "" # pragma: no cover + + +def format_request_headers(request: httpcore.Request, http2: bool = False) -> str: + version = "HTTP/2" if http2 else "HTTP/1.1" + headers = [ + (name.lower() if http2 else name, value) for name, value in request.headers + ] + method = request.method.decode("ascii") + target = request.url.target.decode("ascii") + lines = [f"{method} {target} {version}"] + [ + f"{name.decode('ascii')}: {value.decode('ascii')}" for name, value in headers + ] + return "\n".join(lines) + + +def format_response_headers( + http_version: bytes, + status: int, + reason_phrase: bytes | None, + headers: list[tuple[bytes, bytes]], +) -> str: + version = http_version.decode("ascii") + reason = ( + codes.get_reason_phrase(status) + if reason_phrase is None + else reason_phrase.decode("ascii") + ) + lines = [f"{version} {status} {reason}"] + [ + f"{name.decode('ascii')}: {value.decode('ascii')}" for name, value in headers + ] + return "\n".join(lines) + + +def print_request_headers(request: httpcore.Request, http2: bool = False) -> None: + console = rich.console.Console() + http_text = format_request_headers(request, http2=http2) + syntax = rich.syntax.Syntax(http_text, "http", theme="ansi_dark", word_wrap=True) + console.print(syntax) + syntax = rich.syntax.Syntax("", "http", theme="ansi_dark", word_wrap=True) + console.print(syntax) + + +def print_response_headers( + http_version: bytes, + status: int, + reason_phrase: bytes | None, + headers: list[tuple[bytes, bytes]], +) -> None: + console = rich.console.Console() + http_text = format_response_headers(http_version, status, reason_phrase, headers) + syntax = rich.syntax.Syntax(http_text, "http", theme="ansi_dark", word_wrap=True) + console.print(syntax) + syntax = rich.syntax.Syntax("", "http", theme="ansi_dark", word_wrap=True) + console.print(syntax) + + +def print_response(response: Response) -> None: + console = rich.console.Console() + lexer_name = get_lexer_for_response(response) + if lexer_name: + if lexer_name.lower() == "json": + try: + data = response.json() + text = json.dumps(data, indent=4) + except ValueError: # pragma: no cover + text = response.text + else: + text = response.text + + syntax = rich.syntax.Syntax(text, lexer_name, theme="ansi_dark", word_wrap=True) + console.print(syntax) + else: + console.print(f"<{len(response.content)} bytes of binary data>") + + +_PCTRTT = typing.Tuple[typing.Tuple[str, str], ...] +_PCTRTTT = typing.Tuple[_PCTRTT, ...] +_PeerCertRetDictType = typing.Dict[str, typing.Union[str, _PCTRTTT, _PCTRTT]] + + +def format_certificate(cert: _PeerCertRetDictType) -> str: # pragma: no cover + lines = [] + for key, value in cert.items(): + if isinstance(value, (list, tuple)): + lines.append(f"* {key}:") + for item in value: + if key in ("subject", "issuer"): + for sub_item in item: + lines.append(f"* {sub_item[0]}: {sub_item[1]!r}") + elif isinstance(item, tuple) and len(item) == 2: + lines.append(f"* {item[0]}: {item[1]!r}") + else: + lines.append(f"* {item!r}") + else: + lines.append(f"* {key}: {value!r}") + return "\n".join(lines) + + +def trace( + name: str, info: typing.Mapping[str, typing.Any], verbose: bool = False +) -> None: + console = rich.console.Console() + if name == "connection.connect_tcp.started" and verbose: + host = info["host"] + console.print(f"* Connecting to {host!r}") + elif name == "connection.connect_tcp.complete" and verbose: + stream = info["return_value"] + server_addr = stream.get_extra_info("server_addr") + console.print(f"* Connected to {server_addr[0]!r} on port {server_addr[1]}") + elif name == "connection.start_tls.complete" and verbose: # pragma: no cover + stream = info["return_value"] + ssl_object = stream.get_extra_info("ssl_object") + version = ssl_object.version() + cipher = ssl_object.cipher() + server_cert = ssl_object.getpeercert() + alpn = ssl_object.selected_alpn_protocol() + console.print(f"* SSL established using {version!r} / {cipher[0]!r}") + console.print(f"* Selected ALPN protocol: {alpn!r}") + if server_cert: + console.print("* Server certificate:") + console.print(format_certificate(server_cert)) + elif name == "http11.send_request_headers.started" and verbose: + request = info["request"] + print_request_headers(request, http2=False) + elif name == "http2.send_request_headers.started" and verbose: # pragma: no cover + request = info["request"] + print_request_headers(request, http2=True) + elif name == "http11.receive_response_headers.complete": + http_version, status, reason_phrase, headers = info["return_value"] + print_response_headers(http_version, status, reason_phrase, headers) + elif name == "http2.receive_response_headers.complete": # pragma: no cover + status, headers = info["return_value"] + http_version = b"HTTP/2" + reason_phrase = None + print_response_headers(http_version, status, reason_phrase, headers) + + +def download_response(response: Response, download: typing.BinaryIO) -> None: + console = rich.console.Console() + console.print() + content_length = response.headers.get("Content-Length") + with rich.progress.Progress( + "[progress.description]{task.description}", + "[progress.percentage]{task.percentage:>3.0f}%", + rich.progress.BarColumn(bar_width=None), + rich.progress.DownloadColumn(), + rich.progress.TransferSpeedColumn(), + ) as progress: + description = f"Downloading [bold]{rich.markup.escape(download.name)}" + download_task = progress.add_task( + description, + total=int(content_length or 0), + start=content_length is not None, + ) + for chunk in response.iter_bytes(): + download.write(chunk) + progress.update(download_task, completed=response.num_bytes_downloaded) + + +def validate_json( + ctx: click.Context, + param: click.Option | click.Parameter, + value: typing.Any, +) -> typing.Any: + if value is None: + return None + + try: + return json.loads(value) + except json.JSONDecodeError: # pragma: no cover + raise click.BadParameter("Not valid JSON") + + +def validate_auth( + ctx: click.Context, + param: click.Option | click.Parameter, + value: typing.Any, +) -> typing.Any: + if value == (None, None): + return None + + username, password = value + if password == "-": # pragma: no cover + password = click.prompt("Password", hide_input=True) + return (username, password) + + +def handle_help( + ctx: click.Context, + param: click.Option | click.Parameter, + value: typing.Any, +) -> None: + if not value or ctx.resilient_parsing: + return + + print_help() + ctx.exit() + + +@click.command(add_help_option=False) +@click.argument("url", type=str) +@click.option( + "--method", + "-m", + "method", + type=str, + help=( + "Request method, such as GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD. " + "[Default: GET, or POST if a request body is included]" + ), +) +@click.option( + "--params", + "-p", + "params", + type=(str, str), + multiple=True, + help="Query parameters to include in the request URL.", +) +@click.option( + "--content", + "-c", + "content", + type=str, + help="Byte content to include in the request body.", +) +@click.option( + "--data", + "-d", + "data", + type=(str, str), + multiple=True, + help="Form data to include in the request body.", +) +@click.option( + "--files", + "-f", + "files", + type=(str, click.File(mode="rb")), + multiple=True, + help="Form files to include in the request body.", +) +@click.option( + "--json", + "-j", + "json", + type=str, + callback=validate_json, + help="JSON data to include in the request body.", +) +@click.option( + "--headers", + "-h", + "headers", + type=(str, str), + multiple=True, + help="Include additional HTTP headers in the request.", +) +@click.option( + "--cookies", + "cookies", + type=(str, str), + multiple=True, + help="Cookies to include in the request.", +) +@click.option( + "--auth", + "auth", + type=(str, str), + default=(None, None), + callback=validate_auth, + help=( + "Username and password to include in the request. " + "Specify '-' for the password to use a password prompt. " + "Note that using --verbose/-v will expose the Authorization header, " + "including the password encoding in a trivially reversible format." + ), +) +@click.option( + "--proxy", + "proxy", + type=str, + default=None, + help="Send the request via a proxy. Should be the URL giving the proxy address.", +) +@click.option( + "--timeout", + "timeout", + type=float, + default=5.0, + help=( + "Timeout value to use for network operations, such as establishing the " + "connection, reading some data, etc... [Default: 5.0]" + ), +) +@click.option( + "--follow-redirects", + "follow_redirects", + is_flag=True, + default=False, + help="Automatically follow redirects.", +) +@click.option( + "--no-verify", + "verify", + is_flag=True, + default=True, + help="Disable SSL verification.", +) +@click.option( + "--http2", + "http2", + type=bool, + is_flag=True, + default=False, + help="Send the request using HTTP/2, if the remote server supports it.", +) +@click.option( + "--download", + type=click.File("wb"), + help="Save the response content as a file, rather than displaying it.", +) +@click.option( + "--verbose", + "-v", + type=bool, + is_flag=True, + default=False, + help="Verbose. Show request as well as response.", +) +@click.option( + "--help", + is_flag=True, + is_eager=True, + expose_value=False, + callback=handle_help, + help="Show this message and exit.", +) +def main( + url: str, + method: str, + params: list[tuple[str, str]], + content: str, + data: list[tuple[str, str]], + files: list[tuple[str, click.File]], + json: str, + headers: list[tuple[str, str]], + cookies: list[tuple[str, str]], + auth: tuple[str, str] | None, + proxy: str, + timeout: float, + follow_redirects: bool, + verify: bool, + http2: bool, + download: typing.BinaryIO | None, + verbose: bool, +) -> None: + """ + An HTTP command line client. + Sends a request and displays the response. + """ + if not method: + method = "POST" if content or data or files or json else "GET" + + try: + with Client(proxy=proxy, timeout=timeout, http2=http2, verify=verify) as client: + with client.stream( + method, + url, + params=list(params), + content=content, + data=dict(data), + files=files, # type: ignore + json=json, + headers=headers, + cookies=dict(cookies), + auth=auth, + follow_redirects=follow_redirects, + extensions={"trace": functools.partial(trace, verbose=verbose)}, + ) as response: + if download is not None: + download_response(response, download) + else: + response.read() + if response.content: + print_response(response) + + except RequestError as exc: + console = rich.console.Console() + console.print(f"[red]{type(exc).__name__}[/red]: {exc}") + sys.exit(1) + + sys.exit(0 if response.is_success else 1) diff --git a/source/target-vendors/cp312/httpx/_models.py b/source/target-vendors/cp312/httpx/_models.py new file mode 100644 index 0000000..67d74bf --- /dev/null +++ b/source/target-vendors/cp312/httpx/_models.py @@ -0,0 +1,1277 @@ +from __future__ import annotations + +import codecs +import datetime +import email.message +import json as jsonlib +import re +import typing +import urllib.request +from collections.abc import Mapping +from http.cookiejar import Cookie, CookieJar + +from ._content import ByteStream, UnattachedStream, encode_request, encode_response +from ._decoders import ( + SUPPORTED_DECODERS, + ByteChunker, + ContentDecoder, + IdentityDecoder, + LineDecoder, + MultiDecoder, + TextChunker, + TextDecoder, +) +from ._exceptions import ( + CookieConflict, + HTTPStatusError, + RequestNotRead, + ResponseNotRead, + StreamClosed, + StreamConsumed, + request_context, +) +from ._multipart import get_multipart_boundary_from_content_type +from ._status_codes import codes +from ._types import ( + AsyncByteStream, + CookieTypes, + HeaderTypes, + QueryParamTypes, + RequestContent, + RequestData, + RequestExtensions, + RequestFiles, + ResponseContent, + ResponseExtensions, + SyncByteStream, +) +from ._urls import URL +from ._utils import to_bytes_or_str, to_str + +__all__ = ["Cookies", "Headers", "Request", "Response"] + +SENSITIVE_HEADERS = {"authorization", "proxy-authorization"} + + +def _is_known_encoding(encoding: str) -> bool: + """ + Return `True` if `encoding` is a known codec. + """ + try: + codecs.lookup(encoding) + except LookupError: + return False + return True + + +def _normalize_header_key(key: str | bytes, encoding: str | None = None) -> bytes: + """ + Coerce str/bytes into a strictly byte-wise HTTP header key. + """ + return key if isinstance(key, bytes) else key.encode(encoding or "ascii") + + +def _normalize_header_value(value: str | bytes, encoding: str | None = None) -> bytes: + """ + Coerce str/bytes into a strictly byte-wise HTTP header value. + """ + if isinstance(value, bytes): + return value + if not isinstance(value, str): + raise TypeError(f"Header value must be str or bytes, not {type(value)}") + return value.encode(encoding or "ascii") + + +def _parse_content_type_charset(content_type: str) -> str | None: + # We used to use `cgi.parse_header()` here, but `cgi` became a dead battery. + # See: https://peps.python.org/pep-0594/#cgi + msg = email.message.Message() + msg["content-type"] = content_type + return msg.get_content_charset(failobj=None) + + +def _parse_header_links(value: str) -> list[dict[str, str]]: + """ + Returns a list of parsed link headers, for more info see: + https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link + The generic syntax of those is: + Link: < uri-reference >; param1=value1; param2="value2" + So for instance: + Link; '; type="image/jpeg",;' + would return + [ + {"url": "http:/.../front.jpeg", "type": "image/jpeg"}, + {"url": "http://.../back.jpeg"}, + ] + :param value: HTTP Link entity-header field + :return: list of parsed link headers + """ + links: list[dict[str, str]] = [] + replace_chars = " '\"" + value = value.strip(replace_chars) + if not value: + return links + for val in re.split(", *<", value): + try: + url, params = val.split(";", 1) + except ValueError: + url, params = val, "" + link = {"url": url.strip("<> '\"")} + for param in params.split(";"): + try: + key, value = param.split("=") + except ValueError: + break + link[key.strip(replace_chars)] = value.strip(replace_chars) + links.append(link) + return links + + +def _obfuscate_sensitive_headers( + items: typing.Iterable[tuple[typing.AnyStr, typing.AnyStr]], +) -> typing.Iterator[tuple[typing.AnyStr, typing.AnyStr]]: + for k, v in items: + if to_str(k.lower()) in SENSITIVE_HEADERS: + v = to_bytes_or_str("[secure]", match_type_of=v) + yield k, v + + +class Headers(typing.MutableMapping[str, str]): + """ + HTTP headers, as a case-insensitive multi-dict. + """ + + def __init__( + self, + headers: HeaderTypes | None = None, + encoding: str | None = None, + ) -> None: + self._list = [] # type: typing.List[typing.Tuple[bytes, bytes, bytes]] + + if isinstance(headers, Headers): + self._list = list(headers._list) + elif isinstance(headers, Mapping): + for k, v in headers.items(): + bytes_key = _normalize_header_key(k, encoding) + bytes_value = _normalize_header_value(v, encoding) + self._list.append((bytes_key, bytes_key.lower(), bytes_value)) + elif headers is not None: + for k, v in headers: + bytes_key = _normalize_header_key(k, encoding) + bytes_value = _normalize_header_value(v, encoding) + self._list.append((bytes_key, bytes_key.lower(), bytes_value)) + + self._encoding = encoding + + @property + def encoding(self) -> str: + """ + Header encoding is mandated as ascii, but we allow fallbacks to utf-8 + or iso-8859-1. + """ + if self._encoding is None: + for encoding in ["ascii", "utf-8"]: + for key, value in self.raw: + try: + key.decode(encoding) + value.decode(encoding) + except UnicodeDecodeError: + break + else: + # The else block runs if 'break' did not occur, meaning + # all values fitted the encoding. + self._encoding = encoding + break + else: + # The ISO-8859-1 encoding covers all 256 code points in a byte, + # so will never raise decode errors. + self._encoding = "iso-8859-1" + return self._encoding + + @encoding.setter + def encoding(self, value: str) -> None: + self._encoding = value + + @property + def raw(self) -> list[tuple[bytes, bytes]]: + """ + Returns a list of the raw header items, as byte pairs. + """ + return [(raw_key, value) for raw_key, _, value in self._list] + + def keys(self) -> typing.KeysView[str]: + return {key.decode(self.encoding): None for _, key, value in self._list}.keys() + + def values(self) -> typing.ValuesView[str]: + values_dict: dict[str, str] = {} + for _, key, value in self._list: + str_key = key.decode(self.encoding) + str_value = value.decode(self.encoding) + if str_key in values_dict: + values_dict[str_key] += f", {str_value}" + else: + values_dict[str_key] = str_value + return values_dict.values() + + def items(self) -> typing.ItemsView[str, str]: + """ + Return `(key, value)` items of headers. Concatenate headers + into a single comma separated value when a key occurs multiple times. + """ + values_dict: dict[str, str] = {} + for _, key, value in self._list: + str_key = key.decode(self.encoding) + str_value = value.decode(self.encoding) + if str_key in values_dict: + values_dict[str_key] += f", {str_value}" + else: + values_dict[str_key] = str_value + return values_dict.items() + + def multi_items(self) -> list[tuple[str, str]]: + """ + Return a list of `(key, value)` pairs of headers. Allow multiple + occurrences of the same key without concatenating into a single + comma separated value. + """ + return [ + (key.decode(self.encoding), value.decode(self.encoding)) + for _, key, value in self._list + ] + + def get(self, key: str, default: typing.Any = None) -> typing.Any: + """ + Return a header value. If multiple occurrences of the header occur + then concatenate them together with commas. + """ + try: + return self[key] + except KeyError: + return default + + def get_list(self, key: str, split_commas: bool = False) -> list[str]: + """ + Return a list of all header values for a given key. + If `split_commas=True` is passed, then any comma separated header + values are split into multiple return strings. + """ + get_header_key = key.lower().encode(self.encoding) + + values = [ + item_value.decode(self.encoding) + for _, item_key, item_value in self._list + if item_key.lower() == get_header_key + ] + + if not split_commas: + return values + + split_values = [] + for value in values: + split_values.extend([item.strip() for item in value.split(",")]) + return split_values + + def update(self, headers: HeaderTypes | None = None) -> None: # type: ignore + headers = Headers(headers) + for key in headers.keys(): + if key in self: + self.pop(key) + self._list.extend(headers._list) + + def copy(self) -> Headers: + return Headers(self, encoding=self.encoding) + + def __getitem__(self, key: str) -> str: + """ + Return a single header value. + + If there are multiple headers with the same key, then we concatenate + them with commas. See: https://tools.ietf.org/html/rfc7230#section-3.2.2 + """ + normalized_key = key.lower().encode(self.encoding) + + items = [ + header_value.decode(self.encoding) + for _, header_key, header_value in self._list + if header_key == normalized_key + ] + + if items: + return ", ".join(items) + + raise KeyError(key) + + def __setitem__(self, key: str, value: str) -> None: + """ + Set the header `key` to `value`, removing any duplicate entries. + Retains insertion order. + """ + set_key = key.encode(self._encoding or "utf-8") + set_value = value.encode(self._encoding or "utf-8") + lookup_key = set_key.lower() + + found_indexes = [ + idx + for idx, (_, item_key, _) in enumerate(self._list) + if item_key == lookup_key + ] + + for idx in reversed(found_indexes[1:]): + del self._list[idx] + + if found_indexes: + idx = found_indexes[0] + self._list[idx] = (set_key, lookup_key, set_value) + else: + self._list.append((set_key, lookup_key, set_value)) + + def __delitem__(self, key: str) -> None: + """ + Remove the header `key`. + """ + del_key = key.lower().encode(self.encoding) + + pop_indexes = [ + idx + for idx, (_, item_key, _) in enumerate(self._list) + if item_key.lower() == del_key + ] + + if not pop_indexes: + raise KeyError(key) + + for idx in reversed(pop_indexes): + del self._list[idx] + + def __contains__(self, key: typing.Any) -> bool: + header_key = key.lower().encode(self.encoding) + return header_key in [key for _, key, _ in self._list] + + def __iter__(self) -> typing.Iterator[typing.Any]: + return iter(self.keys()) + + def __len__(self) -> int: + return len(self._list) + + def __eq__(self, other: typing.Any) -> bool: + try: + other_headers = Headers(other) + except ValueError: + return False + + self_list = [(key, value) for _, key, value in self._list] + other_list = [(key, value) for _, key, value in other_headers._list] + return sorted(self_list) == sorted(other_list) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + + encoding_str = "" + if self.encoding != "ascii": + encoding_str = f", encoding={self.encoding!r}" + + as_list = list(_obfuscate_sensitive_headers(self.multi_items())) + as_dict = dict(as_list) + + no_duplicate_keys = len(as_dict) == len(as_list) + if no_duplicate_keys: + return f"{class_name}({as_dict!r}{encoding_str})" + return f"{class_name}({as_list!r}{encoding_str})" + + +class Request: + def __init__( + self, + method: str, + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + stream: SyncByteStream | AsyncByteStream | None = None, + extensions: RequestExtensions | None = None, + ) -> None: + self.method = method.upper() + self.url = URL(url) if params is None else URL(url, params=params) + self.headers = Headers(headers) + self.extensions = {} if extensions is None else dict(extensions) + + if cookies: + Cookies(cookies).set_cookie_header(self) + + if stream is None: + content_type: str | None = self.headers.get("content-type") + headers, stream = encode_request( + content=content, + data=data, + files=files, + json=json, + boundary=get_multipart_boundary_from_content_type( + content_type=content_type.encode(self.headers.encoding) + if content_type + else None + ), + ) + self._prepare(headers) + self.stream = stream + # Load the request body, except for streaming content. + if isinstance(stream, ByteStream): + self.read() + else: + # There's an important distinction between `Request(content=...)`, + # and `Request(stream=...)`. + # + # Using `content=...` implies automatically populated `Host` and content + # headers, of either `Content-Length: ...` or `Transfer-Encoding: chunked`. + # + # Using `stream=...` will not automatically include *any* + # auto-populated headers. + # + # As an end-user you don't really need `stream=...`. It's only + # useful when: + # + # * Preserving the request stream when copying requests, eg for redirects. + # * Creating request instances on the *server-side* of the transport API. + self.stream = stream + + def _prepare(self, default_headers: dict[str, str]) -> None: + for key, value in default_headers.items(): + # Ignore Transfer-Encoding if the Content-Length has been set explicitly. + if key.lower() == "transfer-encoding" and "Content-Length" in self.headers: + continue + self.headers.setdefault(key, value) + + auto_headers: list[tuple[bytes, bytes]] = [] + + has_host = "Host" in self.headers + has_content_length = ( + "Content-Length" in self.headers or "Transfer-Encoding" in self.headers + ) + + if not has_host and self.url.host: + auto_headers.append((b"Host", self.url.netloc)) + if not has_content_length and self.method in ("POST", "PUT", "PATCH"): + auto_headers.append((b"Content-Length", b"0")) + + self.headers = Headers(auto_headers + self.headers.raw) + + @property + def content(self) -> bytes: + if not hasattr(self, "_content"): + raise RequestNotRead() + return self._content + + def read(self) -> bytes: + """ + Read and return the request content. + """ + if not hasattr(self, "_content"): + assert isinstance(self.stream, typing.Iterable) + self._content = b"".join(self.stream) + if not isinstance(self.stream, ByteStream): + # If a streaming request has been read entirely into memory, then + # we can replace the stream with a raw bytes implementation, + # to ensure that any non-replayable streams can still be used. + self.stream = ByteStream(self._content) + return self._content + + async def aread(self) -> bytes: + """ + Read and return the request content. + """ + if not hasattr(self, "_content"): + assert isinstance(self.stream, typing.AsyncIterable) + self._content = b"".join([part async for part in self.stream]) + if not isinstance(self.stream, ByteStream): + # If a streaming request has been read entirely into memory, then + # we can replace the stream with a raw bytes implementation, + # to ensure that any non-replayable streams can still be used. + self.stream = ByteStream(self._content) + return self._content + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + url = str(self.url) + return f"<{class_name}({self.method!r}, {url!r})>" + + def __getstate__(self) -> dict[str, typing.Any]: + return { + name: value + for name, value in self.__dict__.items() + if name not in ["extensions", "stream"] + } + + def __setstate__(self, state: dict[str, typing.Any]) -> None: + for name, value in state.items(): + setattr(self, name, value) + self.extensions = {} + self.stream = UnattachedStream() + + +class Response: + def __init__( + self, + status_code: int, + *, + headers: HeaderTypes | None = None, + content: ResponseContent | None = None, + text: str | None = None, + html: str | None = None, + json: typing.Any = None, + stream: SyncByteStream | AsyncByteStream | None = None, + request: Request | None = None, + extensions: ResponseExtensions | None = None, + history: list[Response] | None = None, + default_encoding: str | typing.Callable[[bytes], str] = "utf-8", + ) -> None: + self.status_code = status_code + self.headers = Headers(headers) + + self._request: Request | None = request + + # When follow_redirects=False and a redirect is received, + # the client will set `response.next_request`. + self.next_request: Request | None = None + + self.extensions = {} if extensions is None else dict(extensions) + self.history = [] if history is None else list(history) + + self.is_closed = False + self.is_stream_consumed = False + + self.default_encoding = default_encoding + + if stream is None: + headers, stream = encode_response(content, text, html, json) + self._prepare(headers) + self.stream = stream + if isinstance(stream, ByteStream): + # Load the response body, except for streaming content. + self.read() + else: + # There's an important distinction between `Response(content=...)`, + # and `Response(stream=...)`. + # + # Using `content=...` implies automatically populated content headers, + # of either `Content-Length: ...` or `Transfer-Encoding: chunked`. + # + # Using `stream=...` will not automatically include any content headers. + # + # As an end-user you don't really need `stream=...`. It's only + # useful when creating response instances having received a stream + # from the transport API. + self.stream = stream + + self._num_bytes_downloaded = 0 + + def _prepare(self, default_headers: dict[str, str]) -> None: + for key, value in default_headers.items(): + # Ignore Transfer-Encoding if the Content-Length has been set explicitly. + if key.lower() == "transfer-encoding" and "content-length" in self.headers: + continue + self.headers.setdefault(key, value) + + @property + def elapsed(self) -> datetime.timedelta: + """ + Returns the time taken for the complete request/response + cycle to complete. + """ + if not hasattr(self, "_elapsed"): + raise RuntimeError( + "'.elapsed' may only be accessed after the response " + "has been read or closed." + ) + return self._elapsed + + @elapsed.setter + def elapsed(self, elapsed: datetime.timedelta) -> None: + self._elapsed = elapsed + + @property + def request(self) -> Request: + """ + Returns the request instance associated to the current response. + """ + if self._request is None: + raise RuntimeError( + "The request instance has not been set on this response." + ) + return self._request + + @request.setter + def request(self, value: Request) -> None: + self._request = value + + @property + def http_version(self) -> str: + try: + http_version: bytes = self.extensions["http_version"] + except KeyError: + return "HTTP/1.1" + else: + return http_version.decode("ascii", errors="ignore") + + @property + def reason_phrase(self) -> str: + try: + reason_phrase: bytes = self.extensions["reason_phrase"] + except KeyError: + return codes.get_reason_phrase(self.status_code) + else: + return reason_phrase.decode("ascii", errors="ignore") + + @property + def url(self) -> URL: + """ + Returns the URL for which the request was made. + """ + return self.request.url + + @property + def content(self) -> bytes: + if not hasattr(self, "_content"): + raise ResponseNotRead() + return self._content + + @property + def text(self) -> str: + if not hasattr(self, "_text"): + content = self.content + if not content: + self._text = "" + else: + decoder = TextDecoder(encoding=self.encoding or "utf-8") + self._text = "".join([decoder.decode(self.content), decoder.flush()]) + return self._text + + @property + def encoding(self) -> str | None: + """ + Return an encoding to use for decoding the byte content into text. + The priority for determining this is given by... + + * `.encoding = <>` has been set explicitly. + * The encoding as specified by the charset parameter in the Content-Type header. + * The encoding as determined by `default_encoding`, which may either be + a string like "utf-8" indicating the encoding to use, or may be a callable + which enables charset autodetection. + """ + if not hasattr(self, "_encoding"): + encoding = self.charset_encoding + if encoding is None or not _is_known_encoding(encoding): + if isinstance(self.default_encoding, str): + encoding = self.default_encoding + elif hasattr(self, "_content"): + encoding = self.default_encoding(self._content) + self._encoding = encoding or "utf-8" + return self._encoding + + @encoding.setter + def encoding(self, value: str) -> None: + """ + Set the encoding to use for decoding the byte content into text. + + If the `text` attribute has been accessed, attempting to set the + encoding will throw a ValueError. + """ + if hasattr(self, "_text"): + raise ValueError( + "Setting encoding after `text` has been accessed is not allowed." + ) + self._encoding = value + + @property + def charset_encoding(self) -> str | None: + """ + Return the encoding, as specified by the Content-Type header. + """ + content_type = self.headers.get("Content-Type") + if content_type is None: + return None + + return _parse_content_type_charset(content_type) + + def _get_content_decoder(self) -> ContentDecoder: + """ + Returns a decoder instance which can be used to decode the raw byte + content, depending on the Content-Encoding used in the response. + """ + if not hasattr(self, "_decoder"): + decoders: list[ContentDecoder] = [] + values = self.headers.get_list("content-encoding", split_commas=True) + for value in values: + value = value.strip().lower() + try: + decoder_cls = SUPPORTED_DECODERS[value] + decoders.append(decoder_cls()) + except KeyError: + continue + + if len(decoders) == 1: + self._decoder = decoders[0] + elif len(decoders) > 1: + self._decoder = MultiDecoder(children=decoders) + else: + self._decoder = IdentityDecoder() + + return self._decoder + + @property + def is_informational(self) -> bool: + """ + A property which is `True` for 1xx status codes, `False` otherwise. + """ + return codes.is_informational(self.status_code) + + @property + def is_success(self) -> bool: + """ + A property which is `True` for 2xx status codes, `False` otherwise. + """ + return codes.is_success(self.status_code) + + @property + def is_redirect(self) -> bool: + """ + A property which is `True` for 3xx status codes, `False` otherwise. + + Note that not all responses with a 3xx status code indicate a URL redirect. + + Use `response.has_redirect_location` to determine responses with a properly + formed URL redirection. + """ + return codes.is_redirect(self.status_code) + + @property + def is_client_error(self) -> bool: + """ + A property which is `True` for 4xx status codes, `False` otherwise. + """ + return codes.is_client_error(self.status_code) + + @property + def is_server_error(self) -> bool: + """ + A property which is `True` for 5xx status codes, `False` otherwise. + """ + return codes.is_server_error(self.status_code) + + @property + def is_error(self) -> bool: + """ + A property which is `True` for 4xx and 5xx status codes, `False` otherwise. + """ + return codes.is_error(self.status_code) + + @property + def has_redirect_location(self) -> bool: + """ + Returns True for 3xx responses with a properly formed URL redirection, + `False` otherwise. + """ + return ( + self.status_code + in ( + # 301 (Cacheable redirect. Method may change to GET.) + codes.MOVED_PERMANENTLY, + # 302 (Uncacheable redirect. Method may change to GET.) + codes.FOUND, + # 303 (Client should make a GET or HEAD request.) + codes.SEE_OTHER, + # 307 (Equiv. 302, but retain method) + codes.TEMPORARY_REDIRECT, + # 308 (Equiv. 301, but retain method) + codes.PERMANENT_REDIRECT, + ) + and "Location" in self.headers + ) + + def raise_for_status(self) -> Response: + """ + Raise the `HTTPStatusError` if one occurred. + """ + request = self._request + if request is None: + raise RuntimeError( + "Cannot call `raise_for_status` as the request " + "instance has not been set on this response." + ) + + if self.is_success: + return self + + if self.has_redirect_location: + message = ( + "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n" + "Redirect location: '{0.headers[location]}'\n" + "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}" + ) + else: + message = ( + "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n" + "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}" + ) + + status_class = self.status_code // 100 + error_types = { + 1: "Informational response", + 3: "Redirect response", + 4: "Client error", + 5: "Server error", + } + error_type = error_types.get(status_class, "Invalid status code") + message = message.format(self, error_type=error_type) + raise HTTPStatusError(message, request=request, response=self) + + def json(self, **kwargs: typing.Any) -> typing.Any: + return jsonlib.loads(self.content, **kwargs) + + @property + def cookies(self) -> Cookies: + if not hasattr(self, "_cookies"): + self._cookies = Cookies() + self._cookies.extract_cookies(self) + return self._cookies + + @property + def links(self) -> dict[str | None, dict[str, str]]: + """ + Returns the parsed header links of the response, if any + """ + header = self.headers.get("link") + if header is None: + return {} + + return { + (link.get("rel") or link.get("url")): link + for link in _parse_header_links(header) + } + + @property + def num_bytes_downloaded(self) -> int: + return self._num_bytes_downloaded + + def __repr__(self) -> str: + return f"" + + def __getstate__(self) -> dict[str, typing.Any]: + return { + name: value + for name, value in self.__dict__.items() + if name not in ["extensions", "stream", "is_closed", "_decoder"] + } + + def __setstate__(self, state: dict[str, typing.Any]) -> None: + for name, value in state.items(): + setattr(self, name, value) + self.is_closed = True + self.extensions = {} + self.stream = UnattachedStream() + + def read(self) -> bytes: + """ + Read and return the response content. + """ + if not hasattr(self, "_content"): + self._content = b"".join(self.iter_bytes()) + return self._content + + def iter_bytes(self, chunk_size: int | None = None) -> typing.Iterator[bytes]: + """ + A byte-iterator over the decoded response content. + This allows us to handle gzip, deflate, brotli, and zstd encoded responses. + """ + if hasattr(self, "_content"): + chunk_size = len(self._content) if chunk_size is None else chunk_size + for i in range(0, len(self._content), max(chunk_size, 1)): + yield self._content[i : i + chunk_size] + else: + decoder = self._get_content_decoder() + chunker = ByteChunker(chunk_size=chunk_size) + with request_context(request=self._request): + for raw_bytes in self.iter_raw(): + decoded = decoder.decode(raw_bytes) + for chunk in chunker.decode(decoded): + yield chunk + decoded = decoder.flush() + for chunk in chunker.decode(decoded): + yield chunk # pragma: no cover + for chunk in chunker.flush(): + yield chunk + + def iter_text(self, chunk_size: int | None = None) -> typing.Iterator[str]: + """ + A str-iterator over the decoded response content + that handles both gzip, deflate, etc but also detects the content's + string encoding. + """ + decoder = TextDecoder(encoding=self.encoding or "utf-8") + chunker = TextChunker(chunk_size=chunk_size) + with request_context(request=self._request): + for byte_content in self.iter_bytes(): + text_content = decoder.decode(byte_content) + for chunk in chunker.decode(text_content): + yield chunk + text_content = decoder.flush() + for chunk in chunker.decode(text_content): + yield chunk # pragma: no cover + for chunk in chunker.flush(): + yield chunk + + def iter_lines(self) -> typing.Iterator[str]: + decoder = LineDecoder() + with request_context(request=self._request): + for text in self.iter_text(): + for line in decoder.decode(text): + yield line + for line in decoder.flush(): + yield line + + def iter_raw(self, chunk_size: int | None = None) -> typing.Iterator[bytes]: + """ + A byte-iterator over the raw response content. + """ + if self.is_stream_consumed: + raise StreamConsumed() + if self.is_closed: + raise StreamClosed() + if not isinstance(self.stream, SyncByteStream): + raise RuntimeError("Attempted to call a sync iterator on an async stream.") + + self.is_stream_consumed = True + self._num_bytes_downloaded = 0 + chunker = ByteChunker(chunk_size=chunk_size) + + with request_context(request=self._request): + for raw_stream_bytes in self.stream: + self._num_bytes_downloaded += len(raw_stream_bytes) + for chunk in chunker.decode(raw_stream_bytes): + yield chunk + + for chunk in chunker.flush(): + yield chunk + + self.close() + + def close(self) -> None: + """ + Close the response and release the connection. + Automatically called if the response body is read to completion. + """ + if not isinstance(self.stream, SyncByteStream): + raise RuntimeError("Attempted to call an sync close on an async stream.") + + if not self.is_closed: + self.is_closed = True + with request_context(request=self._request): + self.stream.close() + + async def aread(self) -> bytes: + """ + Read and return the response content. + """ + if not hasattr(self, "_content"): + self._content = b"".join([part async for part in self.aiter_bytes()]) + return self._content + + async def aiter_bytes( + self, chunk_size: int | None = None + ) -> typing.AsyncIterator[bytes]: + """ + A byte-iterator over the decoded response content. + This allows us to handle gzip, deflate, brotli, and zstd encoded responses. + """ + if hasattr(self, "_content"): + chunk_size = len(self._content) if chunk_size is None else chunk_size + for i in range(0, len(self._content), max(chunk_size, 1)): + yield self._content[i : i + chunk_size] + else: + decoder = self._get_content_decoder() + chunker = ByteChunker(chunk_size=chunk_size) + with request_context(request=self._request): + async for raw_bytes in self.aiter_raw(): + decoded = decoder.decode(raw_bytes) + for chunk in chunker.decode(decoded): + yield chunk + decoded = decoder.flush() + for chunk in chunker.decode(decoded): + yield chunk # pragma: no cover + for chunk in chunker.flush(): + yield chunk + + async def aiter_text( + self, chunk_size: int | None = None + ) -> typing.AsyncIterator[str]: + """ + A str-iterator over the decoded response content + that handles both gzip, deflate, etc but also detects the content's + string encoding. + """ + decoder = TextDecoder(encoding=self.encoding or "utf-8") + chunker = TextChunker(chunk_size=chunk_size) + with request_context(request=self._request): + async for byte_content in self.aiter_bytes(): + text_content = decoder.decode(byte_content) + for chunk in chunker.decode(text_content): + yield chunk + text_content = decoder.flush() + for chunk in chunker.decode(text_content): + yield chunk # pragma: no cover + for chunk in chunker.flush(): + yield chunk + + async def aiter_lines(self) -> typing.AsyncIterator[str]: + decoder = LineDecoder() + with request_context(request=self._request): + async for text in self.aiter_text(): + for line in decoder.decode(text): + yield line + for line in decoder.flush(): + yield line + + async def aiter_raw( + self, chunk_size: int | None = None + ) -> typing.AsyncIterator[bytes]: + """ + A byte-iterator over the raw response content. + """ + if self.is_stream_consumed: + raise StreamConsumed() + if self.is_closed: + raise StreamClosed() + if not isinstance(self.stream, AsyncByteStream): + raise RuntimeError("Attempted to call an async iterator on an sync stream.") + + self.is_stream_consumed = True + self._num_bytes_downloaded = 0 + chunker = ByteChunker(chunk_size=chunk_size) + + with request_context(request=self._request): + async for raw_stream_bytes in self.stream: + self._num_bytes_downloaded += len(raw_stream_bytes) + for chunk in chunker.decode(raw_stream_bytes): + yield chunk + + for chunk in chunker.flush(): + yield chunk + + await self.aclose() + + async def aclose(self) -> None: + """ + Close the response and release the connection. + Automatically called if the response body is read to completion. + """ + if not isinstance(self.stream, AsyncByteStream): + raise RuntimeError("Attempted to call an async close on an sync stream.") + + if not self.is_closed: + self.is_closed = True + with request_context(request=self._request): + await self.stream.aclose() + + +class Cookies(typing.MutableMapping[str, str]): + """ + HTTP Cookies, as a mutable mapping. + """ + + def __init__(self, cookies: CookieTypes | None = None) -> None: + if cookies is None or isinstance(cookies, dict): + self.jar = CookieJar() + if isinstance(cookies, dict): + for key, value in cookies.items(): + self.set(key, value) + elif isinstance(cookies, list): + self.jar = CookieJar() + for key, value in cookies: + self.set(key, value) + elif isinstance(cookies, Cookies): + self.jar = CookieJar() + for cookie in cookies.jar: + self.jar.set_cookie(cookie) + else: + self.jar = cookies + + def extract_cookies(self, response: Response) -> None: + """ + Loads any cookies based on the response `Set-Cookie` headers. + """ + urllib_response = self._CookieCompatResponse(response) + urllib_request = self._CookieCompatRequest(response.request) + + self.jar.extract_cookies(urllib_response, urllib_request) # type: ignore + + def set_cookie_header(self, request: Request) -> None: + """ + Sets an appropriate 'Cookie:' HTTP header on the `Request`. + """ + urllib_request = self._CookieCompatRequest(request) + self.jar.add_cookie_header(urllib_request) + + def set(self, name: str, value: str, domain: str = "", path: str = "/") -> None: + """ + Set a cookie value by name. May optionally include domain and path. + """ + kwargs = { + "version": 0, + "name": name, + "value": value, + "port": None, + "port_specified": False, + "domain": domain, + "domain_specified": bool(domain), + "domain_initial_dot": domain.startswith("."), + "path": path, + "path_specified": bool(path), + "secure": False, + "expires": None, + "discard": True, + "comment": None, + "comment_url": None, + "rest": {"HttpOnly": None}, + "rfc2109": False, + } + cookie = Cookie(**kwargs) # type: ignore + self.jar.set_cookie(cookie) + + def get( # type: ignore + self, + name: str, + default: str | None = None, + domain: str | None = None, + path: str | None = None, + ) -> str | None: + """ + Get a cookie by name. May optionally include domain and path + in order to specify exactly which cookie to retrieve. + """ + value = None + for cookie in self.jar: + if cookie.name == name: + if domain is None or cookie.domain == domain: + if path is None or cookie.path == path: + if value is not None: + message = f"Multiple cookies exist with name={name}" + raise CookieConflict(message) + value = cookie.value + + if value is None: + return default + return value + + def delete( + self, + name: str, + domain: str | None = None, + path: str | None = None, + ) -> None: + """ + Delete a cookie by name. May optionally include domain and path + in order to specify exactly which cookie to delete. + """ + if domain is not None and path is not None: + return self.jar.clear(domain, path, name) + + remove = [ + cookie + for cookie in self.jar + if cookie.name == name + and (domain is None or cookie.domain == domain) + and (path is None or cookie.path == path) + ] + + for cookie in remove: + self.jar.clear(cookie.domain, cookie.path, cookie.name) + + def clear(self, domain: str | None = None, path: str | None = None) -> None: + """ + Delete all cookies. Optionally include a domain and path in + order to only delete a subset of all the cookies. + """ + args = [] + if domain is not None: + args.append(domain) + if path is not None: + assert domain is not None + args.append(path) + self.jar.clear(*args) + + def update(self, cookies: CookieTypes | None = None) -> None: # type: ignore + cookies = Cookies(cookies) + for cookie in cookies.jar: + self.jar.set_cookie(cookie) + + def __setitem__(self, name: str, value: str) -> None: + return self.set(name, value) + + def __getitem__(self, name: str) -> str: + value = self.get(name) + if value is None: + raise KeyError(name) + return value + + def __delitem__(self, name: str) -> None: + return self.delete(name) + + def __len__(self) -> int: + return len(self.jar) + + def __iter__(self) -> typing.Iterator[str]: + return (cookie.name for cookie in self.jar) + + def __bool__(self) -> bool: + for _ in self.jar: + return True + return False + + def __repr__(self) -> str: + cookies_repr = ", ".join( + [ + f"" + for cookie in self.jar + ] + ) + + return f"" + + class _CookieCompatRequest(urllib.request.Request): + """ + Wraps a `Request` instance up in a compatibility interface suitable + for use with `CookieJar` operations. + """ + + def __init__(self, request: Request) -> None: + super().__init__( + url=str(request.url), + headers=dict(request.headers), + method=request.method, + ) + self.request = request + + def add_unredirected_header(self, key: str, value: str) -> None: + super().add_unredirected_header(key, value) + self.request.headers[key] = value + + class _CookieCompatResponse: + """ + Wraps a `Request` instance up in a compatibility interface suitable + for use with `CookieJar` operations. + """ + + def __init__(self, response: Response) -> None: + self.response = response + + def info(self) -> email.message.Message: + info = email.message.Message() + for key, value in self.response.headers.multi_items(): + # Note that setting `info[key]` here is an "append" operation, + # not a "replace" operation. + # https://docs.python.org/3/library/email.compat32-message.html#email.message.Message.__setitem__ + info[key] = value + return info diff --git a/source/target-vendors/cp312/httpx/_multipart.py b/source/target-vendors/cp312/httpx/_multipart.py new file mode 100644 index 0000000..b4761af --- /dev/null +++ b/source/target-vendors/cp312/httpx/_multipart.py @@ -0,0 +1,300 @@ +from __future__ import annotations + +import io +import mimetypes +import os +import re +import typing +from pathlib import Path + +from ._types import ( + AsyncByteStream, + FileContent, + FileTypes, + RequestData, + RequestFiles, + SyncByteStream, +) +from ._utils import ( + peek_filelike_length, + primitive_value_to_str, + to_bytes, +) + +_HTML5_FORM_ENCODING_REPLACEMENTS = {'"': "%22", "\\": "\\\\"} +_HTML5_FORM_ENCODING_REPLACEMENTS.update( + {chr(c): "%{:02X}".format(c) for c in range(0x1F + 1) if c != 0x1B} +) +_HTML5_FORM_ENCODING_RE = re.compile( + r"|".join([re.escape(c) for c in _HTML5_FORM_ENCODING_REPLACEMENTS.keys()]) +) + + +def _format_form_param(name: str, value: str) -> bytes: + """ + Encode a name/value pair within a multipart form. + """ + + def replacer(match: typing.Match[str]) -> str: + return _HTML5_FORM_ENCODING_REPLACEMENTS[match.group(0)] + + value = _HTML5_FORM_ENCODING_RE.sub(replacer, value) + return f'{name}="{value}"'.encode() + + +def _guess_content_type(filename: str | None) -> str | None: + """ + Guesses the mimetype based on a filename. Defaults to `application/octet-stream`. + + Returns `None` if `filename` is `None` or empty. + """ + if filename: + return mimetypes.guess_type(filename)[0] or "application/octet-stream" + return None + + +def get_multipart_boundary_from_content_type( + content_type: bytes | None, +) -> bytes | None: + if not content_type or not content_type.startswith(b"multipart/form-data"): + return None + # parse boundary according to + # https://www.rfc-editor.org/rfc/rfc2046#section-5.1.1 + if b";" in content_type: + for section in content_type.split(b";"): + if section.strip().lower().startswith(b"boundary="): + return section.strip()[len(b"boundary=") :].strip(b'"') + return None + + +class DataField: + """ + A single form field item, within a multipart form field. + """ + + def __init__(self, name: str, value: str | bytes | int | float | None) -> None: + if not isinstance(name, str): + raise TypeError( + f"Invalid type for name. Expected str, got {type(name)}: {name!r}" + ) + if value is not None and not isinstance(value, (str, bytes, int, float)): + raise TypeError( + "Invalid type for value. Expected primitive type," + f" got {type(value)}: {value!r}" + ) + self.name = name + self.value: str | bytes = ( + value if isinstance(value, bytes) else primitive_value_to_str(value) + ) + + def render_headers(self) -> bytes: + if not hasattr(self, "_headers"): + name = _format_form_param("name", self.name) + self._headers = b"".join( + [b"Content-Disposition: form-data; ", name, b"\r\n\r\n"] + ) + + return self._headers + + def render_data(self) -> bytes: + if not hasattr(self, "_data"): + self._data = to_bytes(self.value) + + return self._data + + def get_length(self) -> int: + headers = self.render_headers() + data = self.render_data() + return len(headers) + len(data) + + def render(self) -> typing.Iterator[bytes]: + yield self.render_headers() + yield self.render_data() + + +class FileField: + """ + A single file field item, within a multipart form field. + """ + + CHUNK_SIZE = 64 * 1024 + + def __init__(self, name: str, value: FileTypes) -> None: + self.name = name + + fileobj: FileContent + + headers: dict[str, str] = {} + content_type: str | None = None + + # This large tuple based API largely mirror's requests' API + # It would be good to think of better APIs for this that we could + # include in httpx 2.0 since variable length tuples(especially of 4 elements) + # are quite unwieldly + if isinstance(value, tuple): + if len(value) == 2: + # neither the 3rd parameter (content_type) nor the 4th (headers) + # was included + filename, fileobj = value + elif len(value) == 3: + filename, fileobj, content_type = value + else: + # all 4 parameters included + filename, fileobj, content_type, headers = value # type: ignore + else: + filename = Path(str(getattr(value, "name", "upload"))).name + fileobj = value + + if content_type is None: + content_type = _guess_content_type(filename) + + has_content_type_header = any("content-type" in key.lower() for key in headers) + if content_type is not None and not has_content_type_header: + # note that unlike requests, we ignore the content_type provided in the 3rd + # tuple element if it is also included in the headers requests does + # the opposite (it overwrites the headerwith the 3rd tuple element) + headers["Content-Type"] = content_type + + if isinstance(fileobj, io.StringIO): + raise TypeError( + "Multipart file uploads require 'io.BytesIO', not 'io.StringIO'." + ) + if isinstance(fileobj, io.TextIOBase): + raise TypeError( + "Multipart file uploads must be opened in binary mode, not text mode." + ) + + self.filename = filename + self.file = fileobj + self.headers = headers + + def get_length(self) -> int | None: + headers = self.render_headers() + + if isinstance(self.file, (str, bytes)): + return len(headers) + len(to_bytes(self.file)) + + file_length = peek_filelike_length(self.file) + + # If we can't determine the filesize without reading it into memory, + # then return `None` here, to indicate an unknown file length. + if file_length is None: + return None + + return len(headers) + file_length + + def render_headers(self) -> bytes: + if not hasattr(self, "_headers"): + parts = [ + b"Content-Disposition: form-data; ", + _format_form_param("name", self.name), + ] + if self.filename: + filename = _format_form_param("filename", self.filename) + parts.extend([b"; ", filename]) + for header_name, header_value in self.headers.items(): + key, val = f"\r\n{header_name}: ".encode(), header_value.encode() + parts.extend([key, val]) + parts.append(b"\r\n\r\n") + self._headers = b"".join(parts) + + return self._headers + + def render_data(self) -> typing.Iterator[bytes]: + if isinstance(self.file, (str, bytes)): + yield to_bytes(self.file) + return + + if hasattr(self.file, "seek"): + try: + self.file.seek(0) + except io.UnsupportedOperation: + pass + + chunk = self.file.read(self.CHUNK_SIZE) + while chunk: + yield to_bytes(chunk) + chunk = self.file.read(self.CHUNK_SIZE) + + def render(self) -> typing.Iterator[bytes]: + yield self.render_headers() + yield from self.render_data() + + +class MultipartStream(SyncByteStream, AsyncByteStream): + """ + Request content as streaming multipart encoded form data. + """ + + def __init__( + self, + data: RequestData, + files: RequestFiles, + boundary: bytes | None = None, + ) -> None: + if boundary is None: + boundary = os.urandom(16).hex().encode("ascii") + + self.boundary = boundary + self.content_type = "multipart/form-data; boundary=%s" % boundary.decode( + "ascii" + ) + self.fields = list(self._iter_fields(data, files)) + + def _iter_fields( + self, data: RequestData, files: RequestFiles + ) -> typing.Iterator[FileField | DataField]: + for name, value in data.items(): + if isinstance(value, (tuple, list)): + for item in value: + yield DataField(name=name, value=item) + else: + yield DataField(name=name, value=value) + + file_items = files.items() if isinstance(files, typing.Mapping) else files + for name, value in file_items: + yield FileField(name=name, value=value) + + def iter_chunks(self) -> typing.Iterator[bytes]: + for field in self.fields: + yield b"--%s\r\n" % self.boundary + yield from field.render() + yield b"\r\n" + yield b"--%s--\r\n" % self.boundary + + def get_content_length(self) -> int | None: + """ + Return the length of the multipart encoded content, or `None` if + any of the files have a length that cannot be determined upfront. + """ + boundary_length = len(self.boundary) + length = 0 + + for field in self.fields: + field_length = field.get_length() + if field_length is None: + return None + + length += 2 + boundary_length + 2 # b"--{boundary}\r\n" + length += field_length + length += 2 # b"\r\n" + + length += 2 + boundary_length + 4 # b"--{boundary}--\r\n" + return length + + # Content stream interface. + + def get_headers(self) -> dict[str, str]: + content_length = self.get_content_length() + content_type = self.content_type + if content_length is None: + return {"Transfer-Encoding": "chunked", "Content-Type": content_type} + return {"Content-Length": str(content_length), "Content-Type": content_type} + + def __iter__(self) -> typing.Iterator[bytes]: + for chunk in self.iter_chunks(): + yield chunk + + async def __aiter__(self) -> typing.AsyncIterator[bytes]: + for chunk in self.iter_chunks(): + yield chunk diff --git a/source/target-vendors/cp312/httpx/_status_codes.py b/source/target-vendors/cp312/httpx/_status_codes.py new file mode 100644 index 0000000..133a623 --- /dev/null +++ b/source/target-vendors/cp312/httpx/_status_codes.py @@ -0,0 +1,162 @@ +from __future__ import annotations + +from enum import IntEnum + +__all__ = ["codes"] + + +class codes(IntEnum): + """HTTP status codes and reason phrases + + Status codes from the following RFCs are all observed: + + * RFC 7231: Hypertext Transfer Protocol (HTTP/1.1), obsoletes 2616 + * RFC 6585: Additional HTTP Status Codes + * RFC 3229: Delta encoding in HTTP + * RFC 4918: HTTP Extensions for WebDAV, obsoletes 2518 + * RFC 5842: Binding Extensions to WebDAV + * RFC 7238: Permanent Redirect + * RFC 2295: Transparent Content Negotiation in HTTP + * RFC 2774: An HTTP Extension Framework + * RFC 7540: Hypertext Transfer Protocol Version 2 (HTTP/2) + * RFC 2324: Hyper Text Coffee Pot Control Protocol (HTCPCP/1.0) + * RFC 7725: An HTTP Status Code to Report Legal Obstacles + * RFC 8297: An HTTP Status Code for Indicating Hints + * RFC 8470: Using Early Data in HTTP + """ + + def __new__(cls, value: int, phrase: str = "") -> codes: + obj = int.__new__(cls, value) + obj._value_ = value + + obj.phrase = phrase # type: ignore[attr-defined] + return obj + + def __str__(self) -> str: + return str(self.value) + + @classmethod + def get_reason_phrase(cls, value: int) -> str: + try: + return codes(value).phrase # type: ignore + except ValueError: + return "" + + @classmethod + def is_informational(cls, value: int) -> bool: + """ + Returns `True` for 1xx status codes, `False` otherwise. + """ + return 100 <= value <= 199 + + @classmethod + def is_success(cls, value: int) -> bool: + """ + Returns `True` for 2xx status codes, `False` otherwise. + """ + return 200 <= value <= 299 + + @classmethod + def is_redirect(cls, value: int) -> bool: + """ + Returns `True` for 3xx status codes, `False` otherwise. + """ + return 300 <= value <= 399 + + @classmethod + def is_client_error(cls, value: int) -> bool: + """ + Returns `True` for 4xx status codes, `False` otherwise. + """ + return 400 <= value <= 499 + + @classmethod + def is_server_error(cls, value: int) -> bool: + """ + Returns `True` for 5xx status codes, `False` otherwise. + """ + return 500 <= value <= 599 + + @classmethod + def is_error(cls, value: int) -> bool: + """ + Returns `True` for 4xx or 5xx status codes, `False` otherwise. + """ + return 400 <= value <= 599 + + # informational + CONTINUE = 100, "Continue" + SWITCHING_PROTOCOLS = 101, "Switching Protocols" + PROCESSING = 102, "Processing" + EARLY_HINTS = 103, "Early Hints" + + # success + OK = 200, "OK" + CREATED = 201, "Created" + ACCEPTED = 202, "Accepted" + NON_AUTHORITATIVE_INFORMATION = 203, "Non-Authoritative Information" + NO_CONTENT = 204, "No Content" + RESET_CONTENT = 205, "Reset Content" + PARTIAL_CONTENT = 206, "Partial Content" + MULTI_STATUS = 207, "Multi-Status" + ALREADY_REPORTED = 208, "Already Reported" + IM_USED = 226, "IM Used" + + # redirection + MULTIPLE_CHOICES = 300, "Multiple Choices" + MOVED_PERMANENTLY = 301, "Moved Permanently" + FOUND = 302, "Found" + SEE_OTHER = 303, "See Other" + NOT_MODIFIED = 304, "Not Modified" + USE_PROXY = 305, "Use Proxy" + TEMPORARY_REDIRECT = 307, "Temporary Redirect" + PERMANENT_REDIRECT = 308, "Permanent Redirect" + + # client error + BAD_REQUEST = 400, "Bad Request" + UNAUTHORIZED = 401, "Unauthorized" + PAYMENT_REQUIRED = 402, "Payment Required" + FORBIDDEN = 403, "Forbidden" + NOT_FOUND = 404, "Not Found" + METHOD_NOT_ALLOWED = 405, "Method Not Allowed" + NOT_ACCEPTABLE = 406, "Not Acceptable" + PROXY_AUTHENTICATION_REQUIRED = 407, "Proxy Authentication Required" + REQUEST_TIMEOUT = 408, "Request Timeout" + CONFLICT = 409, "Conflict" + GONE = 410, "Gone" + LENGTH_REQUIRED = 411, "Length Required" + PRECONDITION_FAILED = 412, "Precondition Failed" + REQUEST_ENTITY_TOO_LARGE = 413, "Request Entity Too Large" + REQUEST_URI_TOO_LONG = 414, "Request-URI Too Long" + UNSUPPORTED_MEDIA_TYPE = 415, "Unsupported Media Type" + REQUESTED_RANGE_NOT_SATISFIABLE = 416, "Requested Range Not Satisfiable" + EXPECTATION_FAILED = 417, "Expectation Failed" + IM_A_TEAPOT = 418, "I'm a teapot" + MISDIRECTED_REQUEST = 421, "Misdirected Request" + UNPROCESSABLE_ENTITY = 422, "Unprocessable Entity" + LOCKED = 423, "Locked" + FAILED_DEPENDENCY = 424, "Failed Dependency" + TOO_EARLY = 425, "Too Early" + UPGRADE_REQUIRED = 426, "Upgrade Required" + PRECONDITION_REQUIRED = 428, "Precondition Required" + TOO_MANY_REQUESTS = 429, "Too Many Requests" + REQUEST_HEADER_FIELDS_TOO_LARGE = 431, "Request Header Fields Too Large" + UNAVAILABLE_FOR_LEGAL_REASONS = 451, "Unavailable For Legal Reasons" + + # server errors + INTERNAL_SERVER_ERROR = 500, "Internal Server Error" + NOT_IMPLEMENTED = 501, "Not Implemented" + BAD_GATEWAY = 502, "Bad Gateway" + SERVICE_UNAVAILABLE = 503, "Service Unavailable" + GATEWAY_TIMEOUT = 504, "Gateway Timeout" + HTTP_VERSION_NOT_SUPPORTED = 505, "HTTP Version Not Supported" + VARIANT_ALSO_NEGOTIATES = 506, "Variant Also Negotiates" + INSUFFICIENT_STORAGE = 507, "Insufficient Storage" + LOOP_DETECTED = 508, "Loop Detected" + NOT_EXTENDED = 510, "Not Extended" + NETWORK_AUTHENTICATION_REQUIRED = 511, "Network Authentication Required" + + +# Include lower-case styles for `requests` compatibility. +for code in codes: + setattr(codes, code._name_.lower(), int(code)) diff --git a/source/target-vendors/cp312/httpx/_transports/__init__.py b/source/target-vendors/cp312/httpx/_transports/__init__.py new file mode 100644 index 0000000..7a32105 --- /dev/null +++ b/source/target-vendors/cp312/httpx/_transports/__init__.py @@ -0,0 +1,15 @@ +from .asgi import * +from .base import * +from .default import * +from .mock import * +from .wsgi import * + +__all__ = [ + "ASGITransport", + "AsyncBaseTransport", + "BaseTransport", + "AsyncHTTPTransport", + "HTTPTransport", + "MockTransport", + "WSGITransport", +] diff --git a/source/target-vendors/cp312/httpx/_transports/asgi.py b/source/target-vendors/cp312/httpx/_transports/asgi.py new file mode 100644 index 0000000..2bc4efa --- /dev/null +++ b/source/target-vendors/cp312/httpx/_transports/asgi.py @@ -0,0 +1,187 @@ +from __future__ import annotations + +import typing + +from .._models import Request, Response +from .._types import AsyncByteStream +from .base import AsyncBaseTransport + +if typing.TYPE_CHECKING: # pragma: no cover + import asyncio + + import trio + + Event = typing.Union[asyncio.Event, trio.Event] + + +_Message = typing.MutableMapping[str, typing.Any] +_Receive = typing.Callable[[], typing.Awaitable[_Message]] +_Send = typing.Callable[ + [typing.MutableMapping[str, typing.Any]], typing.Awaitable[None] +] +_ASGIApp = typing.Callable[ + [typing.MutableMapping[str, typing.Any], _Receive, _Send], typing.Awaitable[None] +] + +__all__ = ["ASGITransport"] + + +def is_running_trio() -> bool: + try: + # sniffio is a dependency of trio. + + # See https://github.com/python-trio/trio/issues/2802 + import sniffio + + if sniffio.current_async_library() == "trio": + return True + except ImportError: # pragma: nocover + pass + + return False + + +def create_event() -> Event: + if is_running_trio(): + import trio + + return trio.Event() + + import asyncio + + return asyncio.Event() + + +class ASGIResponseStream(AsyncByteStream): + def __init__(self, body: list[bytes]) -> None: + self._body = body + + async def __aiter__(self) -> typing.AsyncIterator[bytes]: + yield b"".join(self._body) + + +class ASGITransport(AsyncBaseTransport): + """ + A custom AsyncTransport that handles sending requests directly to an ASGI app. + + ```python + transport = httpx.ASGITransport( + app=app, + root_path="/submount", + client=("1.2.3.4", 123) + ) + client = httpx.AsyncClient(transport=transport) + ``` + + Arguments: + + * `app` - The ASGI application. + * `raise_app_exceptions` - Boolean indicating if exceptions in the application + should be raised. Default to `True`. Can be set to `False` for use cases + such as testing the content of a client 500 response. + * `root_path` - The root path on which the ASGI application should be mounted. + * `client` - A two-tuple indicating the client IP and port of incoming requests. + ``` + """ + + def __init__( + self, + app: _ASGIApp, + raise_app_exceptions: bool = True, + root_path: str = "", + client: tuple[str, int] = ("127.0.0.1", 123), + ) -> None: + self.app = app + self.raise_app_exceptions = raise_app_exceptions + self.root_path = root_path + self.client = client + + async def handle_async_request( + self, + request: Request, + ) -> Response: + assert isinstance(request.stream, AsyncByteStream) + + # ASGI scope. + scope = { + "type": "http", + "asgi": {"version": "3.0"}, + "http_version": "1.1", + "method": request.method, + "headers": [(k.lower(), v) for (k, v) in request.headers.raw], + "scheme": request.url.scheme, + "path": request.url.path, + "raw_path": request.url.raw_path.split(b"?")[0], + "query_string": request.url.query, + "server": (request.url.host, request.url.port), + "client": self.client, + "root_path": self.root_path, + } + + # Request. + request_body_chunks = request.stream.__aiter__() + request_complete = False + + # Response. + status_code = None + response_headers = None + body_parts = [] + response_started = False + response_complete = create_event() + + # ASGI callables. + + async def receive() -> dict[str, typing.Any]: + nonlocal request_complete + + if request_complete: + await response_complete.wait() + return {"type": "http.disconnect"} + + try: + body = await request_body_chunks.__anext__() + except StopAsyncIteration: + request_complete = True + return {"type": "http.request", "body": b"", "more_body": False} + return {"type": "http.request", "body": body, "more_body": True} + + async def send(message: typing.MutableMapping[str, typing.Any]) -> None: + nonlocal status_code, response_headers, response_started + + if message["type"] == "http.response.start": + assert not response_started + + status_code = message["status"] + response_headers = message.get("headers", []) + response_started = True + + elif message["type"] == "http.response.body": + assert not response_complete.is_set() + body = message.get("body", b"") + more_body = message.get("more_body", False) + + if body and request.method != "HEAD": + body_parts.append(body) + + if not more_body: + response_complete.set() + + try: + await self.app(scope, receive, send) + except Exception: # noqa: PIE-786 + if self.raise_app_exceptions: + raise + + response_complete.set() + if status_code is None: + status_code = 500 + if response_headers is None: + response_headers = {} + + assert response_complete.is_set() + assert status_code is not None + assert response_headers is not None + + stream = ASGIResponseStream(body_parts) + + return Response(status_code, headers=response_headers, stream=stream) diff --git a/source/target-vendors/cp312/httpx/_transports/base.py b/source/target-vendors/cp312/httpx/_transports/base.py new file mode 100644 index 0000000..66fd99d --- /dev/null +++ b/source/target-vendors/cp312/httpx/_transports/base.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import typing +from types import TracebackType + +from .._models import Request, Response + +T = typing.TypeVar("T", bound="BaseTransport") +A = typing.TypeVar("A", bound="AsyncBaseTransport") + +__all__ = ["AsyncBaseTransport", "BaseTransport"] + + +class BaseTransport: + def __enter__(self: T) -> T: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: TracebackType | None = None, + ) -> None: + self.close() + + def handle_request(self, request: Request) -> Response: + """ + Send a single HTTP request and return a response. + + Developers shouldn't typically ever need to call into this API directly, + since the Client class provides all the higher level user-facing API + niceties. + + In order to properly release any network resources, the response + stream should *either* be consumed immediately, with a call to + `response.stream.read()`, or else the `handle_request` call should + be followed with a try/finally block to ensuring the stream is + always closed. + + Example usage: + + with httpx.HTTPTransport() as transport: + req = httpx.Request( + method=b"GET", + url=(b"https", b"www.example.com", 443, b"/"), + headers=[(b"Host", b"www.example.com")], + ) + resp = transport.handle_request(req) + body = resp.stream.read() + print(resp.status_code, resp.headers, body) + + + Takes a `Request` instance as the only argument. + + Returns a `Response` instance. + """ + raise NotImplementedError( + "The 'handle_request' method must be implemented." + ) # pragma: no cover + + def close(self) -> None: + pass + + +class AsyncBaseTransport: + async def __aenter__(self: A) -> A: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: TracebackType | None = None, + ) -> None: + await self.aclose() + + async def handle_async_request( + self, + request: Request, + ) -> Response: + raise NotImplementedError( + "The 'handle_async_request' method must be implemented." + ) # pragma: no cover + + async def aclose(self) -> None: + pass diff --git a/source/target-vendors/cp312/httpx/_transports/default.py b/source/target-vendors/cp312/httpx/_transports/default.py new file mode 100644 index 0000000..d5aa05f --- /dev/null +++ b/source/target-vendors/cp312/httpx/_transports/default.py @@ -0,0 +1,406 @@ +""" +Custom transports, with nicely configured defaults. + +The following additional keyword arguments are currently supported by httpcore... + +* uds: str +* local_address: str +* retries: int + +Example usages... + +# Disable HTTP/2 on a single specific domain. +mounts = { + "all://": httpx.HTTPTransport(http2=True), + "all://*example.org": httpx.HTTPTransport() +} + +# Using advanced httpcore configuration, with connection retries. +transport = httpx.HTTPTransport(retries=1) +client = httpx.Client(transport=transport) + +# Using advanced httpcore configuration, with unix domain sockets. +transport = httpx.HTTPTransport(uds="socket.uds") +client = httpx.Client(transport=transport) +""" + +from __future__ import annotations + +import contextlib +import typing +from types import TracebackType + +if typing.TYPE_CHECKING: + import ssl # pragma: no cover + + import httpx # pragma: no cover + +from .._config import DEFAULT_LIMITS, Limits, Proxy, create_ssl_context +from .._exceptions import ( + ConnectError, + ConnectTimeout, + LocalProtocolError, + NetworkError, + PoolTimeout, + ProtocolError, + ProxyError, + ReadError, + ReadTimeout, + RemoteProtocolError, + TimeoutException, + UnsupportedProtocol, + WriteError, + WriteTimeout, +) +from .._models import Request, Response +from .._types import AsyncByteStream, CertTypes, ProxyTypes, SyncByteStream +from .._urls import URL +from .base import AsyncBaseTransport, BaseTransport + +T = typing.TypeVar("T", bound="HTTPTransport") +A = typing.TypeVar("A", bound="AsyncHTTPTransport") + +SOCKET_OPTION = typing.Union[ + typing.Tuple[int, int, int], + typing.Tuple[int, int, typing.Union[bytes, bytearray]], + typing.Tuple[int, int, None, int], +] + +__all__ = ["AsyncHTTPTransport", "HTTPTransport"] + +HTTPCORE_EXC_MAP: dict[type[Exception], type[httpx.HTTPError]] = {} + + +def _load_httpcore_exceptions() -> dict[type[Exception], type[httpx.HTTPError]]: + import httpcore + + return { + httpcore.TimeoutException: TimeoutException, + httpcore.ConnectTimeout: ConnectTimeout, + httpcore.ReadTimeout: ReadTimeout, + httpcore.WriteTimeout: WriteTimeout, + httpcore.PoolTimeout: PoolTimeout, + httpcore.NetworkError: NetworkError, + httpcore.ConnectError: ConnectError, + httpcore.ReadError: ReadError, + httpcore.WriteError: WriteError, + httpcore.ProxyError: ProxyError, + httpcore.UnsupportedProtocol: UnsupportedProtocol, + httpcore.ProtocolError: ProtocolError, + httpcore.LocalProtocolError: LocalProtocolError, + httpcore.RemoteProtocolError: RemoteProtocolError, + } + + +@contextlib.contextmanager +def map_httpcore_exceptions() -> typing.Iterator[None]: + global HTTPCORE_EXC_MAP + if len(HTTPCORE_EXC_MAP) == 0: + HTTPCORE_EXC_MAP = _load_httpcore_exceptions() + try: + yield + except Exception as exc: + mapped_exc = None + + for from_exc, to_exc in HTTPCORE_EXC_MAP.items(): + if not isinstance(exc, from_exc): + continue + # We want to map to the most specific exception we can find. + # Eg if `exc` is an `httpcore.ReadTimeout`, we want to map to + # `httpx.ReadTimeout`, not just `httpx.TimeoutException`. + if mapped_exc is None or issubclass(to_exc, mapped_exc): + mapped_exc = to_exc + + if mapped_exc is None: # pragma: no cover + raise + + message = str(exc) + raise mapped_exc(message) from exc + + +class ResponseStream(SyncByteStream): + def __init__(self, httpcore_stream: typing.Iterable[bytes]) -> None: + self._httpcore_stream = httpcore_stream + + def __iter__(self) -> typing.Iterator[bytes]: + with map_httpcore_exceptions(): + for part in self._httpcore_stream: + yield part + + def close(self) -> None: + if hasattr(self._httpcore_stream, "close"): + self._httpcore_stream.close() + + +class HTTPTransport(BaseTransport): + def __init__( + self, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, + http1: bool = True, + http2: bool = False, + limits: Limits = DEFAULT_LIMITS, + proxy: ProxyTypes | None = None, + uds: str | None = None, + local_address: str | None = None, + retries: int = 0, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> None: + import httpcore + + proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy + ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env) + + if proxy is None: + self._pool = httpcore.ConnectionPool( + ssl_context=ssl_context, + max_connections=limits.max_connections, + max_keepalive_connections=limits.max_keepalive_connections, + keepalive_expiry=limits.keepalive_expiry, + http1=http1, + http2=http2, + uds=uds, + local_address=local_address, + retries=retries, + socket_options=socket_options, + ) + elif proxy.url.scheme in ("http", "https"): + self._pool = httpcore.HTTPProxy( + proxy_url=httpcore.URL( + scheme=proxy.url.raw_scheme, + host=proxy.url.raw_host, + port=proxy.url.port, + target=proxy.url.raw_path, + ), + proxy_auth=proxy.raw_auth, + proxy_headers=proxy.headers.raw, + ssl_context=ssl_context, + proxy_ssl_context=proxy.ssl_context, + max_connections=limits.max_connections, + max_keepalive_connections=limits.max_keepalive_connections, + keepalive_expiry=limits.keepalive_expiry, + http1=http1, + http2=http2, + socket_options=socket_options, + ) + elif proxy.url.scheme in ("socks5", "socks5h"): + try: + import socksio # noqa + except ImportError: # pragma: no cover + raise ImportError( + "Using SOCKS proxy, but the 'socksio' package is not installed. " + "Make sure to install httpx using `pip install httpx[socks]`." + ) from None + + self._pool = httpcore.SOCKSProxy( + proxy_url=httpcore.URL( + scheme=proxy.url.raw_scheme, + host=proxy.url.raw_host, + port=proxy.url.port, + target=proxy.url.raw_path, + ), + proxy_auth=proxy.raw_auth, + ssl_context=ssl_context, + max_connections=limits.max_connections, + max_keepalive_connections=limits.max_keepalive_connections, + keepalive_expiry=limits.keepalive_expiry, + http1=http1, + http2=http2, + ) + else: # pragma: no cover + raise ValueError( + "Proxy protocol must be either 'http', 'https', 'socks5', or 'socks5h'," + f" but got {proxy.url.scheme!r}." + ) + + def __enter__(self: T) -> T: # Use generics for subclass support. + self._pool.__enter__() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: TracebackType | None = None, + ) -> None: + with map_httpcore_exceptions(): + self._pool.__exit__(exc_type, exc_value, traceback) + + def handle_request( + self, + request: Request, + ) -> Response: + assert isinstance(request.stream, SyncByteStream) + import httpcore + + req = httpcore.Request( + method=request.method, + url=httpcore.URL( + scheme=request.url.raw_scheme, + host=request.url.raw_host, + port=request.url.port, + target=request.url.raw_path, + ), + headers=request.headers.raw, + content=request.stream, + extensions=request.extensions, + ) + with map_httpcore_exceptions(): + resp = self._pool.handle_request(req) + + assert isinstance(resp.stream, typing.Iterable) + + return Response( + status_code=resp.status, + headers=resp.headers, + stream=ResponseStream(resp.stream), + extensions=resp.extensions, + ) + + def close(self) -> None: + self._pool.close() + + +class AsyncResponseStream(AsyncByteStream): + def __init__(self, httpcore_stream: typing.AsyncIterable[bytes]) -> None: + self._httpcore_stream = httpcore_stream + + async def __aiter__(self) -> typing.AsyncIterator[bytes]: + with map_httpcore_exceptions(): + async for part in self._httpcore_stream: + yield part + + async def aclose(self) -> None: + if hasattr(self._httpcore_stream, "aclose"): + await self._httpcore_stream.aclose() + + +class AsyncHTTPTransport(AsyncBaseTransport): + def __init__( + self, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, + http1: bool = True, + http2: bool = False, + limits: Limits = DEFAULT_LIMITS, + proxy: ProxyTypes | None = None, + uds: str | None = None, + local_address: str | None = None, + retries: int = 0, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> None: + import httpcore + + proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy + ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env) + + if proxy is None: + self._pool = httpcore.AsyncConnectionPool( + ssl_context=ssl_context, + max_connections=limits.max_connections, + max_keepalive_connections=limits.max_keepalive_connections, + keepalive_expiry=limits.keepalive_expiry, + http1=http1, + http2=http2, + uds=uds, + local_address=local_address, + retries=retries, + socket_options=socket_options, + ) + elif proxy.url.scheme in ("http", "https"): + self._pool = httpcore.AsyncHTTPProxy( + proxy_url=httpcore.URL( + scheme=proxy.url.raw_scheme, + host=proxy.url.raw_host, + port=proxy.url.port, + target=proxy.url.raw_path, + ), + proxy_auth=proxy.raw_auth, + proxy_headers=proxy.headers.raw, + proxy_ssl_context=proxy.ssl_context, + ssl_context=ssl_context, + max_connections=limits.max_connections, + max_keepalive_connections=limits.max_keepalive_connections, + keepalive_expiry=limits.keepalive_expiry, + http1=http1, + http2=http2, + socket_options=socket_options, + ) + elif proxy.url.scheme in ("socks5", "socks5h"): + try: + import socksio # noqa + except ImportError: # pragma: no cover + raise ImportError( + "Using SOCKS proxy, but the 'socksio' package is not installed. " + "Make sure to install httpx using `pip install httpx[socks]`." + ) from None + + self._pool = httpcore.AsyncSOCKSProxy( + proxy_url=httpcore.URL( + scheme=proxy.url.raw_scheme, + host=proxy.url.raw_host, + port=proxy.url.port, + target=proxy.url.raw_path, + ), + proxy_auth=proxy.raw_auth, + ssl_context=ssl_context, + max_connections=limits.max_connections, + max_keepalive_connections=limits.max_keepalive_connections, + keepalive_expiry=limits.keepalive_expiry, + http1=http1, + http2=http2, + ) + else: # pragma: no cover + raise ValueError( + "Proxy protocol must be either 'http', 'https', 'socks5', or 'socks5h'," + " but got {proxy.url.scheme!r}." + ) + + async def __aenter__(self: A) -> A: # Use generics for subclass support. + await self._pool.__aenter__() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: TracebackType | None = None, + ) -> None: + with map_httpcore_exceptions(): + await self._pool.__aexit__(exc_type, exc_value, traceback) + + async def handle_async_request( + self, + request: Request, + ) -> Response: + assert isinstance(request.stream, AsyncByteStream) + import httpcore + + req = httpcore.Request( + method=request.method, + url=httpcore.URL( + scheme=request.url.raw_scheme, + host=request.url.raw_host, + port=request.url.port, + target=request.url.raw_path, + ), + headers=request.headers.raw, + content=request.stream, + extensions=request.extensions, + ) + with map_httpcore_exceptions(): + resp = await self._pool.handle_async_request(req) + + assert isinstance(resp.stream, typing.AsyncIterable) + + return Response( + status_code=resp.status, + headers=resp.headers, + stream=AsyncResponseStream(resp.stream), + extensions=resp.extensions, + ) + + async def aclose(self) -> None: + await self._pool.aclose() diff --git a/source/target-vendors/cp312/httpx/_transports/mock.py b/source/target-vendors/cp312/httpx/_transports/mock.py new file mode 100644 index 0000000..8c418f5 --- /dev/null +++ b/source/target-vendors/cp312/httpx/_transports/mock.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import typing + +from .._models import Request, Response +from .base import AsyncBaseTransport, BaseTransport + +SyncHandler = typing.Callable[[Request], Response] +AsyncHandler = typing.Callable[[Request], typing.Coroutine[None, None, Response]] + + +__all__ = ["MockTransport"] + + +class MockTransport(AsyncBaseTransport, BaseTransport): + def __init__(self, handler: SyncHandler | AsyncHandler) -> None: + self.handler = handler + + def handle_request( + self, + request: Request, + ) -> Response: + request.read() + response = self.handler(request) + if not isinstance(response, Response): # pragma: no cover + raise TypeError("Cannot use an async handler in a sync Client") + return response + + async def handle_async_request( + self, + request: Request, + ) -> Response: + await request.aread() + response = self.handler(request) + + # Allow handler to *optionally* be an `async` function. + # If it is, then the `response` variable need to be awaited to actually + # return the result. + + if not isinstance(response, Response): + response = await response + + return response diff --git a/source/target-vendors/cp312/httpx/_transports/wsgi.py b/source/target-vendors/cp312/httpx/_transports/wsgi.py new file mode 100644 index 0000000..8592ffe --- /dev/null +++ b/source/target-vendors/cp312/httpx/_transports/wsgi.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +import io +import itertools +import sys +import typing + +from .._models import Request, Response +from .._types import SyncByteStream +from .base import BaseTransport + +if typing.TYPE_CHECKING: + from _typeshed import OptExcInfo # pragma: no cover + from _typeshed.wsgi import WSGIApplication # pragma: no cover + +_T = typing.TypeVar("_T") + + +__all__ = ["WSGITransport"] + + +def _skip_leading_empty_chunks(body: typing.Iterable[_T]) -> typing.Iterable[_T]: + body = iter(body) + for chunk in body: + if chunk: + return itertools.chain([chunk], body) + return [] + + +class WSGIByteStream(SyncByteStream): + def __init__(self, result: typing.Iterable[bytes]) -> None: + self._close = getattr(result, "close", None) + self._result = _skip_leading_empty_chunks(result) + + def __iter__(self) -> typing.Iterator[bytes]: + for part in self._result: + yield part + + def close(self) -> None: + if self._close is not None: + self._close() + + +class WSGITransport(BaseTransport): + """ + A custom transport that handles sending requests directly to an WSGI app. + The simplest way to use this functionality is to use the `app` argument. + + ``` + client = httpx.Client(app=app) + ``` + + Alternatively, you can setup the transport instance explicitly. + This allows you to include any additional configuration arguments specific + to the WSGITransport class: + + ``` + transport = httpx.WSGITransport( + app=app, + script_name="/submount", + remote_addr="1.2.3.4" + ) + client = httpx.Client(transport=transport) + ``` + + Arguments: + + * `app` - The WSGI application. + * `raise_app_exceptions` - Boolean indicating if exceptions in the application + should be raised. Default to `True`. Can be set to `False` for use cases + such as testing the content of a client 500 response. + * `script_name` - The root path on which the WSGI application should be mounted. + * `remote_addr` - A string indicating the client IP of incoming requests. + ``` + """ + + def __init__( + self, + app: WSGIApplication, + raise_app_exceptions: bool = True, + script_name: str = "", + remote_addr: str = "127.0.0.1", + wsgi_errors: typing.TextIO | None = None, + ) -> None: + self.app = app + self.raise_app_exceptions = raise_app_exceptions + self.script_name = script_name + self.remote_addr = remote_addr + self.wsgi_errors = wsgi_errors + + def handle_request(self, request: Request) -> Response: + request.read() + wsgi_input = io.BytesIO(request.content) + + port = request.url.port or {"http": 80, "https": 443}[request.url.scheme] + environ = { + "wsgi.version": (1, 0), + "wsgi.url_scheme": request.url.scheme, + "wsgi.input": wsgi_input, + "wsgi.errors": self.wsgi_errors or sys.stderr, + "wsgi.multithread": True, + "wsgi.multiprocess": False, + "wsgi.run_once": False, + "REQUEST_METHOD": request.method, + "SCRIPT_NAME": self.script_name, + "PATH_INFO": request.url.path, + "QUERY_STRING": request.url.query.decode("ascii"), + "SERVER_NAME": request.url.host, + "SERVER_PORT": str(port), + "SERVER_PROTOCOL": "HTTP/1.1", + "REMOTE_ADDR": self.remote_addr, + } + for header_key, header_value in request.headers.raw: + key = header_key.decode("ascii").upper().replace("-", "_") + if key not in ("CONTENT_TYPE", "CONTENT_LENGTH"): + key = "HTTP_" + key + environ[key] = header_value.decode("ascii") + + seen_status = None + seen_response_headers = None + seen_exc_info = None + + def start_response( + status: str, + response_headers: list[tuple[str, str]], + exc_info: OptExcInfo | None = None, + ) -> typing.Callable[[bytes], typing.Any]: + nonlocal seen_status, seen_response_headers, seen_exc_info + seen_status = status + seen_response_headers = response_headers + seen_exc_info = exc_info + return lambda _: None + + result = self.app(environ, start_response) + + stream = WSGIByteStream(result) + + assert seen_status is not None + assert seen_response_headers is not None + if seen_exc_info and seen_exc_info[0] and self.raise_app_exceptions: + raise seen_exc_info[1] + + status_code = int(seen_status.split()[0]) + headers = [ + (key.encode("ascii"), value.encode("ascii")) + for key, value in seen_response_headers + ] + + return Response(status_code, headers=headers, stream=stream) diff --git a/source/target-vendors/cp312/httpx/_types.py b/source/target-vendors/cp312/httpx/_types.py new file mode 100644 index 0000000..704dfdf --- /dev/null +++ b/source/target-vendors/cp312/httpx/_types.py @@ -0,0 +1,114 @@ +""" +Type definitions for type checking purposes. +""" + +from http.cookiejar import CookieJar +from typing import ( + IO, + TYPE_CHECKING, + Any, + AsyncIterable, + AsyncIterator, + Callable, + Dict, + Iterable, + Iterator, + List, + Mapping, + Optional, + Sequence, + Tuple, + Union, +) + +if TYPE_CHECKING: # pragma: no cover + from ._auth import Auth # noqa: F401 + from ._config import Proxy, Timeout # noqa: F401 + from ._models import Cookies, Headers, Request # noqa: F401 + from ._urls import URL, QueryParams # noqa: F401 + + +PrimitiveData = Optional[Union[str, int, float, bool]] + +URLTypes = Union["URL", str] + +QueryParamTypes = Union[ + "QueryParams", + Mapping[str, Union[PrimitiveData, Sequence[PrimitiveData]]], + List[Tuple[str, PrimitiveData]], + Tuple[Tuple[str, PrimitiveData], ...], + str, + bytes, +] + +HeaderTypes = Union[ + "Headers", + Mapping[str, str], + Mapping[bytes, bytes], + Sequence[Tuple[str, str]], + Sequence[Tuple[bytes, bytes]], +] + +CookieTypes = Union["Cookies", CookieJar, Dict[str, str], List[Tuple[str, str]]] + +TimeoutTypes = Union[ + Optional[float], + Tuple[Optional[float], Optional[float], Optional[float], Optional[float]], + "Timeout", +] +ProxyTypes = Union["URL", str, "Proxy"] +CertTypes = Union[str, Tuple[str, str], Tuple[str, str, str]] + +AuthTypes = Union[ + Tuple[Union[str, bytes], Union[str, bytes]], + Callable[["Request"], "Request"], + "Auth", +] + +RequestContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]] +ResponseContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]] +ResponseExtensions = Mapping[str, Any] + +RequestData = Mapping[str, Any] + +FileContent = Union[IO[bytes], bytes, str] +FileTypes = Union[ + # file (or bytes) + FileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], FileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], FileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], +] +RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]] + +RequestExtensions = Mapping[str, Any] + +__all__ = ["AsyncByteStream", "SyncByteStream"] + + +class SyncByteStream: + def __iter__(self) -> Iterator[bytes]: + raise NotImplementedError( + "The '__iter__' method must be implemented." + ) # pragma: no cover + yield b"" # pragma: no cover + + def close(self) -> None: + """ + Subclasses can override this method to release any network resources + after a request/response cycle is complete. + """ + + +class AsyncByteStream: + async def __aiter__(self) -> AsyncIterator[bytes]: + raise NotImplementedError( + "The '__aiter__' method must be implemented." + ) # pragma: no cover + yield b"" # pragma: no cover + + async def aclose(self) -> None: + pass diff --git a/source/target-vendors/cp312/httpx/_urlparse.py b/source/target-vendors/cp312/httpx/_urlparse.py new file mode 100644 index 0000000..bf190fd --- /dev/null +++ b/source/target-vendors/cp312/httpx/_urlparse.py @@ -0,0 +1,527 @@ +""" +An implementation of `urlparse` that provides URL validation and normalization +as described by RFC3986. + +We rely on this implementation rather than the one in Python's stdlib, because: + +* It provides more complete URL validation. +* It properly differentiates between an empty querystring and an absent querystring, + to distinguish URLs with a trailing '?'. +* It handles scheme, hostname, port, and path normalization. +* It supports IDNA hostnames, normalizing them to their encoded form. +* The API supports passing individual components, as well as the complete URL string. + +Previously we relied on the excellent `rfc3986` package to handle URL parsing and +validation, but this module provides a simpler alternative, with less indirection +required. +""" + +from __future__ import annotations + +import ipaddress +import re +import typing + +import idna + +from ._exceptions import InvalidURL + +MAX_URL_LENGTH = 65536 + +# https://datatracker.ietf.org/doc/html/rfc3986.html#section-2.3 +UNRESERVED_CHARACTERS = ( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" +) +SUB_DELIMS = "!$&'()*+,;=" + +PERCENT_ENCODED_REGEX = re.compile("%[A-Fa-f0-9]{2}") + +# https://url.spec.whatwg.org/#percent-encoded-bytes + +# The fragment percent-encode set is the C0 control percent-encode set +# and U+0020 SPACE, U+0022 ("), U+003C (<), U+003E (>), and U+0060 (`). +FRAG_SAFE = "".join( + [chr(i) for i in range(0x20, 0x7F) if i not in (0x20, 0x22, 0x3C, 0x3E, 0x60)] +) + +# The query percent-encode set is the C0 control percent-encode set +# and U+0020 SPACE, U+0022 ("), U+0023 (#), U+003C (<), and U+003E (>). +QUERY_SAFE = "".join( + [chr(i) for i in range(0x20, 0x7F) if i not in (0x20, 0x22, 0x23, 0x3C, 0x3E)] +) + +# The path percent-encode set is the query percent-encode set +# and U+003F (?), U+0060 (`), U+007B ({), and U+007D (}). +PATH_SAFE = "".join( + [ + chr(i) + for i in range(0x20, 0x7F) + if i not in (0x20, 0x22, 0x23, 0x3C, 0x3E) + (0x3F, 0x60, 0x7B, 0x7D) + ] +) + +# The userinfo percent-encode set is the path percent-encode set +# and U+002F (/), U+003A (:), U+003B (;), U+003D (=), U+0040 (@), +# U+005B ([) to U+005E (^), inclusive, and U+007C (|). +USERNAME_SAFE = "".join( + [ + chr(i) + for i in range(0x20, 0x7F) + if i + not in (0x20, 0x22, 0x23, 0x3C, 0x3E) + + (0x3F, 0x60, 0x7B, 0x7D) + + (0x2F, 0x3A, 0x3B, 0x3D, 0x40, 0x5B, 0x5C, 0x5D, 0x5E, 0x7C) + ] +) +PASSWORD_SAFE = "".join( + [ + chr(i) + for i in range(0x20, 0x7F) + if i + not in (0x20, 0x22, 0x23, 0x3C, 0x3E) + + (0x3F, 0x60, 0x7B, 0x7D) + + (0x2F, 0x3A, 0x3B, 0x3D, 0x40, 0x5B, 0x5C, 0x5D, 0x5E, 0x7C) + ] +) +# Note... The terminology 'userinfo' percent-encode set in the WHATWG document +# is used for the username and password quoting. For the joint userinfo component +# we remove U+003A (:) from the safe set. +USERINFO_SAFE = "".join( + [ + chr(i) + for i in range(0x20, 0x7F) + if i + not in (0x20, 0x22, 0x23, 0x3C, 0x3E) + + (0x3F, 0x60, 0x7B, 0x7D) + + (0x2F, 0x3B, 0x3D, 0x40, 0x5B, 0x5C, 0x5D, 0x5E, 0x7C) + ] +) + + +# {scheme}: (optional) +# //{authority} (optional) +# {path} +# ?{query} (optional) +# #{fragment} (optional) +URL_REGEX = re.compile( + ( + r"(?:(?P{scheme}):)?" + r"(?://(?P{authority}))?" + r"(?P{path})" + r"(?:\?(?P{query}))?" + r"(?:#(?P{fragment}))?" + ).format( + scheme="([a-zA-Z][a-zA-Z0-9+.-]*)?", + authority="[^/?#]*", + path="[^?#]*", + query="[^#]*", + fragment=".*", + ) +) + +# {userinfo}@ (optional) +# {host} +# :{port} (optional) +AUTHORITY_REGEX = re.compile( + ( + r"(?:(?P{userinfo})@)?" r"(?P{host})" r":?(?P{port})?" + ).format( + userinfo=".*", # Any character sequence. + host="(\\[.*\\]|[^:@]*)", # Either any character sequence excluding ':' or '@', + # or an IPv6 address enclosed within square brackets. + port=".*", # Any character sequence. + ) +) + + +# If we call urlparse with an individual component, then we need to regex +# validate that component individually. +# Note that we're duplicating the same strings as above. Shock! Horror!! +COMPONENT_REGEX = { + "scheme": re.compile("([a-zA-Z][a-zA-Z0-9+.-]*)?"), + "authority": re.compile("[^/?#]*"), + "path": re.compile("[^?#]*"), + "query": re.compile("[^#]*"), + "fragment": re.compile(".*"), + "userinfo": re.compile("[^@]*"), + "host": re.compile("(\\[.*\\]|[^:]*)"), + "port": re.compile(".*"), +} + + +# We use these simple regexs as a first pass before handing off to +# the stdlib 'ipaddress' module for IP address validation. +IPv4_STYLE_HOSTNAME = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$") +IPv6_STYLE_HOSTNAME = re.compile(r"^\[.*\]$") + + +class ParseResult(typing.NamedTuple): + scheme: str + userinfo: str + host: str + port: int | None + path: str + query: str | None + fragment: str | None + + @property + def authority(self) -> str: + return "".join( + [ + f"{self.userinfo}@" if self.userinfo else "", + f"[{self.host}]" if ":" in self.host else self.host, + f":{self.port}" if self.port is not None else "", + ] + ) + + @property + def netloc(self) -> str: + return "".join( + [ + f"[{self.host}]" if ":" in self.host else self.host, + f":{self.port}" if self.port is not None else "", + ] + ) + + def copy_with(self, **kwargs: str | None) -> ParseResult: + if not kwargs: + return self + + defaults = { + "scheme": self.scheme, + "authority": self.authority, + "path": self.path, + "query": self.query, + "fragment": self.fragment, + } + defaults.update(kwargs) + return urlparse("", **defaults) + + def __str__(self) -> str: + authority = self.authority + return "".join( + [ + f"{self.scheme}:" if self.scheme else "", + f"//{authority}" if authority else "", + self.path, + f"?{self.query}" if self.query is not None else "", + f"#{self.fragment}" if self.fragment is not None else "", + ] + ) + + +def urlparse(url: str = "", **kwargs: str | None) -> ParseResult: + # Initial basic checks on allowable URLs. + # --------------------------------------- + + # Hard limit the maximum allowable URL length. + if len(url) > MAX_URL_LENGTH: + raise InvalidURL("URL too long") + + # If a URL includes any ASCII control characters including \t, \r, \n, + # then treat it as invalid. + if any(char.isascii() and not char.isprintable() for char in url): + char = next(char for char in url if char.isascii() and not char.isprintable()) + idx = url.find(char) + error = ( + f"Invalid non-printable ASCII character in URL, {char!r} at position {idx}." + ) + raise InvalidURL(error) + + # Some keyword arguments require special handling. + # ------------------------------------------------ + + # Coerce "port" to a string, if it is provided as an integer. + if "port" in kwargs: + port = kwargs["port"] + kwargs["port"] = str(port) if isinstance(port, int) else port + + # Replace "netloc" with "host and "port". + if "netloc" in kwargs: + netloc = kwargs.pop("netloc") or "" + kwargs["host"], _, kwargs["port"] = netloc.partition(":") + + # Replace "username" and/or "password" with "userinfo". + if "username" in kwargs or "password" in kwargs: + username = quote(kwargs.pop("username", "") or "", safe=USERNAME_SAFE) + password = quote(kwargs.pop("password", "") or "", safe=PASSWORD_SAFE) + kwargs["userinfo"] = f"{username}:{password}" if password else username + + # Replace "raw_path" with "path" and "query". + if "raw_path" in kwargs: + raw_path = kwargs.pop("raw_path") or "" + kwargs["path"], seperator, kwargs["query"] = raw_path.partition("?") + if not seperator: + kwargs["query"] = None + + # Ensure that IPv6 "host" addresses are always escaped with "[...]". + if "host" in kwargs: + host = kwargs.get("host") or "" + if ":" in host and not (host.startswith("[") and host.endswith("]")): + kwargs["host"] = f"[{host}]" + + # If any keyword arguments are provided, ensure they are valid. + # ------------------------------------------------------------- + + for key, value in kwargs.items(): + if value is not None: + if len(value) > MAX_URL_LENGTH: + raise InvalidURL(f"URL component '{key}' too long") + + # If a component includes any ASCII control characters including \t, \r, \n, + # then treat it as invalid. + if any(char.isascii() and not char.isprintable() for char in value): + char = next( + char for char in value if char.isascii() and not char.isprintable() + ) + idx = value.find(char) + error = ( + f"Invalid non-printable ASCII character in URL {key} component, " + f"{char!r} at position {idx}." + ) + raise InvalidURL(error) + + # Ensure that keyword arguments match as a valid regex. + if not COMPONENT_REGEX[key].fullmatch(value): + raise InvalidURL(f"Invalid URL component '{key}'") + + # The URL_REGEX will always match, but may have empty components. + url_match = URL_REGEX.match(url) + assert url_match is not None + url_dict = url_match.groupdict() + + # * 'scheme', 'authority', and 'path' may be empty strings. + # * 'query' may be 'None', indicating no trailing "?" portion. + # Any string including the empty string, indicates a trailing "?". + # * 'fragment' may be 'None', indicating no trailing "#" portion. + # Any string including the empty string, indicates a trailing "#". + scheme = kwargs.get("scheme", url_dict["scheme"]) or "" + authority = kwargs.get("authority", url_dict["authority"]) or "" + path = kwargs.get("path", url_dict["path"]) or "" + query = kwargs.get("query", url_dict["query"]) + frag = kwargs.get("fragment", url_dict["fragment"]) + + # The AUTHORITY_REGEX will always match, but may have empty components. + authority_match = AUTHORITY_REGEX.match(authority) + assert authority_match is not None + authority_dict = authority_match.groupdict() + + # * 'userinfo' and 'host' may be empty strings. + # * 'port' may be 'None'. + userinfo = kwargs.get("userinfo", authority_dict["userinfo"]) or "" + host = kwargs.get("host", authority_dict["host"]) or "" + port = kwargs.get("port", authority_dict["port"]) + + # Normalize and validate each component. + # We end up with a parsed representation of the URL, + # with components that are plain ASCII bytestrings. + parsed_scheme: str = scheme.lower() + parsed_userinfo: str = quote(userinfo, safe=USERINFO_SAFE) + parsed_host: str = encode_host(host) + parsed_port: int | None = normalize_port(port, scheme) + + has_scheme = parsed_scheme != "" + has_authority = ( + parsed_userinfo != "" or parsed_host != "" or parsed_port is not None + ) + validate_path(path, has_scheme=has_scheme, has_authority=has_authority) + if has_scheme or has_authority: + path = normalize_path(path) + + parsed_path: str = quote(path, safe=PATH_SAFE) + parsed_query: str | None = None if query is None else quote(query, safe=QUERY_SAFE) + parsed_frag: str | None = None if frag is None else quote(frag, safe=FRAG_SAFE) + + # The parsed ASCII bytestrings are our canonical form. + # All properties of the URL are derived from these. + return ParseResult( + parsed_scheme, + parsed_userinfo, + parsed_host, + parsed_port, + parsed_path, + parsed_query, + parsed_frag, + ) + + +def encode_host(host: str) -> str: + if not host: + return "" + + elif IPv4_STYLE_HOSTNAME.match(host): + # Validate IPv4 hostnames like #.#.#.# + # + # From https://datatracker.ietf.org/doc/html/rfc3986/#section-3.2.2 + # + # IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet + try: + ipaddress.IPv4Address(host) + except ipaddress.AddressValueError: + raise InvalidURL(f"Invalid IPv4 address: {host!r}") + return host + + elif IPv6_STYLE_HOSTNAME.match(host): + # Validate IPv6 hostnames like [...] + # + # From https://datatracker.ietf.org/doc/html/rfc3986/#section-3.2.2 + # + # "A host identified by an Internet Protocol literal address, version 6 + # [RFC3513] or later, is distinguished by enclosing the IP literal + # within square brackets ("[" and "]"). This is the only place where + # square bracket characters are allowed in the URI syntax." + try: + ipaddress.IPv6Address(host[1:-1]) + except ipaddress.AddressValueError: + raise InvalidURL(f"Invalid IPv6 address: {host!r}") + return host[1:-1] + + elif host.isascii(): + # Regular ASCII hostnames + # + # From https://datatracker.ietf.org/doc/html/rfc3986/#section-3.2.2 + # + # reg-name = *( unreserved / pct-encoded / sub-delims ) + WHATWG_SAFE = '"`{}%|\\' + return quote(host.lower(), safe=SUB_DELIMS + WHATWG_SAFE) + + # IDNA hostnames + try: + return idna.encode(host.lower()).decode("ascii") + except idna.IDNAError: + raise InvalidURL(f"Invalid IDNA hostname: {host!r}") + + +def normalize_port(port: str | int | None, scheme: str) -> int | None: + # From https://tools.ietf.org/html/rfc3986#section-3.2.3 + # + # "A scheme may define a default port. For example, the "http" scheme + # defines a default port of "80", corresponding to its reserved TCP + # port number. The type of port designated by the port number (e.g., + # TCP, UDP, SCTP) is defined by the URI scheme. URI producers and + # normalizers should omit the port component and its ":" delimiter if + # port is empty or if its value would be the same as that of the + # scheme's default." + if port is None or port == "": + return None + + try: + port_as_int = int(port) + except ValueError: + raise InvalidURL(f"Invalid port: {port!r}") + + # See https://url.spec.whatwg.org/#url-miscellaneous + default_port = {"ftp": 21, "http": 80, "https": 443, "ws": 80, "wss": 443}.get( + scheme + ) + if port_as_int == default_port: + return None + return port_as_int + + +def validate_path(path: str, has_scheme: bool, has_authority: bool) -> None: + """ + Path validation rules that depend on if the URL contains + a scheme or authority component. + + See https://datatracker.ietf.org/doc/html/rfc3986.html#section-3.3 + """ + if has_authority: + # If a URI contains an authority component, then the path component + # must either be empty or begin with a slash ("/") character." + if path and not path.startswith("/"): + raise InvalidURL("For absolute URLs, path must be empty or begin with '/'") + + if not has_scheme and not has_authority: + # If a URI does not contain an authority component, then the path cannot begin + # with two slash characters ("//"). + if path.startswith("//"): + raise InvalidURL("Relative URLs cannot have a path starting with '//'") + + # In addition, a URI reference (Section 4.1) may be a relative-path reference, + # in which case the first path segment cannot contain a colon (":") character. + if path.startswith(":"): + raise InvalidURL("Relative URLs cannot have a path starting with ':'") + + +def normalize_path(path: str) -> str: + """ + Drop "." and ".." segments from a URL path. + + For example: + + normalize_path("/path/./to/somewhere/..") == "/path/to" + """ + # Fast return when no '.' characters in the path. + if "." not in path: + return path + + components = path.split("/") + + # Fast return when no '.' or '..' components in the path. + if "." not in components and ".." not in components: + return path + + # https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4 + output: list[str] = [] + for component in components: + if component == ".": + pass + elif component == "..": + if output and output != [""]: + output.pop() + else: + output.append(component) + return "/".join(output) + + +def PERCENT(string: str) -> str: + return "".join([f"%{byte:02X}" for byte in string.encode("utf-8")]) + + +def percent_encoded(string: str, safe: str) -> str: + """ + Use percent-encoding to quote a string. + """ + NON_ESCAPED_CHARS = UNRESERVED_CHARACTERS + safe + + # Fast path for strings that don't need escaping. + if not string.rstrip(NON_ESCAPED_CHARS): + return string + + return "".join( + [char if char in NON_ESCAPED_CHARS else PERCENT(char) for char in string] + ) + + +def quote(string: str, safe: str) -> str: + """ + Use percent-encoding to quote a string, omitting existing '%xx' escape sequences. + + See: https://www.rfc-editor.org/rfc/rfc3986#section-2.1 + + * `string`: The string to be percent-escaped. + * `safe`: A string containing characters that may be treated as safe, and do not + need to be escaped. Unreserved characters are always treated as safe. + See: https://www.rfc-editor.org/rfc/rfc3986#section-2.3 + """ + parts = [] + current_position = 0 + for match in re.finditer(PERCENT_ENCODED_REGEX, string): + start_position, end_position = match.start(), match.end() + matched_text = match.group(0) + # Add any text up to the '%xx' escape sequence. + if start_position != current_position: + leading_text = string[current_position:start_position] + parts.append(percent_encoded(leading_text, safe=safe)) + + # Add the '%xx' escape sequence. + parts.append(matched_text) + current_position = end_position + + # Add any text after the final '%xx' escape sequence. + if current_position != len(string): + trailing_text = string[current_position:] + parts.append(percent_encoded(trailing_text, safe=safe)) + + return "".join(parts) diff --git a/source/target-vendors/cp312/httpx/_urls.py b/source/target-vendors/cp312/httpx/_urls.py new file mode 100644 index 0000000..147a8fa --- /dev/null +++ b/source/target-vendors/cp312/httpx/_urls.py @@ -0,0 +1,641 @@ +from __future__ import annotations + +import typing +from urllib.parse import parse_qs, unquote, urlencode + +import idna + +from ._types import QueryParamTypes +from ._urlparse import urlparse +from ._utils import primitive_value_to_str + +__all__ = ["URL", "QueryParams"] + + +class URL: + """ + url = httpx.URL("HTTPS://jo%40email.com:a%20secret@müller.de:1234/pa%20th?search=ab#anchorlink") + + assert url.scheme == "https" + assert url.username == "jo@email.com" + assert url.password == "a secret" + assert url.userinfo == b"jo%40email.com:a%20secret" + assert url.host == "müller.de" + assert url.raw_host == b"xn--mller-kva.de" + assert url.port == 1234 + assert url.netloc == b"xn--mller-kva.de:1234" + assert url.path == "/pa th" + assert url.query == b"?search=ab" + assert url.raw_path == b"/pa%20th?search=ab" + assert url.fragment == "anchorlink" + + The components of a URL are broken down like this: + + https://jo%40email.com:a%20secret@müller.de:1234/pa%20th?search=ab#anchorlink + [scheme] [ username ] [password] [ host ][port][ path ] [ query ] [fragment] + [ userinfo ] [ netloc ][ raw_path ] + + Note that: + + * `url.scheme` is normalized to always be lowercased. + + * `url.host` is normalized to always be lowercased. Internationalized domain + names are represented in unicode, without IDNA encoding applied. For instance: + + url = httpx.URL("http://中国.icom.museum") + assert url.host == "中国.icom.museum" + url = httpx.URL("http://xn--fiqs8s.icom.museum") + assert url.host == "中国.icom.museum" + + * `url.raw_host` is normalized to always be lowercased, and is IDNA encoded. + + url = httpx.URL("http://中国.icom.museum") + assert url.raw_host == b"xn--fiqs8s.icom.museum" + url = httpx.URL("http://xn--fiqs8s.icom.museum") + assert url.raw_host == b"xn--fiqs8s.icom.museum" + + * `url.port` is either None or an integer. URLs that include the default port for + "http", "https", "ws", "wss", and "ftp" schemes have their port + normalized to `None`. + + assert httpx.URL("http://example.com") == httpx.URL("http://example.com:80") + assert httpx.URL("http://example.com").port is None + assert httpx.URL("http://example.com:80").port is None + + * `url.userinfo` is raw bytes, without URL escaping. Usually you'll want to work + with `url.username` and `url.password` instead, which handle the URL escaping. + + * `url.raw_path` is raw bytes of both the path and query, without URL escaping. + This portion is used as the target when constructing HTTP requests. Usually you'll + want to work with `url.path` instead. + + * `url.query` is raw bytes, without URL escaping. A URL query string portion can + only be properly URL escaped when decoding the parameter names and values + themselves. + """ + + def __init__(self, url: URL | str = "", **kwargs: typing.Any) -> None: + if kwargs: + allowed = { + "scheme": str, + "username": str, + "password": str, + "userinfo": bytes, + "host": str, + "port": int, + "netloc": bytes, + "path": str, + "query": bytes, + "raw_path": bytes, + "fragment": str, + "params": object, + } + + # Perform type checking for all supported keyword arguments. + for key, value in kwargs.items(): + if key not in allowed: + message = f"{key!r} is an invalid keyword argument for URL()" + raise TypeError(message) + if value is not None and not isinstance(value, allowed[key]): + expected = allowed[key].__name__ + seen = type(value).__name__ + message = f"Argument {key!r} must be {expected} but got {seen}" + raise TypeError(message) + if isinstance(value, bytes): + kwargs[key] = value.decode("ascii") + + if "params" in kwargs: + # Replace any "params" keyword with the raw "query" instead. + # + # Ensure that empty params use `kwargs["query"] = None` rather + # than `kwargs["query"] = ""`, so that generated URLs do not + # include an empty trailing "?". + params = kwargs.pop("params") + kwargs["query"] = None if not params else str(QueryParams(params)) + + if isinstance(url, str): + self._uri_reference = urlparse(url, **kwargs) + elif isinstance(url, URL): + self._uri_reference = url._uri_reference.copy_with(**kwargs) + else: + raise TypeError( + "Invalid type for url. Expected str or httpx.URL," + f" got {type(url)}: {url!r}" + ) + + @property + def scheme(self) -> str: + """ + The URL scheme, such as "http", "https". + Always normalised to lowercase. + """ + return self._uri_reference.scheme + + @property + def raw_scheme(self) -> bytes: + """ + The raw bytes representation of the URL scheme, such as b"http", b"https". + Always normalised to lowercase. + """ + return self._uri_reference.scheme.encode("ascii") + + @property + def userinfo(self) -> bytes: + """ + The URL userinfo as a raw bytestring. + For example: b"jo%40email.com:a%20secret". + """ + return self._uri_reference.userinfo.encode("ascii") + + @property + def username(self) -> str: + """ + The URL username as a string, with URL decoding applied. + For example: "jo@email.com" + """ + userinfo = self._uri_reference.userinfo + return unquote(userinfo.partition(":")[0]) + + @property + def password(self) -> str: + """ + The URL password as a string, with URL decoding applied. + For example: "a secret" + """ + userinfo = self._uri_reference.userinfo + return unquote(userinfo.partition(":")[2]) + + @property + def host(self) -> str: + """ + The URL host as a string. + Always normalized to lowercase, with IDNA hosts decoded into unicode. + + Examples: + + url = httpx.URL("http://www.EXAMPLE.org") + assert url.host == "www.example.org" + + url = httpx.URL("http://中国.icom.museum") + assert url.host == "中国.icom.museum" + + url = httpx.URL("http://xn--fiqs8s.icom.museum") + assert url.host == "中国.icom.museum" + + url = httpx.URL("https://[::ffff:192.168.0.1]") + assert url.host == "::ffff:192.168.0.1" + """ + host: str = self._uri_reference.host + + if host.startswith("xn--"): + host = idna.decode(host) + + return host + + @property + def raw_host(self) -> bytes: + """ + The raw bytes representation of the URL host. + Always normalized to lowercase, and IDNA encoded. + + Examples: + + url = httpx.URL("http://www.EXAMPLE.org") + assert url.raw_host == b"www.example.org" + + url = httpx.URL("http://中国.icom.museum") + assert url.raw_host == b"xn--fiqs8s.icom.museum" + + url = httpx.URL("http://xn--fiqs8s.icom.museum") + assert url.raw_host == b"xn--fiqs8s.icom.museum" + + url = httpx.URL("https://[::ffff:192.168.0.1]") + assert url.raw_host == b"::ffff:192.168.0.1" + """ + return self._uri_reference.host.encode("ascii") + + @property + def port(self) -> int | None: + """ + The URL port as an integer. + + Note that the URL class performs port normalization as per the WHATWG spec. + Default ports for "http", "https", "ws", "wss", and "ftp" schemes are always + treated as `None`. + + For example: + + assert httpx.URL("http://www.example.com") == httpx.URL("http://www.example.com:80") + assert httpx.URL("http://www.example.com:80").port is None + """ + return self._uri_reference.port + + @property + def netloc(self) -> bytes: + """ + Either `` or `:` as bytes. + Always normalized to lowercase, and IDNA encoded. + + This property may be used for generating the value of a request + "Host" header. + """ + return self._uri_reference.netloc.encode("ascii") + + @property + def path(self) -> str: + """ + The URL path as a string. Excluding the query string, and URL decoded. + + For example: + + url = httpx.URL("https://example.com/pa%20th") + assert url.path == "/pa th" + """ + path = self._uri_reference.path or "/" + return unquote(path) + + @property + def query(self) -> bytes: + """ + The URL query string, as raw bytes, excluding the leading b"?". + + This is necessarily a bytewise interface, because we cannot + perform URL decoding of this representation until we've parsed + the keys and values into a QueryParams instance. + + For example: + + url = httpx.URL("https://example.com/?filter=some%20search%20terms") + assert url.query == b"filter=some%20search%20terms" + """ + query = self._uri_reference.query or "" + return query.encode("ascii") + + @property + def params(self) -> QueryParams: + """ + The URL query parameters, neatly parsed and packaged into an immutable + multidict representation. + """ + return QueryParams(self._uri_reference.query) + + @property + def raw_path(self) -> bytes: + """ + The complete URL path and query string as raw bytes. + Used as the target when constructing HTTP requests. + + For example: + + GET /users?search=some%20text HTTP/1.1 + Host: www.example.org + Connection: close + """ + path = self._uri_reference.path or "/" + if self._uri_reference.query is not None: + path += "?" + self._uri_reference.query + return path.encode("ascii") + + @property + def fragment(self) -> str: + """ + The URL fragments, as used in HTML anchors. + As a string, without the leading '#'. + """ + return unquote(self._uri_reference.fragment or "") + + @property + def is_absolute_url(self) -> bool: + """ + Return `True` for absolute URLs such as 'http://example.com/path', + and `False` for relative URLs such as '/path'. + """ + # We don't use `.is_absolute` from `rfc3986` because it treats + # URLs with a fragment portion as not absolute. + # What we actually care about is if the URL provides + # a scheme and hostname to which connections should be made. + return bool(self._uri_reference.scheme and self._uri_reference.host) + + @property + def is_relative_url(self) -> bool: + """ + Return `False` for absolute URLs such as 'http://example.com/path', + and `True` for relative URLs such as '/path'. + """ + return not self.is_absolute_url + + def copy_with(self, **kwargs: typing.Any) -> URL: + """ + Copy this URL, returning a new URL with some components altered. + Accepts the same set of parameters as the components that are made + available via properties on the `URL` class. + + For example: + + url = httpx.URL("https://www.example.com").copy_with( + username="jo@gmail.com", password="a secret" + ) + assert url == "https://jo%40email.com:a%20secret@www.example.com" + """ + return URL(self, **kwargs) + + def copy_set_param(self, key: str, value: typing.Any = None) -> URL: + return self.copy_with(params=self.params.set(key, value)) + + def copy_add_param(self, key: str, value: typing.Any = None) -> URL: + return self.copy_with(params=self.params.add(key, value)) + + def copy_remove_param(self, key: str) -> URL: + return self.copy_with(params=self.params.remove(key)) + + def copy_merge_params(self, params: QueryParamTypes) -> URL: + return self.copy_with(params=self.params.merge(params)) + + def join(self, url: URL | str) -> URL: + """ + Return an absolute URL, using this URL as the base. + + Eg. + + url = httpx.URL("https://www.example.com/test") + url = url.join("/new/path") + assert url == "https://www.example.com/new/path" + """ + from urllib.parse import urljoin + + return URL(urljoin(str(self), str(URL(url)))) + + def __hash__(self) -> int: + return hash(str(self)) + + def __eq__(self, other: typing.Any) -> bool: + return isinstance(other, (URL, str)) and str(self) == str(URL(other)) + + def __str__(self) -> str: + return str(self._uri_reference) + + def __repr__(self) -> str: + scheme, userinfo, host, port, path, query, fragment = self._uri_reference + + if ":" in userinfo: + # Mask any password component. + userinfo = f'{userinfo.split(":")[0]}:[secure]' + + authority = "".join( + [ + f"{userinfo}@" if userinfo else "", + f"[{host}]" if ":" in host else host, + f":{port}" if port is not None else "", + ] + ) + url = "".join( + [ + f"{self.scheme}:" if scheme else "", + f"//{authority}" if authority else "", + path, + f"?{query}" if query is not None else "", + f"#{fragment}" if fragment is not None else "", + ] + ) + + return f"{self.__class__.__name__}({url!r})" + + @property + def raw(self) -> tuple[bytes, bytes, int, bytes]: # pragma: nocover + import collections + import warnings + + warnings.warn("URL.raw is deprecated.") + RawURL = collections.namedtuple( + "RawURL", ["raw_scheme", "raw_host", "port", "raw_path"] + ) + return RawURL( + raw_scheme=self.raw_scheme, + raw_host=self.raw_host, + port=self.port, + raw_path=self.raw_path, + ) + + +class QueryParams(typing.Mapping[str, str]): + """ + URL query parameters, as a multi-dict. + """ + + def __init__(self, *args: QueryParamTypes | None, **kwargs: typing.Any) -> None: + assert len(args) < 2, "Too many arguments." + assert not (args and kwargs), "Cannot mix named and unnamed arguments." + + value = args[0] if args else kwargs + + if value is None or isinstance(value, (str, bytes)): + value = value.decode("ascii") if isinstance(value, bytes) else value + self._dict = parse_qs(value, keep_blank_values=True) + elif isinstance(value, QueryParams): + self._dict = {k: list(v) for k, v in value._dict.items()} + else: + dict_value: dict[typing.Any, list[typing.Any]] = {} + if isinstance(value, (list, tuple)): + # Convert list inputs like: + # [("a", "123"), ("a", "456"), ("b", "789")] + # To a dict representation, like: + # {"a": ["123", "456"], "b": ["789"]} + for item in value: + dict_value.setdefault(item[0], []).append(item[1]) + else: + # Convert dict inputs like: + # {"a": "123", "b": ["456", "789"]} + # To dict inputs where values are always lists, like: + # {"a": ["123"], "b": ["456", "789"]} + dict_value = { + k: list(v) if isinstance(v, (list, tuple)) else [v] + for k, v in value.items() + } + + # Ensure that keys and values are neatly coerced to strings. + # We coerce values `True` and `False` to JSON-like "true" and "false" + # representations, and coerce `None` values to the empty string. + self._dict = { + str(k): [primitive_value_to_str(item) for item in v] + for k, v in dict_value.items() + } + + def keys(self) -> typing.KeysView[str]: + """ + Return all the keys in the query params. + + Usage: + + q = httpx.QueryParams("a=123&a=456&b=789") + assert list(q.keys()) == ["a", "b"] + """ + return self._dict.keys() + + def values(self) -> typing.ValuesView[str]: + """ + Return all the values in the query params. If a key occurs more than once + only the first item for that key is returned. + + Usage: + + q = httpx.QueryParams("a=123&a=456&b=789") + assert list(q.values()) == ["123", "789"] + """ + return {k: v[0] for k, v in self._dict.items()}.values() + + def items(self) -> typing.ItemsView[str, str]: + """ + Return all items in the query params. If a key occurs more than once + only the first item for that key is returned. + + Usage: + + q = httpx.QueryParams("a=123&a=456&b=789") + assert list(q.items()) == [("a", "123"), ("b", "789")] + """ + return {k: v[0] for k, v in self._dict.items()}.items() + + def multi_items(self) -> list[tuple[str, str]]: + """ + Return all items in the query params. Allow duplicate keys to occur. + + Usage: + + q = httpx.QueryParams("a=123&a=456&b=789") + assert list(q.multi_items()) == [("a", "123"), ("a", "456"), ("b", "789")] + """ + multi_items: list[tuple[str, str]] = [] + for k, v in self._dict.items(): + multi_items.extend([(k, i) for i in v]) + return multi_items + + def get(self, key: typing.Any, default: typing.Any = None) -> typing.Any: + """ + Get a value from the query param for a given key. If the key occurs + more than once, then only the first value is returned. + + Usage: + + q = httpx.QueryParams("a=123&a=456&b=789") + assert q.get("a") == "123" + """ + if key in self._dict: + return self._dict[str(key)][0] + return default + + def get_list(self, key: str) -> list[str]: + """ + Get all values from the query param for a given key. + + Usage: + + q = httpx.QueryParams("a=123&a=456&b=789") + assert q.get_list("a") == ["123", "456"] + """ + return list(self._dict.get(str(key), [])) + + def set(self, key: str, value: typing.Any = None) -> QueryParams: + """ + Return a new QueryParams instance, setting the value of a key. + + Usage: + + q = httpx.QueryParams("a=123") + q = q.set("a", "456") + assert q == httpx.QueryParams("a=456") + """ + q = QueryParams() + q._dict = dict(self._dict) + q._dict[str(key)] = [primitive_value_to_str(value)] + return q + + def add(self, key: str, value: typing.Any = None) -> QueryParams: + """ + Return a new QueryParams instance, setting or appending the value of a key. + + Usage: + + q = httpx.QueryParams("a=123") + q = q.add("a", "456") + assert q == httpx.QueryParams("a=123&a=456") + """ + q = QueryParams() + q._dict = dict(self._dict) + q._dict[str(key)] = q.get_list(key) + [primitive_value_to_str(value)] + return q + + def remove(self, key: str) -> QueryParams: + """ + Return a new QueryParams instance, removing the value of a key. + + Usage: + + q = httpx.QueryParams("a=123") + q = q.remove("a") + assert q == httpx.QueryParams("") + """ + q = QueryParams() + q._dict = dict(self._dict) + q._dict.pop(str(key), None) + return q + + def merge(self, params: QueryParamTypes | None = None) -> QueryParams: + """ + Return a new QueryParams instance, updated with. + + Usage: + + q = httpx.QueryParams("a=123") + q = q.merge({"b": "456"}) + assert q == httpx.QueryParams("a=123&b=456") + + q = httpx.QueryParams("a=123") + q = q.merge({"a": "456", "b": "789"}) + assert q == httpx.QueryParams("a=456&b=789") + """ + q = QueryParams(params) + q._dict = {**self._dict, **q._dict} + return q + + def __getitem__(self, key: typing.Any) -> str: + return self._dict[key][0] + + def __contains__(self, key: typing.Any) -> bool: + return key in self._dict + + def __iter__(self) -> typing.Iterator[typing.Any]: + return iter(self.keys()) + + def __len__(self) -> int: + return len(self._dict) + + def __bool__(self) -> bool: + return bool(self._dict) + + def __hash__(self) -> int: + return hash(str(self)) + + def __eq__(self, other: typing.Any) -> bool: + if not isinstance(other, self.__class__): + return False + return sorted(self.multi_items()) == sorted(other.multi_items()) + + def __str__(self) -> str: + return urlencode(self.multi_items()) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + query_string = str(self) + return f"{class_name}({query_string!r})" + + def update(self, params: QueryParamTypes | None = None) -> None: + raise RuntimeError( + "QueryParams are immutable since 0.18.0. " + "Use `q = q.merge(...)` to create an updated copy." + ) + + def __setitem__(self, key: str, value: str) -> None: + raise RuntimeError( + "QueryParams are immutable since 0.18.0. " + "Use `q = q.set(key, value)` to create an updated copy." + ) diff --git a/source/target-vendors/cp312/httpx/_utils.py b/source/target-vendors/cp312/httpx/_utils.py new file mode 100644 index 0000000..7fe827d --- /dev/null +++ b/source/target-vendors/cp312/httpx/_utils.py @@ -0,0 +1,242 @@ +from __future__ import annotations + +import ipaddress +import os +import re +import typing +from urllib.request import getproxies + +from ._types import PrimitiveData + +if typing.TYPE_CHECKING: # pragma: no cover + from ._urls import URL + + +def primitive_value_to_str(value: PrimitiveData) -> str: + """ + Coerce a primitive data type into a string value. + + Note that we prefer JSON-style 'true'/'false' for boolean values here. + """ + if value is True: + return "true" + elif value is False: + return "false" + elif value is None: + return "" + return str(value) + + +def get_environment_proxies() -> dict[str, str | None]: + """Gets proxy information from the environment""" + + # urllib.request.getproxies() falls back on System + # Registry and Config for proxies on Windows and macOS. + # We don't want to propagate non-HTTP proxies into + # our configuration such as 'TRAVIS_APT_PROXY'. + proxy_info = getproxies() + mounts: dict[str, str | None] = {} + + for scheme in ("http", "https", "all"): + if proxy_info.get(scheme): + hostname = proxy_info[scheme] + mounts[f"{scheme}://"] = ( + hostname if "://" in hostname else f"http://{hostname}" + ) + + no_proxy_hosts = [host.strip() for host in proxy_info.get("no", "").split(",")] + for hostname in no_proxy_hosts: + # See https://curl.haxx.se/libcurl/c/CURLOPT_NOPROXY.html for details + # on how names in `NO_PROXY` are handled. + if hostname == "*": + # If NO_PROXY=* is used or if "*" occurs as any one of the comma + # separated hostnames, then we should just bypass any information + # from HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, and always ignore + # proxies. + return {} + elif hostname: + # NO_PROXY=.google.com is marked as "all://*.google.com, + # which disables "www.google.com" but not "google.com" + # NO_PROXY=google.com is marked as "all://*google.com, + # which disables "www.google.com" and "google.com". + # (But not "wwwgoogle.com") + # NO_PROXY can include domains, IPv6, IPv4 addresses and "localhost" + # NO_PROXY=example.com,::1,localhost,192.168.0.0/16 + if "://" in hostname: + mounts[hostname] = None + elif is_ipv4_hostname(hostname): + mounts[f"all://{hostname}"] = None + elif is_ipv6_hostname(hostname): + mounts[f"all://[{hostname}]"] = None + elif hostname.lower() == "localhost": + mounts[f"all://{hostname}"] = None + else: + mounts[f"all://*{hostname}"] = None + + return mounts + + +def to_bytes(value: str | bytes, encoding: str = "utf-8") -> bytes: + return value.encode(encoding) if isinstance(value, str) else value + + +def to_str(value: str | bytes, encoding: str = "utf-8") -> str: + return value if isinstance(value, str) else value.decode(encoding) + + +def to_bytes_or_str(value: str, match_type_of: typing.AnyStr) -> typing.AnyStr: + return value if isinstance(match_type_of, str) else value.encode() + + +def unquote(value: str) -> str: + return value[1:-1] if value[0] == value[-1] == '"' else value + + +def peek_filelike_length(stream: typing.Any) -> int | None: + """ + Given a file-like stream object, return its length in number of bytes + without reading it into memory. + """ + try: + # Is it an actual file? + fd = stream.fileno() + # Yup, seems to be an actual file. + length = os.fstat(fd).st_size + except (AttributeError, OSError): + # No... Maybe it's something that supports random access, like `io.BytesIO`? + try: + # Assuming so, go to end of stream to figure out its length, + # then put it back in place. + offset = stream.tell() + length = stream.seek(0, os.SEEK_END) + stream.seek(offset) + except (AttributeError, OSError): + # Not even that? Sorry, we're doomed... + return None + + return length + + +class URLPattern: + """ + A utility class currently used for making lookups against proxy keys... + + # Wildcard matching... + >>> pattern = URLPattern("all://") + >>> pattern.matches(httpx.URL("http://example.com")) + True + + # Witch scheme matching... + >>> pattern = URLPattern("https://") + >>> pattern.matches(httpx.URL("https://example.com")) + True + >>> pattern.matches(httpx.URL("http://example.com")) + False + + # With domain matching... + >>> pattern = URLPattern("https://example.com") + >>> pattern.matches(httpx.URL("https://example.com")) + True + >>> pattern.matches(httpx.URL("http://example.com")) + False + >>> pattern.matches(httpx.URL("https://other.com")) + False + + # Wildcard scheme, with domain matching... + >>> pattern = URLPattern("all://example.com") + >>> pattern.matches(httpx.URL("https://example.com")) + True + >>> pattern.matches(httpx.URL("http://example.com")) + True + >>> pattern.matches(httpx.URL("https://other.com")) + False + + # With port matching... + >>> pattern = URLPattern("https://example.com:1234") + >>> pattern.matches(httpx.URL("https://example.com:1234")) + True + >>> pattern.matches(httpx.URL("https://example.com")) + False + """ + + def __init__(self, pattern: str) -> None: + from ._urls import URL + + if pattern and ":" not in pattern: + raise ValueError( + f"Proxy keys should use proper URL forms rather " + f"than plain scheme strings. " + f'Instead of "{pattern}", use "{pattern}://"' + ) + + url = URL(pattern) + self.pattern = pattern + self.scheme = "" if url.scheme == "all" else url.scheme + self.host = "" if url.host == "*" else url.host + self.port = url.port + if not url.host or url.host == "*": + self.host_regex: typing.Pattern[str] | None = None + elif url.host.startswith("*."): + # *.example.com should match "www.example.com", but not "example.com" + domain = re.escape(url.host[2:]) + self.host_regex = re.compile(f"^.+\\.{domain}$") + elif url.host.startswith("*"): + # *example.com should match "www.example.com" and "example.com" + domain = re.escape(url.host[1:]) + self.host_regex = re.compile(f"^(.+\\.)?{domain}$") + else: + # example.com should match "example.com" but not "www.example.com" + domain = re.escape(url.host) + self.host_regex = re.compile(f"^{domain}$") + + def matches(self, other: URL) -> bool: + if self.scheme and self.scheme != other.scheme: + return False + if ( + self.host + and self.host_regex is not None + and not self.host_regex.match(other.host) + ): + return False + if self.port is not None and self.port != other.port: + return False + return True + + @property + def priority(self) -> tuple[int, int, int]: + """ + The priority allows URLPattern instances to be sortable, so that + we can match from most specific to least specific. + """ + # URLs with a port should take priority over URLs without a port. + port_priority = 0 if self.port is not None else 1 + # Longer hostnames should match first. + host_priority = -len(self.host) + # Longer schemes should match first. + scheme_priority = -len(self.scheme) + return (port_priority, host_priority, scheme_priority) + + def __hash__(self) -> int: + return hash(self.pattern) + + def __lt__(self, other: URLPattern) -> bool: + return self.priority < other.priority + + def __eq__(self, other: typing.Any) -> bool: + return isinstance(other, URLPattern) and self.pattern == other.pattern + + +def is_ipv4_hostname(hostname: str) -> bool: + try: + ipaddress.IPv4Address(hostname.split("/")[0]) + except Exception: + return False + return True + + +def is_ipv6_hostname(hostname: str) -> bool: + try: + ipaddress.IPv6Address(hostname.split("/")[0]) + except Exception: + return False + return True diff --git a/source/target-vendors/cp312/httpx/py.typed b/source/target-vendors/cp312/httpx/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/httpx_sse-0.4.3.dist-info/METADATA b/source/target-vendors/cp312/httpx_sse-0.4.3.dist-info/METADATA new file mode 100644 index 0000000..41f3bf1 --- /dev/null +++ b/source/target-vendors/cp312/httpx_sse-0.4.3.dist-info/METADATA @@ -0,0 +1,351 @@ +Metadata-Version: 2.4 +Name: httpx-sse +Version: 0.4.3 +Summary: Consume Server-Sent Event (SSE) messages with HTTPX. +Author-email: Florimond Manca +License: MIT +Project-URL: Homepage, https://github.com/florimondmanca/httpx-sse +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Requires-Python: >=3.9 +Description-Content-Type: text/markdown +License-File: LICENSE +Dynamic: license-file + +# httpx-sse + +[![Build Status](https://dev.azure.com/florimondmanca/public/_apis/build/status/florimondmanca.httpx-sse?branchName=master)](https://dev.azure.com/florimondmanca/public/_build?definitionId=19) +[![Coverage](https://codecov.io/gh/florimondmanca/httpx-sse/branch/master/graph/badge.svg)](https://codecov.io/gh/florimondmanca/httpx-sse) +[![Package version](https://badge.fury.io/py/httpx-sse.svg)](https://pypi.org/project/httpx-sse) + +Consume [Server-Sent Event (SSE)](https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events) messages with [HTTPX](https://www.python-httpx.org). + +**Table of contents** + +- [Installation](#installation) +- [Quickstart](#quickstart) +- [How-To](#how-to) +- [API Reference](#api-reference) + +## Installation + +**NOTE**: This is beta software. Please be sure to pin your dependencies. + +```bash +pip install httpx-sse=="0.4.*" +``` + +## Quickstart + +`httpx-sse` provides the [`connect_sse`](#connect_sse) and [`aconnect_sse`](#aconnect_sse) helpers for connecting to an SSE endpoint. The resulting [`EventSource`](#eventsource) object exposes the [`.iter_sse()`](#iter_sse) and [`.aiter_sse()`](#aiter_sse) methods to iterate over the server-sent events. + +Example usage: + +```python +import httpx +from httpx_sse import connect_sse + +with httpx.Client() as client: + with connect_sse(client, "GET", "http://localhost:8000/sse") as event_source: + for sse in event_source.iter_sse(): + print(sse.event, sse.data, sse.id, sse.retry) +``` + +You can try this against this example Starlette server ([credit](https://sysid.github.io/sse/)): + +```python +# Requirements: pip install uvicorn starlette sse-starlette +import asyncio +import uvicorn +from starlette.applications import Starlette +from starlette.routing import Route +from sse_starlette.sse import EventSourceResponse + +async def numbers(minimum, maximum): + for i in range(minimum, maximum + 1): + await asyncio.sleep(0.9) + yield {"data": i} + +async def sse(request): + generator = numbers(1, 5) + return EventSourceResponse(generator) + +routes = [ + Route("/sse", endpoint=sse) +] + +app = Starlette(routes=routes) + +if __name__ == "__main__": + uvicorn.run(app) +``` + +## How-To + +### Calling into Python web apps + +You can [call into Python web apps](https://www.python-httpx.org/async/#calling-into-python-web-apps) with HTTPX and `httpx-sse` to test SSE endpoints directly. + +Here's an example of calling into a Starlette ASGI app... + +```python +import asyncio + +import httpx +from httpx_sse import aconnect_sse +from sse_starlette.sse import EventSourceResponse +from starlette.applications import Starlette +from starlette.routing import Route + +async def auth_events(request): + async def events(): + yield { + "event": "login", + "data": '{"user_id": "4135"}', + } + + return EventSourceResponse(events()) + +app = Starlette(routes=[Route("/sse/auth/", endpoint=auth_events)]) + +async def main(): + async with httpx.AsyncClient(transport=httpx.ASGITransport(app)) as client: + async with aconnect_sse( + client, "GET", "http://localhost:8000/sse/auth/" + ) as event_source: + events = [sse async for sse in event_source.aiter_sse()] + (sse,) = events + assert sse.event == "login" + assert sse.json() == {"user_id": "4135"} + +asyncio.run(main()) +``` + +### Handling reconnections + +_(Advanced)_ + +`SSETransport` and `AsyncSSETransport` don't have reconnection built-in. This is because how to perform retries is generally dependent on your use case. As a result, if the connection breaks while attempting to read from the server, you will get an `httpx.ReadError` from `iter_sse()` (or `aiter_sse()`). + +However, `httpx-sse` does allow implementing reconnection by using the `Last-Event-ID` and reconnection time (in milliseconds), exposed as `sse.id` and `sse.retry` respectively. + +Here's how you might achieve this using [`stamina`](https://github.com/hynek/stamina)... + +```python +import time +from typing import Iterator + +import httpx +from httpx_sse import connect_sse, ServerSentEvent +from stamina import retry + +def iter_sse_retrying(client, method, url): + last_event_id = "" + reconnection_delay = 0.0 + + # `stamina` will apply jitter and exponential backoff on top of + # the `retry` reconnection delay sent by the server. + @retry(on=httpx.ReadError) + def _iter_sse(): + nonlocal last_event_id, reconnection_delay + + time.sleep(reconnection_delay) + + headers = {"Accept": "text/event-stream"} + + if last_event_id: + headers["Last-Event-ID"] = last_event_id + + with connect_sse(client, method, url, headers=headers) as event_source: + for sse in event_source.iter_sse(): + last_event_id = sse.id + + if sse.retry is not None: + reconnection_delay = sse.retry / 1000 + + yield sse + + return _iter_sse() +``` + +Usage: + +```python +with httpx.Client() as client: + for sse in iter_sse_retrying(client, "GET", "http://localhost:8000/sse"): + print(sse.event, sse.data) +``` + +## API Reference + +### `connect_sse` + +```python +def connect_sse( + client: httpx.Client, + method: str, + url: Union[str, httpx.URL], + **kwargs, +) -> ContextManager[EventSource] +``` + +Connect to an SSE endpoint and return an [`EventSource`](#eventsource) context manager. + +This sets `Cache-Control: no-store` on the request, as per the SSE spec, as well as `Accept: text/event-stream`. + +If the response `Content-Type` is not `text/event-stream`, this will raise an [`SSEError`](#sseerror). + +### `aconnect_sse` + +```python +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: Union[str, httpx.URL], + **kwargs, +) -> AsyncContextManager[EventSource] +``` + +An async equivalent to [`connect_sse`](#connect_sse). + +### `EventSource` + +```python +def __init__(response: httpx.Response) +``` + +Helper for working with an SSE response. + +#### `response` + +The underlying [`httpx.Response`](https://www.python-httpx.org/api/#response). + +You may use this to perform more operations and checks on the response, such as checking for HTTP status errors: + +```python +with connect_sse(...) as event_source: + event_source.response.raise_for_status() + + for sse in event_source.iter_sse(): + ... +``` + +#### `iter_sse` + +```python +def iter_sse() -> Iterator[ServerSentEvent] +``` + +Decode the response content and yield corresponding [`ServerSentEvent`](#serversentevent). + +Example usage: + +```python +for sse in event_source.iter_sse(): + ... +``` + +#### `aiter_sse` + +```python +async def iter_sse() -> AsyncIterator[ServerSentEvent] +``` + +An async equivalent to `iter_sse`. + +### `ServerSentEvent` + +Represents a server-sent event. + +* `event: str` - Defaults to `"message"`. +* `data: str` - Defaults to `""`. +* `id: str` - Defaults to `""`. +* `retry: str | None` - Defaults to `None`. + +Methods: + +* `json() -> Any` - Returns `sse.data` decoded as JSON. + +### `SSEError` + +An error that occurred while making a request to an SSE endpoint. + +Parents: + +* `httpx.TransportError` + +## License + +MIT + +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## 0.4.3 - 2025-10-10 + +### Fixed + +* Fix performance issue introduced by the improved line parsing from release 0.4.2. (Pull #40) + +## 0.4.2 - 2025-10-07 + +### Fixed + +* Fix incorrect newline parsing that was not compliant with SSE spec. (Pull #37) + +## 0.4.1 - 2025-06-24 + +### Fixed + +* Always close the response async generator in `aiter_sse()`. (Pull #30) + +## 0.4.0 - 2023-12-22 + +### Removed + +* Dropped Python 3.7 support, as it has reached EOL. (Pull #21) + +### Added + +* Add official support for Python 3.12. (Pull #21) + +### Fixed + +* Allow `Content-Type` that contain but are not strictly `text/event-stream`. (Pull #22 by @dbuades) +* Improve error message when `Content-Type` is missing. (Pull #20 by @jamesbraza) + +## 0.3.1 - 2023-06-01 + +### Added + +* Add `__repr__()` for `ServerSentEvent` model, which may help with debugging and other tasks. (Pull #16) + +## 0.3.0 - 2023-04-27 + +### Changed + +* Raising an `SSEError` if the response content type is not `text/event-stream` is now performed as part of `iter_sse()` / `aiter_sse()`, instead of `connect_sse()` / `aconnect_sse()`. This allows inspecting the response before iterating on server-sent events, such as checking for error responses. (Pull #12) + +## 0.2.0 - 2023-03-27 + +### Changed + +* `connect_sse()` and `aconnect_sse()` now require a `method` argument: `connect_sse(client, "GET", "https://example.org")`. This provides support for SSE requests with HTTP verbs other than `GET`. (Pull #7) + +## 0.1.0 - 2023-02-05 + +_Initial release_ + +### Added + +* Add `connect_sse`, `aconnect_sse()`, `ServerSentEvent` and `SSEError`. diff --git a/source/target-vendors/cp312/httpx_sse-0.4.3.dist-info/RECORD b/source/target-vendors/cp312/httpx_sse-0.4.3.dist-info/RECORD new file mode 100644 index 0000000..dc042e8 --- /dev/null +++ b/source/target-vendors/cp312/httpx_sse-0.4.3.dist-info/RECORD @@ -0,0 +1,11 @@ +httpx_sse/__init__.py,sha256=G-F8jpJLo4eJwKvKt6ejTQYPhBHgmksZTNuaQySRAjc,282 +httpx_sse/_api.py,sha256=D7pfkajmdHlms75OV8HpdOrOEr5NbWQlDwMfSFcJ8FU,2919 +httpx_sse/_decoders.py,sha256=na1XyVBiCLbRE8tg0o713U5Gz-m9fBxYPfSMmu_xns8,4005 +httpx_sse/_exceptions.py,sha256=sFZBUT1xIM1DvYQ6ckpzrhBG9d8VffFSZFMIEhv1N58,62 +httpx_sse/_models.py,sha256=4WwGeBEGMR2nbROTZ7PyVlu7chmWg7tbkJyXyd5DTx0,1221 +httpx_sse/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +httpx_sse-0.4.3.dist-info/licenses/LICENSE,sha256=vuxn5O6Dp68m8kK-2Zh6qGPW0t-3dhBqojYMQYfzGQE,1072 +httpx_sse-0.4.3.dist-info/METADATA,sha256=1MbYBO5axG6bO2WVwnq4riunz7X2kseLFv9GPkvRqB4,9671 +httpx_sse-0.4.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91 +httpx_sse-0.4.3.dist-info/top_level.txt,sha256=hbK3kwBH9d-rEZQvJ6yBawdDylIKfJ8fh0CM4EVNs04,10 +httpx_sse-0.4.3.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/httpx_sse-0.4.3.dist-info/WHEEL b/source/target-vendors/cp312/httpx_sse-0.4.3.dist-info/WHEEL new file mode 100644 index 0000000..e7fa31b --- /dev/null +++ b/source/target-vendors/cp312/httpx_sse-0.4.3.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (80.9.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/source/target-vendors/cp312/httpx_sse-0.4.3.dist-info/licenses/LICENSE b/source/target-vendors/cp312/httpx_sse-0.4.3.dist-info/licenses/LICENSE new file mode 100644 index 0000000..36600eb --- /dev/null +++ b/source/target-vendors/cp312/httpx_sse-0.4.3.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Florimond Manca + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/source/target-vendors/cp312/httpx_sse-0.4.3.dist-info/top_level.txt b/source/target-vendors/cp312/httpx_sse-0.4.3.dist-info/top_level.txt new file mode 100644 index 0000000..a5e926f --- /dev/null +++ b/source/target-vendors/cp312/httpx_sse-0.4.3.dist-info/top_level.txt @@ -0,0 +1 @@ +httpx_sse diff --git a/source/target-vendors/cp312/httpx_sse/__init__.py b/source/target-vendors/cp312/httpx_sse/__init__.py new file mode 100644 index 0000000..b83cf09 --- /dev/null +++ b/source/target-vendors/cp312/httpx_sse/__init__.py @@ -0,0 +1,14 @@ +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.3" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/source/target-vendors/cp312/httpx_sse/_api.py b/source/target-vendors/cp312/httpx_sse/_api.py new file mode 100644 index 0000000..017ed5e --- /dev/null +++ b/source/target-vendors/cp312/httpx_sse/_api.py @@ -0,0 +1,93 @@ +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx + +from ._decoders import SSEDecoder, SSELineDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + "Expected response header Content-Type to contain 'text/event-stream', " + f"got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + for line in _iter_sse_lines(self._response): + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], _aiter_sse_lines(self._response)) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse( + client: httpx.Client, method: str, url: str, **kwargs: Any +) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +async def _aiter_sse_lines(response: httpx.Response) -> AsyncIterator[str]: + decoder = SSELineDecoder() + async for text in response.aiter_text(): + for line in decoder.decode(text): + yield line + for line in decoder.flush(): + yield line + + +def _iter_sse_lines(response: httpx.Response) -> Iterator[str]: + decoder = SSELineDecoder() + for text in response.iter_text(): + for line in decoder.decode(text): + yield line + for line in decoder.flush(): + yield line diff --git a/source/target-vendors/cp312/httpx_sse/_decoders.py b/source/target-vendors/cp312/httpx_sse/_decoders.py new file mode 100644 index 0000000..287e384 --- /dev/null +++ b/source/target-vendors/cp312/httpx_sse/_decoders.py @@ -0,0 +1,139 @@ +from typing import List, Optional + +from ._models import ServerSentEvent + + +def _splitlines_sse(text: str) -> List[str]: + """Split text on \r\n, \r, or \n only.""" + if not text: + return [] + + if "\r" not in text: + lines = text.split("\n") + else: + normalized = text.replace("\r\n", "\n").replace("\r", "\n") + lines = normalized.split("\n") + + if text[-1] in "\r\n": + lines.pop() + + return lines + + +class SSELineDecoder: + """ + Handles incrementally reading lines from text. + + Mostly a copy of httpx._decoders.LineDecoder, but as per SSE spec, only \r\n, \r, + and \n are treated as newlines, which differs from the behavior of splitlines() + used by httpx._decoders.LineDecoder. + """ + + def __init__(self) -> None: + self.buffer: list[str] = [] + self.trailing_cr: bool = False + + def decode(self, text: str) -> list[str]: + # We always push a trailing `\r` into the next decode iteration. + if self.trailing_cr: + text = "\r" + text + self.trailing_cr = False + if text.endswith("\r"): + self.trailing_cr = True + text = text[:-1] + + if not text: + # NOTE: the edge case input of empty text doesn't occur in practice, + # because other httpx internals filter out this value + return [] # pragma: no cover + + trailing_newline = text[-1] in "\n\r" + lines = _splitlines_sse(text) + + if len(lines) == 1 and not trailing_newline: + # No new lines, buffer the input and continue. + self.buffer.append(lines[0]) + return [] + + if self.buffer: + # Include any existing buffer in the first portion of the + # splitlines result. + lines = ["".join(self.buffer) + lines[0]] + lines[1:] + self.buffer = [] + + if not trailing_newline: + # If the last segment of splitlines is not newline terminated, + # then drop it from our output and start a new buffer. + self.buffer = [lines.pop()] + + return lines + + def flush(self) -> list[str]: + if not self.buffer and not self.trailing_cr: + return [] + + lines = ["".join(self.buffer)] + self.buffer = [] + self.trailing_cr = False + return lines + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if ( + not self._event + and not self._data + and not self._last_event_id + and self._retry is None + ): + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/source/target-vendors/cp312/httpx_sse/_exceptions.py b/source/target-vendors/cp312/httpx_sse/_exceptions.py new file mode 100644 index 0000000..cd2c4d2 --- /dev/null +++ b/source/target-vendors/cp312/httpx_sse/_exceptions.py @@ -0,0 +1,5 @@ +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/source/target-vendors/cp312/httpx_sse/_models.py b/source/target-vendors/cp312/httpx_sse/_models.py new file mode 100644 index 0000000..d4dc119 --- /dev/null +++ b/source/target-vendors/cp312/httpx_sse/_models.py @@ -0,0 +1,54 @@ +import json +from typing import Any, Optional + + +class ServerSentEvent: + def __init__( + self, + event: Optional[str] = None, + data: Optional[str] = None, + id: Optional[str] = None, + retry: Optional[int] = None, + ) -> None: + if not event: + event = "message" + + if data is None: + data = "" + + if id is None: + id = "" + + self._event = event + self._data = data + self._id = id + self._retry = retry + + @property + def event(self) -> str: + return self._event + + @property + def data(self) -> str: + return self._data + + @property + def id(self) -> str: + return self._id + + @property + def retry(self) -> Optional[int]: + return self._retry + + def json(self) -> Any: + return json.loads(self.data) + + def __repr__(self) -> str: + pieces = [f"event={self.event!r}"] + if self.data != "": + pieces.append(f"data={self.data!r}") + if self.id != "": + pieces.append(f"id={self.id!r}") + if self.retry is not None: + pieces.append(f"retry={self.retry!r}") + return f"ServerSentEvent({', '.join(pieces)})" diff --git a/source/target-vendors/cp312/httpx_sse/py.typed b/source/target-vendors/cp312/httpx_sse/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/idna-3.20.dist-info/METADATA b/source/target-vendors/cp312/idna-3.20.dist-info/METADATA new file mode 100644 index 0000000..edbc446 --- /dev/null +++ b/source/target-vendors/cp312/idna-3.20.dist-info/METADATA @@ -0,0 +1,176 @@ +Metadata-Version: 2.5 +Name: idna +Version: 3.20 +Summary: Internationalized Domain Names in Applications (IDNA) +Author-email: Kim Davies +Requires-Python: >=3.9 +Description-Content-Type: text/markdown +License-Expression: BSD-3-Clause +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: System Administrators +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: 3.15 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Internet :: Name Service (DNS) +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Utilities +License-File: LICENSE.md +Requires-Dist: ruff >= 0.16.0 ; extra == "all" +Requires-Dist: mypy >= 1.11.2 ; extra == "all" +Requires-Dist: ty >= 0.0.37 ; extra == "all" +Requires-Dist: pytest >= 8.3.2 ; extra == "all" +Requires-Dist: hypothesis >= 6.141.1 ; extra == "all" +Requires-Dist: coverage >= 7.10.0 ; extra == "all" +Project-URL: Changelog, https://github.com/kjd/idna/blob/master/HISTORY.md +Project-URL: Issue tracker, https://github.com/kjd/idna/issues +Project-URL: Source, https://github.com/kjd/idna +Provides-Extra: all +Import-Name: idna + +# Internationalized Domain Names in Applications (IDNA) + +Support for [Internationalized Domain Names in Applications +(IDNA)](https://tools.ietf.org/html/rfc5891) and [Unicode IDNA +Compatibility Processing](https://unicode.org/reports/tr46/). It +supersedes the standard library's `encodings.idna`, which only +implements the 2003 specification, offering broader script coverage and +limiting domains with known security vulnerabilities. + +## Usage + +Package may be installed from [PyPI](https://pypi.org/project/idna/) via +the typical methods (e.g. `python3 -m pip install idna`) + +For typical usage, the `encode` and `decode` functions will take a +domain name argument and perform a conversion to ASCII-compatible encoding +(known as A-labels), or to Unicode strings (known as U-labels) +respectively. + +```pycon +>>> import idna +>>> idna.encode('ドメイン.テスト') +b'xn--eckwd4c7c.xn--zckzah' +>>> print(idna.decode('xn--eckwd4c7c.xn--zckzah')) +ドメイン.テスト +``` + +Conversions can be applied at a per-label basis using the `ulabel` or +`alabel` functions for specialized use cases. + + +### Compatibility Mapping (UTS #46) + +This library provides support for [Unicode IDNA Compatibility +Processing](https://unicode.org/reports/tr46/) which normalizes input from +different potential ways a user may input a domain prior to performing the IDNA +conversion operations. This functionality, known as a +[mapping](https://tools.ietf.org/html/rfc5895), is considered by the +specification to be a local user-interface issue distinct from IDNA +conversion functionality. + +For example, "Königsgäßchen" is not a permissible label as capital letters +are not allowed. UTS #46 will convert this into lower case prior to applying +the IDNA conversion. + +```pycon +>>> import idna +>>> idna.encode('Königsgäßchen') +... +idna.core.InvalidCodepoint: Codepoint U+004B at position 1 of 'Königsgäßchen' not allowed +>>> idna.encode('Königsgäßchen', uts46=True) +b'xn--knigsgchen-b4a3dun' +>>> idna.decode('xn--knigsgchen-b4a3dun') +'königsgäßchen' +``` + +When performing a decode operation for display purposes, `decode()` +accepts a `display=True` argument that leaves any `xn--` label that +fails to decode unchanged. This is useful for user interface display +where a domain is in use, the A-label form can be presented when it +is not a valid IDN. + + +## Exceptions + +All errors raised during conversion derive from the `idna.IDNAError` +base class. The more specific exceptions are: + +* `idna.IDNABidiError` — raised when a label contains an illegal + combination of left-to-right and right-to-left characters. +* `idna.InvalidCodepoint` — raised when a label contains a codepoint + that is INVALID for IDNA. +* `idna.InvalidCodepointContext` — raised when a CONTEXTO or CONTEXTJ + codepoint appears in a position whose contextual requirements are + not satisfied. + +Exceptions carry machine-readable attributes so that applications do +not need to parse the message: `code` is a short, stable identifier +for the rule that failed (such as `disallowed_codepoint` or +`label_too_long`); and, when the failure can be attributed to a +particular character, `text` (the label or domain being validated), +`codepoint` (the offending codepoint as an integer) and `position` +are set. + + +## Command-line tool + +The package supports command-line usage to convert domain names +between their Unicode and ASCII-compatible forms. It can be run either +as a module (`python3 -m idna`) or, once installed (such as with `uv +tool` or `pipx`), via the `idna` script: + +```bash +$ uv tool install idna +$ idna xn--e1afmkfd.xn--p1ai +пример.рф +$ idna пример.рф +xn--e1afmkfd.xn--p1ai +``` + +Mode can be specified with `-e`/`--encode` or `-d`/`--decode`, otherwise +it will be chosen automatically based on the first input. Multiple +domains can be supplied either as arguments or through standard input. +UTS #46 mapping is applied by default, which lets the tool accept +inputs that aren't strictly valid IDNA 2008 by normalising them first, +pass `--strict` to disable UTS #46. + +Conversion failures are reported on stderr together with the +offending input; processing continues with the remaining domains and +the tool exits with a non-zero status if any conversion failed. + + +## Additional Notes + +* **Python version support**. This library supports Python 3.9 and higher. + As this library serves as a low-level toolkit for a variety of + applications, we strive to support all versions of Python that are + not beyond end-of-life. Free-threaded Python is also supported, + as the library holds no mutable global state the functions can be + called concurrently from multiple threads. + +* **Unicode version**. The IDNA and UTS #46 lookup tables are generated + from a specific Unicode release. Some Unicode data depends on the + running Python's `unicodedata` module, so on an older Python a + character new to Unicode may be rejected as unknown even if this + library knows about it. + +* **Emoji**. It is an occasional request to support emoji domains in + this library. Encoding of symbols like emoji is expressly prohibited by + the IDNA technical standard, and emoji domains are broadly phased + out across the domain industry due to associated security risks. + +* **Regenerating lookup tables**. The IDNA and UTS #46 functionality + relies upon pre-calculated lookup tables, generated using the + `idna-data` script in [`tools/`](tools/README.md). + diff --git a/source/target-vendors/cp312/idna-3.20.dist-info/RECORD b/source/target-vendors/cp312/idna-3.20.dist-info/RECORD new file mode 100644 index 0000000..1e93cc6 --- /dev/null +++ b/source/target-vendors/cp312/idna-3.20.dist-info/RECORD @@ -0,0 +1,16 @@ +idna/__init__.py,sha256=hRTD7VMTajWW699RL6SHu919pama3K7YLgNj0sMG068,944 +idna/__main__.py,sha256=4JMK66Wj4uLZTKbF-sT3LAxOsr6buig77PmOkJCRRxw,83 +idna/cli.py,sha256=LgGXKiXmev37pdgkATwB6AiJORZTnA2zP3LzfEK4iWo,4245 +idna/codec.py,sha256=ff9CR9debiZcvqEmHxDRIrqSmaNxQpz6AY4JHsC3YC0,8002 +idna/compat.py,sha256=T_eZXOUt0b5Tga56cPybQu9CCYK0FTK1mdiPb96na8A,1376 +idna/core.py,sha256=PdTD_-5P8iMgf6xl84EqHmiVrorR56btYBFXaPNVFSA,32580 +idna/idnadata.py,sha256=I6mowdFd6cY00L8hPXeYEPSFCD_hsPfgkEStBKacx-s,45763 +idna/intranges.py,sha256=g49scLSkqJtAhLmOODa7hVHriSjmb60tiTsEoocJdBI,1851 +idna/package_data.py,sha256=rnWVNKPFgqAKyFbPykZKOe6CUdOTII2K8GtgW8IOAqk,21 +idna/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +idna/uts46data.py,sha256=dw6Em_oVbHGCiolEClf-mupwc18QOuRhbuT2guE9bAA,237174 +idna-3.20.dist-info/entry_points.txt,sha256=7H3nGOHap3jnLE5e7q7Ywr9Vq8axB7WIj5-C_4N2vhw,38 +idna-3.20.dist-info/licenses/LICENSE.md,sha256=GppPDj1HmickDd1ZqRN6ZqtKD539yMphiMwL_YUYfwQ,1541 +idna-3.20.dist-info/WHEEL,sha256=lqN8jXt_QloFOGcx9kStvWbryhfEXoI2F2z-YURm350,81 +idna-3.20.dist-info/METADATA,sha256=29jBTB5MoeDJgkptvHy794iE8461LZEUjNMCX2ceS4U,7207 +idna-3.20.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/idna-3.20.dist-info/WHEEL b/source/target-vendors/cp312/idna-3.20.dist-info/WHEEL new file mode 100644 index 0000000..1ab9a08 --- /dev/null +++ b/source/target-vendors/cp312/idna-3.20.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 4.1.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/source/target-vendors/cp312/idna-3.20.dist-info/entry_points.txt b/source/target-vendors/cp312/idna-3.20.dist-info/entry_points.txt new file mode 100644 index 0000000..59ca7ac --- /dev/null +++ b/source/target-vendors/cp312/idna-3.20.dist-info/entry_points.txt @@ -0,0 +1,3 @@ +[console_scripts] +idna=idna.cli:main + diff --git a/source/target-vendors/cp312/idna-3.20.dist-info/licenses/LICENSE.md b/source/target-vendors/cp312/idna-3.20.dist-info/licenses/LICENSE.md new file mode 100644 index 0000000..f706835 --- /dev/null +++ b/source/target-vendors/cp312/idna-3.20.dist-info/licenses/LICENSE.md @@ -0,0 +1,31 @@ +BSD 3-Clause License + +Copyright (c) 2013-2026, Kim Davies and contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/source/target-vendors/cp312/idna/__init__.py b/source/target-vendors/cp312/idna/__init__.py new file mode 100644 index 0000000..d6c890a --- /dev/null +++ b/source/target-vendors/cp312/idna/__init__.py @@ -0,0 +1,47 @@ +from .core import ( + IDNABidiError, + IDNAError, + InvalidCodepoint, + InvalidCodepointContext, + alabel, + check_bidi, + check_hyphen_ok, + check_initial_combiner, + check_label, + check_nfc, + decode, + encode, + ulabel, + uts46_remap, + valid_contextj, + valid_contexto, + valid_label_length, + valid_string_length, +) +from .idnadata import __version__ as unicode_version +from .intranges import intranges_contain +from .package_data import __version__ + +__all__ = [ + "__version__", + "unicode_version", + "IDNABidiError", + "IDNAError", + "InvalidCodepoint", + "InvalidCodepointContext", + "alabel", + "check_bidi", + "check_hyphen_ok", + "check_initial_combiner", + "check_label", + "check_nfc", + "decode", + "encode", + "intranges_contain", + "ulabel", + "uts46_remap", + "valid_contextj", + "valid_contexto", + "valid_label_length", + "valid_string_length", +] diff --git a/source/target-vendors/cp312/idna/__main__.py b/source/target-vendors/cp312/idna/__main__.py new file mode 100644 index 0000000..dbdd066 --- /dev/null +++ b/source/target-vendors/cp312/idna/__main__.py @@ -0,0 +1,6 @@ +import sys + +from .cli import main + +if __name__ == "__main__": + sys.exit(main()) diff --git a/source/target-vendors/cp312/idna/cli.py b/source/target-vendors/cp312/idna/cli.py new file mode 100644 index 0000000..894d8a0 --- /dev/null +++ b/source/target-vendors/cp312/idna/cli.py @@ -0,0 +1,132 @@ +"""Command-line interface for the :mod:`idna` package. + +Invoked via ``python -m idna``. See :func:`main` for the entry point. +""" + +from __future__ import annotations + +import argparse +import sys +from itertools import chain +from typing import IO, TYPE_CHECKING + +from . import IDNAError, decode, encode, unicode_version +from .core import _alabel_prefix, _unicode_dots_re +from .package_data import __version__ + +if TYPE_CHECKING: + from collections.abc import Iterable + + +def _looks_like_alabel(s: str) -> bool: + """Return True if any label in ``s`` carries the ``xn--`` ACE prefix.""" + prefix = _alabel_prefix.decode("ascii") + return any(label.lower().startswith(prefix) for label in _unicode_dots_re.split(s)) + + +def _build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="python -m idna", + description=( + "Convert a domain name between its Unicode (U-label) and " + "ASCII-compatible (A-label) forms. With no mode flag, the " + "direction is chosen from the first input — if it contains " + "an xn-- label the stream is decoded, otherwise it is " + "encoded — and the same mode is applied to every remaining " + "input. UTS #46 mapping is applied by default; pass " + "--strict to disable it. When no domains are given on the " + "command line and stdin is piped, one domain per line is " + "read from stdin." + ), + ) + mode = parser.add_mutually_exclusive_group() + mode.add_argument( + "-e", + "--encode", + dest="mode", + action="store_const", + const="encode", + help="Encode the input to its ASCII A-label form.", + ) + mode.add_argument( + "-d", + "--decode", + dest="mode", + action="store_const", + const="decode", + help="Decode the input from its ASCII A-label form.", + ) + parser.add_argument( + "--strict", + action="store_true", + help="Disable the default UTS #46 mapping and apply IDNA 2008 rules verbatim.", + ) + parser.add_argument( + "--version", + action="version", + version=f"idna {__version__} (Unicode {unicode_version})", + ) + parser.add_argument( + "domain", + nargs="*", + help="One or more domain names to convert. Omit to read from stdin.", + ) + return parser + + +def _iter_stdin(stream: IO[str]) -> Iterable[str]: + """Yield non-empty stripped lines from ``stream``, ignoring blanks.""" + for line in stream: + stripped = line.strip() + if stripped: + yield stripped + + +def _convert_one(domain: str, mode: str, uts46: bool) -> bool: + """Convert ``domain`` and write the result; return ``False`` on failure.""" + try: + if mode == "decode": + print(decode(domain, uts46=uts46)) + else: + print(encode(domain, uts46=uts46).decode("ascii")) + except IDNAError as err: + print(f"idna: {mode} failed for {domain!r}: {err}", file=sys.stderr) + return False + return True + + +def main(argv: list[str] | None = None) -> int: + """Entry point for ``python -m idna``. + + When more than one domain is supplied (via positional arguments or + piped stdin) and no mode flag is given, the first input determines + the direction and that mode is applied uniformly to the rest. + + :param argv: Argument list excluding the program name. Defaults to + :data:`sys.argv` when ``None``. + :returns: ``0`` on success, ``1`` if any conversion fails. + """ + parser = _build_parser() + args = parser.parse_args(argv) + uts46 = not args.strict + + if args.domain: + domains: Iterable[str] = args.domain + elif not sys.stdin.isatty(): + domains = _iter_stdin(sys.stdin) + else: + parser.error("a domain argument is required when stdin is a terminal") + + iterator = iter(domains) + first = next(iterator, None) + if first is None: + return 0 + + mode = args.mode or ("decode" if _looks_like_alabel(first) else "encode") + + results = [_convert_one(domain, mode, uts46) for domain in chain([first], iterator)] + return 0 if all(results) else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/source/target-vendors/cp312/idna/codec.py b/source/target-vendors/cp312/idna/codec.py new file mode 100644 index 0000000..8efc1ee --- /dev/null +++ b/source/target-vendors/cp312/idna/codec.py @@ -0,0 +1,225 @@ +from __future__ import annotations + +import codecs +from typing import Any + +from .core import IDNAError, _max_domain_length, _unicode_dots_re, alabel, decode, encode, ulabel + + +class Codec(codecs.Codec): + """Stateless IDNA 2008 codec. + + Implements the :class:`codecs.Codec` protocol so that the whole-domain + encoder (:func:`idna.encode`) and decoder (:func:`idna.decode`) are + accessible through the standard codec machinery as ``"idna2008"``. + + Only the ``"strict"`` error handler is supported; any other handler + raises :exc:`~idna.IDNAError`. + """ + + def encode(self, data: str, errors: str = "strict") -> tuple[bytes, int]: # ty: ignore[invalid-method-override] + if errors != "strict": + raise IDNAError(f'Unsupported error handling "{errors}"', code="unsupported_errors") + + if not data: + return b"", 0 + + return encode(data), len(data) + + def decode(self, data: bytes, errors: str = "strict") -> tuple[str, int]: # ty: ignore[invalid-method-override] + if errors != "strict": + raise IDNAError(f'Unsupported error handling "{errors}"', code="unsupported_errors") + + if not data: + return "", 0 + + return decode(data), len(data) + + +class IncrementalEncoder(codecs.BufferedIncrementalEncoder): + """Incremental IDNA 2008 encoder. + + Buffers a partial trailing label across calls until either the next + label separator is seen or ``final=True``, so that streamed input is + encoded one whole label at a time. Any of the four Unicode label + separators (``U+002E``, ``U+3002``, ``U+FF0E``, ``U+FF61``) ends a + label; the result always uses ``U+002E`` as the separator. + + The 253-octet domain length limit (254 with a trailing dot) that + :func:`idna.encode` enforces is applied to the accumulated output, so + that streaming a name and encoding it in one shot either both succeed + with the same result or both raise :exc:`~idna.IDNAError`. + + Only the ``"strict"`` error handler is supported. + """ + + def __init__(self, errors: str = "strict") -> None: + super().__init__(errors) + self._emitted = 0 # octets returned so far + self._trailing_dot = False # whether the output so far ends with "." + + def reset(self) -> None: + super().reset() + self._emitted = 0 + self._trailing_dot = False + + def getstate(self) -> Any: + if not self.buffer and not self._emitted: + return 0 + return (self.buffer, self._emitted, self._trailing_dot) + + def setstate(self, state: Any) -> None: + if state: + self.buffer, self._emitted, self._trailing_dot = state + else: + self.reset() + + def _buffer_encode(self, data: str, errors: str, final: bool) -> tuple[bytes, int]: # ty: ignore[invalid-method-override] + if errors != "strict": + raise IDNAError(f'Unsupported error handling "{errors}"', code="unsupported_errors") + + result_bytes = b"" + size = 0 + if data: + labels = _unicode_dots_re.split(data) + trailing_dot = b"" + if labels: + if not labels[-1]: + trailing_dot = b"." + del labels[-1] + elif not final: + # Keep potentially unfinished label until the next call + del labels[-1] + if labels: + trailing_dot = b"." + + result = [] + for label in labels: + result.append(alabel(label)) + if size: + size += 1 + size += len(label) + + result_bytes = b".".join(result) + trailing_dot + size += len(trailing_dot) + + # Mirror encode(): the whole name may not exceed 253 octets, or 254 + # when it ends with a dot. Until the input is final a trailing dot + # may still arrive, so only the 254-octet ceiling applies before then. + self._emitted += len(result_bytes) + if result_bytes: + self._trailing_dot = result_bytes.endswith(b".") + may_end_with_dot = self._trailing_dot or not final + if self._emitted > _max_domain_length + may_end_with_dot: + raise IDNAError("Domain too long", code="domain_too_long") + return result_bytes, size + + +class IncrementalDecoder(codecs.BufferedIncrementalDecoder): + """Incremental IDNA 2008 decoder. + + Buffers a partial trailing label across calls until either the next + label separator is seen or ``final=True``, so that streamed input is + decoded one whole label at a time. + + The 254-octet input length limit that :func:`idna.decode` enforces is + applied to the accumulated input, so that streaming a name and decoding + it in one shot either both succeed with the same result or both raise + :exc:`~idna.IDNAError`. + + Only the ``"strict"`` error handler is supported. + """ + + def __init__(self, errors: str = "strict") -> None: + super().__init__(errors) + self._consumed = 0 # input octets consumed so far + + def reset(self) -> None: + super().reset() + self._consumed = 0 + + def getstate(self) -> tuple[bytes, int]: + return (self.buffer, self._consumed) + + def setstate(self, state: tuple[bytes, int]) -> None: + self.buffer, self._consumed = state + + def _buffer_decode(self, data: Any, errors: str, final: bool) -> tuple[str, int]: # ty: ignore[invalid-method-override] + if errors != "strict": + raise IDNAError(f'Unsupported error handling "{errors}"', code="unsupported_errors") + + if not data: + return ("", 0) + + if not isinstance(data, str): + try: + data = str(data, "ascii") + except UnicodeDecodeError as err: + raise IDNAError("Invalid ASCII in A-label", code="invalid_ascii") from err + + # Mirror decode(), which rejects input longer than 254 characters + # (253 plus a possible trailing dot) before looking at any label. + # ``data`` is the unconsumed buffer plus the new input, so this is + # the total seen so far. + if self._consumed + len(data) > _max_domain_length + 1: + raise IDNAError("Domain too long", code="domain_too_long") + + labels = _unicode_dots_re.split(data) + trailing_dot = "" + if labels: + if not labels[-1]: + trailing_dot = "." + del labels[-1] + elif not final: + # Keep potentially unfinished label until the next call + del labels[-1] + if labels: + trailing_dot = "." + + result = [] + size = 0 + for label in labels: + result.append(ulabel(label)) + if size: + size += 1 + size += len(label) + + result_str = ".".join(result) + trailing_dot + size += len(trailing_dot) + self._consumed += size + return (result_str, size) + + +class StreamWriter(Codec, codecs.StreamWriter): + pass + + +class StreamReader(Codec, codecs.StreamReader): + pass + + +def search_function(name: str) -> codecs.CodecInfo | None: + """Codec search function registered with :mod:`codecs`. + + Returns a :class:`codecs.CodecInfo` for the ``"idna2008"`` codec name + so that ``str.encode("idna2008")`` and ``bytes.decode("idna2008")`` + invoke the IDNA 2008 codec defined in this module. + + :param name: The codec name being looked up. + :returns: A :class:`codecs.CodecInfo` instance if ``name`` is + ``"idna2008"``, otherwise ``None``. + """ + if name != "idna2008": + return None + return codecs.CodecInfo( + name=name, + encode=Codec().encode, + decode=Codec().decode, # type: ignore + incrementalencoder=IncrementalEncoder, + incrementaldecoder=IncrementalDecoder, + streamwriter=StreamWriter, + streamreader=StreamReader, + ) + + +codecs.register(search_function) diff --git a/source/target-vendors/cp312/idna/compat.py b/source/target-vendors/cp312/idna/compat.py new file mode 100644 index 0000000..5bedee9 --- /dev/null +++ b/source/target-vendors/cp312/idna/compat.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from typing import Any + +from .core import decode, encode + + +def ToASCII(label: str) -> bytes: + """Compatibility shim for :rfc:`3490` ``ToASCII``. + + Delegates to :func:`idna.encode` (IDNA 2008). Provided to ease porting + of code written against the legacy :mod:`encodings.idna` API; new code + should call :func:`idna.encode` directly. + + :param label: The label or domain to encode. + :returns: The encoded form as ASCII :class:`bytes`. + """ + return encode(label) + + +def ToUnicode(label: bytes | bytearray) -> str: + """Compatibility shim for :rfc:`3490` ``ToUnicode``. + + Delegates to :func:`idna.decode` (IDNA 2008). Provided to ease porting + of code written against the legacy :mod:`encodings.idna` API; new code + should call :func:`idna.decode` directly. + + :param label: The label or domain to decode. + :returns: The decoded Unicode form. + """ + return decode(label) + + +def nameprep(s: Any) -> None: + """Stub for :rfc:`3491` Nameprep, which is not used by IDNA 2008. + + IDNA 2008 (:rfc:`5891`) replaces Nameprep with the per-codepoint + validity classes from :rfc:`5892`; this function exists only to + return a clear error if legacy code attempts to call it. + + :raises NotImplementedError: Always. + """ + raise NotImplementedError("IDNA 2008 does not utilise nameprep protocol") diff --git a/source/target-vendors/cp312/idna/core.py b/source/target-vendors/cp312/idna/core.py new file mode 100644 index 0000000..d4a6f0e --- /dev/null +++ b/source/target-vendors/cp312/idna/core.py @@ -0,0 +1,863 @@ +from __future__ import annotations + +import bisect +import re +import unicodedata +import warnings +from typing import Literal + +from . import idnadata +from .intranges import intranges_contain + +_virama_combining_class = 9 +_alabel_prefix = b"xn--" +_max_input_length = 1024 +_max_domain_length = 253 # RFC 1035 octets, excluding any trailing dot +_STATUS_VALID, _STATUS_MAPPED, _STATUS_DEVIATION, _STATUS_IGNORED = b"VMDI" +_unicode_dots_re = re.compile("[\u002e\u3002\uff0e\uff61]") +_std3_disallowed_re = re.compile("[\x00-\x2c\x2f\x3a-\x40A-Z\x5b-\x60\x7b-\x7f]") +_bidi_rtl_first = frozenset({"R", "AL"}) +_bidi_rtl_categories = frozenset({"R", "AL", "AN"}) +_bidi_rtl_allowed = frozenset({"R", "AL", "AN", "EN", "ES", "CS", "ET", "ON", "BN", "NSM"}) +_bidi_rtl_valid_ending = frozenset({"R", "AL", "EN", "AN"}) +_bidi_rtl_numeric = frozenset({"AN", "EN"}) +_bidi_ltr_allowed = frozenset({"L", "EN", "ES", "CS", "ET", "ON", "BN", "NSM"}) +_bidi_ltr_valid_ending = frozenset({"L", "EN"}) +_bidi_joiner_l_or_d = frozenset({"L", "D"}) +_bidi_joiner_r_or_d = frozenset({"R", "D"}) + + +def _joining_type(cp: int) -> str | None: + for jt, ranges in idnadata.joining_types.items(): + if intranges_contain(cp, ranges): + return jt + return None + + +# Machine-readable identifiers for the rule an :class:`IDNAError` reports. +# These strings are stable and documented; exception message wording is not. +_ErrorCode = Literal[ + "input_too_long", + "label_too_long", + "domain_too_long", + "empty_label", + "empty_domain", + "not_nfc", + "hyphen_3_4", + "hyphen_start_end", + "leading_combiner", + "disallowed_codepoint", + "contextj", + "contexto", + "unknown_codepoint", + "bidi_rule_1", + "bidi_rule_2", + "bidi_rule_3", + "bidi_rule_4", + "bidi_rule_5", + "bidi_rule_6", + "bidi_unknown_direction", + "invalid_alabel", + "non_canonical_alabel", + "invalid_ascii", + "invalid_utf8", + "uts46_disallowed", + "uts46_std3", + "unsupported_errors", +] + + +class IDNAError(UnicodeError): + """Base exception for all IDNA-encoding related problems. + + ``str(err)`` is a human-readable description of the failure. The + exception also carries machine-readable attributes so callers do not + need to parse the message: + + * ``code`` -- a short, stable identifier for the rule that failed, such + as ``"disallowed_codepoint"`` or ``"bidi_rule_2"``; the full list is + documented in the README. Message wording, by contrast, may change + between releases. + * ``text`` -- the label (or, for UTS #46 processing, the domain) that + was being validated; + * ``codepoint`` -- the offending codepoint, as an ``int``; + * ``position`` -- the 1-based index of the offending character within + ``text``, matching the position quoted in the message. + + Each is ``None`` when it does not apply. + """ + + code: str | None + text: str | None + codepoint: int | None + position: int | None + + def __init__( + self, + *args: object, + code: _ErrorCode | None = None, + text: str | None = None, + codepoint: int | None = None, + position: int | None = None, + ) -> None: + super().__init__(*args) + self.code = code + self.text = text + self.codepoint = codepoint + self.position = position + + +class IDNABidiError(IDNAError): + """Exception when bidirectional requirements are not satisfied""" + + +class InvalidCodepoint(IDNAError): + """Exception when a disallowed or unallocated codepoint is used""" + + +class InvalidCodepointContext(IDNAError): + """Exception when the codepoint is not valid in the context it is used""" + + +def _combining_class(cp: int) -> int: + v = unicodedata.combining(chr(cp)) + if v == 0 and not unicodedata.name(chr(cp)): + raise ValueError("Unknown character in unicodedata") + return v + + +def _is_script(cp: str, script: str) -> bool: + return intranges_contain(ord(cp), idnadata.scripts[script]) + + +def _punycode(s: str) -> bytes: + return s.encode("punycode") + + +def _unot(s: int) -> str: + return f"U+{s:04X}" + + +def valid_label_length(label: bytes | str) -> bool: + """Check that a label does not exceed the maximum permitted length. + + Per :rfc:`1035` (and :rfc:`5891` §4.2.4) a DNS label must not exceed + 63 octets. The argument may be either a :class:`str` (a U-label, where + length is measured in characters) or :class:`bytes` (an A-label, where + length is measured in octets). + + :param label: The label to check. + :returns: ``True`` if the label is within the length limit, otherwise + ``False``. + """ + return len(label) <= 63 + + +def valid_string_length(domain: bytes | str, trailing_dot: bool) -> bool: + """Check that a full domain name does not exceed the maximum length. + + Per :rfc:`1035`, a domain name is limited to 253 octets when no trailing + dot is present, or 254 octets when one is included. + + :param domain: The full (possibly multi-label) domain name. + :param trailing_dot: ``True`` if ``domain`` includes a trailing ``.``. + :returns: ``True`` if the domain is within the length limit, otherwise + ``False``. + """ + return len(domain) <= _max_domain_length + trailing_dot + + +def check_bidi(label: str, check_ltr: bool = False) -> bool: + """Validate the Bidi Rule from :rfc:`5893` for a single label. + + The Bidi Rule constrains how bidirectional characters (Hebrew, Arabic, + etc.) may appear within a label. By default the check is only applied + when the label contains at least one right-to-left character (Unicode + bidirectional categories ``R``, ``AL``, or ``AN``); set ``check_ltr`` + to ``True`` to apply it to LTR-only labels as well. + + :param label: The label to validate, as a Unicode string. + :param check_ltr: If ``True``, apply the rules even when the label + contains no RTL characters. + :returns: ``True`` if the label satisfies the Bidi Rule. + :raises IDNABidiError: If any of Bidi Rule conditions 1-6 are violated, + or if the directional category of a codepoint cannot be determined. + """ + if len(label) > _max_input_length: + raise IDNAError("Label too long", code="input_too_long") + # Bidi rules should only be applied if string contains RTL characters + bidi_label = False + for idx, cp in enumerate(label, 1): + direction = unicodedata.bidirectional(cp) + if direction == "": + # String likely comes from a newer version of Unicode + raise IDNABidiError( + f"Unknown directionality in label {label!r} at position {idx}", + code="bidi_unknown_direction", + text=label, + codepoint=ord(cp), + position=idx, + ) + if direction in _bidi_rtl_categories: + bidi_label = True + if not bidi_label and not check_ltr: + return True + + # Bidi rule 1 + direction = unicodedata.bidirectional(label[0]) + if direction in _bidi_rtl_first: + rtl = True + elif direction == "L": + rtl = False + else: + raise IDNABidiError( + f"First codepoint in label {label!r} must be directionality L, R or AL", + code="bidi_rule_1", + text=label, + codepoint=ord(label[0]), + position=1, + ) + + valid_ending = False + ending_idx = 1 + number_type: str | None = None + for idx, cp in enumerate(label, 1): + direction = unicodedata.bidirectional(cp) + + if rtl: + # Bidi rule 2 + if direction not in _bidi_rtl_allowed: + raise IDNABidiError( + f"Invalid direction for codepoint at position {idx} in a right-to-left label", + code="bidi_rule_2", + text=label, + codepoint=ord(cp), + position=idx, + ) + # Bidi rule 3 + if direction in _bidi_rtl_valid_ending: + valid_ending = True + ending_idx = idx + elif direction != "NSM": + valid_ending = False + ending_idx = idx + # Bidi rule 4 + if direction in _bidi_rtl_numeric: + if not number_type: + number_type = direction + elif number_type != direction: + raise IDNABidiError( + "Can not mix numeral types in a right-to-left label", + code="bidi_rule_4", + text=label, + codepoint=ord(cp), + position=idx, + ) + else: + # Bidi rule 5 + if direction not in _bidi_ltr_allowed: + raise IDNABidiError( + f"Invalid direction for codepoint at position {idx} in a left-to-right label", + code="bidi_rule_5", + text=label, + codepoint=ord(cp), + position=idx, + ) + # Bidi rule 6 + if direction in _bidi_ltr_valid_ending: + valid_ending = True + ending_idx = idx + elif direction != "NSM": + valid_ending = False + ending_idx = idx + + if not valid_ending: + # Rules 3 and 6 concern the last character that is not a + # non-spacing mark, which is what ``ending_idx`` tracks. + raise IDNABidiError( + "Label ends with illegal codepoint directionality", + code="bidi_rule_3" if rtl else "bidi_rule_6", + text=label, + codepoint=ord(label[ending_idx - 1]), + position=ending_idx, + ) + + return True + + +def check_initial_combiner(label: str) -> bool: + """Reject labels that begin with a combining mark. + + Per :rfc:`5891` §4.2.3.2 a label must not start with a character of + Unicode general category ``M`` (Mark). + + :param label: The label to check. + :returns: ``True`` if the first character is not a combining mark. + :raises IDNAError: If the label begins with a combining character. + """ + if label and unicodedata.category(label[0])[0] == "M": + raise IDNAError( + "Label begins with an illegal combining character", + code="leading_combiner", + text=label, + codepoint=ord(label[0]), + position=1, + ) + return True + + +def check_hyphen_ok(label: str) -> bool: + """Validate the hyphen restrictions for a label. + + Per :rfc:`5891` §4.2.3.1 a label must not start or end with a hyphen + (``U+002D``), and must not have hyphens in both the third and fourth + positions (the prefix reserved for A-labels). + + :param label: The label to check. + :returns: ``True`` if the hyphen restrictions are satisfied. + :raises IDNAError: If any of the hyphen restrictions are violated. + """ + if label[2:4] == "--": + raise IDNAError("Label has disallowed hyphens in 3rd and 4th position", code="hyphen_3_4") + if label.startswith("-") or label.endswith("-"): + raise IDNAError("Label must not start or end with a hyphen", code="hyphen_start_end") + return True + + +def check_nfc(label: str) -> None: + """Require that a label is in Unicode Normalization Form C. + + :param label: The label to check. + :raises IDNAError: If ``label`` differs from its NFC normalisation. + """ + if len(label) > _max_input_length: + raise IDNAError("Label too long", code="input_too_long") + if unicodedata.normalize("NFC", label) != label: + raise IDNAError("Label must be in Normalization Form C", code="not_nfc") + + +def valid_contextj(label: str, pos: int) -> bool: + """Validate the CONTEXTJ rules from :rfc:`5892` Appendix A. + + These rules govern the contextual use of the joiner codepoints + ``U+200C`` (ZERO WIDTH NON-JOINER, Appendix A.1) and ``U+200D`` + (ZERO WIDTH JOINER, Appendix A.2) within a label. + + :param label: The label containing the codepoint. + :param pos: Index of the joiner codepoint within ``label``. + :returns: ``True`` if the codepoint at ``pos`` satisfies its CONTEXTJ + rule, ``False`` otherwise (including when the codepoint at + ``pos`` is not a recognised joiner). + :raises ValueError: If an adjacent codepoint has no Unicode name when + determining its combining class. + :raises IDNAError: If ``label`` exceeds the defensive input length limit. + """ + if len(label) > _max_input_length: + raise IDNAError("Label too long", code="input_too_long") + cp_value = ord(label[pos]) + + if cp_value == 0x200C: + if pos > 0 and _combining_class(ord(label[pos - 1])) == _virama_combining_class: + return True + + ok = False + for i in range(pos - 1, -1, -1): + joining_type = _joining_type(ord(label[i])) + if joining_type == "T": + continue + if joining_type in _bidi_joiner_l_or_d: + ok = True + break + break + + if not ok: + return False + + ok = False + for i in range(pos + 1, len(label)): + joining_type = _joining_type(ord(label[i])) + if joining_type == "T": + continue + if joining_type in _bidi_joiner_r_or_d: + ok = True + break + break + return ok + + if cp_value == 0x200D: + return pos > 0 and _combining_class(ord(label[pos - 1])) == _virama_combining_class + + return False + + +def valid_contexto(label: str, pos: int, exception: bool = False) -> bool: + """Validate the CONTEXTO rules from :rfc:`5892` Appendix A. + + Covers the contextual rules for codepoints such as MIDDLE DOT + (``U+00B7``), Greek lower numeral sign, Hebrew punctuation, Katakana + middle dot, and the Arabic-Indic / Extended Arabic-Indic digit ranges. + + :param label: The label containing the codepoint. + :param pos: Index of the codepoint within ``label``. + :param exception: Reserved for forward compatibility; currently unused. + :returns: ``True`` if the codepoint at ``pos`` satisfies its CONTEXTO + rule, ``False`` otherwise (including when the codepoint is not a + recognised CONTEXTO codepoint). + :raises IDNAError: If ``label`` exceeds the defensive input length limit. + """ + if len(label) > _max_input_length: + raise IDNAError("Label too long", code="input_too_long") + cp_value = ord(label[pos]) + + if cp_value == 0x00B7: + return 0 < pos < len(label) - 1 and ord(label[pos - 1]) == 0x006C and ord(label[pos + 1]) == 0x006C + + if cp_value == 0x0375: + if pos < len(label) - 1 and len(label) > 1: + return _is_script(label[pos + 1], "Greek") + return False + + if cp_value in {0x05F3, 0x05F4}: + if pos > 0: + return _is_script(label[pos - 1], "Hebrew") + return False + + if cp_value == 0x30FB: + for cp in label: + if cp == "\u30fb": + continue + if _is_script(cp, "Hiragana") or _is_script(cp, "Katakana") or _is_script(cp, "Han"): + return True + return False + + if 0x660 <= cp_value <= 0x669: + return not any(0x6F0 <= ord(cp) <= 0x06F9 for cp in label) + + if 0x6F0 <= cp_value <= 0x6F9: + return not any(0x660 <= ord(cp) <= 0x0669 for cp in label) + + return False + + +def check_label(label: str | bytes | bytearray) -> None: + """Run the full set of IDNA 2008 validity checks on a single label. + + Applies, in order: NFC normalisation (:func:`check_nfc`), hyphen + restrictions (:func:`check_hyphen_ok`), the no-leading-combiner rule + (:func:`check_initial_combiner`), per-codepoint validity (PVALID, + CONTEXTJ, CONTEXTO classes from :rfc:`5892`), and the Bidi Rule + (:func:`check_bidi`). + + :param label: The label to validate. ``bytes`` or ``bytearray`` input + is decoded as UTF-8 first. + :raises IDNAError: If the label is empty or fails a structural rule. + :raises InvalidCodepoint: If the label contains a DISALLOWED or + UNASSIGNED codepoint. + :raises InvalidCodepointContext: If a CONTEXTJ or CONTEXTO codepoint + is not valid in its context. + :raises IDNABidiError: If the Bidi Rule is violated. + """ + if len(label) > _max_input_length: + raise IDNAError("Label too long", code="input_too_long") + if isinstance(label, (bytes, bytearray)): + try: + label = label.decode("utf-8") + except UnicodeDecodeError as err: + raise IDNAError("Invalid UTF-8 in label", code="invalid_utf8") from err + if len(label) == 0: + raise IDNAError("Empty Label", code="empty_label") + + # Check against the domain length rather than the label length to + # support some UTS #46 use cases, while still bounding the work done + # by the label contextual rules below. + if not valid_string_length(label, trailing_dot=True): + raise IDNAError("Label too long", code="label_too_long") + + check_nfc(label) + check_hyphen_ok(label) + check_initial_combiner(label) + + for pos, cp in enumerate(label): + cp_value = ord(cp) + if intranges_contain(cp_value, idnadata.codepoint_classes["PVALID"]): + continue + if intranges_contain(cp_value, idnadata.codepoint_classes["CONTEXTJ"]): + try: + contextj_ok = valid_contextj(label, pos) + except ValueError as err: + raise IDNAError( + f"Unknown codepoint adjacent to joiner {_unot(cp_value)} at position {pos + 1} in {label!r}", + code="unknown_codepoint", + text=label, + codepoint=cp_value, + position=pos + 1, + ) from err + if not contextj_ok: + raise InvalidCodepointContext( + f"Joiner {_unot(cp_value)} not allowed at position {pos + 1} in {label!r}", + code="contextj", + text=label, + codepoint=cp_value, + position=pos + 1, + ) + elif intranges_contain(cp_value, idnadata.codepoint_classes["CONTEXTO"]): + if not valid_contexto(label, pos): + raise InvalidCodepointContext( + f"Codepoint {_unot(cp_value)} not allowed at position {pos + 1} in {label!r}", + code="contexto", + text=label, + codepoint=cp_value, + position=pos + 1, + ) + else: + raise InvalidCodepoint( + f"Codepoint {_unot(cp_value)} at position {pos + 1} of {label!r} not allowed", + code="disallowed_codepoint", + text=label, + codepoint=cp_value, + position=pos + 1, + ) + + check_bidi(label) + + +def alabel(label: str) -> bytes: + """Convert a single U-label into its A-label form. + + The result is the ASCII-Compatible Encoding (ACE) form per :rfc:`5891` + §4: the label is validated, Punycode-encoded, and prefixed with + ``xn--``. Pure ASCII labels that are already valid IDNA labels are + returned unchanged (as :class:`bytes`). + + :param label: The label to convert, as a Unicode string. + :returns: The A-label as ASCII-encoded :class:`bytes`. + :raises IDNAError: If the label is invalid or the resulting A-label + exceeds 63 octets. + """ + if len(label) > _max_input_length: + raise IDNAError("Label too long", code="input_too_long") + try: + label_bytes = label.encode("ascii") + except UnicodeEncodeError: + pass + else: + ulabel(label_bytes) + if not valid_label_length(label_bytes): + raise IDNAError("Label too long", code="label_too_long") + return label_bytes + + check_label(label) + label_bytes = _alabel_prefix + _punycode(label) + + if not valid_label_length(label_bytes): + raise IDNAError("Label too long", code="label_too_long") + + return label_bytes + + +def ulabel(label: str | bytes | bytearray) -> str: + """Convert a single A-label into its U-label form. + + Performs the inverse of :func:`alabel`: an ``xn--``-prefixed label is + Punycode-decoded and validated, and is rejected unless it is the + canonical A-label for the decoded U-label (:rfc:`5891` §5.3). Labels + that are already Unicode (or plain ASCII without the ACE prefix) are + validated and returned as a Unicode string. + + :param label: The label to convert. ``bytes`` or ``bytearray`` input + is treated as ASCII. + :returns: The U-label as a Unicode string. + :raises IDNAError: If the label is malformed or fails validation. + """ + if len(label) > _max_input_length: + raise IDNAError("Label too long", code="input_too_long") + if not isinstance(label, (bytes, bytearray)): + try: + label_bytes = label.encode("ascii") + except UnicodeEncodeError: + check_label(label) + return label + else: + label_bytes = bytes(label) + if not label_bytes.isascii(): + raise IDNAError("Invalid ASCII in A-label", code="invalid_ascii") + + label_bytes = label_bytes.lower() + if label_bytes.startswith(_alabel_prefix): + label_bytes = label_bytes[len(_alabel_prefix) :] + if not label_bytes: + raise IDNAError("Malformed A-label, no Punycode eligible content found", code="invalid_alabel") + if label_bytes.endswith(b"-"): + raise IDNAError("A-label must not end with a hyphen", code="invalid_alabel") + else: + check_label(label_bytes) + return label_bytes.decode("ascii") + + try: + label = label_bytes.decode("punycode") + except UnicodeError as err: + raise IDNAError("Invalid A-label", code="invalid_alabel") from err + # RFC 5891 §5.3: the label is rejected unless re-encoding the decoded + # form reproduces the (lowercased) input. This catches "fake A-labels" + # (RFC 5890 §2.3.2.1) such as ``xn---bbk``, a non-canonical Punycode + # spelling of ``xn--bbk`` that would otherwise decode to the same + # U-label and so display identically to a different wire-format name. + if _punycode(label) != label_bytes: + raise IDNAError("A-label is not the canonical Punycode encoding of its U-label", code="non_canonical_alabel") + check_label(label) + return label + + +def _check_std3(text: str, domain: str, offset: int) -> None: + """Raise if ``text``, a slice of ``domain`` starting at ``offset`` that + UTS #46 mapping left unchanged, contains an ASCII character disallowed + under ``UseSTD3ASCIIRules``.""" + match = _std3_disallowed_re.search(text) + if match: + codepoint = ord(match.group()) + position = offset + match.start() + 1 + raise InvalidCodepoint( + f"Codepoint {_unot(codepoint)} not allowed at position {position} in {domain!r}", + code="uts46_std3", + text=domain, + codepoint=codepoint, + position=position, + ) + + +def _warn_transitional() -> None: + warnings.warn( + "Transitional processing is deprecated in UTS #46 and has no effect. " + "The transitional argument will be removed in a future version.", + DeprecationWarning, + stacklevel=3, + ) + + +def uts46_remap(domain: str, std3_rules: bool = True, transitional: bool = False) -> str: + """Apply the UTS #46 character mapping to a domain string. + + Implements the mapping table from `UTS #46 §4 + `_: each character is kept, + replaced, or rejected based on its status (``V``, ``M``, ``D``, + ``I``, ``X``). The result is returned in Normalisation Form C. + + :param domain: The full domain name to remap. + :param std3_rules: If ``True``, apply UTS #46's ``UseSTD3ASCIIRules``: + after mapping, any ASCII character other than a lowercase letter, + digit, hyphen or the label separator ``.`` is rejected, whether it + appeared in the input or was produced by a mapping (e.g. U+FF01 + FULLWIDTH EXCLAMATION MARK maps to ``!``). If ``False``, such + characters are passed through. + :param transitional: Deprecated and ignored. UTS #46 deprecated + transitional processing in Unicode 15.1 and deviation (status + ``D``) codepoints are now always kept, so this has no effect + beyond emitting a :class:`DeprecationWarning`. It will be removed + in a future version. + :returns: The remapped domain, in Normalisation Form C. + :raises InvalidCodepoint: If the domain contains a disallowed + codepoint under the chosen rules. + :raises IDNAError: If ``domain`` exceeds the defensive input length limit. + """ + if transitional: + _warn_transitional() + if len(domain) > _max_input_length: + raise IDNAError("Domain too long", code="input_too_long") + if domain.isascii(): + # The only ASCII mapping in UTS #46 is upper- to lowercase, and + # ASCII is invariant under NFC, so lowercasing is the whole job. + result = domain.lower() + if std3_rules: + _check_std3(result, domain, 0) + return result + + from .uts46data import uts46_replacements, uts46_starts, uts46_statuses + + # ``start`` marks the run of unchanged input not yet copied; a run is + # only sliced out when a character must be replaced or dropped, so the + # common no-change case makes no copy. STD3 is checked per output piece + # to report a violation at its input position. + output: list[str] = [] + start = 0 + for pos, char in enumerate(domain): + code_point = ord(char) + i = code_point if code_point < 256 else bisect.bisect_right(uts46_starts, code_point) - 1 + status = uts46_statuses[i] + # UTS #46 §4: V valid, D deviation (kept), M mapped, I ignored, + # anything else disallowed. + if status == _STATUS_VALID: + continue + if status == _STATUS_MAPPED: + replacement = uts46_replacements[i] + elif status == _STATUS_DEVIATION: + continue + elif status == _STATUS_IGNORED: + replacement = None + else: + raise InvalidCodepoint( + f"Codepoint {_unot(code_point)} not allowed at position {pos + 1} in {domain!r}", + code="uts46_disallowed", + text=domain, + codepoint=code_point, + position=pos + 1, + ) + if start < pos: + run = domain[start:pos] + if std3_rules: + _check_std3(run, domain, start) + output.append(run) + if replacement: + if std3_rules and _std3_disallowed_re.search(replacement): + raise InvalidCodepoint( + f"Codepoint {_unot(code_point)} not allowed at position {pos + 1} in {domain!r}", + code="uts46_std3", + text=domain, + codepoint=code_point, + position=pos + 1, + ) + output.append(replacement) + start = pos + 1 + + if start == 0: + if std3_rules: + _check_std3(domain, domain, 0) + return unicodedata.normalize("NFC", domain) + tail = domain[start:] + if std3_rules: + _check_std3(tail, domain, start) + output.append(tail) + return unicodedata.normalize("NFC", "".join(output)) + + +def encode( + s: str | bytes | bytearray, + strict: bool = False, + uts46: bool = False, + std3_rules: bool = False, + transitional: bool = False, +) -> bytes: + """Encode a Unicode domain name into its ASCII (A-label) form. + + Splits the input on label separators (only ``U+002E`` if ``strict`` is + set; otherwise also IDEOGRAPHIC FULL STOP ``U+3002``, FULLWIDTH FULL + STOP ``U+FF0E``, and HALFWIDTH IDEOGRAPHIC FULL STOP ``U+FF61``), + encodes each label with :func:`alabel`, and rejoins them with ``.``. + Optionally pre-processes the input through :func:`uts46_remap`. + + :param s: The domain name to encode. + :param strict: If ``True``, only ``U+002E`` is recognised as a label + separator. + :param uts46: If ``True``, apply UTS #46 mapping before encoding. + :param std3_rules: Forwarded to :func:`uts46_remap` when ``uts46`` is + ``True``. + :param transitional: Deprecated and ignored (see :func:`uts46_remap`): + emits a :class:`DeprecationWarning` and will be removed in a + future version. + :returns: The encoded domain as ASCII :class:`bytes`. + :raises IDNAError: If the domain is empty, contains an invalid label, + or exceeds the maximum domain length. + """ + if transitional: + _warn_transitional() + if not isinstance(s, str): + try: + s = str(s, "ascii") + except (UnicodeDecodeError, TypeError) as err: + raise IDNAError( + "should pass a unicode string to the function rather than a byte string.", code="invalid_ascii" + ) from err + if len(s) > _max_input_length: + raise IDNAError("Domain too long", code="input_too_long") + if uts46: + s = uts46_remap(s, std3_rules) + + if not valid_string_length(s, trailing_dot=True): + raise IDNAError("Domain too long", code="domain_too_long") + + trailing_dot = False + result = [] + labels = s.split(".") if strict else _unicode_dots_re.split(s) + if not labels or labels == [""]: + raise IDNAError("Empty domain", code="empty_domain") + if labels[-1] == "": + del labels[-1] + trailing_dot = True + for label in labels: + s = alabel(label) + if s: + result.append(s) + else: + raise IDNAError("Empty label", code="empty_label") + if trailing_dot: + result.append(b"") + s = b".".join(result) + if not valid_string_length(s, trailing_dot): + raise IDNAError("Domain too long", code="domain_too_long") + return s + + +def decode( + s: str | bytes | bytearray, + strict: bool = False, + uts46: bool = False, + std3_rules: bool = False, + display: bool = False, +) -> str: + """Decode an A-label-encoded domain name back to Unicode. + + Splits the input on label separators (see :func:`encode` for the + rules), decodes each label with :func:`ulabel`, and rejoins them + with ``.``. Optionally pre-processes the input through + :func:`uts46_remap`. + + :param s: The domain name to decode. + :param strict: If ``True``, only ``U+002E`` is recognised as a label + separator. + :param uts46: If ``True``, apply UTS #46 mapping before decoding. + :param std3_rules: Forwarded to :func:`uts46_remap` when ``uts46`` is + ``True``. + :param display: If ``True``, any ``xn--`` label that fails IDNA + validation is passed through unchanged (lowercased) rather than + aborting the whole call. Intended for "decode for display" + consumers (e.g. URL libraries, HTTP clients) that want to show + the user the label as it appears on the wire when it cannot be + rendered as Unicode. Matches the per-label recovery prescribed + by UTS #46 §4 and the WHATWG URL "domain to Unicode" algorithm. + :returns: The decoded domain as a Unicode string. + :raises IDNAError: If the input is not valid ASCII, contains an + invalid label, or is empty. + """ + if not isinstance(s, str): + try: + s = str(s, "ascii") + except (UnicodeDecodeError, TypeError) as err: + raise IDNAError("Invalid ASCII in A-label", code="invalid_ascii") from err + if len(s) > _max_input_length: + raise IDNAError("Domain too long", code="input_too_long") + if uts46: + s = uts46_remap(s, std3_rules, False) + if not valid_string_length(s, trailing_dot=True): + raise IDNAError("Domain too long", code="domain_too_long") + trailing_dot = False + result = [] + labels = s.split(".") if strict else _unicode_dots_re.split(s) + if not labels or labels == [""]: + raise IDNAError("Empty domain", code="empty_domain") + if not labels[-1]: + del labels[-1] + trailing_dot = True + for label in labels: + try: + u = ulabel(label) + except IDNAError: + if display and label[:4].lower() == "xn--": + u = label.lower() + else: + raise + if u: + result.append(u) + else: + raise IDNAError("Empty label", code="empty_label") + if trailing_dot: + result.append("") + return ".".join(result) diff --git a/source/target-vendors/cp312/idna/idnadata.py b/source/target-vendors/cp312/idna/idnadata.py new file mode 100644 index 0000000..471c047 --- /dev/null +++ b/source/target-vendors/cp312/idna/idnadata.py @@ -0,0 +1,1933 @@ +# This file is automatically generated by tools/idna-data + +__version__ = "18.0.0" + +scripts = { + "Greek": ( + 0x37000000374, + 0x37500000378, + 0x37A0000037E, + 0x37F00000380, + 0x38400000385, + 0x38600000387, + 0x3880000038B, + 0x38C0000038D, + 0x38E000003A2, + 0x3A3000003E2, + 0x3F000000400, + 0x1D2600001D2B, + 0x1D5D00001D62, + 0x1D6600001D6B, + 0x1DBF00001DC0, + 0x1F0000001F16, + 0x1F1800001F1E, + 0x1F2000001F46, + 0x1F4800001F4E, + 0x1F5000001F58, + 0x1F5900001F5A, + 0x1F5B00001F5C, + 0x1F5D00001F5E, + 0x1F5F00001F7E, + 0x1F8000001FB5, + 0x1FB600001FC5, + 0x1FC600001FD4, + 0x1FD600001FDC, + 0x1FDD00001FF0, + 0x1FF200001FF5, + 0x1FF600001FFF, + 0x212600002127, + 0xAB650000AB66, + 0x101400001018F, + 0x101A0000101A1, + 0x1D2000001D246, + 0x1DFF30001DFF5, + ), + "Han": ( + 0x2E8000002E9A, + 0x2E9B00002EF4, + 0x2F0000002FD6, + 0x300500003006, + 0x300700003008, + 0x30210000302A, + 0x30380000303C, + 0x340000004DC0, + 0x4E000000A000, + 0xF9000000FA6E, + 0xFA700000FADA, + 0x16FE200016FE4, + 0x16FF000016FF7, + 0x200000002A6E0, + 0x2A7000002B81F, + 0x2B8200002CEAE, + 0x2CEB00002EBE1, + 0x2EBF00002EE5E, + 0x2F8000002FA1E, + 0x300000003134B, + 0x313500003347A, + ), + "Hebrew": ( + 0x591000005CA, + 0x5D0000005EB, + 0x5EF000005F5, + 0xFB1D0000FB37, + 0xFB380000FB3D, + 0xFB3E0000FB3F, + 0xFB400000FB42, + 0xFB430000FB45, + 0xFB460000FB50, + ), + "Hiragana": ( + 0x304100003097, + 0x309D000030A0, + 0x1B0010001B120, + 0x1B1230001B124, + 0x1B1320001B133, + 0x1B1500001B153, + 0x1F2000001F201, + ), + "Katakana": ( + 0x30A1000030FB, + 0x30FD00003100, + 0x31F000003200, + 0x32D0000032FF, + 0x330000003358, + 0xFF660000FF70, + 0xFF710000FF9E, + 0x1AFF00001AFF4, + 0x1AFF50001AFFC, + 0x1AFFD0001AFFF, + 0x1B0000001B001, + 0x1B1200001B123, + 0x1B1240001B129, + 0x1B1550001B156, + 0x1B1640001B169, + ), +} + + +joining_types = { + "C": ( + 0x64000000641, + 0x7FA000007FB, + 0x88300000886, + 0x180A0000180B, + 0x200D0000200E, + ), + "D": ( + 0x62000000621, + 0x62600000627, + 0x62800000629, + 0x62A0000062F, + 0x63300000640, + 0x64100000648, + 0x6490000064B, + 0x66E00000670, + 0x67800000688, + 0x69A000006C0, + 0x6C1000006C3, + 0x6CC000006CD, + 0x6CE000006CF, + 0x6D0000006D2, + 0x6FA000006FD, + 0x6FF00000700, + 0x71200000715, + 0x71A0000071E, + 0x71F00000728, + 0x7290000072A, + 0x72B0000072C, + 0x72D0000072F, + 0x74E00000759, + 0x75C0000076B, + 0x76D00000771, + 0x77200000773, + 0x77500000778, + 0x77A00000780, + 0x7CA000007EB, + 0x84100000846, + 0x84800000849, + 0x84A00000854, + 0x85500000856, + 0x86000000861, + 0x86200000866, + 0x86800000869, + 0x88600000887, + 0x8890000088E, + 0x88F00000890, + 0x8A0000008AA, + 0x8AF000008B1, + 0x8B3000008B9, + 0x8BA000008C9, + 0x180700001808, + 0x182000001879, + 0x1887000018A9, + 0x18AA000018AB, + 0xA8400000A872, + 0x10AC000010AC5, + 0x10AD300010AD7, + 0x10AD800010ADD, + 0x10ADE00010AE1, + 0x10AEB00010AEF, + 0x10B8000010B81, + 0x10B8200010B83, + 0x10B8600010B89, + 0x10B8A00010B8C, + 0x10B8D00010B8E, + 0x10B9000010B91, + 0x10BAD00010BAF, + 0x10D0100010D22, + 0x10D2300010D24, + 0x10EC300010EC5, + 0x10EC600010EC8, + 0x10F3000010F33, + 0x10F3400010F45, + 0x10F5100010F54, + 0x10F7000010F74, + 0x10F7600010F82, + 0x10FB000010FB1, + 0x10FB200010FB4, + 0x10FB800010FB9, + 0x10FBB00010FBD, + 0x10FBE00010FC0, + 0x10FC100010FC2, + 0x10FC400010FC5, + 0x10FCA00010FCB, + 0x1E9000001E944, + ), + "L": ( + 0xA8720000A873, + 0x10ACD00010ACE, + 0x10AD700010AD8, + 0x10D0000010D01, + 0x10ED900010EEF, + 0x10FCB00010FCC, + ), + "R": ( + 0x62200000626, + 0x62700000628, + 0x6290000062A, + 0x62F00000633, + 0x64800000649, + 0x67100000674, + 0x67500000678, + 0x6880000069A, + 0x6C0000006C1, + 0x6C3000006CC, + 0x6CD000006CE, + 0x6CF000006D0, + 0x6D2000006D4, + 0x6D5000006D6, + 0x6EE000006F0, + 0x71000000711, + 0x7150000071A, + 0x71E0000071F, + 0x72800000729, + 0x72A0000072B, + 0x72C0000072D, + 0x72F00000730, + 0x74D0000074E, + 0x7590000075C, + 0x76B0000076D, + 0x77100000772, + 0x77300000775, + 0x7780000077A, + 0x84000000841, + 0x84600000848, + 0x8490000084A, + 0x85400000855, + 0x85600000859, + 0x86700000868, + 0x8690000086B, + 0x87000000883, + 0x88E0000088F, + 0x8AA000008AD, + 0x8AE000008AF, + 0x8B1000008B3, + 0x8B9000008BA, + 0x10AC500010AC6, + 0x10AC700010AC8, + 0x10AC900010ACB, + 0x10ACE00010AD3, + 0x10ADD00010ADE, + 0x10AE100010AE2, + 0x10AE400010AE5, + 0x10AEF00010AF0, + 0x10B8100010B82, + 0x10B8300010B86, + 0x10B8900010B8A, + 0x10B8C00010B8D, + 0x10B8E00010B90, + 0x10B9100010B92, + 0x10BA900010BAD, + 0x10D2200010D23, + 0x10EC200010EC3, + 0x10F3300010F34, + 0x10F5400010F55, + 0x10F7400010F76, + 0x10FB400010FB7, + 0x10FB900010FBB, + 0x10FBD00010FBE, + 0x10FC200010FC4, + 0x10FC900010FCA, + ), + "T": ( + 0xAD000000AE, + 0x30000000370, + 0x4830000048A, + 0x591000005BE, + 0x5BF000005C0, + 0x5C1000005C3, + 0x5C4000005C6, + 0x5C7000005CA, + 0x6100000061B, + 0x61C0000061D, + 0x64B00000660, + 0x67000000671, + 0x6D6000006DD, + 0x6DF000006E5, + 0x6E7000006E9, + 0x6EA000006EE, + 0x70F00000710, + 0x71100000712, + 0x7300000074B, + 0x7A6000007B1, + 0x7EB000007F4, + 0x7FD000007FE, + 0x8160000081A, + 0x81B00000824, + 0x82500000828, + 0x8290000082E, + 0x8590000085C, + 0x897000008A0, + 0x8CA000008E2, + 0x8E300000903, + 0x93A0000093B, + 0x93C0000093D, + 0x94100000949, + 0x94D0000094E, + 0x95100000958, + 0x96200000964, + 0x98100000982, + 0x9BC000009BD, + 0x9C1000009C5, + 0x9CD000009CE, + 0x9E2000009E4, + 0x9FE000009FF, + 0xA0100000A03, + 0xA3C00000A3D, + 0xA4100000A43, + 0xA4700000A49, + 0xA4B00000A4E, + 0xA5100000A52, + 0xA7000000A72, + 0xA7500000A76, + 0xA8100000A83, + 0xABC00000ABD, + 0xAC100000AC6, + 0xAC700000AC9, + 0xACD00000ACE, + 0xAE200000AE4, + 0xAFA00000B00, + 0xB0100000B02, + 0xB3C00000B3D, + 0xB3F00000B40, + 0xB4100000B45, + 0xB4D00000B4E, + 0xB5300000B57, + 0xB6200000B64, + 0xB8200000B83, + 0xBC000000BC1, + 0xBCD00000BCE, + 0xC0000000C01, + 0xC0400000C05, + 0xC3C00000C3D, + 0xC3E00000C41, + 0xC4600000C49, + 0xC4A00000C4E, + 0xC5500000C57, + 0xC6200000C64, + 0xC8100000C82, + 0xCBC00000CBD, + 0xCBF00000CC0, + 0xCC600000CC7, + 0xCCC00000CCE, + 0xCE200000CE4, + 0xD0000000D02, + 0xD3B00000D3D, + 0xD4100000D45, + 0xD4D00000D4E, + 0xD6200000D64, + 0xD8100000D82, + 0xDCA00000DCB, + 0xDD200000DD5, + 0xDD600000DD7, + 0xE3100000E32, + 0xE3400000E3B, + 0xE4700000E4F, + 0xEB100000EB2, + 0xEB400000EBD, + 0xEC800000ECF, + 0xF1800000F1A, + 0xF3500000F36, + 0xF3700000F38, + 0xF3900000F3A, + 0xF7100000F7F, + 0xF8000000F85, + 0xF8600000F88, + 0xF8D00000F98, + 0xF9900000FBD, + 0xFC600000FC7, + 0x102D00001031, + 0x103200001038, + 0x10390000103B, + 0x103D0000103F, + 0x10580000105A, + 0x105E00001061, + 0x107100001075, + 0x108200001083, + 0x108500001087, + 0x108D0000108E, + 0x109D0000109E, + 0x135D00001360, + 0x171200001715, + 0x173200001734, + 0x175200001754, + 0x177200001774, + 0x17B4000017B6, + 0x17B7000017BE, + 0x17C6000017C7, + 0x17C9000017D4, + 0x17DD000017DE, + 0x180B0000180E, + 0x180F00001810, + 0x188500001887, + 0x18A9000018AA, + 0x192000001923, + 0x192700001929, + 0x193200001933, + 0x19390000193C, + 0x1A1700001A19, + 0x1A1B00001A1C, + 0x1A5600001A57, + 0x1A5800001A5F, + 0x1A6000001A61, + 0x1A6200001A63, + 0x1A6500001A6D, + 0x1A7300001A7D, + 0x1A7F00001A80, + 0x1AB000001AF1, + 0x1B0000001B04, + 0x1B3400001B35, + 0x1B3600001B3B, + 0x1B3C00001B3D, + 0x1B4200001B43, + 0x1B6B00001B74, + 0x1B8000001B82, + 0x1BA200001BA6, + 0x1BA800001BAA, + 0x1BAB00001BAE, + 0x1BE600001BE7, + 0x1BE800001BEA, + 0x1BED00001BEE, + 0x1BEF00001BF2, + 0x1C2C00001C34, + 0x1C3600001C38, + 0x1CD000001CD3, + 0x1CD400001CE1, + 0x1CE200001CE9, + 0x1CED00001CEE, + 0x1CF400001CF5, + 0x1CF800001CFA, + 0x1DC000001E00, + 0x200B0000200C, + 0x200E00002010, + 0x202A0000202F, + 0x206000002065, + 0x206A00002070, + 0x20D0000020F1, + 0x2CEF00002CF2, + 0x2D7F00002D80, + 0x2DE000002E00, + 0x302A0000302E, + 0x30990000309B, + 0xA66F0000A673, + 0xA6740000A67E, + 0xA69E0000A6A0, + 0xA6F00000A6F2, + 0xA8020000A803, + 0xA8060000A807, + 0xA80B0000A80C, + 0xA8250000A827, + 0xA82C0000A82D, + 0xA8C40000A8C6, + 0xA8E00000A8F2, + 0xA8FF0000A900, + 0xA9260000A92E, + 0xA9470000A952, + 0xA9800000A983, + 0xA9B30000A9B4, + 0xA9B60000A9BA, + 0xA9BC0000A9BE, + 0xA9E50000A9E6, + 0xAA290000AA2F, + 0xAA310000AA33, + 0xAA350000AA37, + 0xAA430000AA44, + 0xAA4C0000AA4D, + 0xAA7C0000AA7D, + 0xAAB00000AAB1, + 0xAAB20000AAB5, + 0xAAB70000AAB9, + 0xAABE0000AAC0, + 0xAAC10000AAC2, + 0xAAEC0000AAEE, + 0xAAF60000AAF7, + 0xABE50000ABE6, + 0xABE80000ABE9, + 0xABED0000ABEE, + 0xFB1E0000FB1F, + 0xFE000000FE10, + 0xFE200000FE30, + 0xFEFF0000FF00, + 0xFFF90000FFFC, + 0x101FD000101FE, + 0x102E0000102E1, + 0x103760001037B, + 0x10A0100010A04, + 0x10A0500010A07, + 0x10A0C00010A10, + 0x10A3800010A3B, + 0x10A3F00010A40, + 0x10AE500010AE7, + 0x10D2400010D28, + 0x10D6900010D6E, + 0x10EAB00010EAD, + 0x10ECB00010ED0, + 0x10EF000010F00, + 0x10F4600010F51, + 0x10F8200010F86, + 0x1100100011002, + 0x1103800011047, + 0x1107000011071, + 0x1107300011075, + 0x1107F00011082, + 0x110B3000110B7, + 0x110B9000110BB, + 0x110C2000110C3, + 0x1110000011103, + 0x111270001112C, + 0x1112D00011135, + 0x1117300011174, + 0x1118000011182, + 0x111B6000111BF, + 0x111C9000111CD, + 0x111CF000111D0, + 0x1122F00011232, + 0x1123400011235, + 0x1123600011238, + 0x1123E0001123F, + 0x1124100011242, + 0x112DF000112E0, + 0x112E3000112EB, + 0x1130000011302, + 0x1133B0001133D, + 0x1134000011341, + 0x113660001136D, + 0x1137000011375, + 0x113BB000113C1, + 0x113CE000113CF, + 0x113D0000113D1, + 0x113D2000113D3, + 0x113E1000113E3, + 0x1143800011440, + 0x1144200011445, + 0x1144600011447, + 0x1145E0001145F, + 0x114B3000114B9, + 0x114BA000114BB, + 0x114BF000114C1, + 0x114C2000114C4, + 0x115B2000115B6, + 0x115BC000115BE, + 0x115BF000115C1, + 0x115DC000115DE, + 0x116330001163B, + 0x1163D0001163E, + 0x1163F00011641, + 0x116AB000116AC, + 0x116AD000116AE, + 0x116B0000116B6, + 0x116B7000116B8, + 0x1171D0001171E, + 0x1171F00011720, + 0x1172200011726, + 0x117270001172C, + 0x1182F00011838, + 0x118390001183B, + 0x1193B0001193D, + 0x1193E0001193F, + 0x1194300011944, + 0x119D4000119D8, + 0x119DA000119DC, + 0x119E0000119E1, + 0x11A0100011A0B, + 0x11A3300011A39, + 0x11A3B00011A3F, + 0x11A4700011A48, + 0x11A5100011A57, + 0x11A5900011A5C, + 0x11A8A00011A97, + 0x11A9800011A9A, + 0x11B6000011B61, + 0x11B6200011B65, + 0x11B6600011B67, + 0x11C3000011C37, + 0x11C3800011C3E, + 0x11C3F00011C40, + 0x11C9200011CA8, + 0x11CAA00011CB1, + 0x11CB200011CB4, + 0x11CB500011CB7, + 0x11D3100011D37, + 0x11D3A00011D3B, + 0x11D3C00011D3E, + 0x11D3F00011D46, + 0x11D4700011D48, + 0x11D9000011D92, + 0x11D9500011D96, + 0x11D9700011D98, + 0x11DF000011DF1, + 0x11EF300011EF5, + 0x11F0000011F02, + 0x11F3600011F3B, + 0x11F4000011F41, + 0x11F4200011F43, + 0x11F5A00011F5B, + 0x1343000013441, + 0x1344700013456, + 0x1611E0001612A, + 0x1612D00016130, + 0x16AF000016AF5, + 0x16B3000016B37, + 0x16F4F00016F50, + 0x16F8F00016F93, + 0x16FE400016FE5, + 0x1BC9D0001BC9F, + 0x1BCA00001BCA4, + 0x1CF000001CF2E, + 0x1CF300001CF47, + 0x1D1270001D129, + 0x1D1670001D16A, + 0x1D1730001D183, + 0x1D1850001D18C, + 0x1D1AA0001D1AE, + 0x1D2420001D245, + 0x1D25B0001D25D, + 0x1DA000001DA37, + 0x1DA3B0001DA6D, + 0x1DA750001DA76, + 0x1DA840001DA85, + 0x1DA9B0001DAA0, + 0x1DAA10001DAB0, + 0x1E0000001E007, + 0x1E0080001E019, + 0x1E01B0001E022, + 0x1E0230001E025, + 0x1E0260001E02B, + 0x1E08F0001E090, + 0x1E1300001E137, + 0x1E2AE0001E2AF, + 0x1E2EC0001E2F0, + 0x1E4EC0001E4F0, + 0x1E5EE0001E5F0, + 0x1E6E30001E6E4, + 0x1E6E60001E6E7, + 0x1E6EE0001E6F0, + 0x1E6F50001E6F6, + 0x1E8D00001E8D7, + 0x1E9440001E94C, + 0xE0001000E0002, + 0xE0020000E0080, + 0xE0100000E01F0, + ), +} + + +codepoint_classes = { + "PVALID": ( + 0x2D0000002E, + 0x300000003A, + 0x610000007B, + 0xDF000000F7, + 0xF800000100, + 0x10100000102, + 0x10300000104, + 0x10500000106, + 0x10700000108, + 0x1090000010A, + 0x10B0000010C, + 0x10D0000010E, + 0x10F00000110, + 0x11100000112, + 0x11300000114, + 0x11500000116, + 0x11700000118, + 0x1190000011A, + 0x11B0000011C, + 0x11D0000011E, + 0x11F00000120, + 0x12100000122, + 0x12300000124, + 0x12500000126, + 0x12700000128, + 0x1290000012A, + 0x12B0000012C, + 0x12D0000012E, + 0x12F00000130, + 0x13100000132, + 0x13500000136, + 0x13700000139, + 0x13A0000013B, + 0x13C0000013D, + 0x13E0000013F, + 0x14200000143, + 0x14400000145, + 0x14600000147, + 0x14800000149, + 0x14B0000014C, + 0x14D0000014E, + 0x14F00000150, + 0x15100000152, + 0x15300000154, + 0x15500000156, + 0x15700000158, + 0x1590000015A, + 0x15B0000015C, + 0x15D0000015E, + 0x15F00000160, + 0x16100000162, + 0x16300000164, + 0x16500000166, + 0x16700000168, + 0x1690000016A, + 0x16B0000016C, + 0x16D0000016E, + 0x16F00000170, + 0x17100000172, + 0x17300000174, + 0x17500000176, + 0x17700000178, + 0x17A0000017B, + 0x17C0000017D, + 0x17E0000017F, + 0x18000000181, + 0x18300000184, + 0x18500000186, + 0x18800000189, + 0x18C0000018E, + 0x19200000193, + 0x19500000196, + 0x1990000019C, + 0x19E0000019F, + 0x1A1000001A2, + 0x1A3000001A4, + 0x1A5000001A6, + 0x1A8000001A9, + 0x1AA000001AC, + 0x1AD000001AE, + 0x1B0000001B1, + 0x1B4000001B5, + 0x1B6000001B7, + 0x1B9000001BC, + 0x1BD000001C4, + 0x1CE000001CF, + 0x1D0000001D1, + 0x1D2000001D3, + 0x1D4000001D5, + 0x1D6000001D7, + 0x1D8000001D9, + 0x1DA000001DB, + 0x1DC000001DE, + 0x1DF000001E0, + 0x1E1000001E2, + 0x1E3000001E4, + 0x1E5000001E6, + 0x1E7000001E8, + 0x1E9000001EA, + 0x1EB000001EC, + 0x1ED000001EE, + 0x1EF000001F1, + 0x1F5000001F6, + 0x1F9000001FA, + 0x1FB000001FC, + 0x1FD000001FE, + 0x1FF00000200, + 0x20100000202, + 0x20300000204, + 0x20500000206, + 0x20700000208, + 0x2090000020A, + 0x20B0000020C, + 0x20D0000020E, + 0x20F00000210, + 0x21100000212, + 0x21300000214, + 0x21500000216, + 0x21700000218, + 0x2190000021A, + 0x21B0000021C, + 0x21D0000021E, + 0x21F00000220, + 0x22100000222, + 0x22300000224, + 0x22500000226, + 0x22700000228, + 0x2290000022A, + 0x22B0000022C, + 0x22D0000022E, + 0x22F00000230, + 0x23100000232, + 0x2330000023A, + 0x23C0000023D, + 0x23F00000241, + 0x24200000243, + 0x24700000248, + 0x2490000024A, + 0x24B0000024C, + 0x24D0000024E, + 0x24F000002B0, + 0x2B9000002C2, + 0x2C6000002D2, + 0x2EC000002ED, + 0x2EE000002EF, + 0x30000000340, + 0x34200000343, + 0x3460000034F, + 0x35000000370, + 0x37100000372, + 0x37300000374, + 0x37700000378, + 0x37B0000037E, + 0x39000000391, + 0x3AC000003CF, + 0x3D7000003D8, + 0x3D9000003DA, + 0x3DB000003DC, + 0x3DD000003DE, + 0x3DF000003E0, + 0x3E1000003E2, + 0x3E3000003E4, + 0x3E5000003E6, + 0x3E7000003E8, + 0x3E9000003EA, + 0x3EB000003EC, + 0x3ED000003EE, + 0x3EF000003F0, + 0x3F3000003F4, + 0x3F8000003F9, + 0x3FB000003FD, + 0x43000000460, + 0x46100000462, + 0x46300000464, + 0x46500000466, + 0x46700000468, + 0x4690000046A, + 0x46B0000046C, + 0x46D0000046E, + 0x46F00000470, + 0x47100000472, + 0x47300000474, + 0x47500000476, + 0x47700000478, + 0x4790000047A, + 0x47B0000047C, + 0x47D0000047E, + 0x47F00000480, + 0x48100000482, + 0x48300000488, + 0x48B0000048C, + 0x48D0000048E, + 0x48F00000490, + 0x49100000492, + 0x49300000494, + 0x49500000496, + 0x49700000498, + 0x4990000049A, + 0x49B0000049C, + 0x49D0000049E, + 0x49F000004A0, + 0x4A1000004A2, + 0x4A3000004A4, + 0x4A5000004A6, + 0x4A7000004A8, + 0x4A9000004AA, + 0x4AB000004AC, + 0x4AD000004AE, + 0x4AF000004B0, + 0x4B1000004B2, + 0x4B3000004B4, + 0x4B5000004B6, + 0x4B7000004B8, + 0x4B9000004BA, + 0x4BB000004BC, + 0x4BD000004BE, + 0x4BF000004C0, + 0x4C2000004C3, + 0x4C4000004C5, + 0x4C6000004C7, + 0x4C8000004C9, + 0x4CA000004CB, + 0x4CC000004CD, + 0x4CE000004D0, + 0x4D1000004D2, + 0x4D3000004D4, + 0x4D5000004D6, + 0x4D7000004D8, + 0x4D9000004DA, + 0x4DB000004DC, + 0x4DD000004DE, + 0x4DF000004E0, + 0x4E1000004E2, + 0x4E3000004E4, + 0x4E5000004E6, + 0x4E7000004E8, + 0x4E9000004EA, + 0x4EB000004EC, + 0x4ED000004EE, + 0x4EF000004F0, + 0x4F1000004F2, + 0x4F3000004F4, + 0x4F5000004F6, + 0x4F7000004F8, + 0x4F9000004FA, + 0x4FB000004FC, + 0x4FD000004FE, + 0x4FF00000500, + 0x50100000502, + 0x50300000504, + 0x50500000506, + 0x50700000508, + 0x5090000050A, + 0x50B0000050C, + 0x50D0000050E, + 0x50F00000510, + 0x51100000512, + 0x51300000514, + 0x51500000516, + 0x51700000518, + 0x5190000051A, + 0x51B0000051C, + 0x51D0000051E, + 0x51F00000520, + 0x52100000522, + 0x52300000524, + 0x52500000526, + 0x52700000528, + 0x5290000052A, + 0x52B0000052C, + 0x52D0000052E, + 0x52F00000530, + 0x5590000055A, + 0x56000000587, + 0x58800000589, + 0x591000005BE, + 0x5BF000005C0, + 0x5C1000005C3, + 0x5C4000005C6, + 0x5C7000005CA, + 0x5D0000005EB, + 0x5EF000005F3, + 0x6100000061B, + 0x62000000640, + 0x64100000660, + 0x66E00000675, + 0x679000006D4, + 0x6D5000006DD, + 0x6DF000006E9, + 0x6EA000006F0, + 0x6FA00000700, + 0x7100000074B, + 0x74D000007B2, + 0x7C0000007F6, + 0x7FD000007FE, + 0x8000000082E, + 0x8400000085C, + 0x8600000086B, + 0x87000000888, + 0x88900000890, + 0x897000008E2, + 0x8E300000958, + 0x96000000964, + 0x96600000970, + 0x97100000984, + 0x9850000098D, + 0x98F00000991, + 0x993000009A9, + 0x9AA000009B1, + 0x9B2000009B3, + 0x9B6000009BA, + 0x9BC000009C5, + 0x9C7000009C9, + 0x9CB000009CF, + 0x9D7000009D8, + 0x9E0000009E4, + 0x9E6000009F2, + 0x9FC000009FD, + 0x9FE000009FF, + 0xA0100000A04, + 0xA0500000A0B, + 0xA0F00000A11, + 0xA1300000A29, + 0xA2A00000A31, + 0xA3200000A33, + 0xA3500000A36, + 0xA3800000A3A, + 0xA3C00000A3D, + 0xA3E00000A43, + 0xA4700000A49, + 0xA4B00000A4E, + 0xA5100000A52, + 0xA5C00000A5D, + 0xA6600000A76, + 0xA8100000A84, + 0xA8500000A8E, + 0xA8F00000A92, + 0xA9300000AA9, + 0xAAA00000AB1, + 0xAB200000AB4, + 0xAB500000ABA, + 0xABC00000AC6, + 0xAC700000ACA, + 0xACB00000ACE, + 0xAD000000AD1, + 0xAE000000AE4, + 0xAE600000AF0, + 0xAF900000B00, + 0xB0100000B04, + 0xB0500000B0D, + 0xB0F00000B11, + 0xB1300000B29, + 0xB2A00000B31, + 0xB3200000B34, + 0xB3500000B3A, + 0xB3C00000B45, + 0xB4700000B49, + 0xB4B00000B4E, + 0xB5300000B58, + 0xB5F00000B64, + 0xB6600000B70, + 0xB7100000B72, + 0xB8200000B84, + 0xB8500000B8B, + 0xB8E00000B91, + 0xB9200000B96, + 0xB9900000B9B, + 0xB9C00000B9D, + 0xB9E00000BA0, + 0xBA300000BA5, + 0xBA800000BAB, + 0xBAE00000BBA, + 0xBBE00000BC3, + 0xBC600000BC9, + 0xBCA00000BCE, + 0xBD000000BD1, + 0xBD700000BD8, + 0xBE600000BF0, + 0xC0000000C0D, + 0xC0E00000C11, + 0xC1200000C29, + 0xC2A00000C3A, + 0xC3C00000C45, + 0xC4600000C49, + 0xC4A00000C4E, + 0xC5500000C57, + 0xC5800000C5B, + 0xC5C00000C5E, + 0xC6000000C64, + 0xC6600000C70, + 0xC8000000C84, + 0xC8500000C8D, + 0xC8E00000C91, + 0xC9200000CA9, + 0xCAA00000CB4, + 0xCB500000CBA, + 0xCBC00000CC5, + 0xCC600000CC9, + 0xCCA00000CCE, + 0xCD500000CD7, + 0xCDC00000CDF, + 0xCE000000CE4, + 0xCE600000CF0, + 0xCF100000CF4, + 0xD0000000D0D, + 0xD0E00000D11, + 0xD1200000D45, + 0xD4600000D49, + 0xD4A00000D4F, + 0xD5400000D58, + 0xD5F00000D64, + 0xD6600000D70, + 0xD7A00000D80, + 0xD8100000D84, + 0xD8500000D97, + 0xD9A00000DB2, + 0xDB300000DBC, + 0xDBD00000DBE, + 0xDC000000DC7, + 0xDCA00000DCB, + 0xDCF00000DD5, + 0xDD600000DD7, + 0xDD800000DE0, + 0xDE600000DF0, + 0xDF200000DF4, + 0xE0100000E33, + 0xE3400000E3B, + 0xE4000000E4F, + 0xE5000000E5A, + 0xE8100000E83, + 0xE8400000E85, + 0xE8600000E8B, + 0xE8C00000EA4, + 0xEA500000EA6, + 0xEA700000EB3, + 0xEB400000EBE, + 0xEC000000EC5, + 0xEC600000EC7, + 0xEC800000ECF, + 0xED000000EDA, + 0xEDE00000EE0, + 0xF0000000F01, + 0xF0B00000F0C, + 0xF1800000F1A, + 0xF2000000F2A, + 0xF3500000F36, + 0xF3700000F38, + 0xF3900000F3A, + 0xF3E00000F43, + 0xF4400000F48, + 0xF4900000F4D, + 0xF4E00000F52, + 0xF5300000F57, + 0xF5800000F5C, + 0xF5D00000F69, + 0xF6A00000F6D, + 0xF7100000F73, + 0xF7400000F75, + 0xF7A00000F81, + 0xF8200000F85, + 0xF8600000F93, + 0xF9400000F98, + 0xF9900000F9D, + 0xF9E00000FA2, + 0xFA300000FA7, + 0xFA800000FAC, + 0xFAD00000FB9, + 0xFBA00000FBD, + 0xFC600000FC7, + 0x10000000104A, + 0x10500000109E, + 0x10D0000010FB, + 0x10FD00001100, + 0x120000001249, + 0x124A0000124E, + 0x125000001257, + 0x125800001259, + 0x125A0000125E, + 0x126000001289, + 0x128A0000128E, + 0x1290000012B1, + 0x12B2000012B6, + 0x12B8000012BF, + 0x12C0000012C1, + 0x12C2000012C6, + 0x12C8000012D7, + 0x12D800001311, + 0x131200001316, + 0x13180000135B, + 0x135D00001360, + 0x138000001390, + 0x13A0000013F6, + 0x14010000166D, + 0x166F00001680, + 0x16810000169B, + 0x16A0000016EB, + 0x16F1000016F9, + 0x170000001716, + 0x171F00001735, + 0x174000001754, + 0x17600000176D, + 0x176E00001771, + 0x177200001774, + 0x1780000017B4, + 0x17B6000017D4, + 0x17D7000017D8, + 0x17DC000017DE, + 0x17E0000017EA, + 0x18100000181A, + 0x182000001879, + 0x1880000018AB, + 0x18B0000018F6, + 0x19000000191F, + 0x19200000192C, + 0x19300000193C, + 0x19460000196E, + 0x197000001975, + 0x1980000019AC, + 0x19B0000019CA, + 0x19D0000019DA, + 0x1A0000001A1C, + 0x1A2000001A5F, + 0x1A6000001A7D, + 0x1A7F00001A8A, + 0x1A9000001A9A, + 0x1AA700001AA8, + 0x1AB000001ABE, + 0x1ABF00001AF1, + 0x1B0000001B4D, + 0x1B5000001B5A, + 0x1B6B00001B74, + 0x1B8000001BF4, + 0x1C0000001C38, + 0x1C4000001C4A, + 0x1C4D00001C7E, + 0x1C8A00001C8B, + 0x1CD000001CD3, + 0x1CD400001CFB, + 0x1D0000001D2C, + 0x1D2F00001D30, + 0x1D3B00001D3C, + 0x1D4E00001D4F, + 0x1D6B00001D78, + 0x1D7900001D9B, + 0x1DC000001E00, + 0x1E0100001E02, + 0x1E0300001E04, + 0x1E0500001E06, + 0x1E0700001E08, + 0x1E0900001E0A, + 0x1E0B00001E0C, + 0x1E0D00001E0E, + 0x1E0F00001E10, + 0x1E1100001E12, + 0x1E1300001E14, + 0x1E1500001E16, + 0x1E1700001E18, + 0x1E1900001E1A, + 0x1E1B00001E1C, + 0x1E1D00001E1E, + 0x1E1F00001E20, + 0x1E2100001E22, + 0x1E2300001E24, + 0x1E2500001E26, + 0x1E2700001E28, + 0x1E2900001E2A, + 0x1E2B00001E2C, + 0x1E2D00001E2E, + 0x1E2F00001E30, + 0x1E3100001E32, + 0x1E3300001E34, + 0x1E3500001E36, + 0x1E3700001E38, + 0x1E3900001E3A, + 0x1E3B00001E3C, + 0x1E3D00001E3E, + 0x1E3F00001E40, + 0x1E4100001E42, + 0x1E4300001E44, + 0x1E4500001E46, + 0x1E4700001E48, + 0x1E4900001E4A, + 0x1E4B00001E4C, + 0x1E4D00001E4E, + 0x1E4F00001E50, + 0x1E5100001E52, + 0x1E5300001E54, + 0x1E5500001E56, + 0x1E5700001E58, + 0x1E5900001E5A, + 0x1E5B00001E5C, + 0x1E5D00001E5E, + 0x1E5F00001E60, + 0x1E6100001E62, + 0x1E6300001E64, + 0x1E6500001E66, + 0x1E6700001E68, + 0x1E6900001E6A, + 0x1E6B00001E6C, + 0x1E6D00001E6E, + 0x1E6F00001E70, + 0x1E7100001E72, + 0x1E7300001E74, + 0x1E7500001E76, + 0x1E7700001E78, + 0x1E7900001E7A, + 0x1E7B00001E7C, + 0x1E7D00001E7E, + 0x1E7F00001E80, + 0x1E8100001E82, + 0x1E8300001E84, + 0x1E8500001E86, + 0x1E8700001E88, + 0x1E8900001E8A, + 0x1E8B00001E8C, + 0x1E8D00001E8E, + 0x1E8F00001E90, + 0x1E9100001E92, + 0x1E9300001E94, + 0x1E9500001E9A, + 0x1E9C00001E9E, + 0x1E9F00001EA0, + 0x1EA100001EA2, + 0x1EA300001EA4, + 0x1EA500001EA6, + 0x1EA700001EA8, + 0x1EA900001EAA, + 0x1EAB00001EAC, + 0x1EAD00001EAE, + 0x1EAF00001EB0, + 0x1EB100001EB2, + 0x1EB300001EB4, + 0x1EB500001EB6, + 0x1EB700001EB8, + 0x1EB900001EBA, + 0x1EBB00001EBC, + 0x1EBD00001EBE, + 0x1EBF00001EC0, + 0x1EC100001EC2, + 0x1EC300001EC4, + 0x1EC500001EC6, + 0x1EC700001EC8, + 0x1EC900001ECA, + 0x1ECB00001ECC, + 0x1ECD00001ECE, + 0x1ECF00001ED0, + 0x1ED100001ED2, + 0x1ED300001ED4, + 0x1ED500001ED6, + 0x1ED700001ED8, + 0x1ED900001EDA, + 0x1EDB00001EDC, + 0x1EDD00001EDE, + 0x1EDF00001EE0, + 0x1EE100001EE2, + 0x1EE300001EE4, + 0x1EE500001EE6, + 0x1EE700001EE8, + 0x1EE900001EEA, + 0x1EEB00001EEC, + 0x1EED00001EEE, + 0x1EEF00001EF0, + 0x1EF100001EF2, + 0x1EF300001EF4, + 0x1EF500001EF6, + 0x1EF700001EF8, + 0x1EF900001EFA, + 0x1EFB00001EFC, + 0x1EFD00001EFE, + 0x1EFF00001F08, + 0x1F1000001F16, + 0x1F2000001F28, + 0x1F3000001F38, + 0x1F4000001F46, + 0x1F5000001F58, + 0x1F6000001F68, + 0x1F7000001F71, + 0x1F7200001F73, + 0x1F7400001F75, + 0x1F7600001F77, + 0x1F7800001F79, + 0x1F7A00001F7B, + 0x1F7C00001F7D, + 0x1FB000001FB2, + 0x1FB600001FB7, + 0x1FC600001FC7, + 0x1FD000001FD3, + 0x1FD600001FD8, + 0x1FE000001FE3, + 0x1FE400001FE8, + 0x1FF600001FF7, + 0x208F00002090, + 0x214E0000214F, + 0x218400002185, + 0x2C3000002C60, + 0x2C6100002C62, + 0x2C6500002C67, + 0x2C6800002C69, + 0x2C6A00002C6B, + 0x2C6C00002C6D, + 0x2C7100002C72, + 0x2C7300002C75, + 0x2C7600002C7C, + 0x2C8100002C82, + 0x2C8300002C84, + 0x2C8500002C86, + 0x2C8700002C88, + 0x2C8900002C8A, + 0x2C8B00002C8C, + 0x2C8D00002C8E, + 0x2C8F00002C90, + 0x2C9100002C92, + 0x2C9300002C94, + 0x2C9500002C96, + 0x2C9700002C98, + 0x2C9900002C9A, + 0x2C9B00002C9C, + 0x2C9D00002C9E, + 0x2C9F00002CA0, + 0x2CA100002CA2, + 0x2CA300002CA4, + 0x2CA500002CA6, + 0x2CA700002CA8, + 0x2CA900002CAA, + 0x2CAB00002CAC, + 0x2CAD00002CAE, + 0x2CAF00002CB0, + 0x2CB100002CB2, + 0x2CB300002CB4, + 0x2CB500002CB6, + 0x2CB700002CB8, + 0x2CB900002CBA, + 0x2CBB00002CBC, + 0x2CBD00002CBE, + 0x2CBF00002CC0, + 0x2CC100002CC2, + 0x2CC300002CC4, + 0x2CC500002CC6, + 0x2CC700002CC8, + 0x2CC900002CCA, + 0x2CCB00002CCC, + 0x2CCD00002CCE, + 0x2CCF00002CD0, + 0x2CD100002CD2, + 0x2CD300002CD4, + 0x2CD500002CD6, + 0x2CD700002CD8, + 0x2CD900002CDA, + 0x2CDB00002CDC, + 0x2CDD00002CDE, + 0x2CDF00002CE0, + 0x2CE100002CE2, + 0x2CE300002CE5, + 0x2CEC00002CED, + 0x2CEE00002CF2, + 0x2CF300002CF4, + 0x2D0000002D26, + 0x2D2700002D28, + 0x2D2D00002D2E, + 0x2D3000002D68, + 0x2D7F00002D97, + 0x2DA000002DA7, + 0x2DA800002DAF, + 0x2DB000002DB7, + 0x2DB800002DBF, + 0x2DC000002DC7, + 0x2DC800002DCF, + 0x2DD000002DD7, + 0x2DD800002DDF, + 0x2DE000002E00, + 0x2E2F00002E30, + 0x300500003008, + 0x302A0000302E, + 0x303C0000303D, + 0x304100003097, + 0x30990000309B, + 0x309D0000309F, + 0x30A1000030FB, + 0x30FC000030FF, + 0x310500003130, + 0x31A0000031C0, + 0x31F000003200, + 0x340000004DC0, + 0x4E000000A48D, + 0xA4D00000A4FE, + 0xA5000000A60D, + 0xA6100000A62C, + 0xA6410000A642, + 0xA6430000A644, + 0xA6450000A646, + 0xA6470000A648, + 0xA6490000A64A, + 0xA64B0000A64C, + 0xA64D0000A64E, + 0xA64F0000A650, + 0xA6510000A652, + 0xA6530000A654, + 0xA6550000A656, + 0xA6570000A658, + 0xA6590000A65A, + 0xA65B0000A65C, + 0xA65D0000A65E, + 0xA65F0000A660, + 0xA6610000A662, + 0xA6630000A664, + 0xA6650000A666, + 0xA6670000A668, + 0xA6690000A66A, + 0xA66B0000A66C, + 0xA66D0000A670, + 0xA6740000A67E, + 0xA67F0000A680, + 0xA6810000A682, + 0xA6830000A684, + 0xA6850000A686, + 0xA6870000A688, + 0xA6890000A68A, + 0xA68B0000A68C, + 0xA68D0000A68E, + 0xA68F0000A690, + 0xA6910000A692, + 0xA6930000A694, + 0xA6950000A696, + 0xA6970000A698, + 0xA6990000A69A, + 0xA69B0000A69C, + 0xA69E0000A6E6, + 0xA6F00000A6F2, + 0xA7170000A720, + 0xA7230000A724, + 0xA7250000A726, + 0xA7270000A728, + 0xA7290000A72A, + 0xA72B0000A72C, + 0xA72D0000A72E, + 0xA72F0000A732, + 0xA7330000A734, + 0xA7350000A736, + 0xA7370000A738, + 0xA7390000A73A, + 0xA73B0000A73C, + 0xA73D0000A73E, + 0xA73F0000A740, + 0xA7410000A742, + 0xA7430000A744, + 0xA7450000A746, + 0xA7470000A748, + 0xA7490000A74A, + 0xA74B0000A74C, + 0xA74D0000A74E, + 0xA74F0000A750, + 0xA7510000A752, + 0xA7530000A754, + 0xA7550000A756, + 0xA7570000A758, + 0xA7590000A75A, + 0xA75B0000A75C, + 0xA75D0000A75E, + 0xA75F0000A760, + 0xA7610000A762, + 0xA7630000A764, + 0xA7650000A766, + 0xA7670000A768, + 0xA7690000A76A, + 0xA76B0000A76C, + 0xA76D0000A76E, + 0xA76F0000A770, + 0xA7710000A779, + 0xA77A0000A77B, + 0xA77C0000A77D, + 0xA77F0000A780, + 0xA7810000A782, + 0xA7830000A784, + 0xA7850000A786, + 0xA7870000A789, + 0xA78C0000A78D, + 0xA78E0000A790, + 0xA7910000A792, + 0xA7930000A796, + 0xA7970000A798, + 0xA7990000A79A, + 0xA79B0000A79C, + 0xA79D0000A79E, + 0xA79F0000A7A0, + 0xA7A10000A7A2, + 0xA7A30000A7A4, + 0xA7A50000A7A6, + 0xA7A70000A7A8, + 0xA7A90000A7AA, + 0xA7AF0000A7B0, + 0xA7B50000A7B6, + 0xA7B70000A7B8, + 0xA7B90000A7BA, + 0xA7BB0000A7BC, + 0xA7BD0000A7BE, + 0xA7BF0000A7C0, + 0xA7C10000A7C2, + 0xA7C30000A7C4, + 0xA7C80000A7C9, + 0xA7CA0000A7CB, + 0xA7CD0000A7CE, + 0xA7CF0000A7D0, + 0xA7D10000A7D2, + 0xA7D30000A7D4, + 0xA7D50000A7D6, + 0xA7D70000A7D8, + 0xA7D90000A7DA, + 0xA7DB0000A7DC, + 0xA7F60000A7F8, + 0xA7FA0000A828, + 0xA82C0000A82D, + 0xA8400000A874, + 0xA8800000A8C6, + 0xA8D00000A8DA, + 0xA8E00000A8F8, + 0xA8FB0000A8FC, + 0xA8FD0000A92E, + 0xA9300000A954, + 0xA9800000A9C1, + 0xA9CF0000A9DA, + 0xA9E00000A9FF, + 0xAA000000AA37, + 0xAA400000AA4E, + 0xAA500000AA5A, + 0xAA600000AA77, + 0xAA7A0000AAC3, + 0xAADB0000AADE, + 0xAAE00000AAF0, + 0xAAF20000AAF7, + 0xAB010000AB07, + 0xAB090000AB0F, + 0xAB110000AB17, + 0xAB200000AB27, + 0xAB280000AB2F, + 0xAB300000AB5B, + 0xAB600000AB69, + 0xABC00000ABEB, + 0xABEC0000ABEE, + 0xABF00000ABFA, + 0xAC000000D7A4, + 0xFA0E0000FA10, + 0xFA110000FA12, + 0xFA130000FA15, + 0xFA1F0000FA20, + 0xFA210000FA22, + 0xFA230000FA25, + 0xFA270000FA2A, + 0xFB1E0000FB1F, + 0xFE200000FE30, + 0xFE730000FE74, + 0x100000001000C, + 0x1000D00010027, + 0x100280001003B, + 0x1003C0001003E, + 0x1003F0001004E, + 0x100500001005E, + 0x10080000100FB, + 0x101FD000101FE, + 0x102800001029D, + 0x102A0000102D1, + 0x102E0000102E1, + 0x1030000010320, + 0x1032D00010341, + 0x103420001034A, + 0x103500001037B, + 0x103800001039E, + 0x103A0000103C4, + 0x103C8000103D0, + 0x104280001049E, + 0x104A0000104AA, + 0x104D8000104FC, + 0x1050000010528, + 0x1053000010564, + 0x10597000105A2, + 0x105A3000105B2, + 0x105B3000105BA, + 0x105BB000105BD, + 0x105C0000105F4, + 0x1060000010737, + 0x1074000010756, + 0x1076000010768, + 0x1078000010781, + 0x1080000010806, + 0x1080800010809, + 0x1080A00010836, + 0x1083700010839, + 0x1083C0001083D, + 0x1083F00010856, + 0x1086000010877, + 0x108800001089F, + 0x108E0000108F3, + 0x108F4000108F6, + 0x1090000010916, + 0x109200001093A, + 0x109400001095A, + 0x10980000109B8, + 0x109BE000109C0, + 0x10A0000010A04, + 0x10A0500010A07, + 0x10A0C00010A14, + 0x10A1500010A18, + 0x10A1900010A36, + 0x10A3800010A3B, + 0x10A3F00010A40, + 0x10A6000010A7D, + 0x10A8000010A9D, + 0x10AC000010AC8, + 0x10AC900010AE7, + 0x10B0000010B36, + 0x10B4000010B56, + 0x10B6000010B73, + 0x10B8000010B92, + 0x10C0000010C49, + 0x10CC000010CF3, + 0x10D0000010D28, + 0x10D3000010D3A, + 0x10D4000010D50, + 0x10D6900010D6E, + 0x10D6F00010D86, + 0x10E8000010EAA, + 0x10EAB00010EAD, + 0x10EB000010EB2, + 0x10EC200010EC8, + 0x10ECB00010ED0, + 0x10ED900010EEF, + 0x10EF000010F1D, + 0x10F2700010F28, + 0x10F3000010F51, + 0x10F7000010F86, + 0x10FB000010FC5, + 0x10FE000010FF7, + 0x1100000011047, + 0x1106600011076, + 0x1107F000110BB, + 0x110C2000110C3, + 0x110D0000110E9, + 0x110F0000110FA, + 0x1110000011135, + 0x1113600011140, + 0x1114400011148, + 0x1115000011174, + 0x1117600011177, + 0x11180000111C5, + 0x111C9000111CD, + 0x111CE000111DB, + 0x111DC000111DD, + 0x1120000011212, + 0x1121300011238, + 0x1123E00011242, + 0x1128000011287, + 0x1128800011289, + 0x1128A0001128E, + 0x1128F0001129E, + 0x1129F000112A9, + 0x112B0000112EB, + 0x112F0000112FA, + 0x1130000011304, + 0x113050001130D, + 0x1130F00011311, + 0x1131300011329, + 0x1132A00011331, + 0x1133200011334, + 0x113350001133A, + 0x1133B00011345, + 0x1134700011349, + 0x1134B0001134E, + 0x1135000011351, + 0x1135700011358, + 0x1135D00011364, + 0x113660001136D, + 0x1137000011375, + 0x113800001138A, + 0x1138B0001138C, + 0x1138E0001138F, + 0x11390000113B6, + 0x113B7000113C1, + 0x113C2000113C3, + 0x113C5000113C6, + 0x113C7000113CB, + 0x113CC000113D4, + 0x113E1000113E3, + 0x114000001144B, + 0x114500001145A, + 0x1145E00011462, + 0x11480000114C6, + 0x114C7000114C8, + 0x114D0000114DA, + 0x11580000115B6, + 0x115B8000115C1, + 0x115D8000115DE, + 0x1160000011641, + 0x1164400011645, + 0x116500001165A, + 0x11680000116B9, + 0x116C0000116CA, + 0x116D0000116E4, + 0x117000001171B, + 0x1171D0001172C, + 0x117300001173A, + 0x1174000011747, + 0x118000001183B, + 0x118C0000118EA, + 0x118FF00011907, + 0x119090001190A, + 0x1190C00011914, + 0x1191500011917, + 0x1191800011936, + 0x1193700011939, + 0x1193B00011944, + 0x119500001195A, + 0x119A0000119A8, + 0x119AA000119D8, + 0x119DA000119E2, + 0x119E3000119E5, + 0x11A0000011A3F, + 0x11A4700011A48, + 0x11A5000011A9A, + 0x11A9D00011A9E, + 0x11AB000011AF9, + 0x11B0A00011B0B, + 0x11B6000011B68, + 0x11BC000011BE1, + 0x11BF000011BFA, + 0x11C0000011C09, + 0x11C0A00011C37, + 0x11C3800011C41, + 0x11C5000011C5A, + 0x11C7200011C90, + 0x11C9200011CA8, + 0x11CA900011CB7, + 0x11D0000011D07, + 0x11D0800011D0A, + 0x11D0B00011D37, + 0x11D3A00011D3B, + 0x11D3C00011D3E, + 0x11D3F00011D48, + 0x11D5000011D5A, + 0x11D6000011D66, + 0x11D6700011D69, + 0x11D6A00011D8F, + 0x11D9000011D92, + 0x11D9300011D99, + 0x11DA000011DAA, + 0x11DB000011DDC, + 0x11DE000011DEA, + 0x11DF000011DF2, + 0x11EE000011EF7, + 0x11F0000011F11, + 0x11F1200011F3B, + 0x11F3E00011F43, + 0x11F5000011F5B, + 0x11FB000011FB1, + 0x120000001239A, + 0x1248000012544, + 0x12F9000012FF1, + 0x1300000013430, + 0x1344000013456, + 0x13460000143FB, + 0x1440000014647, + 0x161000001613A, + 0x1680000016A39, + 0x16A4000016A5F, + 0x16A6000016A6A, + 0x16A7000016ABF, + 0x16AC000016ACA, + 0x16AD000016AEE, + 0x16AF000016AF5, + 0x16B0000016B37, + 0x16B4000016B44, + 0x16B5000016B5A, + 0x16B6300016B78, + 0x16B7D00016B90, + 0x16D4000016D6D, + 0x16D7000016D7A, + 0x16E6000016E80, + 0x16EBB00016ED4, + 0x16F0000016F4B, + 0x16F4F00016F88, + 0x16F8F00016FA0, + 0x16FE000016FE2, + 0x16FE300016FE5, + 0x16FF000016FF4, + 0x1700000018CDB, + 0x18CFF00018D21, + 0x18D8000018DF3, + 0x18E0000019192, + 0x191A0000191D3, + 0x1AFF00001AFF4, + 0x1AFF50001AFFC, + 0x1AFFD0001AFFF, + 0x1B0000001B123, + 0x1B1270001B129, + 0x1B1320001B133, + 0x1B1500001B153, + 0x1B1550001B156, + 0x1B1640001B169, + 0x1B1700001B2FC, + 0x1BC000001BC6B, + 0x1BC700001BC7D, + 0x1BC800001BC89, + 0x1BC900001BC9A, + 0x1BC9D0001BC9F, + 0x1CF000001CF2E, + 0x1CF300001CF47, + 0x1D2500001D253, + 0x1D25B0001D25D, + 0x1D25F0001D260, + 0x1D2800001D282, + 0x1DA000001DA37, + 0x1DA3B0001DA6D, + 0x1DA750001DA76, + 0x1DA840001DA85, + 0x1DA9B0001DAA0, + 0x1DAA10001DAB0, + 0x1DF000001DF40, + 0x1DF410001DF48, + 0x1DF490001DF4A, + 0x1DF4B0001DF4D, + 0x1DF4E0001DF51, + 0x1DF520001DF68, + 0x1DF690001DF6A, + 0x1DF6B0001DF6C, + 0x1DF6D0001DF6E, + 0x1DF6F0001DF72, + 0x1DF730001DF74, + 0x1DF750001DF76, + 0x1DF770001DF78, + 0x1DF790001DF7A, + 0x1DF7B0001DF7C, + 0x1DF7D0001DF7E, + 0x1DF7F0001DF82, + 0x1DF900001DF95, + 0x1DF960001DF97, + 0x1E0000001E007, + 0x1E0080001E019, + 0x1E01B0001E022, + 0x1E0230001E025, + 0x1E0260001E02B, + 0x1E08F0001E090, + 0x1E1000001E12D, + 0x1E1300001E13E, + 0x1E1400001E14A, + 0x1E14E0001E14F, + 0x1E2900001E2AF, + 0x1E2C00001E2FA, + 0x1E4D00001E4FA, + 0x1E5D00001E5FB, + 0x1E6C00001E6DF, + 0x1E6E00001E6F6, + 0x1E6FE0001E700, + 0x1E7E00001E7E7, + 0x1E7E80001E7EC, + 0x1E7ED0001E7EF, + 0x1E7F00001E7FF, + 0x1E8000001E8C5, + 0x1E8D00001E8D7, + 0x1E9220001E94C, + 0x1E9500001E95A, + 0x200000002A6E0, + 0x2A7000002B81F, + 0x2B8200002CEAE, + 0x2CEB00002EBE1, + 0x2EBF00002EE5E, + 0x300000003134B, + 0x313500003347A, + 0x3D0000003FC40, + ), + "CONTEXTJ": (0x200C0000200E,), + "CONTEXTO": ( + 0xB7000000B8, + 0x37500000376, + 0x5F3000005F5, + 0x6600000066A, + 0x6F0000006FA, + 0x30FB000030FC, + ), +} diff --git a/source/target-vendors/cp312/idna/intranges.py b/source/target-vendors/cp312/idna/intranges.py new file mode 100644 index 0000000..19d7781 --- /dev/null +++ b/source/target-vendors/cp312/idna/intranges.py @@ -0,0 +1,55 @@ +""" +Given a list of integers, made up of (hopefully) a small number of long runs +of consecutive integers, compute a representation of the form +((start1, end1), (start2, end2) ...). Then answer the question "was x present +in the original list?" in time O(log(# runs)). +""" + +import bisect + + +def intranges_from_list(list_: list[int]) -> tuple[int, ...]: + """Represent a list of integers as a sequence of ranges: + ((start_0, end_0), (start_1, end_1), ...), such that the original + integers are exactly those x such that start_i <= x < end_i for some i. + + Ranges are encoded as single integers (start << 32 | end), not as tuples. + """ + + sorted_list = sorted(list_) + ranges = [] + last_write = -1 + for i in range(len(sorted_list)): + if i + 1 < len(sorted_list) and sorted_list[i] == sorted_list[i + 1] - 1: + continue + current_range = sorted_list[last_write + 1 : i + 1] + ranges.append(_encode_range(current_range[0], current_range[-1] + 1)) + last_write = i + + return tuple(ranges) + + +def _encode_range(start: int, end: int) -> int: + return (start << 32) | end + + +def _decode_range(r: int) -> tuple[int, int]: + return (r >> 32), (r & ((1 << 32) - 1)) + + +def intranges_contain(int_: int, ranges: tuple[int, ...]) -> bool: + """Determine if `int_` falls into one of the ranges in `ranges`.""" + tuple_ = _encode_range(int_, 0) + pos = bisect.bisect_left(ranges, tuple_) + # we could be immediately ahead of a tuple (start, end) + # with start < int_ <= end + if pos > 0: + left, right = _decode_range(ranges[pos - 1]) + if left <= int_ < right: + return True + # or we could be immediately behind a tuple (int_, end) + if pos < len(ranges): + left, _ = _decode_range(ranges[pos]) + if left == int_: + return True + return False diff --git a/source/target-vendors/cp312/idna/package_data.py b/source/target-vendors/cp312/idna/package_data.py new file mode 100644 index 0000000..2f9ff7e --- /dev/null +++ b/source/target-vendors/cp312/idna/package_data.py @@ -0,0 +1 @@ +__version__ = "3.20" diff --git a/source/target-vendors/cp312/idna/py.typed b/source/target-vendors/cp312/idna/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/idna/uts46data.py b/source/target-vendors/cp312/idna/uts46data.py new file mode 100644 index 0000000..dbfb60f --- /dev/null +++ b/source/target-vendors/cp312/idna/uts46data.py @@ -0,0 +1,17117 @@ +# This file is automatically generated by tools/idna-data + +from __future__ import annotations + +from array import array + +"""IDNA Mapping Table from UTS46.""" + + +__version__ = "18.0.0" + +uts46_starts: array[int] = array( + "I", + ( + 0x0, + 0x1, + 0x2, + 0x3, + 0x4, + 0x5, + 0x6, + 0x7, + 0x8, + 0x9, + 0xA, + 0xB, + 0xC, + 0xD, + 0xE, + 0xF, + 0x10, + 0x11, + 0x12, + 0x13, + 0x14, + 0x15, + 0x16, + 0x17, + 0x18, + 0x19, + 0x1A, + 0x1B, + 0x1C, + 0x1D, + 0x1E, + 0x1F, + 0x20, + 0x21, + 0x22, + 0x23, + 0x24, + 0x25, + 0x26, + 0x27, + 0x28, + 0x29, + 0x2A, + 0x2B, + 0x2C, + 0x2D, + 0x2E, + 0x2F, + 0x30, + 0x31, + 0x32, + 0x33, + 0x34, + 0x35, + 0x36, + 0x37, + 0x38, + 0x39, + 0x3A, + 0x3B, + 0x3C, + 0x3D, + 0x3E, + 0x3F, + 0x40, + 0x41, + 0x42, + 0x43, + 0x44, + 0x45, + 0x46, + 0x47, + 0x48, + 0x49, + 0x4A, + 0x4B, + 0x4C, + 0x4D, + 0x4E, + 0x4F, + 0x50, + 0x51, + 0x52, + 0x53, + 0x54, + 0x55, + 0x56, + 0x57, + 0x58, + 0x59, + 0x5A, + 0x5B, + 0x5C, + 0x5D, + 0x5E, + 0x5F, + 0x60, + 0x61, + 0x62, + 0x63, + 0x64, + 0x65, + 0x66, + 0x67, + 0x68, + 0x69, + 0x6A, + 0x6B, + 0x6C, + 0x6D, + 0x6E, + 0x6F, + 0x70, + 0x71, + 0x72, + 0x73, + 0x74, + 0x75, + 0x76, + 0x77, + 0x78, + 0x79, + 0x7A, + 0x7B, + 0x7C, + 0x7D, + 0x7E, + 0x7F, + 0x80, + 0x81, + 0x82, + 0x83, + 0x84, + 0x85, + 0x86, + 0x87, + 0x88, + 0x89, + 0x8A, + 0x8B, + 0x8C, + 0x8D, + 0x8E, + 0x8F, + 0x90, + 0x91, + 0x92, + 0x93, + 0x94, + 0x95, + 0x96, + 0x97, + 0x98, + 0x99, + 0x9A, + 0x9B, + 0x9C, + 0x9D, + 0x9E, + 0x9F, + 0xA0, + 0xA1, + 0xA2, + 0xA3, + 0xA4, + 0xA5, + 0xA6, + 0xA7, + 0xA8, + 0xA9, + 0xAA, + 0xAB, + 0xAC, + 0xAD, + 0xAE, + 0xAF, + 0xB0, + 0xB1, + 0xB2, + 0xB3, + 0xB4, + 0xB5, + 0xB6, + 0xB7, + 0xB8, + 0xB9, + 0xBA, + 0xBB, + 0xBC, + 0xBD, + 0xBE, + 0xBF, + 0xC0, + 0xC1, + 0xC2, + 0xC3, + 0xC4, + 0xC5, + 0xC6, + 0xC7, + 0xC8, + 0xC9, + 0xCA, + 0xCB, + 0xCC, + 0xCD, + 0xCE, + 0xCF, + 0xD0, + 0xD1, + 0xD2, + 0xD3, + 0xD4, + 0xD5, + 0xD6, + 0xD7, + 0xD8, + 0xD9, + 0xDA, + 0xDB, + 0xDC, + 0xDD, + 0xDE, + 0xDF, + 0xE0, + 0xE1, + 0xE2, + 0xE3, + 0xE4, + 0xE5, + 0xE6, + 0xE7, + 0xE8, + 0xE9, + 0xEA, + 0xEB, + 0xEC, + 0xED, + 0xEE, + 0xEF, + 0xF0, + 0xF1, + 0xF2, + 0xF3, + 0xF4, + 0xF5, + 0xF6, + 0xF7, + 0xF8, + 0xF9, + 0xFA, + 0xFB, + 0xFC, + 0xFD, + 0xFE, + 0xFF, + 0x100, + 0x101, + 0x102, + 0x103, + 0x104, + 0x105, + 0x106, + 0x107, + 0x108, + 0x109, + 0x10A, + 0x10B, + 0x10C, + 0x10D, + 0x10E, + 0x10F, + 0x110, + 0x111, + 0x112, + 0x113, + 0x114, + 0x115, + 0x116, + 0x117, + 0x118, + 0x119, + 0x11A, + 0x11B, + 0x11C, + 0x11D, + 0x11E, + 0x11F, + 0x120, + 0x121, + 0x122, + 0x123, + 0x124, + 0x125, + 0x126, + 0x127, + 0x128, + 0x129, + 0x12A, + 0x12B, + 0x12C, + 0x12D, + 0x12E, + 0x12F, + 0x130, + 0x131, + 0x132, + 0x134, + 0x135, + 0x136, + 0x137, + 0x139, + 0x13A, + 0x13B, + 0x13C, + 0x13D, + 0x13E, + 0x13F, + 0x141, + 0x142, + 0x143, + 0x144, + 0x145, + 0x146, + 0x147, + 0x148, + 0x149, + 0x14A, + 0x14B, + 0x14C, + 0x14D, + 0x14E, + 0x14F, + 0x150, + 0x151, + 0x152, + 0x153, + 0x154, + 0x155, + 0x156, + 0x157, + 0x158, + 0x159, + 0x15A, + 0x15B, + 0x15C, + 0x15D, + 0x15E, + 0x15F, + 0x160, + 0x161, + 0x162, + 0x163, + 0x164, + 0x165, + 0x166, + 0x167, + 0x168, + 0x169, + 0x16A, + 0x16B, + 0x16C, + 0x16D, + 0x16E, + 0x16F, + 0x170, + 0x171, + 0x172, + 0x173, + 0x174, + 0x175, + 0x176, + 0x177, + 0x178, + 0x179, + 0x17A, + 0x17B, + 0x17C, + 0x17D, + 0x17E, + 0x17F, + 0x180, + 0x181, + 0x182, + 0x183, + 0x184, + 0x185, + 0x186, + 0x187, + 0x188, + 0x189, + 0x18A, + 0x18B, + 0x18C, + 0x18E, + 0x18F, + 0x190, + 0x191, + 0x192, + 0x193, + 0x194, + 0x195, + 0x196, + 0x197, + 0x198, + 0x199, + 0x19C, + 0x19D, + 0x19E, + 0x19F, + 0x1A0, + 0x1A1, + 0x1A2, + 0x1A3, + 0x1A4, + 0x1A5, + 0x1A6, + 0x1A7, + 0x1A8, + 0x1A9, + 0x1AA, + 0x1AC, + 0x1AD, + 0x1AE, + 0x1AF, + 0x1B0, + 0x1B1, + 0x1B2, + 0x1B3, + 0x1B4, + 0x1B5, + 0x1B6, + 0x1B7, + 0x1B8, + 0x1B9, + 0x1BC, + 0x1BD, + 0x1C4, + 0x1C7, + 0x1CA, + 0x1CD, + 0x1CE, + 0x1CF, + 0x1D0, + 0x1D1, + 0x1D2, + 0x1D3, + 0x1D4, + 0x1D5, + 0x1D6, + 0x1D7, + 0x1D8, + 0x1D9, + 0x1DA, + 0x1DB, + 0x1DC, + 0x1DE, + 0x1DF, + 0x1E0, + 0x1E1, + 0x1E2, + 0x1E3, + 0x1E4, + 0x1E5, + 0x1E6, + 0x1E7, + 0x1E8, + 0x1E9, + 0x1EA, + 0x1EB, + 0x1EC, + 0x1ED, + 0x1EE, + 0x1EF, + 0x1F1, + 0x1F4, + 0x1F5, + 0x1F6, + 0x1F7, + 0x1F8, + 0x1F9, + 0x1FA, + 0x1FB, + 0x1FC, + 0x1FD, + 0x1FE, + 0x1FF, + 0x200, + 0x201, + 0x202, + 0x203, + 0x204, + 0x205, + 0x206, + 0x207, + 0x208, + 0x209, + 0x20A, + 0x20B, + 0x20C, + 0x20D, + 0x20E, + 0x20F, + 0x210, + 0x211, + 0x212, + 0x213, + 0x214, + 0x215, + 0x216, + 0x217, + 0x218, + 0x219, + 0x21A, + 0x21B, + 0x21C, + 0x21D, + 0x21E, + 0x21F, + 0x220, + 0x221, + 0x222, + 0x223, + 0x224, + 0x225, + 0x226, + 0x227, + 0x228, + 0x229, + 0x22A, + 0x22B, + 0x22C, + 0x22D, + 0x22E, + 0x22F, + 0x230, + 0x231, + 0x232, + 0x233, + 0x23A, + 0x23B, + 0x23C, + 0x23D, + 0x23E, + 0x23F, + 0x241, + 0x242, + 0x243, + 0x244, + 0x245, + 0x246, + 0x247, + 0x248, + 0x249, + 0x24A, + 0x24B, + 0x24C, + 0x24D, + 0x24E, + 0x24F, + 0x2B0, + 0x2B1, + 0x2B2, + 0x2B3, + 0x2B4, + 0x2B5, + 0x2B6, + 0x2B7, + 0x2B8, + 0x2B9, + 0x2D8, + 0x2D9, + 0x2DA, + 0x2DB, + 0x2DC, + 0x2DD, + 0x2DE, + 0x2E0, + 0x2E1, + 0x2E2, + 0x2E3, + 0x2E4, + 0x2E5, + 0x340, + 0x341, + 0x342, + 0x343, + 0x344, + 0x345, + 0x346, + 0x34F, + 0x350, + 0x370, + 0x371, + 0x372, + 0x373, + 0x374, + 0x375, + 0x376, + 0x377, + 0x378, + 0x37A, + 0x37B, + 0x37E, + 0x37F, + 0x380, + 0x384, + 0x385, + 0x386, + 0x387, + 0x388, + 0x389, + 0x38A, + 0x38B, + 0x38C, + 0x38D, + 0x38E, + 0x38F, + 0x390, + 0x391, + 0x392, + 0x393, + 0x394, + 0x395, + 0x396, + 0x397, + 0x398, + 0x399, + 0x39A, + 0x39B, + 0x39C, + 0x39D, + 0x39E, + 0x39F, + 0x3A0, + 0x3A1, + 0x3A2, + 0x3A3, + 0x3A4, + 0x3A5, + 0x3A6, + 0x3A7, + 0x3A8, + 0x3A9, + 0x3AA, + 0x3AB, + 0x3AC, + 0x3C2, + 0x3C3, + 0x3CF, + 0x3D0, + 0x3D1, + 0x3D2, + 0x3D3, + 0x3D4, + 0x3D5, + 0x3D6, + 0x3D7, + 0x3D8, + 0x3D9, + 0x3DA, + 0x3DB, + 0x3DC, + 0x3DD, + 0x3DE, + 0x3DF, + 0x3E0, + 0x3E1, + 0x3E2, + 0x3E3, + 0x3E4, + 0x3E5, + 0x3E6, + 0x3E7, + 0x3E8, + 0x3E9, + 0x3EA, + 0x3EB, + 0x3EC, + 0x3ED, + 0x3EE, + 0x3EF, + 0x3F0, + 0x3F1, + 0x3F2, + 0x3F3, + 0x3F4, + 0x3F5, + 0x3F6, + 0x3F7, + 0x3F8, + 0x3F9, + 0x3FA, + 0x3FB, + 0x3FD, + 0x3FE, + 0x3FF, + 0x400, + 0x401, + 0x402, + 0x403, + 0x404, + 0x405, + 0x406, + 0x407, + 0x408, + 0x409, + 0x40A, + 0x40B, + 0x40C, + 0x40D, + 0x40E, + 0x40F, + 0x410, + 0x411, + 0x412, + 0x413, + 0x414, + 0x415, + 0x416, + 0x417, + 0x418, + 0x419, + 0x41A, + 0x41B, + 0x41C, + 0x41D, + 0x41E, + 0x41F, + 0x420, + 0x421, + 0x422, + 0x423, + 0x424, + 0x425, + 0x426, + 0x427, + 0x428, + 0x429, + 0x42A, + 0x42B, + 0x42C, + 0x42D, + 0x42E, + 0x42F, + 0x430, + 0x460, + 0x461, + 0x462, + 0x463, + 0x464, + 0x465, + 0x466, + 0x467, + 0x468, + 0x469, + 0x46A, + 0x46B, + 0x46C, + 0x46D, + 0x46E, + 0x46F, + 0x470, + 0x471, + 0x472, + 0x473, + 0x474, + 0x475, + 0x476, + 0x477, + 0x478, + 0x479, + 0x47A, + 0x47B, + 0x47C, + 0x47D, + 0x47E, + 0x47F, + 0x480, + 0x481, + 0x48A, + 0x48B, + 0x48C, + 0x48D, + 0x48E, + 0x48F, + 0x490, + 0x491, + 0x492, + 0x493, + 0x494, + 0x495, + 0x496, + 0x497, + 0x498, + 0x499, + 0x49A, + 0x49B, + 0x49C, + 0x49D, + 0x49E, + 0x49F, + 0x4A0, + 0x4A1, + 0x4A2, + 0x4A3, + 0x4A4, + 0x4A5, + 0x4A6, + 0x4A7, + 0x4A8, + 0x4A9, + 0x4AA, + 0x4AB, + 0x4AC, + 0x4AD, + 0x4AE, + 0x4AF, + 0x4B0, + 0x4B1, + 0x4B2, + 0x4B3, + 0x4B4, + 0x4B5, + 0x4B6, + 0x4B7, + 0x4B8, + 0x4B9, + 0x4BA, + 0x4BB, + 0x4BC, + 0x4BD, + 0x4BE, + 0x4BF, + 0x4C0, + 0x4C1, + 0x4C2, + 0x4C3, + 0x4C4, + 0x4C5, + 0x4C6, + 0x4C7, + 0x4C8, + 0x4C9, + 0x4CA, + 0x4CB, + 0x4CC, + 0x4CD, + 0x4CE, + 0x4D0, + 0x4D1, + 0x4D2, + 0x4D3, + 0x4D4, + 0x4D5, + 0x4D6, + 0x4D7, + 0x4D8, + 0x4D9, + 0x4DA, + 0x4DB, + 0x4DC, + 0x4DD, + 0x4DE, + 0x4DF, + 0x4E0, + 0x4E1, + 0x4E2, + 0x4E3, + 0x4E4, + 0x4E5, + 0x4E6, + 0x4E7, + 0x4E8, + 0x4E9, + 0x4EA, + 0x4EB, + 0x4EC, + 0x4ED, + 0x4EE, + 0x4EF, + 0x4F0, + 0x4F1, + 0x4F2, + 0x4F3, + 0x4F4, + 0x4F5, + 0x4F6, + 0x4F7, + 0x4F8, + 0x4F9, + 0x4FA, + 0x4FB, + 0x4FC, + 0x4FD, + 0x4FE, + 0x4FF, + 0x500, + 0x501, + 0x502, + 0x503, + 0x504, + 0x505, + 0x506, + 0x507, + 0x508, + 0x509, + 0x50A, + 0x50B, + 0x50C, + 0x50D, + 0x50E, + 0x50F, + 0x510, + 0x511, + 0x512, + 0x513, + 0x514, + 0x515, + 0x516, + 0x517, + 0x518, + 0x519, + 0x51A, + 0x51B, + 0x51C, + 0x51D, + 0x51E, + 0x51F, + 0x520, + 0x521, + 0x522, + 0x523, + 0x524, + 0x525, + 0x526, + 0x527, + 0x528, + 0x529, + 0x52A, + 0x52B, + 0x52C, + 0x52D, + 0x52E, + 0x52F, + 0x530, + 0x531, + 0x532, + 0x533, + 0x534, + 0x535, + 0x536, + 0x537, + 0x538, + 0x539, + 0x53A, + 0x53B, + 0x53C, + 0x53D, + 0x53E, + 0x53F, + 0x540, + 0x541, + 0x542, + 0x543, + 0x544, + 0x545, + 0x546, + 0x547, + 0x548, + 0x549, + 0x54A, + 0x54B, + 0x54C, + 0x54D, + 0x54E, + 0x54F, + 0x550, + 0x551, + 0x552, + 0x553, + 0x554, + 0x555, + 0x556, + 0x557, + 0x558, + 0x559, + 0x587, + 0x588, + 0x58B, + 0x58C, + 0x58D, + 0x590, + 0x591, + 0x5CA, + 0x5D0, + 0x5EB, + 0x5EF, + 0x5F5, + 0x606, + 0x61C, + 0x61D, + 0x675, + 0x676, + 0x677, + 0x678, + 0x679, + 0x6DD, + 0x6DE, + 0x70E, + 0x710, + 0x74B, + 0x74D, + 0x7B2, + 0x7C0, + 0x7FB, + 0x7FD, + 0x82E, + 0x830, + 0x83F, + 0x840, + 0x85C, + 0x85E, + 0x85F, + 0x860, + 0x86B, + 0x870, + 0x890, + 0x897, + 0x8E2, + 0x8E3, + 0x958, + 0x959, + 0x95A, + 0x95B, + 0x95C, + 0x95D, + 0x95E, + 0x95F, + 0x960, + 0x984, + 0x985, + 0x98D, + 0x98F, + 0x991, + 0x993, + 0x9A9, + 0x9AA, + 0x9B1, + 0x9B2, + 0x9B3, + 0x9B6, + 0x9BA, + 0x9BC, + 0x9C5, + 0x9C7, + 0x9C9, + 0x9CB, + 0x9CF, + 0x9D7, + 0x9D8, + 0x9DC, + 0x9DD, + 0x9DE, + 0x9DF, + 0x9E0, + 0x9E4, + 0x9E6, + 0x9FF, + 0xA01, + 0xA04, + 0xA05, + 0xA0B, + 0xA0F, + 0xA11, + 0xA13, + 0xA29, + 0xA2A, + 0xA31, + 0xA32, + 0xA33, + 0xA34, + 0xA35, + 0xA36, + 0xA37, + 0xA38, + 0xA3A, + 0xA3C, + 0xA3D, + 0xA3E, + 0xA43, + 0xA47, + 0xA49, + 0xA4B, + 0xA4E, + 0xA51, + 0xA52, + 0xA59, + 0xA5A, + 0xA5B, + 0xA5C, + 0xA5D, + 0xA5E, + 0xA5F, + 0xA66, + 0xA77, + 0xA81, + 0xA84, + 0xA85, + 0xA8E, + 0xA8F, + 0xA92, + 0xA93, + 0xAA9, + 0xAAA, + 0xAB1, + 0xAB2, + 0xAB4, + 0xAB5, + 0xABA, + 0xABC, + 0xAC6, + 0xAC7, + 0xACA, + 0xACB, + 0xACE, + 0xAD0, + 0xAD1, + 0xAE0, + 0xAE4, + 0xAE6, + 0xAF2, + 0xAF9, + 0xB00, + 0xB01, + 0xB04, + 0xB05, + 0xB0D, + 0xB0F, + 0xB11, + 0xB13, + 0xB29, + 0xB2A, + 0xB31, + 0xB32, + 0xB34, + 0xB35, + 0xB3A, + 0xB3C, + 0xB45, + 0xB47, + 0xB49, + 0xB4B, + 0xB4E, + 0xB53, + 0xB58, + 0xB5C, + 0xB5D, + 0xB5E, + 0xB5F, + 0xB64, + 0xB66, + 0xB78, + 0xB82, + 0xB84, + 0xB85, + 0xB8B, + 0xB8E, + 0xB91, + 0xB92, + 0xB96, + 0xB99, + 0xB9B, + 0xB9C, + 0xB9D, + 0xB9E, + 0xBA0, + 0xBA3, + 0xBA5, + 0xBA8, + 0xBAB, + 0xBAE, + 0xBBA, + 0xBBE, + 0xBC3, + 0xBC6, + 0xBC9, + 0xBCA, + 0xBCE, + 0xBD0, + 0xBD1, + 0xBD7, + 0xBD8, + 0xBE6, + 0xBFB, + 0xC00, + 0xC0D, + 0xC0E, + 0xC11, + 0xC12, + 0xC29, + 0xC2A, + 0xC3A, + 0xC3C, + 0xC45, + 0xC46, + 0xC49, + 0xC4A, + 0xC4E, + 0xC55, + 0xC57, + 0xC58, + 0xC5B, + 0xC5C, + 0xC5E, + 0xC60, + 0xC64, + 0xC66, + 0xC70, + 0xC77, + 0xC8D, + 0xC8E, + 0xC91, + 0xC92, + 0xCA9, + 0xCAA, + 0xCB4, + 0xCB5, + 0xCBA, + 0xCBC, + 0xCC5, + 0xCC6, + 0xCC9, + 0xCCA, + 0xCCE, + 0xCD5, + 0xCD7, + 0xCDC, + 0xCDF, + 0xCE0, + 0xCE4, + 0xCE6, + 0xCF0, + 0xCF1, + 0xCF4, + 0xD00, + 0xD0D, + 0xD0E, + 0xD11, + 0xD12, + 0xD45, + 0xD46, + 0xD49, + 0xD4A, + 0xD50, + 0xD54, + 0xD64, + 0xD66, + 0xD80, + 0xD81, + 0xD84, + 0xD85, + 0xD97, + 0xD9A, + 0xDB2, + 0xDB3, + 0xDBC, + 0xDBD, + 0xDBE, + 0xDC0, + 0xDC7, + 0xDCA, + 0xDCB, + 0xDCF, + 0xDD5, + 0xDD6, + 0xDD7, + 0xDD8, + 0xDE0, + 0xDE6, + 0xDF0, + 0xDF2, + 0xDF5, + 0xE01, + 0xE33, + 0xE34, + 0xE3B, + 0xE3F, + 0xE5C, + 0xE81, + 0xE83, + 0xE84, + 0xE85, + 0xE86, + 0xE8B, + 0xE8C, + 0xEA4, + 0xEA5, + 0xEA6, + 0xEA7, + 0xEB3, + 0xEB4, + 0xEBE, + 0xEC0, + 0xEC5, + 0xEC6, + 0xEC7, + 0xEC8, + 0xECF, + 0xED0, + 0xEDA, + 0xEDC, + 0xEDD, + 0xEDE, + 0xEE0, + 0xF00, + 0xF0C, + 0xF0D, + 0xF43, + 0xF44, + 0xF48, + 0xF49, + 0xF4D, + 0xF4E, + 0xF52, + 0xF53, + 0xF57, + 0xF58, + 0xF5C, + 0xF5D, + 0xF69, + 0xF6A, + 0xF6D, + 0xF71, + 0xF73, + 0xF74, + 0xF75, + 0xF76, + 0xF77, + 0xF78, + 0xF79, + 0xF7A, + 0xF81, + 0xF82, + 0xF93, + 0xF94, + 0xF98, + 0xF99, + 0xF9D, + 0xF9E, + 0xFA2, + 0xFA3, + 0xFA7, + 0xFA8, + 0xFAC, + 0xFAD, + 0xFB9, + 0xFBA, + 0xFBD, + 0xFBE, + 0xFCD, + 0xFCE, + 0xFDB, + 0x1000, + 0x10A0, + 0x10A1, + 0x10A2, + 0x10A3, + 0x10A4, + 0x10A5, + 0x10A6, + 0x10A7, + 0x10A8, + 0x10A9, + 0x10AA, + 0x10AB, + 0x10AC, + 0x10AD, + 0x10AE, + 0x10AF, + 0x10B0, + 0x10B1, + 0x10B2, + 0x10B3, + 0x10B4, + 0x10B5, + 0x10B6, + 0x10B7, + 0x10B8, + 0x10B9, + 0x10BA, + 0x10BB, + 0x10BC, + 0x10BD, + 0x10BE, + 0x10BF, + 0x10C0, + 0x10C1, + 0x10C2, + 0x10C3, + 0x10C4, + 0x10C5, + 0x10C6, + 0x10C7, + 0x10C8, + 0x10CD, + 0x10CE, + 0x10D0, + 0x10FC, + 0x10FD, + 0x115F, + 0x1161, + 0x1249, + 0x124A, + 0x124E, + 0x1250, + 0x1257, + 0x1258, + 0x1259, + 0x125A, + 0x125E, + 0x1260, + 0x1289, + 0x128A, + 0x128E, + 0x1290, + 0x12B1, + 0x12B2, + 0x12B6, + 0x12B8, + 0x12BF, + 0x12C0, + 0x12C1, + 0x12C2, + 0x12C6, + 0x12C8, + 0x12D7, + 0x12D8, + 0x1311, + 0x1312, + 0x1316, + 0x1318, + 0x135B, + 0x135D, + 0x137D, + 0x1380, + 0x139A, + 0x13A0, + 0x13F6, + 0x13F8, + 0x13F9, + 0x13FA, + 0x13FB, + 0x13FC, + 0x13FD, + 0x13FE, + 0x1400, + 0x1680, + 0x1681, + 0x169D, + 0x16A0, + 0x16F9, + 0x1700, + 0x1716, + 0x171F, + 0x1737, + 0x1740, + 0x1754, + 0x1760, + 0x176D, + 0x176E, + 0x1771, + 0x1772, + 0x1774, + 0x1780, + 0x17B4, + 0x17B6, + 0x17DE, + 0x17E0, + 0x17EA, + 0x17F0, + 0x17FA, + 0x1800, + 0x180B, + 0x1810, + 0x181A, + 0x1820, + 0x1879, + 0x1880, + 0x18AB, + 0x18B0, + 0x18F6, + 0x1900, + 0x191F, + 0x1920, + 0x192C, + 0x1930, + 0x193C, + 0x1940, + 0x1941, + 0x1944, + 0x196E, + 0x1970, + 0x1975, + 0x1980, + 0x19AC, + 0x19B0, + 0x19CA, + 0x19D0, + 0x19DB, + 0x19DE, + 0x1A1C, + 0x1A1E, + 0x1A5F, + 0x1A60, + 0x1A7D, + 0x1A7F, + 0x1A8A, + 0x1A90, + 0x1A9A, + 0x1AA0, + 0x1AAE, + 0x1AB0, + 0x1AF1, + 0x1B00, + 0x1B4D, + 0x1B4E, + 0x1BF4, + 0x1BFC, + 0x1C38, + 0x1C3B, + 0x1C4A, + 0x1C4D, + 0x1C80, + 0x1C81, + 0x1C82, + 0x1C83, + 0x1C84, + 0x1C86, + 0x1C87, + 0x1C88, + 0x1C89, + 0x1C8A, + 0x1C8B, + 0x1C90, + 0x1C91, + 0x1C92, + 0x1C93, + 0x1C94, + 0x1C95, + 0x1C96, + 0x1C97, + 0x1C98, + 0x1C99, + 0x1C9A, + 0x1C9B, + 0x1C9C, + 0x1C9D, + 0x1C9E, + 0x1C9F, + 0x1CA0, + 0x1CA1, + 0x1CA2, + 0x1CA3, + 0x1CA4, + 0x1CA5, + 0x1CA6, + 0x1CA7, + 0x1CA8, + 0x1CA9, + 0x1CAA, + 0x1CAB, + 0x1CAC, + 0x1CAD, + 0x1CAE, + 0x1CAF, + 0x1CB0, + 0x1CB1, + 0x1CB2, + 0x1CB3, + 0x1CB4, + 0x1CB5, + 0x1CB6, + 0x1CB7, + 0x1CB8, + 0x1CB9, + 0x1CBA, + 0x1CBB, + 0x1CBD, + 0x1CBE, + 0x1CBF, + 0x1CC0, + 0x1CC8, + 0x1CD0, + 0x1CFB, + 0x1D00, + 0x1D2C, + 0x1D2D, + 0x1D2E, + 0x1D2F, + 0x1D30, + 0x1D31, + 0x1D32, + 0x1D33, + 0x1D34, + 0x1D35, + 0x1D36, + 0x1D37, + 0x1D38, + 0x1D39, + 0x1D3A, + 0x1D3B, + 0x1D3C, + 0x1D3D, + 0x1D3E, + 0x1D3F, + 0x1D40, + 0x1D41, + 0x1D42, + 0x1D43, + 0x1D44, + 0x1D45, + 0x1D46, + 0x1D47, + 0x1D48, + 0x1D49, + 0x1D4A, + 0x1D4B, + 0x1D4C, + 0x1D4D, + 0x1D4E, + 0x1D4F, + 0x1D50, + 0x1D51, + 0x1D52, + 0x1D53, + 0x1D54, + 0x1D55, + 0x1D56, + 0x1D57, + 0x1D58, + 0x1D59, + 0x1D5A, + 0x1D5B, + 0x1D5C, + 0x1D5D, + 0x1D5E, + 0x1D5F, + 0x1D60, + 0x1D61, + 0x1D62, + 0x1D63, + 0x1D64, + 0x1D65, + 0x1D66, + 0x1D67, + 0x1D68, + 0x1D69, + 0x1D6A, + 0x1D6B, + 0x1D78, + 0x1D79, + 0x1D9B, + 0x1D9C, + 0x1D9D, + 0x1D9E, + 0x1D9F, + 0x1DA0, + 0x1DA1, + 0x1DA2, + 0x1DA3, + 0x1DA4, + 0x1DA5, + 0x1DA6, + 0x1DA7, + 0x1DA8, + 0x1DA9, + 0x1DAA, + 0x1DAB, + 0x1DAC, + 0x1DAD, + 0x1DAE, + 0x1DAF, + 0x1DB0, + 0x1DB1, + 0x1DB2, + 0x1DB3, + 0x1DB4, + 0x1DB5, + 0x1DB6, + 0x1DB7, + 0x1DB8, + 0x1DB9, + 0x1DBA, + 0x1DBB, + 0x1DBC, + 0x1DBD, + 0x1DBE, + 0x1DBF, + 0x1DC0, + 0x1E00, + 0x1E01, + 0x1E02, + 0x1E03, + 0x1E04, + 0x1E05, + 0x1E06, + 0x1E07, + 0x1E08, + 0x1E09, + 0x1E0A, + 0x1E0B, + 0x1E0C, + 0x1E0D, + 0x1E0E, + 0x1E0F, + 0x1E10, + 0x1E11, + 0x1E12, + 0x1E13, + 0x1E14, + 0x1E15, + 0x1E16, + 0x1E17, + 0x1E18, + 0x1E19, + 0x1E1A, + 0x1E1B, + 0x1E1C, + 0x1E1D, + 0x1E1E, + 0x1E1F, + 0x1E20, + 0x1E21, + 0x1E22, + 0x1E23, + 0x1E24, + 0x1E25, + 0x1E26, + 0x1E27, + 0x1E28, + 0x1E29, + 0x1E2A, + 0x1E2B, + 0x1E2C, + 0x1E2D, + 0x1E2E, + 0x1E2F, + 0x1E30, + 0x1E31, + 0x1E32, + 0x1E33, + 0x1E34, + 0x1E35, + 0x1E36, + 0x1E37, + 0x1E38, + 0x1E39, + 0x1E3A, + 0x1E3B, + 0x1E3C, + 0x1E3D, + 0x1E3E, + 0x1E3F, + 0x1E40, + 0x1E41, + 0x1E42, + 0x1E43, + 0x1E44, + 0x1E45, + 0x1E46, + 0x1E47, + 0x1E48, + 0x1E49, + 0x1E4A, + 0x1E4B, + 0x1E4C, + 0x1E4D, + 0x1E4E, + 0x1E4F, + 0x1E50, + 0x1E51, + 0x1E52, + 0x1E53, + 0x1E54, + 0x1E55, + 0x1E56, + 0x1E57, + 0x1E58, + 0x1E59, + 0x1E5A, + 0x1E5B, + 0x1E5C, + 0x1E5D, + 0x1E5E, + 0x1E5F, + 0x1E60, + 0x1E61, + 0x1E62, + 0x1E63, + 0x1E64, + 0x1E65, + 0x1E66, + 0x1E67, + 0x1E68, + 0x1E69, + 0x1E6A, + 0x1E6B, + 0x1E6C, + 0x1E6D, + 0x1E6E, + 0x1E6F, + 0x1E70, + 0x1E71, + 0x1E72, + 0x1E73, + 0x1E74, + 0x1E75, + 0x1E76, + 0x1E77, + 0x1E78, + 0x1E79, + 0x1E7A, + 0x1E7B, + 0x1E7C, + 0x1E7D, + 0x1E7E, + 0x1E7F, + 0x1E80, + 0x1E81, + 0x1E82, + 0x1E83, + 0x1E84, + 0x1E85, + 0x1E86, + 0x1E87, + 0x1E88, + 0x1E89, + 0x1E8A, + 0x1E8B, + 0x1E8C, + 0x1E8D, + 0x1E8E, + 0x1E8F, + 0x1E90, + 0x1E91, + 0x1E92, + 0x1E93, + 0x1E94, + 0x1E95, + 0x1E9A, + 0x1E9B, + 0x1E9C, + 0x1E9E, + 0x1E9F, + 0x1EA0, + 0x1EA1, + 0x1EA2, + 0x1EA3, + 0x1EA4, + 0x1EA5, + 0x1EA6, + 0x1EA7, + 0x1EA8, + 0x1EA9, + 0x1EAA, + 0x1EAB, + 0x1EAC, + 0x1EAD, + 0x1EAE, + 0x1EAF, + 0x1EB0, + 0x1EB1, + 0x1EB2, + 0x1EB3, + 0x1EB4, + 0x1EB5, + 0x1EB6, + 0x1EB7, + 0x1EB8, + 0x1EB9, + 0x1EBA, + 0x1EBB, + 0x1EBC, + 0x1EBD, + 0x1EBE, + 0x1EBF, + 0x1EC0, + 0x1EC1, + 0x1EC2, + 0x1EC3, + 0x1EC4, + 0x1EC5, + 0x1EC6, + 0x1EC7, + 0x1EC8, + 0x1EC9, + 0x1ECA, + 0x1ECB, + 0x1ECC, + 0x1ECD, + 0x1ECE, + 0x1ECF, + 0x1ED0, + 0x1ED1, + 0x1ED2, + 0x1ED3, + 0x1ED4, + 0x1ED5, + 0x1ED6, + 0x1ED7, + 0x1ED8, + 0x1ED9, + 0x1EDA, + 0x1EDB, + 0x1EDC, + 0x1EDD, + 0x1EDE, + 0x1EDF, + 0x1EE0, + 0x1EE1, + 0x1EE2, + 0x1EE3, + 0x1EE4, + 0x1EE5, + 0x1EE6, + 0x1EE7, + 0x1EE8, + 0x1EE9, + 0x1EEA, + 0x1EEB, + 0x1EEC, + 0x1EED, + 0x1EEE, + 0x1EEF, + 0x1EF0, + 0x1EF1, + 0x1EF2, + 0x1EF3, + 0x1EF4, + 0x1EF5, + 0x1EF6, + 0x1EF7, + 0x1EF8, + 0x1EF9, + 0x1EFA, + 0x1EFB, + 0x1EFC, + 0x1EFD, + 0x1EFE, + 0x1EFF, + 0x1F08, + 0x1F09, + 0x1F0A, + 0x1F0B, + 0x1F0C, + 0x1F0D, + 0x1F0E, + 0x1F0F, + 0x1F10, + 0x1F16, + 0x1F18, + 0x1F19, + 0x1F1A, + 0x1F1B, + 0x1F1C, + 0x1F1D, + 0x1F1E, + 0x1F20, + 0x1F28, + 0x1F29, + 0x1F2A, + 0x1F2B, + 0x1F2C, + 0x1F2D, + 0x1F2E, + 0x1F2F, + 0x1F30, + 0x1F38, + 0x1F39, + 0x1F3A, + 0x1F3B, + 0x1F3C, + 0x1F3D, + 0x1F3E, + 0x1F3F, + 0x1F40, + 0x1F46, + 0x1F48, + 0x1F49, + 0x1F4A, + 0x1F4B, + 0x1F4C, + 0x1F4D, + 0x1F4E, + 0x1F50, + 0x1F58, + 0x1F59, + 0x1F5A, + 0x1F5B, + 0x1F5C, + 0x1F5D, + 0x1F5E, + 0x1F5F, + 0x1F60, + 0x1F68, + 0x1F69, + 0x1F6A, + 0x1F6B, + 0x1F6C, + 0x1F6D, + 0x1F6E, + 0x1F6F, + 0x1F70, + 0x1F71, + 0x1F72, + 0x1F73, + 0x1F74, + 0x1F75, + 0x1F76, + 0x1F77, + 0x1F78, + 0x1F79, + 0x1F7A, + 0x1F7B, + 0x1F7C, + 0x1F7D, + 0x1F7E, + 0x1F80, + 0x1F81, + 0x1F82, + 0x1F83, + 0x1F84, + 0x1F85, + 0x1F86, + 0x1F87, + 0x1F88, + 0x1F89, + 0x1F8A, + 0x1F8B, + 0x1F8C, + 0x1F8D, + 0x1F8E, + 0x1F8F, + 0x1F90, + 0x1F91, + 0x1F92, + 0x1F93, + 0x1F94, + 0x1F95, + 0x1F96, + 0x1F97, + 0x1F98, + 0x1F99, + 0x1F9A, + 0x1F9B, + 0x1F9C, + 0x1F9D, + 0x1F9E, + 0x1F9F, + 0x1FA0, + 0x1FA1, + 0x1FA2, + 0x1FA3, + 0x1FA4, + 0x1FA5, + 0x1FA6, + 0x1FA7, + 0x1FA8, + 0x1FA9, + 0x1FAA, + 0x1FAB, + 0x1FAC, + 0x1FAD, + 0x1FAE, + 0x1FAF, + 0x1FB0, + 0x1FB2, + 0x1FB3, + 0x1FB4, + 0x1FB5, + 0x1FB6, + 0x1FB7, + 0x1FB8, + 0x1FB9, + 0x1FBA, + 0x1FBB, + 0x1FBC, + 0x1FBD, + 0x1FBE, + 0x1FBF, + 0x1FC0, + 0x1FC1, + 0x1FC2, + 0x1FC3, + 0x1FC4, + 0x1FC5, + 0x1FC6, + 0x1FC7, + 0x1FC8, + 0x1FC9, + 0x1FCA, + 0x1FCB, + 0x1FCC, + 0x1FCD, + 0x1FCE, + 0x1FCF, + 0x1FD0, + 0x1FD3, + 0x1FD4, + 0x1FD6, + 0x1FD8, + 0x1FD9, + 0x1FDA, + 0x1FDB, + 0x1FDC, + 0x1FDD, + 0x1FDE, + 0x1FDF, + 0x1FE0, + 0x1FE3, + 0x1FE4, + 0x1FE8, + 0x1FE9, + 0x1FEA, + 0x1FEB, + 0x1FEC, + 0x1FED, + 0x1FEE, + 0x1FEF, + 0x1FF0, + 0x1FF2, + 0x1FF3, + 0x1FF4, + 0x1FF5, + 0x1FF6, + 0x1FF7, + 0x1FF8, + 0x1FF9, + 0x1FFA, + 0x1FFB, + 0x1FFC, + 0x1FFD, + 0x1FFE, + 0x1FFF, + 0x2000, + 0x200B, + 0x200C, + 0x200E, + 0x2010, + 0x2011, + 0x2012, + 0x2017, + 0x2018, + 0x2024, + 0x2027, + 0x2028, + 0x202F, + 0x2030, + 0x2033, + 0x2034, + 0x2035, + 0x2036, + 0x2037, + 0x2038, + 0x203C, + 0x203D, + 0x203E, + 0x203F, + 0x2047, + 0x2048, + 0x2049, + 0x204A, + 0x2057, + 0x2058, + 0x205F, + 0x2060, + 0x2065, + 0x206A, + 0x2070, + 0x2071, + 0x2072, + 0x2074, + 0x2075, + 0x2076, + 0x2077, + 0x2078, + 0x2079, + 0x207A, + 0x207B, + 0x207C, + 0x207D, + 0x207E, + 0x207F, + 0x2080, + 0x2081, + 0x2082, + 0x2083, + 0x2084, + 0x2085, + 0x2086, + 0x2087, + 0x2088, + 0x2089, + 0x208A, + 0x208B, + 0x208C, + 0x208D, + 0x208E, + 0x208F, + 0x2090, + 0x2091, + 0x2092, + 0x2093, + 0x2094, + 0x2095, + 0x2096, + 0x2097, + 0x2098, + 0x2099, + 0x209A, + 0x209B, + 0x209C, + 0x209D, + 0x209E, + 0x209F, + 0x20A0, + 0x20A8, + 0x20A9, + 0x20C5, + 0x20D0, + 0x20F1, + 0x2100, + 0x2101, + 0x2102, + 0x2103, + 0x2104, + 0x2105, + 0x2106, + 0x2107, + 0x2108, + 0x2109, + 0x210A, + 0x210B, + 0x210F, + 0x2110, + 0x2112, + 0x2114, + 0x2115, + 0x2116, + 0x2117, + 0x2119, + 0x211A, + 0x211B, + 0x211E, + 0x2120, + 0x2121, + 0x2122, + 0x2123, + 0x2124, + 0x2125, + 0x2126, + 0x2127, + 0x2128, + 0x2129, + 0x212A, + 0x212B, + 0x212C, + 0x212D, + 0x212E, + 0x212F, + 0x2131, + 0x2132, + 0x2133, + 0x2134, + 0x2135, + 0x2136, + 0x2137, + 0x2138, + 0x2139, + 0x213A, + 0x213B, + 0x213C, + 0x213D, + 0x213F, + 0x2140, + 0x2141, + 0x2145, + 0x2147, + 0x2148, + 0x2149, + 0x214A, + 0x2150, + 0x2151, + 0x2152, + 0x2153, + 0x2154, + 0x2155, + 0x2156, + 0x2157, + 0x2158, + 0x2159, + 0x215A, + 0x215B, + 0x215C, + 0x215D, + 0x215E, + 0x215F, + 0x2160, + 0x2161, + 0x2162, + 0x2163, + 0x2164, + 0x2165, + 0x2166, + 0x2167, + 0x2168, + 0x2169, + 0x216A, + 0x216B, + 0x216C, + 0x216D, + 0x216E, + 0x216F, + 0x2170, + 0x2171, + 0x2172, + 0x2173, + 0x2174, + 0x2175, + 0x2176, + 0x2177, + 0x2178, + 0x2179, + 0x217A, + 0x217B, + 0x217C, + 0x217D, + 0x217E, + 0x217F, + 0x2180, + 0x2183, + 0x2184, + 0x2189, + 0x218A, + 0x218C, + 0x2190, + 0x222C, + 0x222D, + 0x222E, + 0x222F, + 0x2230, + 0x2231, + 0x2329, + 0x232A, + 0x232B, + 0x242A, + 0x2440, + 0x244B, + 0x2460, + 0x2461, + 0x2462, + 0x2463, + 0x2464, + 0x2465, + 0x2466, + 0x2467, + 0x2468, + 0x2469, + 0x246A, + 0x246B, + 0x246C, + 0x246D, + 0x246E, + 0x246F, + 0x2470, + 0x2471, + 0x2472, + 0x2473, + 0x2474, + 0x2475, + 0x2476, + 0x2477, + 0x2478, + 0x2479, + 0x247A, + 0x247B, + 0x247C, + 0x247D, + 0x247E, + 0x247F, + 0x2480, + 0x2481, + 0x2482, + 0x2483, + 0x2484, + 0x2485, + 0x2486, + 0x2487, + 0x2488, + 0x249C, + 0x249D, + 0x249E, + 0x249F, + 0x24A0, + 0x24A1, + 0x24A2, + 0x24A3, + 0x24A4, + 0x24A5, + 0x24A6, + 0x24A7, + 0x24A8, + 0x24A9, + 0x24AA, + 0x24AB, + 0x24AC, + 0x24AD, + 0x24AE, + 0x24AF, + 0x24B0, + 0x24B1, + 0x24B2, + 0x24B3, + 0x24B4, + 0x24B5, + 0x24B6, + 0x24B7, + 0x24B8, + 0x24B9, + 0x24BA, + 0x24BB, + 0x24BC, + 0x24BD, + 0x24BE, + 0x24BF, + 0x24C0, + 0x24C1, + 0x24C2, + 0x24C3, + 0x24C4, + 0x24C5, + 0x24C6, + 0x24C7, + 0x24C8, + 0x24C9, + 0x24CA, + 0x24CB, + 0x24CC, + 0x24CD, + 0x24CE, + 0x24CF, + 0x24D0, + 0x24D1, + 0x24D2, + 0x24D3, + 0x24D4, + 0x24D5, + 0x24D6, + 0x24D7, + 0x24D8, + 0x24D9, + 0x24DA, + 0x24DB, + 0x24DC, + 0x24DD, + 0x24DE, + 0x24DF, + 0x24E0, + 0x24E1, + 0x24E2, + 0x24E3, + 0x24E4, + 0x24E5, + 0x24E6, + 0x24E7, + 0x24E8, + 0x24E9, + 0x24EA, + 0x24EB, + 0x2A0C, + 0x2A0D, + 0x2A74, + 0x2A75, + 0x2A76, + 0x2A77, + 0x2ADC, + 0x2ADD, + 0x2B74, + 0x2B76, + 0x2C00, + 0x2C01, + 0x2C02, + 0x2C03, + 0x2C04, + 0x2C05, + 0x2C06, + 0x2C07, + 0x2C08, + 0x2C09, + 0x2C0A, + 0x2C0B, + 0x2C0C, + 0x2C0D, + 0x2C0E, + 0x2C0F, + 0x2C10, + 0x2C11, + 0x2C12, + 0x2C13, + 0x2C14, + 0x2C15, + 0x2C16, + 0x2C17, + 0x2C18, + 0x2C19, + 0x2C1A, + 0x2C1B, + 0x2C1C, + 0x2C1D, + 0x2C1E, + 0x2C1F, + 0x2C20, + 0x2C21, + 0x2C22, + 0x2C23, + 0x2C24, + 0x2C25, + 0x2C26, + 0x2C27, + 0x2C28, + 0x2C29, + 0x2C2A, + 0x2C2B, + 0x2C2C, + 0x2C2D, + 0x2C2E, + 0x2C2F, + 0x2C30, + 0x2C60, + 0x2C61, + 0x2C62, + 0x2C63, + 0x2C64, + 0x2C65, + 0x2C67, + 0x2C68, + 0x2C69, + 0x2C6A, + 0x2C6B, + 0x2C6C, + 0x2C6D, + 0x2C6E, + 0x2C6F, + 0x2C70, + 0x2C71, + 0x2C72, + 0x2C73, + 0x2C75, + 0x2C76, + 0x2C7C, + 0x2C7D, + 0x2C7E, + 0x2C7F, + 0x2C80, + 0x2C81, + 0x2C82, + 0x2C83, + 0x2C84, + 0x2C85, + 0x2C86, + 0x2C87, + 0x2C88, + 0x2C89, + 0x2C8A, + 0x2C8B, + 0x2C8C, + 0x2C8D, + 0x2C8E, + 0x2C8F, + 0x2C90, + 0x2C91, + 0x2C92, + 0x2C93, + 0x2C94, + 0x2C95, + 0x2C96, + 0x2C97, + 0x2C98, + 0x2C99, + 0x2C9A, + 0x2C9B, + 0x2C9C, + 0x2C9D, + 0x2C9E, + 0x2C9F, + 0x2CA0, + 0x2CA1, + 0x2CA2, + 0x2CA3, + 0x2CA4, + 0x2CA5, + 0x2CA6, + 0x2CA7, + 0x2CA8, + 0x2CA9, + 0x2CAA, + 0x2CAB, + 0x2CAC, + 0x2CAD, + 0x2CAE, + 0x2CAF, + 0x2CB0, + 0x2CB1, + 0x2CB2, + 0x2CB3, + 0x2CB4, + 0x2CB5, + 0x2CB6, + 0x2CB7, + 0x2CB8, + 0x2CB9, + 0x2CBA, + 0x2CBB, + 0x2CBC, + 0x2CBD, + 0x2CBE, + 0x2CBF, + 0x2CC0, + 0x2CC1, + 0x2CC2, + 0x2CC3, + 0x2CC4, + 0x2CC5, + 0x2CC6, + 0x2CC7, + 0x2CC8, + 0x2CC9, + 0x2CCA, + 0x2CCB, + 0x2CCC, + 0x2CCD, + 0x2CCE, + 0x2CCF, + 0x2CD0, + 0x2CD1, + 0x2CD2, + 0x2CD3, + 0x2CD4, + 0x2CD5, + 0x2CD6, + 0x2CD7, + 0x2CD8, + 0x2CD9, + 0x2CDA, + 0x2CDB, + 0x2CDC, + 0x2CDD, + 0x2CDE, + 0x2CDF, + 0x2CE0, + 0x2CE1, + 0x2CE2, + 0x2CE3, + 0x2CEB, + 0x2CEC, + 0x2CED, + 0x2CEE, + 0x2CF2, + 0x2CF3, + 0x2CF4, + 0x2CF9, + 0x2D26, + 0x2D27, + 0x2D28, + 0x2D2D, + 0x2D2E, + 0x2D30, + 0x2D68, + 0x2D6F, + 0x2D70, + 0x2D71, + 0x2D7F, + 0x2D97, + 0x2DA0, + 0x2DA7, + 0x2DA8, + 0x2DAF, + 0x2DB0, + 0x2DB7, + 0x2DB8, + 0x2DBF, + 0x2DC0, + 0x2DC7, + 0x2DC8, + 0x2DCF, + 0x2DD0, + 0x2DD7, + 0x2DD8, + 0x2DDF, + 0x2DE0, + 0x2E5E, + 0x2E60, + 0x2E64, + 0x2E80, + 0x2E9A, + 0x2E9B, + 0x2E9F, + 0x2EA0, + 0x2EF3, + 0x2EF4, + 0x2F00, + 0x2F01, + 0x2F02, + 0x2F03, + 0x2F04, + 0x2F05, + 0x2F06, + 0x2F07, + 0x2F08, + 0x2F09, + 0x2F0A, + 0x2F0B, + 0x2F0C, + 0x2F0D, + 0x2F0E, + 0x2F0F, + 0x2F10, + 0x2F11, + 0x2F12, + 0x2F13, + 0x2F14, + 0x2F15, + 0x2F16, + 0x2F17, + 0x2F18, + 0x2F19, + 0x2F1A, + 0x2F1B, + 0x2F1C, + 0x2F1D, + 0x2F1E, + 0x2F1F, + 0x2F20, + 0x2F21, + 0x2F22, + 0x2F23, + 0x2F24, + 0x2F25, + 0x2F26, + 0x2F27, + 0x2F28, + 0x2F29, + 0x2F2A, + 0x2F2B, + 0x2F2C, + 0x2F2D, + 0x2F2E, + 0x2F2F, + 0x2F30, + 0x2F31, + 0x2F32, + 0x2F33, + 0x2F34, + 0x2F35, + 0x2F36, + 0x2F37, + 0x2F38, + 0x2F39, + 0x2F3A, + 0x2F3B, + 0x2F3C, + 0x2F3D, + 0x2F3E, + 0x2F3F, + 0x2F40, + 0x2F41, + 0x2F42, + 0x2F43, + 0x2F44, + 0x2F45, + 0x2F46, + 0x2F47, + 0x2F48, + 0x2F49, + 0x2F4A, + 0x2F4B, + 0x2F4C, + 0x2F4D, + 0x2F4E, + 0x2F4F, + 0x2F50, + 0x2F51, + 0x2F52, + 0x2F53, + 0x2F54, + 0x2F55, + 0x2F56, + 0x2F57, + 0x2F58, + 0x2F59, + 0x2F5A, + 0x2F5B, + 0x2F5C, + 0x2F5D, + 0x2F5E, + 0x2F5F, + 0x2F60, + 0x2F61, + 0x2F62, + 0x2F63, + 0x2F64, + 0x2F65, + 0x2F66, + 0x2F67, + 0x2F68, + 0x2F69, + 0x2F6A, + 0x2F6B, + 0x2F6C, + 0x2F6D, + 0x2F6E, + 0x2F6F, + 0x2F70, + 0x2F71, + 0x2F72, + 0x2F73, + 0x2F74, + 0x2F75, + 0x2F76, + 0x2F77, + 0x2F78, + 0x2F79, + 0x2F7A, + 0x2F7B, + 0x2F7C, + 0x2F7D, + 0x2F7E, + 0x2F7F, + 0x2F80, + 0x2F81, + 0x2F82, + 0x2F83, + 0x2F84, + 0x2F85, + 0x2F86, + 0x2F87, + 0x2F88, + 0x2F89, + 0x2F8A, + 0x2F8B, + 0x2F8C, + 0x2F8D, + 0x2F8E, + 0x2F8F, + 0x2F90, + 0x2F91, + 0x2F92, + 0x2F93, + 0x2F94, + 0x2F95, + 0x2F96, + 0x2F97, + 0x2F98, + 0x2F99, + 0x2F9A, + 0x2F9B, + 0x2F9C, + 0x2F9D, + 0x2F9E, + 0x2F9F, + 0x2FA0, + 0x2FA1, + 0x2FA2, + 0x2FA3, + 0x2FA4, + 0x2FA5, + 0x2FA6, + 0x2FA7, + 0x2FA8, + 0x2FA9, + 0x2FAA, + 0x2FAB, + 0x2FAC, + 0x2FAD, + 0x2FAE, + 0x2FAF, + 0x2FB0, + 0x2FB1, + 0x2FB2, + 0x2FB3, + 0x2FB4, + 0x2FB5, + 0x2FB6, + 0x2FB7, + 0x2FB8, + 0x2FB9, + 0x2FBA, + 0x2FBB, + 0x2FBC, + 0x2FBD, + 0x2FBE, + 0x2FBF, + 0x2FC0, + 0x2FC1, + 0x2FC2, + 0x2FC3, + 0x2FC4, + 0x2FC5, + 0x2FC6, + 0x2FC7, + 0x2FC8, + 0x2FC9, + 0x2FCA, + 0x2FCB, + 0x2FCC, + 0x2FCD, + 0x2FCE, + 0x2FCF, + 0x2FD0, + 0x2FD1, + 0x2FD2, + 0x2FD3, + 0x2FD4, + 0x2FD5, + 0x2FD6, + 0x3000, + 0x3001, + 0x3002, + 0x3003, + 0x3036, + 0x3037, + 0x3038, + 0x3039, + 0x303A, + 0x303B, + 0x3040, + 0x3041, + 0x3097, + 0x3099, + 0x309B, + 0x309C, + 0x309D, + 0x309F, + 0x30A0, + 0x30FF, + 0x3100, + 0x3105, + 0x3130, + 0x3131, + 0x3132, + 0x3133, + 0x3134, + 0x3135, + 0x3136, + 0x3137, + 0x3138, + 0x3139, + 0x313A, + 0x313B, + 0x313C, + 0x313D, + 0x313E, + 0x313F, + 0x3140, + 0x3141, + 0x3142, + 0x3143, + 0x3144, + 0x3145, + 0x3146, + 0x3147, + 0x3148, + 0x3149, + 0x314A, + 0x314B, + 0x314C, + 0x314D, + 0x314E, + 0x314F, + 0x3150, + 0x3151, + 0x3152, + 0x3153, + 0x3154, + 0x3155, + 0x3156, + 0x3157, + 0x3158, + 0x3159, + 0x315A, + 0x315B, + 0x315C, + 0x315D, + 0x315E, + 0x315F, + 0x3160, + 0x3161, + 0x3162, + 0x3163, + 0x3164, + 0x3165, + 0x3166, + 0x3167, + 0x3168, + 0x3169, + 0x316A, + 0x316B, + 0x316C, + 0x316D, + 0x316E, + 0x316F, + 0x3170, + 0x3171, + 0x3172, + 0x3173, + 0x3174, + 0x3175, + 0x3176, + 0x3177, + 0x3178, + 0x3179, + 0x317A, + 0x317B, + 0x317C, + 0x317D, + 0x317E, + 0x317F, + 0x3180, + 0x3181, + 0x3182, + 0x3183, + 0x3184, + 0x3185, + 0x3186, + 0x3187, + 0x3188, + 0x3189, + 0x318A, + 0x318B, + 0x318C, + 0x318D, + 0x318E, + 0x318F, + 0x3190, + 0x3192, + 0x3193, + 0x3194, + 0x3195, + 0x3196, + 0x3197, + 0x3198, + 0x3199, + 0x319A, + 0x319B, + 0x319C, + 0x319D, + 0x319E, + 0x319F, + 0x31A0, + 0x31E6, + 0x31F0, + 0x3200, + 0x3201, + 0x3202, + 0x3203, + 0x3204, + 0x3205, + 0x3206, + 0x3207, + 0x3208, + 0x3209, + 0x320A, + 0x320B, + 0x320C, + 0x320D, + 0x320E, + 0x320F, + 0x3210, + 0x3211, + 0x3212, + 0x3213, + 0x3214, + 0x3215, + 0x3216, + 0x3217, + 0x3218, + 0x3219, + 0x321A, + 0x321B, + 0x321C, + 0x321D, + 0x321E, + 0x321F, + 0x3220, + 0x3221, + 0x3222, + 0x3223, + 0x3224, + 0x3225, + 0x3226, + 0x3227, + 0x3228, + 0x3229, + 0x322A, + 0x322B, + 0x322C, + 0x322D, + 0x322E, + 0x322F, + 0x3230, + 0x3231, + 0x3232, + 0x3233, + 0x3234, + 0x3235, + 0x3236, + 0x3237, + 0x3238, + 0x3239, + 0x323A, + 0x323B, + 0x323C, + 0x323D, + 0x323E, + 0x323F, + 0x3240, + 0x3241, + 0x3242, + 0x3243, + 0x3244, + 0x3245, + 0x3246, + 0x3247, + 0x3248, + 0x3250, + 0x3251, + 0x3252, + 0x3253, + 0x3254, + 0x3255, + 0x3256, + 0x3257, + 0x3258, + 0x3259, + 0x325A, + 0x325B, + 0x325C, + 0x325D, + 0x325E, + 0x325F, + 0x3260, + 0x3261, + 0x3262, + 0x3263, + 0x3264, + 0x3265, + 0x3266, + 0x3267, + 0x3268, + 0x3269, + 0x326A, + 0x326B, + 0x326C, + 0x326D, + 0x326E, + 0x326F, + 0x3270, + 0x3271, + 0x3272, + 0x3273, + 0x3274, + 0x3275, + 0x3276, + 0x3277, + 0x3278, + 0x3279, + 0x327A, + 0x327B, + 0x327C, + 0x327D, + 0x327E, + 0x327F, + 0x3280, + 0x3281, + 0x3282, + 0x3283, + 0x3284, + 0x3285, + 0x3286, + 0x3287, + 0x3288, + 0x3289, + 0x328A, + 0x328B, + 0x328C, + 0x328D, + 0x328E, + 0x328F, + 0x3290, + 0x3291, + 0x3292, + 0x3293, + 0x3294, + 0x3295, + 0x3296, + 0x3297, + 0x3298, + 0x3299, + 0x329A, + 0x329B, + 0x329C, + 0x329D, + 0x329E, + 0x329F, + 0x32A0, + 0x32A1, + 0x32A2, + 0x32A3, + 0x32A4, + 0x32A5, + 0x32A6, + 0x32A7, + 0x32A8, + 0x32A9, + 0x32AA, + 0x32AB, + 0x32AC, + 0x32AD, + 0x32AE, + 0x32AF, + 0x32B0, + 0x32B1, + 0x32B2, + 0x32B3, + 0x32B4, + 0x32B5, + 0x32B6, + 0x32B7, + 0x32B8, + 0x32B9, + 0x32BA, + 0x32BB, + 0x32BC, + 0x32BD, + 0x32BE, + 0x32BF, + 0x32C0, + 0x32C1, + 0x32C2, + 0x32C3, + 0x32C4, + 0x32C5, + 0x32C6, + 0x32C7, + 0x32C8, + 0x32C9, + 0x32CA, + 0x32CB, + 0x32CC, + 0x32CD, + 0x32CE, + 0x32CF, + 0x32D0, + 0x32D1, + 0x32D2, + 0x32D3, + 0x32D4, + 0x32D5, + 0x32D6, + 0x32D7, + 0x32D8, + 0x32D9, + 0x32DA, + 0x32DB, + 0x32DC, + 0x32DD, + 0x32DE, + 0x32DF, + 0x32E0, + 0x32E1, + 0x32E2, + 0x32E3, + 0x32E4, + 0x32E5, + 0x32E6, + 0x32E7, + 0x32E8, + 0x32E9, + 0x32EA, + 0x32EB, + 0x32EC, + 0x32ED, + 0x32EE, + 0x32EF, + 0x32F0, + 0x32F1, + 0x32F2, + 0x32F3, + 0x32F4, + 0x32F5, + 0x32F6, + 0x32F7, + 0x32F8, + 0x32F9, + 0x32FA, + 0x32FB, + 0x32FC, + 0x32FD, + 0x32FE, + 0x32FF, + 0x3300, + 0x3301, + 0x3302, + 0x3303, + 0x3304, + 0x3305, + 0x3306, + 0x3307, + 0x3308, + 0x3309, + 0x330A, + 0x330B, + 0x330C, + 0x330D, + 0x330E, + 0x330F, + 0x3310, + 0x3311, + 0x3312, + 0x3313, + 0x3314, + 0x3315, + 0x3316, + 0x3317, + 0x3318, + 0x3319, + 0x331A, + 0x331B, + 0x331C, + 0x331D, + 0x331E, + 0x331F, + 0x3320, + 0x3321, + 0x3322, + 0x3323, + 0x3324, + 0x3325, + 0x3326, + 0x3327, + 0x3328, + 0x3329, + 0x332A, + 0x332B, + 0x332C, + 0x332D, + 0x332E, + 0x332F, + 0x3330, + 0x3331, + 0x3332, + 0x3333, + 0x3334, + 0x3335, + 0x3336, + 0x3337, + 0x3338, + 0x3339, + 0x333A, + 0x333B, + 0x333C, + 0x333D, + 0x333E, + 0x333F, + 0x3340, + 0x3341, + 0x3342, + 0x3343, + 0x3344, + 0x3345, + 0x3346, + 0x3347, + 0x3348, + 0x3349, + 0x334A, + 0x334B, + 0x334C, + 0x334D, + 0x334E, + 0x334F, + 0x3350, + 0x3351, + 0x3352, + 0x3353, + 0x3354, + 0x3355, + 0x3356, + 0x3357, + 0x3358, + 0x3359, + 0x335A, + 0x335B, + 0x335C, + 0x335D, + 0x335E, + 0x335F, + 0x3360, + 0x3361, + 0x3362, + 0x3363, + 0x3364, + 0x3365, + 0x3366, + 0x3367, + 0x3368, + 0x3369, + 0x336A, + 0x336B, + 0x336C, + 0x336D, + 0x336E, + 0x336F, + 0x3370, + 0x3371, + 0x3372, + 0x3373, + 0x3374, + 0x3375, + 0x3376, + 0x3377, + 0x3378, + 0x3379, + 0x337A, + 0x337B, + 0x337C, + 0x337D, + 0x337E, + 0x337F, + 0x3380, + 0x3381, + 0x3382, + 0x3383, + 0x3384, + 0x3385, + 0x3386, + 0x3387, + 0x3388, + 0x3389, + 0x338A, + 0x338B, + 0x338C, + 0x338D, + 0x338E, + 0x338F, + 0x3390, + 0x3391, + 0x3392, + 0x3393, + 0x3394, + 0x3395, + 0x3396, + 0x3397, + 0x3398, + 0x3399, + 0x339A, + 0x339B, + 0x339C, + 0x339D, + 0x339E, + 0x339F, + 0x33A0, + 0x33A1, + 0x33A2, + 0x33A3, + 0x33A4, + 0x33A5, + 0x33A6, + 0x33A7, + 0x33A8, + 0x33A9, + 0x33AA, + 0x33AB, + 0x33AC, + 0x33AD, + 0x33AE, + 0x33AF, + 0x33B0, + 0x33B1, + 0x33B2, + 0x33B3, + 0x33B4, + 0x33B5, + 0x33B6, + 0x33B7, + 0x33B8, + 0x33B9, + 0x33BA, + 0x33BB, + 0x33BC, + 0x33BD, + 0x33BE, + 0x33BF, + 0x33C0, + 0x33C1, + 0x33C2, + 0x33C3, + 0x33C4, + 0x33C5, + 0x33C6, + 0x33C7, + 0x33C8, + 0x33C9, + 0x33CA, + 0x33CB, + 0x33CC, + 0x33CD, + 0x33CE, + 0x33CF, + 0x33D0, + 0x33D1, + 0x33D2, + 0x33D3, + 0x33D4, + 0x33D5, + 0x33D6, + 0x33D7, + 0x33D8, + 0x33D9, + 0x33DA, + 0x33DB, + 0x33DC, + 0x33DD, + 0x33DE, + 0x33DF, + 0x33E0, + 0x33E1, + 0x33E2, + 0x33E3, + 0x33E4, + 0x33E5, + 0x33E6, + 0x33E7, + 0x33E8, + 0x33E9, + 0x33EA, + 0x33EB, + 0x33EC, + 0x33ED, + 0x33EE, + 0x33EF, + 0x33F0, + 0x33F1, + 0x33F2, + 0x33F3, + 0x33F4, + 0x33F5, + 0x33F6, + 0x33F7, + 0x33F8, + 0x33F9, + 0x33FA, + 0x33FB, + 0x33FC, + 0x33FD, + 0x33FE, + 0x33FF, + 0x3400, + 0xA48D, + 0xA490, + 0xA4C7, + 0xA4D0, + 0xA62C, + 0xA640, + 0xA641, + 0xA642, + 0xA643, + 0xA644, + 0xA645, + 0xA646, + 0xA647, + 0xA648, + 0xA649, + 0xA64A, + 0xA64B, + 0xA64C, + 0xA64D, + 0xA64E, + 0xA64F, + 0xA650, + 0xA651, + 0xA652, + 0xA653, + 0xA654, + 0xA655, + 0xA656, + 0xA657, + 0xA658, + 0xA659, + 0xA65A, + 0xA65B, + 0xA65C, + 0xA65D, + 0xA65E, + 0xA65F, + 0xA660, + 0xA661, + 0xA662, + 0xA663, + 0xA664, + 0xA665, + 0xA666, + 0xA667, + 0xA668, + 0xA669, + 0xA66A, + 0xA66B, + 0xA66C, + 0xA66D, + 0xA680, + 0xA681, + 0xA682, + 0xA683, + 0xA684, + 0xA685, + 0xA686, + 0xA687, + 0xA688, + 0xA689, + 0xA68A, + 0xA68B, + 0xA68C, + 0xA68D, + 0xA68E, + 0xA68F, + 0xA690, + 0xA691, + 0xA692, + 0xA693, + 0xA694, + 0xA695, + 0xA696, + 0xA697, + 0xA698, + 0xA699, + 0xA69A, + 0xA69B, + 0xA69C, + 0xA69D, + 0xA69E, + 0xA6F8, + 0xA700, + 0xA722, + 0xA723, + 0xA724, + 0xA725, + 0xA726, + 0xA727, + 0xA728, + 0xA729, + 0xA72A, + 0xA72B, + 0xA72C, + 0xA72D, + 0xA72E, + 0xA72F, + 0xA732, + 0xA733, + 0xA734, + 0xA735, + 0xA736, + 0xA737, + 0xA738, + 0xA739, + 0xA73A, + 0xA73B, + 0xA73C, + 0xA73D, + 0xA73E, + 0xA73F, + 0xA740, + 0xA741, + 0xA742, + 0xA743, + 0xA744, + 0xA745, + 0xA746, + 0xA747, + 0xA748, + 0xA749, + 0xA74A, + 0xA74B, + 0xA74C, + 0xA74D, + 0xA74E, + 0xA74F, + 0xA750, + 0xA751, + 0xA752, + 0xA753, + 0xA754, + 0xA755, + 0xA756, + 0xA757, + 0xA758, + 0xA759, + 0xA75A, + 0xA75B, + 0xA75C, + 0xA75D, + 0xA75E, + 0xA75F, + 0xA760, + 0xA761, + 0xA762, + 0xA763, + 0xA764, + 0xA765, + 0xA766, + 0xA767, + 0xA768, + 0xA769, + 0xA76A, + 0xA76B, + 0xA76C, + 0xA76D, + 0xA76E, + 0xA76F, + 0xA770, + 0xA771, + 0xA779, + 0xA77A, + 0xA77B, + 0xA77C, + 0xA77D, + 0xA77E, + 0xA77F, + 0xA780, + 0xA781, + 0xA782, + 0xA783, + 0xA784, + 0xA785, + 0xA786, + 0xA787, + 0xA78B, + 0xA78C, + 0xA78D, + 0xA78E, + 0xA790, + 0xA791, + 0xA792, + 0xA793, + 0xA796, + 0xA797, + 0xA798, + 0xA799, + 0xA79A, + 0xA79B, + 0xA79C, + 0xA79D, + 0xA79E, + 0xA79F, + 0xA7A0, + 0xA7A1, + 0xA7A2, + 0xA7A3, + 0xA7A4, + 0xA7A5, + 0xA7A6, + 0xA7A7, + 0xA7A8, + 0xA7A9, + 0xA7AA, + 0xA7AB, + 0xA7AC, + 0xA7AD, + 0xA7AE, + 0xA7AF, + 0xA7B0, + 0xA7B1, + 0xA7B2, + 0xA7B3, + 0xA7B4, + 0xA7B5, + 0xA7B6, + 0xA7B7, + 0xA7B8, + 0xA7B9, + 0xA7BA, + 0xA7BB, + 0xA7BC, + 0xA7BD, + 0xA7BE, + 0xA7BF, + 0xA7C0, + 0xA7C1, + 0xA7C2, + 0xA7C3, + 0xA7C4, + 0xA7C5, + 0xA7C6, + 0xA7C7, + 0xA7C8, + 0xA7C9, + 0xA7CA, + 0xA7CB, + 0xA7CC, + 0xA7CD, + 0xA7CE, + 0xA7CF, + 0xA7D0, + 0xA7D1, + 0xA7D2, + 0xA7D3, + 0xA7D4, + 0xA7D5, + 0xA7D6, + 0xA7D7, + 0xA7D8, + 0xA7D9, + 0xA7DA, + 0xA7DB, + 0xA7DC, + 0xA7DD, + 0xA7DE, + 0xA7E2, + 0xA7E3, + 0xA7F1, + 0xA7F2, + 0xA7F3, + 0xA7F4, + 0xA7F5, + 0xA7F6, + 0xA7F8, + 0xA7F9, + 0xA7FA, + 0xA82D, + 0xA830, + 0xA83A, + 0xA840, + 0xA878, + 0xA880, + 0xA8C6, + 0xA8CE, + 0xA8DA, + 0xA8E0, + 0xA954, + 0xA95F, + 0xA97D, + 0xA980, + 0xA9CE, + 0xA9CF, + 0xA9DA, + 0xA9DE, + 0xA9FF, + 0xAA00, + 0xAA37, + 0xAA40, + 0xAA4E, + 0xAA50, + 0xAA5A, + 0xAA5C, + 0xAAC3, + 0xAADB, + 0xAAF7, + 0xAB01, + 0xAB07, + 0xAB09, + 0xAB0F, + 0xAB11, + 0xAB17, + 0xAB20, + 0xAB27, + 0xAB28, + 0xAB2F, + 0xAB30, + 0xAB5C, + 0xAB5D, + 0xAB5E, + 0xAB5F, + 0xAB60, + 0xAB69, + 0xAB6A, + 0xAB6C, + 0xAB6D, + 0xAB6E, + 0xAB70, + 0xAB71, + 0xAB72, + 0xAB73, + 0xAB74, + 0xAB75, + 0xAB76, + 0xAB77, + 0xAB78, + 0xAB79, + 0xAB7A, + 0xAB7B, + 0xAB7C, + 0xAB7D, + 0xAB7E, + 0xAB7F, + 0xAB80, + 0xAB81, + 0xAB82, + 0xAB83, + 0xAB84, + 0xAB85, + 0xAB86, + 0xAB87, + 0xAB88, + 0xAB89, + 0xAB8A, + 0xAB8B, + 0xAB8C, + 0xAB8D, + 0xAB8E, + 0xAB8F, + 0xAB90, + 0xAB91, + 0xAB92, + 0xAB93, + 0xAB94, + 0xAB95, + 0xAB96, + 0xAB97, + 0xAB98, + 0xAB99, + 0xAB9A, + 0xAB9B, + 0xAB9C, + 0xAB9D, + 0xAB9E, + 0xAB9F, + 0xABA0, + 0xABA1, + 0xABA2, + 0xABA3, + 0xABA4, + 0xABA5, + 0xABA6, + 0xABA7, + 0xABA8, + 0xABA9, + 0xABAA, + 0xABAB, + 0xABAC, + 0xABAD, + 0xABAE, + 0xABAF, + 0xABB0, + 0xABB1, + 0xABB2, + 0xABB3, + 0xABB4, + 0xABB5, + 0xABB6, + 0xABB7, + 0xABB8, + 0xABB9, + 0xABBA, + 0xABBB, + 0xABBC, + 0xABBD, + 0xABBE, + 0xABBF, + 0xABC0, + 0xABEE, + 0xABF0, + 0xABFA, + 0xAC00, + 0xD7A4, + 0xD7B0, + 0xD7C7, + 0xD7CB, + 0xD7FC, + 0xF900, + 0xF901, + 0xF902, + 0xF903, + 0xF904, + 0xF905, + 0xF906, + 0xF907, + 0xF909, + 0xF90A, + 0xF90B, + 0xF90C, + 0xF90D, + 0xF90E, + 0xF90F, + 0xF910, + 0xF911, + 0xF912, + 0xF913, + 0xF914, + 0xF915, + 0xF916, + 0xF917, + 0xF918, + 0xF919, + 0xF91A, + 0xF91B, + 0xF91C, + 0xF91D, + 0xF91E, + 0xF91F, + 0xF920, + 0xF921, + 0xF922, + 0xF923, + 0xF924, + 0xF925, + 0xF926, + 0xF927, + 0xF928, + 0xF929, + 0xF92A, + 0xF92B, + 0xF92C, + 0xF92D, + 0xF92E, + 0xF92F, + 0xF930, + 0xF931, + 0xF932, + 0xF933, + 0xF934, + 0xF935, + 0xF936, + 0xF937, + 0xF938, + 0xF939, + 0xF93A, + 0xF93B, + 0xF93C, + 0xF93D, + 0xF93E, + 0xF93F, + 0xF940, + 0xF941, + 0xF942, + 0xF943, + 0xF944, + 0xF945, + 0xF946, + 0xF947, + 0xF948, + 0xF949, + 0xF94A, + 0xF94B, + 0xF94C, + 0xF94D, + 0xF94E, + 0xF94F, + 0xF950, + 0xF951, + 0xF952, + 0xF953, + 0xF954, + 0xF955, + 0xF956, + 0xF957, + 0xF958, + 0xF959, + 0xF95A, + 0xF95B, + 0xF95C, + 0xF95D, + 0xF95E, + 0xF95F, + 0xF960, + 0xF961, + 0xF962, + 0xF963, + 0xF964, + 0xF965, + 0xF966, + 0xF967, + 0xF968, + 0xF969, + 0xF96A, + 0xF96B, + 0xF96C, + 0xF96D, + 0xF96E, + 0xF96F, + 0xF970, + 0xF971, + 0xF972, + 0xF973, + 0xF974, + 0xF975, + 0xF976, + 0xF977, + 0xF978, + 0xF979, + 0xF97A, + 0xF97B, + 0xF97C, + 0xF97D, + 0xF97E, + 0xF97F, + 0xF980, + 0xF981, + 0xF982, + 0xF983, + 0xF984, + 0xF985, + 0xF986, + 0xF987, + 0xF988, + 0xF989, + 0xF98A, + 0xF98B, + 0xF98C, + 0xF98D, + 0xF98E, + 0xF98F, + 0xF990, + 0xF991, + 0xF992, + 0xF993, + 0xF994, + 0xF995, + 0xF996, + 0xF997, + 0xF998, + 0xF999, + 0xF99A, + 0xF99B, + 0xF99C, + 0xF99D, + 0xF99E, + 0xF99F, + 0xF9A0, + 0xF9A1, + 0xF9A2, + 0xF9A3, + 0xF9A4, + 0xF9A5, + 0xF9A6, + 0xF9A7, + 0xF9A8, + 0xF9A9, + 0xF9AA, + 0xF9AB, + 0xF9AC, + 0xF9AD, + 0xF9AE, + 0xF9AF, + 0xF9B0, + 0xF9B1, + 0xF9B2, + 0xF9B3, + 0xF9B4, + 0xF9B5, + 0xF9B6, + 0xF9B7, + 0xF9B8, + 0xF9B9, + 0xF9BA, + 0xF9BB, + 0xF9BC, + 0xF9BD, + 0xF9BE, + 0xF9BF, + 0xF9C0, + 0xF9C1, + 0xF9C2, + 0xF9C3, + 0xF9C4, + 0xF9C5, + 0xF9C6, + 0xF9C7, + 0xF9C8, + 0xF9C9, + 0xF9CA, + 0xF9CB, + 0xF9CC, + 0xF9CD, + 0xF9CE, + 0xF9CF, + 0xF9D0, + 0xF9D1, + 0xF9D2, + 0xF9D3, + 0xF9D4, + 0xF9D5, + 0xF9D6, + 0xF9D7, + 0xF9D8, + 0xF9D9, + 0xF9DA, + 0xF9DB, + 0xF9DC, + 0xF9DD, + 0xF9DE, + 0xF9DF, + 0xF9E0, + 0xF9E1, + 0xF9E2, + 0xF9E3, + 0xF9E4, + 0xF9E5, + 0xF9E6, + 0xF9E7, + 0xF9E8, + 0xF9E9, + 0xF9EA, + 0xF9EB, + 0xF9EC, + 0xF9ED, + 0xF9EE, + 0xF9EF, + 0xF9F0, + 0xF9F1, + 0xF9F2, + 0xF9F3, + 0xF9F4, + 0xF9F5, + 0xF9F6, + 0xF9F7, + 0xF9F8, + 0xF9F9, + 0xF9FA, + 0xF9FB, + 0xF9FC, + 0xF9FD, + 0xF9FE, + 0xF9FF, + 0xFA00, + 0xFA01, + 0xFA02, + 0xFA03, + 0xFA04, + 0xFA05, + 0xFA06, + 0xFA07, + 0xFA08, + 0xFA09, + 0xFA0A, + 0xFA0B, + 0xFA0C, + 0xFA0D, + 0xFA0E, + 0xFA10, + 0xFA11, + 0xFA12, + 0xFA13, + 0xFA15, + 0xFA16, + 0xFA17, + 0xFA18, + 0xFA19, + 0xFA1A, + 0xFA1B, + 0xFA1C, + 0xFA1D, + 0xFA1E, + 0xFA1F, + 0xFA20, + 0xFA21, + 0xFA22, + 0xFA23, + 0xFA25, + 0xFA26, + 0xFA27, + 0xFA2A, + 0xFA2B, + 0xFA2C, + 0xFA2D, + 0xFA2E, + 0xFA2F, + 0xFA30, + 0xFA31, + 0xFA32, + 0xFA33, + 0xFA34, + 0xFA35, + 0xFA36, + 0xFA37, + 0xFA38, + 0xFA39, + 0xFA3A, + 0xFA3B, + 0xFA3C, + 0xFA3D, + 0xFA3E, + 0xFA3F, + 0xFA40, + 0xFA41, + 0xFA42, + 0xFA43, + 0xFA44, + 0xFA45, + 0xFA46, + 0xFA47, + 0xFA48, + 0xFA49, + 0xFA4A, + 0xFA4B, + 0xFA4C, + 0xFA4D, + 0xFA4E, + 0xFA4F, + 0xFA50, + 0xFA51, + 0xFA52, + 0xFA53, + 0xFA54, + 0xFA55, + 0xFA56, + 0xFA57, + 0xFA58, + 0xFA59, + 0xFA5A, + 0xFA5B, + 0xFA5C, + 0xFA5D, + 0xFA5F, + 0xFA60, + 0xFA61, + 0xFA62, + 0xFA63, + 0xFA64, + 0xFA65, + 0xFA66, + 0xFA67, + 0xFA68, + 0xFA69, + 0xFA6A, + 0xFA6B, + 0xFA6C, + 0xFA6D, + 0xFA6E, + 0xFA70, + 0xFA71, + 0xFA72, + 0xFA73, + 0xFA74, + 0xFA75, + 0xFA76, + 0xFA77, + 0xFA78, + 0xFA79, + 0xFA7A, + 0xFA7B, + 0xFA7C, + 0xFA7D, + 0xFA7E, + 0xFA7F, + 0xFA80, + 0xFA81, + 0xFA82, + 0xFA83, + 0xFA84, + 0xFA85, + 0xFA86, + 0xFA87, + 0xFA88, + 0xFA89, + 0xFA8A, + 0xFA8B, + 0xFA8C, + 0xFA8D, + 0xFA8E, + 0xFA8F, + 0xFA90, + 0xFA91, + 0xFA92, + 0xFA93, + 0xFA94, + 0xFA95, + 0xFA96, + 0xFA97, + 0xFA98, + 0xFA99, + 0xFA9A, + 0xFA9B, + 0xFA9C, + 0xFA9D, + 0xFA9E, + 0xFA9F, + 0xFAA0, + 0xFAA1, + 0xFAA2, + 0xFAA3, + 0xFAA4, + 0xFAA5, + 0xFAA6, + 0xFAA7, + 0xFAA8, + 0xFAA9, + 0xFAAA, + 0xFAAB, + 0xFAAC, + 0xFAAD, + 0xFAAE, + 0xFAAF, + 0xFAB0, + 0xFAB1, + 0xFAB2, + 0xFAB3, + 0xFAB4, + 0xFAB5, + 0xFAB6, + 0xFAB7, + 0xFAB8, + 0xFAB9, + 0xFABA, + 0xFABB, + 0xFABC, + 0xFABD, + 0xFABE, + 0xFABF, + 0xFAC0, + 0xFAC1, + 0xFAC2, + 0xFAC3, + 0xFAC4, + 0xFAC5, + 0xFAC6, + 0xFAC7, + 0xFAC8, + 0xFAC9, + 0xFACA, + 0xFACB, + 0xFACC, + 0xFACD, + 0xFACE, + 0xFACF, + 0xFAD0, + 0xFAD1, + 0xFAD2, + 0xFAD3, + 0xFAD4, + 0xFAD5, + 0xFAD6, + 0xFAD7, + 0xFAD8, + 0xFAD9, + 0xFADA, + 0xFB00, + 0xFB01, + 0xFB02, + 0xFB03, + 0xFB04, + 0xFB05, + 0xFB07, + 0xFB13, + 0xFB14, + 0xFB15, + 0xFB16, + 0xFB17, + 0xFB18, + 0xFB1D, + 0xFB1E, + 0xFB1F, + 0xFB20, + 0xFB21, + 0xFB22, + 0xFB23, + 0xFB24, + 0xFB25, + 0xFB26, + 0xFB27, + 0xFB28, + 0xFB29, + 0xFB2A, + 0xFB2B, + 0xFB2C, + 0xFB2D, + 0xFB2E, + 0xFB2F, + 0xFB30, + 0xFB31, + 0xFB32, + 0xFB33, + 0xFB34, + 0xFB35, + 0xFB36, + 0xFB37, + 0xFB38, + 0xFB39, + 0xFB3A, + 0xFB3B, + 0xFB3C, + 0xFB3D, + 0xFB3E, + 0xFB3F, + 0xFB40, + 0xFB41, + 0xFB42, + 0xFB43, + 0xFB44, + 0xFB45, + 0xFB46, + 0xFB47, + 0xFB48, + 0xFB49, + 0xFB4A, + 0xFB4B, + 0xFB4C, + 0xFB4D, + 0xFB4E, + 0xFB4F, + 0xFB50, + 0xFB52, + 0xFB56, + 0xFB5A, + 0xFB5E, + 0xFB62, + 0xFB66, + 0xFB6A, + 0xFB6E, + 0xFB72, + 0xFB76, + 0xFB7A, + 0xFB7E, + 0xFB82, + 0xFB84, + 0xFB86, + 0xFB88, + 0xFB8A, + 0xFB8C, + 0xFB8E, + 0xFB92, + 0xFB96, + 0xFB9A, + 0xFB9E, + 0xFBA0, + 0xFBA4, + 0xFBA6, + 0xFBAA, + 0xFBAE, + 0xFBB0, + 0xFBB2, + 0xFBD3, + 0xFBD7, + 0xFBD9, + 0xFBDB, + 0xFBDD, + 0xFBDE, + 0xFBE0, + 0xFBE2, + 0xFBE4, + 0xFBE8, + 0xFBEA, + 0xFBEC, + 0xFBEE, + 0xFBF0, + 0xFBF2, + 0xFBF4, + 0xFBF6, + 0xFBF9, + 0xFBFC, + 0xFC00, + 0xFC01, + 0xFC02, + 0xFC03, + 0xFC04, + 0xFC05, + 0xFC06, + 0xFC07, + 0xFC08, + 0xFC09, + 0xFC0A, + 0xFC0B, + 0xFC0C, + 0xFC0D, + 0xFC0E, + 0xFC0F, + 0xFC10, + 0xFC11, + 0xFC12, + 0xFC13, + 0xFC14, + 0xFC15, + 0xFC16, + 0xFC17, + 0xFC18, + 0xFC19, + 0xFC1A, + 0xFC1B, + 0xFC1C, + 0xFC1D, + 0xFC1E, + 0xFC1F, + 0xFC20, + 0xFC21, + 0xFC22, + 0xFC23, + 0xFC24, + 0xFC25, + 0xFC26, + 0xFC27, + 0xFC28, + 0xFC29, + 0xFC2A, + 0xFC2B, + 0xFC2C, + 0xFC2D, + 0xFC2E, + 0xFC2F, + 0xFC30, + 0xFC31, + 0xFC32, + 0xFC33, + 0xFC34, + 0xFC35, + 0xFC36, + 0xFC37, + 0xFC38, + 0xFC39, + 0xFC3A, + 0xFC3B, + 0xFC3C, + 0xFC3D, + 0xFC3E, + 0xFC3F, + 0xFC40, + 0xFC41, + 0xFC42, + 0xFC43, + 0xFC44, + 0xFC45, + 0xFC46, + 0xFC47, + 0xFC48, + 0xFC49, + 0xFC4A, + 0xFC4B, + 0xFC4C, + 0xFC4D, + 0xFC4E, + 0xFC4F, + 0xFC50, + 0xFC51, + 0xFC52, + 0xFC53, + 0xFC54, + 0xFC55, + 0xFC56, + 0xFC57, + 0xFC58, + 0xFC59, + 0xFC5A, + 0xFC5B, + 0xFC5C, + 0xFC5D, + 0xFC5E, + 0xFC5F, + 0xFC60, + 0xFC61, + 0xFC62, + 0xFC63, + 0xFC64, + 0xFC65, + 0xFC66, + 0xFC67, + 0xFC68, + 0xFC69, + 0xFC6A, + 0xFC6B, + 0xFC6C, + 0xFC6D, + 0xFC6E, + 0xFC6F, + 0xFC70, + 0xFC71, + 0xFC72, + 0xFC73, + 0xFC74, + 0xFC75, + 0xFC76, + 0xFC77, + 0xFC78, + 0xFC79, + 0xFC7A, + 0xFC7B, + 0xFC7C, + 0xFC7D, + 0xFC7E, + 0xFC7F, + 0xFC80, + 0xFC81, + 0xFC82, + 0xFC83, + 0xFC84, + 0xFC85, + 0xFC86, + 0xFC87, + 0xFC88, + 0xFC89, + 0xFC8A, + 0xFC8B, + 0xFC8C, + 0xFC8D, + 0xFC8E, + 0xFC8F, + 0xFC90, + 0xFC91, + 0xFC92, + 0xFC93, + 0xFC94, + 0xFC95, + 0xFC96, + 0xFC97, + 0xFC98, + 0xFC99, + 0xFC9A, + 0xFC9B, + 0xFC9C, + 0xFC9D, + 0xFC9E, + 0xFC9F, + 0xFCA0, + 0xFCA1, + 0xFCA2, + 0xFCA3, + 0xFCA4, + 0xFCA5, + 0xFCA6, + 0xFCA7, + 0xFCA8, + 0xFCA9, + 0xFCAA, + 0xFCAB, + 0xFCAC, + 0xFCAD, + 0xFCAE, + 0xFCAF, + 0xFCB0, + 0xFCB1, + 0xFCB2, + 0xFCB3, + 0xFCB4, + 0xFCB5, + 0xFCB6, + 0xFCB7, + 0xFCB8, + 0xFCB9, + 0xFCBA, + 0xFCBB, + 0xFCBC, + 0xFCBD, + 0xFCBE, + 0xFCBF, + 0xFCC0, + 0xFCC1, + 0xFCC2, + 0xFCC3, + 0xFCC4, + 0xFCC5, + 0xFCC6, + 0xFCC7, + 0xFCC8, + 0xFCC9, + 0xFCCA, + 0xFCCB, + 0xFCCC, + 0xFCCD, + 0xFCCE, + 0xFCCF, + 0xFCD0, + 0xFCD1, + 0xFCD2, + 0xFCD3, + 0xFCD4, + 0xFCD5, + 0xFCD6, + 0xFCD7, + 0xFCD8, + 0xFCD9, + 0xFCDA, + 0xFCDB, + 0xFCDC, + 0xFCDD, + 0xFCDE, + 0xFCDF, + 0xFCE0, + 0xFCE1, + 0xFCE2, + 0xFCE3, + 0xFCE4, + 0xFCE5, + 0xFCE6, + 0xFCE7, + 0xFCE8, + 0xFCE9, + 0xFCEA, + 0xFCEB, + 0xFCEC, + 0xFCED, + 0xFCEE, + 0xFCEF, + 0xFCF0, + 0xFCF1, + 0xFCF2, + 0xFCF3, + 0xFCF4, + 0xFCF5, + 0xFCF6, + 0xFCF7, + 0xFCF8, + 0xFCF9, + 0xFCFA, + 0xFCFB, + 0xFCFC, + 0xFCFD, + 0xFCFE, + 0xFCFF, + 0xFD00, + 0xFD01, + 0xFD02, + 0xFD03, + 0xFD04, + 0xFD05, + 0xFD06, + 0xFD07, + 0xFD08, + 0xFD09, + 0xFD0A, + 0xFD0B, + 0xFD0C, + 0xFD0D, + 0xFD0E, + 0xFD0F, + 0xFD10, + 0xFD11, + 0xFD12, + 0xFD13, + 0xFD14, + 0xFD15, + 0xFD16, + 0xFD17, + 0xFD18, + 0xFD19, + 0xFD1A, + 0xFD1B, + 0xFD1C, + 0xFD1D, + 0xFD1E, + 0xFD1F, + 0xFD20, + 0xFD21, + 0xFD22, + 0xFD23, + 0xFD24, + 0xFD25, + 0xFD26, + 0xFD27, + 0xFD28, + 0xFD29, + 0xFD2A, + 0xFD2B, + 0xFD2C, + 0xFD2D, + 0xFD2E, + 0xFD2F, + 0xFD30, + 0xFD31, + 0xFD32, + 0xFD33, + 0xFD34, + 0xFD35, + 0xFD36, + 0xFD37, + 0xFD38, + 0xFD39, + 0xFD3A, + 0xFD3B, + 0xFD3C, + 0xFD3E, + 0xFD50, + 0xFD51, + 0xFD53, + 0xFD54, + 0xFD55, + 0xFD56, + 0xFD57, + 0xFD58, + 0xFD5A, + 0xFD5B, + 0xFD5C, + 0xFD5D, + 0xFD5E, + 0xFD5F, + 0xFD61, + 0xFD62, + 0xFD64, + 0xFD66, + 0xFD67, + 0xFD69, + 0xFD6A, + 0xFD6C, + 0xFD6E, + 0xFD6F, + 0xFD71, + 0xFD73, + 0xFD74, + 0xFD75, + 0xFD76, + 0xFD78, + 0xFD79, + 0xFD7A, + 0xFD7B, + 0xFD7C, + 0xFD7E, + 0xFD7F, + 0xFD80, + 0xFD81, + 0xFD82, + 0xFD83, + 0xFD85, + 0xFD87, + 0xFD89, + 0xFD8A, + 0xFD8B, + 0xFD8C, + 0xFD8D, + 0xFD8E, + 0xFD8F, + 0xFD90, + 0xFD92, + 0xFD93, + 0xFD94, + 0xFD95, + 0xFD96, + 0xFD97, + 0xFD99, + 0xFD9A, + 0xFD9B, + 0xFD9C, + 0xFD9E, + 0xFD9F, + 0xFDA0, + 0xFDA1, + 0xFDA2, + 0xFDA3, + 0xFDA4, + 0xFDA5, + 0xFDA6, + 0xFDA7, + 0xFDA8, + 0xFDA9, + 0xFDAA, + 0xFDAB, + 0xFDAC, + 0xFDAD, + 0xFDAE, + 0xFDAF, + 0xFDB0, + 0xFDB1, + 0xFDB2, + 0xFDB3, + 0xFDB4, + 0xFDB5, + 0xFDB6, + 0xFDB7, + 0xFDB8, + 0xFDB9, + 0xFDBA, + 0xFDBB, + 0xFDBC, + 0xFDBD, + 0xFDBE, + 0xFDBF, + 0xFDC0, + 0xFDC1, + 0xFDC2, + 0xFDC3, + 0xFDC4, + 0xFDC5, + 0xFDC6, + 0xFDC7, + 0xFDC8, + 0xFDD0, + 0xFDF0, + 0xFDF1, + 0xFDF2, + 0xFDF3, + 0xFDF4, + 0xFDF5, + 0xFDF6, + 0xFDF7, + 0xFDF8, + 0xFDF9, + 0xFDFA, + 0xFDFB, + 0xFDFC, + 0xFDFD, + 0xFE00, + 0xFE10, + 0xFE11, + 0xFE12, + 0xFE13, + 0xFE14, + 0xFE15, + 0xFE16, + 0xFE17, + 0xFE18, + 0xFE19, + 0xFE20, + 0xFE30, + 0xFE31, + 0xFE32, + 0xFE33, + 0xFE35, + 0xFE36, + 0xFE37, + 0xFE38, + 0xFE39, + 0xFE3A, + 0xFE3B, + 0xFE3C, + 0xFE3D, + 0xFE3E, + 0xFE3F, + 0xFE40, + 0xFE41, + 0xFE42, + 0xFE43, + 0xFE44, + 0xFE45, + 0xFE47, + 0xFE48, + 0xFE49, + 0xFE4D, + 0xFE50, + 0xFE51, + 0xFE52, + 0xFE54, + 0xFE55, + 0xFE56, + 0xFE57, + 0xFE58, + 0xFE59, + 0xFE5A, + 0xFE5B, + 0xFE5C, + 0xFE5D, + 0xFE5E, + 0xFE5F, + 0xFE60, + 0xFE61, + 0xFE62, + 0xFE63, + 0xFE64, + 0xFE65, + 0xFE66, + 0xFE67, + 0xFE68, + 0xFE69, + 0xFE6A, + 0xFE6B, + 0xFE6C, + 0xFE70, + 0xFE71, + 0xFE72, + 0xFE73, + 0xFE74, + 0xFE75, + 0xFE76, + 0xFE77, + 0xFE78, + 0xFE79, + 0xFE7A, + 0xFE7B, + 0xFE7C, + 0xFE7D, + 0xFE7E, + 0xFE7F, + 0xFE80, + 0xFE81, + 0xFE83, + 0xFE85, + 0xFE87, + 0xFE89, + 0xFE8D, + 0xFE8F, + 0xFE93, + 0xFE95, + 0xFE99, + 0xFE9D, + 0xFEA1, + 0xFEA5, + 0xFEA9, + 0xFEAB, + 0xFEAD, + 0xFEAF, + 0xFEB1, + 0xFEB5, + 0xFEB9, + 0xFEBD, + 0xFEC1, + 0xFEC5, + 0xFEC9, + 0xFECD, + 0xFED1, + 0xFED5, + 0xFED9, + 0xFEDD, + 0xFEE1, + 0xFEE5, + 0xFEE9, + 0xFEED, + 0xFEEF, + 0xFEF1, + 0xFEF5, + 0xFEF7, + 0xFEF9, + 0xFEFB, + 0xFEFD, + 0xFEFF, + 0xFF00, + 0xFF01, + 0xFF02, + 0xFF03, + 0xFF04, + 0xFF05, + 0xFF06, + 0xFF07, + 0xFF08, + 0xFF09, + 0xFF0A, + 0xFF0B, + 0xFF0C, + 0xFF0D, + 0xFF0E, + 0xFF0F, + 0xFF10, + 0xFF11, + 0xFF12, + 0xFF13, + 0xFF14, + 0xFF15, + 0xFF16, + 0xFF17, + 0xFF18, + 0xFF19, + 0xFF1A, + 0xFF1B, + 0xFF1C, + 0xFF1D, + 0xFF1E, + 0xFF1F, + 0xFF20, + 0xFF21, + 0xFF22, + 0xFF23, + 0xFF24, + 0xFF25, + 0xFF26, + 0xFF27, + 0xFF28, + 0xFF29, + 0xFF2A, + 0xFF2B, + 0xFF2C, + 0xFF2D, + 0xFF2E, + 0xFF2F, + 0xFF30, + 0xFF31, + 0xFF32, + 0xFF33, + 0xFF34, + 0xFF35, + 0xFF36, + 0xFF37, + 0xFF38, + 0xFF39, + 0xFF3A, + 0xFF3B, + 0xFF3C, + 0xFF3D, + 0xFF3E, + 0xFF3F, + 0xFF40, + 0xFF41, + 0xFF42, + 0xFF43, + 0xFF44, + 0xFF45, + 0xFF46, + 0xFF47, + 0xFF48, + 0xFF49, + 0xFF4A, + 0xFF4B, + 0xFF4C, + 0xFF4D, + 0xFF4E, + 0xFF4F, + 0xFF50, + 0xFF51, + 0xFF52, + 0xFF53, + 0xFF54, + 0xFF55, + 0xFF56, + 0xFF57, + 0xFF58, + 0xFF59, + 0xFF5A, + 0xFF5B, + 0xFF5C, + 0xFF5D, + 0xFF5E, + 0xFF5F, + 0xFF60, + 0xFF61, + 0xFF62, + 0xFF63, + 0xFF64, + 0xFF65, + 0xFF66, + 0xFF67, + 0xFF68, + 0xFF69, + 0xFF6A, + 0xFF6B, + 0xFF6C, + 0xFF6D, + 0xFF6E, + 0xFF6F, + 0xFF70, + 0xFF71, + 0xFF72, + 0xFF73, + 0xFF74, + 0xFF75, + 0xFF76, + 0xFF77, + 0xFF78, + 0xFF79, + 0xFF7A, + 0xFF7B, + 0xFF7C, + 0xFF7D, + 0xFF7E, + 0xFF7F, + 0xFF80, + 0xFF81, + 0xFF82, + 0xFF83, + 0xFF84, + 0xFF85, + 0xFF86, + 0xFF87, + 0xFF88, + 0xFF89, + 0xFF8A, + 0xFF8B, + 0xFF8C, + 0xFF8D, + 0xFF8E, + 0xFF8F, + 0xFF90, + 0xFF91, + 0xFF92, + 0xFF93, + 0xFF94, + 0xFF95, + 0xFF96, + 0xFF97, + 0xFF98, + 0xFF99, + 0xFF9A, + 0xFF9B, + 0xFF9C, + 0xFF9D, + 0xFF9E, + 0xFF9F, + 0xFFA0, + 0xFFA1, + 0xFFA2, + 0xFFA3, + 0xFFA4, + 0xFFA5, + 0xFFA6, + 0xFFA7, + 0xFFA8, + 0xFFA9, + 0xFFAA, + 0xFFAB, + 0xFFAC, + 0xFFAD, + 0xFFAE, + 0xFFAF, + 0xFFB0, + 0xFFB1, + 0xFFB2, + 0xFFB3, + 0xFFB4, + 0xFFB5, + 0xFFB6, + 0xFFB7, + 0xFFB8, + 0xFFB9, + 0xFFBA, + 0xFFBB, + 0xFFBC, + 0xFFBD, + 0xFFBE, + 0xFFBF, + 0xFFC2, + 0xFFC3, + 0xFFC4, + 0xFFC5, + 0xFFC6, + 0xFFC7, + 0xFFC8, + 0xFFCA, + 0xFFCB, + 0xFFCC, + 0xFFCD, + 0xFFCE, + 0xFFCF, + 0xFFD0, + 0xFFD2, + 0xFFD3, + 0xFFD4, + 0xFFD5, + 0xFFD6, + 0xFFD7, + 0xFFD8, + 0xFFDA, + 0xFFDB, + 0xFFDC, + 0xFFDD, + 0xFFE0, + 0xFFE1, + 0xFFE2, + 0xFFE3, + 0xFFE4, + 0xFFE5, + 0xFFE6, + 0xFFE7, + 0xFFE8, + 0xFFE9, + 0xFFEA, + 0xFFEB, + 0xFFEC, + 0xFFED, + 0xFFEE, + 0xFFEF, + 0x10000, + 0x1000C, + 0x1000D, + 0x10027, + 0x10028, + 0x1003B, + 0x1003C, + 0x1003E, + 0x1003F, + 0x1004E, + 0x10050, + 0x1005E, + 0x10080, + 0x100FB, + 0x10100, + 0x10103, + 0x10107, + 0x10134, + 0x10137, + 0x1018F, + 0x10190, + 0x1019D, + 0x101A0, + 0x101A1, + 0x101D0, + 0x101FE, + 0x10280, + 0x1029D, + 0x102A0, + 0x102D1, + 0x102E0, + 0x102FC, + 0x10300, + 0x10324, + 0x1032D, + 0x1034B, + 0x10350, + 0x1037B, + 0x10380, + 0x1039E, + 0x1039F, + 0x103C4, + 0x103C8, + 0x103D6, + 0x10400, + 0x10401, + 0x10402, + 0x10403, + 0x10404, + 0x10405, + 0x10406, + 0x10407, + 0x10408, + 0x10409, + 0x1040A, + 0x1040B, + 0x1040C, + 0x1040D, + 0x1040E, + 0x1040F, + 0x10410, + 0x10411, + 0x10412, + 0x10413, + 0x10414, + 0x10415, + 0x10416, + 0x10417, + 0x10418, + 0x10419, + 0x1041A, + 0x1041B, + 0x1041C, + 0x1041D, + 0x1041E, + 0x1041F, + 0x10420, + 0x10421, + 0x10422, + 0x10423, + 0x10424, + 0x10425, + 0x10426, + 0x10427, + 0x10428, + 0x1049E, + 0x104A0, + 0x104AA, + 0x104B0, + 0x104B1, + 0x104B2, + 0x104B3, + 0x104B4, + 0x104B5, + 0x104B6, + 0x104B7, + 0x104B8, + 0x104B9, + 0x104BA, + 0x104BB, + 0x104BC, + 0x104BD, + 0x104BE, + 0x104BF, + 0x104C0, + 0x104C1, + 0x104C2, + 0x104C3, + 0x104C4, + 0x104C5, + 0x104C6, + 0x104C7, + 0x104C8, + 0x104C9, + 0x104CA, + 0x104CB, + 0x104CC, + 0x104CD, + 0x104CE, + 0x104CF, + 0x104D0, + 0x104D1, + 0x104D2, + 0x104D3, + 0x104D4, + 0x104D8, + 0x104FC, + 0x10500, + 0x10528, + 0x10530, + 0x10564, + 0x1056F, + 0x10570, + 0x10571, + 0x10572, + 0x10573, + 0x10574, + 0x10575, + 0x10576, + 0x10577, + 0x10578, + 0x10579, + 0x1057A, + 0x1057B, + 0x1057C, + 0x1057D, + 0x1057E, + 0x1057F, + 0x10580, + 0x10581, + 0x10582, + 0x10583, + 0x10584, + 0x10585, + 0x10586, + 0x10587, + 0x10588, + 0x10589, + 0x1058A, + 0x1058B, + 0x1058C, + 0x1058D, + 0x1058E, + 0x1058F, + 0x10590, + 0x10591, + 0x10592, + 0x10593, + 0x10594, + 0x10595, + 0x10596, + 0x10597, + 0x105A2, + 0x105A3, + 0x105B2, + 0x105B3, + 0x105BA, + 0x105BB, + 0x105BD, + 0x105C0, + 0x105F4, + 0x10600, + 0x10737, + 0x10740, + 0x10756, + 0x10760, + 0x10768, + 0x10780, + 0x10781, + 0x10782, + 0x10783, + 0x10784, + 0x10785, + 0x10786, + 0x10787, + 0x10788, + 0x10789, + 0x1078A, + 0x1078B, + 0x1078C, + 0x1078D, + 0x1078E, + 0x1078F, + 0x10790, + 0x10791, + 0x10792, + 0x10793, + 0x10794, + 0x10795, + 0x10796, + 0x10797, + 0x10798, + 0x10799, + 0x1079A, + 0x1079B, + 0x1079C, + 0x1079D, + 0x1079E, + 0x1079F, + 0x107A0, + 0x107A1, + 0x107A2, + 0x107A3, + 0x107A4, + 0x107A5, + 0x107A6, + 0x107A7, + 0x107A8, + 0x107A9, + 0x107AA, + 0x107AB, + 0x107AC, + 0x107AD, + 0x107AE, + 0x107AF, + 0x107B0, + 0x107B1, + 0x107B2, + 0x107B3, + 0x107B4, + 0x107B5, + 0x107B6, + 0x107B7, + 0x107B8, + 0x107B9, + 0x107BA, + 0x107BB, + 0x107BC, + 0x107BD, + 0x107BE, + 0x107BF, + 0x107C0, + 0x10800, + 0x10806, + 0x10808, + 0x10809, + 0x1080A, + 0x10836, + 0x10837, + 0x10839, + 0x1083C, + 0x1083D, + 0x1083F, + 0x10856, + 0x10857, + 0x1089F, + 0x108A7, + 0x108B0, + 0x108E0, + 0x108F3, + 0x108F4, + 0x108F6, + 0x108FB, + 0x1091C, + 0x1091F, + 0x1093A, + 0x1093F, + 0x1095A, + 0x10980, + 0x109B8, + 0x109BC, + 0x109D0, + 0x109D2, + 0x10A04, + 0x10A05, + 0x10A07, + 0x10A0C, + 0x10A14, + 0x10A15, + 0x10A18, + 0x10A19, + 0x10A36, + 0x10A38, + 0x10A3B, + 0x10A3F, + 0x10A49, + 0x10A50, + 0x10A59, + 0x10A60, + 0x10AA0, + 0x10AC0, + 0x10AE7, + 0x10AEB, + 0x10AF7, + 0x10B00, + 0x10B36, + 0x10B39, + 0x10B56, + 0x10B58, + 0x10B73, + 0x10B78, + 0x10B92, + 0x10B99, + 0x10B9D, + 0x10BA9, + 0x10BB0, + 0x10C00, + 0x10C49, + 0x10C80, + 0x10C81, + 0x10C82, + 0x10C83, + 0x10C84, + 0x10C85, + 0x10C86, + 0x10C87, + 0x10C88, + 0x10C89, + 0x10C8A, + 0x10C8B, + 0x10C8C, + 0x10C8D, + 0x10C8E, + 0x10C8F, + 0x10C90, + 0x10C91, + 0x10C92, + 0x10C93, + 0x10C94, + 0x10C95, + 0x10C96, + 0x10C97, + 0x10C98, + 0x10C99, + 0x10C9A, + 0x10C9B, + 0x10C9C, + 0x10C9D, + 0x10C9E, + 0x10C9F, + 0x10CA0, + 0x10CA1, + 0x10CA2, + 0x10CA3, + 0x10CA4, + 0x10CA5, + 0x10CA6, + 0x10CA7, + 0x10CA8, + 0x10CA9, + 0x10CAA, + 0x10CAB, + 0x10CAC, + 0x10CAD, + 0x10CAE, + 0x10CAF, + 0x10CB0, + 0x10CB1, + 0x10CB2, + 0x10CB3, + 0x10CC0, + 0x10CF3, + 0x10CFA, + 0x10D28, + 0x10D30, + 0x10D3A, + 0x10D40, + 0x10D50, + 0x10D51, + 0x10D52, + 0x10D53, + 0x10D54, + 0x10D55, + 0x10D56, + 0x10D57, + 0x10D58, + 0x10D59, + 0x10D5A, + 0x10D5B, + 0x10D5C, + 0x10D5D, + 0x10D5E, + 0x10D5F, + 0x10D60, + 0x10D61, + 0x10D62, + 0x10D63, + 0x10D64, + 0x10D65, + 0x10D66, + 0x10D69, + 0x10D86, + 0x10D8E, + 0x10D90, + 0x10E60, + 0x10E7F, + 0x10E80, + 0x10EAA, + 0x10EAB, + 0x10EAE, + 0x10EB0, + 0x10EB2, + 0x10EC2, + 0x10EC8, + 0x10EC9, + 0x10EEF, + 0x10EF0, + 0x10F28, + 0x10F30, + 0x10F5A, + 0x10F70, + 0x10F8A, + 0x10FB0, + 0x10FCC, + 0x10FE0, + 0x10FF7, + 0x11000, + 0x1104E, + 0x11052, + 0x11076, + 0x1107F, + 0x110BD, + 0x110BE, + 0x110C3, + 0x110D0, + 0x110E9, + 0x110F0, + 0x110FA, + 0x11100, + 0x11135, + 0x11136, + 0x11148, + 0x11150, + 0x11177, + 0x11180, + 0x111E0, + 0x111E1, + 0x111F5, + 0x11200, + 0x11212, + 0x11213, + 0x11242, + 0x11280, + 0x11287, + 0x11288, + 0x11289, + 0x1128A, + 0x1128E, + 0x1128F, + 0x1129E, + 0x1129F, + 0x112AA, + 0x112B0, + 0x112EB, + 0x112F0, + 0x112FA, + 0x11300, + 0x11304, + 0x11305, + 0x1130D, + 0x1130F, + 0x11311, + 0x11313, + 0x11329, + 0x1132A, + 0x11331, + 0x11332, + 0x11334, + 0x11335, + 0x1133A, + 0x1133B, + 0x11345, + 0x11347, + 0x11349, + 0x1134B, + 0x1134E, + 0x11350, + 0x11351, + 0x11357, + 0x11358, + 0x1135D, + 0x11364, + 0x11366, + 0x1136D, + 0x11370, + 0x11375, + 0x11380, + 0x1138A, + 0x1138B, + 0x1138C, + 0x1138E, + 0x1138F, + 0x11390, + 0x113B6, + 0x113B7, + 0x113C1, + 0x113C2, + 0x113C3, + 0x113C5, + 0x113C6, + 0x113C7, + 0x113CB, + 0x113CC, + 0x113D6, + 0x113D7, + 0x113D9, + 0x113E1, + 0x113E3, + 0x11400, + 0x1145C, + 0x1145D, + 0x11462, + 0x11480, + 0x114C8, + 0x114D0, + 0x114DA, + 0x11580, + 0x115B6, + 0x115B8, + 0x115DE, + 0x11600, + 0x11645, + 0x11650, + 0x1165A, + 0x11660, + 0x1166D, + 0x11680, + 0x116BA, + 0x116C0, + 0x116CA, + 0x116D0, + 0x116E4, + 0x11700, + 0x1171B, + 0x1171D, + 0x1172C, + 0x11730, + 0x11747, + 0x11800, + 0x1183C, + 0x118A0, + 0x118A1, + 0x118A2, + 0x118A3, + 0x118A4, + 0x118A5, + 0x118A6, + 0x118A7, + 0x118A8, + 0x118A9, + 0x118AA, + 0x118AB, + 0x118AC, + 0x118AD, + 0x118AE, + 0x118AF, + 0x118B0, + 0x118B1, + 0x118B2, + 0x118B3, + 0x118B4, + 0x118B5, + 0x118B6, + 0x118B7, + 0x118B8, + 0x118B9, + 0x118BA, + 0x118BB, + 0x118BC, + 0x118BD, + 0x118BE, + 0x118BF, + 0x118C0, + 0x118F3, + 0x118FF, + 0x11907, + 0x11909, + 0x1190A, + 0x1190C, + 0x11914, + 0x11915, + 0x11917, + 0x11918, + 0x11936, + 0x11937, + 0x11939, + 0x1193B, + 0x11947, + 0x11950, + 0x1195A, + 0x119A0, + 0x119A8, + 0x119AA, + 0x119D8, + 0x119DA, + 0x119E5, + 0x11A00, + 0x11A48, + 0x11A50, + 0x11AA3, + 0x11AB0, + 0x11AF9, + 0x11B00, + 0x11B0B, + 0x11B60, + 0x11B68, + 0x11BC0, + 0x11BE2, + 0x11BF0, + 0x11BFA, + 0x11C00, + 0x11C09, + 0x11C0A, + 0x11C37, + 0x11C38, + 0x11C46, + 0x11C50, + 0x11C6D, + 0x11C70, + 0x11C90, + 0x11C92, + 0x11CA8, + 0x11CA9, + 0x11CB7, + 0x11D00, + 0x11D07, + 0x11D08, + 0x11D0A, + 0x11D0B, + 0x11D37, + 0x11D3A, + 0x11D3B, + 0x11D3C, + 0x11D3E, + 0x11D3F, + 0x11D48, + 0x11D50, + 0x11D5A, + 0x11D60, + 0x11D66, + 0x11D67, + 0x11D69, + 0x11D6A, + 0x11D8F, + 0x11D90, + 0x11D92, + 0x11D93, + 0x11D99, + 0x11DA0, + 0x11DAA, + 0x11DB0, + 0x11DDC, + 0x11DE0, + 0x11DEA, + 0x11DF0, + 0x11DF2, + 0x11EE0, + 0x11EF9, + 0x11F00, + 0x11F11, + 0x11F12, + 0x11F3B, + 0x11F3E, + 0x11F5B, + 0x11FB0, + 0x11FB1, + 0x11FC0, + 0x11FF2, + 0x11FFF, + 0x1239A, + 0x12400, + 0x12544, + 0x12550, + 0x12687, + 0x12F90, + 0x12FF3, + 0x13000, + 0x13430, + 0x13440, + 0x13456, + 0x13460, + 0x143FB, + 0x14400, + 0x14647, + 0x16100, + 0x1613A, + 0x16800, + 0x16A39, + 0x16A40, + 0x16A5F, + 0x16A60, + 0x16A6A, + 0x16A6E, + 0x16ABF, + 0x16AC0, + 0x16ACA, + 0x16AD0, + 0x16AEE, + 0x16AF0, + 0x16AF6, + 0x16B00, + 0x16B46, + 0x16B50, + 0x16B5A, + 0x16B5B, + 0x16B62, + 0x16B63, + 0x16B78, + 0x16B7D, + 0x16B90, + 0x16D40, + 0x16D7A, + 0x16E40, + 0x16E41, + 0x16E42, + 0x16E43, + 0x16E44, + 0x16E45, + 0x16E46, + 0x16E47, + 0x16E48, + 0x16E49, + 0x16E4A, + 0x16E4B, + 0x16E4C, + 0x16E4D, + 0x16E4E, + 0x16E4F, + 0x16E50, + 0x16E51, + 0x16E52, + 0x16E53, + 0x16E54, + 0x16E55, + 0x16E56, + 0x16E57, + 0x16E58, + 0x16E59, + 0x16E5A, + 0x16E5B, + 0x16E5C, + 0x16E5D, + 0x16E5E, + 0x16E5F, + 0x16E60, + 0x16E9B, + 0x16EA0, + 0x16EA1, + 0x16EA2, + 0x16EA3, + 0x16EA4, + 0x16EA5, + 0x16EA6, + 0x16EA7, + 0x16EA8, + 0x16EA9, + 0x16EAA, + 0x16EAB, + 0x16EAC, + 0x16EAD, + 0x16EAE, + 0x16EAF, + 0x16EB0, + 0x16EB1, + 0x16EB2, + 0x16EB3, + 0x16EB4, + 0x16EB5, + 0x16EB6, + 0x16EB7, + 0x16EB8, + 0x16EB9, + 0x16EBB, + 0x16ED4, + 0x16F00, + 0x16F4B, + 0x16F4F, + 0x16F88, + 0x16F8F, + 0x16FA0, + 0x16FE0, + 0x16FE5, + 0x16FF0, + 0x16FF7, + 0x17000, + 0x18CDB, + 0x18CFF, + 0x18D21, + 0x18D80, + 0x18DF3, + 0x18E00, + 0x19192, + 0x191A0, + 0x191D3, + 0x1AFF0, + 0x1AFF4, + 0x1AFF5, + 0x1AFFC, + 0x1AFFD, + 0x1AFFF, + 0x1B000, + 0x1B123, + 0x1B124, + 0x1B125, + 0x1B126, + 0x1B127, + 0x1B129, + 0x1B132, + 0x1B133, + 0x1B150, + 0x1B153, + 0x1B155, + 0x1B156, + 0x1B164, + 0x1B169, + 0x1B170, + 0x1B2FC, + 0x1BC00, + 0x1BC6B, + 0x1BC70, + 0x1BC7D, + 0x1BC80, + 0x1BC89, + 0x1BC90, + 0x1BC9A, + 0x1BC9C, + 0x1BCA0, + 0x1BCA4, + 0x1CC00, + 0x1CCD6, + 0x1CCD7, + 0x1CCD8, + 0x1CCD9, + 0x1CCDA, + 0x1CCDB, + 0x1CCDC, + 0x1CCDD, + 0x1CCDE, + 0x1CCDF, + 0x1CCE0, + 0x1CCE1, + 0x1CCE2, + 0x1CCE3, + 0x1CCE4, + 0x1CCE5, + 0x1CCE6, + 0x1CCE7, + 0x1CCE8, + 0x1CCE9, + 0x1CCEA, + 0x1CCEB, + 0x1CCEC, + 0x1CCED, + 0x1CCEE, + 0x1CCEF, + 0x1CCF0, + 0x1CCF1, + 0x1CCF2, + 0x1CCF3, + 0x1CCF4, + 0x1CCF5, + 0x1CCF6, + 0x1CCF7, + 0x1CCF8, + 0x1CCF9, + 0x1CCFA, + 0x1CCFD, + 0x1CD00, + 0x1CEB4, + 0x1CEBA, + 0x1CED1, + 0x1CED2, + 0x1CED5, + 0x1CEDD, + 0x1CEFE, + 0x1CF00, + 0x1CF2E, + 0x1CF30, + 0x1CF47, + 0x1CF50, + 0x1CFC4, + 0x1D000, + 0x1D0F6, + 0x1D100, + 0x1D15E, + 0x1D15F, + 0x1D160, + 0x1D161, + 0x1D162, + 0x1D163, + 0x1D164, + 0x1D165, + 0x1D173, + 0x1D17B, + 0x1D1BB, + 0x1D1BC, + 0x1D1BD, + 0x1D1BE, + 0x1D1BF, + 0x1D1C0, + 0x1D1C1, + 0x1D246, + 0x1D250, + 0x1D282, + 0x1D2C0, + 0x1D2D4, + 0x1D2E0, + 0x1D2F4, + 0x1D300, + 0x1D357, + 0x1D360, + 0x1D379, + 0x1D400, + 0x1D401, + 0x1D402, + 0x1D403, + 0x1D404, + 0x1D405, + 0x1D406, + 0x1D407, + 0x1D408, + 0x1D409, + 0x1D40A, + 0x1D40B, + 0x1D40C, + 0x1D40D, + 0x1D40E, + 0x1D40F, + 0x1D410, + 0x1D411, + 0x1D412, + 0x1D413, + 0x1D414, + 0x1D415, + 0x1D416, + 0x1D417, + 0x1D418, + 0x1D419, + 0x1D41A, + 0x1D41B, + 0x1D41C, + 0x1D41D, + 0x1D41E, + 0x1D41F, + 0x1D420, + 0x1D421, + 0x1D422, + 0x1D423, + 0x1D424, + 0x1D425, + 0x1D426, + 0x1D427, + 0x1D428, + 0x1D429, + 0x1D42A, + 0x1D42B, + 0x1D42C, + 0x1D42D, + 0x1D42E, + 0x1D42F, + 0x1D430, + 0x1D431, + 0x1D432, + 0x1D433, + 0x1D434, + 0x1D435, + 0x1D436, + 0x1D437, + 0x1D438, + 0x1D439, + 0x1D43A, + 0x1D43B, + 0x1D43C, + 0x1D43D, + 0x1D43E, + 0x1D43F, + 0x1D440, + 0x1D441, + 0x1D442, + 0x1D443, + 0x1D444, + 0x1D445, + 0x1D446, + 0x1D447, + 0x1D448, + 0x1D449, + 0x1D44A, + 0x1D44B, + 0x1D44C, + 0x1D44D, + 0x1D44E, + 0x1D44F, + 0x1D450, + 0x1D451, + 0x1D452, + 0x1D453, + 0x1D454, + 0x1D455, + 0x1D456, + 0x1D457, + 0x1D458, + 0x1D459, + 0x1D45A, + 0x1D45B, + 0x1D45C, + 0x1D45D, + 0x1D45E, + 0x1D45F, + 0x1D460, + 0x1D461, + 0x1D462, + 0x1D463, + 0x1D464, + 0x1D465, + 0x1D466, + 0x1D467, + 0x1D468, + 0x1D469, + 0x1D46A, + 0x1D46B, + 0x1D46C, + 0x1D46D, + 0x1D46E, + 0x1D46F, + 0x1D470, + 0x1D471, + 0x1D472, + 0x1D473, + 0x1D474, + 0x1D475, + 0x1D476, + 0x1D477, + 0x1D478, + 0x1D479, + 0x1D47A, + 0x1D47B, + 0x1D47C, + 0x1D47D, + 0x1D47E, + 0x1D47F, + 0x1D480, + 0x1D481, + 0x1D482, + 0x1D483, + 0x1D484, + 0x1D485, + 0x1D486, + 0x1D487, + 0x1D488, + 0x1D489, + 0x1D48A, + 0x1D48B, + 0x1D48C, + 0x1D48D, + 0x1D48E, + 0x1D48F, + 0x1D490, + 0x1D491, + 0x1D492, + 0x1D493, + 0x1D494, + 0x1D495, + 0x1D496, + 0x1D497, + 0x1D498, + 0x1D499, + 0x1D49A, + 0x1D49B, + 0x1D49C, + 0x1D49D, + 0x1D49E, + 0x1D49F, + 0x1D4A0, + 0x1D4A2, + 0x1D4A3, + 0x1D4A5, + 0x1D4A6, + 0x1D4A7, + 0x1D4A9, + 0x1D4AA, + 0x1D4AB, + 0x1D4AC, + 0x1D4AD, + 0x1D4AE, + 0x1D4AF, + 0x1D4B0, + 0x1D4B1, + 0x1D4B2, + 0x1D4B3, + 0x1D4B4, + 0x1D4B5, + 0x1D4B6, + 0x1D4B7, + 0x1D4B8, + 0x1D4B9, + 0x1D4BA, + 0x1D4BB, + 0x1D4BC, + 0x1D4BD, + 0x1D4BE, + 0x1D4BF, + 0x1D4C0, + 0x1D4C1, + 0x1D4C2, + 0x1D4C3, + 0x1D4C4, + 0x1D4C5, + 0x1D4C6, + 0x1D4C7, + 0x1D4C8, + 0x1D4C9, + 0x1D4CA, + 0x1D4CB, + 0x1D4CC, + 0x1D4CD, + 0x1D4CE, + 0x1D4CF, + 0x1D4D0, + 0x1D4D1, + 0x1D4D2, + 0x1D4D3, + 0x1D4D4, + 0x1D4D5, + 0x1D4D6, + 0x1D4D7, + 0x1D4D8, + 0x1D4D9, + 0x1D4DA, + 0x1D4DB, + 0x1D4DC, + 0x1D4DD, + 0x1D4DE, + 0x1D4DF, + 0x1D4E0, + 0x1D4E1, + 0x1D4E2, + 0x1D4E3, + 0x1D4E4, + 0x1D4E5, + 0x1D4E6, + 0x1D4E7, + 0x1D4E8, + 0x1D4E9, + 0x1D4EA, + 0x1D4EB, + 0x1D4EC, + 0x1D4ED, + 0x1D4EE, + 0x1D4EF, + 0x1D4F0, + 0x1D4F1, + 0x1D4F2, + 0x1D4F3, + 0x1D4F4, + 0x1D4F5, + 0x1D4F6, + 0x1D4F7, + 0x1D4F8, + 0x1D4F9, + 0x1D4FA, + 0x1D4FB, + 0x1D4FC, + 0x1D4FD, + 0x1D4FE, + 0x1D4FF, + 0x1D500, + 0x1D501, + 0x1D502, + 0x1D503, + 0x1D504, + 0x1D505, + 0x1D506, + 0x1D507, + 0x1D508, + 0x1D509, + 0x1D50A, + 0x1D50B, + 0x1D50D, + 0x1D50E, + 0x1D50F, + 0x1D510, + 0x1D511, + 0x1D512, + 0x1D513, + 0x1D514, + 0x1D515, + 0x1D516, + 0x1D517, + 0x1D518, + 0x1D519, + 0x1D51A, + 0x1D51B, + 0x1D51C, + 0x1D51D, + 0x1D51E, + 0x1D51F, + 0x1D520, + 0x1D521, + 0x1D522, + 0x1D523, + 0x1D524, + 0x1D525, + 0x1D526, + 0x1D527, + 0x1D528, + 0x1D529, + 0x1D52A, + 0x1D52B, + 0x1D52C, + 0x1D52D, + 0x1D52E, + 0x1D52F, + 0x1D530, + 0x1D531, + 0x1D532, + 0x1D533, + 0x1D534, + 0x1D535, + 0x1D536, + 0x1D537, + 0x1D538, + 0x1D539, + 0x1D53A, + 0x1D53B, + 0x1D53C, + 0x1D53D, + 0x1D53E, + 0x1D53F, + 0x1D540, + 0x1D541, + 0x1D542, + 0x1D543, + 0x1D544, + 0x1D545, + 0x1D546, + 0x1D547, + 0x1D54A, + 0x1D54B, + 0x1D54C, + 0x1D54D, + 0x1D54E, + 0x1D54F, + 0x1D550, + 0x1D551, + 0x1D552, + 0x1D553, + 0x1D554, + 0x1D555, + 0x1D556, + 0x1D557, + 0x1D558, + 0x1D559, + 0x1D55A, + 0x1D55B, + 0x1D55C, + 0x1D55D, + 0x1D55E, + 0x1D55F, + 0x1D560, + 0x1D561, + 0x1D562, + 0x1D563, + 0x1D564, + 0x1D565, + 0x1D566, + 0x1D567, + 0x1D568, + 0x1D569, + 0x1D56A, + 0x1D56B, + 0x1D56C, + 0x1D56D, + 0x1D56E, + 0x1D56F, + 0x1D570, + 0x1D571, + 0x1D572, + 0x1D573, + 0x1D574, + 0x1D575, + 0x1D576, + 0x1D577, + 0x1D578, + 0x1D579, + 0x1D57A, + 0x1D57B, + 0x1D57C, + 0x1D57D, + 0x1D57E, + 0x1D57F, + 0x1D580, + 0x1D581, + 0x1D582, + 0x1D583, + 0x1D584, + 0x1D585, + 0x1D586, + 0x1D587, + 0x1D588, + 0x1D589, + 0x1D58A, + 0x1D58B, + 0x1D58C, + 0x1D58D, + 0x1D58E, + 0x1D58F, + 0x1D590, + 0x1D591, + 0x1D592, + 0x1D593, + 0x1D594, + 0x1D595, + 0x1D596, + 0x1D597, + 0x1D598, + 0x1D599, + 0x1D59A, + 0x1D59B, + 0x1D59C, + 0x1D59D, + 0x1D59E, + 0x1D59F, + 0x1D5A0, + 0x1D5A1, + 0x1D5A2, + 0x1D5A3, + 0x1D5A4, + 0x1D5A5, + 0x1D5A6, + 0x1D5A7, + 0x1D5A8, + 0x1D5A9, + 0x1D5AA, + 0x1D5AB, + 0x1D5AC, + 0x1D5AD, + 0x1D5AE, + 0x1D5AF, + 0x1D5B0, + 0x1D5B1, + 0x1D5B2, + 0x1D5B3, + 0x1D5B4, + 0x1D5B5, + 0x1D5B6, + 0x1D5B7, + 0x1D5B8, + 0x1D5B9, + 0x1D5BA, + 0x1D5BB, + 0x1D5BC, + 0x1D5BD, + 0x1D5BE, + 0x1D5BF, + 0x1D5C0, + 0x1D5C1, + 0x1D5C2, + 0x1D5C3, + 0x1D5C4, + 0x1D5C5, + 0x1D5C6, + 0x1D5C7, + 0x1D5C8, + 0x1D5C9, + 0x1D5CA, + 0x1D5CB, + 0x1D5CC, + 0x1D5CD, + 0x1D5CE, + 0x1D5CF, + 0x1D5D0, + 0x1D5D1, + 0x1D5D2, + 0x1D5D3, + 0x1D5D4, + 0x1D5D5, + 0x1D5D6, + 0x1D5D7, + 0x1D5D8, + 0x1D5D9, + 0x1D5DA, + 0x1D5DB, + 0x1D5DC, + 0x1D5DD, + 0x1D5DE, + 0x1D5DF, + 0x1D5E0, + 0x1D5E1, + 0x1D5E2, + 0x1D5E3, + 0x1D5E4, + 0x1D5E5, + 0x1D5E6, + 0x1D5E7, + 0x1D5E8, + 0x1D5E9, + 0x1D5EA, + 0x1D5EB, + 0x1D5EC, + 0x1D5ED, + 0x1D5EE, + 0x1D5EF, + 0x1D5F0, + 0x1D5F1, + 0x1D5F2, + 0x1D5F3, + 0x1D5F4, + 0x1D5F5, + 0x1D5F6, + 0x1D5F7, + 0x1D5F8, + 0x1D5F9, + 0x1D5FA, + 0x1D5FB, + 0x1D5FC, + 0x1D5FD, + 0x1D5FE, + 0x1D5FF, + 0x1D600, + 0x1D601, + 0x1D602, + 0x1D603, + 0x1D604, + 0x1D605, + 0x1D606, + 0x1D607, + 0x1D608, + 0x1D609, + 0x1D60A, + 0x1D60B, + 0x1D60C, + 0x1D60D, + 0x1D60E, + 0x1D60F, + 0x1D610, + 0x1D611, + 0x1D612, + 0x1D613, + 0x1D614, + 0x1D615, + 0x1D616, + 0x1D617, + 0x1D618, + 0x1D619, + 0x1D61A, + 0x1D61B, + 0x1D61C, + 0x1D61D, + 0x1D61E, + 0x1D61F, + 0x1D620, + 0x1D621, + 0x1D622, + 0x1D623, + 0x1D624, + 0x1D625, + 0x1D626, + 0x1D627, + 0x1D628, + 0x1D629, + 0x1D62A, + 0x1D62B, + 0x1D62C, + 0x1D62D, + 0x1D62E, + 0x1D62F, + 0x1D630, + 0x1D631, + 0x1D632, + 0x1D633, + 0x1D634, + 0x1D635, + 0x1D636, + 0x1D637, + 0x1D638, + 0x1D639, + 0x1D63A, + 0x1D63B, + 0x1D63C, + 0x1D63D, + 0x1D63E, + 0x1D63F, + 0x1D640, + 0x1D641, + 0x1D642, + 0x1D643, + 0x1D644, + 0x1D645, + 0x1D646, + 0x1D647, + 0x1D648, + 0x1D649, + 0x1D64A, + 0x1D64B, + 0x1D64C, + 0x1D64D, + 0x1D64E, + 0x1D64F, + 0x1D650, + 0x1D651, + 0x1D652, + 0x1D653, + 0x1D654, + 0x1D655, + 0x1D656, + 0x1D657, + 0x1D658, + 0x1D659, + 0x1D65A, + 0x1D65B, + 0x1D65C, + 0x1D65D, + 0x1D65E, + 0x1D65F, + 0x1D660, + 0x1D661, + 0x1D662, + 0x1D663, + 0x1D664, + 0x1D665, + 0x1D666, + 0x1D667, + 0x1D668, + 0x1D669, + 0x1D66A, + 0x1D66B, + 0x1D66C, + 0x1D66D, + 0x1D66E, + 0x1D66F, + 0x1D670, + 0x1D671, + 0x1D672, + 0x1D673, + 0x1D674, + 0x1D675, + 0x1D676, + 0x1D677, + 0x1D678, + 0x1D679, + 0x1D67A, + 0x1D67B, + 0x1D67C, + 0x1D67D, + 0x1D67E, + 0x1D67F, + 0x1D680, + 0x1D681, + 0x1D682, + 0x1D683, + 0x1D684, + 0x1D685, + 0x1D686, + 0x1D687, + 0x1D688, + 0x1D689, + 0x1D68A, + 0x1D68B, + 0x1D68C, + 0x1D68D, + 0x1D68E, + 0x1D68F, + 0x1D690, + 0x1D691, + 0x1D692, + 0x1D693, + 0x1D694, + 0x1D695, + 0x1D696, + 0x1D697, + 0x1D698, + 0x1D699, + 0x1D69A, + 0x1D69B, + 0x1D69C, + 0x1D69D, + 0x1D69E, + 0x1D69F, + 0x1D6A0, + 0x1D6A1, + 0x1D6A2, + 0x1D6A3, + 0x1D6A4, + 0x1D6A5, + 0x1D6A6, + 0x1D6A7, + 0x1D6A8, + 0x1D6A9, + 0x1D6AA, + 0x1D6AB, + 0x1D6AC, + 0x1D6AD, + 0x1D6AE, + 0x1D6AF, + 0x1D6B0, + 0x1D6B1, + 0x1D6B2, + 0x1D6B3, + 0x1D6B4, + 0x1D6B5, + 0x1D6B6, + 0x1D6B7, + 0x1D6B8, + 0x1D6B9, + 0x1D6BA, + 0x1D6BB, + 0x1D6BC, + 0x1D6BD, + 0x1D6BE, + 0x1D6BF, + 0x1D6C0, + 0x1D6C1, + 0x1D6C2, + 0x1D6C3, + 0x1D6C4, + 0x1D6C5, + 0x1D6C6, + 0x1D6C7, + 0x1D6C8, + 0x1D6C9, + 0x1D6CA, + 0x1D6CB, + 0x1D6CC, + 0x1D6CD, + 0x1D6CE, + 0x1D6CF, + 0x1D6D0, + 0x1D6D1, + 0x1D6D2, + 0x1D6D3, + 0x1D6D5, + 0x1D6D6, + 0x1D6D7, + 0x1D6D8, + 0x1D6D9, + 0x1D6DA, + 0x1D6DB, + 0x1D6DC, + 0x1D6DD, + 0x1D6DE, + 0x1D6DF, + 0x1D6E0, + 0x1D6E1, + 0x1D6E2, + 0x1D6E3, + 0x1D6E4, + 0x1D6E5, + 0x1D6E6, + 0x1D6E7, + 0x1D6E8, + 0x1D6E9, + 0x1D6EA, + 0x1D6EB, + 0x1D6EC, + 0x1D6ED, + 0x1D6EE, + 0x1D6EF, + 0x1D6F0, + 0x1D6F1, + 0x1D6F2, + 0x1D6F3, + 0x1D6F4, + 0x1D6F5, + 0x1D6F6, + 0x1D6F7, + 0x1D6F8, + 0x1D6F9, + 0x1D6FA, + 0x1D6FB, + 0x1D6FC, + 0x1D6FD, + 0x1D6FE, + 0x1D6FF, + 0x1D700, + 0x1D701, + 0x1D702, + 0x1D703, + 0x1D704, + 0x1D705, + 0x1D706, + 0x1D707, + 0x1D708, + 0x1D709, + 0x1D70A, + 0x1D70B, + 0x1D70C, + 0x1D70D, + 0x1D70F, + 0x1D710, + 0x1D711, + 0x1D712, + 0x1D713, + 0x1D714, + 0x1D715, + 0x1D716, + 0x1D717, + 0x1D718, + 0x1D719, + 0x1D71A, + 0x1D71B, + 0x1D71C, + 0x1D71D, + 0x1D71E, + 0x1D71F, + 0x1D720, + 0x1D721, + 0x1D722, + 0x1D723, + 0x1D724, + 0x1D725, + 0x1D726, + 0x1D727, + 0x1D728, + 0x1D729, + 0x1D72A, + 0x1D72B, + 0x1D72C, + 0x1D72D, + 0x1D72E, + 0x1D72F, + 0x1D730, + 0x1D731, + 0x1D732, + 0x1D733, + 0x1D734, + 0x1D735, + 0x1D736, + 0x1D737, + 0x1D738, + 0x1D739, + 0x1D73A, + 0x1D73B, + 0x1D73C, + 0x1D73D, + 0x1D73E, + 0x1D73F, + 0x1D740, + 0x1D741, + 0x1D742, + 0x1D743, + 0x1D744, + 0x1D745, + 0x1D746, + 0x1D747, + 0x1D749, + 0x1D74A, + 0x1D74B, + 0x1D74C, + 0x1D74D, + 0x1D74E, + 0x1D74F, + 0x1D750, + 0x1D751, + 0x1D752, + 0x1D753, + 0x1D754, + 0x1D755, + 0x1D756, + 0x1D757, + 0x1D758, + 0x1D759, + 0x1D75A, + 0x1D75B, + 0x1D75C, + 0x1D75D, + 0x1D75E, + 0x1D75F, + 0x1D760, + 0x1D761, + 0x1D762, + 0x1D763, + 0x1D764, + 0x1D765, + 0x1D766, + 0x1D767, + 0x1D768, + 0x1D769, + 0x1D76A, + 0x1D76B, + 0x1D76C, + 0x1D76D, + 0x1D76E, + 0x1D76F, + 0x1D770, + 0x1D771, + 0x1D772, + 0x1D773, + 0x1D774, + 0x1D775, + 0x1D776, + 0x1D777, + 0x1D778, + 0x1D779, + 0x1D77A, + 0x1D77B, + 0x1D77C, + 0x1D77D, + 0x1D77E, + 0x1D77F, + 0x1D780, + 0x1D781, + 0x1D783, + 0x1D784, + 0x1D785, + 0x1D786, + 0x1D787, + 0x1D788, + 0x1D789, + 0x1D78A, + 0x1D78B, + 0x1D78C, + 0x1D78D, + 0x1D78E, + 0x1D78F, + 0x1D790, + 0x1D791, + 0x1D792, + 0x1D793, + 0x1D794, + 0x1D795, + 0x1D796, + 0x1D797, + 0x1D798, + 0x1D799, + 0x1D79A, + 0x1D79B, + 0x1D79C, + 0x1D79D, + 0x1D79E, + 0x1D79F, + 0x1D7A0, + 0x1D7A1, + 0x1D7A2, + 0x1D7A3, + 0x1D7A4, + 0x1D7A5, + 0x1D7A6, + 0x1D7A7, + 0x1D7A8, + 0x1D7A9, + 0x1D7AA, + 0x1D7AB, + 0x1D7AC, + 0x1D7AD, + 0x1D7AE, + 0x1D7AF, + 0x1D7B0, + 0x1D7B1, + 0x1D7B2, + 0x1D7B3, + 0x1D7B4, + 0x1D7B5, + 0x1D7B6, + 0x1D7B7, + 0x1D7B8, + 0x1D7B9, + 0x1D7BA, + 0x1D7BB, + 0x1D7BD, + 0x1D7BE, + 0x1D7BF, + 0x1D7C0, + 0x1D7C1, + 0x1D7C2, + 0x1D7C3, + 0x1D7C4, + 0x1D7C5, + 0x1D7C6, + 0x1D7C7, + 0x1D7C8, + 0x1D7C9, + 0x1D7CA, + 0x1D7CC, + 0x1D7CE, + 0x1D7CF, + 0x1D7D0, + 0x1D7D1, + 0x1D7D2, + 0x1D7D3, + 0x1D7D4, + 0x1D7D5, + 0x1D7D6, + 0x1D7D7, + 0x1D7D8, + 0x1D7D9, + 0x1D7DA, + 0x1D7DB, + 0x1D7DC, + 0x1D7DD, + 0x1D7DE, + 0x1D7DF, + 0x1D7E0, + 0x1D7E1, + 0x1D7E2, + 0x1D7E3, + 0x1D7E4, + 0x1D7E5, + 0x1D7E6, + 0x1D7E7, + 0x1D7E8, + 0x1D7E9, + 0x1D7EA, + 0x1D7EB, + 0x1D7EC, + 0x1D7ED, + 0x1D7EE, + 0x1D7EF, + 0x1D7F0, + 0x1D7F1, + 0x1D7F2, + 0x1D7F3, + 0x1D7F4, + 0x1D7F5, + 0x1D7F6, + 0x1D7F7, + 0x1D7F8, + 0x1D7F9, + 0x1D7FA, + 0x1D7FB, + 0x1D7FC, + 0x1D7FD, + 0x1D7FE, + 0x1D7FF, + 0x1D800, + 0x1DA8C, + 0x1DA9B, + 0x1DAA0, + 0x1DAA1, + 0x1DAB0, + 0x1DB00, + 0x1DB1D, + 0x1DF00, + 0x1DF40, + 0x1DF41, + 0x1DF48, + 0x1DF49, + 0x1DF4A, + 0x1DF4B, + 0x1DF4D, + 0x1DF4E, + 0x1DF51, + 0x1DF52, + 0x1DF68, + 0x1DF69, + 0x1DF6A, + 0x1DF6B, + 0x1DF6C, + 0x1DF6D, + 0x1DF6E, + 0x1DF6F, + 0x1DF72, + 0x1DF73, + 0x1DF74, + 0x1DF75, + 0x1DF76, + 0x1DF77, + 0x1DF78, + 0x1DF79, + 0x1DF7A, + 0x1DF7B, + 0x1DF7C, + 0x1DF7D, + 0x1DF7E, + 0x1DF7F, + 0x1DF82, + 0x1DF90, + 0x1DF95, + 0x1DF96, + 0x1DF97, + 0x1DFCD, + 0x1DFCE, + 0x1DFCF, + 0x1DFD0, + 0x1DFD1, + 0x1DFD2, + 0x1DFD3, + 0x1DFD4, + 0x1DFD5, + 0x1DFD6, + 0x1DFD7, + 0x1DFD8, + 0x1DFD9, + 0x1DFDA, + 0x1DFDB, + 0x1DFDC, + 0x1DFDD, + 0x1DFDE, + 0x1DFDF, + 0x1DFE0, + 0x1DFE1, + 0x1DFE2, + 0x1DFE3, + 0x1DFE4, + 0x1DFE5, + 0x1DFE6, + 0x1DFE7, + 0x1DFE8, + 0x1DFE9, + 0x1DFEA, + 0x1DFEB, + 0x1DFEC, + 0x1DFED, + 0x1DFEE, + 0x1DFEF, + 0x1DFF0, + 0x1DFF1, + 0x1DFF2, + 0x1DFF3, + 0x1DFF4, + 0x1DFF5, + 0x1DFF6, + 0x1DFF7, + 0x1DFF8, + 0x1DFF9, + 0x1DFFA, + 0x1DFFB, + 0x1DFFC, + 0x1DFFD, + 0x1DFFE, + 0x1DFFF, + 0x1E000, + 0x1E007, + 0x1E008, + 0x1E019, + 0x1E01B, + 0x1E022, + 0x1E023, + 0x1E025, + 0x1E026, + 0x1E02B, + 0x1E030, + 0x1E031, + 0x1E032, + 0x1E033, + 0x1E034, + 0x1E035, + 0x1E036, + 0x1E037, + 0x1E038, + 0x1E039, + 0x1E03A, + 0x1E03B, + 0x1E03C, + 0x1E03D, + 0x1E03E, + 0x1E03F, + 0x1E040, + 0x1E041, + 0x1E042, + 0x1E043, + 0x1E044, + 0x1E045, + 0x1E046, + 0x1E047, + 0x1E048, + 0x1E049, + 0x1E04A, + 0x1E04B, + 0x1E04C, + 0x1E04D, + 0x1E04E, + 0x1E04F, + 0x1E050, + 0x1E051, + 0x1E052, + 0x1E053, + 0x1E054, + 0x1E055, + 0x1E056, + 0x1E057, + 0x1E058, + 0x1E059, + 0x1E05A, + 0x1E05B, + 0x1E05C, + 0x1E05D, + 0x1E05E, + 0x1E05F, + 0x1E060, + 0x1E061, + 0x1E062, + 0x1E063, + 0x1E064, + 0x1E065, + 0x1E066, + 0x1E067, + 0x1E068, + 0x1E069, + 0x1E06A, + 0x1E06B, + 0x1E06C, + 0x1E06D, + 0x1E06E, + 0x1E08F, + 0x1E090, + 0x1E100, + 0x1E12D, + 0x1E130, + 0x1E13E, + 0x1E140, + 0x1E14A, + 0x1E14E, + 0x1E150, + 0x1E290, + 0x1E2AF, + 0x1E2C0, + 0x1E2FA, + 0x1E2FF, + 0x1E300, + 0x1E4D0, + 0x1E4FA, + 0x1E5D0, + 0x1E5FB, + 0x1E5FF, + 0x1E600, + 0x1E6C0, + 0x1E6DF, + 0x1E6E0, + 0x1E6F6, + 0x1E6FE, + 0x1E700, + 0x1E7E0, + 0x1E7E7, + 0x1E7E8, + 0x1E7EC, + 0x1E7ED, + 0x1E7EF, + 0x1E7F0, + 0x1E7FF, + 0x1E800, + 0x1E8C5, + 0x1E8C7, + 0x1E8D7, + 0x1E900, + 0x1E901, + 0x1E902, + 0x1E903, + 0x1E904, + 0x1E905, + 0x1E906, + 0x1E907, + 0x1E908, + 0x1E909, + 0x1E90A, + 0x1E90B, + 0x1E90C, + 0x1E90D, + 0x1E90E, + 0x1E90F, + 0x1E910, + 0x1E911, + 0x1E912, + 0x1E913, + 0x1E914, + 0x1E915, + 0x1E916, + 0x1E917, + 0x1E918, + 0x1E919, + 0x1E91A, + 0x1E91B, + 0x1E91C, + 0x1E91D, + 0x1E91E, + 0x1E91F, + 0x1E920, + 0x1E921, + 0x1E922, + 0x1E94C, + 0x1E950, + 0x1E95A, + 0x1E95E, + 0x1E960, + 0x1EC71, + 0x1ECB5, + 0x1ED01, + 0x1ED3E, + 0x1EE00, + 0x1EE01, + 0x1EE02, + 0x1EE03, + 0x1EE04, + 0x1EE05, + 0x1EE06, + 0x1EE07, + 0x1EE08, + 0x1EE09, + 0x1EE0A, + 0x1EE0B, + 0x1EE0C, + 0x1EE0D, + 0x1EE0E, + 0x1EE0F, + 0x1EE10, + 0x1EE11, + 0x1EE12, + 0x1EE13, + 0x1EE14, + 0x1EE15, + 0x1EE16, + 0x1EE17, + 0x1EE18, + 0x1EE19, + 0x1EE1A, + 0x1EE1B, + 0x1EE1C, + 0x1EE1D, + 0x1EE1E, + 0x1EE1F, + 0x1EE20, + 0x1EE21, + 0x1EE22, + 0x1EE23, + 0x1EE24, + 0x1EE25, + 0x1EE27, + 0x1EE28, + 0x1EE29, + 0x1EE2A, + 0x1EE2B, + 0x1EE2C, + 0x1EE2D, + 0x1EE2E, + 0x1EE2F, + 0x1EE30, + 0x1EE31, + 0x1EE32, + 0x1EE33, + 0x1EE34, + 0x1EE35, + 0x1EE36, + 0x1EE37, + 0x1EE38, + 0x1EE39, + 0x1EE3A, + 0x1EE3B, + 0x1EE3C, + 0x1EE42, + 0x1EE43, + 0x1EE47, + 0x1EE48, + 0x1EE49, + 0x1EE4A, + 0x1EE4B, + 0x1EE4C, + 0x1EE4D, + 0x1EE4E, + 0x1EE4F, + 0x1EE50, + 0x1EE51, + 0x1EE52, + 0x1EE53, + 0x1EE54, + 0x1EE55, + 0x1EE57, + 0x1EE58, + 0x1EE59, + 0x1EE5A, + 0x1EE5B, + 0x1EE5C, + 0x1EE5D, + 0x1EE5E, + 0x1EE5F, + 0x1EE60, + 0x1EE61, + 0x1EE62, + 0x1EE63, + 0x1EE64, + 0x1EE65, + 0x1EE67, + 0x1EE68, + 0x1EE69, + 0x1EE6A, + 0x1EE6B, + 0x1EE6C, + 0x1EE6D, + 0x1EE6E, + 0x1EE6F, + 0x1EE70, + 0x1EE71, + 0x1EE72, + 0x1EE73, + 0x1EE74, + 0x1EE75, + 0x1EE76, + 0x1EE77, + 0x1EE78, + 0x1EE79, + 0x1EE7A, + 0x1EE7B, + 0x1EE7C, + 0x1EE7D, + 0x1EE7E, + 0x1EE7F, + 0x1EE80, + 0x1EE81, + 0x1EE82, + 0x1EE83, + 0x1EE84, + 0x1EE85, + 0x1EE86, + 0x1EE87, + 0x1EE88, + 0x1EE89, + 0x1EE8A, + 0x1EE8B, + 0x1EE8C, + 0x1EE8D, + 0x1EE8E, + 0x1EE8F, + 0x1EE90, + 0x1EE91, + 0x1EE92, + 0x1EE93, + 0x1EE94, + 0x1EE95, + 0x1EE96, + 0x1EE97, + 0x1EE98, + 0x1EE99, + 0x1EE9A, + 0x1EE9B, + 0x1EE9C, + 0x1EEA1, + 0x1EEA2, + 0x1EEA3, + 0x1EEA4, + 0x1EEA5, + 0x1EEA6, + 0x1EEA7, + 0x1EEA8, + 0x1EEA9, + 0x1EEAA, + 0x1EEAB, + 0x1EEAC, + 0x1EEAD, + 0x1EEAE, + 0x1EEAF, + 0x1EEB0, + 0x1EEB1, + 0x1EEB2, + 0x1EEB3, + 0x1EEB4, + 0x1EEB5, + 0x1EEB6, + 0x1EEB7, + 0x1EEB8, + 0x1EEB9, + 0x1EEBA, + 0x1EEBB, + 0x1EEBC, + 0x1EEF0, + 0x1EEF2, + 0x1F000, + 0x1F02C, + 0x1F030, + 0x1F094, + 0x1F0A0, + 0x1F0AF, + 0x1F0B1, + 0x1F0C0, + 0x1F0C1, + 0x1F0D0, + 0x1F0D1, + 0x1F0F6, + 0x1F101, + 0x1F102, + 0x1F103, + 0x1F104, + 0x1F105, + 0x1F106, + 0x1F107, + 0x1F108, + 0x1F109, + 0x1F10A, + 0x1F10B, + 0x1F110, + 0x1F111, + 0x1F112, + 0x1F113, + 0x1F114, + 0x1F115, + 0x1F116, + 0x1F117, + 0x1F118, + 0x1F119, + 0x1F11A, + 0x1F11B, + 0x1F11C, + 0x1F11D, + 0x1F11E, + 0x1F11F, + 0x1F120, + 0x1F121, + 0x1F122, + 0x1F123, + 0x1F124, + 0x1F125, + 0x1F126, + 0x1F127, + 0x1F128, + 0x1F129, + 0x1F12A, + 0x1F12B, + 0x1F12C, + 0x1F12D, + 0x1F12E, + 0x1F12F, + 0x1F130, + 0x1F131, + 0x1F132, + 0x1F133, + 0x1F134, + 0x1F135, + 0x1F136, + 0x1F137, + 0x1F138, + 0x1F139, + 0x1F13A, + 0x1F13B, + 0x1F13C, + 0x1F13D, + 0x1F13E, + 0x1F13F, + 0x1F140, + 0x1F141, + 0x1F142, + 0x1F143, + 0x1F144, + 0x1F145, + 0x1F146, + 0x1F147, + 0x1F148, + 0x1F149, + 0x1F14A, + 0x1F14B, + 0x1F14C, + 0x1F14D, + 0x1F14E, + 0x1F14F, + 0x1F150, + 0x1F16A, + 0x1F16B, + 0x1F16C, + 0x1F16D, + 0x1F190, + 0x1F191, + 0x1F1AF, + 0x1F1E6, + 0x1F200, + 0x1F201, + 0x1F202, + 0x1F203, + 0x1F210, + 0x1F211, + 0x1F212, + 0x1F213, + 0x1F214, + 0x1F215, + 0x1F216, + 0x1F217, + 0x1F218, + 0x1F219, + 0x1F21A, + 0x1F21B, + 0x1F21C, + 0x1F21D, + 0x1F21E, + 0x1F21F, + 0x1F220, + 0x1F221, + 0x1F222, + 0x1F223, + 0x1F224, + 0x1F225, + 0x1F226, + 0x1F227, + 0x1F228, + 0x1F229, + 0x1F22A, + 0x1F22B, + 0x1F22C, + 0x1F22D, + 0x1F22E, + 0x1F22F, + 0x1F230, + 0x1F231, + 0x1F232, + 0x1F233, + 0x1F234, + 0x1F235, + 0x1F236, + 0x1F237, + 0x1F238, + 0x1F239, + 0x1F23A, + 0x1F23B, + 0x1F23C, + 0x1F240, + 0x1F241, + 0x1F242, + 0x1F243, + 0x1F244, + 0x1F245, + 0x1F246, + 0x1F247, + 0x1F248, + 0x1F249, + 0x1F250, + 0x1F251, + 0x1F252, + 0x1F260, + 0x1F266, + 0x1F300, + 0x1F6DA, + 0x1F6DC, + 0x1F6ED, + 0x1F6F0, + 0x1F6FD, + 0x1F700, + 0x1F7DC, + 0x1F7E0, + 0x1F7EC, + 0x1F7F0, + 0x1F80C, + 0x1F810, + 0x1F848, + 0x1F850, + 0x1F85A, + 0x1F860, + 0x1F888, + 0x1F890, + 0x1F8AE, + 0x1F8B0, + 0x1F8BC, + 0x1F8C0, + 0x1F8C2, + 0x1F8D0, + 0x1F8D9, + 0x1F900, + 0x1FA58, + 0x1FA60, + 0x1FA6E, + 0x1FA70, + 0x1FA7D, + 0x1FA80, + 0x1FAC7, + 0x1FAC8, + 0x1FAC9, + 0x1FACC, + 0x1FADE, + 0x1FADF, + 0x1FAEC, + 0x1FAEF, + 0x1FAFB, + 0x1FB00, + 0x1FB93, + 0x1FB94, + 0x1FBF0, + 0x1FBF1, + 0x1FBF2, + 0x1FBF3, + 0x1FBF4, + 0x1FBF5, + 0x1FBF6, + 0x1FBF7, + 0x1FBF8, + 0x1FBF9, + 0x1FBFA, + 0x1FBFB, + 0x20000, + 0x2A6E0, + 0x2A700, + 0x2B81F, + 0x2B820, + 0x2CEAE, + 0x2CEB0, + 0x2EBE1, + 0x2EBF0, + 0x2EE5E, + 0x2F800, + 0x2F801, + 0x2F802, + 0x2F803, + 0x2F804, + 0x2F805, + 0x2F806, + 0x2F807, + 0x2F808, + 0x2F809, + 0x2F80A, + 0x2F80B, + 0x2F80C, + 0x2F80D, + 0x2F80E, + 0x2F80F, + 0x2F810, + 0x2F811, + 0x2F812, + 0x2F813, + 0x2F814, + 0x2F815, + 0x2F816, + 0x2F817, + 0x2F818, + 0x2F819, + 0x2F81A, + 0x2F81B, + 0x2F81C, + 0x2F81D, + 0x2F81E, + 0x2F81F, + 0x2F820, + 0x2F821, + 0x2F822, + 0x2F823, + 0x2F824, + 0x2F825, + 0x2F826, + 0x2F827, + 0x2F828, + 0x2F829, + 0x2F82A, + 0x2F82B, + 0x2F82C, + 0x2F82D, + 0x2F82E, + 0x2F82F, + 0x2F830, + 0x2F831, + 0x2F834, + 0x2F835, + 0x2F836, + 0x2F837, + 0x2F838, + 0x2F839, + 0x2F83A, + 0x2F83B, + 0x2F83C, + 0x2F83D, + 0x2F83E, + 0x2F83F, + 0x2F840, + 0x2F841, + 0x2F842, + 0x2F843, + 0x2F844, + 0x2F845, + 0x2F847, + 0x2F848, + 0x2F849, + 0x2F84A, + 0x2F84B, + 0x2F84C, + 0x2F84D, + 0x2F84E, + 0x2F84F, + 0x2F850, + 0x2F851, + 0x2F852, + 0x2F853, + 0x2F854, + 0x2F855, + 0x2F856, + 0x2F857, + 0x2F858, + 0x2F859, + 0x2F85A, + 0x2F85B, + 0x2F85C, + 0x2F85D, + 0x2F85E, + 0x2F85F, + 0x2F860, + 0x2F861, + 0x2F862, + 0x2F863, + 0x2F864, + 0x2F865, + 0x2F866, + 0x2F867, + 0x2F868, + 0x2F869, + 0x2F86A, + 0x2F86C, + 0x2F86D, + 0x2F86E, + 0x2F86F, + 0x2F870, + 0x2F871, + 0x2F872, + 0x2F873, + 0x2F874, + 0x2F875, + 0x2F876, + 0x2F877, + 0x2F878, + 0x2F879, + 0x2F87A, + 0x2F87B, + 0x2F87C, + 0x2F87D, + 0x2F87E, + 0x2F87F, + 0x2F880, + 0x2F881, + 0x2F882, + 0x2F883, + 0x2F884, + 0x2F885, + 0x2F886, + 0x2F887, + 0x2F888, + 0x2F889, + 0x2F88A, + 0x2F88B, + 0x2F88C, + 0x2F88D, + 0x2F88E, + 0x2F88F, + 0x2F890, + 0x2F891, + 0x2F893, + 0x2F894, + 0x2F896, + 0x2F897, + 0x2F898, + 0x2F899, + 0x2F89A, + 0x2F89B, + 0x2F89C, + 0x2F89D, + 0x2F89E, + 0x2F89F, + 0x2F8A0, + 0x2F8A1, + 0x2F8A2, + 0x2F8A3, + 0x2F8A4, + 0x2F8A5, + 0x2F8A6, + 0x2F8A7, + 0x2F8A8, + 0x2F8A9, + 0x2F8AA, + 0x2F8AB, + 0x2F8AC, + 0x2F8AD, + 0x2F8AE, + 0x2F8AF, + 0x2F8B0, + 0x2F8B1, + 0x2F8B2, + 0x2F8B3, + 0x2F8B4, + 0x2F8B5, + 0x2F8B6, + 0x2F8B7, + 0x2F8B8, + 0x2F8B9, + 0x2F8BA, + 0x2F8BB, + 0x2F8BC, + 0x2F8BD, + 0x2F8BE, + 0x2F8BF, + 0x2F8C0, + 0x2F8C1, + 0x2F8C2, + 0x2F8C3, + 0x2F8C4, + 0x2F8C5, + 0x2F8C6, + 0x2F8C7, + 0x2F8C8, + 0x2F8C9, + 0x2F8CA, + 0x2F8CB, + 0x2F8CC, + 0x2F8CD, + 0x2F8CE, + 0x2F8CF, + 0x2F8D0, + 0x2F8D1, + 0x2F8D2, + 0x2F8D3, + 0x2F8D4, + 0x2F8D5, + 0x2F8D6, + 0x2F8D7, + 0x2F8D8, + 0x2F8D9, + 0x2F8DA, + 0x2F8DB, + 0x2F8DC, + 0x2F8DD, + 0x2F8DE, + 0x2F8DF, + 0x2F8E0, + 0x2F8E1, + 0x2F8E2, + 0x2F8E3, + 0x2F8E4, + 0x2F8E5, + 0x2F8E6, + 0x2F8E7, + 0x2F8E8, + 0x2F8E9, + 0x2F8EA, + 0x2F8EB, + 0x2F8EC, + 0x2F8ED, + 0x2F8EE, + 0x2F8EF, + 0x2F8F0, + 0x2F8F1, + 0x2F8F2, + 0x2F8F3, + 0x2F8F4, + 0x2F8F5, + 0x2F8F6, + 0x2F8F7, + 0x2F8F8, + 0x2F8F9, + 0x2F8FA, + 0x2F8FB, + 0x2F8FC, + 0x2F8FD, + 0x2F8FE, + 0x2F8FF, + 0x2F900, + 0x2F901, + 0x2F902, + 0x2F903, + 0x2F904, + 0x2F905, + 0x2F906, + 0x2F907, + 0x2F908, + 0x2F909, + 0x2F90A, + 0x2F90B, + 0x2F90C, + 0x2F90D, + 0x2F90E, + 0x2F90F, + 0x2F910, + 0x2F911, + 0x2F912, + 0x2F913, + 0x2F914, + 0x2F915, + 0x2F916, + 0x2F917, + 0x2F918, + 0x2F919, + 0x2F91A, + 0x2F91B, + 0x2F91C, + 0x2F91D, + 0x2F91E, + 0x2F91F, + 0x2F920, + 0x2F921, + 0x2F922, + 0x2F923, + 0x2F924, + 0x2F925, + 0x2F926, + 0x2F927, + 0x2F928, + 0x2F929, + 0x2F92A, + 0x2F92B, + 0x2F92C, + 0x2F92E, + 0x2F92F, + 0x2F930, + 0x2F931, + 0x2F932, + 0x2F933, + 0x2F934, + 0x2F935, + 0x2F936, + 0x2F937, + 0x2F938, + 0x2F939, + 0x2F93A, + 0x2F93B, + 0x2F93C, + 0x2F93D, + 0x2F93E, + 0x2F93F, + 0x2F940, + 0x2F941, + 0x2F942, + 0x2F943, + 0x2F944, + 0x2F945, + 0x2F946, + 0x2F948, + 0x2F949, + 0x2F94A, + 0x2F94B, + 0x2F94C, + 0x2F94D, + 0x2F94E, + 0x2F94F, + 0x2F950, + 0x2F951, + 0x2F952, + 0x2F953, + 0x2F954, + 0x2F955, + 0x2F956, + 0x2F957, + 0x2F958, + 0x2F959, + 0x2F95A, + 0x2F95B, + 0x2F95C, + 0x2F95D, + 0x2F95F, + 0x2F960, + 0x2F961, + 0x2F962, + 0x2F963, + 0x2F964, + 0x2F965, + 0x2F966, + 0x2F967, + 0x2F968, + 0x2F969, + 0x2F96A, + 0x2F96B, + 0x2F96C, + 0x2F96D, + 0x2F96E, + 0x2F96F, + 0x2F970, + 0x2F971, + 0x2F972, + 0x2F973, + 0x2F974, + 0x2F975, + 0x2F976, + 0x2F977, + 0x2F978, + 0x2F979, + 0x2F97A, + 0x2F97B, + 0x2F97C, + 0x2F97D, + 0x2F97E, + 0x2F97F, + 0x2F980, + 0x2F981, + 0x2F982, + 0x2F983, + 0x2F984, + 0x2F985, + 0x2F986, + 0x2F987, + 0x2F988, + 0x2F989, + 0x2F98A, + 0x2F98B, + 0x2F98C, + 0x2F98D, + 0x2F98E, + 0x2F98F, + 0x2F990, + 0x2F991, + 0x2F992, + 0x2F993, + 0x2F994, + 0x2F995, + 0x2F996, + 0x2F997, + 0x2F998, + 0x2F999, + 0x2F99A, + 0x2F99B, + 0x2F99C, + 0x2F99D, + 0x2F99E, + 0x2F99F, + 0x2F9A0, + 0x2F9A1, + 0x2F9A2, + 0x2F9A3, + 0x2F9A4, + 0x2F9A5, + 0x2F9A6, + 0x2F9A7, + 0x2F9A8, + 0x2F9A9, + 0x2F9AA, + 0x2F9AB, + 0x2F9AC, + 0x2F9AD, + 0x2F9AE, + 0x2F9AF, + 0x2F9B0, + 0x2F9B1, + 0x2F9B2, + 0x2F9B3, + 0x2F9B4, + 0x2F9B5, + 0x2F9B6, + 0x2F9B7, + 0x2F9B8, + 0x2F9B9, + 0x2F9BA, + 0x2F9BB, + 0x2F9BC, + 0x2F9BD, + 0x2F9BE, + 0x2F9BF, + 0x2F9C0, + 0x2F9C1, + 0x2F9C2, + 0x2F9C3, + 0x2F9C4, + 0x2F9C5, + 0x2F9C6, + 0x2F9C7, + 0x2F9C8, + 0x2F9C9, + 0x2F9CA, + 0x2F9CB, + 0x2F9CC, + 0x2F9CD, + 0x2F9CE, + 0x2F9CF, + 0x2F9D0, + 0x2F9D1, + 0x2F9D2, + 0x2F9D3, + 0x2F9D4, + 0x2F9D5, + 0x2F9D6, + 0x2F9D7, + 0x2F9D8, + 0x2F9D9, + 0x2F9DA, + 0x2F9DB, + 0x2F9DC, + 0x2F9DD, + 0x2F9DE, + 0x2F9DF, + 0x2F9E0, + 0x2F9E1, + 0x2F9E2, + 0x2F9E3, + 0x2F9E4, + 0x2F9E5, + 0x2F9E6, + 0x2F9E7, + 0x2F9E8, + 0x2F9E9, + 0x2F9EA, + 0x2F9EB, + 0x2F9EC, + 0x2F9ED, + 0x2F9EE, + 0x2F9EF, + 0x2F9F0, + 0x2F9F1, + 0x2F9F2, + 0x2F9F3, + 0x2F9F4, + 0x2F9F5, + 0x2F9F6, + 0x2F9F7, + 0x2F9F8, + 0x2F9F9, + 0x2F9FA, + 0x2F9FB, + 0x2F9FC, + 0x2F9FD, + 0x2F9FE, + 0x2FA00, + 0x2FA01, + 0x2FA02, + 0x2FA03, + 0x2FA04, + 0x2FA05, + 0x2FA06, + 0x2FA07, + 0x2FA08, + 0x2FA09, + 0x2FA0A, + 0x2FA0B, + 0x2FA0C, + 0x2FA0D, + 0x2FA0E, + 0x2FA0F, + 0x2FA10, + 0x2FA11, + 0x2FA12, + 0x2FA13, + 0x2FA14, + 0x2FA15, + 0x2FA16, + 0x2FA17, + 0x2FA18, + 0x2FA19, + 0x2FA1A, + 0x2FA1B, + 0x2FA1C, + 0x2FA1D, + 0x2FA1E, + 0x30000, + 0x3134B, + 0x31350, + 0x3347A, + 0x3D000, + 0x3FC40, + 0xE0100, + 0xE01F0, + ), +) + +uts46_statuses: bytes = ( + b"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV" + b"VMMMMMMMMMMMMMMMMMMMMMMMMMMVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV" + b"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXMVVVVVVVMVMVVIVMVVMMMMVVMMMVMMMV" + b"MMMMMMMMMMMMMMMMMMMMMMMVMMMMMMMDVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV" + b"MVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMMVMVMVMVMVMMV" + b"MVMVMVMMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMMVMVMVMVMM" + b"VMVMMVMMMVMMMMVMMVMMMVMMVMMVMVMVMMVMVMVMMVMMMVMVMMVMVMMMMVMVMVMV" + b"MVMVMVMVMVMVMVMVMVMVMVMVMVMMVMMMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVM" + b"VMVMVMVMVMVMVMVMVMVMVMVMVMVMMVMMVMVMMMMVMVMVMVMVMMMMMMMMMVMMMMMM" + b"VMMMMMVMMVMMMVIVMVMVMVMVXMVMMXMMMMMMMXMXMMVMMMMMMMMMMMMMMMMMXMMM" + b"MMMMMMVDVMMMMMMMMVMVMVMVMVMVMVMVMVMVMVMVMVMMMVMMVMVMMVMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVMVMVMVMVMVMVMVMVMVMVMV" + b"MVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMV" + b"MVMMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVM" + b"VMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVXMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMXMVMVMMVXVXVXVXVXVMMMMVXVXVXVXVXVXVXVXVX" + b"VXVXVXVMMMMMMMMVXVXVXVXVXVXVXVXVXVXVXMMXMVXVXVXVXVXVXVXVMXVMXVXV" + b"XVXVXVXVXMMMVXMXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVX" + b"VXVXMMXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXV" + b"XVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXV" + b"XVXVMVXVXVXVXVXVXVXVMVXVXVXVXVXMMVXVMVMVXVMVMVMVMVMVXVMVMMMMMVMV" + b"MVXVMVMVMVMVMVXVXVXVMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXMXMXV" + b"MVIVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXMMMMMMXVXVXVXVXVXVXVXVX" + b"VXVIVXVXVXVIVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVMMM" + b"MMMMMMVXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXMMMVXVXVMMMV" + b"MMMMMMMMMMMVMMMMMMMMMMMMMMMMMMVMMMMMMMMMMMMMMMMMMMMMMMMMMMMVMVMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVMVMVMVMVMVMVMVMVMVMVMVMVMVMV" + b"MVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMV" + b"MVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMMVMVM" + b"VMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVM" + b"VMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMMMMMMMMVXMMMMMMXVMMMMMMMMVMMMMMM" + b"MMVXMMMMMMXVXMXMXMXMVMMMMMMMMVMVMVMVMVMVMVMXMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMVMMMXVMMMMMMMMMMMMMMXVMMMMMMMMMVMXVM" + b"MMMXMMMVMVMMMMMMMMXMMMXVMMMMMMMMXMIDXVMVMVXVXMVMMVMMVMVMVMMMVMVM" + b"IXIMMXMMMMMMMMMMMMMMMMMMMMMMMMMMMVMMMMMMMMMMMMMMMMVMVXVXMMMMVMMM" + b"VMMMMMMVMMVMMMVMMMVMVMVMVMMMMVMMMMMMMMMMVMMMMMVMMMMVMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVMVMVXVMMVMMVMMVXVXMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVMVMMMVMVXVMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVMVMMMVMVMVMVMMMMVMVMV" + b"MMMMMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMV" + b"MVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVXVXVXVXVXMVXVXVXVX" + b"VXVXVXVXVXVXVXVXVXVMVMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXMVMVMVMMMVXVXVMMVM" + b"VMXVXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMIMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXVMMMMMMMMMMMMMMVXVMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXMMMMXMMMMMMMMM" + b"MMMMMMMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVXVXVXMVMVMVMVMVM" + b"VMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVM" + b"MVXVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMV" + b"MVMVMVMVMVMVMVMVMVMVMVMMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMMM" + b"MMVMMMMMVMVMVMVMVMVMVMVMMMMVMVMMVMVMVMVMVMVMVMVMMXMXMMMMMVMMVXVX" + b"VXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVMMMMVMVMMXMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMV" + b"XVXVXVXVXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMVMVMVMMMMMMMMMMVMVMVMMVMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMXMMMMMMXMMMMMXMVMMMMMMMMMMMMMMMMMMMMM" + b"MMMXMMMMMXMXMMXMMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMVMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVXMMMMMMMMMMMM" + b"MVIMMXMMMMMMXVXMMMMMMMMMMMMMMMMMMMVMMMMMMXMMMMMMMMMMMMMMMMMMMXMM" + b"MMXMMMVMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXIXMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMIMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXMMM" + b"MMMXMMMMMMXMMMMMMXMMMXMMMMMMMXMMMMMMMXVXVXVXVXVXVXVXVXVXVXVXVXVX" + b"VXVXVXVXVXVXVXVXVXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVXVXMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXVXVXVXVMMMMMMMMMMMXMMMMMMMMMM" + b"MMMMMXMMMMMMMXMMXVXVXVXVXVXVXVXVXVMMMMMXMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMXMMMMMMMMMMMMMMXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVX" + b"VXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMXVXVXVXVMMMMMMMMMMMMMMMMMMMMMMXVXVXVXVXVX" + b"VXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVX" + b"VXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVX" + b"VXVXVXVXVXVXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVXVXVXVXVXVXVXVXVXVX" + b"VXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVX" + b"VXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMVXMMMMMMMMMMMMMMMMMMMMMMMMMXVXVXVXVXVXVX" + b"VXVXVXVXVXVXVXVXVMMMMVXVXVXVXVXVXVXVXVXVXVIXVMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMVXVXVXVXVXVXVXVXVXVMMMMMMMVIVMMMMMMVXVXVXVXVXVX" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMXMMXMXMMXMMMMXMMMMMMMMMMMMXMXMMMMMM" + b"MXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMXMMMMXMMMMMMMMXMMMMMMMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMXMMMMXMMMM" + b"MXMXMMMMMMMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"XMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVXVXVXVXVMVMV" + b"MVMVMVMVMVMVMVMVMVMVMVMVMVMVXVMVXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMVXVXVXVXVXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXV" + b"XVXVXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVXVXVXVXVXMMMMXMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMXMMXMXMXMMMMMMMMMMXMMMMXMXMXMXMXMXMXMMMXMMXMXMX" + b"MXMXMXMXMMXMXMMMMXMMMMMMMXMMMMXMMMMXMXMMMMMMMMMMXMMMMMMMMMMMMMMM" + b"MMXMMMXMMMMMXMMMMMMMMMMMMMMMMMXVXVXVXVXVXVXVXMMMMMMMMMMVMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMVMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVMMMVMVX" + b"VMMMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXMMMMMMMMMXMMXV" + b"XVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVMMMMMMMMMMVXVXVXVX" + b"VXVXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMXVXVXVXIX" +) + +uts46_replacements: tuple[str | None, ...] = ( + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + " ", + None, + None, + None, + None, + None, + None, + None, + " ̈", + None, + "a", + None, + None, + None, + None, + " ̄", + None, + None, + "2", + "3", + " ́", + "μ", + None, + None, + " ̧", + "1", + "o", + None, + "1⁄4", + "1⁄2", + "3⁄4", + None, + "à", + "á", + "â", + "ã", + "ä", + "å", + "æ", + "ç", + "è", + "é", + "ê", + "ë", + "ì", + "í", + "î", + "ï", + "ð", + "ñ", + "ò", + "ó", + "ô", + "õ", + "ö", + None, + "ø", + "ù", + "ú", + "û", + "ü", + "ý", + "þ", + "ss", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ā", + None, + "ă", + None, + "ą", + None, + "ć", + None, + "ĉ", + None, + "ċ", + None, + "č", + None, + "ď", + None, + "đ", + None, + "ē", + None, + "ĕ", + None, + "ė", + None, + "ę", + None, + "ě", + None, + "ĝ", + None, + "ğ", + None, + "ġ", + None, + "ģ", + None, + "ĥ", + None, + "ħ", + None, + "ĩ", + None, + "ī", + None, + "ĭ", + None, + "į", + None, + "i̇", + None, + "ij", + "ĵ", + None, + "ķ", + None, + "ĺ", + None, + "ļ", + None, + "ľ", + None, + "l·", + "ł", + None, + "ń", + None, + "ņ", + None, + "ň", + None, + "ʼn", + "ŋ", + None, + "ō", + None, + "ŏ", + None, + "ő", + None, + "œ", + None, + "ŕ", + None, + "ŗ", + None, + "ř", + None, + "ś", + None, + "ŝ", + None, + "ş", + None, + "š", + None, + "ţ", + None, + "ť", + None, + "ŧ", + None, + "ũ", + None, + "ū", + None, + "ŭ", + None, + "ů", + None, + "ű", + None, + "ų", + None, + "ŵ", + None, + "ŷ", + None, + "ÿ", + "ź", + None, + "ż", + None, + "ž", + None, + "s", + None, + "ɓ", + "ƃ", + None, + "ƅ", + None, + "ɔ", + "ƈ", + None, + "ɖ", + "ɗ", + "ƌ", + None, + "ǝ", + "ə", + "ɛ", + "ƒ", + None, + "ɠ", + "ɣ", + None, + "ɩ", + "ɨ", + "ƙ", + None, + "ɯ", + "ɲ", + None, + "ɵ", + "ơ", + None, + "ƣ", + None, + "ƥ", + None, + "ʀ", + "ƨ", + None, + "ʃ", + None, + "ƭ", + None, + "ʈ", + "ư", + None, + "ʊ", + "ʋ", + "ƴ", + None, + "ƶ", + None, + "ʒ", + "ƹ", + None, + "ƽ", + None, + "dž", + "lj", + "nj", + "ǎ", + None, + "ǐ", + None, + "ǒ", + None, + "ǔ", + None, + "ǖ", + None, + "ǘ", + None, + "ǚ", + None, + "ǜ", + None, + "ǟ", + None, + "ǡ", + None, + "ǣ", + None, + "ǥ", + None, + "ǧ", + None, + "ǩ", + None, + "ǫ", + None, + "ǭ", + None, + "ǯ", + None, + "dz", + "ǵ", + None, + "ƕ", + "ƿ", + "ǹ", + None, + "ǻ", + None, + "ǽ", + None, + "ǿ", + None, + "ȁ", + None, + "ȃ", + None, + "ȅ", + None, + "ȇ", + None, + "ȉ", + None, + "ȋ", + None, + "ȍ", + None, + "ȏ", + None, + "ȑ", + None, + "ȓ", + None, + "ȕ", + None, + "ȗ", + None, + "ș", + None, + "ț", + None, + "ȝ", + None, + "ȟ", + None, + "ƞ", + None, + "ȣ", + None, + "ȥ", + None, + "ȧ", + None, + "ȩ", + None, + "ȫ", + None, + "ȭ", + None, + "ȯ", + None, + "ȱ", + None, + "ȳ", + None, + "ⱥ", + "ȼ", + None, + "ƚ", + "ⱦ", + None, + "ɂ", + None, + "ƀ", + "ʉ", + "ʌ", + "ɇ", + None, + "ɉ", + None, + "ɋ", + None, + "ɍ", + None, + "ɏ", + None, + "h", + "ɦ", + "j", + "r", + "ɹ", + "ɻ", + "ʁ", + "w", + "y", + None, + " ̆", + " ̇", + " ̊", + " ̨", + " ̃", + " ̋", + None, + "ɣ", + "l", + "s", + "x", + "ʕ", + None, + "̀", + "́", + None, + "̓", + "̈́", + "ι", + None, + None, + None, + "ͱ", + None, + "ͳ", + None, + "ʹ", + None, + "ͷ", + None, + None, + " ι", + None, + ";", + "ϳ", + None, + " ́", + " ̈́", + "ά", + "·", + "έ", + "ή", + "ί", + None, + "ό", + None, + "ύ", + "ώ", + None, + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + None, + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "ϊ", + "ϋ", + None, + "σ", + None, + "ϗ", + "β", + "θ", + "υ", + "ύ", + "ϋ", + "φ", + "π", + None, + "ϙ", + None, + "ϛ", + None, + "ϝ", + None, + "ϟ", + None, + "ϡ", + None, + "ϣ", + None, + "ϥ", + None, + "ϧ", + None, + "ϩ", + None, + "ϫ", + None, + "ϭ", + None, + "ϯ", + None, + "κ", + "ρ", + "σ", + None, + "θ", + "ε", + None, + "ϸ", + None, + "σ", + "ϻ", + None, + "ͻ", + "ͼ", + "ͽ", + "ѐ", + "ё", + "ђ", + "ѓ", + "є", + "ѕ", + "і", + "ї", + "ј", + "љ", + "њ", + "ћ", + "ќ", + "ѝ", + "ў", + "џ", + "а", + "б", + "в", + "г", + "д", + "е", + "ж", + "з", + "и", + "й", + "к", + "л", + "м", + "н", + "о", + "п", + "р", + "с", + "т", + "у", + "ф", + "х", + "ц", + "ч", + "ш", + "щ", + "ъ", + "ы", + "ь", + "э", + "ю", + "я", + None, + "ѡ", + None, + "ѣ", + None, + "ѥ", + None, + "ѧ", + None, + "ѩ", + None, + "ѫ", + None, + "ѭ", + None, + "ѯ", + None, + "ѱ", + None, + "ѳ", + None, + "ѵ", + None, + "ѷ", + None, + "ѹ", + None, + "ѻ", + None, + "ѽ", + None, + "ѿ", + None, + "ҁ", + None, + "ҋ", + None, + "ҍ", + None, + "ҏ", + None, + "ґ", + None, + "ғ", + None, + "ҕ", + None, + "җ", + None, + "ҙ", + None, + "қ", + None, + "ҝ", + None, + "ҟ", + None, + "ҡ", + None, + "ң", + None, + "ҥ", + None, + "ҧ", + None, + "ҩ", + None, + "ҫ", + None, + "ҭ", + None, + "ү", + None, + "ұ", + None, + "ҳ", + None, + "ҵ", + None, + "ҷ", + None, + "ҹ", + None, + "һ", + None, + "ҽ", + None, + "ҿ", + None, + "ӏ", + "ӂ", + None, + "ӄ", + None, + "ӆ", + None, + "ӈ", + None, + "ӊ", + None, + "ӌ", + None, + "ӎ", + None, + "ӑ", + None, + "ӓ", + None, + "ӕ", + None, + "ӗ", + None, + "ә", + None, + "ӛ", + None, + "ӝ", + None, + "ӟ", + None, + "ӡ", + None, + "ӣ", + None, + "ӥ", + None, + "ӧ", + None, + "ө", + None, + "ӫ", + None, + "ӭ", + None, + "ӯ", + None, + "ӱ", + None, + "ӳ", + None, + "ӵ", + None, + "ӷ", + None, + "ӹ", + None, + "ӻ", + None, + "ӽ", + None, + "ӿ", + None, + "ԁ", + None, + "ԃ", + None, + "ԅ", + None, + "ԇ", + None, + "ԉ", + None, + "ԋ", + None, + "ԍ", + None, + "ԏ", + None, + "ԑ", + None, + "ԓ", + None, + "ԕ", + None, + "ԗ", + None, + "ԙ", + None, + "ԛ", + None, + "ԝ", + None, + "ԟ", + None, + "ԡ", + None, + "ԣ", + None, + "ԥ", + None, + "ԧ", + None, + "ԩ", + None, + "ԫ", + None, + "ԭ", + None, + "ԯ", + None, + None, + "ա", + "բ", + "գ", + "դ", + "ե", + "զ", + "է", + "ը", + "թ", + "ժ", + "ի", + "լ", + "խ", + "ծ", + "կ", + "հ", + "ձ", + "ղ", + "ճ", + "մ", + "յ", + "ն", + "շ", + "ո", + "չ", + "պ", + "ջ", + "ռ", + "ս", + "վ", + "տ", + "ր", + "ց", + "ւ", + "փ", + "ք", + "օ", + "ֆ", + None, + "է", + None, + "եւ", + None, + "ի", + "յ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "اٴ", + "وٴ", + "ۇٴ", + "يٴ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "क़", + "ख़", + "ग़", + "ज़", + "ड़", + "ढ़", + "फ़", + "य़", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ড়", + "ঢ়", + None, + "য়", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ਲ਼", + None, + None, + "ਸ਼", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ਖ਼", + "ਗ਼", + "ਜ਼", + None, + None, + "ਫ਼", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ଡ଼", + "ଢ଼", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ํา", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ໍາ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ຫນ", + "ຫມ", + None, + None, + None, + "་", + None, + "གྷ", + None, + None, + None, + "ཌྷ", + None, + "དྷ", + None, + "བྷ", + None, + "ཛྷ", + None, + "ཀྵ", + None, + None, + None, + "ཱི", + None, + "ཱུ", + "ྲྀ", + "ྲཱྀ", + "ླྀ", + "ླཱྀ", + None, + "ཱྀ", + None, + "ྒྷ", + None, + None, + None, + "ྜྷ", + None, + "ྡྷ", + None, + "ྦྷ", + None, + "ྫྷ", + None, + "ྐྵ", + None, + None, + None, + None, + None, + None, + None, + "ⴀ", + "ⴁ", + "ⴂ", + "ⴃ", + "ⴄ", + "ⴅ", + "ⴆ", + "ⴇ", + "ⴈ", + "ⴉ", + "ⴊ", + "ⴋ", + "ⴌ", + "ⴍ", + "ⴎ", + "ⴏ", + "ⴐ", + "ⴑ", + "ⴒ", + "ⴓ", + "ⴔ", + "ⴕ", + "ⴖ", + "ⴗ", + "ⴘ", + "ⴙ", + "ⴚ", + "ⴛ", + "ⴜ", + "ⴝ", + "ⴞ", + "ⴟ", + "ⴠ", + "ⴡ", + "ⴢ", + "ⴣ", + "ⴤ", + "ⴥ", + None, + "ⴧ", + None, + "ⴭ", + None, + None, + "ნ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "Ᏸ", + "Ᏹ", + "Ᏺ", + "Ᏻ", + "Ᏼ", + "Ᏽ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "в", + "д", + "о", + "с", + "т", + "ъ", + "ѣ", + "ꙋ", + "ᲊ", + None, + None, + "ა", + "ბ", + "გ", + "დ", + "ე", + "ვ", + "ზ", + "თ", + "ი", + "კ", + "ლ", + "მ", + "ნ", + "ო", + "პ", + "ჟ", + "რ", + "ს", + "ტ", + "უ", + "ფ", + "ქ", + "ღ", + "ყ", + "შ", + "ჩ", + "ც", + "ძ", + "წ", + "ჭ", + "ხ", + "ჯ", + "ჰ", + "ჱ", + "ჲ", + "ჳ", + "ჴ", + "ჵ", + "ჶ", + "ჷ", + "ჸ", + "ჹ", + "ჺ", + None, + "ჽ", + "ჾ", + "ჿ", + None, + None, + None, + None, + None, + "a", + "æ", + "b", + None, + "d", + "e", + "ǝ", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + None, + "o", + "ȣ", + "p", + "r", + "t", + "u", + "w", + "a", + "ɐ", + "ɑ", + "ᴂ", + "b", + "d", + "e", + "ə", + "ɛ", + "ɜ", + "g", + None, + "k", + "m", + "ŋ", + "o", + "ɔ", + "ᴖ", + "ᴗ", + "p", + "t", + "u", + "ᴝ", + "ɯ", + "v", + "ᴥ", + "β", + "γ", + "δ", + "φ", + "χ", + "i", + "r", + "u", + "v", + "β", + "γ", + "ρ", + "φ", + "χ", + None, + "н", + None, + "ɒ", + "c", + "ɕ", + "ð", + "ɜ", + "f", + "ɟ", + "ɡ", + "ɥ", + "ɨ", + "ɩ", + "ɪ", + "ᵻ", + "ʝ", + "ɭ", + "ᶅ", + "ʟ", + "ɱ", + "ɰ", + "ɲ", + "ɳ", + "ɴ", + "ɵ", + "ɸ", + "ʂ", + "ʃ", + "ƫ", + "ʉ", + "ʊ", + "ᴜ", + "ʋ", + "ʌ", + "z", + "ʐ", + "ʑ", + "ʒ", + "θ", + None, + "ḁ", + None, + "ḃ", + None, + "ḅ", + None, + "ḇ", + None, + "ḉ", + None, + "ḋ", + None, + "ḍ", + None, + "ḏ", + None, + "ḑ", + None, + "ḓ", + None, + "ḕ", + None, + "ḗ", + None, + "ḙ", + None, + "ḛ", + None, + "ḝ", + None, + "ḟ", + None, + "ḡ", + None, + "ḣ", + None, + "ḥ", + None, + "ḧ", + None, + "ḩ", + None, + "ḫ", + None, + "ḭ", + None, + "ḯ", + None, + "ḱ", + None, + "ḳ", + None, + "ḵ", + None, + "ḷ", + None, + "ḹ", + None, + "ḻ", + None, + "ḽ", + None, + "ḿ", + None, + "ṁ", + None, + "ṃ", + None, + "ṅ", + None, + "ṇ", + None, + "ṉ", + None, + "ṋ", + None, + "ṍ", + None, + "ṏ", + None, + "ṑ", + None, + "ṓ", + None, + "ṕ", + None, + "ṗ", + None, + "ṙ", + None, + "ṛ", + None, + "ṝ", + None, + "ṟ", + None, + "ṡ", + None, + "ṣ", + None, + "ṥ", + None, + "ṧ", + None, + "ṩ", + None, + "ṫ", + None, + "ṭ", + None, + "ṯ", + None, + "ṱ", + None, + "ṳ", + None, + "ṵ", + None, + "ṷ", + None, + "ṹ", + None, + "ṻ", + None, + "ṽ", + None, + "ṿ", + None, + "ẁ", + None, + "ẃ", + None, + "ẅ", + None, + "ẇ", + None, + "ẉ", + None, + "ẋ", + None, + "ẍ", + None, + "ẏ", + None, + "ẑ", + None, + "ẓ", + None, + "ẕ", + None, + "aʾ", + "ṡ", + None, + "ß", + None, + "ạ", + None, + "ả", + None, + "ấ", + None, + "ầ", + None, + "ẩ", + None, + "ẫ", + None, + "ậ", + None, + "ắ", + None, + "ằ", + None, + "ẳ", + None, + "ẵ", + None, + "ặ", + None, + "ẹ", + None, + "ẻ", + None, + "ẽ", + None, + "ế", + None, + "ề", + None, + "ể", + None, + "ễ", + None, + "ệ", + None, + "ỉ", + None, + "ị", + None, + "ọ", + None, + "ỏ", + None, + "ố", + None, + "ồ", + None, + "ổ", + None, + "ỗ", + None, + "ộ", + None, + "ớ", + None, + "ờ", + None, + "ở", + None, + "ỡ", + None, + "ợ", + None, + "ụ", + None, + "ủ", + None, + "ứ", + None, + "ừ", + None, + "ử", + None, + "ữ", + None, + "ự", + None, + "ỳ", + None, + "ỵ", + None, + "ỷ", + None, + "ỹ", + None, + "ỻ", + None, + "ỽ", + None, + "ỿ", + None, + "ἀ", + "ἁ", + "ἂ", + "ἃ", + "ἄ", + "ἅ", + "ἆ", + "ἇ", + None, + None, + "ἐ", + "ἑ", + "ἒ", + "ἓ", + "ἔ", + "ἕ", + None, + None, + "ἠ", + "ἡ", + "ἢ", + "ἣ", + "ἤ", + "ἥ", + "ἦ", + "ἧ", + None, + "ἰ", + "ἱ", + "ἲ", + "ἳ", + "ἴ", + "ἵ", + "ἶ", + "ἷ", + None, + None, + "ὀ", + "ὁ", + "ὂ", + "ὃ", + "ὄ", + "ὅ", + None, + None, + None, + "ὑ", + None, + "ὓ", + None, + "ὕ", + None, + "ὗ", + None, + "ὠ", + "ὡ", + "ὢ", + "ὣ", + "ὤ", + "ὥ", + "ὦ", + "ὧ", + None, + "ά", + None, + "έ", + None, + "ή", + None, + "ί", + None, + "ό", + None, + "ύ", + None, + "ώ", + None, + "ἀι", + "ἁι", + "ἂι", + "ἃι", + "ἄι", + "ἅι", + "ἆι", + "ἇι", + "ἀι", + "ἁι", + "ἂι", + "ἃι", + "ἄι", + "ἅι", + "ἆι", + "ἇι", + "ἠι", + "ἡι", + "ἢι", + "ἣι", + "ἤι", + "ἥι", + "ἦι", + "ἧι", + "ἠι", + "ἡι", + "ἢι", + "ἣι", + "ἤι", + "ἥι", + "ἦι", + "ἧι", + "ὠι", + "ὡι", + "ὢι", + "ὣι", + "ὤι", + "ὥι", + "ὦι", + "ὧι", + "ὠι", + "ὡι", + "ὢι", + "ὣι", + "ὤι", + "ὥι", + "ὦι", + "ὧι", + None, + "ὰι", + "αι", + "άι", + None, + None, + "ᾶι", + "ᾰ", + "ᾱ", + "ὰ", + "ά", + "αι", + " ̓", + "ι", + " ̓", + " ͂", + " ̈͂", + "ὴι", + "ηι", + "ήι", + None, + None, + "ῆι", + "ὲ", + "έ", + "ὴ", + "ή", + "ηι", + " ̓̀", + " ̓́", + " ̓͂", + None, + "ΐ", + None, + None, + "ῐ", + "ῑ", + "ὶ", + "ί", + None, + " ̔̀", + " ̔́", + " ̔͂", + None, + "ΰ", + None, + "ῠ", + "ῡ", + "ὺ", + "ύ", + "ῥ", + " ̈̀", + " ̈́", + "`", + None, + "ὼι", + "ωι", + "ώι", + None, + None, + "ῶι", + "ὸ", + "ό", + "ὼ", + "ώ", + "ωι", + " ́", + " ̔", + None, + " ", + None, + "", + None, + None, + "‐", + None, + " ̳", + None, + None, + None, + None, + " ", + None, + "′′", + "′′′", + None, + "‵‵", + "‵‵‵", + None, + "!!", + None, + " ̅", + None, + "??", + "?!", + "!?", + None, + "′′′′", + None, + " ", + None, + None, + None, + "0", + "i", + None, + "4", + "5", + "6", + "7", + "8", + "9", + "+", + "−", + "=", + "(", + ")", + "n", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "+", + "−", + "=", + "(", + ")", + None, + "a", + "e", + "o", + "x", + "ə", + "h", + "k", + "l", + "m", + "n", + "p", + "s", + "t", + "w", + "y", + "z", + None, + "rs", + None, + None, + None, + None, + "a/c", + "a/s", + "c", + "°c", + None, + "c/o", + "c/u", + "ɛ", + None, + "°f", + "g", + "h", + "ħ", + "i", + "l", + None, + "n", + "no", + None, + "p", + "q", + "r", + None, + "sm", + "tel", + "tm", + None, + "z", + None, + "ω", + None, + "z", + None, + "k", + "å", + "b", + "c", + None, + "e", + "f", + "ⅎ", + "m", + "o", + "א", + "ב", + "ג", + "ד", + "i", + None, + "fax", + "π", + "γ", + "π", + "∑", + None, + "d", + "e", + "i", + "j", + None, + "1⁄7", + "1⁄9", + "1⁄10", + "1⁄3", + "2⁄3", + "1⁄5", + "2⁄5", + "3⁄5", + "4⁄5", + "1⁄6", + "5⁄6", + "1⁄8", + "3⁄8", + "5⁄8", + "7⁄8", + "1⁄", + "i", + "ii", + "iii", + "iv", + "v", + "vi", + "vii", + "viii", + "ix", + "x", + "xi", + "xii", + "l", + "c", + "d", + "m", + "i", + "ii", + "iii", + "iv", + "v", + "vi", + "vii", + "viii", + "ix", + "x", + "xi", + "xii", + "l", + "c", + "d", + "m", + None, + "ↄ", + None, + "0⁄3", + None, + None, + None, + "∫∫", + "∫∫∫", + None, + "∮∮", + "∮∮∮", + None, + "〈", + "〉", + None, + None, + None, + None, + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "(1)", + "(2)", + "(3)", + "(4)", + "(5)", + "(6)", + "(7)", + "(8)", + "(9)", + "(10)", + "(11)", + "(12)", + "(13)", + "(14)", + "(15)", + "(16)", + "(17)", + "(18)", + "(19)", + "(20)", + None, + "(a)", + "(b)", + "(c)", + "(d)", + "(e)", + "(f)", + "(g)", + "(h)", + "(i)", + "(j)", + "(k)", + "(l)", + "(m)", + "(n)", + "(o)", + "(p)", + "(q)", + "(r)", + "(s)", + "(t)", + "(u)", + "(v)", + "(w)", + "(x)", + "(y)", + "(z)", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "0", + None, + "∫∫∫∫", + None, + "::=", + "==", + "===", + None, + "⫝̸", + None, + None, + None, + "ⰰ", + "ⰱ", + "ⰲ", + "ⰳ", + "ⰴ", + "ⰵ", + "ⰶ", + "ⰷ", + "ⰸ", + "ⰹ", + "ⰺ", + "ⰻ", + "ⰼ", + "ⰽ", + "ⰾ", + "ⰿ", + "ⱀ", + "ⱁ", + "ⱂ", + "ⱃ", + "ⱄ", + "ⱅ", + "ⱆ", + "ⱇ", + "ⱈ", + "ⱉ", + "ⱊ", + "ⱋ", + "ⱌ", + "ⱍ", + "ⱎ", + "ⱏ", + "ⱐ", + "ⱑ", + "ⱒ", + "ⱓ", + "ⱔ", + "ⱕ", + "ⱖ", + "ⱗ", + "ⱘ", + "ⱙ", + "ⱚ", + "ⱛ", + "ⱜ", + "ⱝ", + "ⱞ", + "ⱟ", + None, + "ⱡ", + None, + "ɫ", + "ᵽ", + "ɽ", + None, + "ⱨ", + None, + "ⱪ", + None, + "ⱬ", + None, + "ɑ", + "ɱ", + "ɐ", + "ɒ", + None, + "ⱳ", + None, + "ⱶ", + None, + "j", + "v", + "ȿ", + "ɀ", + "ⲁ", + None, + "ⲃ", + None, + "ⲅ", + None, + "ⲇ", + None, + "ⲉ", + None, + "ⲋ", + None, + "ⲍ", + None, + "ⲏ", + None, + "ⲑ", + None, + "ⲓ", + None, + "ⲕ", + None, + "ⲗ", + None, + "ⲙ", + None, + "ⲛ", + None, + "ⲝ", + None, + "ⲟ", + None, + "ⲡ", + None, + "ⲣ", + None, + "ⲥ", + None, + "ⲧ", + None, + "ⲩ", + None, + "ⲫ", + None, + "ⲭ", + None, + "ⲯ", + None, + "ⲱ", + None, + "ⲳ", + None, + "ⲵ", + None, + "ⲷ", + None, + "ⲹ", + None, + "ⲻ", + None, + "ⲽ", + None, + "ⲿ", + None, + "ⳁ", + None, + "ⳃ", + None, + "ⳅ", + None, + "ⳇ", + None, + "ⳉ", + None, + "ⳋ", + None, + "ⳍ", + None, + "ⳏ", + None, + "ⳑ", + None, + "ⳓ", + None, + "ⳕ", + None, + "ⳗ", + None, + "ⳙ", + None, + "ⳛ", + None, + "ⳝ", + None, + "ⳟ", + None, + "ⳡ", + None, + "ⳣ", + None, + "ⳬ", + None, + "ⳮ", + None, + "ⳳ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ⵡ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "母", + None, + "龟", + None, + "一", + "丨", + "丶", + "丿", + "乙", + "亅", + "二", + "亠", + "人", + "儿", + "入", + "八", + "冂", + "冖", + "冫", + "几", + "凵", + "刀", + "力", + "勹", + "匕", + "匚", + "匸", + "十", + "卜", + "卩", + "厂", + "厶", + "又", + "口", + "囗", + "土", + "士", + "夂", + "夊", + "夕", + "大", + "女", + "子", + "宀", + "寸", + "小", + "尢", + "尸", + "屮", + "山", + "巛", + "工", + "己", + "巾", + "干", + "幺", + "广", + "廴", + "廾", + "弋", + "弓", + "彐", + "彡", + "彳", + "心", + "戈", + "戶", + "手", + "支", + "攴", + "文", + "斗", + "斤", + "方", + "无", + "日", + "曰", + "月", + "木", + "欠", + "止", + "歹", + "殳", + "毋", + "比", + "毛", + "氏", + "气", + "水", + "火", + "爪", + "父", + "爻", + "爿", + "片", + "牙", + "牛", + "犬", + "玄", + "玉", + "瓜", + "瓦", + "甘", + "生", + "用", + "田", + "疋", + "疒", + "癶", + "白", + "皮", + "皿", + "目", + "矛", + "矢", + "石", + "示", + "禸", + "禾", + "穴", + "立", + "竹", + "米", + "糸", + "缶", + "网", + "羊", + "羽", + "老", + "而", + "耒", + "耳", + "聿", + "肉", + "臣", + "自", + "至", + "臼", + "舌", + "舛", + "舟", + "艮", + "色", + "艸", + "虍", + "虫", + "血", + "行", + "衣", + "襾", + "見", + "角", + "言", + "谷", + "豆", + "豕", + "豸", + "貝", + "赤", + "走", + "足", + "身", + "車", + "辛", + "辰", + "辵", + "邑", + "酉", + "釆", + "里", + "金", + "長", + "門", + "阜", + "隶", + "隹", + "雨", + "靑", + "非", + "面", + "革", + "韋", + "韭", + "音", + "頁", + "風", + "飛", + "食", + "首", + "香", + "馬", + "骨", + "高", + "髟", + "鬥", + "鬯", + "鬲", + "鬼", + "魚", + "鳥", + "鹵", + "鹿", + "麥", + "麻", + "黃", + "黍", + "黑", + "黹", + "黽", + "鼎", + "鼓", + "鼠", + "鼻", + "齊", + "齒", + "龍", + "龜", + "龠", + None, + " ", + None, + ".", + None, + "〒", + None, + "十", + "卄", + "卅", + None, + None, + None, + None, + None, + " ゙", + " ゚", + None, + "より", + None, + "コト", + None, + None, + None, + "ᄀ", + "ᄁ", + "ᆪ", + "ᄂ", + "ᆬ", + "ᆭ", + "ᄃ", + "ᄄ", + "ᄅ", + "ᆰ", + "ᆱ", + "ᆲ", + "ᆳ", + "ᆴ", + "ᆵ", + "ᄚ", + "ᄆ", + "ᄇ", + "ᄈ", + "ᄡ", + "ᄉ", + "ᄊ", + "ᄋ", + "ᄌ", + "ᄍ", + "ᄎ", + "ᄏ", + "ᄐ", + "ᄑ", + "ᄒ", + "ᅡ", + "ᅢ", + "ᅣ", + "ᅤ", + "ᅥ", + "ᅦ", + "ᅧ", + "ᅨ", + "ᅩ", + "ᅪ", + "ᅫ", + "ᅬ", + "ᅭ", + "ᅮ", + "ᅯ", + "ᅰ", + "ᅱ", + "ᅲ", + "ᅳ", + "ᅴ", + "ᅵ", + None, + "ᄔ", + "ᄕ", + "ᇇ", + "ᇈ", + "ᇌ", + "ᇎ", + "ᇓ", + "ᇗ", + "ᇙ", + "ᄜ", + "ᇝ", + "ᇟ", + "ᄝ", + "ᄞ", + "ᄠ", + "ᄢ", + "ᄣ", + "ᄧ", + "ᄩ", + "ᄫ", + "ᄬ", + "ᄭ", + "ᄮ", + "ᄯ", + "ᄲ", + "ᄶ", + "ᅀ", + "ᅇ", + "ᅌ", + "ᇱ", + "ᇲ", + "ᅗ", + "ᅘ", + "ᅙ", + "ᆄ", + "ᆅ", + "ᆈ", + "ᆑ", + "ᆒ", + "ᆔ", + "ᆞ", + "ᆡ", + None, + None, + "一", + "二", + "三", + "四", + "上", + "中", + "下", + "甲", + "乙", + "丙", + "丁", + "天", + "地", + "人", + None, + None, + None, + "(ᄀ)", + "(ᄂ)", + "(ᄃ)", + "(ᄅ)", + "(ᄆ)", + "(ᄇ)", + "(ᄉ)", + "(ᄋ)", + "(ᄌ)", + "(ᄎ)", + "(ᄏ)", + "(ᄐ)", + "(ᄑ)", + "(ᄒ)", + "(가)", + "(나)", + "(다)", + "(라)", + "(마)", + "(바)", + "(사)", + "(아)", + "(자)", + "(차)", + "(카)", + "(타)", + "(파)", + "(하)", + "(주)", + "(오전)", + "(오후)", + None, + "(一)", + "(二)", + "(三)", + "(四)", + "(五)", + "(六)", + "(七)", + "(八)", + "(九)", + "(十)", + "(月)", + "(火)", + "(水)", + "(木)", + "(金)", + "(土)", + "(日)", + "(株)", + "(有)", + "(社)", + "(名)", + "(特)", + "(財)", + "(祝)", + "(労)", + "(代)", + "(呼)", + "(学)", + "(監)", + "(企)", + "(資)", + "(協)", + "(祭)", + "(休)", + "(自)", + "(至)", + "問", + "幼", + "文", + "箏", + None, + "pte", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "32", + "33", + "34", + "35", + "ᄀ", + "ᄂ", + "ᄃ", + "ᄅ", + "ᄆ", + "ᄇ", + "ᄉ", + "ᄋ", + "ᄌ", + "ᄎ", + "ᄏ", + "ᄐ", + "ᄑ", + "ᄒ", + "가", + "나", + "다", + "라", + "마", + "바", + "사", + "아", + "자", + "차", + "카", + "타", + "파", + "하", + "참고", + "주의", + "우", + None, + "一", + "二", + "三", + "四", + "五", + "六", + "七", + "八", + "九", + "十", + "月", + "火", + "水", + "木", + "金", + "土", + "日", + "株", + "有", + "社", + "名", + "特", + "財", + "祝", + "労", + "秘", + "男", + "女", + "適", + "優", + "印", + "注", + "項", + "休", + "写", + "正", + "上", + "中", + "下", + "左", + "右", + "医", + "宗", + "学", + "監", + "企", + "資", + "協", + "夜", + "36", + "37", + "38", + "39", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "50", + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月", + "hg", + "erg", + "ev", + "ltd", + "ア", + "イ", + "ウ", + "エ", + "オ", + "カ", + "キ", + "ク", + "ケ", + "コ", + "サ", + "シ", + "ス", + "セ", + "ソ", + "タ", + "チ", + "ツ", + "テ", + "ト", + "ナ", + "ニ", + "ヌ", + "ネ", + "ノ", + "ハ", + "ヒ", + "フ", + "ヘ", + "ホ", + "マ", + "ミ", + "ム", + "メ", + "モ", + "ヤ", + "ユ", + "ヨ", + "ラ", + "リ", + "ル", + "レ", + "ロ", + "ワ", + "ヰ", + "ヱ", + "ヲ", + "令和", + "アパート", + "アルファ", + "アンペア", + "アール", + "イニング", + "インチ", + "ウォン", + "エスクード", + "エーカー", + "オンス", + "オーム", + "カイリ", + "カラット", + "カロリー", + "ガロン", + "ガンマ", + "ギガ", + "ギニー", + "キュリー", + "ギルダー", + "キロ", + "キログラム", + "キロメートル", + "キロワット", + "グラム", + "グラムトン", + "クルゼイロ", + "クローネ", + "ケース", + "コルナ", + "コーポ", + "サイクル", + "サンチーム", + "シリング", + "センチ", + "セント", + "ダース", + "デシ", + "ドル", + "トン", + "ナノ", + "ノット", + "ハイツ", + "パーセント", + "パーツ", + "バーレル", + "ピアストル", + "ピクル", + "ピコ", + "ビル", + "ファラッド", + "フィート", + "ブッシェル", + "フラン", + "ヘクタール", + "ペソ", + "ペニヒ", + "ヘルツ", + "ペンス", + "ページ", + "ベータ", + "ポイント", + "ボルト", + "ホン", + "ポンド", + "ホール", + "ホーン", + "マイクロ", + "マイル", + "マッハ", + "マルク", + "マンション", + "ミクロン", + "ミリ", + "ミリバール", + "メガ", + "メガトン", + "メートル", + "ヤード", + "ヤール", + "ユアン", + "リットル", + "リラ", + "ルピー", + "ルーブル", + "レム", + "レントゲン", + "ワット", + "0点", + "1点", + "2点", + "3点", + "4点", + "5点", + "6点", + "7点", + "8点", + "9点", + "10点", + "11点", + "12点", + "13点", + "14点", + "15点", + "16点", + "17点", + "18点", + "19点", + "20点", + "21点", + "22点", + "23点", + "24点", + "hpa", + "da", + "au", + "bar", + "ov", + "pc", + "dm", + "dm2", + "dm3", + "iu", + "平成", + "昭和", + "大正", + "明治", + "株式会社", + "pa", + "na", + "μa", + "ma", + "ka", + "kb", + "mb", + "gb", + "cal", + "kcal", + "pf", + "nf", + "μf", + "μg", + "mg", + "kg", + "hz", + "khz", + "mhz", + "ghz", + "thz", + "μl", + "ml", + "dl", + "kl", + "fm", + "nm", + "μm", + "mm", + "cm", + "km", + "mm2", + "cm2", + "m2", + "km2", + "mm3", + "cm3", + "m3", + "km3", + "m∕s", + "m∕s2", + "pa", + "kpa", + "mpa", + "gpa", + "rad", + "rad∕s", + "rad∕s2", + "ps", + "ns", + "μs", + "ms", + "pv", + "nv", + "μv", + "mv", + "kv", + "mv", + "pw", + "nw", + "μw", + "mw", + "kw", + "mw", + "kω", + "mω", + None, + "bq", + "cc", + "cd", + "c∕kg", + None, + "db", + "gy", + "ha", + "hp", + "in", + "kk", + "km", + "kt", + "lm", + "ln", + "log", + "lx", + "mb", + "mil", + "mol", + "ph", + None, + "ppm", + "pr", + "sr", + "sv", + "wb", + "v∕m", + "a∕m", + "1日", + "2日", + "3日", + "4日", + "5日", + "6日", + "7日", + "8日", + "9日", + "10日", + "11日", + "12日", + "13日", + "14日", + "15日", + "16日", + "17日", + "18日", + "19日", + "20日", + "21日", + "22日", + "23日", + "24日", + "25日", + "26日", + "27日", + "28日", + "29日", + "30日", + "31日", + "gal", + None, + None, + None, + None, + None, + None, + "ꙁ", + None, + "ꙃ", + None, + "ꙅ", + None, + "ꙇ", + None, + "ꙉ", + None, + "ꙋ", + None, + "ꙍ", + None, + "ꙏ", + None, + "ꙑ", + None, + "ꙓ", + None, + "ꙕ", + None, + "ꙗ", + None, + "ꙙ", + None, + "ꙛ", + None, + "ꙝ", + None, + "ꙟ", + None, + "ꙡ", + None, + "ꙣ", + None, + "ꙥ", + None, + "ꙧ", + None, + "ꙩ", + None, + "ꙫ", + None, + "ꙭ", + None, + "ꚁ", + None, + "ꚃ", + None, + "ꚅ", + None, + "ꚇ", + None, + "ꚉ", + None, + "ꚋ", + None, + "ꚍ", + None, + "ꚏ", + None, + "ꚑ", + None, + "ꚓ", + None, + "ꚕ", + None, + "ꚗ", + None, + "ꚙ", + None, + "ꚛ", + None, + "ъ", + "ь", + None, + None, + None, + "ꜣ", + None, + "ꜥ", + None, + "ꜧ", + None, + "ꜩ", + None, + "ꜫ", + None, + "ꜭ", + None, + "ꜯ", + None, + "ꜳ", + None, + "ꜵ", + None, + "ꜷ", + None, + "ꜹ", + None, + "ꜻ", + None, + "ꜽ", + None, + "ꜿ", + None, + "ꝁ", + None, + "ꝃ", + None, + "ꝅ", + None, + "ꝇ", + None, + "ꝉ", + None, + "ꝋ", + None, + "ꝍ", + None, + "ꝏ", + None, + "ꝑ", + None, + "ꝓ", + None, + "ꝕ", + None, + "ꝗ", + None, + "ꝙ", + None, + "ꝛ", + None, + "ꝝ", + None, + "ꝟ", + None, + "ꝡ", + None, + "ꝣ", + None, + "ꝥ", + None, + "ꝧ", + None, + "ꝩ", + None, + "ꝫ", + None, + "ꝭ", + None, + "ꝯ", + None, + "ꝯ", + None, + "ꝺ", + None, + "ꝼ", + None, + "ᵹ", + "ꝿ", + None, + "ꞁ", + None, + "ꞃ", + None, + "ꞅ", + None, + "ꞇ", + None, + "ꞌ", + None, + "ɥ", + None, + "ꞑ", + None, + "ꞓ", + None, + "ꞗ", + None, + "ꞙ", + None, + "ꞛ", + None, + "ꞝ", + None, + "ꞟ", + None, + "ꞡ", + None, + "ꞣ", + None, + "ꞥ", + None, + "ꞧ", + None, + "ꞩ", + None, + "ɦ", + "ɜ", + "ɡ", + "ɬ", + "ɪ", + None, + "ʞ", + "ʇ", + "ʝ", + "ꭓ", + "ꞵ", + None, + "ꞷ", + None, + "ꞹ", + None, + "ꞻ", + None, + "ꞽ", + None, + "ꞿ", + None, + "ꟁ", + None, + "ꟃ", + None, + "ꞔ", + "ʂ", + "ᶎ", + "ꟈ", + None, + "ꟊ", + None, + "ɤ", + "ꟍ", + None, + "꟏", + None, + "ꟑ", + None, + "ꟓ", + None, + "ꟕ", + None, + "ꟗ", + None, + "ꟙ", + None, + "ꟛ", + None, + "ƛ", + "ɷ", + None, + "ɼ", + None, + "s", + "c", + "f", + "q", + "ꟶ", + None, + "ħ", + "œ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ꜧ", + "ꬷ", + "ɫ", + "ꭒ", + None, + "ʍ", + None, + "ꭋ", + "ꭌ", + None, + "Ꭰ", + "Ꭱ", + "Ꭲ", + "Ꭳ", + "Ꭴ", + "Ꭵ", + "Ꭶ", + "Ꭷ", + "Ꭸ", + "Ꭹ", + "Ꭺ", + "Ꭻ", + "Ꭼ", + "Ꭽ", + "Ꭾ", + "Ꭿ", + "Ꮀ", + "Ꮁ", + "Ꮂ", + "Ꮃ", + "Ꮄ", + "Ꮅ", + "Ꮆ", + "Ꮇ", + "Ꮈ", + "Ꮉ", + "Ꮊ", + "Ꮋ", + "Ꮌ", + "Ꮍ", + "Ꮎ", + "Ꮏ", + "Ꮐ", + "Ꮑ", + "Ꮒ", + "Ꮓ", + "Ꮔ", + "Ꮕ", + "Ꮖ", + "Ꮗ", + "Ꮘ", + "Ꮙ", + "Ꮚ", + "Ꮛ", + "Ꮜ", + "Ꮝ", + "Ꮞ", + "Ꮟ", + "Ꮠ", + "Ꮡ", + "Ꮢ", + "Ꮣ", + "Ꮤ", + "Ꮥ", + "Ꮦ", + "Ꮧ", + "Ꮨ", + "Ꮩ", + "Ꮪ", + "Ꮫ", + "Ꮬ", + "Ꮭ", + "Ꮮ", + "Ꮯ", + "Ꮰ", + "Ꮱ", + "Ꮲ", + "Ꮳ", + "Ꮴ", + "Ꮵ", + "Ꮶ", + "Ꮷ", + "Ꮸ", + "Ꮹ", + "Ꮺ", + "Ꮻ", + "Ꮼ", + "Ꮽ", + "Ꮾ", + "Ꮿ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "豈", + "更", + "車", + "賈", + "滑", + "串", + "句", + "龜", + "契", + "金", + "喇", + "奈", + "懶", + "癩", + "羅", + "蘿", + "螺", + "裸", + "邏", + "樂", + "洛", + "烙", + "珞", + "落", + "酪", + "駱", + "亂", + "卵", + "欄", + "爛", + "蘭", + "鸞", + "嵐", + "濫", + "藍", + "襤", + "拉", + "臘", + "蠟", + "廊", + "朗", + "浪", + "狼", + "郎", + "來", + "冷", + "勞", + "擄", + "櫓", + "爐", + "盧", + "老", + "蘆", + "虜", + "路", + "露", + "魯", + "鷺", + "碌", + "祿", + "綠", + "菉", + "錄", + "鹿", + "論", + "壟", + "弄", + "籠", + "聾", + "牢", + "磊", + "賂", + "雷", + "壘", + "屢", + "樓", + "淚", + "漏", + "累", + "縷", + "陋", + "勒", + "肋", + "凜", + "凌", + "稜", + "綾", + "菱", + "陵", + "讀", + "拏", + "樂", + "諾", + "丹", + "寧", + "怒", + "率", + "異", + "北", + "磻", + "便", + "復", + "不", + "泌", + "數", + "索", + "參", + "塞", + "省", + "葉", + "說", + "殺", + "辰", + "沈", + "拾", + "若", + "掠", + "略", + "亮", + "兩", + "凉", + "梁", + "糧", + "良", + "諒", + "量", + "勵", + "呂", + "女", + "廬", + "旅", + "濾", + "礪", + "閭", + "驪", + "麗", + "黎", + "力", + "曆", + "歷", + "轢", + "年", + "憐", + "戀", + "撚", + "漣", + "煉", + "璉", + "秊", + "練", + "聯", + "輦", + "蓮", + "連", + "鍊", + "列", + "劣", + "咽", + "烈", + "裂", + "說", + "廉", + "念", + "捻", + "殮", + "簾", + "獵", + "令", + "囹", + "寧", + "嶺", + "怜", + "玲", + "瑩", + "羚", + "聆", + "鈴", + "零", + "靈", + "領", + "例", + "禮", + "醴", + "隸", + "惡", + "了", + "僚", + "寮", + "尿", + "料", + "樂", + "燎", + "療", + "蓼", + "遼", + "龍", + "暈", + "阮", + "劉", + "杻", + "柳", + "流", + "溜", + "琉", + "留", + "硫", + "紐", + "類", + "六", + "戮", + "陸", + "倫", + "崙", + "淪", + "輪", + "律", + "慄", + "栗", + "率", + "隆", + "利", + "吏", + "履", + "易", + "李", + "梨", + "泥", + "理", + "痢", + "罹", + "裏", + "裡", + "里", + "離", + "匿", + "溺", + "吝", + "燐", + "璘", + "藺", + "隣", + "鱗", + "麟", + "林", + "淋", + "臨", + "立", + "笠", + "粒", + "狀", + "炙", + "識", + "什", + "茶", + "刺", + "切", + "度", + "拓", + "糖", + "宅", + "洞", + "暴", + "輻", + "行", + "降", + "見", + "廓", + "兀", + "嗀", + None, + "塚", + None, + "晴", + None, + "凞", + "猪", + "益", + "礼", + "神", + "祥", + "福", + "靖", + "精", + "羽", + None, + "蘒", + None, + "諸", + None, + "逸", + "都", + None, + "飯", + "飼", + "館", + "鶴", + "郞", + "隷", + "侮", + "僧", + "免", + "勉", + "勤", + "卑", + "喝", + "嘆", + "器", + "塀", + "墨", + "層", + "屮", + "悔", + "慨", + "憎", + "懲", + "敏", + "既", + "暑", + "梅", + "海", + "渚", + "漢", + "煮", + "爫", + "琢", + "碑", + "社", + "祉", + "祈", + "祐", + "祖", + "祝", + "禍", + "禎", + "穀", + "突", + "節", + "練", + "縉", + "繁", + "署", + "者", + "臭", + "艹", + "著", + "褐", + "視", + "謁", + "謹", + "賓", + "贈", + "辶", + "逸", + "難", + "響", + "頻", + "恵", + "𤋮", + "舘", + None, + "並", + "况", + "全", + "侀", + "充", + "冀", + "勇", + "勺", + "喝", + "啕", + "喙", + "嗢", + "塚", + "墳", + "奄", + "奔", + "婢", + "嬨", + "廒", + "廙", + "彩", + "徭", + "惘", + "慎", + "愈", + "憎", + "慠", + "懲", + "戴", + "揄", + "搜", + "摒", + "敖", + "晴", + "朗", + "望", + "杖", + "歹", + "殺", + "流", + "滛", + "滋", + "漢", + "瀞", + "煮", + "瞧", + "爵", + "犯", + "猪", + "瑱", + "甆", + "画", + "瘝", + "瘟", + "益", + "盛", + "直", + "睊", + "着", + "磌", + "窱", + "節", + "类", + "絛", + "練", + "缾", + "者", + "荒", + "華", + "蝹", + "襁", + "覆", + "視", + "調", + "諸", + "請", + "謁", + "諾", + "諭", + "謹", + "變", + "贈", + "輸", + "遲", + "醙", + "鉶", + "陼", + "難", + "靖", + "韛", + "響", + "頋", + "頻", + "鬒", + "龜", + "𢡊", + "𢡄", + "𣏕", + "㮝", + "䀘", + "䀹", + "𥉉", + "𥳐", + "𧻓", + "齃", + "龎", + None, + "ff", + "fi", + "fl", + "ffi", + "ffl", + "st", + None, + "մն", + "մե", + "մի", + "վն", + "մխ", + None, + "יִ", + None, + "ײַ", + "ע", + "א", + "ד", + "ה", + "כ", + "ל", + "ם", + "ר", + "ת", + "+", + "שׁ", + "שׂ", + "שּׁ", + "שּׂ", + "אַ", + "אָ", + "אּ", + "בּ", + "גּ", + "דּ", + "הּ", + "וּ", + "זּ", + None, + "טּ", + "יּ", + "ךּ", + "כּ", + "לּ", + None, + "מּ", + None, + "נּ", + "סּ", + None, + "ףּ", + "פּ", + None, + "צּ", + "קּ", + "רּ", + "שּ", + "תּ", + "וֹ", + "בֿ", + "כֿ", + "פֿ", + "אל", + "ٱ", + "ٻ", + "پ", + "ڀ", + "ٺ", + "ٿ", + "ٹ", + "ڤ", + "ڦ", + "ڄ", + "ڃ", + "چ", + "ڇ", + "ڍ", + "ڌ", + "ڎ", + "ڈ", + "ژ", + "ڑ", + "ک", + "گ", + "ڳ", + "ڱ", + "ں", + "ڻ", + "ۀ", + "ہ", + "ھ", + "ے", + "ۓ", + None, + "ڭ", + "ۇ", + "ۆ", + "ۈ", + "ۇٴ", + "ۋ", + "ۅ", + "ۉ", + "ې", + "ى", + "ئا", + "ئە", + "ئو", + "ئۇ", + "ئۆ", + "ئۈ", + "ئې", + "ئى", + "ی", + "ئج", + "ئح", + "ئم", + "ئى", + "ئي", + "بج", + "بح", + "بخ", + "بم", + "بى", + "بي", + "تج", + "تح", + "تخ", + "تم", + "تى", + "تي", + "ثج", + "ثم", + "ثى", + "ثي", + "جح", + "جم", + "حج", + "حم", + "خج", + "خح", + "خم", + "سج", + "سح", + "سخ", + "سم", + "صح", + "صم", + "ضج", + "ضح", + "ضخ", + "ضم", + "طح", + "طم", + "ظم", + "عج", + "عم", + "غج", + "غم", + "فج", + "فح", + "فخ", + "فم", + "فى", + "في", + "قح", + "قم", + "قى", + "قي", + "كا", + "كج", + "كح", + "كخ", + "كل", + "كم", + "كى", + "كي", + "لج", + "لح", + "لخ", + "لم", + "لى", + "لي", + "مج", + "مح", + "مخ", + "مم", + "مى", + "مي", + "نج", + "نح", + "نخ", + "نم", + "نى", + "ني", + "هج", + "هم", + "هى", + "هي", + "يج", + "يح", + "يخ", + "يم", + "يى", + "يي", + "ذٰ", + "رٰ", + "ىٰ", + " ٌّ", + " ٍّ", + " َّ", + " ُّ", + " ِّ", + " ّٰ", + "ئر", + "ئز", + "ئم", + "ئن", + "ئى", + "ئي", + "بر", + "بز", + "بم", + "بن", + "بى", + "بي", + "تر", + "تز", + "تم", + "تن", + "تى", + "تي", + "ثر", + "ثز", + "ثم", + "ثن", + "ثى", + "ثي", + "فى", + "في", + "قى", + "قي", + "كا", + "كل", + "كم", + "كى", + "كي", + "لم", + "لى", + "لي", + "ما", + "مم", + "نر", + "نز", + "نم", + "نن", + "نى", + "ني", + "ىٰ", + "ير", + "يز", + "يم", + "ين", + "يى", + "يي", + "ئج", + "ئح", + "ئخ", + "ئم", + "ئه", + "بج", + "بح", + "بخ", + "بم", + "به", + "تج", + "تح", + "تخ", + "تم", + "ته", + "ثم", + "جح", + "جم", + "حج", + "حم", + "خج", + "خم", + "سج", + "سح", + "سخ", + "سم", + "صح", + "صخ", + "صم", + "ضج", + "ضح", + "ضخ", + "ضم", + "طح", + "ظم", + "عج", + "عم", + "غج", + "غم", + "فج", + "فح", + "فخ", + "فم", + "قح", + "قم", + "كج", + "كح", + "كخ", + "كل", + "كم", + "لج", + "لح", + "لخ", + "لم", + "له", + "مج", + "مح", + "مخ", + "مم", + "نج", + "نح", + "نخ", + "نم", + "نه", + "هج", + "هم", + "هٰ", + "يج", + "يح", + "يخ", + "يم", + "يه", + "ئم", + "ئه", + "بم", + "به", + "تم", + "ته", + "ثم", + "ثه", + "سم", + "سه", + "شم", + "شه", + "كل", + "كم", + "لم", + "نم", + "نه", + "يم", + "يه", + "ـَّ", + "ـُّ", + "ـِّ", + "طى", + "طي", + "عى", + "عي", + "غى", + "غي", + "سى", + "سي", + "شى", + "شي", + "حى", + "حي", + "جى", + "جي", + "خى", + "خي", + "صى", + "صي", + "ضى", + "ضي", + "شج", + "شح", + "شخ", + "شم", + "شر", + "سر", + "صر", + "ضر", + "طى", + "طي", + "عى", + "عي", + "غى", + "غي", + "سى", + "سي", + "شى", + "شي", + "حى", + "حي", + "جى", + "جي", + "خى", + "خي", + "صى", + "صي", + "ضى", + "ضي", + "شج", + "شح", + "شخ", + "شم", + "شر", + "سر", + "صر", + "ضر", + "شج", + "شح", + "شخ", + "شم", + "سه", + "شه", + "طم", + "سج", + "سح", + "سخ", + "شج", + "شح", + "شخ", + "طم", + "ظم", + "اً", + None, + "تجم", + "تحج", + "تحم", + "تخم", + "تمج", + "تمح", + "تمخ", + "جمح", + "حمي", + "حمى", + "سحج", + "سجح", + "سجى", + "سمح", + "سمج", + "سمم", + "صحح", + "صمم", + "شحم", + "شجي", + "شمخ", + "شمم", + "ضحى", + "ضخم", + "طمح", + "طمم", + "طمي", + "عجم", + "عمم", + "عمى", + "غمم", + "غمي", + "غمى", + "فخم", + "قمح", + "قمم", + "لحم", + "لحي", + "لحى", + "لجج", + "لخم", + "لمح", + "محج", + "محم", + "محي", + "مجح", + "مجم", + "مخج", + "مخم", + None, + "مجخ", + "همج", + "همم", + "نحم", + "نحى", + "نجم", + "نجى", + "نمي", + "نمى", + "يمم", + "بخي", + "تجي", + "تجى", + "تخي", + "تخى", + "تمي", + "تمى", + "جمي", + "جحى", + "جمى", + "سخى", + "صحي", + "شحي", + "ضحي", + "لجي", + "لمي", + "يحي", + "يجي", + "يمي", + "ممي", + "قمي", + "نحي", + "قمح", + "لحم", + "عمي", + "كمي", + "نجح", + "مخي", + "لجم", + "كمم", + "لجم", + "نجح", + "جحي", + "حجي", + "مجي", + "فمي", + "بحي", + "كمم", + "عجم", + "صمم", + "سخي", + "نجي", + None, + None, + "صلے", + "قلے", + "الله", + "اكبر", + "محمد", + "صلعم", + "رسول", + "عليه", + "وسلم", + "صلى", + "صلى الله عليه وسلم", + "جل جلاله", + "ریال", + None, + None, + ",", + "、", + None, + ":", + ";", + "!", + "?", + "〖", + "〗", + None, + None, + None, + "—", + "–", + "_", + "(", + ")", + "{", + "}", + "〔", + "〕", + "【", + "】", + "《", + "》", + "〈", + "〉", + "「", + "」", + "『", + "』", + None, + "[", + "]", + " ̅", + "_", + ",", + "、", + None, + ";", + ":", + "?", + "!", + "—", + "(", + ")", + "{", + "}", + "〔", + "〕", + "#", + "&", + "*", + "+", + "-", + "<", + ">", + "=", + None, + "\\", + "$", + "%", + "@", + None, + " ً", + "ـً", + " ٌ", + None, + " ٍ", + None, + " َ", + "ـَ", + " ُ", + "ـُ", + " ِ", + "ـِ", + " ّ", + "ـّ", + " ْ", + "ـْ", + "ء", + "آ", + "أ", + "ؤ", + "إ", + "ئ", + "ا", + "ب", + "ة", + "ت", + "ث", + "ج", + "ح", + "خ", + "د", + "ذ", + "ر", + "ز", + "س", + "ش", + "ص", + "ض", + "ط", + "ظ", + "ع", + "غ", + "ف", + "ق", + "ك", + "ل", + "م", + "ن", + "ه", + "و", + "ى", + "ي", + "لآ", + "لأ", + "لإ", + "لا", + None, + None, + None, + "!", + '"', + "#", + "$", + "%", + "&", + "'", + "(", + ")", + "*", + "+", + ",", + "-", + ".", + "/", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + ":", + ";", + "<", + "=", + ">", + "?", + "@", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "[", + "\\", + "]", + "^", + "_", + "`", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "{", + "|", + "}", + "~", + "⦅", + "⦆", + ".", + "「", + "」", + "、", + "・", + "ヲ", + "ァ", + "ィ", + "ゥ", + "ェ", + "ォ", + "ャ", + "ュ", + "ョ", + "ッ", + "ー", + "ア", + "イ", + "ウ", + "エ", + "オ", + "カ", + "キ", + "ク", + "ケ", + "コ", + "サ", + "シ", + "ス", + "セ", + "ソ", + "タ", + "チ", + "ツ", + "テ", + "ト", + "ナ", + "ニ", + "ヌ", + "ネ", + "ノ", + "ハ", + "ヒ", + "フ", + "ヘ", + "ホ", + "マ", + "ミ", + "ム", + "メ", + "モ", + "ヤ", + "ユ", + "ヨ", + "ラ", + "リ", + "ル", + "レ", + "ロ", + "ワ", + "ン", + "゙", + "゚", + None, + "ᄀ", + "ᄁ", + "ᆪ", + "ᄂ", + "ᆬ", + "ᆭ", + "ᄃ", + "ᄄ", + "ᄅ", + "ᆰ", + "ᆱ", + "ᆲ", + "ᆳ", + "ᆴ", + "ᆵ", + "ᄚ", + "ᄆ", + "ᄇ", + "ᄈ", + "ᄡ", + "ᄉ", + "ᄊ", + "ᄋ", + "ᄌ", + "ᄍ", + "ᄎ", + "ᄏ", + "ᄐ", + "ᄑ", + "ᄒ", + None, + "ᅡ", + "ᅢ", + "ᅣ", + "ᅤ", + "ᅥ", + "ᅦ", + None, + "ᅧ", + "ᅨ", + "ᅩ", + "ᅪ", + "ᅫ", + "ᅬ", + None, + "ᅭ", + "ᅮ", + "ᅯ", + "ᅰ", + "ᅱ", + "ᅲ", + None, + "ᅳ", + "ᅴ", + "ᅵ", + None, + "¢", + "£", + "¬", + " ̄", + "¦", + "¥", + "₩", + None, + "│", + "←", + "↑", + "→", + "↓", + "■", + "○", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "𐐨", + "𐐩", + "𐐪", + "𐐫", + "𐐬", + "𐐭", + "𐐮", + "𐐯", + "𐐰", + "𐐱", + "𐐲", + "𐐳", + "𐐴", + "𐐵", + "𐐶", + "𐐷", + "𐐸", + "𐐹", + "𐐺", + "𐐻", + "𐐼", + "𐐽", + "𐐾", + "𐐿", + "𐑀", + "𐑁", + "𐑂", + "𐑃", + "𐑄", + "𐑅", + "𐑆", + "𐑇", + "𐑈", + "𐑉", + "𐑊", + "𐑋", + "𐑌", + "𐑍", + "𐑎", + "𐑏", + None, + None, + None, + None, + "𐓘", + "𐓙", + "𐓚", + "𐓛", + "𐓜", + "𐓝", + "𐓞", + "𐓟", + "𐓠", + "𐓡", + "𐓢", + "𐓣", + "𐓤", + "𐓥", + "𐓦", + "𐓧", + "𐓨", + "𐓩", + "𐓪", + "𐓫", + "𐓬", + "𐓭", + "𐓮", + "𐓯", + "𐓰", + "𐓱", + "𐓲", + "𐓳", + "𐓴", + "𐓵", + "𐓶", + "𐓷", + "𐓸", + "𐓹", + "𐓺", + "𐓻", + None, + None, + None, + None, + None, + None, + None, + None, + "𐖗", + "𐖘", + "𐖙", + "𐖚", + "𐖛", + "𐖜", + "𐖝", + "𐖞", + "𐖟", + "𐖠", + "𐖡", + None, + "𐖣", + "𐖤", + "𐖥", + "𐖦", + "𐖧", + "𐖨", + "𐖩", + "𐖪", + "𐖫", + "𐖬", + "𐖭", + "𐖮", + "𐖯", + "𐖰", + "𐖱", + None, + "𐖳", + "𐖴", + "𐖵", + "𐖶", + "𐖷", + "𐖸", + "𐖹", + None, + "𐖻", + "𐖼", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ː", + "ˑ", + "æ", + "ʙ", + "ɓ", + None, + "ʣ", + "ꭦ", + "ʥ", + "ʤ", + "ɖ", + "ɗ", + "ᶑ", + "ɘ", + "ɞ", + "ʩ", + "ɤ", + "ɢ", + "ɠ", + "ʛ", + "ħ", + "ʜ", + "ɧ", + "ʄ", + "ʪ", + "ʫ", + "ɬ", + "𝼄", + "ꞎ", + "ɮ", + "𝼅", + "ʎ", + "𝼆", + "ø", + "ɶ", + "ɷ", + "q", + "ɺ", + "𝼈", + "ɽ", + "ɾ", + "ʀ", + "ʨ", + "ʦ", + "ꭧ", + "ʧ", + "ʈ", + "ⱱ", + None, + "ʏ", + "ʡ", + "ʢ", + "ʘ", + "ǀ", + "ǁ", + "ǂ", + "𝼊", + "𝼞", + "ʇ", + "ʖ", + "ʗ", + "ʞ", + "𝼋", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "𐳀", + "𐳁", + "𐳂", + "𐳃", + "𐳄", + "𐳅", + "𐳆", + "𐳇", + "𐳈", + "𐳉", + "𐳊", + "𐳋", + "𐳌", + "𐳍", + "𐳎", + "𐳏", + "𐳐", + "𐳑", + "𐳒", + "𐳓", + "𐳔", + "𐳕", + "𐳖", + "𐳗", + "𐳘", + "𐳙", + "𐳚", + "𐳛", + "𐳜", + "𐳝", + "𐳞", + "𐳟", + "𐳠", + "𐳡", + "𐳢", + "𐳣", + "𐳤", + "𐳥", + "𐳦", + "𐳧", + "𐳨", + "𐳩", + "𐳪", + "𐳫", + "𐳬", + "𐳭", + "𐳮", + "𐳯", + "𐳰", + "𐳱", + "𐳲", + None, + None, + None, + None, + None, + None, + None, + None, + "𐵰", + "𐵱", + "𐵲", + "𐵳", + "𐵴", + "𐵵", + "𐵶", + "𐵷", + "𐵸", + "𐵹", + "𐵺", + "𐵻", + "𐵼", + "𐵽", + "𐵾", + "𐵿", + "𐶀", + "𐶁", + "𐶂", + "𐶃", + "𐶄", + "𐶅", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "𑣀", + "𑣁", + "𑣂", + "𑣃", + "𑣄", + "𑣅", + "𑣆", + "𑣇", + "𑣈", + "𑣉", + "𑣊", + "𑣋", + "𑣌", + "𑣍", + "𑣎", + "𑣏", + "𑣐", + "𑣑", + "𑣒", + "𑣓", + "𑣔", + "𑣕", + "𑣖", + "𑣗", + "𑣘", + "𑣙", + "𑣚", + "𑣛", + "𑣜", + "𑣝", + "𑣞", + "𑣟", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "𖹠", + "𖹡", + "𖹢", + "𖹣", + "𖹤", + "𖹥", + "𖹦", + "𖹧", + "𖹨", + "𖹩", + "𖹪", + "𖹫", + "𖹬", + "𖹭", + "𖹮", + "𖹯", + "𖹰", + "𖹱", + "𖹲", + "𖹳", + "𖹴", + "𖹵", + "𖹶", + "𖹷", + "𖹸", + "𖹹", + "𖹺", + "𖹻", + "𖹼", + "𖹽", + "𖹾", + "𖹿", + None, + None, + "𖺻", + "𖺼", + "𖺽", + "𖺾", + "𖺿", + "𖻀", + "𖻁", + "𖻂", + "𖻃", + "𖻄", + "𖻅", + "𖻆", + "𖻇", + "𖻈", + "𖻉", + "𖻊", + "𖻋", + "𖻌", + "𖻍", + "𖻎", + "𖻏", + "𖻐", + "𖻑", + "𖻒", + "𖻓", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "こと", + "トキ", + "トテ", + "ヨリ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "𝅗𝅥", + "𝅘𝅥", + "𝅘𝅥𝅮", + "𝅘𝅥𝅯", + "𝅘𝅥𝅰", + "𝅘𝅥𝅱", + "𝅘𝅥𝅲", + None, + None, + None, + "𝆹𝅥", + "𝆺𝅥", + "𝆹𝅥𝅮", + "𝆺𝅥𝅮", + "𝆹𝅥𝅯", + "𝆺𝅥𝅯", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + None, + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + None, + "c", + "d", + None, + "g", + None, + "j", + "k", + None, + "n", + "o", + "p", + "q", + None, + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + None, + "f", + None, + "h", + "i", + "j", + "k", + "l", + "m", + "n", + None, + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + None, + "d", + "e", + "f", + "g", + None, + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + None, + "s", + "t", + "u", + "v", + "w", + "x", + "y", + None, + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + None, + "d", + "e", + "f", + "g", + None, + "i", + "j", + "k", + "l", + "m", + None, + "o", + None, + "s", + "t", + "u", + "v", + "w", + "x", + "y", + None, + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "ı", + "ȷ", + "ss", + None, + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + "θ", + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "∇", + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "∂", + "ε", + "θ", + "κ", + "φ", + "ρ", + "π", + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + "θ", + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "∇", + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "∂", + "ε", + "θ", + "κ", + "φ", + "ρ", + "π", + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + "θ", + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "∇", + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "∂", + "ε", + "θ", + "κ", + "φ", + "ρ", + "π", + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + "θ", + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "∇", + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "∂", + "ε", + "θ", + "κ", + "φ", + "ρ", + "π", + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + "θ", + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "∇", + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "∂", + "ε", + "θ", + "κ", + "φ", + "ρ", + "π", + "ϝ", + None, + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + None, + None, + None, + None, + None, + None, + None, + None, + None, + "𝽁", + None, + "𝽉", + None, + "𝽋", + None, + "𝽎", + None, + "𝽒", + None, + "𝽩", + None, + "𝽫", + None, + "𝽭", + None, + "𝽯", + None, + "𝽳", + None, + "𝽵", + None, + "𝽷", + None, + "𝽹", + None, + "𝽻", + None, + "𝽽", + None, + "𝽿", + None, + None, + None, + "ss", + None, + None, + "𝽗", + "𝽘", + "𝽙", + "ɣ", + "ᴘ", + "ƀ", + "đ", + "ǥ", + "ł", + "ƚ", + "𝽐", + "𝼾", + "ɉ", + "ᵿ", + "𝼿", + "ƞ", + "ɼ", + "ȸ", + "ȹ", + "𝼫", + "𝼙", + "ʆ", + "ᶘ", + "𝼬", + "𝼜", + "ʓ", + "ᶚ", + "ᴅ", + "ᴀ", + "ᴇ", + "ɿ", + "ʅ", + "ʮ", + "ʯ", + "ȡ", + "ȴ", + "ȵ", + "ȶ", + "ψ", + "ω", + "ᶁ", + "ꞕ", + "ᶇ", + "ᶊ", + "ᶎ", + "ƈ", + "ƙ", + "ƥ", + "ʠ", + "ƭ", + "𝼉", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "а", + "б", + "в", + "г", + "д", + "е", + "ж", + "з", + "и", + "к", + "л", + "м", + "о", + "п", + "р", + "с", + "т", + "у", + "ф", + "х", + "ц", + "ч", + "ш", + "ы", + "э", + "ю", + "ꚉ", + "ә", + "і", + "ј", + "ө", + "ү", + "ӏ", + "а", + "б", + "в", + "г", + "д", + "е", + "ж", + "з", + "и", + "к", + "л", + "о", + "п", + "с", + "у", + "ф", + "х", + "ц", + "ч", + "ш", + "ъ", + "ы", + "ґ", + "і", + "ѕ", + "џ", + "ҫ", + "ꙑ", + "ұ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "𞤢", + "𞤣", + "𞤤", + "𞤥", + "𞤦", + "𞤧", + "𞤨", + "𞤩", + "𞤪", + "𞤫", + "𞤬", + "𞤭", + "𞤮", + "𞤯", + "𞤰", + "𞤱", + "𞤲", + "𞤳", + "𞤴", + "𞤵", + "𞤶", + "𞤷", + "𞤸", + "𞤹", + "𞤺", + "𞤻", + "𞤼", + "𞤽", + "𞤾", + "𞤿", + "𞥀", + "𞥁", + "𞥂", + "𞥃", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ا", + "ب", + "ج", + "د", + None, + "و", + "ز", + "ح", + "ط", + "ي", + "ك", + "ل", + "م", + "ن", + "س", + "ع", + "ف", + "ص", + "ق", + "ر", + "ش", + "ت", + "ث", + "خ", + "ذ", + "ض", + "ظ", + "غ", + "ٮ", + "ں", + "ڡ", + "ٯ", + None, + "ب", + "ج", + None, + "ه", + None, + "ح", + None, + "ي", + "ك", + "ل", + "م", + "ن", + "س", + "ع", + "ف", + "ص", + "ق", + None, + "ش", + "ت", + "ث", + "خ", + None, + "ض", + None, + "غ", + None, + "ج", + None, + "ح", + None, + "ي", + None, + "ل", + None, + "ن", + "س", + "ع", + None, + "ص", + "ق", + None, + "ش", + None, + "خ", + None, + "ض", + None, + "غ", + None, + "ں", + None, + "ٯ", + None, + "ب", + "ج", + None, + "ه", + None, + "ح", + "ط", + "ي", + "ك", + None, + "م", + "ن", + "س", + "ع", + "ف", + "ص", + "ق", + None, + "ش", + "ت", + "ث", + "خ", + None, + "ض", + "ظ", + "غ", + "ٮ", + None, + "ڡ", + None, + "ا", + "ب", + "ج", + "د", + "ه", + "و", + "ز", + "ح", + "ط", + "ي", + None, + "ل", + "م", + "ن", + "س", + "ع", + "ف", + "ص", + "ق", + "ر", + "ش", + "ت", + "ث", + "خ", + "ذ", + "ض", + "ظ", + "غ", + None, + "ب", + "ج", + "د", + None, + "و", + "ز", + "ح", + "ط", + "ي", + None, + "ل", + "م", + "ن", + "س", + "ع", + "ف", + "ص", + "ق", + "ر", + "ش", + "ت", + "ث", + "خ", + "ذ", + "ض", + "ظ", + "غ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "0,", + "1,", + "2,", + "3,", + "4,", + "5,", + "6,", + "7,", + "8,", + "9,", + None, + "(a)", + "(b)", + "(c)", + "(d)", + "(e)", + "(f)", + "(g)", + "(h)", + "(i)", + "(j)", + "(k)", + "(l)", + "(m)", + "(n)", + "(o)", + "(p)", + "(q)", + "(r)", + "(s)", + "(t)", + "(u)", + "(v)", + "(w)", + "(x)", + "(y)", + "(z)", + "〔s〕", + "c", + "r", + "cd", + "wz", + None, + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "hv", + "mv", + "sd", + "ss", + "ppv", + "wc", + None, + "mc", + "md", + "mr", + None, + "dj", + None, + None, + None, + "ほか", + "ココ", + "サ", + None, + "手", + "字", + "双", + "デ", + "二", + "多", + "解", + "天", + "交", + "映", + "無", + "料", + "前", + "後", + "再", + "新", + "初", + "終", + "生", + "販", + "声", + "吹", + "演", + "投", + "捕", + "一", + "三", + "遊", + "左", + "中", + "右", + "指", + "走", + "打", + "禁", + "空", + "合", + "満", + "有", + "月", + "申", + "割", + "営", + "配", + None, + "〔本〕", + "〔三〕", + "〔二〕", + "〔安〕", + "〔点〕", + "〔打〕", + "〔盗〕", + "〔勝〕", + "〔敗〕", + None, + "得", + "可", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "丽", + "丸", + "乁", + "𠄢", + "你", + "侮", + "侻", + "倂", + "偺", + "備", + "僧", + "像", + "㒞", + "𠘺", + "免", + "兔", + "兤", + "具", + "𠔜", + "㒹", + "內", + "再", + "𠕋", + "冗", + "冤", + "仌", + "冬", + "况", + "𩇟", + "凵", + "刃", + "㓟", + "刻", + "剆", + "割", + "剷", + "㔕", + "勇", + "勉", + "勤", + "勺", + "包", + "匆", + "北", + "卉", + "卑", + "博", + "即", + "卽", + "卿", + "𠨬", + "灰", + "及", + "叟", + "𠭣", + "叫", + "叱", + "吆", + "咞", + "吸", + "呈", + "周", + "咢", + "哶", + "唐", + "啓", + "啣", + "善", + "喙", + "喫", + "喳", + "嗂", + "圖", + "嘆", + "圗", + "噑", + "噴", + "切", + "壮", + "城", + "埴", + "堍", + "型", + "堲", + "報", + "墬", + "𡓤", + "売", + "壷", + "夆", + "多", + "夢", + "奢", + "𡚨", + "𡛪", + "姬", + "娛", + "娧", + "姘", + "婦", + "㛮", + "㛼", + "嬈", + "嬾", + "𡧈", + "寃", + "寘", + "寧", + "寳", + "𡬘", + "寿", + "将", + "当", + "尢", + "㞁", + "屠", + "屮", + "峀", + "岍", + "𡷤", + "嵃", + "𡷦", + "嵮", + "嵫", + "嵼", + "巡", + "巢", + "㠯", + "巽", + "帨", + "帽", + "幩", + "㡢", + "𢆃", + "㡼", + "庰", + "庳", + "庶", + "廊", + "𪎒", + "廾", + "𢌱", + "舁", + "弢", + "㣇", + "𣊸", + "𦇚", + "形", + "彫", + "㣣", + "徚", + "忍", + "志", + "忹", + "悁", + "㤺", + "㤜", + "悔", + "𢛔", + "惇", + "慈", + "慌", + "慎", + "慌", + "慺", + "憎", + "憲", + "憤", + "憯", + "懞", + "懲", + "懶", + "成", + "戛", + "扝", + "抱", + "拔", + "捐", + "𢬌", + "挽", + "拼", + "捨", + "掃", + "揤", + "𢯱", + "搢", + "揅", + "掩", + "㨮", + "摩", + "摾", + "撝", + "摷", + "㩬", + "敏", + "敬", + "𣀊", + "旣", + "書", + "晉", + "㬙", + "暑", + "㬈", + "㫤", + "冒", + "冕", + "最", + "暜", + "肭", + "䏙", + "朗", + "望", + "朡", + "杞", + "杓", + "𣏃", + "㭉", + "柺", + "枅", + "桒", + "梅", + "𣑭", + "梎", + "栟", + "椔", + "㮝", + "楂", + "榣", + "槪", + "檨", + "𣚣", + "櫛", + "㰘", + "次", + "𣢧", + "歔", + "㱎", + "歲", + "殟", + "殺", + "殻", + "𣪍", + "𡴋", + "𣫺", + "汎", + "𣲼", + "沿", + "泍", + "汧", + "洖", + "派", + "海", + "流", + "浩", + "浸", + "涅", + "𣴞", + "洴", + "港", + "湮", + "㴳", + "滋", + "滇", + "𣻑", + "淹", + "潮", + "𣽞", + "𣾎", + "濆", + "瀹", + "瀞", + "瀛", + "㶖", + "灊", + "災", + "灷", + "炭", + "𠔥", + "煅", + "𤉣", + "熜", + "𤎫", + "爨", + "爵", + "牐", + "𤘈", + "犀", + "犕", + "𤜵", + "𤠔", + "獺", + "王", + "㺬", + "玥", + "㺸", + "瑇", + "瑜", + "瑱", + "璅", + "瓊", + "㼛", + "甤", + "𤰶", + "甾", + "𤲒", + "異", + "𢆟", + "瘐", + "𤾡", + "𤾸", + "𥁄", + "㿼", + "䀈", + "直", + "𥃳", + "𥃲", + "𥄙", + "𥄳", + "眞", + "真", + "睊", + "䀹", + "瞋", + "䁆", + "䂖", + "𥐝", + "硎", + "碌", + "磌", + "䃣", + "𥘦", + "祖", + "𥚚", + "𥛅", + "福", + "秫", + "䄯", + "穀", + "穊", + "穏", + "𥥼", + "𥪧", + "竮", + "䈂", + "𥮫", + "篆", + "築", + "䈧", + "𥲀", + "糒", + "䊠", + "糨", + "糣", + "紀", + "𥾆", + "絣", + "䌁", + "緇", + "縂", + "繅", + "䌴", + "𦈨", + "𦉇", + "䍙", + "𦋙", + "罺", + "𦌾", + "羕", + "翺", + "者", + "𦓚", + "𦔣", + "聠", + "𦖨", + "聰", + "𣍟", + "䏕", + "育", + "脃", + "䐋", + "脾", + "媵", + "𦞧", + "𦞵", + "𣎓", + "𣎜", + "舁", + "舄", + "辞", + "䑫", + "芑", + "芋", + "芝", + "劳", + "花", + "芳", + "芽", + "苦", + "𦬼", + "若", + "茝", + "荣", + "莭", + "茣", + "莽", + "菧", + "著", + "荓", + "菊", + "菌", + "菜", + "𦰶", + "𦵫", + "𦳕", + "䔫", + "蓱", + "蓳", + "蔖", + "𧏊", + "蕤", + "𦼬", + "䕝", + "䕡", + "𦾱", + "𧃒", + "䕫", + "虐", + "虜", + "虧", + "虩", + "蚩", + "蚈", + "蜎", + "蛢", + "蝹", + "蜨", + "蝫", + "螆", + "䗗", + "蟡", + "蠁", + "䗹", + "衠", + "衣", + "𧙧", + "裗", + "裞", + "䘵", + "裺", + "㒻", + "𧢮", + "𧥦", + "䚾", + "䛇", + "誠", + "諭", + "變", + "豕", + "𧲨", + "貫", + "賁", + "贛", + "起", + "𧼯", + "𠠄", + "跋", + "趼", + "跰", + "𠣞", + "軔", + "輸", + "𨗒", + "𨗭", + "邔", + "郱", + "鄑", + "𨜮", + "鄛", + "鈸", + "鋗", + "鋘", + "鉼", + "鏹", + "鐕", + "𨯺", + "開", + "䦕", + "閷", + "𨵷", + "䧦", + "雃", + "嶲", + "霣", + "𩅅", + "𩈚", + "䩮", + "䩶", + "韠", + "𩐊", + "䪲", + "𩒖", + "頋", + "頩", + "𩖶", + "飢", + "䬳", + "餩", + "馧", + "駂", + "駾", + "䯎", + "𩬰", + "鬒", + "鱀", + "鳽", + "䳎", + "䳭", + "鵧", + "𪃎", + "䳸", + "𪄅", + "𪈎", + "𪊑", + "麻", + "䵖", + "黹", + "黾", + "鼅", + "鼏", + "鼖", + "鼻", + "𪘀", + None, + None, + None, + None, + None, + None, + None, + None, + None, +) diff --git a/source/target-vendors/cp312/jsonschema-4.26.0.dist-info/METADATA b/source/target-vendors/cp312/jsonschema-4.26.0.dist-info/METADATA new file mode 100644 index 0000000..ba828d2 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema-4.26.0.dist-info/METADATA @@ -0,0 +1,170 @@ +Metadata-Version: 2.4 +Name: jsonschema +Version: 4.26.0 +Summary: An implementation of JSON Schema validation for Python +Project-URL: Homepage, https://github.com/python-jsonschema/jsonschema +Project-URL: Documentation, https://python-jsonschema.readthedocs.io/ +Project-URL: Issues, https://github.com/python-jsonschema/jsonschema/issues/ +Project-URL: Funding, https://github.com/sponsors/Julian +Project-URL: Tidelift, https://tidelift.com/subscription/pkg/pypi-jsonschema?utm_source=pypi-jsonschema&utm_medium=referral&utm_campaign=pypi-link +Project-URL: Changelog, https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst +Project-URL: Source, https://github.com/python-jsonschema/jsonschema +Author-email: Julian Berman +License-Expression: MIT +License-File: COPYING +Keywords: data validation,json,json schema,jsonschema,validation +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: File Formats :: JSON +Classifier: Topic :: File Formats :: JSON :: JSON Schema +Requires-Python: >=3.10 +Requires-Dist: attrs>=22.2.0 +Requires-Dist: jsonschema-specifications>=2023.03.6 +Requires-Dist: referencing>=0.28.4 +Requires-Dist: rpds-py>=0.25.0 +Provides-Extra: format +Requires-Dist: fqdn; extra == 'format' +Requires-Dist: idna; extra == 'format' +Requires-Dist: isoduration; extra == 'format' +Requires-Dist: jsonpointer>1.13; extra == 'format' +Requires-Dist: rfc3339-validator; extra == 'format' +Requires-Dist: rfc3987; extra == 'format' +Requires-Dist: uri-template; extra == 'format' +Requires-Dist: webcolors>=1.11; extra == 'format' +Provides-Extra: format-nongpl +Requires-Dist: fqdn; extra == 'format-nongpl' +Requires-Dist: idna; extra == 'format-nongpl' +Requires-Dist: isoduration; extra == 'format-nongpl' +Requires-Dist: jsonpointer>1.13; extra == 'format-nongpl' +Requires-Dist: rfc3339-validator; extra == 'format-nongpl' +Requires-Dist: rfc3986-validator>0.1.0; extra == 'format-nongpl' +Requires-Dist: rfc3987-syntax>=1.1.0; extra == 'format-nongpl' +Requires-Dist: uri-template; extra == 'format-nongpl' +Requires-Dist: webcolors>=24.6.0; extra == 'format-nongpl' +Description-Content-Type: text/x-rst + +========== +jsonschema +========== + +|PyPI| |Pythons| |CI| |ReadTheDocs| |Precommit| |Zenodo| + +.. |PyPI| image:: https://img.shields.io/pypi/v/jsonschema.svg + :alt: PyPI version + :target: https://pypi.org/project/jsonschema/ + +.. |Pythons| image:: https://img.shields.io/pypi/pyversions/jsonschema.svg + :alt: Supported Python versions + :target: https://pypi.org/project/jsonschema/ + +.. |CI| image:: https://github.com/python-jsonschema/jsonschema/workflows/CI/badge.svg + :alt: Build status + :target: https://github.com/python-jsonschema/jsonschema/actions?query=workflow%3ACI + +.. |ReadTheDocs| image:: https://readthedocs.org/projects/python-jsonschema/badge/?version=stable&style=flat + :alt: ReadTheDocs status + :target: https://python-jsonschema.readthedocs.io/en/stable/ + +.. |Precommit| image:: https://results.pre-commit.ci/badge/github/python-jsonschema/jsonschema/main.svg + :alt: pre-commit.ci status + :target: https://results.pre-commit.ci/latest/github/python-jsonschema/jsonschema/main + +.. |Zenodo| image:: https://zenodo.org/badge/3072629.svg + :alt: Zenodo DOI + :target: https://zenodo.org/badge/latestdoi/3072629 + + +``jsonschema`` is an implementation of the `JSON Schema `_ specification for Python. + +.. code:: python + + >>> from jsonschema import validate + + >>> # A sample schema, like what we'd get from json.load() + >>> schema = { + ... "type" : "object", + ... "properties" : { + ... "price" : {"type" : "number"}, + ... "name" : {"type" : "string"}, + ... }, + ... } + + >>> # If no exception is raised by validate(), the instance is valid. + >>> validate(instance={"name" : "Eggs", "price" : 34.99}, schema=schema) + + >>> validate( + ... instance={"name" : "Eggs", "price" : "Invalid"}, schema=schema, + ... ) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValidationError: 'Invalid' is not of type 'number' + +It can also be used from the command line by installing `check-jsonschema `_. + +Features +-------- + +* Full support for `Draft 2020-12 `_, `Draft 2019-09 `_, `Draft 7 `_, `Draft 6 `_, `Draft 4 `_ and `Draft 3 `_ + +* `Lazy validation `_ that can iteratively report *all* validation errors. + +* `Programmatic querying `_ of which properties or items failed validation. + + +Installation +------------ + +``jsonschema`` is available on `PyPI `_. You can install using `pip `_: + +.. code:: bash + + $ pip install jsonschema + + +Extras +====== + +Two extras are available when installing the package, both currently related to ``format`` validation: + + * ``format`` + * ``format-nongpl`` + +They can be used when installing in order to include additional dependencies, e.g.: + +.. code:: bash + + $ pip install jsonschema'[format]' + +Be aware that the mere presence of these dependencies – or even the specification of ``format`` checks in a schema – do *not* activate format checks (as per the specification). +Please read the `format validation documentation `_ for further details. + +About +----- + +I'm Julian Berman. + +``jsonschema`` is on `GitHub `_. + +Get in touch, via GitHub or otherwise, if you've got something to contribute, it'd be most welcome! + +If you feel overwhelmingly grateful, you can also `sponsor me `_. + +And for companies who appreciate ``jsonschema`` and its continued support and growth, ``jsonschema`` is also now supportable via `TideLift `_. + + +Release Information +------------------- + +v4.26.0 +======= + +* Decrease import time by delaying importing of ``urllib.request`` (#1416). diff --git a/source/target-vendors/cp312/jsonschema-4.26.0.dist-info/RECORD b/source/target-vendors/cp312/jsonschema-4.26.0.dist-info/RECORD new file mode 100644 index 0000000..aa5b1a8 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema-4.26.0.dist-info/RECORD @@ -0,0 +1,43 @@ +jsonschema/__init__.py,sha256=p-Rw4TS_0OPHZIJyImDWsdWgmd6CPWHMXLq7BuQxTGc,3941 +jsonschema/__main__.py,sha256=iLsZf2upUB3ilBKTlMnyK-HHt2Cnnfkwwxi_c6gLvSA,115 +jsonschema/_format.py,sha256=lrs1U7iSB2ZX9Z2EalsROYeQm1d9kefw2CLo08G8P4s,15568 +jsonschema/_keywords.py,sha256=r8_DrqAfn6QLwQnmXEggveiSU-UaIL2p2nuPINelfFc,14949 +jsonschema/_legacy_keywords.py,sha256=2tWuwRPWbYS7EAl8wBIC_rabGuv1J4dfYLqNEPpShhA,15191 +jsonschema/_types.py,sha256=f_nkTDbrja3E3fsINTC73y_gMXTU210I_H7qK0NN30c,5456 +jsonschema/_typing.py,sha256=pXLnGy3jNtIUTq037kUfq0we17085eHtJwDJ2t--mQE,617 +jsonschema/_utils.py,sha256=Xv6_wKKslBJlwyj9-j2c8JDFw-4z4aWFnVe2pX8h7U4,10659 +jsonschema/cli.py,sha256=av90OtpSxuiko3FAyEHtxpI-NSuX3WMtoQpIx09obJY,8445 +jsonschema/exceptions.py,sha256=b42hUDOfPFcprI4ZlNpjDeLKv8k9vOhgWct2jyDlxzk,15256 +jsonschema/protocols.py,sha256=lgyryVHqFijSFRAz95ch3VN2Fz-oTFyhj9Obcy5P_CI,7198 +jsonschema/validators.py,sha256=l_FxuZW0sMoFUwqTZqElL7F1P5nzy3sQaLbZqrEJb6U,47159 +jsonschema/benchmarks/__init__.py,sha256=A0sQrxDBVHSyQ-8ru3L11hMXf3q9gVuB9x_YgHb4R9M,70 +jsonschema/benchmarks/const_vs_enum.py,sha256=DVFi3WDqBalZFOibnjpX1uTSr3Rxa2cPgFcowd7Ukrs,830 +jsonschema/benchmarks/contains.py,sha256=gexQoUrCOwECofbt19BeosQZ7WFL6PDdkX49DWwBlOg,786 +jsonschema/benchmarks/import_benchmark.py,sha256=2DRIOVS6p-bqLn_822B_X5FkIRvsJ2V1CYnV2druzDs,779 +jsonschema/benchmarks/issue232.py,sha256=3LLYLIlBGQnVuyyo2iAv-xky5P6PRFHANx4-zIIQOoE,521 +jsonschema/benchmarks/json_schema_test_suite.py,sha256=PvfabpUYcF4_7csYDTcTauED8rnFEGYbdY5RqTXD08s,320 +jsonschema/benchmarks/nested_schemas.py,sha256=mo07dx-CIgmSOI62CNs4g5xu1FzHklLBpkQoDxWYcKs,1892 +jsonschema/benchmarks/subcomponents.py,sha256=fEyiMzsWeK2pd7DEGCuuY-vzGunwhHczRBWEnBRLKIo,1113 +jsonschema/benchmarks/unused_registry.py,sha256=hwRwONc9cefPtYzkoX_TYRO3GyUojriv0-YQaK3vnj0,940 +jsonschema/benchmarks/useless_applicator_schemas.py,sha256=EVm5-EtOEFoLP_Vt2j4SrCwlx05NhPqNuZQ6LIMP1Dc,3342 +jsonschema/benchmarks/useless_keywords.py,sha256=bj_zKr1oVctFlqyZaObCsYTgFjiiNgPzC0hr1Y868mE,867 +jsonschema/benchmarks/validator_creation.py,sha256=UkUQlLAnussnr_KdCIdad6xx2pXxQLmYtsXoiirKeWQ,285 +jsonschema/benchmarks/issue232/issue.json,sha256=eaPOZjMRu5u8RpKrsA9uk7ucPZS5tkKG4D_hkOTQ3Hk,117105 +jsonschema/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +jsonschema/tests/_suite.py,sha256=2k0X91N7dOHhQc5mrYv40OKf1weioj6RMBqWgLT6-PI,8374 +jsonschema/tests/fuzz_validate.py,sha256=fUA7yTJIihaCwJplkUehZeyB84HcXEcqtY5oPJXIO7I,1114 +jsonschema/tests/test_cli.py,sha256=A89r5LOHy-peLPZA5YDkOaMTWqzQO_w2Tu8WFz_vphM,28544 +jsonschema/tests/test_deprecations.py,sha256=yG6mkRJHpTHbWoxpLC5y5H7fk8erGOs8f_9V4tCBEh8,15754 +jsonschema/tests/test_exceptions.py,sha256=rsEqdjknUz3dEyJb6_DiModPHXsJTEWzDrxUscg8G5o,24301 +jsonschema/tests/test_format.py,sha256=eVm5SMaWF2lOPO28bPAwNvkiQvHCQKy-MnuAgEchfEc,3188 +jsonschema/tests/test_jsonschema_test_suite.py,sha256=tAfxknM65OR9LyDPHu1pkEaombLgjRLnJ6FPiWPdxjg,8461 +jsonschema/tests/test_types.py,sha256=cF51KTDmdsx06MrIc4fXKt0X9fIsVgw5uhT8CamVa8U,6977 +jsonschema/tests/test_utils.py,sha256=sao74o1PyYMxBfqweokQN48CFSS6yhJk5FkCfMJ5PsI,4163 +jsonschema/tests/test_validators.py,sha256=uzl-agyLoZPTufouIHSxFPfPYrdIdjNwjHcD_5NyU4E,87949 +jsonschema/tests/typing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +jsonschema/tests/typing/test_all_concrete_validators_match_protocol.py,sha256=I5XUl5ZYUSZo3APkF10l8Mnfk09oXKqvXWttGGd3sDk,1238 +jsonschema-4.26.0.dist-info/METADATA,sha256=BbGJhmypipbaOTS_a4NofO0_-FMlJzt34WtwX22LJtI,7592 +jsonschema-4.26.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87 +jsonschema-4.26.0.dist-info/entry_points.txt,sha256=vO7rX4Fs_xIVJy2pnAtKgTSxfpnozAVQ0DjCmpMxnWE,51 +jsonschema-4.26.0.dist-info/licenses/COPYING,sha256=T5KgFaE8TRoEC-8BiqE0MLTxvHO0Gxa7hGw0Z2bedDk,1057 +jsonschema-4.26.0.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/jsonschema-4.26.0.dist-info/WHEEL b/source/target-vendors/cp312/jsonschema-4.26.0.dist-info/WHEEL new file mode 100644 index 0000000..ae8ec1b --- /dev/null +++ b/source/target-vendors/cp312/jsonschema-4.26.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.28.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/source/target-vendors/cp312/jsonschema-4.26.0.dist-info/entry_points.txt b/source/target-vendors/cp312/jsonschema-4.26.0.dist-info/entry_points.txt new file mode 100644 index 0000000..eecef9d --- /dev/null +++ b/source/target-vendors/cp312/jsonschema-4.26.0.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +jsonschema = jsonschema.cli:main diff --git a/source/target-vendors/cp312/jsonschema-4.26.0.dist-info/licenses/COPYING b/source/target-vendors/cp312/jsonschema-4.26.0.dist-info/licenses/COPYING new file mode 100644 index 0000000..af9cfbd --- /dev/null +++ b/source/target-vendors/cp312/jsonschema-4.26.0.dist-info/licenses/COPYING @@ -0,0 +1,19 @@ +Copyright (c) 2013 Julian Berman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/source/target-vendors/cp312/jsonschema/__init__.py b/source/target-vendors/cp312/jsonschema/__init__.py new file mode 100644 index 0000000..d8dec8c --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/__init__.py @@ -0,0 +1,120 @@ +""" +An implementation of JSON Schema for Python. + +The main functionality is provided by the validator classes for each of the +supported JSON Schema versions. + +Most commonly, `jsonschema.validators.validate` is the quickest way to simply +validate a given instance under a schema, and will create a validator +for you. +""" +import warnings + +from jsonschema._format import FormatChecker +from jsonschema._types import TypeChecker +from jsonschema.exceptions import SchemaError, ValidationError +from jsonschema.validators import ( + Draft3Validator, + Draft4Validator, + Draft6Validator, + Draft7Validator, + Draft201909Validator, + Draft202012Validator, + validate, +) + + +def __getattr__(name): + if name == "__version__": + warnings.warn( + "Accessing jsonschema.__version__ is deprecated and will be " + "removed in a future release. Use importlib.metadata directly " + "to query for jsonschema's version.", + DeprecationWarning, + stacklevel=2, + ) + + from importlib import metadata + return metadata.version("jsonschema") + elif name == "RefResolver": + from jsonschema.validators import _RefResolver + warnings.warn( + _RefResolver._DEPRECATION_MESSAGE, + DeprecationWarning, + stacklevel=2, + ) + return _RefResolver + elif name == "ErrorTree": + warnings.warn( + "Importing ErrorTree directly from the jsonschema package " + "is deprecated and will become an ImportError. Import it from " + "jsonschema.exceptions instead.", + DeprecationWarning, + stacklevel=2, + ) + from jsonschema.exceptions import ErrorTree + return ErrorTree + elif name == "FormatError": + warnings.warn( + "Importing FormatError directly from the jsonschema package " + "is deprecated and will become an ImportError. Import it from " + "jsonschema.exceptions instead.", + DeprecationWarning, + stacklevel=2, + ) + from jsonschema.exceptions import FormatError + return FormatError + elif name == "Validator": + warnings.warn( + "Importing Validator directly from the jsonschema package " + "is deprecated and will become an ImportError. Import it from " + "jsonschema.protocols instead.", + DeprecationWarning, + stacklevel=2, + ) + from jsonschema.protocols import Validator + return Validator + elif name == "RefResolutionError": + from jsonschema.exceptions import _RefResolutionError + warnings.warn( + _RefResolutionError._DEPRECATION_MESSAGE, + DeprecationWarning, + stacklevel=2, + ) + return _RefResolutionError + + format_checkers = { + "draft3_format_checker": Draft3Validator, + "draft4_format_checker": Draft4Validator, + "draft6_format_checker": Draft6Validator, + "draft7_format_checker": Draft7Validator, + "draft201909_format_checker": Draft201909Validator, + "draft202012_format_checker": Draft202012Validator, + } + ValidatorForFormat = format_checkers.get(name) + if ValidatorForFormat is not None: + warnings.warn( + f"Accessing jsonschema.{name} is deprecated and will be " + "removed in a future release. Instead, use the FORMAT_CHECKER " + "attribute on the corresponding Validator.", + DeprecationWarning, + stacklevel=2, + ) + return ValidatorForFormat.FORMAT_CHECKER + + raise AttributeError(f"module {__name__} has no attribute {name}") + + +__all__ = [ + "Draft3Validator", + "Draft4Validator", + "Draft6Validator", + "Draft7Validator", + "Draft201909Validator", + "Draft202012Validator", + "FormatChecker", + "SchemaError", + "TypeChecker", + "ValidationError", + "validate", +] diff --git a/source/target-vendors/cp312/jsonschema/__main__.py b/source/target-vendors/cp312/jsonschema/__main__.py new file mode 100644 index 0000000..fb260ae --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/__main__.py @@ -0,0 +1,6 @@ +""" +The jsonschema CLI is now deprecated in favor of check-jsonschema. +""" +from jsonschema.cli import main + +main() diff --git a/source/target-vendors/cp312/jsonschema/_format.py b/source/target-vendors/cp312/jsonschema/_format.py new file mode 100644 index 0000000..5efda86 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/_format.py @@ -0,0 +1,546 @@ +from __future__ import annotations + +from contextlib import suppress +from datetime import date, datetime +from uuid import UUID +import ipaddress +import re +import typing +import warnings + +from jsonschema.exceptions import FormatError + +_FormatCheckCallable = typing.Callable[[object], bool] +#: A format checker callable. +_F = typing.TypeVar("_F", bound=_FormatCheckCallable) +_RaisesType = type[Exception] | tuple[type[Exception], ...] + +_RE_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$", re.ASCII) + + +class FormatChecker: + """ + A ``format`` property checker. + + JSON Schema does not mandate that the ``format`` property actually do any + validation. If validation is desired however, instances of this class can + be hooked into validators to enable format validation. + + `FormatChecker` objects always return ``True`` when asked about + formats that they do not know how to validate. + + To add a check for a custom format use the `FormatChecker.checks` + decorator. + + Arguments: + + formats: + + The known formats to validate. This argument can be used to + limit which formats will be used during validation. + + """ + + checkers: dict[ + str, + tuple[_FormatCheckCallable, _RaisesType], + ] = {} # noqa: RUF012 + + def __init__(self, formats: typing.Iterable[str] | None = None): + if formats is None: + formats = self.checkers.keys() + self.checkers = {k: self.checkers[k] for k in formats} + + def __repr__(self): + return f"" + + def checks( + self, format: str, raises: _RaisesType = (), + ) -> typing.Callable[[_F], _F]: + """ + Register a decorated function as validating a new format. + + Arguments: + + format: + + The format that the decorated function will check. + + raises: + + The exception(s) raised by the decorated function when an + invalid instance is found. + + The exception object will be accessible as the + `jsonschema.exceptions.ValidationError.cause` attribute of the + resulting validation error. + + """ + + def _checks(func: _F) -> _F: + self.checkers[format] = (func, raises) + return func + + return _checks + + @classmethod + def cls_checks( + cls, format: str, raises: _RaisesType = (), + ) -> typing.Callable[[_F], _F]: + warnings.warn( + ( + "FormatChecker.cls_checks is deprecated. Call " + "FormatChecker.checks on a specific FormatChecker instance " + "instead." + ), + DeprecationWarning, + stacklevel=2, + ) + return cls._cls_checks(format=format, raises=raises) + + @classmethod + def _cls_checks( + cls, format: str, raises: _RaisesType = (), + ) -> typing.Callable[[_F], _F]: + def _checks(func: _F) -> _F: + cls.checkers[format] = (func, raises) + return func + + return _checks + + def check(self, instance: object, format: str) -> None: + """ + Check whether the instance conforms to the given format. + + Arguments: + + instance (*any primitive type*, i.e. str, number, bool): + + The instance to check + + format: + + The format that instance should conform to + + Raises: + + FormatError: + + if the instance does not conform to ``format`` + + """ + if format not in self.checkers: + return + + func, raises = self.checkers[format] + result, cause = None, None + try: + result = func(instance) + except raises as e: + cause = e + if not result: + raise FormatError(f"{instance!r} is not a {format!r}", cause=cause) + + def conforms(self, instance: object, format: str) -> bool: + """ + Check whether the instance conforms to the given format. + + Arguments: + + instance (*any primitive type*, i.e. str, number, bool): + + The instance to check + + format: + + The format that instance should conform to + + Returns: + + bool: whether it conformed + + """ + try: + self.check(instance, format) + except FormatError: + return False + else: + return True + + +draft3_format_checker = FormatChecker() +draft4_format_checker = FormatChecker() +draft6_format_checker = FormatChecker() +draft7_format_checker = FormatChecker() +draft201909_format_checker = FormatChecker() +draft202012_format_checker = FormatChecker() + +_draft_checkers: dict[str, FormatChecker] = dict( + draft3=draft3_format_checker, + draft4=draft4_format_checker, + draft6=draft6_format_checker, + draft7=draft7_format_checker, + draft201909=draft201909_format_checker, + draft202012=draft202012_format_checker, +) + + +def _checks_drafts( + name=None, + draft3=None, + draft4=None, + draft6=None, + draft7=None, + draft201909=None, + draft202012=None, + raises=(), +) -> typing.Callable[[_F], _F]: + draft3 = draft3 or name + draft4 = draft4 or name + draft6 = draft6 or name + draft7 = draft7 or name + draft201909 = draft201909 or name + draft202012 = draft202012 or name + + def wrap(func: _F) -> _F: + if draft3: + func = _draft_checkers["draft3"].checks(draft3, raises)(func) + if draft4: + func = _draft_checkers["draft4"].checks(draft4, raises)(func) + if draft6: + func = _draft_checkers["draft6"].checks(draft6, raises)(func) + if draft7: + func = _draft_checkers["draft7"].checks(draft7, raises)(func) + if draft201909: + func = _draft_checkers["draft201909"].checks(draft201909, raises)( + func, + ) + if draft202012: + func = _draft_checkers["draft202012"].checks(draft202012, raises)( + func, + ) + + # Oy. This is bad global state, but relied upon for now, until + # deprecation. See #519 and test_format_checkers_come_with_defaults + FormatChecker._cls_checks( + draft202012 or draft201909 or draft7 or draft6 or draft4 or draft3, + raises, + )(func) + return func + + return wrap + + +@_checks_drafts(name="idn-email") +@_checks_drafts(name="email") +def is_email(instance: object) -> bool: + if not isinstance(instance, str): + return True + return "@" in instance + + +@_checks_drafts( + draft3="ip-address", + draft4="ipv4", + draft6="ipv4", + draft7="ipv4", + draft201909="ipv4", + draft202012="ipv4", + raises=ipaddress.AddressValueError, +) +def is_ipv4(instance: object) -> bool: + if not isinstance(instance, str): + return True + return bool(ipaddress.IPv4Address(instance)) + + +@_checks_drafts(name="ipv6", raises=ipaddress.AddressValueError) +def is_ipv6(instance: object) -> bool: + if not isinstance(instance, str): + return True + address = ipaddress.IPv6Address(instance) + return not getattr(address, "scope_id", "") + + +with suppress(ImportError): + from fqdn import FQDN + + @_checks_drafts( + draft3="host-name", + draft4="hostname", + draft6="hostname", + draft7="hostname", + draft201909="hostname", + draft202012="hostname", + # fqdn.FQDN("") raises a ValueError due to a bug + # however, it's not clear when or if that will be fixed, so catch it + # here for now + raises=ValueError, + ) + def is_host_name(instance: object) -> bool: + if not isinstance(instance, str): + return True + return FQDN(instance, min_labels=1).is_valid + + +with suppress(ImportError): + # The built-in `idna` codec only implements RFC 3890, so we go elsewhere. + import idna + + @_checks_drafts( + draft7="idn-hostname", + draft201909="idn-hostname", + draft202012="idn-hostname", + raises=(idna.IDNAError, UnicodeError), + ) + def is_idn_host_name(instance: object) -> bool: + if not isinstance(instance, str): + return True + idna.encode(instance) + return True + + +try: + import rfc3987 +except ImportError: + with suppress(ImportError): + from rfc3986_validator import validate_rfc3986 + + @_checks_drafts(name="uri") + def is_uri(instance: object) -> bool: + if not isinstance(instance, str): + return True + return validate_rfc3986(instance, rule="URI") + + @_checks_drafts( + draft6="uri-reference", + draft7="uri-reference", + draft201909="uri-reference", + draft202012="uri-reference", + raises=ValueError, + ) + def is_uri_reference(instance: object) -> bool: + if not isinstance(instance, str): + return True + return validate_rfc3986(instance, rule="URI_reference") + + with suppress(ImportError): + from rfc3987_syntax import is_valid_syntax as _rfc3987_is_valid_syntax + + @_checks_drafts( + draft7="iri", + draft201909="iri", + draft202012="iri", + raises=ValueError, + ) + def is_iri(instance: object) -> bool: + if not isinstance(instance, str): + return True + return _rfc3987_is_valid_syntax("iri", instance) + + @_checks_drafts( + draft7="iri-reference", + draft201909="iri-reference", + draft202012="iri-reference", + raises=ValueError, + ) + def is_iri_reference(instance: object) -> bool: + if not isinstance(instance, str): + return True + return _rfc3987_is_valid_syntax("iri_reference", instance) + +else: + + @_checks_drafts( + draft7="iri", + draft201909="iri", + draft202012="iri", + raises=ValueError, + ) + def is_iri(instance: object) -> bool: + if not isinstance(instance, str): + return True + return rfc3987.parse(instance, rule="IRI") + + @_checks_drafts( + draft7="iri-reference", + draft201909="iri-reference", + draft202012="iri-reference", + raises=ValueError, + ) + def is_iri_reference(instance: object) -> bool: + if not isinstance(instance, str): + return True + return rfc3987.parse(instance, rule="IRI_reference") + + @_checks_drafts(name="uri", raises=ValueError) + def is_uri(instance: object) -> bool: + if not isinstance(instance, str): + return True + return rfc3987.parse(instance, rule="URI") + + @_checks_drafts( + draft6="uri-reference", + draft7="uri-reference", + draft201909="uri-reference", + draft202012="uri-reference", + raises=ValueError, + ) + def is_uri_reference(instance: object) -> bool: + if not isinstance(instance, str): + return True + return rfc3987.parse(instance, rule="URI_reference") + + +with suppress(ImportError): + from rfc3339_validator import validate_rfc3339 + + @_checks_drafts(name="date-time") + def is_datetime(instance: object) -> bool: + if not isinstance(instance, str): + return True + return validate_rfc3339(instance.upper()) + + @_checks_drafts( + draft7="time", + draft201909="time", + draft202012="time", + ) + def is_time(instance: object) -> bool: + if not isinstance(instance, str): + return True + return is_datetime("1970-01-01T" + instance) + + +@_checks_drafts(name="regex", raises=re.error) +def is_regex(instance: object) -> bool: + if not isinstance(instance, str): + return True + return bool(re.compile(instance)) + + +@_checks_drafts( + draft3="date", + draft7="date", + draft201909="date", + draft202012="date", + raises=ValueError, +) +def is_date(instance: object) -> bool: + if not isinstance(instance, str): + return True + return bool(_RE_DATE.fullmatch(instance) and date.fromisoformat(instance)) + + +@_checks_drafts(draft3="time", raises=ValueError) +def is_draft3_time(instance: object) -> bool: + if not isinstance(instance, str): + return True + return bool(datetime.strptime(instance, "%H:%M:%S")) # noqa: DTZ007 + + +with suppress(ImportError): + import webcolors + + @_checks_drafts(draft3="color", raises=(ValueError, TypeError)) + def is_css21_color(instance: object) -> bool: + if isinstance(instance, str): + try: + webcolors.name_to_hex(instance) + except ValueError: + webcolors.normalize_hex(instance.lower()) + return True + + +with suppress(ImportError): + import jsonpointer + + @_checks_drafts( + draft6="json-pointer", + draft7="json-pointer", + draft201909="json-pointer", + draft202012="json-pointer", + raises=jsonpointer.JsonPointerException, + ) + def is_json_pointer(instance: object) -> bool: + if not isinstance(instance, str): + return True + return bool(jsonpointer.JsonPointer(instance)) + + # TODO: I don't want to maintain this, so it + # needs to go either into jsonpointer (pending + # https://github.com/stefankoegl/python-json-pointer/issues/34) or + # into a new external library. + @_checks_drafts( + draft7="relative-json-pointer", + draft201909="relative-json-pointer", + draft202012="relative-json-pointer", + raises=jsonpointer.JsonPointerException, + ) + def is_relative_json_pointer(instance: object) -> bool: + # Definition taken from: + # https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01#section-3 + if not isinstance(instance, str): + return True + if not instance: + return False + + non_negative_integer, rest = [], "" + for i, character in enumerate(instance): + if character.isdigit(): + # digits with a leading "0" are not allowed + if i > 0 and int(instance[i - 1]) == 0: + return False + + non_negative_integer.append(character) + continue + + if not non_negative_integer: + return False + + rest = instance[i:] + break + return (rest == "#") or bool(jsonpointer.JsonPointer(rest)) + + +with suppress(ImportError): + import uri_template + + @_checks_drafts( + draft6="uri-template", + draft7="uri-template", + draft201909="uri-template", + draft202012="uri-template", + ) + def is_uri_template(instance: object) -> bool: + if not isinstance(instance, str): + return True + return uri_template.validate(instance) + + +with suppress(ImportError): + import isoduration + + @_checks_drafts( + draft201909="duration", + draft202012="duration", + raises=isoduration.DurationParsingException, + ) + def is_duration(instance: object) -> bool: + if not isinstance(instance, str): + return True + isoduration.parse_duration(instance) + # FIXME: See bolsote/isoduration#25 and bolsote/isoduration#21 + return instance.endswith(tuple("DMYWHMS")) + + +@_checks_drafts( + draft201909="uuid", + draft202012="uuid", + raises=ValueError, +) +def is_uuid(instance: object) -> bool: + if not isinstance(instance, str): + return True + UUID(instance) + return all(instance[position] == "-" for position in (8, 13, 18, 23)) diff --git a/source/target-vendors/cp312/jsonschema/_keywords.py b/source/target-vendors/cp312/jsonschema/_keywords.py new file mode 100644 index 0000000..f30f954 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/_keywords.py @@ -0,0 +1,449 @@ +from fractions import Fraction +import re + +from jsonschema._utils import ( + ensure_list, + equal, + extras_msg, + find_additional_properties, + find_evaluated_item_indexes_by_schema, + find_evaluated_property_keys_by_schema, + uniq, +) +from jsonschema.exceptions import FormatError, ValidationError + + +def patternProperties(validator, patternProperties, instance, schema): + if not validator.is_type(instance, "object"): + return + + for pattern, subschema in patternProperties.items(): + for k, v in instance.items(): + if re.search(pattern, k): + yield from validator.descend( + v, subschema, path=k, schema_path=pattern, + ) + + +def propertyNames(validator, propertyNames, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property in instance: + yield from validator.descend(instance=property, schema=propertyNames) + + +def additionalProperties(validator, aP, instance, schema): + if not validator.is_type(instance, "object"): + return + + extras = set(find_additional_properties(instance, schema)) + + if validator.is_type(aP, "object"): + for extra in extras: + yield from validator.descend(instance[extra], aP, path=extra) + elif not aP and extras: + if "patternProperties" in schema: + verb = "does" if len(extras) == 1 else "do" + joined = ", ".join(repr(each) for each in sorted(extras)) + patterns = ", ".join( + repr(each) for each in sorted(schema["patternProperties"]) + ) + error = f"{joined} {verb} not match any of the regexes: {patterns}" + yield ValidationError(error) + else: + error = "Additional properties are not allowed (%s %s unexpected)" + yield ValidationError(error % extras_msg(sorted(extras, key=str))) + + +def items(validator, items, instance, schema): + if not validator.is_type(instance, "array"): + return + + prefix = len(schema.get("prefixItems", [])) + total = len(instance) + extra = total - prefix + if extra <= 0: + return + + if items is False: + rest = instance[prefix:] if extra != 1 else instance[prefix] + item = "items" if prefix != 1 else "item" + yield ValidationError( + f"Expected at most {prefix} {item} but found {extra} " + f"extra: {rest!r}", + ) + else: + for index in range(prefix, total): + yield from validator.descend( + instance=instance[index], + schema=items, + path=index, + ) + + +def const(validator, const, instance, schema): + if not equal(instance, const): + yield ValidationError(f"{const!r} was expected") + + +def contains(validator, contains, instance, schema): + if not validator.is_type(instance, "array"): + return + + matches = 0 + min_contains = schema.get("minContains", 1) + max_contains = schema.get("maxContains", len(instance)) + + contains_validator = validator.evolve(schema=contains) + + for each in instance: + if contains_validator.is_valid(each): + matches += 1 + if matches > max_contains: + yield ValidationError( + "Too many items match the given schema " + f"(expected at most {max_contains})", + validator="maxContains", + validator_value=max_contains, + ) + return + + if matches < min_contains: + if not matches: + yield ValidationError( + f"{instance!r} does not contain items " + "matching the given schema", + ) + else: + yield ValidationError( + "Too few items match the given schema (expected at least " + f"{min_contains} but only {matches} matched)", + validator="minContains", + validator_value=min_contains, + ) + + +def exclusiveMinimum(validator, minimum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if instance <= minimum: + yield ValidationError( + f"{instance!r} is less than or equal to " + f"the minimum of {minimum!r}", + ) + + +def exclusiveMaximum(validator, maximum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if instance >= maximum: + yield ValidationError( + f"{instance!r} is greater than or equal " + f"to the maximum of {maximum!r}", + ) + + +def minimum(validator, minimum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if instance < minimum: + message = f"{instance!r} is less than the minimum of {minimum!r}" + yield ValidationError(message) + + +def maximum(validator, maximum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if instance > maximum: + message = f"{instance!r} is greater than the maximum of {maximum!r}" + yield ValidationError(message) + + +def multipleOf(validator, dB, instance, schema): + if not validator.is_type(instance, "number"): + return + + if isinstance(dB, float): + quotient = instance / dB + try: + failed = int(quotient) != quotient + except OverflowError: + # When `instance` is large and `dB` is less than one, + # quotient can overflow to infinity; and then casting to int + # raises an error. + # + # In this case we fall back to Fraction logic, which is + # exact and cannot overflow. The performance is also + # acceptable: we try the fast all-float option first, and + # we know that fraction(dB) can have at most a few hundred + # digits in each part. The worst-case slowdown is therefore + # for already-slow enormous integers or Decimals. + failed = (Fraction(instance) / Fraction(dB)).denominator != 1 + else: + failed = instance % dB + + if failed: + yield ValidationError(f"{instance!r} is not a multiple of {dB}") + + +def minItems(validator, mI, instance, schema): + if validator.is_type(instance, "array") and len(instance) < mI: + message = "should be non-empty" if mI == 1 else "is too short" + yield ValidationError(f"{instance!r} {message}") + + +def maxItems(validator, mI, instance, schema): + if validator.is_type(instance, "array") and len(instance) > mI: + message = "is expected to be empty" if mI == 0 else "is too long" + yield ValidationError(f"{instance!r} {message}") + + +def uniqueItems(validator, uI, instance, schema): + if ( + uI + and validator.is_type(instance, "array") + and not uniq(instance) + ): + yield ValidationError(f"{instance!r} has non-unique elements") + + +def pattern(validator, patrn, instance, schema): + if ( + validator.is_type(instance, "string") + and not re.search(patrn, instance) + ): + yield ValidationError(f"{instance!r} does not match {patrn!r}") + + +def format(validator, format, instance, schema): + if validator.format_checker is not None: + try: + validator.format_checker.check(instance, format) + except FormatError as error: + yield ValidationError(error.message, cause=error.cause) + + +def minLength(validator, mL, instance, schema): + if validator.is_type(instance, "string") and len(instance) < mL: + message = "should be non-empty" if mL == 1 else "is too short" + yield ValidationError(f"{instance!r} {message}") + + +def maxLength(validator, mL, instance, schema): + if validator.is_type(instance, "string") and len(instance) > mL: + message = "is expected to be empty" if mL == 0 else "is too long" + yield ValidationError(f"{instance!r} {message}") + + +def dependentRequired(validator, dependentRequired, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property, dependency in dependentRequired.items(): + if property not in instance: + continue + + for each in dependency: + if each not in instance: + message = f"{each!r} is a dependency of {property!r}" + yield ValidationError(message) + + +def dependentSchemas(validator, dependentSchemas, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property, dependency in dependentSchemas.items(): + if property not in instance: + continue + yield from validator.descend( + instance, dependency, schema_path=property, + ) + + +def enum(validator, enums, instance, schema): + if all(not equal(each, instance) for each in enums): + yield ValidationError(f"{instance!r} is not one of {enums!r}") + + +def ref(validator, ref, instance, schema): + yield from validator._validate_reference(ref=ref, instance=instance) + + +def dynamicRef(validator, dynamicRef, instance, schema): + yield from validator._validate_reference(ref=dynamicRef, instance=instance) + + +def type(validator, types, instance, schema): + types = ensure_list(types) + + if not any(validator.is_type(instance, type) for type in types): + reprs = ", ".join(repr(type) for type in types) + yield ValidationError(f"{instance!r} is not of type {reprs}") + + +def properties(validator, properties, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property, subschema in properties.items(): + if property in instance: + yield from validator.descend( + instance[property], + subschema, + path=property, + schema_path=property, + ) + + +def required(validator, required, instance, schema): + if not validator.is_type(instance, "object"): + return + for property in required: + if property not in instance: + yield ValidationError(f"{property!r} is a required property") + + +def minProperties(validator, mP, instance, schema): + if validator.is_type(instance, "object") and len(instance) < mP: + message = ( + "should be non-empty" if mP == 1 + else "does not have enough properties" + ) + yield ValidationError(f"{instance!r} {message}") + + +def maxProperties(validator, mP, instance, schema): + if not validator.is_type(instance, "object"): + return + if validator.is_type(instance, "object") and len(instance) > mP: + message = ( + "is expected to be empty" if mP == 0 + else "has too many properties" + ) + yield ValidationError(f"{instance!r} {message}") + + +def allOf(validator, allOf, instance, schema): + for index, subschema in enumerate(allOf): + yield from validator.descend(instance, subschema, schema_path=index) + + +def anyOf(validator, anyOf, instance, schema): + all_errors = [] + for index, subschema in enumerate(anyOf): + errs = list(validator.descend(instance, subschema, schema_path=index)) + if not errs: + break + all_errors.extend(errs) + else: + yield ValidationError( + f"{instance!r} is not valid under any of the given schemas", + context=all_errors, + ) + + +def oneOf(validator, oneOf, instance, schema): + subschemas = enumerate(oneOf) + all_errors = [] + for index, subschema in subschemas: + errs = list(validator.descend(instance, subschema, schema_path=index)) + if not errs: + first_valid = subschema + break + all_errors.extend(errs) + else: + yield ValidationError( + f"{instance!r} is not valid under any of the given schemas", + context=all_errors, + ) + + more_valid = [ + each for _, each in subschemas + if validator.evolve(schema=each).is_valid(instance) + ] + if more_valid: + more_valid.append(first_valid) + reprs = ", ".join(repr(schema) for schema in more_valid) + yield ValidationError(f"{instance!r} is valid under each of {reprs}") + + +def not_(validator, not_schema, instance, schema): + if validator.evolve(schema=not_schema).is_valid(instance): + message = f"{instance!r} should not be valid under {not_schema!r}" + yield ValidationError(message) + + +def if_(validator, if_schema, instance, schema): + if validator.evolve(schema=if_schema).is_valid(instance): + if "then" in schema: + then = schema["then"] + yield from validator.descend(instance, then, schema_path="then") + elif "else" in schema: + else_ = schema["else"] + yield from validator.descend(instance, else_, schema_path="else") + + +def unevaluatedItems(validator, unevaluatedItems, instance, schema): + if not validator.is_type(instance, "array"): + return + evaluated_item_indexes = find_evaluated_item_indexes_by_schema( + validator, instance, schema, + ) + unevaluated_items = [ + item for index, item in enumerate(instance) + if index not in evaluated_item_indexes + ] + if unevaluated_items: + error = "Unevaluated items are not allowed (%s %s unexpected)" + yield ValidationError(error % extras_msg(unevaluated_items)) + + +def unevaluatedProperties(validator, unevaluatedProperties, instance, schema): + if not validator.is_type(instance, "object"): + return + evaluated_keys = find_evaluated_property_keys_by_schema( + validator, instance, schema, + ) + unevaluated_keys = [] + for property in instance: + if property not in evaluated_keys: + for _ in validator.descend( + instance[property], + unevaluatedProperties, + path=property, + schema_path=property, + ): + # FIXME: Include context for each unevaluated property + # indicating why it's invalid under the subschema. + unevaluated_keys.append(property) # noqa: PERF401 + + if unevaluated_keys: + if unevaluatedProperties is False: + error = "Unevaluated properties are not allowed (%s %s unexpected)" + extras = sorted(unevaluated_keys, key=str) + yield ValidationError(error % extras_msg(extras)) + else: + error = ( + "Unevaluated properties are not valid under " + "the given schema (%s %s unevaluated and invalid)" + ) + yield ValidationError(error % extras_msg(unevaluated_keys)) + + +def prefixItems(validator, prefixItems, instance, schema): + if not validator.is_type(instance, "array"): + return + + for (index, item), subschema in zip(enumerate(instance), prefixItems): + yield from validator.descend( + instance=item, + schema=subschema, + schema_path=index, + path=index, + ) diff --git a/source/target-vendors/cp312/jsonschema/_legacy_keywords.py b/source/target-vendors/cp312/jsonschema/_legacy_keywords.py new file mode 100644 index 0000000..c691589 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/_legacy_keywords.py @@ -0,0 +1,449 @@ +import re + +from referencing.jsonschema import lookup_recursive_ref + +from jsonschema import _utils +from jsonschema.exceptions import ValidationError + + +def ignore_ref_siblings(schema): + """ + Ignore siblings of ``$ref`` if it is present. + + Otherwise, return all keywords. + + Suitable for use with `create`'s ``applicable_validators`` argument. + """ + ref = schema.get("$ref") + if ref is not None: + return [("$ref", ref)] + else: + return schema.items() + + +def dependencies_draft3(validator, dependencies, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property, dependency in dependencies.items(): + if property not in instance: + continue + + if validator.is_type(dependency, "object"): + yield from validator.descend( + instance, dependency, schema_path=property, + ) + elif validator.is_type(dependency, "string"): + if dependency not in instance: + message = f"{dependency!r} is a dependency of {property!r}" + yield ValidationError(message) + else: + for each in dependency: + if each not in instance: + message = f"{each!r} is a dependency of {property!r}" + yield ValidationError(message) + + +def dependencies_draft4_draft6_draft7( + validator, + dependencies, + instance, + schema, +): + """ + Support for the ``dependencies`` keyword from pre-draft 2019-09. + + In later drafts, the keyword was split into separate + ``dependentRequired`` and ``dependentSchemas`` validators. + """ + if not validator.is_type(instance, "object"): + return + + for property, dependency in dependencies.items(): + if property not in instance: + continue + + if validator.is_type(dependency, "array"): + for each in dependency: + if each not in instance: + message = f"{each!r} is a dependency of {property!r}" + yield ValidationError(message) + else: + yield from validator.descend( + instance, dependency, schema_path=property, + ) + + +def disallow_draft3(validator, disallow, instance, schema): + for disallowed in _utils.ensure_list(disallow): + if validator.evolve(schema={"type": [disallowed]}).is_valid(instance): + message = f"{disallowed!r} is disallowed for {instance!r}" + yield ValidationError(message) + + +def extends_draft3(validator, extends, instance, schema): + if validator.is_type(extends, "object"): + yield from validator.descend(instance, extends) + return + for index, subschema in enumerate(extends): + yield from validator.descend(instance, subschema, schema_path=index) + + +def items_draft3_draft4(validator, items, instance, schema): + if not validator.is_type(instance, "array"): + return + + if validator.is_type(items, "object"): + for index, item in enumerate(instance): + yield from validator.descend(item, items, path=index) + else: + for (index, item), subschema in zip(enumerate(instance), items): + yield from validator.descend( + item, subschema, path=index, schema_path=index, + ) + + +def additionalItems(validator, aI, instance, schema): + if ( + not validator.is_type(instance, "array") + or validator.is_type(schema.get("items", {}), "object") + ): + return + + len_items = len(schema.get("items", [])) + if validator.is_type(aI, "object"): + for index, item in enumerate(instance[len_items:], start=len_items): + yield from validator.descend(item, aI, path=index) + elif not aI and len(instance) > len(schema.get("items", [])): + error = "Additional items are not allowed (%s %s unexpected)" + yield ValidationError( + error % _utils.extras_msg(instance[len(schema.get("items", [])):]), + ) + + +def items_draft6_draft7_draft201909(validator, items, instance, schema): + if not validator.is_type(instance, "array"): + return + + if validator.is_type(items, "array"): + for (index, item), subschema in zip(enumerate(instance), items): + yield from validator.descend( + item, subschema, path=index, schema_path=index, + ) + else: + for index, item in enumerate(instance): + yield from validator.descend(item, items, path=index) + + +def minimum_draft3_draft4(validator, minimum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if schema.get("exclusiveMinimum", False): + failed = instance <= minimum + cmp = "less than or equal to" + else: + failed = instance < minimum + cmp = "less than" + + if failed: + message = f"{instance!r} is {cmp} the minimum of {minimum!r}" + yield ValidationError(message) + + +def maximum_draft3_draft4(validator, maximum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if schema.get("exclusiveMaximum", False): + failed = instance >= maximum + cmp = "greater than or equal to" + else: + failed = instance > maximum + cmp = "greater than" + + if failed: + message = f"{instance!r} is {cmp} the maximum of {maximum!r}" + yield ValidationError(message) + + +def properties_draft3(validator, properties, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property, subschema in properties.items(): + if property in instance: + yield from validator.descend( + instance[property], + subschema, + path=property, + schema_path=property, + ) + elif subschema.get("required", False): + error = ValidationError(f"{property!r} is a required property") + error._set( + validator="required", + validator_value=subschema["required"], + instance=instance, + schema=schema, + ) + error.path.appendleft(property) + error.schema_path.extend([property, "required"]) + yield error + + +def type_draft3(validator, types, instance, schema): + types = _utils.ensure_list(types) + + all_errors = [] + for index, type in enumerate(types): + if validator.is_type(type, "object"): + errors = list(validator.descend(instance, type, schema_path=index)) + if not errors: + return + all_errors.extend(errors) + elif validator.is_type(instance, type): + return + + reprs = [] + for type in types: + try: + reprs.append(repr(type["name"])) + except Exception: # noqa: BLE001 + reprs.append(repr(type)) + yield ValidationError( + f"{instance!r} is not of type {', '.join(reprs)}", + context=all_errors, + ) + + +def contains_draft6_draft7(validator, contains, instance, schema): + if not validator.is_type(instance, "array"): + return + + if not any( + validator.evolve(schema=contains).is_valid(element) + for element in instance + ): + yield ValidationError( + f"None of {instance!r} are valid under the given schema", + ) + + +def recursiveRef(validator, recursiveRef, instance, schema): + resolved = lookup_recursive_ref(validator._resolver) + yield from validator.descend( + instance, + resolved.contents, + resolver=resolved.resolver, + ) + + +def find_evaluated_item_indexes_by_schema(validator, instance, schema): + """ + Get all indexes of items that get evaluated under the current schema. + + Covers all keywords related to unevaluatedItems: items, prefixItems, if, + then, else, contains, unevaluatedItems, allOf, oneOf, anyOf + """ + if validator.is_type(schema, "boolean"): + return [] + evaluated_indexes = [] + + ref = schema.get("$ref") + if ref is not None: + resolved = validator._resolver.lookup(ref) + evaluated_indexes.extend( + find_evaluated_item_indexes_by_schema( + validator.evolve( + schema=resolved.contents, + _resolver=resolved.resolver, + ), + instance, + resolved.contents, + ), + ) + + if "$recursiveRef" in schema: + resolved = lookup_recursive_ref(validator._resolver) + evaluated_indexes.extend( + find_evaluated_item_indexes_by_schema( + validator.evolve( + schema=resolved.contents, + _resolver=resolved.resolver, + ), + instance, + resolved.contents, + ), + ) + + if "items" in schema: + if "additionalItems" in schema: + return list(range(len(instance))) + + if validator.is_type(schema["items"], "object"): + return list(range(len(instance))) + evaluated_indexes += list(range(len(schema["items"]))) + + if "if" in schema: + if validator.evolve(schema=schema["if"]).is_valid(instance): + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, schema["if"], + ) + if "then" in schema: + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, schema["then"], + ) + elif "else" in schema: + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, schema["else"], + ) + + for keyword in ["contains", "unevaluatedItems"]: + if keyword in schema: + for k, v in enumerate(instance): + if validator.evolve(schema=schema[keyword]).is_valid(v): + evaluated_indexes.append(k) + + for keyword in ["allOf", "oneOf", "anyOf"]: + if keyword in schema: + for subschema in schema[keyword]: + errs = next(validator.descend(instance, subschema), None) + if errs is None: + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, subschema, + ) + + return evaluated_indexes + + +def unevaluatedItems_draft2019(validator, unevaluatedItems, instance, schema): + if not validator.is_type(instance, "array"): + return + evaluated_item_indexes = find_evaluated_item_indexes_by_schema( + validator, instance, schema, + ) + unevaluated_items = [ + item for index, item in enumerate(instance) + if index not in evaluated_item_indexes + ] + if unevaluated_items: + error = "Unevaluated items are not allowed (%s %s unexpected)" + yield ValidationError(error % _utils.extras_msg(unevaluated_items)) + + +def find_evaluated_property_keys_by_schema(validator, instance, schema): + if validator.is_type(schema, "boolean"): + return [] + evaluated_keys = [] + + ref = schema.get("$ref") + if ref is not None: + resolved = validator._resolver.lookup(ref) + evaluated_keys.extend( + find_evaluated_property_keys_by_schema( + validator.evolve( + schema=resolved.contents, + _resolver=resolved.resolver, + ), + instance, + resolved.contents, + ), + ) + + if "$recursiveRef" in schema: + resolved = lookup_recursive_ref(validator._resolver) + evaluated_keys.extend( + find_evaluated_property_keys_by_schema( + validator.evolve( + schema=resolved.contents, + _resolver=resolved.resolver, + ), + instance, + resolved.contents, + ), + ) + + for keyword in [ + "properties", "additionalProperties", "unevaluatedProperties", + ]: + if keyword in schema: + schema_value = schema[keyword] + if validator.is_type(schema_value, "boolean") and schema_value: + evaluated_keys += instance.keys() + + elif validator.is_type(schema_value, "object"): + for property in schema_value: + if property in instance: + evaluated_keys.append(property) + + if "patternProperties" in schema: + for property in instance: + for pattern in schema["patternProperties"]: + if re.search(pattern, property): + evaluated_keys.append(property) + + if "dependentSchemas" in schema: + for property, subschema in schema["dependentSchemas"].items(): + if property not in instance: + continue + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, subschema, + ) + + for keyword in ["allOf", "oneOf", "anyOf"]: + if keyword in schema: + for subschema in schema[keyword]: + errs = next(validator.descend(instance, subschema), None) + if errs is None: + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, subschema, + ) + + if "if" in schema: + if validator.evolve(schema=schema["if"]).is_valid(instance): + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, schema["if"], + ) + if "then" in schema: + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, schema["then"], + ) + elif "else" in schema: + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, schema["else"], + ) + + return evaluated_keys + + +def unevaluatedProperties_draft2019(validator, uP, instance, schema): + if not validator.is_type(instance, "object"): + return + evaluated_keys = find_evaluated_property_keys_by_schema( + validator, instance, schema, + ) + unevaluated_keys = [] + for property in instance: + if property not in evaluated_keys: + for _ in validator.descend( + instance[property], + uP, + path=property, + schema_path=property, + ): + # FIXME: Include context for each unevaluated property + # indicating why it's invalid under the subschema. + unevaluated_keys.append(property) # noqa: PERF401 + + if unevaluated_keys: + if uP is False: + error = "Unevaluated properties are not allowed (%s %s unexpected)" + extras = sorted(unevaluated_keys, key=str) + yield ValidationError(error % _utils.extras_msg(extras)) + else: + error = ( + "Unevaluated properties are not valid under " + "the given schema (%s %s unevaluated and invalid)" + ) + yield ValidationError(error % _utils.extras_msg(unevaluated_keys)) diff --git a/source/target-vendors/cp312/jsonschema/_types.py b/source/target-vendors/cp312/jsonschema/_types.py new file mode 100644 index 0000000..fcfe3f0 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/_types.py @@ -0,0 +1,204 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +import numbers + +from attrs import evolve, field, frozen +from rpds import HashTrieMap + +from jsonschema.exceptions import UndefinedTypeCheck + +if TYPE_CHECKING: + from collections.abc import Callable, Mapping + from typing import Any + + +# unfortunately, the type of HashTrieMap is generic, and if used as an attrs +# converter, the generic type is presented to mypy, which then fails to match +# the concrete type of a type checker mapping +# this "do nothing" wrapper presents the correct information to mypy +def _typed_map_converter( + init_val: Mapping[str, Callable[[TypeChecker, Any], bool]], +) -> HashTrieMap[str, Callable[[TypeChecker, Any], bool]]: + return HashTrieMap.convert(init_val) + + +def is_array(checker, instance): + return isinstance(instance, list) + + +def is_bool(checker, instance): + return isinstance(instance, bool) + + +def is_integer(checker, instance): + # bool inherits from int, so ensure bools aren't reported as ints + if isinstance(instance, bool): + return False + return isinstance(instance, int) + + +def is_null(checker, instance): + return instance is None + + +def is_number(checker, instance): + # bool inherits from int, so ensure bools aren't reported as ints + if isinstance(instance, bool): + return False + return isinstance(instance, numbers.Number) + + +def is_object(checker, instance): + return isinstance(instance, dict) + + +def is_string(checker, instance): + return isinstance(instance, str) + + +def is_any(checker, instance): + return True + + +@frozen(repr=False) +class TypeChecker: + """ + A :kw:`type` property checker. + + A `TypeChecker` performs type checking for a `Validator`, converting + between the defined JSON Schema types and some associated Python types or + objects. + + Modifying the behavior just mentioned by redefining which Python objects + are considered to be of which JSON Schema types can be done using + `TypeChecker.redefine` or `TypeChecker.redefine_many`, and types can be + removed via `TypeChecker.remove`. Each of these return a new `TypeChecker`. + + Arguments: + + type_checkers: + + The initial mapping of types to their checking functions. + + """ + + _type_checkers: HashTrieMap[ + str, Callable[[TypeChecker, Any], bool], + ] = field(default=HashTrieMap(), converter=_typed_map_converter) + + def __repr__(self): + types = ", ".join(repr(k) for k in sorted(self._type_checkers)) + return f"<{self.__class__.__name__} types={{{types}}}>" + + def is_type(self, instance, type: str) -> bool: + """ + Check if the instance is of the appropriate type. + + Arguments: + + instance: + + The instance to check + + type: + + The name of the type that is expected. + + Raises: + + `jsonschema.exceptions.UndefinedTypeCheck`: + + if ``type`` is unknown to this object. + + """ + try: + fn = self._type_checkers[type] + except KeyError: + raise UndefinedTypeCheck(type) from None + + return fn(self, instance) + + def redefine(self, type: str, fn) -> TypeChecker: + """ + Produce a new checker with the given type redefined. + + Arguments: + + type: + + The name of the type to check. + + fn (collections.abc.Callable): + + A callable taking exactly two parameters - the type + checker calling the function and the instance to check. + The function should return true if instance is of this + type and false otherwise. + + """ + return self.redefine_many({type: fn}) + + def redefine_many(self, definitions=()) -> TypeChecker: + """ + Produce a new checker with the given types redefined. + + Arguments: + + definitions (dict): + + A dictionary mapping types to their checking functions. + + """ + type_checkers = self._type_checkers.update(definitions) + return evolve(self, type_checkers=type_checkers) + + def remove(self, *types) -> TypeChecker: + """ + Produce a new checker with the given types forgotten. + + Arguments: + + types: + + the names of the types to remove. + + Raises: + + `jsonschema.exceptions.UndefinedTypeCheck`: + + if any given type is unknown to this object + + """ + type_checkers = self._type_checkers + for each in types: + try: + type_checkers = type_checkers.remove(each) + except KeyError: + raise UndefinedTypeCheck(each) from None + return evolve(self, type_checkers=type_checkers) + + +draft3_type_checker = TypeChecker( + { + "any": is_any, + "array": is_array, + "boolean": is_bool, + "integer": is_integer, + "object": is_object, + "null": is_null, + "number": is_number, + "string": is_string, + }, +) +draft4_type_checker = draft3_type_checker.remove("any") +draft6_type_checker = draft4_type_checker.redefine( + "integer", + lambda checker, instance: ( + is_integer(checker, instance) + or (isinstance(instance, float) and instance.is_integer()) + ), +) +draft7_type_checker = draft6_type_checker +draft201909_type_checker = draft7_type_checker +draft202012_type_checker = draft201909_type_checker diff --git a/source/target-vendors/cp312/jsonschema/_typing.py b/source/target-vendors/cp312/jsonschema/_typing.py new file mode 100644 index 0000000..f8dda63 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/_typing.py @@ -0,0 +1,29 @@ +""" +Some (initially private) typing helpers for jsonschema's types. +""" +from collections.abc import Callable, Iterable +from typing import Any, Protocol + +import referencing.jsonschema + +from jsonschema.protocols import Validator + + +class SchemaKeywordValidator(Protocol): + def __call__( + self, + validator: Validator, + value: Any, + instance: Any, + schema: referencing.jsonschema.Schema, + ) -> None: + ... + + +id_of = Callable[[referencing.jsonschema.Schema], str | None] + + +ApplicableValidators = Callable[ + [referencing.jsonschema.Schema], + Iterable[tuple[str, Any]], +] diff --git a/source/target-vendors/cp312/jsonschema/_utils.py b/source/target-vendors/cp312/jsonschema/_utils.py new file mode 100644 index 0000000..84a0965 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/_utils.py @@ -0,0 +1,355 @@ +from collections.abc import Mapping, MutableMapping, Sequence +from urllib.parse import urlsplit +import itertools +import re + + +class URIDict(MutableMapping): + """ + Dictionary which uses normalized URIs as keys. + """ + + def normalize(self, uri): + return urlsplit(uri).geturl() + + def __init__(self, *args, **kwargs): + self.store = dict() + self.store.update(*args, **kwargs) + + def __getitem__(self, uri): + return self.store[self.normalize(uri)] + + def __setitem__(self, uri, value): + self.store[self.normalize(uri)] = value + + def __delitem__(self, uri): + del self.store[self.normalize(uri)] + + def __iter__(self): + return iter(self.store) + + def __len__(self): # pragma: no cover -- untested, but to be removed + return len(self.store) + + def __repr__(self): # pragma: no cover -- untested, but to be removed + return repr(self.store) + + +class Unset: + """ + An as-of-yet unset attribute or unprovided default parameter. + """ + + def __repr__(self): # pragma: no cover + return "" + + +def format_as_index(container, indices): + """ + Construct a single string containing indexing operations for the indices. + + For example for a container ``bar``, [1, 2, "foo"] -> bar[1][2]["foo"] + + Arguments: + + container (str): + + A word to use for the thing being indexed + + indices (sequence): + + The indices to format. + + """ + if not indices: + return container + return f"{container}[{']['.join(repr(index) for index in indices)}]" + + +def find_additional_properties(instance, schema): + """ + Return the set of additional properties for the given ``instance``. + + Weeds out properties that should have been validated by ``properties`` and + / or ``patternProperties``. + + Assumes ``instance`` is dict-like already. + """ + properties = schema.get("properties", {}) + patterns = "|".join(schema.get("patternProperties", {})) + for property in instance: + if property not in properties: + if patterns and re.search(patterns, property): + continue + yield property + + +def extras_msg(extras): + """ + Create an error message for extra items or properties. + """ + verb = "was" if len(extras) == 1 else "were" + return ", ".join(repr(extra) for extra in extras), verb + + +def ensure_list(thing): + """ + Wrap ``thing`` in a list if it's a single str. + + Otherwise, return it unchanged. + """ + if isinstance(thing, str): + return [thing] + return thing + + +def _mapping_equal(one, two): + """ + Check if two mappings are equal using the semantics of `equal`. + """ + if len(one) != len(two): + return False + return all( + key in two and equal(value, two[key]) + for key, value in one.items() + ) + + +def _sequence_equal(one, two): + """ + Check if two sequences are equal using the semantics of `equal`. + """ + if len(one) != len(two): + return False + return all(equal(i, j) for i, j in zip(one, two)) + + +def equal(one, two): + """ + Check if two things are equal evading some Python type hierarchy semantics. + + Specifically in JSON Schema, evade `bool` inheriting from `int`, + recursing into sequences to do the same. + """ + if one is two: + return True + if isinstance(one, str) or isinstance(two, str): + return one == two + if isinstance(one, Sequence) and isinstance(two, Sequence): + return _sequence_equal(one, two) + if isinstance(one, Mapping) and isinstance(two, Mapping): + return _mapping_equal(one, two) + return unbool(one) == unbool(two) + + +def unbool(element, true=object(), false=object()): + """ + A hack to make True and 1 and False and 0 unique for ``uniq``. + """ + if element is True: + return true + elif element is False: + return false + return element + + +def uniq(container): + """ + Check if all of a container's elements are unique. + + Tries to rely on the container being recursively sortable, or otherwise + falls back on (slow) brute force. + """ + try: + sort = sorted(unbool(i) for i in container) + sliced = itertools.islice(sort, 1, None) + + for i, j in zip(sort, sliced): + if equal(i, j): + return False + + except (NotImplementedError, TypeError): + seen = [] + for e in container: + e = unbool(e) + + for i in seen: + if equal(i, e): + return False + + seen.append(e) + return True + + +def find_evaluated_item_indexes_by_schema(validator, instance, schema): + """ + Get all indexes of items that get evaluated under the current schema. + + Covers all keywords related to unevaluatedItems: items, prefixItems, if, + then, else, contains, unevaluatedItems, allOf, oneOf, anyOf + """ + if validator.is_type(schema, "boolean"): + return [] + evaluated_indexes = [] + + if "items" in schema: + return list(range(len(instance))) + + ref = schema.get("$ref") + if ref is not None: + resolved = validator._resolver.lookup(ref) + evaluated_indexes.extend( + find_evaluated_item_indexes_by_schema( + validator.evolve( + schema=resolved.contents, + _resolver=resolved.resolver, + ), + instance, + resolved.contents, + ), + ) + + dynamicRef = schema.get("$dynamicRef") + if dynamicRef is not None: + resolved = validator._resolver.lookup(dynamicRef) + evaluated_indexes.extend( + find_evaluated_item_indexes_by_schema( + validator.evolve( + schema=resolved.contents, + _resolver=resolved.resolver, + ), + instance, + resolved.contents, + ), + ) + + if "prefixItems" in schema: + evaluated_indexes += list(range(len(schema["prefixItems"]))) + + if "if" in schema: + if validator.evolve(schema=schema["if"]).is_valid(instance): + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, schema["if"], + ) + if "then" in schema: + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, schema["then"], + ) + elif "else" in schema: + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, schema["else"], + ) + + for keyword in ["contains", "unevaluatedItems"]: + if keyword in schema: + for k, v in enumerate(instance): + if validator.evolve(schema=schema[keyword]).is_valid(v): + evaluated_indexes.append(k) + + for keyword in ["allOf", "oneOf", "anyOf"]: + if keyword in schema: + for subschema in schema[keyword]: + errs = next(validator.descend(instance, subschema), None) + if errs is None: + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, subschema, + ) + + return evaluated_indexes + + +def find_evaluated_property_keys_by_schema(validator, instance, schema): + """ + Get all keys of items that get evaluated under the current schema. + + Covers all keywords related to unevaluatedProperties: properties, + additionalProperties, unevaluatedProperties, patternProperties, + dependentSchemas, allOf, oneOf, anyOf, if, then, else + """ + if validator.is_type(schema, "boolean"): + return [] + evaluated_keys = [] + + ref = schema.get("$ref") + if ref is not None: + resolved = validator._resolver.lookup(ref) + evaluated_keys.extend( + find_evaluated_property_keys_by_schema( + validator.evolve( + schema=resolved.contents, + _resolver=resolved.resolver, + ), + instance, + resolved.contents, + ), + ) + + dynamicRef = schema.get("$dynamicRef") + if dynamicRef is not None: + resolved = validator._resolver.lookup(dynamicRef) + evaluated_keys.extend( + find_evaluated_property_keys_by_schema( + validator.evolve( + schema=resolved.contents, + _resolver=resolved.resolver, + ), + instance, + resolved.contents, + ), + ) + + properties = schema.get("properties") + if validator.is_type(properties, "object"): + evaluated_keys += properties.keys() & instance.keys() + + for keyword in ["additionalProperties", "unevaluatedProperties"]: + if (subschema := schema.get(keyword)) is None: + continue + evaluated_keys += ( + key + for key, value in instance.items() + if is_valid(validator.descend(value, subschema)) + ) + + if "patternProperties" in schema: + for property in instance: + for pattern in schema["patternProperties"]: + if re.search(pattern, property): + evaluated_keys.append(property) + + if "dependentSchemas" in schema: + for property, subschema in schema["dependentSchemas"].items(): + if property not in instance: + continue + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, subschema, + ) + + for keyword in ["allOf", "oneOf", "anyOf"]: + for subschema in schema.get(keyword, []): + if not is_valid(validator.descend(instance, subschema)): + continue + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, subschema, + ) + + if "if" in schema: + if validator.evolve(schema=schema["if"]).is_valid(instance): + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, schema["if"], + ) + if "then" in schema: + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, schema["then"], + ) + elif "else" in schema: + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, schema["else"], + ) + + return evaluated_keys + + +def is_valid(errs_it): + """Whether there are no errors in the given iterator.""" + return next(errs_it, None) is None diff --git a/source/target-vendors/cp312/jsonschema/benchmarks/__init__.py b/source/target-vendors/cp312/jsonschema/benchmarks/__init__.py new file mode 100644 index 0000000..e3dcc68 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/benchmarks/__init__.py @@ -0,0 +1,5 @@ +""" +Benchmarks for validation. + +This package is *not* public API. +""" diff --git a/source/target-vendors/cp312/jsonschema/benchmarks/const_vs_enum.py b/source/target-vendors/cp312/jsonschema/benchmarks/const_vs_enum.py new file mode 100644 index 0000000..c6fecd1 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/benchmarks/const_vs_enum.py @@ -0,0 +1,30 @@ +""" +A benchmark for comparing equivalent validation of `const` and `enum`. +""" + +from pyperf import Runner + +from jsonschema import Draft202012Validator + +value = [37] * 100 +const_schema = {"const": list(value)} +enum_schema = {"enum": [list(value)]} + +valid = list(value) +invalid = [*valid, 73] + +const = Draft202012Validator(const_schema) +enum = Draft202012Validator(enum_schema) + +assert const.is_valid(valid) +assert enum.is_valid(valid) +assert not const.is_valid(invalid) +assert not enum.is_valid(invalid) + + +if __name__ == "__main__": + runner = Runner() + runner.bench_func("const valid", lambda: const.is_valid(valid)) + runner.bench_func("const invalid", lambda: const.is_valid(invalid)) + runner.bench_func("enum valid", lambda: enum.is_valid(valid)) + runner.bench_func("enum invalid", lambda: enum.is_valid(invalid)) diff --git a/source/target-vendors/cp312/jsonschema/benchmarks/contains.py b/source/target-vendors/cp312/jsonschema/benchmarks/contains.py new file mode 100644 index 0000000..739cd04 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/benchmarks/contains.py @@ -0,0 +1,28 @@ +""" +A benchmark for validation of the `contains` keyword. +""" + +from pyperf import Runner + +from jsonschema import Draft202012Validator + +schema = { + "type": "array", + "contains": {"const": 37}, +} +validator = Draft202012Validator(schema) + +size = 1000 +beginning = [37] + [0] * (size - 1) +middle = [0] * (size // 2) + [37] + [0] * (size // 2) +end = [0] * (size - 1) + [37] +invalid = [0] * size + + +if __name__ == "__main__": + runner = Runner() + runner.bench_func("baseline", lambda: validator.is_valid([])) + runner.bench_func("beginning", lambda: validator.is_valid(beginning)) + runner.bench_func("middle", lambda: validator.is_valid(middle)) + runner.bench_func("end", lambda: validator.is_valid(end)) + runner.bench_func("invalid", lambda: validator.is_valid(invalid)) diff --git a/source/target-vendors/cp312/jsonschema/benchmarks/import_benchmark.py b/source/target-vendors/cp312/jsonschema/benchmarks/import_benchmark.py new file mode 100644 index 0000000..59fa921 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/benchmarks/import_benchmark.py @@ -0,0 +1,31 @@ +""" +A benchmark which measures the import time of jsonschema +""" + +import subprocess +import sys + + +def import_time(loops): + total_us = 0 + for _ in range(loops): + p = subprocess.run( # noqa: S603 (arguments are static) + [sys.executable, "-X", "importtime", "-c", "import jsonschema"], + stderr=subprocess.PIPE, + stdout=subprocess.DEVNULL, + check=True, + ) + + line = p.stderr.splitlines()[-1] + field = line.split(b"|")[-2].strip() + us = int(field) # microseconds + total_us += us + + # pyperf expects seconds + return total_us / 1_000_000.0 + +if __name__ == "__main__": + from pyperf import Runner + runner = Runner() + + runner.bench_time_func("Import time (cumulative)", import_time) diff --git a/source/target-vendors/cp312/jsonschema/benchmarks/issue232.py b/source/target-vendors/cp312/jsonschema/benchmarks/issue232.py new file mode 100644 index 0000000..efd0715 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/benchmarks/issue232.py @@ -0,0 +1,25 @@ +""" +A performance benchmark using the example from issue #232. + +See https://github.com/python-jsonschema/jsonschema/pull/232. +""" +from pathlib import Path + +from pyperf import Runner +from referencing import Registry + +from jsonschema.tests._suite import Version +import jsonschema + +issue232 = Version( + path=Path(__file__).parent / "issue232", + remotes=Registry(), + name="issue232", +) + + +if __name__ == "__main__": + issue232.benchmark( + runner=Runner(), + Validator=jsonschema.Draft4Validator, + ) diff --git a/source/target-vendors/cp312/jsonschema/benchmarks/issue232/issue.json b/source/target-vendors/cp312/jsonschema/benchmarks/issue232/issue.json new file mode 100644 index 0000000..804c340 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/benchmarks/issue232/issue.json @@ -0,0 +1,2653 @@ +[ + { + "description": "Petstore", + "schema": { + "title": "A JSON Schema for Swagger 2.0 API.", + "id": "http://swagger.io/v2/schema.json#", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "required": [ + "swagger", + "info", + "paths" + ], + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "swagger": { + "type": "string", + "enum": [ + "2.0" + ], + "description": "The Swagger version of this document." + }, + "info": { + "$ref": "#/definitions/info" + }, + "host": { + "type": "string", + "pattern": "^[^{}/ :\\\\]+(?::\\d+)?$", + "description": "The host (name or ip) of the API. Example: 'swagger.io'" + }, + "basePath": { + "type": "string", + "pattern": "^/", + "description": "The base path to the API. Example: '/api'." + }, + "schemes": { + "$ref": "#/definitions/schemesList" + }, + "consumes": { + "description": "A list of MIME types accepted by the API.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "produces": { + "description": "A list of MIME types the API can produce.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "paths": { + "$ref": "#/definitions/paths" + }, + "definitions": { + "$ref": "#/definitions/definitions" + }, + "parameters": { + "$ref": "#/definitions/parameterDefinitions" + }, + "responses": { + "$ref": "#/definitions/responseDefinitions" + }, + "security": { + "$ref": "#/definitions/security" + }, + "securityDefinitions": { + "$ref": "#/definitions/securityDefinitions" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/tag" + }, + "uniqueItems": true + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + } + }, + "definitions": { + "info": { + "type": "object", + "description": "General information about the API.", + "required": [ + "version", + "title" + ], + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "title": { + "type": "string", + "description": "A unique and precise title of the API." + }, + "version": { + "type": "string", + "description": "A semantic version number of the API." + }, + "description": { + "type": "string", + "description": "A longer description of the API. Should be different from the title. GitHub Flavored Markdown is allowed." + }, + "termsOfService": { + "type": "string", + "description": "The terms of service for the API." + }, + "contact": { + "$ref": "#/definitions/contact" + }, + "license": { + "$ref": "#/definitions/license" + } + } + }, + "contact": { + "type": "object", + "description": "Contact information for the owners of the API.", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "The identifying name of the contact person/organization." + }, + "url": { + "type": "string", + "description": "The URL pointing to the contact information.", + "format": "uri" + }, + "email": { + "type": "string", + "description": "The email address of the contact person/organization.", + "format": "email" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "license": { + "type": "object", + "required": [ + "name" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "The name of the license type. It's encouraged to use an OSI compatible license." + }, + "url": { + "type": "string", + "description": "The URL pointing to the license.", + "format": "uri" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "paths": { + "type": "object", + "description": "Relative paths to the individual endpoints. They must be relative to the 'basePath'.", + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + }, + "^/": { + "$ref": "#/definitions/pathItem" + } + }, + "additionalProperties": false + }, + "definitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/schema" + }, + "description": "One or more JSON objects describing the schemas being consumed and produced by the API." + }, + "parameterDefinitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/parameter" + }, + "description": "One or more JSON representations for parameters" + }, + "responseDefinitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/response" + }, + "description": "One or more JSON representations for parameters" + }, + "externalDocs": { + "type": "object", + "additionalProperties": false, + "description": "information about external documentation", + "required": [ + "url" + ], + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "examples": { + "type": "object", + "additionalProperties": true + }, + "mimeType": { + "type": "string", + "description": "The MIME type of the HTTP message." + }, + "operation": { + "type": "object", + "required": [ + "responses" + ], + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "summary": { + "type": "string", + "description": "A brief summary of the operation." + }, + "description": { + "type": "string", + "description": "A longer description of the operation, GitHub Flavored Markdown is allowed." + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "operationId": { + "type": "string", + "description": "A unique identifier of the operation." + }, + "produces": { + "description": "A list of MIME types the API can produce.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "consumes": { + "description": "A list of MIME types the API can consume.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "parameters": { + "$ref": "#/definitions/parametersList" + }, + "responses": { + "$ref": "#/definitions/responses" + }, + "schemes": { + "$ref": "#/definitions/schemesList" + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "security": { + "$ref": "#/definitions/security" + } + } + }, + "pathItem": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "$ref": { + "type": "string" + }, + "get": { + "$ref": "#/definitions/operation" + }, + "put": { + "$ref": "#/definitions/operation" + }, + "post": { + "$ref": "#/definitions/operation" + }, + "delete": { + "$ref": "#/definitions/operation" + }, + "options": { + "$ref": "#/definitions/operation" + }, + "head": { + "$ref": "#/definitions/operation" + }, + "patch": { + "$ref": "#/definitions/operation" + }, + "parameters": { + "$ref": "#/definitions/parametersList" + } + } + }, + "responses": { + "type": "object", + "description": "Response objects names can either be any valid HTTP status code or 'default'.", + "minProperties": 1, + "additionalProperties": false, + "patternProperties": { + "^([0-9]{3})$|^(default)$": { + "$ref": "#/definitions/responseValue" + }, + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "not": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + } + }, + "responseValue": { + "oneOf": [ + { + "$ref": "#/definitions/response" + }, + { + "$ref": "#/definitions/jsonReference" + } + ] + }, + "response": { + "type": "object", + "required": [ + "description" + ], + "properties": { + "description": { + "type": "string" + }, + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/schema" + }, + { + "$ref": "#/definitions/fileSchema" + } + ] + }, + "headers": { + "$ref": "#/definitions/headers" + }, + "examples": { + "$ref": "#/definitions/examples" + } + }, + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/header" + } + }, + "header": { + "type": "object", + "additionalProperties": false, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "string", + "number", + "integer", + "boolean", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "vendorExtension": { + "description": "Any property starting with x- is valid.", + "additionalProperties": true, + "additionalItems": true + }, + "bodyParameter": { + "type": "object", + "required": [ + "name", + "in", + "schema" + ], + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "body" + ] + }, + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "schema": { + "$ref": "#/definitions/schema" + } + }, + "additionalProperties": false + }, + "headerParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "header" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "queryParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "query" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "allowEmptyValue": { + "type": "boolean", + "default": false, + "description": "allows sending a parameter by name only or with an empty value." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormatWithMulti" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "formDataParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "formData" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "allowEmptyValue": { + "type": "boolean", + "default": false, + "description": "allows sending a parameter by name only or with an empty value." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array", + "file" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormatWithMulti" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "pathParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "required": [ + "required" + ], + "properties": { + "required": { + "type": "boolean", + "enum": [ + true + ], + "description": "Determines whether or not this parameter is required or optional." + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "path" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "nonBodyParameter": { + "type": "object", + "required": [ + "name", + "in", + "type" + ], + "oneOf": [ + { + "$ref": "#/definitions/headerParameterSubSchema" + }, + { + "$ref": "#/definitions/formDataParameterSubSchema" + }, + { + "$ref": "#/definitions/queryParameterSubSchema" + }, + { + "$ref": "#/definitions/pathParameterSubSchema" + } + ] + }, + "parameter": { + "oneOf": [ + { + "$ref": "#/definitions/bodyParameter" + }, + { + "$ref": "#/definitions/nonBodyParameter" + } + ] + }, + "schema": { + "type": "object", + "description": "A deterministic version of a JSON Schema object.", + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "$ref": { + "type": "string" + }, + "format": { + "type": "string" + }, + "title": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + }, + "description": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/description" + }, + "default": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/default" + }, + "multipleOf": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/multipleOf" + }, + "maximum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/maximum" + }, + "exclusiveMaximum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMaximum" + }, + "minimum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/minimum" + }, + "exclusiveMinimum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMinimum" + }, + "maxLength": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minLength": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "pattern": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/pattern" + }, + "maxItems": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minItems": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "uniqueItems": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/uniqueItems" + }, + "maxProperties": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minProperties": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "required": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/stringArray" + }, + "enum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/enum" + }, + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/definitions/schema" + }, + { + "type": "boolean" + } + ], + "default": {} + }, + "type": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/type" + }, + "items": { + "anyOf": [ + { + "$ref": "#/definitions/schema" + }, + { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/schema" + } + } + ], + "default": {} + }, + "allOf": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/schema" + } + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/schema" + }, + "default": {} + }, + "discriminator": { + "type": "string" + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "xml": { + "$ref": "#/definitions/xml" + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "example": {} + }, + "additionalProperties": false + }, + "fileSchema": { + "type": "object", + "description": "A deterministic version of a JSON Schema object.", + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "required": [ + "type" + ], + "properties": { + "format": { + "type": "string" + }, + "title": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + }, + "description": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/description" + }, + "default": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/default" + }, + "required": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/stringArray" + }, + "type": { + "type": "string", + "enum": [ + "file" + ] + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "example": {} + }, + "additionalProperties": false + }, + "primitivesItems": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "string", + "number", + "integer", + "boolean", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "security": { + "type": "array", + "items": { + "$ref": "#/definitions/securityRequirement" + }, + "uniqueItems": true + }, + "securityRequirement": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "xml": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "prefix": { + "type": "string" + }, + "attribute": { + "type": "boolean", + "default": false + }, + "wrapped": { + "type": "boolean", + "default": false + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "tag": { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "securityDefinitions": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/basicAuthenticationSecurity" + }, + { + "$ref": "#/definitions/apiKeySecurity" + }, + { + "$ref": "#/definitions/oauth2ImplicitSecurity" + }, + { + "$ref": "#/definitions/oauth2PasswordSecurity" + }, + { + "$ref": "#/definitions/oauth2ApplicationSecurity" + }, + { + "$ref": "#/definitions/oauth2AccessCodeSecurity" + } + ] + } + }, + "basicAuthenticationSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "basic" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "apiKeySecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "name", + "in" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "apiKey" + ] + }, + "name": { + "type": "string" + }, + "in": { + "type": "string", + "enum": [ + "header", + "query" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2ImplicitSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "authorizationUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "implicit" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2PasswordSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "tokenUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "password" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2ApplicationSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "tokenUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "application" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2AccessCodeSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "authorizationUrl", + "tokenUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "accessCode" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2Scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "mediaTypeList": { + "type": "array", + "items": { + "$ref": "#/definitions/mimeType" + }, + "uniqueItems": true + }, + "parametersList": { + "type": "array", + "description": "The parameters needed to send a valid API call.", + "additionalItems": false, + "items": { + "oneOf": [ + { + "$ref": "#/definitions/parameter" + }, + { + "$ref": "#/definitions/jsonReference" + } + ] + }, + "uniqueItems": true + }, + "schemesList": { + "type": "array", + "description": "The transfer protocol of the API.", + "items": { + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss" + ] + }, + "uniqueItems": true + }, + "collectionFormat": { + "type": "string", + "enum": [ + "csv", + "ssv", + "tsv", + "pipes" + ], + "default": "csv" + }, + "collectionFormatWithMulti": { + "type": "string", + "enum": [ + "csv", + "ssv", + "tsv", + "pipes", + "multi" + ], + "default": "csv" + }, + "title": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + }, + "description": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/description" + }, + "default": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/default" + }, + "multipleOf": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/multipleOf" + }, + "maximum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/maximum" + }, + "exclusiveMaximum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMaximum" + }, + "minimum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/minimum" + }, + "exclusiveMinimum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMinimum" + }, + "maxLength": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minLength": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "pattern": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/pattern" + }, + "maxItems": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minItems": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "uniqueItems": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/uniqueItems" + }, + "enum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/enum" + }, + "jsonReference": { + "type": "object", + "required": [ + "$ref" + ], + "additionalProperties": false, + "properties": { + "$ref": { + "type": "string" + } + } + } + } + }, + "tests": [ + { + "description": "Example petsore", + "data": { + "swagger": "2.0", + "info": { + "description": "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.", + "version": "1.0.0", + "title": "Swagger Petstore", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "email": "apiteam@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "host": "petstore.swagger.io", + "basePath": "/v2", + "tags": [ + { + "name": "pet", + "description": "Everything about your Pets", + "externalDocs": { + "description": "Find out more", + "url": "http://swagger.io" + } + }, + { + "name": "store", + "description": "Access to Petstore orders" + }, + { + "name": "user", + "description": "Operations about user", + "externalDocs": { + "description": "Find out more about our store", + "url": "http://swagger.io" + } + } + ], + "schemes": [ + "http" + ], + "paths": { + "/pet": { + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "description": "", + "operationId": "addPet", + "consumes": [ + "application/json", + "application/xml" + ], + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": true, + "schema": { + "$ref": "#/definitions/Pet" + } + } + ], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "put": { + "tags": [ + "pet" + ], + "summary": "Update an existing pet", + "description": "", + "operationId": "updatePet", + "consumes": [ + "application/json", + "application/xml" + ], + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": true, + "schema": { + "$ref": "#/definitions/Pet" + } + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "405": { + "description": "Validation exception" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/findByStatus": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by status", + "description": "Multiple status values can be provided with comma separated strings", + "operationId": "findPetsByStatus", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Status values that need to be considered for filter", + "required": true, + "type": "array", + "items": { + "type": "string", + "enum": [ + "available", + "pending", + "sold" + ], + "default": "available" + }, + "collectionFormat": "multi" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + }, + "400": { + "description": "Invalid status value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/findByTags": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by tags", + "description": "Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", + "operationId": "findPetsByTags", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "Tags to filter by", + "required": true, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + }, + "400": { + "description": "Invalid tag value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ], + "deprecated": true + } + }, + "/pet/{petId}": { + "get": { + "tags": [ + "pet" + ], + "summary": "Find pet by ID", + "description": "Returns a single pet", + "operationId": "getPetById", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to return", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Pet" + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + }, + "security": [ + { + "api_key": [] + } + ] + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store with form data", + "description": "", + "operationId": "updatePetWithForm", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be updated", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "name", + "in": "formData", + "description": "Updated name of the pet", + "required": false, + "type": "string" + }, + { + "name": "status", + "in": "formData", + "description": "Updated status of the pet", + "required": false, + "type": "string" + } + ], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "delete": { + "tags": [ + "pet" + ], + "summary": "Deletes a pet", + "description": "", + "operationId": "deletePet", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "api_key", + "in": "header", + "required": false, + "type": "string" + }, + { + "name": "petId", + "in": "path", + "description": "Pet id to delete", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/{petId}/uploadImage": { + "post": { + "tags": [ + "pet" + ], + "summary": "uploads an image", + "description": "", + "operationId": "uploadFile", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to update", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "additionalMetadata", + "in": "formData", + "description": "Additional data to pass to server", + "required": false, + "type": "string" + }, + { + "name": "file", + "in": "formData", + "description": "file to upload", + "required": false, + "type": "file" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/ApiResponse" + } + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/store/inventory": { + "get": { + "tags": [ + "store" + ], + "summary": "Returns pet inventories by status", + "description": "Returns a map of status codes to quantities", + "operationId": "getInventory", + "produces": [ + "application/json" + ], + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/store/order": { + "post": { + "tags": [ + "store" + ], + "summary": "Place an order for a pet", + "description": "", + "operationId": "placeOrder", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "order placed for purchasing the pet", + "required": true, + "schema": { + "$ref": "#/definitions/Order" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Order" + } + }, + "400": { + "description": "Invalid Order" + } + } + } + }, + "/store/order/{orderId}": { + "get": { + "tags": [ + "store" + ], + "summary": "Find purchase order by ID", + "description": "For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions", + "operationId": "getOrderById", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of pet that needs to be fetched", + "required": true, + "type": "integer", + "maximum": 10.0, + "minimum": 1.0, + "format": "int64" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Order" + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + }, + "delete": { + "tags": [ + "store" + ], + "summary": "Delete purchase order by ID", + "description": "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors", + "operationId": "deleteOrder", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of the order that needs to be deleted", + "required": true, + "type": "integer", + "minimum": 1.0, + "format": "int64" + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + } + }, + "/user": { + "post": { + "tags": [ + "user" + ], + "summary": "Create user", + "description": "This can only be done by the logged in user.", + "operationId": "createUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Created user object", + "required": true, + "schema": { + "$ref": "#/definitions/User" + } + } + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/createWithArray": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "description": "", + "operationId": "createUsersWithArrayInput", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "List of user object", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + } + } + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/createWithList": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "description": "", + "operationId": "createUsersWithListInput", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "List of user object", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + } + } + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/login": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs user into the system", + "description": "", + "operationId": "loginUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "username", + "in": "query", + "description": "The user name for login", + "required": true, + "type": "string" + }, + { + "name": "password", + "in": "query", + "description": "The password for login in clear text", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "string" + }, + "headers": { + "X-Rate-Limit": { + "type": "integer", + "format": "int32", + "description": "calls per hour allowed by the user" + }, + "X-Expires-After": { + "type": "string", + "format": "date-time", + "description": "date in UTC when token expires" + } + } + }, + "400": { + "description": "Invalid username/password supplied" + } + } + } + }, + "/user/logout": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs out current logged in user session", + "description": "", + "operationId": "logoutUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/{username}": { + "get": { + "tags": [ + "user" + ], + "summary": "Get user by user name", + "description": "", + "operationId": "getUserByName", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be fetched. Use user1 for testing. ", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/User" + } + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + }, + "put": { + "tags": [ + "user" + ], + "summary": "Updated user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "username", + "in": "path", + "description": "name that need to be updated", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "description": "Updated user object", + "required": true, + "schema": { + "$ref": "#/definitions/User" + } + } + ], + "responses": { + "400": { + "description": "Invalid user supplied" + }, + "404": { + "description": "User not found" + } + } + }, + "delete": { + "tags": [ + "user" + ], + "summary": "Delete user", + "description": "This can only be done by the logged in user.", + "operationId": "deleteUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be deleted", + "required": true, + "type": "string" + } + ], + "responses": { + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + } + } + }, + "securityDefinitions": { + "petstore_auth": { + "type": "oauth2", + "authorizationUrl": "http://petstore.swagger.io/oauth/dialog", + "flow": "implicit", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + } + }, + "definitions": { + "Order": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "petId": { + "type": "integer", + "format": "int64" + }, + "quantity": { + "type": "integer", + "format": "int32" + }, + "shipDate": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "Order Status", + "enum": [ + "placed", + "approved", + "delivered" + ] + }, + "complete": { + "type": "boolean", + "default": false + } + }, + "xml": { + "name": "Order" + } + }, + "Category": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Category" + } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "username": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "userStatus": { + "type": "integer", + "format": "int32", + "description": "User Status" + } + }, + "xml": { + "name": "User" + } + }, + "Tag": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Tag" + } + }, + "Pet": { + "type": "object", + "required": [ + "name", + "photoUrls" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "category": { + "$ref": "#/definitions/Category" + }, + "name": { + "type": "string", + "example": "doggie" + }, + "photoUrls": { + "type": "array", + "xml": { + "name": "photoUrl", + "wrapped": true + }, + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "xml": { + "name": "tag", + "wrapped": true + }, + "items": { + "$ref": "#/definitions/Tag" + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + } + }, + "xml": { + "name": "Pet" + } + }, + "ApiResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + }, + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + } + }, + "valid": true + } + ] + } +] diff --git a/source/target-vendors/cp312/jsonschema/benchmarks/json_schema_test_suite.py b/source/target-vendors/cp312/jsonschema/benchmarks/json_schema_test_suite.py new file mode 100644 index 0000000..905fb6a --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/benchmarks/json_schema_test_suite.py @@ -0,0 +1,12 @@ +""" +A performance benchmark using the official test suite. + +This benchmarks jsonschema using every valid example in the +JSON-Schema-Test-Suite. It will take some time to complete. +""" +from pyperf import Runner + +from jsonschema.tests._suite import Suite + +if __name__ == "__main__": + Suite().benchmark(runner=Runner()) diff --git a/source/target-vendors/cp312/jsonschema/benchmarks/nested_schemas.py b/source/target-vendors/cp312/jsonschema/benchmarks/nested_schemas.py new file mode 100644 index 0000000..b025c47 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/benchmarks/nested_schemas.py @@ -0,0 +1,56 @@ +""" +Validating highly nested schemas shouldn't cause exponential time blowups. + +See https://github.com/python-jsonschema/jsonschema/issues/1097. +""" +from itertools import cycle + +from jsonschema.validators import validator_for + +metaschemaish = { + "$id": "https://example.com/draft/2020-12/schema/strict", + "$schema": "https://json-schema.org/draft/2020-12/schema", + + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": True, + "https://json-schema.org/draft/2020-12/vocab/applicator": True, + "https://json-schema.org/draft/2020-12/vocab/unevaluated": True, + "https://json-schema.org/draft/2020-12/vocab/validation": True, + "https://json-schema.org/draft/2020-12/vocab/meta-data": True, + "https://json-schema.org/draft/2020-12/vocab/format-annotation": True, + "https://json-schema.org/draft/2020-12/vocab/content": True, + }, + "$dynamicAnchor": "meta", + + "$ref": "https://json-schema.org/draft/2020-12/schema", + "unevaluatedProperties": False, +} + + +def nested_schema(levels): + """ + Produce a schema which validates deeply nested objects and arrays. + """ + + names = cycle(["foo", "bar", "baz", "quux", "spam", "eggs"]) + schema = {"type": "object", "properties": {"ham": {"type": "string"}}} + for _, name in zip(range(levels - 1), names): + schema = {"type": "object", "properties": {name: schema}} + return schema + + +validator = validator_for(metaschemaish)(metaschemaish) + +if __name__ == "__main__": + from pyperf import Runner + runner = Runner() + + not_nested = nested_schema(levels=1) + runner.bench_func("not nested", lambda: validator.is_valid(not_nested)) + + for levels in range(1, 11, 3): + schema = nested_schema(levels=levels) + runner.bench_func( + f"nested * {levels}", + lambda schema=schema: validator.is_valid(schema), + ) diff --git a/source/target-vendors/cp312/jsonschema/benchmarks/subcomponents.py b/source/target-vendors/cp312/jsonschema/benchmarks/subcomponents.py new file mode 100644 index 0000000..6d78c7b --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/benchmarks/subcomponents.py @@ -0,0 +1,42 @@ +""" +A benchmark which tries to compare the possible slow subparts of validation. +""" +from referencing import Registry +from referencing.jsonschema import DRAFT202012 +from rpds import HashTrieMap, HashTrieSet + +from jsonschema import Draft202012Validator + +schema = { + "type": "array", + "minLength": 1, + "maxLength": 1, + "items": {"type": "integer"}, +} + +hmap = HashTrieMap() +hset = HashTrieSet() + +registry = Registry() + +v = Draft202012Validator(schema) + + +def registry_data_structures(): + return hmap.insert("foo", "bar"), hset.insert("foo") + + +def registry_add(): + resource = DRAFT202012.create_resource(schema) + return registry.with_resource(uri="urn:example", resource=resource) + + +if __name__ == "__main__": + from pyperf import Runner + runner = Runner() + + runner.bench_func("HashMap/HashSet insertion", registry_data_structures) + runner.bench_func("Registry insertion", registry_add) + runner.bench_func("Success", lambda: v.is_valid([1])) + runner.bench_func("Failure", lambda: v.is_valid(["foo"])) + runner.bench_func("Metaschema validation", lambda: v.check_schema(schema)) diff --git a/source/target-vendors/cp312/jsonschema/benchmarks/unused_registry.py b/source/target-vendors/cp312/jsonschema/benchmarks/unused_registry.py new file mode 100644 index 0000000..7b272c2 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/benchmarks/unused_registry.py @@ -0,0 +1,35 @@ +""" +An unused schema registry should not cause slower validation. + +"Unused" here means one where no reference resolution is occurring anyhow. + +See https://github.com/python-jsonschema/jsonschema/issues/1088. +""" +from pyperf import Runner +from referencing import Registry +from referencing.jsonschema import DRAFT201909 + +from jsonschema import Draft201909Validator + +registry = Registry().with_resource( + "urn:example:foo", + DRAFT201909.create_resource({}), +) + +schema = {"$ref": "https://json-schema.org/draft/2019-09/schema"} +instance = {"maxLength": 4} + +no_registry = Draft201909Validator(schema) +with_useless_registry = Draft201909Validator(schema, registry=registry) + +if __name__ == "__main__": + runner = Runner() + + runner.bench_func( + "no registry", + lambda: no_registry.is_valid(instance), + ) + runner.bench_func( + "useless registry", + lambda: with_useless_registry.is_valid(instance), + ) diff --git a/source/target-vendors/cp312/jsonschema/benchmarks/useless_applicator_schemas.py b/source/target-vendors/cp312/jsonschema/benchmarks/useless_applicator_schemas.py new file mode 100644 index 0000000..f3229c0 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/benchmarks/useless_applicator_schemas.py @@ -0,0 +1,106 @@ + +""" +A benchmark for validation of applicators containing lots of useless schemas. + +Signals a small possible optimization to remove all such schemas ahead of time. +""" + +from pyperf import Runner + +from jsonschema import Draft202012Validator as Validator + +NUM_USELESS = 100000 + +subschema = {"const": 37} + +valid = 37 +invalid = 12 + +baseline = Validator(subschema) + + +# These should be indistinguishable from just `subschema` +by_name = { + "single subschema": { + "anyOf": Validator({"anyOf": [subschema]}), + "allOf": Validator({"allOf": [subschema]}), + "oneOf": Validator({"oneOf": [subschema]}), + }, + "redundant subschemas": { + "anyOf": Validator({"anyOf": [subschema] * NUM_USELESS}), + "allOf": Validator({"allOf": [subschema] * NUM_USELESS}), + }, + "useless successful subschemas (beginning)": { + "anyOf": Validator({"anyOf": [subschema, *[True] * NUM_USELESS]}), + "allOf": Validator({"allOf": [subschema, *[True] * NUM_USELESS]}), + }, + "useless successful subschemas (middle)": { + "anyOf": Validator( + { + "anyOf": [ + *[True] * (NUM_USELESS // 2), + subschema, + *[True] * (NUM_USELESS // 2), + ], + }, + ), + "allOf": Validator( + { + "allOf": [ + *[True] * (NUM_USELESS // 2), + subschema, + *[True] * (NUM_USELESS // 2), + ], + }, + ), + }, + "useless successful subschemas (end)": { + "anyOf": Validator({"anyOf": [*[True] * NUM_USELESS, subschema]}), + "allOf": Validator({"allOf": [*[True] * NUM_USELESS, subschema]}), + }, + "useless failing subschemas (beginning)": { + "anyOf": Validator({"anyOf": [subschema, *[False] * NUM_USELESS]}), + "oneOf": Validator({"oneOf": [subschema, *[False] * NUM_USELESS]}), + }, + "useless failing subschemas (middle)": { + "anyOf": Validator( + { + "anyOf": [ + *[False] * (NUM_USELESS // 2), + subschema, + *[False] * (NUM_USELESS // 2), + ], + }, + ), + "oneOf": Validator( + { + "oneOf": [ + *[False] * (NUM_USELESS // 2), + subschema, + *[False] * (NUM_USELESS // 2), + ], + }, + ), + }, + "useless failing subschemas (end)": { + "anyOf": Validator({"anyOf": [*[False] * NUM_USELESS, subschema]}), + "oneOf": Validator({"oneOf": [*[False] * NUM_USELESS, subschema]}), + }, +} + +if __name__ == "__main__": + runner = Runner() + + runner.bench_func("baseline valid", lambda: baseline.is_valid(valid)) + runner.bench_func("baseline invalid", lambda: baseline.is_valid(invalid)) + + for group, applicators in by_name.items(): + for applicator, validator in applicators.items(): + runner.bench_func( + f"{group}: {applicator} valid", + lambda validator=validator: validator.is_valid(valid), + ) + runner.bench_func( + f"{group}: {applicator} invalid", + lambda validator=validator: validator.is_valid(invalid), + ) diff --git a/source/target-vendors/cp312/jsonschema/benchmarks/useless_keywords.py b/source/target-vendors/cp312/jsonschema/benchmarks/useless_keywords.py new file mode 100644 index 0000000..50f4359 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/benchmarks/useless_keywords.py @@ -0,0 +1,32 @@ +""" +A benchmark for validation of schemas containing lots of useless keywords. + +Checks we filter them out once, ahead of time. +""" + +from pyperf import Runner + +from jsonschema import Draft202012Validator + +NUM_USELESS = 100000 +schema = dict( + [ + ("not", {"const": 42}), + *((str(i), i) for i in range(NUM_USELESS)), + ("type", "integer"), + *((str(i), i) for i in range(NUM_USELESS, NUM_USELESS)), + ("minimum", 37), + ], +) +validator = Draft202012Validator(schema) + +valid = 3737 +invalid = 12 + + +if __name__ == "__main__": + runner = Runner() + runner.bench_func("beginning of schema", lambda: validator.is_valid(42)) + runner.bench_func("middle of schema", lambda: validator.is_valid("foo")) + runner.bench_func("end of schema", lambda: validator.is_valid(12)) + runner.bench_func("valid", lambda: validator.is_valid(3737)) diff --git a/source/target-vendors/cp312/jsonschema/benchmarks/validator_creation.py b/source/target-vendors/cp312/jsonschema/benchmarks/validator_creation.py new file mode 100644 index 0000000..4baeb3a --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/benchmarks/validator_creation.py @@ -0,0 +1,14 @@ +from pyperf import Runner + +from jsonschema import Draft202012Validator + +schema = { + "type": "array", + "minLength": 1, + "maxLength": 1, + "items": {"type": "integer"}, +} + + +if __name__ == "__main__": + Runner().bench_func("validator creation", Draft202012Validator, schema) diff --git a/source/target-vendors/cp312/jsonschema/cli.py b/source/target-vendors/cp312/jsonschema/cli.py new file mode 100644 index 0000000..f3ca4d6 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/cli.py @@ -0,0 +1,292 @@ +""" +The ``jsonschema`` command line. +""" + +from importlib import metadata +from json import JSONDecodeError +from pkgutil import resolve_name +from textwrap import dedent +import argparse +import json +import sys +import traceback +import warnings + +from attrs import define, field + +from jsonschema.exceptions import SchemaError +from jsonschema.validators import _RefResolver, validator_for + +warnings.warn( + ( + "The jsonschema CLI is deprecated and will be removed in a future " + "version. Please use check-jsonschema instead, which can be installed " + "from https://pypi.org/project/check-jsonschema/" + ), + DeprecationWarning, + stacklevel=2, +) + + +class _CannotLoadFile(Exception): + pass + + +@define +class _Outputter: + + _formatter = field() + _stdout = field() + _stderr = field() + + @classmethod + def from_arguments(cls, arguments, stdout, stderr): + if arguments["output"] == "plain": + formatter = _PlainFormatter(arguments["error_format"]) + elif arguments["output"] == "pretty": + formatter = _PrettyFormatter() + return cls(formatter=formatter, stdout=stdout, stderr=stderr) + + def load(self, path): + try: + file = open(path) # noqa: SIM115, PTH123 + except FileNotFoundError as error: + self.filenotfound_error(path=path, exc_info=sys.exc_info()) + raise _CannotLoadFile() from error + + with file: + try: + return json.load(file) + except JSONDecodeError as error: + self.parsing_error(path=path, exc_info=sys.exc_info()) + raise _CannotLoadFile() from error + + def filenotfound_error(self, **kwargs): + self._stderr.write(self._formatter.filenotfound_error(**kwargs)) + + def parsing_error(self, **kwargs): + self._stderr.write(self._formatter.parsing_error(**kwargs)) + + def validation_error(self, **kwargs): + self._stderr.write(self._formatter.validation_error(**kwargs)) + + def validation_success(self, **kwargs): + self._stdout.write(self._formatter.validation_success(**kwargs)) + + +@define +class _PrettyFormatter: + + _ERROR_MSG = dedent( + """\ + ===[{type}]===({path})=== + + {body} + ----------------------------- + """, + ) + _SUCCESS_MSG = "===[SUCCESS]===({path})===\n" + + def filenotfound_error(self, path, exc_info): + return self._ERROR_MSG.format( + path=path, + type="FileNotFoundError", + body=f"{path!r} does not exist.", + ) + + def parsing_error(self, path, exc_info): + exc_type, exc_value, exc_traceback = exc_info + exc_lines = "".join( + traceback.format_exception(exc_type, exc_value, exc_traceback), + ) + return self._ERROR_MSG.format( + path=path, + type=exc_type.__name__, + body=exc_lines, + ) + + def validation_error(self, instance_path, error): + return self._ERROR_MSG.format( + path=instance_path, + type=error.__class__.__name__, + body=error, + ) + + def validation_success(self, instance_path): + return self._SUCCESS_MSG.format(path=instance_path) + + +@define +class _PlainFormatter: + + _error_format = field() + + def filenotfound_error(self, path, exc_info): + return f"{path!r} does not exist.\n" + + def parsing_error(self, path, exc_info): + return "Failed to parse {}: {}\n".format( + "" if path == "" else repr(path), + exc_info[1], + ) + + def validation_error(self, instance_path, error): + return self._error_format.format(file_name=instance_path, error=error) + + def validation_success(self, instance_path): + return "" + + +def _resolve_name_with_default(name): + if "." not in name: + name = "jsonschema." + name + return resolve_name(name) + + +parser = argparse.ArgumentParser( + description="JSON Schema Validation CLI", +) +parser.add_argument( + "-i", "--instance", + action="append", + dest="instances", + help=""" + a path to a JSON instance (i.e. filename.json) to validate (may + be specified multiple times). If no instances are provided via this + option, one will be expected on standard input. + """, +) +parser.add_argument( + "-F", "--error-format", + help=""" + the format to use for each validation error message, specified + in a form suitable for str.format. This string will be passed + one formatted object named 'error' for each ValidationError. + Only provide this option when using --output=plain, which is the + default. If this argument is unprovided and --output=plain is + used, a simple default representation will be used. + """, +) +parser.add_argument( + "-o", "--output", + choices=["plain", "pretty"], + default="plain", + help=""" + an output format to use. 'plain' (default) will produce minimal + text with one line for each error, while 'pretty' will produce + more detailed human-readable output on multiple lines. + """, +) +parser.add_argument( + "-V", "--validator", + type=_resolve_name_with_default, + help=""" + the fully qualified object name of a validator to use, or, for + validators that are registered with jsonschema, simply the name + of the class. + """, +) +parser.add_argument( + "--base-uri", + help=""" + a base URI to assign to the provided schema, even if it does not + declare one (via e.g. $id). This option can be used if you wish to + resolve relative references to a particular URI (or local path) + """, +) +parser.add_argument( + "--version", + action="version", + version=metadata.version("jsonschema"), +) +parser.add_argument( + "schema", + help="the path to a JSON Schema to validate with (i.e. schema.json)", +) + + +def parse_args(args): # noqa: D103 + arguments = vars(parser.parse_args(args=args or ["--help"])) + if arguments["output"] != "plain" and arguments["error_format"]: + raise parser.error( + "--error-format can only be used with --output plain", + ) + if arguments["output"] == "plain" and arguments["error_format"] is None: + arguments["error_format"] = "{error.instance}: {error.message}\n" + return arguments + + +def _validate_instance(instance_path, instance, validator, outputter): + invalid = False + for error in validator.iter_errors(instance): + invalid = True + outputter.validation_error(instance_path=instance_path, error=error) + + if not invalid: + outputter.validation_success(instance_path=instance_path) + return invalid + + +def main(args=sys.argv[1:]): # noqa: D103 + sys.exit(run(arguments=parse_args(args=args))) + + +def run(arguments, stdout=sys.stdout, stderr=sys.stderr, stdin=sys.stdin): # noqa: D103 + outputter = _Outputter.from_arguments( + arguments=arguments, + stdout=stdout, + stderr=stderr, + ) + + try: + schema = outputter.load(arguments["schema"]) + except _CannotLoadFile: + return 1 + + Validator = arguments["validator"] + if Validator is None: + Validator = validator_for(schema) + + try: + Validator.check_schema(schema) + except SchemaError as error: + outputter.validation_error( + instance_path=arguments["schema"], + error=error, + ) + return 1 + + if arguments["instances"]: + load, instances = outputter.load, arguments["instances"] + else: + def load(_): + try: + return json.load(stdin) + except JSONDecodeError as error: + outputter.parsing_error( + path="", exc_info=sys.exc_info(), + ) + raise _CannotLoadFile() from error + instances = [""] + + resolver = _RefResolver( + base_uri=arguments["base_uri"], + referrer=schema, + ) if arguments["base_uri"] is not None else None + + validator = Validator(schema, resolver=resolver) + exit_code = 0 + for each in instances: + try: + instance = load(each) + except _CannotLoadFile: + exit_code = 1 + else: + exit_code |= _validate_instance( + instance_path=each, + instance=instance, + validator=validator, + outputter=outputter, + ) + + return exit_code diff --git a/source/target-vendors/cp312/jsonschema/exceptions.py b/source/target-vendors/cp312/jsonschema/exceptions.py new file mode 100644 index 0000000..d955e35 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/exceptions.py @@ -0,0 +1,490 @@ +""" +Validation errors, and some surrounding helpers. +""" +from __future__ import annotations + +from collections import defaultdict, deque +from pprint import pformat +from textwrap import dedent, indent +from typing import TYPE_CHECKING, Any, ClassVar +import heapq +import re +import warnings + +from attrs import define +from referencing.exceptions import Unresolvable as _Unresolvable + +from jsonschema import _utils + +if TYPE_CHECKING: + from collections.abc import Iterable, Mapping, MutableMapping, Sequence + + from jsonschema import _types + +WEAK_MATCHES: frozenset[str] = frozenset(["anyOf", "oneOf"]) +STRONG_MATCHES: frozenset[str] = frozenset() + +_JSON_PATH_COMPATIBLE_PROPERTY_PATTERN = re.compile("^[a-zA-Z][a-zA-Z0-9_]*$") + +_unset = _utils.Unset() + + +def _pretty(thing: Any, prefix: str): + """ + Format something for an error message as prettily as we currently can. + """ + return indent(pformat(thing, width=72, sort_dicts=False), prefix).lstrip() + + +def __getattr__(name): + if name == "RefResolutionError": + warnings.warn( + _RefResolutionError._DEPRECATION_MESSAGE, + DeprecationWarning, + stacklevel=2, + ) + return _RefResolutionError + raise AttributeError(f"module {__name__} has no attribute {name}") + + +class _Error(Exception): + + _word_for_schema_in_error_message: ClassVar[str] + _word_for_instance_in_error_message: ClassVar[str] + + def __init__( + self, + message: str, + validator: str = _unset, # type: ignore[assignment] + path: Iterable[str | int] = (), + cause: Exception | None = None, + context=(), + validator_value: Any = _unset, + instance: Any = _unset, + schema: Mapping[str, Any] | bool = _unset, # type: ignore[assignment] + schema_path: Iterable[str | int] = (), + parent: _Error | None = None, + type_checker: _types.TypeChecker = _unset, # type: ignore[assignment] + ) -> None: + super().__init__( + message, + validator, + path, + cause, + context, + validator_value, + instance, + schema, + schema_path, + parent, + ) + self.message = message + self.path = self.relative_path = deque(path) + self.schema_path = self.relative_schema_path = deque(schema_path) + self.context = list(context) + self.cause = self.__cause__ = cause + self.validator = validator + self.validator_value = validator_value + self.instance = instance + self.schema = schema + self.parent = parent + self._type_checker = type_checker + + for error in context: + error.parent = self + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}: {self.message!r}>" + + def __str__(self) -> str: + essential_for_verbose = ( + self.validator, self.validator_value, self.instance, self.schema, + ) + if any(m is _unset for m in essential_for_verbose): + return self.message + + schema_path = _utils.format_as_index( + container=self._word_for_schema_in_error_message, + indices=list(self.relative_schema_path)[:-1], + ) + instance_path = _utils.format_as_index( + container=self._word_for_instance_in_error_message, + indices=self.relative_path, + ) + prefix = 16 * " " + + return dedent( + f"""\ + {self.message} + + Failed validating {self.validator!r} in {schema_path}: + {_pretty(self.schema, prefix=prefix)} + + On {instance_path}: + {_pretty(self.instance, prefix=prefix)} + """.rstrip(), + ) + + @classmethod + def create_from(cls, other: _Error): + return cls(**other._contents()) + + @property + def absolute_path(self) -> Sequence[str | int]: + parent = self.parent + if parent is None: + return self.relative_path + + path = deque(self.relative_path) + path.extendleft(reversed(parent.absolute_path)) + return path + + @property + def absolute_schema_path(self) -> Sequence[str | int]: + parent = self.parent + if parent is None: + return self.relative_schema_path + + path = deque(self.relative_schema_path) + path.extendleft(reversed(parent.absolute_schema_path)) + return path + + @property + def json_path(self) -> str: + path = "$" + for elem in self.absolute_path: + if isinstance(elem, int): + path += "[" + str(elem) + "]" + elif _JSON_PATH_COMPATIBLE_PROPERTY_PATTERN.match(elem): + path += "." + elem + else: + escaped_elem = elem.replace("\\", "\\\\").replace("'", r"\'") + path += "['" + escaped_elem + "']" + return path + + def _set( + self, + type_checker: _types.TypeChecker | None = None, + **kwargs: Any, + ) -> None: + if type_checker is not None and self._type_checker is _unset: + self._type_checker = type_checker + + for k, v in kwargs.items(): + if getattr(self, k) is _unset: + setattr(self, k, v) + + def _contents(self): + attrs = ( + "message", "cause", "context", "validator", "validator_value", + "path", "schema_path", "instance", "schema", "parent", + ) + return {attr: getattr(self, attr) for attr in attrs} + + def _matches_type(self) -> bool: + try: + # We ignore this as we want to simply crash if this happens + expected = self.schema["type"] # type: ignore[index] + except (KeyError, TypeError): + return False + + if isinstance(expected, str): + return self._type_checker.is_type(self.instance, expected) + + return any( + self._type_checker.is_type(self.instance, expected_type) + for expected_type in expected + ) + + +class ValidationError(_Error): + """ + An instance was invalid under a provided schema. + """ + + _word_for_schema_in_error_message = "schema" + _word_for_instance_in_error_message = "instance" + + +class SchemaError(_Error): + """ + A schema was invalid under its corresponding metaschema. + """ + + _word_for_schema_in_error_message = "metaschema" + _word_for_instance_in_error_message = "schema" + + +@define(slots=False) +class _RefResolutionError(Exception): # noqa: PLW1641 + """ + A ref could not be resolved. + """ + + _DEPRECATION_MESSAGE = ( + "jsonschema.exceptions.RefResolutionError is deprecated as of version " + "4.18.0. If you wish to catch potential reference resolution errors, " + "directly catch referencing.exceptions.Unresolvable." + ) + + _cause: Exception + + def __eq__(self, other): + if self.__class__ is not other.__class__: + return NotImplemented # pragma: no cover -- uncovered but deprecated # noqa: E501 + return self._cause == other._cause + + def __str__(self) -> str: + return str(self._cause) + + +class _WrappedReferencingError(_RefResolutionError, _Unresolvable): # pragma: no cover -- partially uncovered but to be removed # noqa: E501 + def __init__(self, cause: _Unresolvable): + object.__setattr__(self, "_wrapped", cause) + + def __eq__(self, other): + if other.__class__ is self.__class__: + return self._wrapped == other._wrapped + elif other.__class__ is self._wrapped.__class__: + return self._wrapped == other + return NotImplemented + + def __getattr__(self, attr): + return getattr(self._wrapped, attr) + + def __hash__(self): + return hash(self._wrapped) + + def __repr__(self): + return f"" + + def __str__(self): + return f"{self._wrapped.__class__.__name__}: {self._wrapped}" + + +class UndefinedTypeCheck(Exception): + """ + A type checker was asked to check a type it did not have registered. + """ + + def __init__(self, type: str) -> None: + self.type = type + + def __str__(self) -> str: + return f"Type {self.type!r} is unknown to this type checker" + + +class UnknownType(Exception): + """ + A validator was asked to validate an instance against an unknown type. + """ + + def __init__(self, type, instance, schema): + self.type = type + self.instance = instance + self.schema = schema + + def __str__(self): + prefix = 16 * " " + + return dedent( + f"""\ + Unknown type {self.type!r} for validator with schema: + {_pretty(self.schema, prefix=prefix)} + + While checking instance: + {_pretty(self.instance, prefix=prefix)} + """.rstrip(), + ) + + +class FormatError(Exception): + """ + Validating a format failed. + """ + + def __init__(self, message, cause=None): + super().__init__(message, cause) + self.message = message + self.cause = self.__cause__ = cause + + def __str__(self): + return self.message + + +class ErrorTree: + """ + ErrorTrees make it easier to check which validations failed. + """ + + _instance = _unset + + def __init__(self, errors: Iterable[ValidationError] = ()): + self.errors: MutableMapping[str, ValidationError] = {} + self._contents: Mapping[str, ErrorTree] = defaultdict(self.__class__) + + for error in errors: + container = self + for element in error.path: + container = container[element] + container.errors[error.validator] = error + + container._instance = error.instance + + def __contains__(self, index: str | int): + """ + Check whether ``instance[index]`` has any errors. + """ + return index in self._contents + + def __getitem__(self, index): + """ + Retrieve the child tree one level down at the given ``index``. + + If the index is not in the instance that this tree corresponds + to and is not known by this tree, whatever error would be raised + by ``instance.__getitem__`` will be propagated (usually this is + some subclass of `LookupError`. + """ + if self._instance is not _unset and index not in self: + self._instance[index] + return self._contents[index] + + def __setitem__(self, index: str | int, value: ErrorTree): + """ + Add an error to the tree at the given ``index``. + + .. deprecated:: v4.20.0 + + Setting items on an `ErrorTree` is deprecated without replacement. + To populate a tree, provide all of its sub-errors when you + construct the tree. + """ + warnings.warn( + "ErrorTree.__setitem__ is deprecated without replacement.", + DeprecationWarning, + stacklevel=2, + ) + self._contents[index] = value # type: ignore[index] + + def __iter__(self): + """ + Iterate (non-recursively) over the indices in the instance with errors. + """ + return iter(self._contents) + + def __len__(self): + """ + Return the `total_errors`. + """ + return self.total_errors + + def __repr__(self): + total = len(self) + errors = "error" if total == 1 else "errors" + return f"<{self.__class__.__name__} ({total} total {errors})>" + + @property + def total_errors(self): + """ + The total number of errors in the entire tree, including children. + """ + child_errors = sum(len(tree) for _, tree in self._contents.items()) + return len(self.errors) + child_errors + + +def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES): + """ + Create a key function that can be used to sort errors by relevance. + + Arguments: + weak (set): + a collection of validation keywords to consider to be + "weak". If there are two errors at the same level of the + instance and one is in the set of weak validation keywords, + the other error will take priority. By default, :kw:`anyOf` + and :kw:`oneOf` are considered weak keywords and will be + superseded by other same-level validation errors. + + strong (set): + a collection of validation keywords to consider to be + "strong" + + """ + + def relevance(error): + validator = error.validator + return ( # prefer errors which are ... + -len(error.path), # 'deeper' and thereby more specific + error.path, # earlier (for sibling errors) + validator not in weak, # for a non-low-priority keyword + validator in strong, # for a high priority keyword + not error._matches_type(), # at least match the instance's type + ) # otherwise we'll treat them the same + + return relevance + + +relevance = by_relevance() +""" +A key function (e.g. to use with `sorted`) which sorts errors by relevance. + +Example: + +.. code:: python + + sorted(validator.iter_errors(12), key=jsonschema.exceptions.relevance) +""" + + +def best_match(errors, key=relevance): + """ + Try to find an error that appears to be the best match among given errors. + + In general, errors that are higher up in the instance (i.e. for which + `ValidationError.path` is shorter) are considered better matches, + since they indicate "more" is wrong with the instance. + + If the resulting match is either :kw:`oneOf` or :kw:`anyOf`, the + *opposite* assumption is made -- i.e. the deepest error is picked, + since these keywords only need to match once, and any other errors + may not be relevant. + + Arguments: + errors (collections.abc.Iterable): + + the errors to select from. Do not provide a mixture of + errors from different validation attempts (i.e. from + different instances or schemas), since it won't produce + sensical output. + + key (collections.abc.Callable): + + the key to use when sorting errors. See `relevance` and + transitively `by_relevance` for more details (the default is + to sort with the defaults of that function). Changing the + default is only useful if you want to change the function + that rates errors but still want the error context descent + done by this function. + + Returns: + the best matching error, or ``None`` if the iterable was empty + + .. note:: + + This function is a heuristic. Its return value may change for a given + set of inputs from version to version if better heuristics are added. + + """ + best = max(errors, key=key, default=None) + if best is None: + return + + while best.context: + # Calculate the minimum via nsmallest, because we don't recurse if + # all nested errors have the same relevance (i.e. if min == max == all) + smallest = heapq.nsmallest(2, best.context, key=key) + if len(smallest) == 2 and key(smallest[0]) == key(smallest[1]): # noqa: PLR2004 + return best + best = smallest[0] + return best diff --git a/source/target-vendors/cp312/jsonschema/protocols.py b/source/target-vendors/cp312/jsonschema/protocols.py new file mode 100644 index 0000000..b6288dc --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/protocols.py @@ -0,0 +1,230 @@ +""" +typing.Protocol classes for jsonschema interfaces. +""" + +# for reference material on Protocols, see +# https://www.python.org/dev/peps/pep-0544/ + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, ClassVar, Protocol, runtime_checkable + +# in order for Sphinx to resolve references accurately from type annotations, +# it needs to see names like `jsonschema.TypeChecker` +# therefore, only import at type-checking time (to avoid circular references), +# but use `jsonschema` for any types which will otherwise not be resolvable +if TYPE_CHECKING: + from collections.abc import Iterable, Mapping + + import referencing.jsonschema + + from jsonschema import _typing + from jsonschema.exceptions import ValidationError + import jsonschema + import jsonschema.validators + +# For code authors working on the validator protocol, these are the three +# use-cases which should be kept in mind: +# +# 1. As a protocol class, it can be used in type annotations to describe the +# available methods and attributes of a validator +# 2. It is the source of autodoc for the validator documentation +# 3. It is runtime_checkable, meaning that it can be used in isinstance() +# checks. +# +# Since protocols are not base classes, isinstance() checking is limited in +# its capabilities. See docs on runtime_checkable for detail + + +@runtime_checkable +class Validator(Protocol): + """ + The protocol to which all validator classes adhere. + + Arguments: + + schema: + + The schema that the validator object will validate with. + It is assumed to be valid, and providing + an invalid schema can lead to undefined behavior. See + `Validator.check_schema` to validate a schema first. + + registry: + + a schema registry that will be used for looking up JSON references + + resolver: + + a resolver that will be used to resolve :kw:`$ref` + properties (JSON references). If unprovided, one will be created. + + .. deprecated:: v4.18.0 + + `RefResolver <_RefResolver>` has been deprecated in favor of + `referencing`, and with it, this argument. + + format_checker: + + if provided, a checker which will be used to assert about + :kw:`format` properties present in the schema. If unprovided, + *no* format validation is done, and the presence of format + within schemas is strictly informational. Certain formats + require additional packages to be installed in order to assert + against instances. Ensure you've installed `jsonschema` with + its `extra (optional) dependencies ` when + invoking ``pip``. + + .. deprecated:: v4.12.0 + + Subclassing validator classes now explicitly warns this is not part of + their public API. + + """ + + #: An object representing the validator's meta schema (the schema that + #: describes valid schemas in the given version). + META_SCHEMA: ClassVar[Mapping] + + #: A mapping of validation keywords (`str`\s) to functions that + #: validate the keyword with that name. For more information see + #: `creating-validators`. + VALIDATORS: ClassVar[Mapping] + + #: A `jsonschema.TypeChecker` that will be used when validating + #: :kw:`type` keywords in JSON schemas. + TYPE_CHECKER: ClassVar[jsonschema.TypeChecker] + + #: A `jsonschema.FormatChecker` that will be used when validating + #: :kw:`format` keywords in JSON schemas. + FORMAT_CHECKER: ClassVar[jsonschema.FormatChecker] + + #: A function which given a schema returns its ID. + ID_OF: _typing.id_of + + #: The schema that will be used to validate instances + schema: Mapping | bool + + def __init__( + self, + schema: Mapping | bool, + resolver: Any = None, # deprecated + format_checker: jsonschema.FormatChecker | None = None, + *, + registry: referencing.jsonschema.SchemaRegistry = ..., + ) -> None: ... + + @classmethod + def check_schema(cls, schema: Mapping | bool) -> None: + """ + Validate the given schema against the validator's `META_SCHEMA`. + + Raises: + + `jsonschema.exceptions.SchemaError`: + + if the schema is invalid + + """ + + def is_type(self, instance: Any, type: str) -> bool: + """ + Check if the instance is of the given (JSON Schema) type. + + Arguments: + + instance: + + the value to check + + type: + + the name of a known (JSON Schema) type + + Returns: + + whether the instance is of the given type + + Raises: + + `jsonschema.exceptions.UnknownType`: + + if ``type`` is not a known type + + """ + + def is_valid(self, instance: Any) -> bool: + """ + Check if the instance is valid under the current `schema`. + + Returns: + + whether the instance is valid or not + + >>> schema = {"maxItems" : 2} + >>> Draft202012Validator(schema).is_valid([2, 3, 4]) + False + + """ + + def iter_errors(self, instance: Any) -> Iterable[ValidationError]: + r""" + Lazily yield each of the validation errors in the given instance. + + >>> schema = { + ... "type" : "array", + ... "items" : {"enum" : [1, 2, 3]}, + ... "maxItems" : 2, + ... } + >>> v = Draft202012Validator(schema) + >>> for error in sorted(v.iter_errors([2, 3, 4]), key=str): + ... print(error.message) + 4 is not one of [1, 2, 3] + [2, 3, 4] is too long + + .. deprecated:: v4.0.0 + + Calling this function with a second schema argument is deprecated. + Use `Validator.evolve` instead. + """ + + def validate(self, instance: Any) -> None: + """ + Check if the instance is valid under the current `schema`. + + Raises: + + `jsonschema.exceptions.ValidationError`: + + if the instance is invalid + + >>> schema = {"maxItems" : 2} + >>> Draft202012Validator(schema).validate([2, 3, 4]) + Traceback (most recent call last): + ... + ValidationError: [2, 3, 4] is too long + + """ + + def evolve(self, **kwargs) -> Validator: + """ + Create a new validator like this one, but with given changes. + + Preserves all other attributes, so can be used to e.g. create a + validator with a different schema but with the same :kw:`$ref` + resolution behavior. + + >>> validator = Draft202012Validator({}) + >>> validator.evolve(schema={"type": "number"}) + Draft202012Validator(schema={'type': 'number'}, format_checker=None) + + The returned object satisfies the validator protocol, but may not + be of the same concrete class! In particular this occurs + when a :kw:`$ref` occurs to a schema with a different + :kw:`$schema` than this one (i.e. for a different draft). + + >>> validator.evolve( + ... schema={"$schema": Draft7Validator.META_SCHEMA["$id"]} + ... ) + Draft7Validator(schema=..., format_checker=None) + """ diff --git a/source/target-vendors/cp312/jsonschema/tests/__init__.py b/source/target-vendors/cp312/jsonschema/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/jsonschema/tests/_suite.py b/source/target-vendors/cp312/jsonschema/tests/_suite.py new file mode 100644 index 0000000..d61d382 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/tests/_suite.py @@ -0,0 +1,285 @@ +""" +Python representations of the JSON Schema Test Suite tests. +""" +from __future__ import annotations + +from contextlib import suppress +from functools import partial +from pathlib import Path +from typing import TYPE_CHECKING, Any +import json +import os +import re +import sys +import unittest + +from attrs import field, frozen +from referencing import Registry +import referencing.jsonschema + +if TYPE_CHECKING: + from collections.abc import Iterable, Mapping, Sequence + + from referencing.jsonschema import Schema + import pyperf + +from jsonschema.validators import _VALIDATORS +import jsonschema + +MAGIC_REMOTE_URL = "http://localhost:1234" + +_DELIMITERS = re.compile(r"[\W\- ]+") + + +def _find_suite(): + root = os.environ.get("JSON_SCHEMA_TEST_SUITE") + if root is not None: + return Path(root) + + root = Path(jsonschema.__file__).parent.parent / "json" + if not root.is_dir(): # pragma: no cover + raise ValueError( + ( + "Can't find the JSON-Schema-Test-Suite directory. " + "Set the 'JSON_SCHEMA_TEST_SUITE' environment " + "variable or run the tests from alongside a checkout " + "of the suite." + ), + ) + return root + + +@frozen +class Suite: + + _root: Path = field(factory=_find_suite) + + + def benchmark(self, runner: pyperf.Runner): # pragma: no cover + for name, Validator in _VALIDATORS.items(): + self.version(name=name).benchmark( + runner=runner, + Validator=Validator, + ) + + def version(self, name) -> Version: + Validator = _VALIDATORS[name] + uri: str = Validator.ID_OF(Validator.META_SCHEMA) # type: ignore[assignment] + specification = referencing.jsonschema.specification_with(uri) + + registry = Registry().with_contents( + remotes_in(root=self._root / "remotes", name=name, uri=uri), + default_specification=specification, + ) + return Version( + name=name, + path=self._root / "tests" / name, + remotes=registry, + ) + + +@frozen +class Version: + + _path: Path + _remotes: referencing.jsonschema.SchemaRegistry + + name: str + + def benchmark(self, **kwargs): # pragma: no cover + for case in self.cases(): + case.benchmark(**kwargs) + + def cases(self) -> Iterable[_Case]: + return self._cases_in(paths=self._path.glob("*.json")) + + def format_cases(self) -> Iterable[_Case]: + return self._cases_in(paths=self._path.glob("optional/format/*.json")) + + def optional_cases_of(self, name: str) -> Iterable[_Case]: + return self._cases_in(paths=[self._path / "optional" / f"{name}.json"]) + + def to_unittest_testcase(self, *groups, **kwargs): + name = kwargs.pop("name", "Test" + self.name.title().replace("-", "")) + methods = { + method.__name__: method + for method in ( + test.to_unittest_method(**kwargs) + for group in groups + for case in group + for test in case.tests + ) + } + cls = type(name, (unittest.TestCase,), methods) + + # We're doing crazy things, so if they go wrong, like a function + # behaving differently on some other interpreter, just make them + # not happen. + with suppress(Exception): + cls.__module__ = _someone_save_us_the_module_of_the_caller() + + return cls + + def _cases_in(self, paths: Iterable[Path]) -> Iterable[_Case]: + for path in paths: + for case in json.loads(path.read_text(encoding="utf-8")): + yield _Case.from_dict( + case, + version=self, + subject=path.stem, + remotes=self._remotes, + ) + + +@frozen +class _Case: + + version: Version + + subject: str + description: str + schema: Mapping[str, Any] | bool + tests: list[_Test] + comment: str | None = None + specification: Sequence[dict[str, str]] = () + + @classmethod + def from_dict(cls, data, remotes, **kwargs): + data.update(kwargs) + tests = [ + _Test( + version=data["version"], + subject=data["subject"], + case_description=data["description"], + schema=data["schema"], + remotes=remotes, + **test, + ) for test in data.pop("tests") + ] + return cls(tests=tests, **data) + + def benchmark(self, runner: pyperf.Runner, **kwargs): # pragma: no cover + for test in self.tests: + runner.bench_func( + test.fully_qualified_name, + partial(test.validate_ignoring_errors, **kwargs), + ) + + +def remotes_in( + root: Path, + name: str, + uri: str, +) -> Iterable[tuple[str, Schema]]: + # This messy logic is because the test suite is terrible at indicating + # what remotes are needed for what drafts, and mixes in schemas which + # have no $schema and which are invalid under earlier versions, in with + # other schemas which are needed for tests. + + for each in root.rglob("*.json"): + schema = json.loads(each.read_text()) + + relative = str(each.relative_to(root)).replace("\\", "/") + + if ( + ( # invalid boolean schema + name in {"draft3", "draft4"} + and each.stem == "tree" + ) or + ( # draft/*.json + "$schema" not in schema + and relative.startswith("draft") + and not relative.startswith(name) + ) + ): + continue + yield f"{MAGIC_REMOTE_URL}/{relative}", schema + + +@frozen(repr=False) +class _Test: + + version: Version + + subject: str + case_description: str + description: str + + data: Any + schema: Mapping[str, Any] | bool + + valid: bool + + _remotes: referencing.jsonschema.SchemaRegistry + + comment: str | None = None + + def __repr__(self): # pragma: no cover + return f"" + + @property + def fully_qualified_name(self): # pragma: no cover + return " > ".join( # noqa: FLY002 + [ + self.version.name, + self.subject, + self.case_description, + self.description, + ], + ) + + def to_unittest_method(self, skip=lambda test: None, **kwargs): + if self.valid: + def fn(this): + self.validate(**kwargs) + else: + def fn(this): + with this.assertRaises(jsonschema.ValidationError): + self.validate(**kwargs) + + fn.__name__ = "_".join( + [ + "test", + _DELIMITERS.sub("_", self.subject), + _DELIMITERS.sub("_", self.case_description), + _DELIMITERS.sub("_", self.description), + ], + ) + reason = skip(self) + if reason is None or os.environ.get("JSON_SCHEMA_DEBUG", "0") != "0": + return fn + elif os.environ.get("JSON_SCHEMA_EXPECTED_FAILURES", "0") != "0": # pragma: no cover # noqa: E501 + return unittest.expectedFailure(fn) + else: + return unittest.skip(reason)(fn) + + def validate(self, Validator, **kwargs): + Validator.check_schema(self.schema) + validator = Validator( + schema=self.schema, + registry=self._remotes, + **kwargs, + ) + if os.environ.get("JSON_SCHEMA_DEBUG", "0") != "0": # pragma: no cover + breakpoint() # noqa: T100 + validator.validate(instance=self.data) + + def validate_ignoring_errors(self, Validator): # pragma: no cover + with suppress(jsonschema.ValidationError): + self.validate(Validator=Validator) + + +def _someone_save_us_the_module_of_the_caller(): + """ + The FQON of the module 2nd stack frames up from here. + + This is intended to allow us to dynamically return test case classes that + are indistinguishable from being defined in the module that wants them. + + Otherwise, trial will mis-print the FQON, and copy pasting it won't re-run + the class that really is running. + + Save us all, this is all so so so so so terrible. + """ + + return sys._getframe(2).f_globals["__name__"] diff --git a/source/target-vendors/cp312/jsonschema/tests/fuzz_validate.py b/source/target-vendors/cp312/jsonschema/tests/fuzz_validate.py new file mode 100644 index 0000000..c12e88b --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/tests/fuzz_validate.py @@ -0,0 +1,50 @@ +""" +Fuzzing setup for OSS-Fuzz. + +See https://github.com/google/oss-fuzz/tree/master/projects/jsonschema for the +other half of the setup here. +""" +import sys + +from hypothesis import given, strategies + +import jsonschema + +PRIM = strategies.one_of( + strategies.booleans(), + strategies.integers(), + strategies.floats(allow_nan=False, allow_infinity=False), + strategies.text(), +) +DICT = strategies.recursive( + base=strategies.one_of( + strategies.booleans(), + strategies.dictionaries(strategies.text(), PRIM), + ), + extend=lambda inner: strategies.dictionaries(strategies.text(), inner), +) + + +@given(obj1=DICT, obj2=DICT) +def test_schemas(obj1, obj2): + try: + jsonschema.validate(instance=obj1, schema=obj2) + except jsonschema.exceptions.ValidationError: + pass + except jsonschema.exceptions.SchemaError: + pass + + +def main(): + atheris.instrument_all() + atheris.Setup( + sys.argv, + test_schemas.hypothesis.fuzz_one_input, + enable_python_coverage=True, + ) + atheris.Fuzz() + + +if __name__ == "__main__": + import atheris + main() diff --git a/source/target-vendors/cp312/jsonschema/tests/test_cli.py b/source/target-vendors/cp312/jsonschema/tests/test_cli.py new file mode 100644 index 0000000..bed9f3e --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/tests/test_cli.py @@ -0,0 +1,904 @@ +from contextlib import redirect_stderr, redirect_stdout +from importlib import metadata +from io import StringIO +from json import JSONDecodeError +from pathlib import Path +from textwrap import dedent +from unittest import TestCase +import json +import os +import subprocess +import sys +import tempfile +import warnings + +from jsonschema import Draft4Validator, Draft202012Validator +from jsonschema.exceptions import ( + SchemaError, + ValidationError, + _RefResolutionError, +) +from jsonschema.validators import _LATEST_VERSION, validate + +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + from jsonschema import cli + + +def fake_validator(*errors): + errors = list(reversed(errors)) + + class FakeValidator: + def __init__(self, *args, **kwargs): + pass + + def iter_errors(self, instance): + if errors: + return errors.pop() + return [] # pragma: no cover + + @classmethod + def check_schema(self, schema): + pass + + return FakeValidator + + +def fake_open(all_contents): + def open(path): + contents = all_contents.get(path) + if contents is None: + raise FileNotFoundError(path) + return StringIO(contents) + return open + + +def _message_for(non_json): + try: + json.loads(non_json) + except JSONDecodeError as error: + return str(error) + else: # pragma: no cover + raise RuntimeError("Tried and failed to capture a JSON dump error.") + + +class TestCLI(TestCase): + def run_cli( + self, argv, files=None, stdin=StringIO(), exit_code=0, **override, + ): + arguments = cli.parse_args(argv) + arguments.update(override) + + self.assertFalse(hasattr(cli, "open")) + cli.open = fake_open(files or {}) + try: + stdout, stderr = StringIO(), StringIO() + actual_exit_code = cli.run( + arguments, + stdin=stdin, + stdout=stdout, + stderr=stderr, + ) + finally: + del cli.open + + self.assertEqual( + actual_exit_code, exit_code, msg=dedent( + f""" + Expected an exit code of {exit_code} != {actual_exit_code}. + + stdout: {stdout.getvalue()} + + stderr: {stderr.getvalue()} + """, + ), + ) + return stdout.getvalue(), stderr.getvalue() + + def assertOutputs(self, stdout="", stderr="", **kwargs): + self.assertEqual( + self.run_cli(**kwargs), + (dedent(stdout), dedent(stderr)), + ) + + def test_invalid_instance(self): + error = ValidationError("I am an error!", instance=12) + self.assertOutputs( + files=dict( + some_schema='{"does not": "matter since it is stubbed"}', + some_instance=json.dumps(error.instance), + ), + validator=fake_validator([error]), + + argv=["-i", "some_instance", "some_schema"], + + exit_code=1, + stderr="12: I am an error!\n", + ) + + def test_invalid_instance_pretty_output(self): + error = ValidationError("I am an error!", instance=12) + self.assertOutputs( + files=dict( + some_schema='{"does not": "matter since it is stubbed"}', + some_instance=json.dumps(error.instance), + ), + validator=fake_validator([error]), + + argv=["-i", "some_instance", "--output", "pretty", "some_schema"], + + exit_code=1, + stderr="""\ + ===[ValidationError]===(some_instance)=== + + I am an error! + ----------------------------- + """, + ) + + def test_invalid_instance_explicit_plain_output(self): + error = ValidationError("I am an error!", instance=12) + self.assertOutputs( + files=dict( + some_schema='{"does not": "matter since it is stubbed"}', + some_instance=json.dumps(error.instance), + ), + validator=fake_validator([error]), + + argv=["--output", "plain", "-i", "some_instance", "some_schema"], + + exit_code=1, + stderr="12: I am an error!\n", + ) + + def test_invalid_instance_multiple_errors(self): + instance = 12 + first = ValidationError("First error", instance=instance) + second = ValidationError("Second error", instance=instance) + + self.assertOutputs( + files=dict( + some_schema='{"does not": "matter since it is stubbed"}', + some_instance=json.dumps(instance), + ), + validator=fake_validator([first, second]), + + argv=["-i", "some_instance", "some_schema"], + + exit_code=1, + stderr="""\ + 12: First error + 12: Second error + """, + ) + + def test_invalid_instance_multiple_errors_pretty_output(self): + instance = 12 + first = ValidationError("First error", instance=instance) + second = ValidationError("Second error", instance=instance) + + self.assertOutputs( + files=dict( + some_schema='{"does not": "matter since it is stubbed"}', + some_instance=json.dumps(instance), + ), + validator=fake_validator([first, second]), + + argv=["-i", "some_instance", "--output", "pretty", "some_schema"], + + exit_code=1, + stderr="""\ + ===[ValidationError]===(some_instance)=== + + First error + ----------------------------- + ===[ValidationError]===(some_instance)=== + + Second error + ----------------------------- + """, + ) + + def test_multiple_invalid_instances(self): + first_instance = 12 + first_errors = [ + ValidationError("An error", instance=first_instance), + ValidationError("Another error", instance=first_instance), + ] + second_instance = "foo" + second_errors = [ValidationError("BOOM", instance=second_instance)] + + self.assertOutputs( + files=dict( + some_schema='{"does not": "matter since it is stubbed"}', + some_first_instance=json.dumps(first_instance), + some_second_instance=json.dumps(second_instance), + ), + validator=fake_validator(first_errors, second_errors), + + argv=[ + "-i", "some_first_instance", + "-i", "some_second_instance", + "some_schema", + ], + + exit_code=1, + stderr="""\ + 12: An error + 12: Another error + foo: BOOM + """, + ) + + def test_multiple_invalid_instances_pretty_output(self): + first_instance = 12 + first_errors = [ + ValidationError("An error", instance=first_instance), + ValidationError("Another error", instance=first_instance), + ] + second_instance = "foo" + second_errors = [ValidationError("BOOM", instance=second_instance)] + + self.assertOutputs( + files=dict( + some_schema='{"does not": "matter since it is stubbed"}', + some_first_instance=json.dumps(first_instance), + some_second_instance=json.dumps(second_instance), + ), + validator=fake_validator(first_errors, second_errors), + + argv=[ + "--output", "pretty", + "-i", "some_first_instance", + "-i", "some_second_instance", + "some_schema", + ], + + exit_code=1, + stderr="""\ + ===[ValidationError]===(some_first_instance)=== + + An error + ----------------------------- + ===[ValidationError]===(some_first_instance)=== + + Another error + ----------------------------- + ===[ValidationError]===(some_second_instance)=== + + BOOM + ----------------------------- + """, + ) + + def test_custom_error_format(self): + first_instance = 12 + first_errors = [ + ValidationError("An error", instance=first_instance), + ValidationError("Another error", instance=first_instance), + ] + second_instance = "foo" + second_errors = [ValidationError("BOOM", instance=second_instance)] + + self.assertOutputs( + files=dict( + some_schema='{"does not": "matter since it is stubbed"}', + some_first_instance=json.dumps(first_instance), + some_second_instance=json.dumps(second_instance), + ), + validator=fake_validator(first_errors, second_errors), + + argv=[ + "--error-format", ":{error.message}._-_.{error.instance}:", + "-i", "some_first_instance", + "-i", "some_second_instance", + "some_schema", + ], + + exit_code=1, + stderr=":An error._-_.12::Another error._-_.12::BOOM._-_.foo:", + ) + + def test_invalid_schema(self): + self.assertOutputs( + files=dict(some_schema='{"type": 12}'), + argv=["some_schema"], + + exit_code=1, + stderr="""\ + 12: 12 is not valid under any of the given schemas + """, + ) + + def test_invalid_schema_pretty_output(self): + schema = {"type": 12} + + with self.assertRaises(SchemaError) as e: + validate(schema=schema, instance="") + error = str(e.exception) + + self.assertOutputs( + files=dict(some_schema=json.dumps(schema)), + argv=["--output", "pretty", "some_schema"], + + exit_code=1, + stderr=( + "===[SchemaError]===(some_schema)===\n\n" + + str(error) + + "\n-----------------------------\n" + ), + ) + + def test_invalid_schema_multiple_errors(self): + self.assertOutputs( + files=dict(some_schema='{"type": 12, "items": 57}'), + argv=["some_schema"], + + exit_code=1, + stderr="""\ + 57: 57 is not of type 'object', 'boolean' + """, + ) + + def test_invalid_schema_multiple_errors_pretty_output(self): + schema = {"type": 12, "items": 57} + + with self.assertRaises(SchemaError) as e: + validate(schema=schema, instance="") + error = str(e.exception) + + self.assertOutputs( + files=dict(some_schema=json.dumps(schema)), + argv=["--output", "pretty", "some_schema"], + + exit_code=1, + stderr=( + "===[SchemaError]===(some_schema)===\n\n" + + str(error) + + "\n-----------------------------\n" + ), + ) + + def test_invalid_schema_with_invalid_instance(self): + """ + "Validating" an instance that's invalid under an invalid schema + just shows the schema error. + """ + self.assertOutputs( + files=dict( + some_schema='{"type": 12, "minimum": 30}', + some_instance="13", + ), + argv=["-i", "some_instance", "some_schema"], + + exit_code=1, + stderr="""\ + 12: 12 is not valid under any of the given schemas + """, + ) + + def test_invalid_schema_with_invalid_instance_pretty_output(self): + instance, schema = 13, {"type": 12, "minimum": 30} + + with self.assertRaises(SchemaError) as e: + validate(schema=schema, instance=instance) + error = str(e.exception) + + self.assertOutputs( + files=dict( + some_schema=json.dumps(schema), + some_instance=json.dumps(instance), + ), + argv=["--output", "pretty", "-i", "some_instance", "some_schema"], + + exit_code=1, + stderr=( + "===[SchemaError]===(some_schema)===\n\n" + + str(error) + + "\n-----------------------------\n" + ), + ) + + def test_invalid_instance_continues_with_the_rest(self): + self.assertOutputs( + files=dict( + some_schema='{"minimum": 30}', + first_instance="not valid JSON!", + second_instance="12", + ), + argv=[ + "-i", "first_instance", + "-i", "second_instance", + "some_schema", + ], + + exit_code=1, + stderr="""\ + Failed to parse 'first_instance': {} + 12: 12 is less than the minimum of 30 + """.format(_message_for("not valid JSON!")), + ) + + def test_custom_error_format_applies_to_schema_errors(self): + instance, schema = 13, {"type": 12, "minimum": 30} + + with self.assertRaises(SchemaError): + validate(schema=schema, instance=instance) + + self.assertOutputs( + files=dict(some_schema=json.dumps(schema)), + + argv=[ + "--error-format", ":{error.message}._-_.{error.instance}:", + "some_schema", + ], + + exit_code=1, + stderr=":12 is not valid under any of the given schemas._-_.12:", + ) + + def test_instance_is_invalid_JSON(self): + instance = "not valid JSON!" + + self.assertOutputs( + files=dict(some_schema="{}", some_instance=instance), + argv=["-i", "some_instance", "some_schema"], + + exit_code=1, + stderr=f"""\ + Failed to parse 'some_instance': {_message_for(instance)} + """, + ) + + def test_instance_is_invalid_JSON_pretty_output(self): + stdout, stderr = self.run_cli( + files=dict( + some_schema="{}", + some_instance="not valid JSON!", + ), + + argv=["--output", "pretty", "-i", "some_instance", "some_schema"], + + exit_code=1, + ) + self.assertFalse(stdout) + self.assertIn( + "(some_instance)===\n\nTraceback (most recent call last):\n", + stderr, + ) + self.assertNotIn("some_schema", stderr) + + def test_instance_is_invalid_JSON_on_stdin(self): + instance = "not valid JSON!" + + self.assertOutputs( + files=dict(some_schema="{}"), + stdin=StringIO(instance), + + argv=["some_schema"], + + exit_code=1, + stderr=f"""\ + Failed to parse : {_message_for(instance)} + """, + ) + + def test_instance_is_invalid_JSON_on_stdin_pretty_output(self): + stdout, stderr = self.run_cli( + files=dict(some_schema="{}"), + stdin=StringIO("not valid JSON!"), + + argv=["--output", "pretty", "some_schema"], + + exit_code=1, + ) + self.assertFalse(stdout) + self.assertIn( + "()===\n\nTraceback (most recent call last):\n", + stderr, + ) + self.assertNotIn("some_schema", stderr) + + def test_schema_is_invalid_JSON(self): + schema = "not valid JSON!" + + self.assertOutputs( + files=dict(some_schema=schema), + + argv=["some_schema"], + + exit_code=1, + stderr=f"""\ + Failed to parse 'some_schema': {_message_for(schema)} + """, + ) + + def test_schema_is_invalid_JSON_pretty_output(self): + stdout, stderr = self.run_cli( + files=dict(some_schema="not valid JSON!"), + + argv=["--output", "pretty", "some_schema"], + + exit_code=1, + ) + self.assertFalse(stdout) + self.assertIn( + "(some_schema)===\n\nTraceback (most recent call last):\n", + stderr, + ) + + def test_schema_and_instance_are_both_invalid_JSON(self): + """ + Only the schema error is reported, as we abort immediately. + """ + schema, instance = "not valid JSON!", "also not valid JSON!" + self.assertOutputs( + files=dict(some_schema=schema, some_instance=instance), + + argv=["some_schema"], + + exit_code=1, + stderr=f"""\ + Failed to parse 'some_schema': {_message_for(schema)} + """, + ) + + def test_schema_and_instance_are_both_invalid_JSON_pretty_output(self): + """ + Only the schema error is reported, as we abort immediately. + """ + stdout, stderr = self.run_cli( + files=dict( + some_schema="not valid JSON!", + some_instance="also not valid JSON!", + ), + + argv=["--output", "pretty", "-i", "some_instance", "some_schema"], + + exit_code=1, + ) + self.assertFalse(stdout) + self.assertIn( + "(some_schema)===\n\nTraceback (most recent call last):\n", + stderr, + ) + self.assertNotIn("some_instance", stderr) + + def test_instance_does_not_exist(self): + self.assertOutputs( + files=dict(some_schema="{}"), + argv=["-i", "nonexisting_instance", "some_schema"], + + exit_code=1, + stderr="""\ + 'nonexisting_instance' does not exist. + """, + ) + + def test_instance_does_not_exist_pretty_output(self): + self.assertOutputs( + files=dict(some_schema="{}"), + argv=[ + "--output", "pretty", + "-i", "nonexisting_instance", + "some_schema", + ], + + exit_code=1, + stderr="""\ + ===[FileNotFoundError]===(nonexisting_instance)=== + + 'nonexisting_instance' does not exist. + ----------------------------- + """, + ) + + def test_schema_does_not_exist(self): + self.assertOutputs( + argv=["nonexisting_schema"], + + exit_code=1, + stderr="'nonexisting_schema' does not exist.\n", + ) + + def test_schema_does_not_exist_pretty_output(self): + self.assertOutputs( + argv=["--output", "pretty", "nonexisting_schema"], + + exit_code=1, + stderr="""\ + ===[FileNotFoundError]===(nonexisting_schema)=== + + 'nonexisting_schema' does not exist. + ----------------------------- + """, + ) + + def test_neither_instance_nor_schema_exist(self): + self.assertOutputs( + argv=["-i", "nonexisting_instance", "nonexisting_schema"], + + exit_code=1, + stderr="'nonexisting_schema' does not exist.\n", + ) + + def test_neither_instance_nor_schema_exist_pretty_output(self): + self.assertOutputs( + argv=[ + "--output", "pretty", + "-i", "nonexisting_instance", + "nonexisting_schema", + ], + + exit_code=1, + stderr="""\ + ===[FileNotFoundError]===(nonexisting_schema)=== + + 'nonexisting_schema' does not exist. + ----------------------------- + """, + ) + + def test_successful_validation(self): + self.assertOutputs( + files=dict(some_schema="{}", some_instance="{}"), + argv=["-i", "some_instance", "some_schema"], + stdout="", + stderr="", + ) + + def test_successful_validation_pretty_output(self): + self.assertOutputs( + files=dict(some_schema="{}", some_instance="{}"), + argv=["--output", "pretty", "-i", "some_instance", "some_schema"], + stdout="===[SUCCESS]===(some_instance)===\n", + stderr="", + ) + + def test_successful_validation_of_stdin(self): + self.assertOutputs( + files=dict(some_schema="{}"), + stdin=StringIO("{}"), + argv=["some_schema"], + stdout="", + stderr="", + ) + + def test_successful_validation_of_stdin_pretty_output(self): + self.assertOutputs( + files=dict(some_schema="{}"), + stdin=StringIO("{}"), + argv=["--output", "pretty", "some_schema"], + stdout="===[SUCCESS]===()===\n", + stderr="", + ) + + def test_successful_validation_of_just_the_schema(self): + self.assertOutputs( + files=dict(some_schema="{}", some_instance="{}"), + argv=["-i", "some_instance", "some_schema"], + stdout="", + stderr="", + ) + + def test_successful_validation_of_just_the_schema_pretty_output(self): + self.assertOutputs( + files=dict(some_schema="{}", some_instance="{}"), + argv=["--output", "pretty", "-i", "some_instance", "some_schema"], + stdout="===[SUCCESS]===(some_instance)===\n", + stderr="", + ) + + def test_successful_validation_via_explicit_base_uri(self): + ref_schema_file = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115 + ref_schema_file.close() + self.addCleanup(os.remove, ref_schema_file.name) + + ref_path = Path(ref_schema_file.name) + ref_path.write_text('{"definitions": {"num": {"type": "integer"}}}') + + schema = f'{{"$ref": "{ref_path.name}#/definitions/num"}}' + + self.assertOutputs( + files=dict(some_schema=schema, some_instance="1"), + argv=[ + "-i", "some_instance", + "--base-uri", ref_path.parent.as_uri() + "/", + "some_schema", + ], + stdout="", + stderr="", + ) + + def test_unsuccessful_validation_via_explicit_base_uri(self): + ref_schema_file = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115 + ref_schema_file.close() + self.addCleanup(os.remove, ref_schema_file.name) + + ref_path = Path(ref_schema_file.name) + ref_path.write_text('{"definitions": {"num": {"type": "integer"}}}') + + schema = f'{{"$ref": "{ref_path.name}#/definitions/num"}}' + + self.assertOutputs( + files=dict(some_schema=schema, some_instance='"1"'), + argv=[ + "-i", "some_instance", + "--base-uri", ref_path.parent.as_uri() + "/", + "some_schema", + ], + exit_code=1, + stdout="", + stderr="1: '1' is not of type 'integer'\n", + ) + + def test_nonexistent_file_with_explicit_base_uri(self): + schema = '{"$ref": "someNonexistentFile.json#definitions/num"}' + instance = "1" + + with self.assertRaises(_RefResolutionError) as e: + self.assertOutputs( + files=dict( + some_schema=schema, + some_instance=instance, + ), + argv=[ + "-i", "some_instance", + "--base-uri", Path.cwd().as_uri(), + "some_schema", + ], + ) + error = str(e.exception) + self.assertIn(f"{os.sep}someNonexistentFile.json'", error) + + def test_invalid_explicit_base_uri(self): + schema = '{"$ref": "foo.json#definitions/num"}' + instance = "1" + + with self.assertRaises(_RefResolutionError) as e: + self.assertOutputs( + files=dict( + some_schema=schema, + some_instance=instance, + ), + argv=[ + "-i", "some_instance", + "--base-uri", "not@UR1", + "some_schema", + ], + ) + error = str(e.exception) + self.assertEqual( + error, "unknown url type: 'foo.json'", + ) + + def test_it_validates_using_the_latest_validator_when_unspecified(self): + # There isn't a better way now I can think of to ensure that the + # latest version was used, given that the call to validator_for + # is hidden inside the CLI, so guard that that's the case, and + # this test will have to be updated when versions change until + # we can think of a better way to ensure this behavior. + self.assertIs(Draft202012Validator, _LATEST_VERSION) + + self.assertOutputs( + files=dict(some_schema='{"const": "check"}', some_instance='"a"'), + argv=["-i", "some_instance", "some_schema"], + exit_code=1, + stdout="", + stderr="a: 'check' was expected\n", + ) + + def test_it_validates_using_draft7_when_specified(self): + """ + Specifically, `const` validation applies for Draft 7. + """ + schema = """ + { + "$schema": "http://json-schema.org/draft-07/schema#", + "const": "check" + } + """ + instance = '"foo"' + self.assertOutputs( + files=dict(some_schema=schema, some_instance=instance), + argv=["-i", "some_instance", "some_schema"], + exit_code=1, + stdout="", + stderr="foo: 'check' was expected\n", + ) + + def test_it_validates_using_draft4_when_specified(self): + """ + Specifically, `const` validation *does not* apply for Draft 4. + """ + schema = """ + { + "$schema": "http://json-schema.org/draft-04/schema#", + "const": "check" + } + """ + instance = '"foo"' + self.assertOutputs( + files=dict(some_schema=schema, some_instance=instance), + argv=["-i", "some_instance", "some_schema"], + stdout="", + stderr="", + ) + + +class TestParser(TestCase): + + FakeValidator = fake_validator() + + def test_find_validator_by_fully_qualified_object_name(self): + arguments = cli.parse_args( + [ + "--validator", + "jsonschema.tests.test_cli.TestParser.FakeValidator", + "--instance", "mem://some/instance", + "mem://some/schema", + ], + ) + self.assertIs(arguments["validator"], self.FakeValidator) + + def test_find_validator_in_jsonschema(self): + arguments = cli.parse_args( + [ + "--validator", "Draft4Validator", + "--instance", "mem://some/instance", + "mem://some/schema", + ], + ) + self.assertIs(arguments["validator"], Draft4Validator) + + def cli_output_for(self, *argv): + stdout, stderr = StringIO(), StringIO() + with redirect_stdout(stdout), redirect_stderr(stderr): # noqa: SIM117 + with self.assertRaises(SystemExit): + cli.parse_args(argv) + return stdout.getvalue(), stderr.getvalue() + + def test_unknown_output(self): + stdout, stderr = self.cli_output_for( + "--output", "foo", + "mem://some/schema", + ) + self.assertIn("invalid choice: 'foo'", stderr) + self.assertFalse(stdout) + + def test_useless_error_format(self): + stdout, stderr = self.cli_output_for( + "--output", "pretty", + "--error-format", "foo", + "mem://some/schema", + ) + self.assertIn( + "--error-format can only be used with --output plain", + stderr, + ) + self.assertFalse(stdout) + + +class TestCLIIntegration(TestCase): + def test_license(self): + our_metadata = metadata.metadata("jsonschema") + self.assertEqual(our_metadata.get("License-Expression"), "MIT") + + def test_version(self): + version = subprocess.check_output( + [sys.executable, "-W", "ignore", "-m", "jsonschema", "--version"], + stderr=subprocess.STDOUT, + ) + version = version.decode("utf-8").strip() + self.assertEqual(version, metadata.version("jsonschema")) + + def test_no_arguments_shows_usage_notes(self): + output = subprocess.check_output( + [sys.executable, "-m", "jsonschema"], + stderr=subprocess.STDOUT, + ) + output_for_help = subprocess.check_output( + [sys.executable, "-m", "jsonschema", "--help"], + stderr=subprocess.STDOUT, + ) + self.assertEqual(output, output_for_help) diff --git a/source/target-vendors/cp312/jsonschema/tests/test_deprecations.py b/source/target-vendors/cp312/jsonschema/tests/test_deprecations.py new file mode 100644 index 0000000..a54b02f --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/tests/test_deprecations.py @@ -0,0 +1,432 @@ +from contextlib import contextmanager +from io import BytesIO +from unittest import TestCase, mock +import importlib.metadata +import json +import subprocess +import sys +import urllib.request + +import referencing.exceptions + +from jsonschema import FormatChecker, exceptions, protocols, validators + + +class TestDeprecations(TestCase): + def test_version(self): + """ + As of v4.0.0, __version__ is deprecated in favor of importlib.metadata. + """ + + message = "Accessing jsonschema.__version__ is deprecated" + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import __version__ + + self.assertEqual(__version__, importlib.metadata.version("jsonschema")) + self.assertEqual(w.filename, __file__) + + def test_validators_ErrorTree(self): + """ + As of v4.0.0, importing ErrorTree from jsonschema.validators is + deprecated in favor of doing so from jsonschema.exceptions. + """ + + message = "Importing ErrorTree from jsonschema.validators is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema.validators import ErrorTree + + self.assertEqual(ErrorTree, exceptions.ErrorTree) + self.assertEqual(w.filename, __file__) + + def test_import_ErrorTree(self): + """ + As of v4.18.0, importing ErrorTree from the package root is + deprecated in favor of doing so from jsonschema.exceptions. + """ + + message = "Importing ErrorTree directly from the jsonschema package " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import ErrorTree + + self.assertEqual(ErrorTree, exceptions.ErrorTree) + self.assertEqual(w.filename, __file__) + + def test_ErrorTree_setitem(self): + """ + As of v4.20.0, setting items on an ErrorTree is deprecated. + """ + + e = exceptions.ValidationError("some error", path=["foo"]) + tree = exceptions.ErrorTree() + subtree = exceptions.ErrorTree(errors=[e]) + + message = "ErrorTree.__setitem__ is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + tree["foo"] = subtree + + self.assertEqual(tree["foo"], subtree) + self.assertEqual(w.filename, __file__) + + def test_import_FormatError(self): + """ + As of v4.18.0, importing FormatError from the package root is + deprecated in favor of doing so from jsonschema.exceptions. + """ + + message = "Importing FormatError directly from the jsonschema package " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import FormatError + + self.assertEqual(FormatError, exceptions.FormatError) + self.assertEqual(w.filename, __file__) + + def test_import_Validator(self): + """ + As of v4.19.0, importing Validator from the package root is + deprecated in favor of doing so from jsonschema.protocols. + """ + + message = "Importing Validator directly from the jsonschema package " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import Validator + + self.assertEqual(Validator, protocols.Validator) + self.assertEqual(w.filename, __file__) + + def test_validators_validators(self): + """ + As of v4.0.0, accessing jsonschema.validators.validators is + deprecated. + """ + + message = "Accessing jsonschema.validators.validators is deprecated" + with self.assertWarnsRegex(DeprecationWarning, message) as w: + value = validators.validators + + self.assertEqual(value, validators._VALIDATORS) + self.assertEqual(w.filename, __file__) + + def test_validators_meta_schemas(self): + """ + As of v4.0.0, accessing jsonschema.validators.meta_schemas is + deprecated. + """ + + message = "Accessing jsonschema.validators.meta_schemas is deprecated" + with self.assertWarnsRegex(DeprecationWarning, message) as w: + value = validators.meta_schemas + + self.assertEqual(value, validators._META_SCHEMAS) + self.assertEqual(w.filename, __file__) + + def test_RefResolver_in_scope(self): + """ + As of v4.0.0, RefResolver.in_scope is deprecated. + """ + + resolver = validators._RefResolver.from_schema({}) + message = "jsonschema.RefResolver.in_scope is deprecated " + with self.assertWarnsRegex(DeprecationWarning, message) as w: # noqa: SIM117 + with resolver.in_scope("foo"): + pass + + self.assertEqual(w.filename, __file__) + + def test_Validator_is_valid_two_arguments(self): + """ + As of v4.0.0, calling is_valid with two arguments (to provide a + different schema) is deprecated. + """ + + validator = validators.Draft7Validator({}) + message = "Passing a schema to Validator.is_valid is deprecated " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + result = validator.is_valid("foo", {"type": "number"}) + + self.assertFalse(result) + self.assertEqual(w.filename, __file__) + + def test_Validator_iter_errors_two_arguments(self): + """ + As of v4.0.0, calling iter_errors with two arguments (to provide a + different schema) is deprecated. + """ + + validator = validators.Draft7Validator({}) + message = "Passing a schema to Validator.iter_errors is deprecated " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + error, = validator.iter_errors("foo", {"type": "number"}) + + self.assertEqual(error.validator, "type") + self.assertEqual(w.filename, __file__) + + def test_Validator_resolver(self): + """ + As of v4.18.0, accessing Validator.resolver is deprecated. + """ + + validator = validators.Draft7Validator({}) + message = "Accessing Draft7Validator.resolver is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + self.assertIsInstance(validator.resolver, validators._RefResolver) + + self.assertEqual(w.filename, __file__) + + def test_RefResolver(self): + """ + As of v4.18.0, RefResolver is fully deprecated. + """ + + message = "jsonschema.RefResolver is deprecated" + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import RefResolver + self.assertEqual(w.filename, __file__) + + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema.validators import RefResolver # noqa: F401 + self.assertEqual(w.filename, __file__) + + def test_RefResolutionError(self): + """ + As of v4.18.0, RefResolutionError is deprecated in favor of directly + catching errors from the referencing library. + """ + + message = "jsonschema.exceptions.RefResolutionError is deprecated" + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import RefResolutionError + + self.assertEqual(RefResolutionError, exceptions._RefResolutionError) + self.assertEqual(w.filename, __file__) + + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema.exceptions import RefResolutionError + + self.assertEqual(RefResolutionError, exceptions._RefResolutionError) + self.assertEqual(w.filename, __file__) + + def test_catching_Unresolvable_directly(self): + """ + This behavior is the intended behavior (i.e. it's not deprecated), but + given we do "tricksy" things in the iterim to wrap exceptions in a + multiple inheritance subclass, we need to be extra sure it works and + stays working. + """ + validator = validators.Draft202012Validator({"$ref": "urn:nothing"}) + + with self.assertRaises(referencing.exceptions.Unresolvable) as e: + validator.validate(12) + + expected = referencing.exceptions.Unresolvable(ref="urn:nothing") + self.assertEqual( + (e.exception, str(e.exception)), + (expected, "Unresolvable: urn:nothing"), + ) + + def test_catching_Unresolvable_via_RefResolutionError(self): + """ + Until RefResolutionError is removed, it is still possible to catch + exceptions from reference resolution using it, even though they may + have been raised by referencing. + """ + with self.assertWarns(DeprecationWarning): + from jsonschema import RefResolutionError + + validator = validators.Draft202012Validator({"$ref": "urn:nothing"}) + + with self.assertRaises(referencing.exceptions.Unresolvable) as u: + validator.validate(12) + + with self.assertRaises(RefResolutionError) as e: + validator.validate(12) + + self.assertEqual( + (e.exception, str(e.exception)), + (u.exception, "Unresolvable: urn:nothing"), + ) + + def test_WrappedReferencingError_hashability(self): + """ + Ensure the wrapped referencing errors are hashable when possible. + """ + with self.assertWarns(DeprecationWarning): + from jsonschema import RefResolutionError + + validator = validators.Draft202012Validator({"$ref": "urn:nothing"}) + + with self.assertRaises(referencing.exceptions.Unresolvable) as u: + validator.validate(12) + + with self.assertRaises(RefResolutionError) as e: + validator.validate(12) + + self.assertIn(e.exception, {u.exception}) + self.assertIn(u.exception, {e.exception}) + + def test_Validator_subclassing(self): + """ + As of v4.12.0, subclassing a validator class produces an explicit + deprecation warning. + + This was never intended to be public API (and some comments over the + years in issues said so, but obviously that's not a great way to make + sure it's followed). + + A future version will explicitly raise an error. + """ + + message = "Subclassing validator classes is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + class Subclass(validators.Draft202012Validator): + pass + + self.assertEqual(w.filename, __file__) + + with self.assertWarnsRegex(DeprecationWarning, message) as w: + class AnotherSubclass(validators.create(meta_schema={})): + pass + + def test_FormatChecker_cls_checks(self): + """ + As of v4.14.0, FormatChecker.cls_checks is deprecated without + replacement. + """ + + self.addCleanup(FormatChecker.checkers.pop, "boom", None) + + message = "FormatChecker.cls_checks " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + FormatChecker.cls_checks("boom") + + self.assertEqual(w.filename, __file__) + + def test_draftN_format_checker(self): + """ + As of v4.16.0, accessing jsonschema.draftn_format_checker is deprecated + in favor of Validator.FORMAT_CHECKER. + """ + + message = "Accessing jsonschema.draft202012_format_checker is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import draft202012_format_checker + + self.assertIs( + draft202012_format_checker, + validators.Draft202012Validator.FORMAT_CHECKER, + ) + self.assertEqual(w.filename, __file__) + + message = "Accessing jsonschema.draft201909_format_checker is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import draft201909_format_checker + + self.assertIs( + draft201909_format_checker, + validators.Draft201909Validator.FORMAT_CHECKER, + ) + self.assertEqual(w.filename, __file__) + + message = "Accessing jsonschema.draft7_format_checker is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import draft7_format_checker + + self.assertIs( + draft7_format_checker, + validators.Draft7Validator.FORMAT_CHECKER, + ) + self.assertEqual(w.filename, __file__) + + message = "Accessing jsonschema.draft6_format_checker is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import draft6_format_checker + + self.assertIs( + draft6_format_checker, + validators.Draft6Validator.FORMAT_CHECKER, + ) + self.assertEqual(w.filename, __file__) + + message = "Accessing jsonschema.draft4_format_checker is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import draft4_format_checker + + self.assertIs( + draft4_format_checker, + validators.Draft4Validator.FORMAT_CHECKER, + ) + self.assertEqual(w.filename, __file__) + + message = "Accessing jsonschema.draft3_format_checker is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import draft3_format_checker + + self.assertIs( + draft3_format_checker, + validators.Draft3Validator.FORMAT_CHECKER, + ) + self.assertEqual(w.filename, __file__) + + with self.assertRaises(ImportError): + from jsonschema import draft1234_format_checker # noqa: F401 + + def test_import_cli(self): + """ + As of v4.17.0, importing jsonschema.cli is deprecated. + """ + + message = "The jsonschema CLI is deprecated and will be removed " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + import jsonschema.cli + importlib.reload(jsonschema.cli) + + self.assertEqual(w.filename, importlib.__file__) + + def test_cli(self): + """ + As of v4.17.0, the jsonschema CLI is deprecated. + """ + + process = subprocess.run( + [sys.executable, "-m", "jsonschema"], + capture_output=True, + check=True, + ) + self.assertIn(b"The jsonschema CLI is deprecated ", process.stderr) + + def test_automatic_remote_retrieval(self): + """ + Automatic retrieval of remote references is deprecated as of v4.18.0. + """ + ref = "http://bar#/$defs/baz" + schema = {"$defs": {"baz": {"type": "integer"}}} + + if "requests" in sys.modules: # pragma: no cover + self.addCleanup( + sys.modules.__setitem__, "requests", sys.modules["requests"], + ) + sys.modules["requests"] = None + + @contextmanager + def fake_urlopen(request): + self.assertIsInstance(request, urllib.request.Request) + self.assertEqual(request.full_url, "http://bar") + + # Ha ha urllib.request.Request "normalizes" header names and + # Request.get_header does not also normalize them... + (header, value), = request.header_items() + self.assertEqual(header.lower(), "user-agent") + self.assertEqual( + value, "python-jsonschema (deprecated $ref resolution)", + ) + yield BytesIO(json.dumps(schema).encode("utf8")) + + validator = validators.Draft202012Validator({"$ref": ref}) + + message = "Automatically retrieving remote references " + patch = mock.patch.object(urllib.request, "urlopen", new=fake_urlopen) + + with patch, self.assertWarnsRegex(DeprecationWarning, message): + self.assertEqual( + (validator.is_valid({}), validator.is_valid(37)), + (False, True), + ) diff --git a/source/target-vendors/cp312/jsonschema/tests/test_exceptions.py b/source/target-vendors/cp312/jsonschema/tests/test_exceptions.py new file mode 100644 index 0000000..358b924 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/tests/test_exceptions.py @@ -0,0 +1,759 @@ +from unittest import TestCase +import textwrap + +import jsonpath_ng + +from jsonschema import exceptions +from jsonschema.validators import _LATEST_VERSION + + +class TestBestMatch(TestCase): + def best_match_of(self, instance, schema): + errors = list(_LATEST_VERSION(schema).iter_errors(instance)) + msg = f"No errors found for {instance} under {schema!r}!" + self.assertTrue(errors, msg=msg) + + best = exceptions.best_match(iter(errors)) + reversed_best = exceptions.best_match(reversed(errors)) + + self.assertEqual( + best._contents(), + reversed_best._contents(), + f"No consistent best match!\nGot: {best}\n\nThen: {reversed_best}", + ) + return best + + def test_shallower_errors_are_better_matches(self): + schema = { + "properties": { + "foo": { + "minProperties": 2, + "properties": {"bar": {"type": "object"}}, + }, + }, + } + best = self.best_match_of(instance={"foo": {"bar": []}}, schema=schema) + self.assertEqual(best.validator, "minProperties") + + def test_oneOf_and_anyOf_are_weak_matches(self): + """ + A property you *must* match is probably better than one you have to + match a part of. + """ + + schema = { + "minProperties": 2, + "anyOf": [{"type": "string"}, {"type": "number"}], + "oneOf": [{"type": "string"}, {"type": "number"}], + } + best = self.best_match_of(instance={}, schema=schema) + self.assertEqual(best.validator, "minProperties") + + def test_if_the_most_relevant_error_is_anyOf_it_is_traversed(self): + """ + If the most relevant error is an anyOf, then we traverse its context + and select the otherwise *least* relevant error, since in this case + that means the most specific, deep, error inside the instance. + + I.e. since only one of the schemas must match, we look for the most + relevant one. + """ + + schema = { + "properties": { + "foo": { + "anyOf": [ + {"type": "string"}, + {"properties": {"bar": {"type": "array"}}}, + ], + }, + }, + } + best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema) + self.assertEqual(best.validator_value, "array") + + def test_no_anyOf_traversal_for_equally_relevant_errors(self): + """ + We don't traverse into an anyOf (as above) if all of its context errors + seem to be equally "wrong" against the instance. + """ + + schema = { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + {"type": "object"}, + ], + } + best = self.best_match_of(instance=[], schema=schema) + self.assertEqual(best.validator, "anyOf") + + def test_anyOf_traversal_for_single_equally_relevant_error(self): + """ + We *do* traverse anyOf with a single nested error, even though it is + vacuously equally relevant to itself. + """ + + schema = { + "anyOf": [ + {"type": "string"}, + ], + } + best = self.best_match_of(instance=[], schema=schema) + self.assertEqual(best.validator, "type") + + def test_anyOf_traversal_for_single_sibling_errors(self): + """ + We *do* traverse anyOf with a single subschema that fails multiple + times (e.g. on multiple items). + """ + + schema = { + "anyOf": [ + {"items": {"const": 37}}, + ], + } + best = self.best_match_of(instance=[12, 12], schema=schema) + self.assertEqual(best.validator, "const") + + def test_anyOf_traversal_for_non_type_matching_sibling_errors(self): + """ + We *do* traverse anyOf with multiple subschemas when one does not type + match. + """ + + schema = { + "anyOf": [ + {"type": "object"}, + {"items": {"const": 37}}, + ], + } + best = self.best_match_of(instance=[12, 12], schema=schema) + self.assertEqual(best.validator, "const") + + def test_if_the_most_relevant_error_is_oneOf_it_is_traversed(self): + """ + If the most relevant error is an oneOf, then we traverse its context + and select the otherwise *least* relevant error, since in this case + that means the most specific, deep, error inside the instance. + + I.e. since only one of the schemas must match, we look for the most + relevant one. + """ + + schema = { + "properties": { + "foo": { + "oneOf": [ + {"type": "string"}, + {"properties": {"bar": {"type": "array"}}}, + ], + }, + }, + } + best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema) + self.assertEqual(best.validator_value, "array") + + def test_no_oneOf_traversal_for_equally_relevant_errors(self): + """ + We don't traverse into an oneOf (as above) if all of its context errors + seem to be equally "wrong" against the instance. + """ + + schema = { + "oneOf": [ + {"type": "string"}, + {"type": "integer"}, + {"type": "object"}, + ], + } + best = self.best_match_of(instance=[], schema=schema) + self.assertEqual(best.validator, "oneOf") + + def test_oneOf_traversal_for_single_equally_relevant_error(self): + """ + We *do* traverse oneOf with a single nested error, even though it is + vacuously equally relevant to itself. + """ + + schema = { + "oneOf": [ + {"type": "string"}, + ], + } + best = self.best_match_of(instance=[], schema=schema) + self.assertEqual(best.validator, "type") + + def test_oneOf_traversal_for_single_sibling_errors(self): + """ + We *do* traverse oneOf with a single subschema that fails multiple + times (e.g. on multiple items). + """ + + schema = { + "oneOf": [ + {"items": {"const": 37}}, + ], + } + best = self.best_match_of(instance=[12, 12], schema=schema) + self.assertEqual(best.validator, "const") + + def test_oneOf_traversal_for_non_type_matching_sibling_errors(self): + """ + We *do* traverse oneOf with multiple subschemas when one does not type + match. + """ + + schema = { + "oneOf": [ + {"type": "object"}, + {"items": {"const": 37}}, + ], + } + best = self.best_match_of(instance=[12, 12], schema=schema) + self.assertEqual(best.validator, "const") + + def test_if_the_most_relevant_error_is_allOf_it_is_traversed(self): + """ + Now, if the error is allOf, we traverse but select the *most* relevant + error from the context, because all schemas here must match anyways. + """ + + schema = { + "properties": { + "foo": { + "allOf": [ + {"type": "string"}, + {"properties": {"bar": {"type": "array"}}}, + ], + }, + }, + } + best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema) + self.assertEqual(best.validator_value, "string") + + def test_nested_context_for_oneOf(self): + """ + We traverse into nested contexts (a oneOf containing an error in a + nested oneOf here). + """ + + schema = { + "properties": { + "foo": { + "oneOf": [ + {"type": "string"}, + { + "oneOf": [ + {"type": "string"}, + { + "properties": { + "bar": {"type": "array"}, + }, + }, + ], + }, + ], + }, + }, + } + best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema) + self.assertEqual(best.validator_value, "array") + + def test_it_prioritizes_matching_types(self): + schema = { + "properties": { + "foo": { + "anyOf": [ + {"type": "array", "minItems": 2}, + {"type": "string", "minLength": 10}, + ], + }, + }, + } + best = self.best_match_of(instance={"foo": "bar"}, schema=schema) + self.assertEqual(best.validator, "minLength") + + reordered = { + "properties": { + "foo": { + "anyOf": [ + {"type": "string", "minLength": 10}, + {"type": "array", "minItems": 2}, + ], + }, + }, + } + best = self.best_match_of(instance={"foo": "bar"}, schema=reordered) + self.assertEqual(best.validator, "minLength") + + def test_it_prioritizes_matching_union_types(self): + schema = { + "properties": { + "foo": { + "anyOf": [ + {"type": ["array", "object"], "minItems": 2}, + {"type": ["integer", "string"], "minLength": 10}, + ], + }, + }, + } + best = self.best_match_of(instance={"foo": "bar"}, schema=schema) + self.assertEqual(best.validator, "minLength") + + reordered = { + "properties": { + "foo": { + "anyOf": [ + {"type": "string", "minLength": 10}, + {"type": "array", "minItems": 2}, + ], + }, + }, + } + best = self.best_match_of(instance={"foo": "bar"}, schema=reordered) + self.assertEqual(best.validator, "minLength") + + def test_boolean_schemas(self): + schema = {"properties": {"foo": False}} + best = self.best_match_of(instance={"foo": "bar"}, schema=schema) + self.assertIsNone(best.validator) + + def test_one_error(self): + validator = _LATEST_VERSION({"minProperties": 2}) + validator.iter_errors({}) + self.assertEqual( + exceptions.best_match(validator.iter_errors({})).validator, + "minProperties", + ) + + def test_no_errors(self): + validator = _LATEST_VERSION({}) + self.assertIsNone(exceptions.best_match(validator.iter_errors({}))) + + +class TestByRelevance(TestCase): + def test_short_paths_are_better_matches(self): + shallow = exceptions.ValidationError("Oh no!", path=["baz"]) + deep = exceptions.ValidationError("Oh yes!", path=["foo", "bar"]) + match = max([shallow, deep], key=exceptions.relevance) + self.assertIs(match, shallow) + + match = max([deep, shallow], key=exceptions.relevance) + self.assertIs(match, shallow) + + def test_global_errors_are_even_better_matches(self): + shallow = exceptions.ValidationError("Oh no!", path=[]) + deep = exceptions.ValidationError("Oh yes!", path=["foo"]) + + errors = sorted([shallow, deep], key=exceptions.relevance) + self.assertEqual( + [list(error.path) for error in errors], + [["foo"], []], + ) + + errors = sorted([deep, shallow], key=exceptions.relevance) + self.assertEqual( + [list(error.path) for error in errors], + [["foo"], []], + ) + + def test_weak_keywords_are_lower_priority(self): + weak = exceptions.ValidationError("Oh no!", path=[], validator="a") + normal = exceptions.ValidationError("Oh yes!", path=[], validator="b") + + best_match = exceptions.by_relevance(weak="a") + + match = max([weak, normal], key=best_match) + self.assertIs(match, normal) + + match = max([normal, weak], key=best_match) + self.assertIs(match, normal) + + def test_strong_keywords_are_higher_priority(self): + weak = exceptions.ValidationError("Oh no!", path=[], validator="a") + normal = exceptions.ValidationError("Oh yes!", path=[], validator="b") + strong = exceptions.ValidationError("Oh fine!", path=[], validator="c") + + best_match = exceptions.by_relevance(weak="a", strong="c") + + match = max([weak, normal, strong], key=best_match) + self.assertIs(match, strong) + + match = max([strong, normal, weak], key=best_match) + self.assertIs(match, strong) + + +class TestErrorTree(TestCase): + def test_it_knows_how_many_total_errors_it_contains(self): + # FIXME: #442 + errors = [ + exceptions.ValidationError("Something", validator=i) + for i in range(8) + ] + tree = exceptions.ErrorTree(errors) + self.assertEqual(tree.total_errors, 8) + + def test_it_contains_an_item_if_the_item_had_an_error(self): + errors = [exceptions.ValidationError("a message", path=["bar"])] + tree = exceptions.ErrorTree(errors) + self.assertIn("bar", tree) + + def test_it_does_not_contain_an_item_if_the_item_had_no_error(self): + errors = [exceptions.ValidationError("a message", path=["bar"])] + tree = exceptions.ErrorTree(errors) + self.assertNotIn("foo", tree) + + def test_keywords_that_failed_appear_in_errors_dict(self): + error = exceptions.ValidationError("a message", validator="foo") + tree = exceptions.ErrorTree([error]) + self.assertEqual(tree.errors, {"foo": error}) + + def test_it_creates_a_child_tree_for_each_nested_path(self): + errors = [ + exceptions.ValidationError("a bar message", path=["bar"]), + exceptions.ValidationError("a bar -> 0 message", path=["bar", 0]), + ] + tree = exceptions.ErrorTree(errors) + self.assertIn(0, tree["bar"]) + self.assertNotIn(1, tree["bar"]) + + def test_children_have_their_errors_dicts_built(self): + e1, e2 = ( + exceptions.ValidationError("1", validator="foo", path=["bar", 0]), + exceptions.ValidationError("2", validator="quux", path=["bar", 0]), + ) + tree = exceptions.ErrorTree([e1, e2]) + self.assertEqual(tree["bar"][0].errors, {"foo": e1, "quux": e2}) + + def test_multiple_errors_with_instance(self): + e1, e2 = ( + exceptions.ValidationError( + "1", + validator="foo", + path=["bar", "bar2"], + instance="i1"), + exceptions.ValidationError( + "2", + validator="quux", + path=["foobar", 2], + instance="i2"), + ) + exceptions.ErrorTree([e1, e2]) + + def test_it_does_not_contain_subtrees_that_are_not_in_the_instance(self): + error = exceptions.ValidationError("123", validator="foo", instance=[]) + tree = exceptions.ErrorTree([error]) + + with self.assertRaises(IndexError): + tree[0] + + def test_if_its_in_the_tree_anyhow_it_does_not_raise_an_error(self): + """ + If a keyword refers to a path that isn't in the instance, the + tree still properly returns a subtree for that path. + """ + + error = exceptions.ValidationError( + "a message", validator="foo", instance={}, path=["foo"], + ) + tree = exceptions.ErrorTree([error]) + self.assertIsInstance(tree["foo"], exceptions.ErrorTree) + + def test_iter(self): + e1, e2 = ( + exceptions.ValidationError( + "1", + validator="foo", + path=["bar", "bar2"], + instance="i1"), + exceptions.ValidationError( + "2", + validator="quux", + path=["foobar", 2], + instance="i2"), + ) + tree = exceptions.ErrorTree([e1, e2]) + self.assertEqual(set(tree), {"bar", "foobar"}) + + def test_repr_single(self): + error = exceptions.ValidationError( + "1", + validator="foo", + path=["bar", "bar2"], + instance="i1", + ) + tree = exceptions.ErrorTree([error]) + self.assertEqual(repr(tree), "") + + def test_repr_multiple(self): + e1, e2 = ( + exceptions.ValidationError( + "1", + validator="foo", + path=["bar", "bar2"], + instance="i1"), + exceptions.ValidationError( + "2", + validator="quux", + path=["foobar", 2], + instance="i2"), + ) + tree = exceptions.ErrorTree([e1, e2]) + self.assertEqual(repr(tree), "") + + def test_repr_empty(self): + tree = exceptions.ErrorTree([]) + self.assertEqual(repr(tree), "") + + +class TestErrorInitReprStr(TestCase): + def make_error(self, **kwargs): + defaults = dict( + message="hello", + validator="type", + validator_value="string", + instance=5, + schema={"type": "string"}, + ) + defaults.update(kwargs) + return exceptions.ValidationError(**defaults) + + def assertShows(self, expected, **kwargs): + expected = textwrap.dedent(expected).rstrip("\n") + + error = self.make_error(**kwargs) + message_line, _, rest = str(error).partition("\n") + self.assertEqual(message_line, error.message) + self.assertEqual(rest, expected) + + def test_it_calls_super_and_sets_args(self): + error = self.make_error() + self.assertGreater(len(error.args), 1) + + def test_repr(self): + self.assertEqual( + repr(exceptions.ValidationError(message="Hello!")), + "", + ) + + def test_unset_error(self): + error = exceptions.ValidationError("message") + self.assertEqual(str(error), "message") + + kwargs = { + "validator": "type", + "validator_value": "string", + "instance": 5, + "schema": {"type": "string"}, + } + # Just the message should show if any of the attributes are unset + for attr in kwargs: + k = dict(kwargs) + del k[attr] + error = exceptions.ValidationError("message", **k) + self.assertEqual(str(error), "message") + + def test_empty_paths(self): + self.assertShows( + """ + Failed validating 'type' in schema: + {'type': 'string'} + + On instance: + 5 + """, + path=[], + schema_path=[], + ) + + def test_one_item_paths(self): + self.assertShows( + """ + Failed validating 'type' in schema: + {'type': 'string'} + + On instance[0]: + 5 + """, + path=[0], + schema_path=["items"], + ) + + def test_multiple_item_paths(self): + self.assertShows( + """ + Failed validating 'type' in schema['items'][0]: + {'type': 'string'} + + On instance[0]['a']: + 5 + """, + path=[0, "a"], + schema_path=["items", 0, 1], + ) + + def test_uses_pprint(self): + self.assertShows( + """ + Failed validating 'maxLength' in schema: + {0: 0, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + 6: 6, + 7: 7, + 8: 8, + 9: 9, + 10: 10, + 11: 11, + 12: 12, + 13: 13, + 14: 14, + 15: 15, + 16: 16, + 17: 17, + 18: 18, + 19: 19} + + On instance: + [0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24] + """, + instance=list(range(25)), + schema=dict(zip(range(20), range(20))), + validator="maxLength", + ) + + def test_does_not_reorder_dicts(self): + self.assertShows( + """ + Failed validating 'type' in schema: + {'do': 3, 'not': 7, 'sort': 37, 'me': 73} + + On instance: + {'here': 73, 'too': 37, 'no': 7, 'sorting': 3} + """, + schema={ + "do": 3, + "not": 7, + "sort": 37, + "me": 73, + }, + instance={ + "here": 73, + "too": 37, + "no": 7, + "sorting": 3, + }, + ) + + def test_str_works_with_instances_having_overriden_eq_operator(self): + """ + Check for #164 which rendered exceptions unusable when a + `ValidationError` involved instances with an `__eq__` method + that returned truthy values. + """ + + class DontEQMeBro: + def __eq__(this, other): # pragma: no cover + self.fail("Don't!") + + def __ne__(this, other): # pragma: no cover + self.fail("Don't!") + + instance = DontEQMeBro() + error = exceptions.ValidationError( + "a message", + validator="foo", + instance=instance, + validator_value="some", + schema="schema", + ) + self.assertIn(repr(instance), str(error)) + + +class TestHashable(TestCase): + def test_hashable(self): + {exceptions.ValidationError("")} + {exceptions.SchemaError("")} + + +class TestJsonPathRendering(TestCase): + def validate_json_path_rendering(self, property_name, expected_path): + error = exceptions.ValidationError( + path=[property_name], + message="1", + validator="foo", + instance="i1", + ) + + rendered_json_path = error.json_path + self.assertEqual(rendered_json_path, expected_path) + + re_parsed_name = jsonpath_ng.parse(rendered_json_path).right.fields[0] + self.assertEqual(re_parsed_name, property_name) + + def test_basic(self): + self.validate_json_path_rendering("x", "$.x") + + def test_empty(self): + self.validate_json_path_rendering("", "$['']") + + def test_number(self): + self.validate_json_path_rendering("1", "$['1']") + + def test_period(self): + self.validate_json_path_rendering(".", "$['.']") + + def test_single_quote(self): + self.validate_json_path_rendering("'", r"$['\'']") + + def test_space(self): + self.validate_json_path_rendering(" ", "$[' ']") + + def test_backslash(self): + self.validate_json_path_rendering("\\", r"$['\\']") + + def test_backslash_single_quote(self): + self.validate_json_path_rendering(r"\'", r"$['\\\'']") + + def test_underscore(self): + self.validate_json_path_rendering("_", r"$['_']") + + def test_double_quote(self): + self.validate_json_path_rendering('"', """$['"']""") + + def test_hyphen(self): + self.validate_json_path_rendering("-", "$['-']") + + def test_json_path_injection(self): + self.validate_json_path_rendering("a[0]", "$['a[0]']") + + def test_open_bracket(self): + self.validate_json_path_rendering("[", "$['[']") diff --git a/source/target-vendors/cp312/jsonschema/tests/test_format.py b/source/target-vendors/cp312/jsonschema/tests/test_format.py new file mode 100644 index 0000000..d829f98 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/tests/test_format.py @@ -0,0 +1,91 @@ +""" +Tests for the parts of jsonschema related to the :kw:`format` keyword. +""" + +from unittest import TestCase + +from jsonschema import FormatChecker, ValidationError +from jsonschema.exceptions import FormatError +from jsonschema.validators import Draft4Validator + +BOOM = ValueError("Boom!") +BANG = ZeroDivisionError("Bang!") + + +def boom(thing): + if thing == "bang": + raise BANG + raise BOOM + + +class TestFormatChecker(TestCase): + def test_it_can_validate_no_formats(self): + checker = FormatChecker(formats=()) + self.assertFalse(checker.checkers) + + def test_it_raises_a_key_error_for_unknown_formats(self): + with self.assertRaises(KeyError): + FormatChecker(formats=["o noes"]) + + def test_it_can_register_cls_checkers(self): + original = dict(FormatChecker.checkers) + self.addCleanup(FormatChecker.checkers.pop, "boom") + with self.assertWarns(DeprecationWarning): + FormatChecker.cls_checks("boom")(boom) + self.assertEqual( + FormatChecker.checkers, + dict(original, boom=(boom, ())), + ) + + def test_it_can_register_checkers(self): + checker = FormatChecker() + checker.checks("boom")(boom) + self.assertEqual( + checker.checkers, + dict(FormatChecker.checkers, boom=(boom, ())), + ) + + def test_it_catches_registered_errors(self): + checker = FormatChecker() + checker.checks("boom", raises=type(BOOM))(boom) + + with self.assertRaises(FormatError) as cm: + checker.check(instance=12, format="boom") + + self.assertIs(cm.exception.cause, BOOM) + self.assertIs(cm.exception.__cause__, BOOM) + self.assertEqual(str(cm.exception), "12 is not a 'boom'") + + # Unregistered errors should not be caught + with self.assertRaises(type(BANG)): + checker.check(instance="bang", format="boom") + + def test_format_error_causes_become_validation_error_causes(self): + checker = FormatChecker() + checker.checks("boom", raises=ValueError)(boom) + validator = Draft4Validator({"format": "boom"}, format_checker=checker) + + with self.assertRaises(ValidationError) as cm: + validator.validate("BOOM") + + self.assertIs(cm.exception.cause, BOOM) + self.assertIs(cm.exception.__cause__, BOOM) + + def test_format_checkers_come_with_defaults(self): + # This is bad :/ but relied upon. + # The docs for quite awhile recommended people do things like + # validate(..., format_checker=FormatChecker()) + # We should change that, but we can't without deprecation... + checker = FormatChecker() + with self.assertRaises(FormatError): + checker.check(instance="not-an-ipv4", format="ipv4") + + def test_repr(self): + checker = FormatChecker(formats=()) + checker.checks("foo")(lambda thing: True) # pragma: no cover + checker.checks("bar")(lambda thing: True) # pragma: no cover + checker.checks("baz")(lambda thing: True) # pragma: no cover + self.assertEqual( + repr(checker), + "", + ) diff --git a/source/target-vendors/cp312/jsonschema/tests/test_jsonschema_test_suite.py b/source/target-vendors/cp312/jsonschema/tests/test_jsonschema_test_suite.py new file mode 100644 index 0000000..41c9825 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/tests/test_jsonschema_test_suite.py @@ -0,0 +1,262 @@ +""" +Test runner for the JSON Schema official test suite + +Tests comprehensive correctness of each draft's validator. + +See https://github.com/json-schema-org/JSON-Schema-Test-Suite for details. +""" + + +from jsonschema.tests._suite import Suite +import jsonschema + +SUITE = Suite() +DRAFT3 = SUITE.version(name="draft3") +DRAFT4 = SUITE.version(name="draft4") +DRAFT6 = SUITE.version(name="draft6") +DRAFT7 = SUITE.version(name="draft7") +DRAFT201909 = SUITE.version(name="draft2019-09") +DRAFT202012 = SUITE.version(name="draft2020-12") + + +def skip(message, **kwargs): + def skipper(test): + if all(value == getattr(test, attr) for attr, value in kwargs.items()): + return message + return skipper + + +def ecmascript_regex(test): + if test.subject == "ecmascript-regex": + return "ECMA regex support will be added in #1142." + + +def missing_format(Validator): + def missing_format(test): # pragma: no cover + schema = test.schema + if ( + schema is True + or schema is False + or "format" not in schema + or schema["format"] in Validator.FORMAT_CHECKER.checkers + or test.valid + ): + return + + return f"Format checker {schema['format']!r} not found." + return missing_format + + +def complex_email_validation(test): + if test.subject != "email": + return + + message = "Complex email validation is (intentionally) unsupported." + return skip( + message=message, + description="an invalid domain", + )(test) or skip( + message=message, + description="an invalid IPv4-address-literal", + )(test) or skip( + message=message, + description="dot after local part is not valid", + )(test) or skip( + message=message, + description="dot before local part is not valid", + )(test) or skip( + message=message, + description="two subsequent dots inside local part are not valid", + )(test) + + +def leap_second(test): + message = "Leap seconds are unsupported." + return skip( + message=message, + subject="time", + description="a valid time string with leap second", + )(test) or skip( + message=message, + subject="time", + description="a valid time string with leap second, Zulu", + )(test) or skip( + message=message, + subject="time", + description="a valid time string with leap second with offset", + )(test) or skip( + message=message, + subject="time", + description="valid leap second, positive time-offset", + )(test) or skip( + message=message, + subject="time", + description="valid leap second, negative time-offset", + )(test) or skip( + message=message, + subject="time", + description="valid leap second, large positive time-offset", + )(test) or skip( + message=message, + subject="time", + description="valid leap second, large negative time-offset", + )(test) or skip( + message=message, + subject="time", + description="valid leap second, zero time-offset", + )(test) or skip( + message=message, + subject="date-time", + description="a valid date-time with a leap second, UTC", + )(test) or skip( + message=message, + subject="date-time", + description="a valid date-time with a leap second, with minus offset", + )(test) + + +TestDraft3 = DRAFT3.to_unittest_testcase( + DRAFT3.cases(), + DRAFT3.format_cases(), + DRAFT3.optional_cases_of(name="bignum"), + DRAFT3.optional_cases_of(name="non-bmp-regex"), + DRAFT3.optional_cases_of(name="zeroTerminatedFloats"), + Validator=jsonschema.Draft3Validator, + format_checker=jsonschema.Draft3Validator.FORMAT_CHECKER, + skip=lambda test: ( + ecmascript_regex(test) + or missing_format(jsonschema.Draft3Validator)(test) + or complex_email_validation(test) + ), +) + + +TestDraft4 = DRAFT4.to_unittest_testcase( + DRAFT4.cases(), + DRAFT4.format_cases(), + DRAFT4.optional_cases_of(name="bignum"), + DRAFT4.optional_cases_of(name="float-overflow"), + DRAFT4.optional_cases_of(name="id"), + DRAFT4.optional_cases_of(name="non-bmp-regex"), + DRAFT4.optional_cases_of(name="zeroTerminatedFloats"), + Validator=jsonschema.Draft4Validator, + format_checker=jsonschema.Draft4Validator.FORMAT_CHECKER, + skip=lambda test: ( + ecmascript_regex(test) + or leap_second(test) + or missing_format(jsonschema.Draft4Validator)(test) + or complex_email_validation(test) + ), +) + + +TestDraft6 = DRAFT6.to_unittest_testcase( + DRAFT6.cases(), + DRAFT6.format_cases(), + DRAFT6.optional_cases_of(name="bignum"), + DRAFT6.optional_cases_of(name="float-overflow"), + DRAFT6.optional_cases_of(name="id"), + DRAFT6.optional_cases_of(name="non-bmp-regex"), + Validator=jsonschema.Draft6Validator, + format_checker=jsonschema.Draft6Validator.FORMAT_CHECKER, + skip=lambda test: ( + ecmascript_regex(test) + or leap_second(test) + or missing_format(jsonschema.Draft6Validator)(test) + or complex_email_validation(test) + ), +) + + +TestDraft7 = DRAFT7.to_unittest_testcase( + DRAFT7.cases(), + DRAFT7.format_cases(), + DRAFT7.optional_cases_of(name="bignum"), + DRAFT7.optional_cases_of(name="cross-draft"), + DRAFT7.optional_cases_of(name="float-overflow"), + DRAFT6.optional_cases_of(name="id"), + DRAFT7.optional_cases_of(name="non-bmp-regex"), + DRAFT7.optional_cases_of(name="unknownKeyword"), + Validator=jsonschema.Draft7Validator, + format_checker=jsonschema.Draft7Validator.FORMAT_CHECKER, + skip=lambda test: ( + ecmascript_regex(test) + or leap_second(test) + or missing_format(jsonschema.Draft7Validator)(test) + or complex_email_validation(test) + ), +) + + +TestDraft201909 = DRAFT201909.to_unittest_testcase( + DRAFT201909.cases(), + DRAFT201909.optional_cases_of(name="anchor"), + DRAFT201909.optional_cases_of(name="bignum"), + DRAFT201909.optional_cases_of(name="cross-draft"), + DRAFT201909.optional_cases_of(name="float-overflow"), + DRAFT201909.optional_cases_of(name="id"), + DRAFT201909.optional_cases_of(name="no-schema"), + DRAFT201909.optional_cases_of(name="non-bmp-regex"), + DRAFT201909.optional_cases_of(name="refOfUnknownKeyword"), + DRAFT201909.optional_cases_of(name="unknownKeyword"), + Validator=jsonschema.Draft201909Validator, + skip=skip( + message="Vocabulary support is still in-progress.", + subject="vocabulary", + description=( + "no validation: invalid number, but it still validates" + ), + ), +) + + +TestDraft201909Format = DRAFT201909.to_unittest_testcase( + DRAFT201909.format_cases(), + name="TestDraft201909Format", + Validator=jsonschema.Draft201909Validator, + format_checker=jsonschema.Draft201909Validator.FORMAT_CHECKER, + skip=lambda test: ( + complex_email_validation(test) + or ecmascript_regex(test) + or leap_second(test) + or missing_format(jsonschema.Draft201909Validator)(test) + or complex_email_validation(test) + ), +) + + +TestDraft202012 = DRAFT202012.to_unittest_testcase( + DRAFT202012.cases(), + DRAFT201909.optional_cases_of(name="anchor"), + DRAFT202012.optional_cases_of(name="bignum"), + DRAFT202012.optional_cases_of(name="cross-draft"), + DRAFT202012.optional_cases_of(name="float-overflow"), + DRAFT202012.optional_cases_of(name="id"), + DRAFT202012.optional_cases_of(name="no-schema"), + DRAFT202012.optional_cases_of(name="non-bmp-regex"), + DRAFT202012.optional_cases_of(name="refOfUnknownKeyword"), + DRAFT202012.optional_cases_of(name="unknownKeyword"), + Validator=jsonschema.Draft202012Validator, + skip=skip( + message="Vocabulary support is still in-progress.", + subject="vocabulary", + description=( + "no validation: invalid number, but it still validates" + ), + ), +) + + +TestDraft202012Format = DRAFT202012.to_unittest_testcase( + DRAFT202012.format_cases(), + name="TestDraft202012Format", + Validator=jsonschema.Draft202012Validator, + format_checker=jsonschema.Draft202012Validator.FORMAT_CHECKER, + skip=lambda test: ( + complex_email_validation(test) + or ecmascript_regex(test) + or leap_second(test) + or missing_format(jsonschema.Draft202012Validator)(test) + or complex_email_validation(test) + ), +) diff --git a/source/target-vendors/cp312/jsonschema/tests/test_types.py b/source/target-vendors/cp312/jsonschema/tests/test_types.py new file mode 100644 index 0000000..bd97b18 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/tests/test_types.py @@ -0,0 +1,221 @@ +""" +Tests for the `TypeChecker`-based type interface. + +The actual correctness of the type checking is handled in +`test_jsonschema_test_suite`; these tests check that TypeChecker +functions correctly at a more granular level. +""" +from collections import namedtuple +from unittest import TestCase + +from jsonschema import ValidationError, _keywords +from jsonschema._types import TypeChecker +from jsonschema.exceptions import UndefinedTypeCheck, UnknownType +from jsonschema.validators import Draft202012Validator, extend + + +def equals_2(checker, instance): + return instance == 2 + + +def is_namedtuple(instance): + return isinstance(instance, tuple) and getattr(instance, "_fields", None) + + +def is_object_or_named_tuple(checker, instance): + if Draft202012Validator.TYPE_CHECKER.is_type(instance, "object"): + return True + return is_namedtuple(instance) + + +class TestTypeChecker(TestCase): + def test_is_type(self): + checker = TypeChecker({"two": equals_2}) + self.assertEqual( + ( + checker.is_type(instance=2, type="two"), + checker.is_type(instance="bar", type="two"), + ), + (True, False), + ) + + def test_is_unknown_type(self): + with self.assertRaises(UndefinedTypeCheck) as e: + TypeChecker().is_type(4, "foobar") + self.assertIn( + "'foobar' is unknown to this type checker", + str(e.exception), + ) + self.assertTrue( + e.exception.__suppress_context__, + msg="Expected the internal KeyError to be hidden.", + ) + + def test_checks_can_be_added_at_init(self): + checker = TypeChecker({"two": equals_2}) + self.assertEqual(checker, TypeChecker().redefine("two", equals_2)) + + def test_redefine_existing_type(self): + self.assertEqual( + TypeChecker().redefine("two", object()).redefine("two", equals_2), + TypeChecker().redefine("two", equals_2), + ) + + def test_remove(self): + self.assertEqual( + TypeChecker({"two": equals_2}).remove("two"), + TypeChecker(), + ) + + def test_remove_unknown_type(self): + with self.assertRaises(UndefinedTypeCheck) as context: + TypeChecker().remove("foobar") + self.assertIn("foobar", str(context.exception)) + + def test_redefine_many(self): + self.assertEqual( + TypeChecker().redefine_many({"foo": int, "bar": str}), + TypeChecker().redefine("foo", int).redefine("bar", str), + ) + + def test_remove_multiple(self): + self.assertEqual( + TypeChecker({"foo": int, "bar": str}).remove("foo", "bar"), + TypeChecker(), + ) + + def test_type_check_can_raise_key_error(self): + """ + Make sure no one writes: + + try: + self._type_checkers[type](...) + except KeyError: + + ignoring the fact that the function itself can raise that. + """ + + error = KeyError("Stuff") + + def raises_keyerror(checker, instance): + raise error + + with self.assertRaises(KeyError) as context: + TypeChecker({"foo": raises_keyerror}).is_type(4, "foo") + + self.assertIs(context.exception, error) + + def test_repr(self): + checker = TypeChecker({"foo": is_namedtuple, "bar": is_namedtuple}) + self.assertEqual(repr(checker), "") + + +class TestCustomTypes(TestCase): + def test_simple_type_can_be_extended(self): + def int_or_str_int(checker, instance): + if not isinstance(instance, (int, str)): + return False + try: + int(instance) + except ValueError: + return False + return True + + CustomValidator = extend( + Draft202012Validator, + type_checker=Draft202012Validator.TYPE_CHECKER.redefine( + "integer", int_or_str_int, + ), + ) + validator = CustomValidator({"type": "integer"}) + + validator.validate(4) + validator.validate("4") + + with self.assertRaises(ValidationError): + validator.validate(4.4) + + with self.assertRaises(ValidationError): + validator.validate("foo") + + def test_object_can_be_extended(self): + schema = {"type": "object"} + + Point = namedtuple("Point", ["x", "y"]) + + type_checker = Draft202012Validator.TYPE_CHECKER.redefine( + "object", is_object_or_named_tuple, + ) + + CustomValidator = extend( + Draft202012Validator, + type_checker=type_checker, + ) + validator = CustomValidator(schema) + + validator.validate(Point(x=4, y=5)) + + def test_object_extensions_require_custom_validators(self): + schema = {"type": "object", "required": ["x"]} + + type_checker = Draft202012Validator.TYPE_CHECKER.redefine( + "object", is_object_or_named_tuple, + ) + + CustomValidator = extend( + Draft202012Validator, + type_checker=type_checker, + ) + validator = CustomValidator(schema) + + Point = namedtuple("Point", ["x", "y"]) + # Cannot handle required + with self.assertRaises(ValidationError): + validator.validate(Point(x=4, y=5)) + + def test_object_extensions_can_handle_custom_validators(self): + schema = { + "type": "object", + "required": ["x"], + "properties": {"x": {"type": "integer"}}, + } + + type_checker = Draft202012Validator.TYPE_CHECKER.redefine( + "object", is_object_or_named_tuple, + ) + + def coerce_named_tuple(fn): + def coerced(validator, value, instance, schema): + if is_namedtuple(instance): + instance = instance._asdict() + return fn(validator, value, instance, schema) + return coerced + + required = coerce_named_tuple(_keywords.required) + properties = coerce_named_tuple(_keywords.properties) + + CustomValidator = extend( + Draft202012Validator, + type_checker=type_checker, + validators={"required": required, "properties": properties}, + ) + + validator = CustomValidator(schema) + + Point = namedtuple("Point", ["x", "y"]) + # Can now process required and properties + validator.validate(Point(x=4, y=5)) + + with self.assertRaises(ValidationError): + validator.validate(Point(x="not an integer", y=5)) + + # As well as still handle objects. + validator.validate({"x": 4, "y": 5}) + + with self.assertRaises(ValidationError): + validator.validate({"x": "not an integer", "y": 5}) + + def test_unknown_type(self): + with self.assertRaises(UnknownType) as e: + Draft202012Validator({}).is_type(12, "some unknown type") + self.assertIn("'some unknown type'", str(e.exception)) diff --git a/source/target-vendors/cp312/jsonschema/tests/test_utils.py b/source/target-vendors/cp312/jsonschema/tests/test_utils.py new file mode 100644 index 0000000..d9764b0 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/tests/test_utils.py @@ -0,0 +1,138 @@ +from math import nan +from unittest import TestCase + +from jsonschema._utils import equal + + +class TestEqual(TestCase): + def test_none(self): + self.assertTrue(equal(None, None)) + + def test_nan(self): + self.assertTrue(equal(nan, nan)) + + +class TestDictEqual(TestCase): + def test_equal_dictionaries(self): + dict_1 = {"a": "b", "c": "d"} + dict_2 = {"c": "d", "a": "b"} + self.assertTrue(equal(dict_1, dict_2)) + + def test_equal_dictionaries_with_nan(self): + dict_1 = {"a": nan, "c": "d"} + dict_2 = {"c": "d", "a": nan} + self.assertTrue(equal(dict_1, dict_2)) + + def test_missing_key(self): + dict_1 = {"a": "b", "c": "d"} + dict_2 = {"c": "d", "x": "b"} + self.assertFalse(equal(dict_1, dict_2)) + + def test_additional_key(self): + dict_1 = {"a": "b", "c": "d"} + dict_2 = {"c": "d", "a": "b", "x": "x"} + self.assertFalse(equal(dict_1, dict_2)) + + def test_missing_value(self): + dict_1 = {"a": "b", "c": "d"} + dict_2 = {"c": "d", "a": "x"} + self.assertFalse(equal(dict_1, dict_2)) + + def test_empty_dictionaries(self): + dict_1 = {} + dict_2 = {} + self.assertTrue(equal(dict_1, dict_2)) + + def test_one_none(self): + dict_1 = None + dict_2 = {"a": "b", "c": "d"} + self.assertFalse(equal(dict_1, dict_2)) + + def test_same_item(self): + dict_1 = {"a": "b", "c": "d"} + self.assertTrue(equal(dict_1, dict_1)) + + def test_nested_equal(self): + dict_1 = {"a": {"a": "b", "c": "d"}, "c": "d"} + dict_2 = {"c": "d", "a": {"a": "b", "c": "d"}} + self.assertTrue(equal(dict_1, dict_2)) + + def test_nested_dict_unequal(self): + dict_1 = {"a": {"a": "b", "c": "d"}, "c": "d"} + dict_2 = {"c": "d", "a": {"a": "b", "c": "x"}} + self.assertFalse(equal(dict_1, dict_2)) + + def test_mixed_nested_equal(self): + dict_1 = {"a": ["a", "b", "c", "d"], "c": "d"} + dict_2 = {"c": "d", "a": ["a", "b", "c", "d"]} + self.assertTrue(equal(dict_1, dict_2)) + + def test_nested_list_unequal(self): + dict_1 = {"a": ["a", "b", "c", "d"], "c": "d"} + dict_2 = {"c": "d", "a": ["b", "c", "d", "a"]} + self.assertFalse(equal(dict_1, dict_2)) + + +class TestListEqual(TestCase): + def test_equal_lists(self): + list_1 = ["a", "b", "c"] + list_2 = ["a", "b", "c"] + self.assertTrue(equal(list_1, list_2)) + + def test_equal_lists_with_nan(self): + list_1 = ["a", nan, "c"] + list_2 = ["a", nan, "c"] + self.assertTrue(equal(list_1, list_2)) + + def test_unsorted_lists(self): + list_1 = ["a", "b", "c"] + list_2 = ["b", "b", "a"] + self.assertFalse(equal(list_1, list_2)) + + def test_first_list_larger(self): + list_1 = ["a", "b", "c"] + list_2 = ["a", "b"] + self.assertFalse(equal(list_1, list_2)) + + def test_second_list_larger(self): + list_1 = ["a", "b"] + list_2 = ["a", "b", "c"] + self.assertFalse(equal(list_1, list_2)) + + def test_list_with_none_unequal(self): + list_1 = ["a", "b", None] + list_2 = ["a", "b", "c"] + self.assertFalse(equal(list_1, list_2)) + + list_1 = ["a", "b", None] + list_2 = [None, "b", "c"] + self.assertFalse(equal(list_1, list_2)) + + def test_list_with_none_equal(self): + list_1 = ["a", None, "c"] + list_2 = ["a", None, "c"] + self.assertTrue(equal(list_1, list_2)) + + def test_empty_list(self): + list_1 = [] + list_2 = [] + self.assertTrue(equal(list_1, list_2)) + + def test_one_none(self): + list_1 = None + list_2 = [] + self.assertFalse(equal(list_1, list_2)) + + def test_same_list(self): + list_1 = ["a", "b", "c"] + self.assertTrue(equal(list_1, list_1)) + + def test_equal_nested_lists(self): + list_1 = ["a", ["b", "c"], "d"] + list_2 = ["a", ["b", "c"], "d"] + self.assertTrue(equal(list_1, list_2)) + + def test_unequal_nested_lists(self): + list_1 = ["a", ["b", "c"], "d"] + list_2 = ["a", [], "c"] + self.assertFalse(equal(list_1, list_2)) diff --git a/source/target-vendors/cp312/jsonschema/tests/test_validators.py b/source/target-vendors/cp312/jsonschema/tests/test_validators.py new file mode 100644 index 0000000..d419ddf --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/tests/test_validators.py @@ -0,0 +1,2573 @@ +from __future__ import annotations + +from collections import deque, namedtuple +from contextlib import contextmanager +from decimal import Decimal +from io import BytesIO +from typing import Any +from unittest import TestCase, mock +from urllib.request import pathname2url +import json +import os +import sys +import tempfile +import warnings + +from attrs import define, field +from referencing.jsonschema import DRAFT202012 +import referencing.exceptions + +from jsonschema import ( + FormatChecker, + TypeChecker, + exceptions, + protocols, + validators, +) + + +def fail(validator, errors, instance, schema): + for each in errors: + each.setdefault("message", "You told me to fail!") + yield exceptions.ValidationError(**each) + + +class TestCreateAndExtend(TestCase): + def setUp(self): + self.addCleanup( + self.assertEqual, + validators._META_SCHEMAS, + dict(validators._META_SCHEMAS), + ) + self.addCleanup( + self.assertEqual, + validators._VALIDATORS, + dict(validators._VALIDATORS), + ) + + self.meta_schema = {"$id": "some://meta/schema"} + self.validators = {"fail": fail} + self.type_checker = TypeChecker() + self.Validator = validators.create( + meta_schema=self.meta_schema, + validators=self.validators, + type_checker=self.type_checker, + ) + + def test_attrs(self): + self.assertEqual( + ( + self.Validator.VALIDATORS, + self.Validator.META_SCHEMA, + self.Validator.TYPE_CHECKER, + ), ( + self.validators, + self.meta_schema, + self.type_checker, + ), + ) + + def test_init(self): + schema = {"fail": []} + self.assertEqual(self.Validator(schema).schema, schema) + + def test_iter_errors_successful(self): + schema = {"fail": []} + validator = self.Validator(schema) + + errors = list(validator.iter_errors("hello")) + self.assertEqual(errors, []) + + def test_iter_errors_one_error(self): + schema = {"fail": [{"message": "Whoops!"}]} + validator = self.Validator(schema) + + expected_error = exceptions.ValidationError( + "Whoops!", + instance="goodbye", + schema=schema, + validator="fail", + validator_value=[{"message": "Whoops!"}], + schema_path=deque(["fail"]), + ) + + errors = list(validator.iter_errors("goodbye")) + self.assertEqual(len(errors), 1) + self.assertEqual(errors[0]._contents(), expected_error._contents()) + + def test_iter_errors_multiple_errors(self): + schema = { + "fail": [ + {"message": "First"}, + {"message": "Second!", "validator": "asdf"}, + {"message": "Third"}, + ], + } + validator = self.Validator(schema) + + errors = list(validator.iter_errors("goodbye")) + self.assertEqual(len(errors), 3) + + def test_if_a_version_is_provided_it_is_registered(self): + Validator = validators.create( + meta_schema={"$id": "something"}, + version="my version", + ) + self.addCleanup(validators._META_SCHEMAS.pop, "something") + self.addCleanup(validators._VALIDATORS.pop, "my version") + self.assertEqual(Validator.__name__, "MyVersionValidator") + self.assertEqual(Validator.__qualname__, "MyVersionValidator") + + def test_repr(self): + Validator = validators.create( + meta_schema={"$id": "something"}, + version="my version", + ) + self.addCleanup(validators._META_SCHEMAS.pop, "something") + self.addCleanup(validators._VALIDATORS.pop, "my version") + self.assertEqual( + repr(Validator({})), + "MyVersionValidator(schema={}, format_checker=None)", + ) + + def test_long_repr(self): + Validator = validators.create( + meta_schema={"$id": "something"}, + version="my version", + ) + self.addCleanup(validators._META_SCHEMAS.pop, "something") + self.addCleanup(validators._VALIDATORS.pop, "my version") + self.assertEqual( + repr(Validator({"a": list(range(1000))})), ( + "MyVersionValidator(schema={'a': [0, 1, 2, 3, 4, 5, ...]}, " + "format_checker=None)" + ), + ) + + def test_repr_no_version(self): + Validator = validators.create(meta_schema={}) + self.assertEqual( + repr(Validator({})), + "Validator(schema={}, format_checker=None)", + ) + + def test_dashes_are_stripped_from_validator_names(self): + Validator = validators.create( + meta_schema={"$id": "something"}, + version="foo-bar", + ) + self.addCleanup(validators._META_SCHEMAS.pop, "something") + self.addCleanup(validators._VALIDATORS.pop, "foo-bar") + self.assertEqual(Validator.__qualname__, "FooBarValidator") + + def test_if_a_version_is_not_provided_it_is_not_registered(self): + original = dict(validators._META_SCHEMAS) + validators.create(meta_schema={"id": "id"}) + self.assertEqual(validators._META_SCHEMAS, original) + + def test_validates_registers_meta_schema_id(self): + meta_schema_key = "meta schema id" + my_meta_schema = {"id": meta_schema_key} + + validators.create( + meta_schema=my_meta_schema, + version="my version", + id_of=lambda s: s.get("id", ""), + ) + self.addCleanup(validators._META_SCHEMAS.pop, meta_schema_key) + self.addCleanup(validators._VALIDATORS.pop, "my version") + + self.assertIn(meta_schema_key, validators._META_SCHEMAS) + + def test_validates_registers_meta_schema_draft6_id(self): + meta_schema_key = "meta schema $id" + my_meta_schema = {"$id": meta_schema_key} + + validators.create( + meta_schema=my_meta_schema, + version="my version", + ) + self.addCleanup(validators._META_SCHEMAS.pop, meta_schema_key) + self.addCleanup(validators._VALIDATORS.pop, "my version") + + self.assertIn(meta_schema_key, validators._META_SCHEMAS) + + def test_create_default_types(self): + Validator = validators.create(meta_schema={}, validators=()) + self.assertTrue( + all( + Validator({}).is_type(instance=instance, type=type) + for type, instance in [ + ("array", []), + ("boolean", True), + ("integer", 12), + ("null", None), + ("number", 12.0), + ("object", {}), + ("string", "foo"), + ] + ), + ) + + def test_check_schema_with_different_metaschema(self): + """ + One can create a validator class whose metaschema uses a different + dialect than itself. + """ + + NoEmptySchemasValidator = validators.create( + meta_schema={ + "$schema": validators.Draft202012Validator.META_SCHEMA["$id"], + "not": {"const": {}}, + }, + ) + NoEmptySchemasValidator.check_schema({"foo": "bar"}) + + with self.assertRaises(exceptions.SchemaError): + NoEmptySchemasValidator.check_schema({}) + + NoEmptySchemasValidator({"foo": "bar"}).validate("foo") + + def test_check_schema_with_different_metaschema_defaults_to_self(self): + """ + A validator whose metaschema doesn't declare $schema defaults to its + own validation behavior, not the latest "normal" specification. + """ + + NoEmptySchemasValidator = validators.create( + meta_schema={"fail": [{"message": "Meta schema whoops!"}]}, + validators={"fail": fail}, + ) + with self.assertRaises(exceptions.SchemaError): + NoEmptySchemasValidator.check_schema({}) + + def test_extend(self): + original = dict(self.Validator.VALIDATORS) + new = object() + + Extended = validators.extend( + self.Validator, + validators={"new": new}, + ) + self.assertEqual( + ( + Extended.VALIDATORS, + Extended.META_SCHEMA, + Extended.TYPE_CHECKER, + self.Validator.VALIDATORS, + ), ( + dict(original, new=new), + self.Validator.META_SCHEMA, + self.Validator.TYPE_CHECKER, + original, + ), + ) + + def test_extend_idof(self): + """ + Extending a validator preserves its notion of schema IDs. + """ + def id_of(schema): + return schema.get("__test__", self.Validator.ID_OF(schema)) + correct_id = "the://correct/id/" + meta_schema = { + "$id": "the://wrong/id/", + "__test__": correct_id, + } + Original = validators.create( + meta_schema=meta_schema, + validators=self.validators, + type_checker=self.type_checker, + id_of=id_of, + ) + self.assertEqual(Original.ID_OF(Original.META_SCHEMA), correct_id) + + Derived = validators.extend(Original) + self.assertEqual(Derived.ID_OF(Derived.META_SCHEMA), correct_id) + + def test_extend_applicable_validators(self): + """ + Extending a validator preserves its notion of applicable validators. + """ + + schema = { + "$defs": {"test": {"type": "number"}}, + "$ref": "#/$defs/test", + "maximum": 1, + } + + draft4 = validators.Draft4Validator(schema) + self.assertTrue(draft4.is_valid(37)) # as $ref ignores siblings + + Derived = validators.extend(validators.Draft4Validator) + self.assertTrue(Derived(schema).is_valid(37)) + + +class TestValidationErrorMessages(TestCase): + def message_for(self, instance, schema, *args, **kwargs): + cls = kwargs.pop("cls", validators._LATEST_VERSION) + cls.check_schema(schema) + validator = cls(schema, *args, **kwargs) + errors = list(validator.iter_errors(instance)) + self.assertTrue(errors, msg=f"No errors were raised for {instance!r}") + self.assertEqual( + len(errors), + 1, + msg=f"Expected exactly one error, found {errors!r}", + ) + return errors[0].message + + def test_single_type_failure(self): + message = self.message_for(instance=1, schema={"type": "string"}) + self.assertEqual(message, "1 is not of type 'string'") + + def test_single_type_list_failure(self): + message = self.message_for(instance=1, schema={"type": ["string"]}) + self.assertEqual(message, "1 is not of type 'string'") + + def test_multiple_type_failure(self): + types = "string", "object" + message = self.message_for(instance=1, schema={"type": list(types)}) + self.assertEqual(message, "1 is not of type 'string', 'object'") + + def test_object_with_named_type_failure(self): + schema = {"type": [{"name": "Foo", "minimum": 3}]} + message = self.message_for( + instance=1, + schema=schema, + cls=validators.Draft3Validator, + ) + self.assertEqual(message, "1 is not of type 'Foo'") + + def test_minimum(self): + message = self.message_for(instance=1, schema={"minimum": 2}) + self.assertEqual(message, "1 is less than the minimum of 2") + + def test_maximum(self): + message = self.message_for(instance=1, schema={"maximum": 0}) + self.assertEqual(message, "1 is greater than the maximum of 0") + + def test_dependencies_single_element(self): + depend, on = "bar", "foo" + schema = {"dependencies": {depend: on}} + message = self.message_for( + instance={"bar": 2}, + schema=schema, + cls=validators.Draft3Validator, + ) + self.assertEqual(message, "'foo' is a dependency of 'bar'") + + def test_object_without_title_type_failure_draft3(self): + type = {"type": [{"minimum": 3}]} + message = self.message_for( + instance=1, + schema={"type": [type]}, + cls=validators.Draft3Validator, + ) + self.assertEqual( + message, + "1 is not of type {'type': [{'minimum': 3}]}", + ) + + def test_dependencies_list_draft3(self): + depend, on = "bar", "foo" + schema = {"dependencies": {depend: [on]}} + message = self.message_for( + instance={"bar": 2}, + schema=schema, + cls=validators.Draft3Validator, + ) + self.assertEqual(message, "'foo' is a dependency of 'bar'") + + def test_dependencies_list_draft7(self): + depend, on = "bar", "foo" + schema = {"dependencies": {depend: [on]}} + message = self.message_for( + instance={"bar": 2}, + schema=schema, + cls=validators.Draft7Validator, + ) + self.assertEqual(message, "'foo' is a dependency of 'bar'") + + def test_additionalItems_single_failure(self): + message = self.message_for( + instance=[2], + schema={"items": [], "additionalItems": False}, + cls=validators.Draft3Validator, + ) + self.assertIn("(2 was unexpected)", message) + + def test_additionalItems_multiple_failures(self): + message = self.message_for( + instance=[1, 2, 3], + schema={"items": [], "additionalItems": False}, + cls=validators.Draft3Validator, + ) + self.assertIn("(1, 2, 3 were unexpected)", message) + + def test_additionalProperties_single_failure(self): + additional = "foo" + schema = {"additionalProperties": False} + message = self.message_for(instance={additional: 2}, schema=schema) + self.assertIn("('foo' was unexpected)", message) + + def test_additionalProperties_multiple_failures(self): + schema = {"additionalProperties": False} + message = self.message_for( + instance=dict.fromkeys(["foo", "bar"]), + schema=schema, + ) + + self.assertIn(repr("foo"), message) + self.assertIn(repr("bar"), message) + self.assertIn("were unexpected)", message) + + def test_const(self): + schema = {"const": 12} + message = self.message_for( + instance={"foo": "bar"}, + schema=schema, + ) + self.assertIn("12 was expected", message) + + def test_contains_draft_6(self): + schema = {"contains": {"const": 12}} + message = self.message_for( + instance=[2, {}, []], + schema=schema, + cls=validators.Draft6Validator, + ) + self.assertEqual( + message, + "None of [2, {}, []] are valid under the given schema", + ) + + def test_invalid_format_default_message(self): + checker = FormatChecker(formats=()) + checker.checks("thing")(lambda value: False) + + schema = {"format": "thing"} + message = self.message_for( + instance="bla", + schema=schema, + format_checker=checker, + ) + + self.assertIn(repr("bla"), message) + self.assertIn(repr("thing"), message) + self.assertIn("is not a", message) + + def test_additionalProperties_false_patternProperties(self): + schema = {"type": "object", + "additionalProperties": False, + "patternProperties": { + "^abc$": {"type": "string"}, + "^def$": {"type": "string"}, + }} + message = self.message_for( + instance={"zebra": 123}, + schema=schema, + cls=validators.Draft4Validator, + ) + self.assertEqual( + message, + "{} does not match any of the regexes: {}, {}".format( + repr("zebra"), repr("^abc$"), repr("^def$"), + ), + ) + message = self.message_for( + instance={"zebra": 123, "fish": 456}, + schema=schema, + cls=validators.Draft4Validator, + ) + self.assertEqual( + message, + "{}, {} do not match any of the regexes: {}, {}".format( + repr("fish"), repr("zebra"), repr("^abc$"), repr("^def$"), + ), + ) + + def test_False_schema(self): + message = self.message_for( + instance="something", + schema=False, + ) + self.assertEqual(message, "False schema does not allow 'something'") + + def test_multipleOf(self): + message = self.message_for( + instance=3, + schema={"multipleOf": 2}, + ) + self.assertEqual(message, "3 is not a multiple of 2") + + def test_minItems(self): + message = self.message_for(instance=[], schema={"minItems": 2}) + self.assertEqual(message, "[] is too short") + + def test_maxItems(self): + message = self.message_for(instance=[1, 2, 3], schema={"maxItems": 2}) + self.assertEqual(message, "[1, 2, 3] is too long") + + def test_minItems_1(self): + message = self.message_for(instance=[], schema={"minItems": 1}) + self.assertEqual(message, "[] should be non-empty") + + def test_maxItems_0(self): + message = self.message_for(instance=[1, 2, 3], schema={"maxItems": 0}) + self.assertEqual(message, "[1, 2, 3] is expected to be empty") + + def test_minLength(self): + message = self.message_for( + instance="", + schema={"minLength": 2}, + ) + self.assertEqual(message, "'' is too short") + + def test_maxLength(self): + message = self.message_for( + instance="abc", + schema={"maxLength": 2}, + ) + self.assertEqual(message, "'abc' is too long") + + def test_minLength_1(self): + message = self.message_for(instance="", schema={"minLength": 1}) + self.assertEqual(message, "'' should be non-empty") + + def test_maxLength_0(self): + message = self.message_for(instance="abc", schema={"maxLength": 0}) + self.assertEqual(message, "'abc' is expected to be empty") + + def test_minProperties(self): + message = self.message_for(instance={}, schema={"minProperties": 2}) + self.assertEqual(message, "{} does not have enough properties") + + def test_maxProperties(self): + message = self.message_for( + instance={"a": {}, "b": {}, "c": {}}, + schema={"maxProperties": 2}, + ) + self.assertEqual( + message, + "{'a': {}, 'b': {}, 'c': {}} has too many properties", + ) + + def test_minProperties_1(self): + message = self.message_for(instance={}, schema={"minProperties": 1}) + self.assertEqual(message, "{} should be non-empty") + + def test_maxProperties_0(self): + message = self.message_for( + instance={1: 2}, + schema={"maxProperties": 0}, + ) + self.assertEqual(message, "{1: 2} is expected to be empty") + + def test_prefixItems_with_items(self): + message = self.message_for( + instance=[1, 2, "foo"], + schema={"items": False, "prefixItems": [{}, {}]}, + ) + self.assertEqual( + message, + "Expected at most 2 items but found 1 extra: 'foo'", + ) + + def test_prefixItems_with_multiple_extra_items(self): + message = self.message_for( + instance=[1, 2, "foo", 5], + schema={"items": False, "prefixItems": [{}, {}]}, + ) + self.assertEqual( + message, + "Expected at most 2 items but found 2 extra: ['foo', 5]", + ) + + def test_pattern(self): + message = self.message_for( + instance="bbb", + schema={"pattern": "^a*$"}, + ) + self.assertEqual(message, "'bbb' does not match '^a*$'") + + def test_does_not_contain(self): + message = self.message_for( + instance=[], + schema={"contains": {"type": "string"}}, + ) + self.assertEqual( + message, + "[] does not contain items matching the given schema", + ) + + def test_contains_too_few(self): + message = self.message_for( + instance=["foo", 1], + schema={"contains": {"type": "string"}, "minContains": 2}, + ) + self.assertEqual( + message, + "Too few items match the given schema " + "(expected at least 2 but only 1 matched)", + ) + + def test_contains_too_few_both_constrained(self): + message = self.message_for( + instance=["foo", 1], + schema={ + "contains": {"type": "string"}, + "minContains": 2, + "maxContains": 4, + }, + ) + self.assertEqual( + message, + "Too few items match the given schema (expected at least 2 but " + "only 1 matched)", + ) + + def test_contains_too_many(self): + message = self.message_for( + instance=["foo", "bar", "baz"], + schema={"contains": {"type": "string"}, "maxContains": 2}, + ) + self.assertEqual( + message, + "Too many items match the given schema (expected at most 2)", + ) + + def test_contains_too_many_both_constrained(self): + message = self.message_for( + instance=["foo"] * 5, + schema={ + "contains": {"type": "string"}, + "minContains": 2, + "maxContains": 4, + }, + ) + self.assertEqual( + message, + "Too many items match the given schema (expected at most 4)", + ) + + def test_exclusiveMinimum(self): + message = self.message_for( + instance=3, + schema={"exclusiveMinimum": 5}, + ) + self.assertEqual( + message, + "3 is less than or equal to the minimum of 5", + ) + + def test_exclusiveMaximum(self): + message = self.message_for(instance=3, schema={"exclusiveMaximum": 2}) + self.assertEqual( + message, + "3 is greater than or equal to the maximum of 2", + ) + + def test_required(self): + message = self.message_for(instance={}, schema={"required": ["foo"]}) + self.assertEqual(message, "'foo' is a required property") + + def test_dependentRequired(self): + message = self.message_for( + instance={"foo": {}}, + schema={"dependentRequired": {"foo": ["bar"]}}, + ) + self.assertEqual(message, "'bar' is a dependency of 'foo'") + + def test_oneOf_matches_none(self): + message = self.message_for(instance={}, schema={"oneOf": [False]}) + self.assertEqual( + message, + "{} is not valid under any of the given schemas", + ) + + def test_oneOf_matches_too_many(self): + message = self.message_for(instance={}, schema={"oneOf": [True, True]}) + self.assertEqual(message, "{} is valid under each of True, True") + + def test_unevaluated_items(self): + schema = {"type": "array", "unevaluatedItems": False} + message = self.message_for(instance=["foo", "bar"], schema=schema) + self.assertIn( + message, + "Unevaluated items are not allowed ('foo', 'bar' were unexpected)", + ) + + def test_unevaluated_items_on_invalid_type(self): + schema = {"type": "array", "unevaluatedItems": False} + message = self.message_for(instance="foo", schema=schema) + self.assertEqual(message, "'foo' is not of type 'array'") + + def test_unevaluated_properties_invalid_against_subschema(self): + schema = { + "properties": {"foo": {"type": "string"}}, + "unevaluatedProperties": {"const": 12}, + } + message = self.message_for( + instance={ + "foo": "foo", + "bar": "bar", + "baz": 12, + }, + schema=schema, + ) + self.assertEqual( + message, + "Unevaluated properties are not valid under the given schema " + "('bar' was unevaluated and invalid)", + ) + + def test_unevaluated_properties_disallowed(self): + schema = {"type": "object", "unevaluatedProperties": False} + message = self.message_for( + instance={ + "foo": "foo", + "bar": "bar", + }, + schema=schema, + ) + self.assertEqual( + message, + "Unevaluated properties are not allowed " + "('bar', 'foo' were unexpected)", + ) + + def test_unevaluated_properties_on_invalid_type(self): + schema = {"type": "object", "unevaluatedProperties": False} + message = self.message_for(instance="foo", schema=schema) + self.assertEqual(message, "'foo' is not of type 'object'") + + def test_single_item(self): + schema = {"prefixItems": [{}], "items": False} + message = self.message_for( + instance=["foo", "bar", "baz"], + schema=schema, + ) + self.assertEqual( + message, + "Expected at most 1 item but found 2 extra: ['bar', 'baz']", + ) + + def test_heterogeneous_additionalItems_with_Items(self): + schema = {"items": [{}], "additionalItems": False} + message = self.message_for( + instance=["foo", "bar", 37], + schema=schema, + cls=validators.Draft7Validator, + ) + self.assertEqual( + message, + "Additional items are not allowed ('bar', 37 were unexpected)", + ) + + def test_heterogeneous_items_prefixItems(self): + schema = {"prefixItems": [{}], "items": False} + message = self.message_for( + instance=["foo", "bar", 37], + schema=schema, + ) + self.assertEqual( + message, + "Expected at most 1 item but found 2 extra: ['bar', 37]", + ) + + def test_heterogeneous_unevaluatedItems_prefixItems(self): + schema = {"prefixItems": [{}], "unevaluatedItems": False} + message = self.message_for( + instance=["foo", "bar", 37], + schema=schema, + ) + self.assertEqual( + message, + "Unevaluated items are not allowed ('bar', 37 were unexpected)", + ) + + def test_heterogeneous_properties_additionalProperties(self): + """ + Not valid deserialized JSON, but this should not blow up. + """ + schema = {"properties": {"foo": {}}, "additionalProperties": False} + message = self.message_for( + instance={"foo": {}, "a": "baz", 37: 12}, + schema=schema, + ) + self.assertEqual( + message, + "Additional properties are not allowed (37, 'a' were unexpected)", + ) + + def test_heterogeneous_properties_unevaluatedProperties(self): + """ + Not valid deserialized JSON, but this should not blow up. + """ + schema = {"properties": {"foo": {}}, "unevaluatedProperties": False} + message = self.message_for( + instance={"foo": {}, "a": "baz", 37: 12}, + schema=schema, + ) + self.assertEqual( + message, + "Unevaluated properties are not allowed (37, 'a' were unexpected)", + ) + + +class TestValidationErrorDetails(TestCase): + # TODO: These really need unit tests for each individual keyword, rather + # than just these higher level tests. + def test_anyOf(self): + instance = 5 + schema = { + "anyOf": [ + {"minimum": 20}, + {"type": "string"}, + ], + } + + validator = validators.Draft4Validator(schema) + errors = list(validator.iter_errors(instance)) + self.assertEqual(len(errors), 1) + e = errors[0] + + self.assertEqual(e.validator, "anyOf") + self.assertEqual(e.validator_value, schema["anyOf"]) + self.assertEqual(e.instance, instance) + self.assertEqual(e.schema, schema) + self.assertIsNone(e.parent) + + self.assertEqual(e.path, deque([])) + self.assertEqual(e.relative_path, deque([])) + self.assertEqual(e.absolute_path, deque([])) + self.assertEqual(e.json_path, "$") + + self.assertEqual(e.schema_path, deque(["anyOf"])) + self.assertEqual(e.relative_schema_path, deque(["anyOf"])) + self.assertEqual(e.absolute_schema_path, deque(["anyOf"])) + + self.assertEqual(len(e.context), 2) + + e1, e2 = sorted_errors(e.context) + + self.assertEqual(e1.validator, "minimum") + self.assertEqual(e1.validator_value, schema["anyOf"][0]["minimum"]) + self.assertEqual(e1.instance, instance) + self.assertEqual(e1.schema, schema["anyOf"][0]) + self.assertIs(e1.parent, e) + + self.assertEqual(e1.path, deque([])) + self.assertEqual(e1.absolute_path, deque([])) + self.assertEqual(e1.relative_path, deque([])) + self.assertEqual(e1.json_path, "$") + + self.assertEqual(e1.schema_path, deque([0, "minimum"])) + self.assertEqual(e1.relative_schema_path, deque([0, "minimum"])) + self.assertEqual( + e1.absolute_schema_path, deque(["anyOf", 0, "minimum"]), + ) + + self.assertFalse(e1.context) + + self.assertEqual(e2.validator, "type") + self.assertEqual(e2.validator_value, schema["anyOf"][1]["type"]) + self.assertEqual(e2.instance, instance) + self.assertEqual(e2.schema, schema["anyOf"][1]) + self.assertIs(e2.parent, e) + + self.assertEqual(e2.path, deque([])) + self.assertEqual(e2.relative_path, deque([])) + self.assertEqual(e2.absolute_path, deque([])) + self.assertEqual(e2.json_path, "$") + + self.assertEqual(e2.schema_path, deque([1, "type"])) + self.assertEqual(e2.relative_schema_path, deque([1, "type"])) + self.assertEqual(e2.absolute_schema_path, deque(["anyOf", 1, "type"])) + + self.assertEqual(len(e2.context), 0) + + def test_type(self): + instance = {"foo": 1} + schema = { + "type": [ + {"type": "integer"}, + { + "type": "object", + "properties": {"foo": {"enum": [2]}}, + }, + ], + } + + validator = validators.Draft3Validator(schema) + errors = list(validator.iter_errors(instance)) + self.assertEqual(len(errors), 1) + e = errors[0] + + self.assertEqual(e.validator, "type") + self.assertEqual(e.validator_value, schema["type"]) + self.assertEqual(e.instance, instance) + self.assertEqual(e.schema, schema) + self.assertIsNone(e.parent) + + self.assertEqual(e.path, deque([])) + self.assertEqual(e.relative_path, deque([])) + self.assertEqual(e.absolute_path, deque([])) + self.assertEqual(e.json_path, "$") + + self.assertEqual(e.schema_path, deque(["type"])) + self.assertEqual(e.relative_schema_path, deque(["type"])) + self.assertEqual(e.absolute_schema_path, deque(["type"])) + + self.assertEqual(len(e.context), 2) + + e1, e2 = sorted_errors(e.context) + + self.assertEqual(e1.validator, "type") + self.assertEqual(e1.validator_value, schema["type"][0]["type"]) + self.assertEqual(e1.instance, instance) + self.assertEqual(e1.schema, schema["type"][0]) + self.assertIs(e1.parent, e) + + self.assertEqual(e1.path, deque([])) + self.assertEqual(e1.relative_path, deque([])) + self.assertEqual(e1.absolute_path, deque([])) + self.assertEqual(e1.json_path, "$") + + self.assertEqual(e1.schema_path, deque([0, "type"])) + self.assertEqual(e1.relative_schema_path, deque([0, "type"])) + self.assertEqual(e1.absolute_schema_path, deque(["type", 0, "type"])) + + self.assertFalse(e1.context) + + self.assertEqual(e2.validator, "enum") + self.assertEqual(e2.validator_value, [2]) + self.assertEqual(e2.instance, 1) + self.assertEqual(e2.schema, {"enum": [2]}) + self.assertIs(e2.parent, e) + + self.assertEqual(e2.path, deque(["foo"])) + self.assertEqual(e2.relative_path, deque(["foo"])) + self.assertEqual(e2.absolute_path, deque(["foo"])) + self.assertEqual(e2.json_path, "$.foo") + + self.assertEqual( + e2.schema_path, deque([1, "properties", "foo", "enum"]), + ) + self.assertEqual( + e2.relative_schema_path, deque([1, "properties", "foo", "enum"]), + ) + self.assertEqual( + e2.absolute_schema_path, + deque(["type", 1, "properties", "foo", "enum"]), + ) + + self.assertFalse(e2.context) + + def test_single_nesting(self): + instance = {"foo": 2, "bar": [1], "baz": 15, "quux": "spam"} + schema = { + "properties": { + "foo": {"type": "string"}, + "bar": {"minItems": 2}, + "baz": {"maximum": 10, "enum": [2, 4, 6, 8]}, + }, + } + + validator = validators.Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2, e3, e4 = sorted_errors(errors) + + self.assertEqual(e1.path, deque(["bar"])) + self.assertEqual(e2.path, deque(["baz"])) + self.assertEqual(e3.path, deque(["baz"])) + self.assertEqual(e4.path, deque(["foo"])) + + self.assertEqual(e1.relative_path, deque(["bar"])) + self.assertEqual(e2.relative_path, deque(["baz"])) + self.assertEqual(e3.relative_path, deque(["baz"])) + self.assertEqual(e4.relative_path, deque(["foo"])) + + self.assertEqual(e1.absolute_path, deque(["bar"])) + self.assertEqual(e2.absolute_path, deque(["baz"])) + self.assertEqual(e3.absolute_path, deque(["baz"])) + self.assertEqual(e4.absolute_path, deque(["foo"])) + + self.assertEqual(e1.json_path, "$.bar") + self.assertEqual(e2.json_path, "$.baz") + self.assertEqual(e3.json_path, "$.baz") + self.assertEqual(e4.json_path, "$.foo") + + self.assertEqual(e1.validator, "minItems") + self.assertEqual(e2.validator, "enum") + self.assertEqual(e3.validator, "maximum") + self.assertEqual(e4.validator, "type") + + def test_multiple_nesting(self): + instance = [1, {"foo": 2, "bar": {"baz": [1]}}, "quux"] + schema = { + "type": "string", + "items": { + "type": ["string", "object"], + "properties": { + "foo": {"enum": [1, 3]}, + "bar": { + "type": "array", + "properties": { + "bar": {"required": True}, + "baz": {"minItems": 2}, + }, + }, + }, + }, + } + + validator = validators.Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2, e3, e4, e5, e6 = sorted_errors(errors) + + self.assertEqual(e1.path, deque([])) + self.assertEqual(e2.path, deque([0])) + self.assertEqual(e3.path, deque([1, "bar"])) + self.assertEqual(e4.path, deque([1, "bar", "bar"])) + self.assertEqual(e5.path, deque([1, "bar", "baz"])) + self.assertEqual(e6.path, deque([1, "foo"])) + + self.assertEqual(e1.json_path, "$") + self.assertEqual(e2.json_path, "$[0]") + self.assertEqual(e3.json_path, "$[1].bar") + self.assertEqual(e4.json_path, "$[1].bar.bar") + self.assertEqual(e5.json_path, "$[1].bar.baz") + self.assertEqual(e6.json_path, "$[1].foo") + + self.assertEqual(e1.schema_path, deque(["type"])) + self.assertEqual(e2.schema_path, deque(["items", "type"])) + self.assertEqual( + list(e3.schema_path), ["items", "properties", "bar", "type"], + ) + self.assertEqual( + list(e4.schema_path), + ["items", "properties", "bar", "properties", "bar", "required"], + ) + self.assertEqual( + list(e5.schema_path), + ["items", "properties", "bar", "properties", "baz", "minItems"], + ) + self.assertEqual( + list(e6.schema_path), ["items", "properties", "foo", "enum"], + ) + + self.assertEqual(e1.validator, "type") + self.assertEqual(e2.validator, "type") + self.assertEqual(e3.validator, "type") + self.assertEqual(e4.validator, "required") + self.assertEqual(e5.validator, "minItems") + self.assertEqual(e6.validator, "enum") + + def test_recursive(self): + schema = { + "definitions": { + "node": { + "anyOf": [{ + "type": "object", + "required": ["name", "children"], + "properties": { + "name": { + "type": "string", + }, + "children": { + "type": "object", + "patternProperties": { + "^.*$": { + "$ref": "#/definitions/node", + }, + }, + }, + }, + }], + }, + }, + "type": "object", + "required": ["root"], + "properties": {"root": {"$ref": "#/definitions/node"}}, + } + + instance = { + "root": { + "name": "root", + "children": { + "a": { + "name": "a", + "children": { + "ab": { + "name": "ab", + # missing "children" + }, + }, + }, + }, + }, + } + validator = validators.Draft4Validator(schema) + + e, = validator.iter_errors(instance) + self.assertEqual(e.absolute_path, deque(["root"])) + self.assertEqual( + e.absolute_schema_path, deque(["properties", "root", "anyOf"]), + ) + self.assertEqual(e.json_path, "$.root") + + e1, = e.context + self.assertEqual(e1.absolute_path, deque(["root", "children", "a"])) + self.assertEqual( + e1.absolute_schema_path, deque( + [ + "properties", + "root", + "anyOf", + 0, + "properties", + "children", + "patternProperties", + "^.*$", + "anyOf", + ], + ), + ) + self.assertEqual(e1.json_path, "$.root.children.a") + + e2, = e1.context + self.assertEqual( + e2.absolute_path, deque( + ["root", "children", "a", "children", "ab"], + ), + ) + self.assertEqual( + e2.absolute_schema_path, deque( + [ + "properties", + "root", + "anyOf", + 0, + "properties", + "children", + "patternProperties", + "^.*$", + "anyOf", + 0, + "properties", + "children", + "patternProperties", + "^.*$", + "anyOf", + ], + ), + ) + self.assertEqual(e2.json_path, "$.root.children.a.children.ab") + + def test_additionalProperties(self): + instance = {"bar": "bar", "foo": 2} + schema = {"additionalProperties": {"type": "integer", "minimum": 5}} + + validator = validators.Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2 = sorted_errors(errors) + + self.assertEqual(e1.path, deque(["bar"])) + self.assertEqual(e2.path, deque(["foo"])) + + self.assertEqual(e1.json_path, "$.bar") + self.assertEqual(e2.json_path, "$.foo") + + self.assertEqual(e1.validator, "type") + self.assertEqual(e2.validator, "minimum") + + def test_patternProperties(self): + instance = {"bar": 1, "foo": 2} + schema = { + "patternProperties": { + "bar": {"type": "string"}, + "foo": {"minimum": 5}, + }, + } + + validator = validators.Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2 = sorted_errors(errors) + + self.assertEqual(e1.path, deque(["bar"])) + self.assertEqual(e2.path, deque(["foo"])) + + self.assertEqual(e1.json_path, "$.bar") + self.assertEqual(e2.json_path, "$.foo") + + self.assertEqual(e1.validator, "type") + self.assertEqual(e2.validator, "minimum") + + def test_additionalItems(self): + instance = ["foo", 1] + schema = { + "items": [], + "additionalItems": {"type": "integer", "minimum": 5}, + } + + validator = validators.Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2 = sorted_errors(errors) + + self.assertEqual(e1.path, deque([0])) + self.assertEqual(e2.path, deque([1])) + + self.assertEqual(e1.json_path, "$[0]") + self.assertEqual(e2.json_path, "$[1]") + + self.assertEqual(e1.validator, "type") + self.assertEqual(e2.validator, "minimum") + + def test_additionalItems_with_items(self): + instance = ["foo", "bar", 1] + schema = { + "items": [{}], + "additionalItems": {"type": "integer", "minimum": 5}, + } + + validator = validators.Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2 = sorted_errors(errors) + + self.assertEqual(e1.path, deque([1])) + self.assertEqual(e2.path, deque([2])) + + self.assertEqual(e1.json_path, "$[1]") + self.assertEqual(e2.json_path, "$[2]") + + self.assertEqual(e1.validator, "type") + self.assertEqual(e2.validator, "minimum") + + def test_propertyNames(self): + instance = {"foo": 12} + schema = {"propertyNames": {"not": {"const": "foo"}}} + + validator = validators.Draft7Validator(schema) + error, = validator.iter_errors(instance) + + self.assertEqual(error.validator, "not") + self.assertEqual( + error.message, + "'foo' should not be valid under {'const': 'foo'}", + ) + self.assertEqual(error.path, deque([])) + self.assertEqual(error.json_path, "$") + self.assertEqual(error.schema_path, deque(["propertyNames", "not"])) + + def test_if_then(self): + schema = { + "if": {"const": 12}, + "then": {"const": 13}, + } + + validator = validators.Draft7Validator(schema) + error, = validator.iter_errors(12) + + self.assertEqual(error.validator, "const") + self.assertEqual(error.message, "13 was expected") + self.assertEqual(error.path, deque([])) + self.assertEqual(error.json_path, "$") + self.assertEqual(error.schema_path, deque(["then", "const"])) + + def test_if_else(self): + schema = { + "if": {"const": 12}, + "else": {"const": 13}, + } + + validator = validators.Draft7Validator(schema) + error, = validator.iter_errors(15) + + self.assertEqual(error.validator, "const") + self.assertEqual(error.message, "13 was expected") + self.assertEqual(error.path, deque([])) + self.assertEqual(error.json_path, "$") + self.assertEqual(error.schema_path, deque(["else", "const"])) + + def test_boolean_schema_False(self): + validator = validators.Draft7Validator(False) + error, = validator.iter_errors(12) + + self.assertEqual( + ( + error.message, + error.validator, + error.validator_value, + error.instance, + error.schema, + error.schema_path, + error.json_path, + ), + ( + "False schema does not allow 12", + None, + None, + 12, + False, + deque([]), + "$", + ), + ) + + def test_ref(self): + ref, schema = "someRef", {"additionalProperties": {"type": "integer"}} + validator = validators.Draft7Validator( + {"$ref": ref}, + resolver=validators._RefResolver("", {}, store={ref: schema}), + ) + error, = validator.iter_errors({"foo": "notAnInteger"}) + + self.assertEqual( + ( + error.message, + error.validator, + error.validator_value, + error.instance, + error.absolute_path, + error.schema, + error.schema_path, + error.json_path, + ), + ( + "'notAnInteger' is not of type 'integer'", + "type", + "integer", + "notAnInteger", + deque(["foo"]), + {"type": "integer"}, + deque(["additionalProperties", "type"]), + "$.foo", + ), + ) + + def test_prefixItems(self): + schema = {"prefixItems": [{"type": "string"}, {}, {}, {"maximum": 3}]} + validator = validators.Draft202012Validator(schema) + type_error, min_error = validator.iter_errors([1, 2, "foo", 5]) + self.assertEqual( + ( + type_error.message, + type_error.validator, + type_error.validator_value, + type_error.instance, + type_error.absolute_path, + type_error.schema, + type_error.schema_path, + type_error.json_path, + ), + ( + "1 is not of type 'string'", + "type", + "string", + 1, + deque([0]), + {"type": "string"}, + deque(["prefixItems", 0, "type"]), + "$[0]", + ), + ) + self.assertEqual( + ( + min_error.message, + min_error.validator, + min_error.validator_value, + min_error.instance, + min_error.absolute_path, + min_error.schema, + min_error.schema_path, + min_error.json_path, + ), + ( + "5 is greater than the maximum of 3", + "maximum", + 3, + 5, + deque([3]), + {"maximum": 3}, + deque(["prefixItems", 3, "maximum"]), + "$[3]", + ), + ) + + def test_prefixItems_with_items(self): + schema = { + "items": {"type": "string"}, + "prefixItems": [{}], + } + validator = validators.Draft202012Validator(schema) + e1, e2 = validator.iter_errors(["foo", 2, "bar", 4, "baz"]) + self.assertEqual( + ( + e1.message, + e1.validator, + e1.validator_value, + e1.instance, + e1.absolute_path, + e1.schema, + e1.schema_path, + e1.json_path, + ), + ( + "2 is not of type 'string'", + "type", + "string", + 2, + deque([1]), + {"type": "string"}, + deque(["items", "type"]), + "$[1]", + ), + ) + self.assertEqual( + ( + e2.message, + e2.validator, + e2.validator_value, + e2.instance, + e2.absolute_path, + e2.schema, + e2.schema_path, + e2.json_path, + ), + ( + "4 is not of type 'string'", + "type", + "string", + 4, + deque([3]), + {"type": "string"}, + deque(["items", "type"]), + "$[3]", + ), + ) + + def test_contains_too_many(self): + """ + `contains` + `maxContains` produces only one error, even if there are + many more incorrectly matching elements. + """ + schema = {"contains": {"type": "string"}, "maxContains": 2} + validator = validators.Draft202012Validator(schema) + error, = validator.iter_errors(["foo", 2, "bar", 4, "baz", "quux"]) + self.assertEqual( + ( + error.message, + error.validator, + error.validator_value, + error.instance, + error.absolute_path, + error.schema, + error.schema_path, + error.json_path, + ), + ( + "Too many items match the given schema (expected at most 2)", + "maxContains", + 2, + ["foo", 2, "bar", 4, "baz", "quux"], + deque([]), + {"contains": {"type": "string"}, "maxContains": 2}, + deque(["contains"]), + "$", + ), + ) + + def test_contains_too_few(self): + schema = {"contains": {"type": "string"}, "minContains": 2} + validator = validators.Draft202012Validator(schema) + error, = validator.iter_errors(["foo", 2, 4]) + self.assertEqual( + ( + error.message, + error.validator, + error.validator_value, + error.instance, + error.absolute_path, + error.schema, + error.schema_path, + error.json_path, + ), + ( + ( + "Too few items match the given schema " + "(expected at least 2 but only 1 matched)" + ), + "minContains", + 2, + ["foo", 2, 4], + deque([]), + {"contains": {"type": "string"}, "minContains": 2}, + deque(["contains"]), + "$", + ), + ) + + def test_contains_none(self): + schema = {"contains": {"type": "string"}, "minContains": 2} + validator = validators.Draft202012Validator(schema) + error, = validator.iter_errors([2, 4]) + self.assertEqual( + ( + error.message, + error.validator, + error.validator_value, + error.instance, + error.absolute_path, + error.schema, + error.schema_path, + error.json_path, + ), + ( + "[2, 4] does not contain items matching the given schema", + "contains", + {"type": "string"}, + [2, 4], + deque([]), + {"contains": {"type": "string"}, "minContains": 2}, + deque(["contains"]), + "$", + ), + ) + + def test_ref_sibling(self): + schema = { + "$defs": {"foo": {"required": ["bar"]}}, + "properties": { + "aprop": { + "$ref": "#/$defs/foo", + "required": ["baz"], + }, + }, + } + + validator = validators.Draft202012Validator(schema) + e1, e2 = validator.iter_errors({"aprop": {}}) + self.assertEqual( + ( + e1.message, + e1.validator, + e1.validator_value, + e1.instance, + e1.absolute_path, + e1.schema, + e1.schema_path, + e1.relative_schema_path, + e1.json_path, + ), + ( + "'bar' is a required property", + "required", + ["bar"], + {}, + deque(["aprop"]), + {"required": ["bar"]}, + deque(["properties", "aprop", "required"]), + deque(["properties", "aprop", "required"]), + "$.aprop", + ), + ) + self.assertEqual( + ( + e2.message, + e2.validator, + e2.validator_value, + e2.instance, + e2.absolute_path, + e2.schema, + e2.schema_path, + e2.relative_schema_path, + e2.json_path, + ), + ( + "'baz' is a required property", + "required", + ["baz"], + {}, + deque(["aprop"]), + {"$ref": "#/$defs/foo", "required": ["baz"]}, + deque(["properties", "aprop", "required"]), + deque(["properties", "aprop", "required"]), + "$.aprop", + ), + ) + + +class MetaSchemaTestsMixin: + # TODO: These all belong upstream + def test_invalid_properties(self): + with self.assertRaises(exceptions.SchemaError): + self.Validator.check_schema({"properties": 12}) + + def test_minItems_invalid_string(self): + with self.assertRaises(exceptions.SchemaError): + # needs to be an integer + self.Validator.check_schema({"minItems": "1"}) + + def test_enum_allows_empty_arrays(self): + """ + Technically, all the spec says is they SHOULD have elements, not MUST. + + (As of Draft 6. Previous drafts do say MUST). + + See #529. + """ + if self.Validator in { + validators.Draft3Validator, + validators.Draft4Validator, + }: + with self.assertRaises(exceptions.SchemaError): + self.Validator.check_schema({"enum": []}) + else: + self.Validator.check_schema({"enum": []}) + + def test_enum_allows_non_unique_items(self): + """ + Technically, all the spec says is they SHOULD be unique, not MUST. + + (As of Draft 6. Previous drafts do say MUST). + + See #529. + """ + if self.Validator in { + validators.Draft3Validator, + validators.Draft4Validator, + }: + with self.assertRaises(exceptions.SchemaError): + self.Validator.check_schema({"enum": [12, 12]}) + else: + self.Validator.check_schema({"enum": [12, 12]}) + + def test_schema_with_invalid_regex(self): + with self.assertRaises(exceptions.SchemaError): + self.Validator.check_schema({"pattern": "*notaregex"}) + + def test_schema_with_invalid_regex_with_disabled_format_validation(self): + self.Validator.check_schema( + {"pattern": "*notaregex"}, + format_checker=None, + ) + + +class ValidatorTestMixin(MetaSchemaTestsMixin): + def test_it_implements_the_validator_protocol(self): + self.assertIsInstance(self.Validator({}), protocols.Validator) + + def test_valid_instances_are_valid(self): + schema, instance = self.valid + self.assertTrue(self.Validator(schema).is_valid(instance)) + + def test_invalid_instances_are_not_valid(self): + schema, instance = self.invalid + self.assertFalse(self.Validator(schema).is_valid(instance)) + + def test_non_existent_properties_are_ignored(self): + self.Validator({object(): object()}).validate(instance=object()) + + def test_evolve(self): + schema, format_checker = {"type": "integer"}, FormatChecker() + original = self.Validator( + schema, + format_checker=format_checker, + ) + new = original.evolve( + schema={"type": "string"}, + format_checker=self.Validator.FORMAT_CHECKER, + ) + + expected = self.Validator( + {"type": "string"}, + format_checker=self.Validator.FORMAT_CHECKER, + _resolver=new._resolver, + ) + + self.assertEqual(new, expected) + self.assertNotEqual(new, original) + + def test_evolve_with_subclass(self): + """ + Subclassing validators isn't supported public API, but some users have + done it, because we don't actually error entirely when it's done :/ + + We need to deprecate doing so first to help as many of these users + ensure they can move to supported APIs, but this test ensures that in + the interim, we haven't broken those users. + """ + + with self.assertWarns(DeprecationWarning): + @define + class OhNo(self.Validator): + foo = field(factory=lambda: [1, 2, 3]) + _bar = field(default=37) + + validator = OhNo({}, bar=12) + self.assertEqual(validator.foo, [1, 2, 3]) + + new = validator.evolve(schema={"type": "integer"}) + self.assertEqual(new.foo, [1, 2, 3]) + self.assertEqual(new._bar, 12) + + def test_is_type_is_true_for_valid_type(self): + self.assertTrue(self.Validator({}).is_type("foo", "string")) + + def test_is_type_is_false_for_invalid_type(self): + self.assertFalse(self.Validator({}).is_type("foo", "array")) + + def test_is_type_evades_bool_inheriting_from_int(self): + self.assertFalse(self.Validator({}).is_type(True, "integer")) + self.assertFalse(self.Validator({}).is_type(True, "number")) + + def test_it_can_validate_with_decimals(self): + schema = {"items": {"type": "number"}} + Validator = validators.extend( + self.Validator, + type_checker=self.Validator.TYPE_CHECKER.redefine( + "number", + lambda checker, thing: isinstance( + thing, (int, float, Decimal), + ) and not isinstance(thing, bool), + ), + ) + + validator = Validator(schema) + validator.validate([1, 1.1, Decimal(1) / Decimal(8)]) + + invalid = ["foo", {}, [], True, None] + self.assertEqual( + [error.instance for error in validator.iter_errors(invalid)], + invalid, + ) + + def test_it_returns_true_for_formats_it_does_not_know_about(self): + validator = self.Validator( + {"format": "carrot"}, format_checker=FormatChecker(), + ) + validator.validate("bugs") + + def test_it_does_not_validate_formats_by_default(self): + validator = self.Validator({}) + self.assertIsNone(validator.format_checker) + + def test_it_validates_formats_if_a_checker_is_provided(self): + checker = FormatChecker() + bad = ValueError("Bad!") + + @checker.checks("foo", raises=ValueError) + def check(value): + if value == "good": + return True + elif value == "bad": + raise bad + else: # pragma: no cover + self.fail(f"What is {value}? [Baby Don't Hurt Me]") + + validator = self.Validator( + {"format": "foo"}, format_checker=checker, + ) + + validator.validate("good") + with self.assertRaises(exceptions.ValidationError) as cm: + validator.validate("bad") + + # Make sure original cause is attached + self.assertIs(cm.exception.cause, bad) + + def test_non_string_custom_type(self): + non_string_type = object() + schema = {"type": [non_string_type]} + Crazy = validators.extend( + self.Validator, + type_checker=self.Validator.TYPE_CHECKER.redefine( + non_string_type, + lambda checker, thing: isinstance(thing, int), + ), + ) + Crazy(schema).validate(15) + + def test_it_properly_formats_tuples_in_errors(self): + """ + A tuple instance properly formats validation errors for uniqueItems. + + See #224 + """ + TupleValidator = validators.extend( + self.Validator, + type_checker=self.Validator.TYPE_CHECKER.redefine( + "array", + lambda checker, thing: isinstance(thing, tuple), + ), + ) + with self.assertRaises(exceptions.ValidationError) as e: + TupleValidator({"uniqueItems": True}).validate((1, 1)) + self.assertIn("(1, 1) has non-unique elements", str(e.exception)) + + def test_check_redefined_sequence(self): + """ + Allow array to validate against another defined sequence type + """ + schema = {"type": "array", "uniqueItems": True} + MyMapping = namedtuple("MyMapping", "a, b") + Validator = validators.extend( + self.Validator, + type_checker=self.Validator.TYPE_CHECKER.redefine_many( + { + "array": lambda checker, thing: isinstance( + thing, (list, deque), + ), + "object": lambda checker, thing: isinstance( + thing, (dict, MyMapping), + ), + }, + ), + ) + validator = Validator(schema) + + valid_instances = [ + deque(["a", None, "1", "", True]), + deque([[False], [0]]), + [deque([False]), deque([0])], + [[deque([False])], [deque([0])]], + [[[[[deque([False])]]]], [[[[deque([0])]]]]], + [deque([deque([False])]), deque([deque([0])])], + [MyMapping("a", 0), MyMapping("a", False)], + [ + MyMapping("a", [deque([0])]), + MyMapping("a", [deque([False])]), + ], + [ + MyMapping("a", [MyMapping("a", deque([0]))]), + MyMapping("a", [MyMapping("a", deque([False]))]), + ], + [deque(deque(deque([False]))), deque(deque(deque([0])))], + ] + + for instance in valid_instances: + validator.validate(instance) + + invalid_instances = [ + deque(["a", "b", "a"]), + deque([[False], [False]]), + [deque([False]), deque([False])], + [[deque([False])], [deque([False])]], + [[[[[deque([False])]]]], [[[[deque([False])]]]]], + [deque([deque([False])]), deque([deque([False])])], + [MyMapping("a", False), MyMapping("a", False)], + [ + MyMapping("a", [deque([False])]), + MyMapping("a", [deque([False])]), + ], + [ + MyMapping("a", [MyMapping("a", deque([False]))]), + MyMapping("a", [MyMapping("a", deque([False]))]), + ], + [deque(deque(deque([False]))), deque(deque(deque([False])))], + ] + + for instance in invalid_instances: + with self.assertRaises(exceptions.ValidationError): + validator.validate(instance) + + def test_it_creates_a_ref_resolver_if_not_provided(self): + with self.assertWarns(DeprecationWarning): + resolver = self.Validator({}).resolver + self.assertIsInstance(resolver, validators._RefResolver) + + def test_it_upconverts_from_deprecated_RefResolvers(self): + ref, schema = "someCoolRef", {"type": "integer"} + resolver = validators._RefResolver("", {}, store={ref: schema}) + validator = self.Validator({"$ref": ref}, resolver=resolver) + + with self.assertRaises(exceptions.ValidationError): + validator.validate(None) + + def test_it_upconverts_from_yet_older_deprecated_legacy_RefResolvers(self): + """ + Legacy RefResolvers support only the context manager form of + resolution. + """ + + class LegacyRefResolver: + @contextmanager + def resolving(this, ref): + self.assertEqual(ref, "the ref") + yield {"type": "integer"} + + resolver = LegacyRefResolver() + schema = {"$ref": "the ref"} + + with self.assertRaises(exceptions.ValidationError): + self.Validator(schema, resolver=resolver).validate(None) + + +class AntiDraft6LeakMixin: + """ + Make sure functionality from draft 6 doesn't leak backwards in time. + """ + + def test_True_is_not_a_schema(self): + with self.assertRaises(exceptions.SchemaError) as e: + self.Validator.check_schema(True) + self.assertIn("True is not of type", str(e.exception)) + + def test_False_is_not_a_schema(self): + with self.assertRaises(exceptions.SchemaError) as e: + self.Validator.check_schema(False) + self.assertIn("False is not of type", str(e.exception)) + + def test_True_is_not_a_schema_even_if_you_forget_to_check(self): + with self.assertRaises(Exception) as e: + self.Validator(True).validate(12) + self.assertNotIsInstance(e.exception, exceptions.ValidationError) + + def test_False_is_not_a_schema_even_if_you_forget_to_check(self): + with self.assertRaises(Exception) as e: + self.Validator(False).validate(12) + self.assertNotIsInstance(e.exception, exceptions.ValidationError) + + +class TestDraft3Validator(AntiDraft6LeakMixin, ValidatorTestMixin, TestCase): + Validator = validators.Draft3Validator + valid: tuple[dict, dict] = ({}, {}) + invalid = {"type": "integer"}, "foo" + + def test_any_type_is_valid_for_type_any(self): + validator = self.Validator({"type": "any"}) + validator.validate(object()) + + def test_any_type_is_redefinable(self): + """ + Sigh, because why not. + """ + Crazy = validators.extend( + self.Validator, + type_checker=self.Validator.TYPE_CHECKER.redefine( + "any", lambda checker, thing: isinstance(thing, int), + ), + ) + validator = Crazy({"type": "any"}) + validator.validate(12) + with self.assertRaises(exceptions.ValidationError): + validator.validate("foo") + + def test_is_type_is_true_for_any_type(self): + self.assertTrue(self.Validator({"type": "any"}).is_valid(object())) + + def test_is_type_does_not_evade_bool_if_it_is_being_tested(self): + self.assertTrue(self.Validator({}).is_type(True, "boolean")) + self.assertTrue(self.Validator({"type": "any"}).is_valid(True)) + + +class TestDraft4Validator(AntiDraft6LeakMixin, ValidatorTestMixin, TestCase): + Validator = validators.Draft4Validator + valid: tuple[dict, dict] = ({}, {}) + invalid = {"type": "integer"}, "foo" + + +class TestDraft6Validator(ValidatorTestMixin, TestCase): + Validator = validators.Draft6Validator + valid: tuple[dict, dict] = ({}, {}) + invalid = {"type": "integer"}, "foo" + + +class TestDraft7Validator(ValidatorTestMixin, TestCase): + Validator = validators.Draft7Validator + valid: tuple[dict, dict] = ({}, {}) + invalid = {"type": "integer"}, "foo" + + +class TestDraft201909Validator(ValidatorTestMixin, TestCase): + Validator = validators.Draft201909Validator + valid: tuple[dict, dict] = ({}, {}) + invalid = {"type": "integer"}, "foo" + + +class TestDraft202012Validator(ValidatorTestMixin, TestCase): + Validator = validators.Draft202012Validator + valid: tuple[dict, dict] = ({}, {}) + invalid = {"type": "integer"}, "foo" + + +class TestLatestValidator(TestCase): + """ + These really apply to multiple versions but are easiest to test on one. + """ + + def test_ref_resolvers_may_have_boolean_schemas_stored(self): + ref = "someCoolRef" + schema = {"$ref": ref} + resolver = validators._RefResolver("", {}, store={ref: False}) + validator = validators._LATEST_VERSION(schema, resolver=resolver) + + with self.assertRaises(exceptions.ValidationError): + validator.validate(None) + + +class TestValidatorFor(TestCase): + def test_draft_3(self): + schema = {"$schema": "http://json-schema.org/draft-03/schema"} + self.assertIs( + validators.validator_for(schema), + validators.Draft3Validator, + ) + + schema = {"$schema": "http://json-schema.org/draft-03/schema#"} + self.assertIs( + validators.validator_for(schema), + validators.Draft3Validator, + ) + + def test_draft_4(self): + schema = {"$schema": "http://json-schema.org/draft-04/schema"} + self.assertIs( + validators.validator_for(schema), + validators.Draft4Validator, + ) + + schema = {"$schema": "http://json-schema.org/draft-04/schema#"} + self.assertIs( + validators.validator_for(schema), + validators.Draft4Validator, + ) + + def test_draft_6(self): + schema = {"$schema": "http://json-schema.org/draft-06/schema"} + self.assertIs( + validators.validator_for(schema), + validators.Draft6Validator, + ) + + schema = {"$schema": "http://json-schema.org/draft-06/schema#"} + self.assertIs( + validators.validator_for(schema), + validators.Draft6Validator, + ) + + def test_draft_7(self): + schema = {"$schema": "http://json-schema.org/draft-07/schema"} + self.assertIs( + validators.validator_for(schema), + validators.Draft7Validator, + ) + + schema = {"$schema": "http://json-schema.org/draft-07/schema#"} + self.assertIs( + validators.validator_for(schema), + validators.Draft7Validator, + ) + + def test_draft_201909(self): + schema = {"$schema": "https://json-schema.org/draft/2019-09/schema"} + self.assertIs( + validators.validator_for(schema), + validators.Draft201909Validator, + ) + + schema = {"$schema": "https://json-schema.org/draft/2019-09/schema#"} + self.assertIs( + validators.validator_for(schema), + validators.Draft201909Validator, + ) + + def test_draft_202012(self): + schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"} + self.assertIs( + validators.validator_for(schema), + validators.Draft202012Validator, + ) + + schema = {"$schema": "https://json-schema.org/draft/2020-12/schema#"} + self.assertIs( + validators.validator_for(schema), + validators.Draft202012Validator, + ) + + def test_True(self): + self.assertIs( + validators.validator_for(True), + validators._LATEST_VERSION, + ) + + def test_False(self): + self.assertIs( + validators.validator_for(False), + validators._LATEST_VERSION, + ) + + def test_custom_validator(self): + Validator = validators.create( + meta_schema={"id": "meta schema id"}, + version="12", + id_of=lambda s: s.get("id", ""), + ) + schema = {"$schema": "meta schema id"} + self.assertIs( + validators.validator_for(schema), + Validator, + ) + + def test_custom_validator_draft6(self): + Validator = validators.create( + meta_schema={"$id": "meta schema $id"}, + version="13", + ) + schema = {"$schema": "meta schema $id"} + self.assertIs( + validators.validator_for(schema), + Validator, + ) + + def test_validator_for_jsonschema_default(self): + self.assertIs(validators.validator_for({}), validators._LATEST_VERSION) + + def test_validator_for_custom_default(self): + self.assertIs(validators.validator_for({}, default=None), None) + + def test_warns_if_meta_schema_specified_was_not_found(self): + with self.assertWarns(DeprecationWarning) as cm: + validators.validator_for(schema={"$schema": "unknownSchema"}) + + self.assertEqual(cm.filename, __file__) + self.assertEqual( + str(cm.warning), + "The metaschema specified by $schema was not found. " + "Using the latest draft to validate, but this will raise " + "an error in the future.", + ) + + def test_does_not_warn_if_meta_schema_is_unspecified(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + validators.validator_for(schema={}, default={}) + self.assertFalse(w) + + def test_validator_for_custom_default_with_schema(self): + schema, default = {"$schema": "mailto:foo@example.com"}, object() + self.assertIs(validators.validator_for(schema, default), default) + + +class TestValidate(TestCase): + def assertUses(self, schema, Validator): + result = [] + with mock.patch.object(Validator, "check_schema", result.append): + validators.validate({}, schema) + self.assertEqual(result, [schema]) + + def test_draft3_validator_is_chosen(self): + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-03/schema#"}, + Validator=validators.Draft3Validator, + ) + # Make sure it works without the empty fragment + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-03/schema"}, + Validator=validators.Draft3Validator, + ) + + def test_draft4_validator_is_chosen(self): + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-04/schema#"}, + Validator=validators.Draft4Validator, + ) + # Make sure it works without the empty fragment + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-04/schema"}, + Validator=validators.Draft4Validator, + ) + + def test_draft6_validator_is_chosen(self): + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-06/schema#"}, + Validator=validators.Draft6Validator, + ) + # Make sure it works without the empty fragment + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-06/schema"}, + Validator=validators.Draft6Validator, + ) + + def test_draft7_validator_is_chosen(self): + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-07/schema#"}, + Validator=validators.Draft7Validator, + ) + # Make sure it works without the empty fragment + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-07/schema"}, + Validator=validators.Draft7Validator, + ) + + def test_draft202012_validator_is_chosen(self): + self.assertUses( + schema={ + "$schema": "https://json-schema.org/draft/2020-12/schema#", + }, + Validator=validators.Draft202012Validator, + ) + # Make sure it works without the empty fragment + self.assertUses( + schema={ + "$schema": "https://json-schema.org/draft/2020-12/schema", + }, + Validator=validators.Draft202012Validator, + ) + + def test_draft202012_validator_is_the_default(self): + self.assertUses(schema={}, Validator=validators.Draft202012Validator) + + def test_validation_error_message(self): + with self.assertRaises(exceptions.ValidationError) as e: + validators.validate(12, {"type": "string"}) + self.assertRegex( + str(e.exception), + "(?s)Failed validating '.*' in schema.*On instance", + ) + + def test_schema_error_message(self): + with self.assertRaises(exceptions.SchemaError) as e: + validators.validate(12, {"type": 12}) + self.assertRegex( + str(e.exception), + "(?s)Failed validating '.*' in metaschema.*On schema", + ) + + def test_it_uses_best_match(self): + schema = { + "oneOf": [ + {"type": "number", "minimum": 20}, + {"type": "array"}, + ], + } + with self.assertRaises(exceptions.ValidationError) as e: + validators.validate(12, schema) + self.assertIn("12 is less than the minimum of 20", str(e.exception)) + + +class TestThreading(TestCase): + """ + Threading-related functionality tests. + + jsonschema doesn't promise thread safety, and its validation behavior + across multiple threads may change at any time, but that means it isn't + safe to share *validators* across threads, not that anytime one has + multiple threads that jsonschema won't work (it certainly is intended to). + + These tests ensure that this minimal level of functionality continues to + work. + """ + + def test_validation_across_a_second_thread(self): + failed = [] + + def validate(): + try: + validators.validate(instance=37, schema=True) + except: # pragma: no cover # noqa: E722 + failed.append(sys.exc_info()) + + validate() # just verify it succeeds + + from threading import Thread + thread = Thread(target=validate) + thread.start() + thread.join() + self.assertEqual((thread.is_alive(), failed), (False, [])) + + +class TestReferencing(TestCase): + def test_registry_with_retrieve(self): + def retrieve(uri): + return DRAFT202012.create_resource({"type": "integer"}) + + registry = referencing.Registry(retrieve=retrieve) + schema = {"$ref": "https://example.com/"} + validator = validators.Draft202012Validator(schema, registry=registry) + + self.assertEqual( + (validator.is_valid(12), validator.is_valid("foo")), + (True, False), + ) + + def test_custom_registries_do_not_autoretrieve_remote_resources(self): + registry = referencing.Registry() + schema = {"$ref": "https://example.com/"} + validator = validators.Draft202012Validator(schema, registry=registry) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + with self.assertRaises(referencing.exceptions.Unresolvable): + validator.validate(12) + self.assertFalse(w) + + +class TestRefResolver(TestCase): + + base_uri = "" + stored_uri = "foo://stored" + stored_schema = {"stored": "schema"} + + def setUp(self): + self.referrer = {} + self.store = {self.stored_uri: self.stored_schema} + self.resolver = validators._RefResolver( + self.base_uri, self.referrer, self.store, + ) + + def test_it_does_not_retrieve_schema_urls_from_the_network(self): + ref = validators.Draft3Validator.META_SCHEMA["id"] + with mock.patch.object(self.resolver, "resolve_remote") as patched: # noqa: SIM117 + with self.resolver.resolving(ref) as resolved: + pass + self.assertEqual(resolved, validators.Draft3Validator.META_SCHEMA) + self.assertFalse(patched.called) + + def test_it_resolves_local_refs(self): + ref = "#/properties/foo" + self.referrer["properties"] = {"foo": object()} + with self.resolver.resolving(ref) as resolved: + self.assertEqual(resolved, self.referrer["properties"]["foo"]) + + def test_it_resolves_local_refs_with_id(self): + schema = {"id": "http://bar/schema#", "a": {"foo": "bar"}} + resolver = validators._RefResolver.from_schema( + schema, + id_of=lambda schema: schema.get("id", ""), + ) + with resolver.resolving("#/a") as resolved: + self.assertEqual(resolved, schema["a"]) + with resolver.resolving("http://bar/schema#/a") as resolved: + self.assertEqual(resolved, schema["a"]) + + def test_it_retrieves_stored_refs(self): + with self.resolver.resolving(self.stored_uri) as resolved: + self.assertIs(resolved, self.stored_schema) + + self.resolver.store["cached_ref"] = {"foo": 12} + with self.resolver.resolving("cached_ref#/foo") as resolved: + self.assertEqual(resolved, 12) + + def test_it_retrieves_unstored_refs_via_requests(self): + ref = "http://bar#baz" + schema = {"baz": 12} + + if "requests" in sys.modules: # pragma: no cover + self.addCleanup( + sys.modules.__setitem__, "requests", sys.modules["requests"], + ) + sys.modules["requests"] = ReallyFakeRequests({"http://bar": schema}) + + with self.resolver.resolving(ref) as resolved: + self.assertEqual(resolved, 12) + + def test_it_retrieves_unstored_refs_via_urlopen(self): + ref = "http://bar#baz" + schema = {"baz": 12} + + if "requests" in sys.modules: # pragma: no cover + self.addCleanup( + sys.modules.__setitem__, "requests", sys.modules["requests"], + ) + sys.modules["requests"] = None + + @contextmanager + def fake_urlopen(url): + self.assertEqual(url, "http://bar") + yield BytesIO(json.dumps(schema).encode("utf8")) + + with mock.patch("urllib.request.urlopen", new=fake_urlopen): # noqa: SIM117 + with self.resolver.resolving(ref) as resolved: + pass + self.assertEqual(resolved, 12) + + def test_it_retrieves_local_refs_via_urlopen(self): + with tempfile.NamedTemporaryFile(delete=False, mode="wt") as tempf: + self.addCleanup(os.remove, tempf.name) + json.dump({"foo": "bar"}, tempf) + + ref = f"file://{pathname2url(tempf.name)}#foo" + with self.resolver.resolving(ref) as resolved: + self.assertEqual(resolved, "bar") + + def test_it_can_construct_a_base_uri_from_a_schema(self): + schema = {"id": "foo"} + resolver = validators._RefResolver.from_schema( + schema, + id_of=lambda schema: schema.get("id", ""), + ) + self.assertEqual(resolver.base_uri, "foo") + self.assertEqual(resolver.resolution_scope, "foo") + with resolver.resolving("") as resolved: + self.assertEqual(resolved, schema) + with resolver.resolving("#") as resolved: + self.assertEqual(resolved, schema) + with resolver.resolving("foo") as resolved: + self.assertEqual(resolved, schema) + with resolver.resolving("foo#") as resolved: + self.assertEqual(resolved, schema) + + def test_it_can_construct_a_base_uri_from_a_schema_without_id(self): + schema = {} + resolver = validators._RefResolver.from_schema(schema) + self.assertEqual(resolver.base_uri, "") + self.assertEqual(resolver.resolution_scope, "") + with resolver.resolving("") as resolved: + self.assertEqual(resolved, schema) + with resolver.resolving("#") as resolved: + self.assertEqual(resolved, schema) + + def test_custom_uri_scheme_handlers(self): + def handler(url): + self.assertEqual(url, ref) + return schema + + schema = {"foo": "bar"} + ref = "foo://bar" + resolver = validators._RefResolver("", {}, handlers={"foo": handler}) + with resolver.resolving(ref) as resolved: + self.assertEqual(resolved, schema) + + def test_cache_remote_on(self): + response = [object()] + + def handler(url): + try: + return response.pop() + except IndexError: # pragma: no cover + self.fail("Response must not have been cached!") + + ref = "foo://bar" + resolver = validators._RefResolver( + "", {}, cache_remote=True, handlers={"foo": handler}, + ) + with resolver.resolving(ref): + pass + with resolver.resolving(ref): + pass + + def test_cache_remote_off(self): + response = [object()] + + def handler(url): + try: + return response.pop() + except IndexError: # pragma: no cover + self.fail("Handler called twice!") + + ref = "foo://bar" + resolver = validators._RefResolver( + "", {}, cache_remote=False, handlers={"foo": handler}, + ) + with resolver.resolving(ref): + pass + + def test_if_you_give_it_junk_you_get_a_resolution_error(self): + error = ValueError("Oh no! What's this?") + + def handler(url): + raise error + + ref = "foo://bar" + resolver = validators._RefResolver("", {}, handlers={"foo": handler}) + with self.assertRaises(exceptions._RefResolutionError) as err: # noqa: SIM117 + with resolver.resolving(ref): + self.fail("Shouldn't get this far!") # pragma: no cover + self.assertEqual(err.exception, exceptions._RefResolutionError(error)) + + def test_helpful_error_message_on_failed_pop_scope(self): + resolver = validators._RefResolver("", {}) + resolver.pop_scope() + with self.assertRaises(exceptions._RefResolutionError) as exc: + resolver.pop_scope() + self.assertIn("Failed to pop the scope", str(exc.exception)) + + def test_pointer_within_schema_with_different_id(self): + """ + See #1085. + """ + schema = validators.Draft7Validator.META_SCHEMA + one = validators._RefResolver("", schema) + validator = validators.Draft7Validator(schema, resolver=one) + self.assertFalse(validator.is_valid({"maxLength": "foo"})) + + another = { + "allOf": [{"$ref": validators.Draft7Validator.META_SCHEMA["$id"]}], + } + two = validators._RefResolver("", another) + validator = validators.Draft7Validator(another, resolver=two) + self.assertFalse(validator.is_valid({"maxLength": "foo"})) + + def test_newly_created_validator_with_ref_resolver(self): + """ + See https://github.com/python-jsonschema/jsonschema/issues/1061#issuecomment-1624266555. + """ + + def handle(uri): + self.assertEqual(uri, "http://example.com/foo") + return {"type": "integer"} + + resolver = validators._RefResolver("", {}, handlers={"http": handle}) + Validator = validators.create( + meta_schema={}, + validators=validators.Draft4Validator.VALIDATORS, + ) + schema = {"$id": "http://example.com/bar", "$ref": "foo"} + validator = Validator(schema, resolver=resolver) + self.assertEqual( + (validator.is_valid({}), validator.is_valid(37)), + (False, True), + ) + + def test_refresolver_with_pointer_in_schema_with_no_id(self): + """ + See https://github.com/python-jsonschema/jsonschema/issues/1124#issuecomment-1632574249. + """ + + schema = { + "properties": {"x": {"$ref": "#/definitions/x"}}, + "definitions": {"x": {"type": "integer"}}, + } + + validator = validators.Draft202012Validator( + schema, + resolver=validators._RefResolver("", schema), + ) + self.assertEqual( + (validator.is_valid({"x": "y"}), validator.is_valid({"x": 37})), + (False, True), + ) + + + +def sorted_errors(errors): + def key(error): + return ( + [str(e) for e in error.path], + [str(e) for e in error.schema_path], + ) + return sorted(errors, key=key) + + +@define +class ReallyFakeRequests: + + _responses: dict[str, Any] + + def get(self, url): + response = self._responses.get(url) + if url is None: # pragma: no cover + raise ValueError("Unknown URL: " + repr(url)) + return _ReallyFakeJSONResponse(json.dumps(response)) + + +@define +class _ReallyFakeJSONResponse: + + _response: str + + def json(self): + return json.loads(self._response) diff --git a/source/target-vendors/cp312/jsonschema/tests/typing/__init__.py b/source/target-vendors/cp312/jsonschema/tests/typing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/jsonschema/tests/typing/test_all_concrete_validators_match_protocol.py b/source/target-vendors/cp312/jsonschema/tests/typing/test_all_concrete_validators_match_protocol.py new file mode 100644 index 0000000..63e8bd4 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/tests/typing/test_all_concrete_validators_match_protocol.py @@ -0,0 +1,38 @@ +""" +This module acts as a test that type checkers will allow each validator +class to be assigned to a variable of type `type[Validator]` + +The assignation is only valid if type checkers recognize each Validator +implementation as a valid implementer of the protocol. +""" +from jsonschema.protocols import Validator +from jsonschema.validators import ( + Draft3Validator, + Draft4Validator, + Draft6Validator, + Draft7Validator, + Draft201909Validator, + Draft202012Validator, +) + +my_validator: type[Validator] + +my_validator = Draft3Validator +my_validator = Draft4Validator +my_validator = Draft6Validator +my_validator = Draft7Validator +my_validator = Draft201909Validator +my_validator = Draft202012Validator + + +# in order to confirm that none of the above were incorrectly typed as 'Any' +# ensure that each of these assignments to a non-validator variable requires an +# ignore +none_var: None + +none_var = Draft3Validator # type: ignore[assignment] +none_var = Draft4Validator # type: ignore[assignment] +none_var = Draft6Validator # type: ignore[assignment] +none_var = Draft7Validator # type: ignore[assignment] +none_var = Draft201909Validator # type: ignore[assignment] +none_var = Draft202012Validator # type: ignore[assignment] diff --git a/source/target-vendors/cp312/jsonschema/validators.py b/source/target-vendors/cp312/jsonschema/validators.py new file mode 100644 index 0000000..98ddf6b --- /dev/null +++ b/source/target-vendors/cp312/jsonschema/validators.py @@ -0,0 +1,1410 @@ +""" +Creation and extension of validators, with implementations for existing drafts. +""" +from __future__ import annotations + +from collections import deque +from collections.abc import Iterable, Mapping, Sequence +from functools import lru_cache +from operator import methodcaller +from typing import TYPE_CHECKING +from urllib.parse import unquote, urldefrag, urljoin, urlsplit +from warnings import warn +import contextlib +import json +import reprlib +import warnings + +from attrs import define, field, fields +from jsonschema_specifications import REGISTRY as SPECIFICATIONS +from rpds import HashTrieMap +import referencing.exceptions +import referencing.jsonschema + +from jsonschema import ( + _format, + _keywords, + _legacy_keywords, + _types, + _typing, + _utils, + exceptions, +) + +if TYPE_CHECKING: + from jsonschema.protocols import Validator + +_UNSET = _utils.Unset() + +_VALIDATORS: dict[str, Validator] = {} +_META_SCHEMAS = _utils.URIDict() + + +def __getattr__(name): + if name == "ErrorTree": + warnings.warn( + "Importing ErrorTree from jsonschema.validators is deprecated. " + "Instead import it from jsonschema.exceptions.", + DeprecationWarning, + stacklevel=2, + ) + from jsonschema.exceptions import ErrorTree + return ErrorTree + elif name == "validators": + warnings.warn( + "Accessing jsonschema.validators.validators is deprecated. " + "Use jsonschema.validators.validator_for with a given schema.", + DeprecationWarning, + stacklevel=2, + ) + return _VALIDATORS + elif name == "meta_schemas": + warnings.warn( + "Accessing jsonschema.validators.meta_schemas is deprecated. " + "Use jsonschema.validators.validator_for with a given schema.", + DeprecationWarning, + stacklevel=2, + ) + return _META_SCHEMAS + elif name == "RefResolver": + warnings.warn( + _RefResolver._DEPRECATION_MESSAGE, + DeprecationWarning, + stacklevel=2, + ) + return _RefResolver + raise AttributeError(f"module {__name__} has no attribute {name}") + + +def validates(version): + """ + Register the decorated validator for a ``version`` of the specification. + + Registered validators and their meta schemas will be considered when + parsing :kw:`$schema` keywords' URIs. + + Arguments: + + version (str): + + An identifier to use as the version's name + + Returns: + + collections.abc.Callable: + + a class decorator to decorate the validator with the version + + """ + + def _validates(cls): + _VALIDATORS[version] = cls + meta_schema_id = cls.ID_OF(cls.META_SCHEMA) + _META_SCHEMAS[meta_schema_id] = cls + return cls + return _validates + + +def _warn_for_remote_retrieve(uri: str): + from urllib.request import Request, urlopen + headers = {"User-Agent": "python-jsonschema (deprecated $ref resolution)"} + request = Request(uri, headers=headers) # noqa: S310 + with urlopen(request) as response: # noqa: S310 + warnings.warn( + "Automatically retrieving remote references can be a security " + "vulnerability and is discouraged by the JSON Schema " + "specifications. Relying on this behavior is deprecated " + "and will shortly become an error. If you are sure you want to " + "remotely retrieve your reference and that it is safe to do so, " + "you can find instructions for doing so via referencing.Registry " + "in the referencing documentation " + "(https://referencing.readthedocs.org).", + DeprecationWarning, + stacklevel=9, # Ha ha ha ha magic numbers :/ + ) + return referencing.Resource.from_contents( + json.load(response), + default_specification=referencing.jsonschema.DRAFT202012, + ) + + +_REMOTE_WARNING_REGISTRY = SPECIFICATIONS.combine( + referencing.Registry(retrieve=_warn_for_remote_retrieve), # type: ignore[call-arg] +) + + +def create( + meta_schema: referencing.jsonschema.ObjectSchema, + validators: ( + Mapping[str, _typing.SchemaKeywordValidator] + | Iterable[tuple[str, _typing.SchemaKeywordValidator]] + ) = (), + version: str | None = None, + type_checker: _types.TypeChecker = _types.draft202012_type_checker, + format_checker: _format.FormatChecker = _format.draft202012_format_checker, + id_of: _typing.id_of = referencing.jsonschema.DRAFT202012.id_of, + applicable_validators: _typing.ApplicableValidators = methodcaller( + "items", + ), +) -> type[Validator]: + """ + Create a new validator class. + + Arguments: + + meta_schema: + + the meta schema for the new validator class + + validators: + + a mapping from names to callables, where each callable will + validate the schema property with the given name. + + Each callable should take 4 arguments: + + 1. a validator instance, + 2. the value of the property being validated within the + instance + 3. the instance + 4. the schema + + version: + + an identifier for the version that this validator class will + validate. If provided, the returned validator class will + have its ``__name__`` set to include the version, and also + will have `jsonschema.validators.validates` automatically + called for the given version. + + type_checker: + + a type checker, used when applying the :kw:`type` keyword. + + If unprovided, a `jsonschema.TypeChecker` will be created + with a set of default types typical of JSON Schema drafts. + + format_checker: + + a format checker, used when applying the :kw:`format` keyword. + + If unprovided, a `jsonschema.FormatChecker` will be created + with a set of default formats typical of JSON Schema drafts. + + id_of: + + A function that given a schema, returns its ID. + + applicable_validators: + + A function that, given a schema, returns the list of + applicable schema keywords and associated values + which will be used to validate the instance. + This is mostly used to support pre-draft 7 versions of JSON Schema + which specified behavior around ignoring keywords if they were + siblings of a ``$ref`` keyword. If you're not attempting to + implement similar behavior, you can typically ignore this argument + and leave it at its default. + + Returns: + + a new `jsonschema.protocols.Validator` class + + """ + # preemptively don't shadow the `Validator.format_checker` local + format_checker_arg = format_checker + + specification = referencing.jsonschema.specification_with( + dialect_id=id_of(meta_schema) or "urn:unknown-dialect", + default=referencing.Specification.OPAQUE, + ) + + @define + class Validator: + + VALIDATORS = dict(validators) # noqa: RUF012 + META_SCHEMA = dict(meta_schema) # noqa: RUF012 + TYPE_CHECKER = type_checker + FORMAT_CHECKER = format_checker_arg + ID_OF = staticmethod(id_of) + + _APPLICABLE_VALIDATORS = applicable_validators + _validators = field(init=False, repr=False, eq=False) + + schema: referencing.jsonschema.Schema = field(repr=reprlib.repr) + _ref_resolver = field(default=None, repr=False, alias="resolver") + format_checker: _format.FormatChecker | None = field(default=None) + # TODO: include new meta-schemas added at runtime + _registry: referencing.jsonschema.SchemaRegistry = field( + default=_REMOTE_WARNING_REGISTRY, + kw_only=True, + repr=False, + ) + _resolver = field( + alias="_resolver", + default=None, + kw_only=True, + repr=False, + ) + + def __init_subclass__(cls): + warnings.warn( + ( + "Subclassing validator classes is not intended to " + "be part of their public API. A future version " + "will make doing so an error, as the behavior of " + "subclasses isn't guaranteed to stay the same " + "between releases of jsonschema. Instead, prefer " + "composition of validators, wrapping them in an object " + "owned entirely by the downstream library." + ), + DeprecationWarning, + stacklevel=2, + ) + + def evolve(self, **changes): + cls = self.__class__ + schema = changes.setdefault("schema", self.schema) + NewValidator = validator_for(schema, default=cls) + + for field in fields(cls): # noqa: F402 + if not field.init: + continue + attr_name = field.name + init_name = field.alias + if init_name not in changes: + changes[init_name] = getattr(self, attr_name) + + return NewValidator(**changes) + + cls.evolve = evolve + + def __attrs_post_init__(self): + if self._resolver is None: + registry = self._registry + if registry is not _REMOTE_WARNING_REGISTRY: + registry = SPECIFICATIONS.combine(registry) + resource = specification.create_resource(self.schema) + self._resolver = registry.resolver_with_root(resource) + + if self.schema is True or self.schema is False: + self._validators = [] + else: + self._validators = [ + (self.VALIDATORS[k], k, v) + for k, v in applicable_validators(self.schema) + if k in self.VALIDATORS + ] + + # REMOVEME: Legacy ref resolution state management. + push_scope = getattr(self._ref_resolver, "push_scope", None) + if push_scope is not None: + id = id_of(self.schema) + if id is not None: + push_scope(id) + + @classmethod + def check_schema(cls, schema, format_checker=_UNSET): + Validator = validator_for(cls.META_SCHEMA, default=cls) + if format_checker is _UNSET: + format_checker = Validator.FORMAT_CHECKER + validator = Validator( + schema=cls.META_SCHEMA, + format_checker=format_checker, + ) + for error in validator.iter_errors(schema): + raise exceptions.SchemaError.create_from(error) + + @property + def resolver(self): + warnings.warn( + ( + f"Accessing {self.__class__.__name__}.resolver is " + "deprecated as of v4.18.0, in favor of the " + "https://github.com/python-jsonschema/referencing " + "library, which provides more compliant referencing " + "behavior as well as more flexible APIs for " + "customization." + ), + DeprecationWarning, + stacklevel=2, + ) + if self._ref_resolver is None: + self._ref_resolver = _RefResolver.from_schema( + self.schema, + id_of=id_of, + ) + return self._ref_resolver + + def evolve(self, **changes): + schema = changes.setdefault("schema", self.schema) + NewValidator = validator_for(schema, default=self.__class__) + + for (attr_name, init_name) in evolve_fields: + if init_name not in changes: + changes[init_name] = getattr(self, attr_name) + + return NewValidator(**changes) + + def iter_errors(self, instance, _schema=None): + if _schema is not None: + warnings.warn( + ( + "Passing a schema to Validator.iter_errors " + "is deprecated and will be removed in a future " + "release. Call validator.evolve(schema=new_schema)." + "iter_errors(...) instead." + ), + DeprecationWarning, + stacklevel=2, + ) + validators = [ + (self.VALIDATORS[k], k, v) + for k, v in applicable_validators(_schema) + if k in self.VALIDATORS + ] + else: + _schema, validators = self.schema, self._validators + + if _schema is True: + return + elif _schema is False: + yield exceptions.ValidationError( + f"False schema does not allow {instance!r}", + validator=None, + validator_value=None, + instance=instance, + schema=_schema, + ) + return + + for validator, k, v in validators: + errors = validator(self, v, instance, _schema) or () + for error in errors: + # set details if not already set by the called fn + error._set( + validator=k, + validator_value=v, + instance=instance, + schema=_schema, + type_checker=self.TYPE_CHECKER, + ) + if k not in {"if", "$ref"}: + error.schema_path.appendleft(k) + yield error + + def descend( + self, + instance, + schema, + path=None, + schema_path=None, + resolver=None, + ): + if schema is True: + return + elif schema is False: + yield exceptions.ValidationError( + f"False schema does not allow {instance!r}", + validator=None, + validator_value=None, + instance=instance, + schema=schema, + ) + return + + if self._ref_resolver is not None: + evolved = self.evolve(schema=schema) + else: + if resolver is None: + resolver = self._resolver.in_subresource( + specification.create_resource(schema), + ) + evolved = self.evolve(schema=schema, _resolver=resolver) + + for k, v in applicable_validators(schema): + validator = evolved.VALIDATORS.get(k) + if validator is None: + continue + + errors = validator(evolved, v, instance, schema) or () + for error in errors: + # set details if not already set by the called fn + error._set( + validator=k, + validator_value=v, + instance=instance, + schema=schema, + type_checker=evolved.TYPE_CHECKER, + ) + if k not in {"if", "$ref"}: + error.schema_path.appendleft(k) + if path is not None: + error.path.appendleft(path) + if schema_path is not None: + error.schema_path.appendleft(schema_path) + yield error + + def validate(self, *args, **kwargs): + for error in self.iter_errors(*args, **kwargs): + raise error + + def is_type(self, instance, type): + try: + return self.TYPE_CHECKER.is_type(instance, type) + except exceptions.UndefinedTypeCheck: + exc = exceptions.UnknownType(type, instance, self.schema) + raise exc from None + + def _validate_reference(self, ref, instance): + if self._ref_resolver is None: + try: + resolved = self._resolver.lookup(ref) + except referencing.exceptions.Unresolvable as err: + raise exceptions._WrappedReferencingError(err) from err + + return self.descend( + instance, + resolved.contents, + resolver=resolved.resolver, + ) + else: + resolve = getattr(self._ref_resolver, "resolve", None) + if resolve is None: + with self._ref_resolver.resolving(ref) as resolved: + return self.descend(instance, resolved) + else: + scope, resolved = resolve(ref) + self._ref_resolver.push_scope(scope) + + try: + return list(self.descend(instance, resolved)) + finally: + self._ref_resolver.pop_scope() + + def is_valid(self, instance, _schema=None): + if _schema is not None: + warnings.warn( + ( + "Passing a schema to Validator.is_valid is deprecated " + "and will be removed in a future release. Call " + "validator.evolve(schema=new_schema).is_valid(...) " + "instead." + ), + DeprecationWarning, + stacklevel=2, + ) + self = self.evolve(schema=_schema) + + error = next(self.iter_errors(instance), None) + return error is None + + evolve_fields = [ + (field.name, field.alias) + for field in fields(Validator) + if field.init + ] + + if version is not None: + safe = version.title().replace(" ", "").replace("-", "") + Validator.__name__ = Validator.__qualname__ = f"{safe}Validator" + Validator = validates(version)(Validator) # type: ignore[misc] + + return Validator # type: ignore[return-value] + + +def extend( + validator, + validators=(), + version=None, + type_checker=None, + format_checker=None, +): + """ + Create a new validator class by extending an existing one. + + Arguments: + + validator (jsonschema.protocols.Validator): + + an existing validator class + + validators (collections.abc.Mapping): + + a mapping of new validator callables to extend with, whose + structure is as in `create`. + + .. note:: + + Any validator callables with the same name as an + existing one will (silently) replace the old validator + callable entirely, effectively overriding any validation + done in the "parent" validator class. + + If you wish to instead extend the behavior of a parent's + validator callable, delegate and call it directly in + the new validator function by retrieving it using + ``OldValidator.VALIDATORS["validation_keyword_name"]``. + + version (str): + + a version for the new validator class + + type_checker (jsonschema.TypeChecker): + + a type checker, used when applying the :kw:`type` keyword. + + If unprovided, the type checker of the extended + `jsonschema.protocols.Validator` will be carried along. + + format_checker (jsonschema.FormatChecker): + + a format checker, used when applying the :kw:`format` keyword. + + If unprovided, the format checker of the extended + `jsonschema.protocols.Validator` will be carried along. + + Returns: + + a new `jsonschema.protocols.Validator` class extending the one + provided + + .. note:: Meta Schemas + + The new validator class will have its parent's meta schema. + + If you wish to change or extend the meta schema in the new + validator class, modify ``META_SCHEMA`` directly on the returned + class. Note that no implicit copying is done, so a copy should + likely be made before modifying it, in order to not affect the + old validator. + + """ + all_validators = dict(validator.VALIDATORS) + all_validators.update(validators) + + if type_checker is None: + type_checker = validator.TYPE_CHECKER + if format_checker is None: + format_checker = validator.FORMAT_CHECKER + return create( + meta_schema=validator.META_SCHEMA, + validators=all_validators, + version=version, + type_checker=type_checker, + format_checker=format_checker, + id_of=validator.ID_OF, + applicable_validators=validator._APPLICABLE_VALIDATORS, + ) + + +Draft3Validator = create( + meta_schema=SPECIFICATIONS.contents( + "http://json-schema.org/draft-03/schema#", + ), + validators={ + "$ref": _keywords.ref, + "additionalItems": _legacy_keywords.additionalItems, + "additionalProperties": _keywords.additionalProperties, + "dependencies": _legacy_keywords.dependencies_draft3, + "disallow": _legacy_keywords.disallow_draft3, + "divisibleBy": _keywords.multipleOf, + "enum": _keywords.enum, + "extends": _legacy_keywords.extends_draft3, + "format": _keywords.format, + "items": _legacy_keywords.items_draft3_draft4, + "maxItems": _keywords.maxItems, + "maxLength": _keywords.maxLength, + "maximum": _legacy_keywords.maximum_draft3_draft4, + "minItems": _keywords.minItems, + "minLength": _keywords.minLength, + "minimum": _legacy_keywords.minimum_draft3_draft4, + "pattern": _keywords.pattern, + "patternProperties": _keywords.patternProperties, + "properties": _legacy_keywords.properties_draft3, + "type": _legacy_keywords.type_draft3, + "uniqueItems": _keywords.uniqueItems, + }, + type_checker=_types.draft3_type_checker, + format_checker=_format.draft3_format_checker, + version="draft3", + id_of=referencing.jsonschema.DRAFT3.id_of, + applicable_validators=_legacy_keywords.ignore_ref_siblings, +) + +Draft4Validator = create( + meta_schema=SPECIFICATIONS.contents( + "http://json-schema.org/draft-04/schema#", + ), + validators={ + "$ref": _keywords.ref, + "additionalItems": _legacy_keywords.additionalItems, + "additionalProperties": _keywords.additionalProperties, + "allOf": _keywords.allOf, + "anyOf": _keywords.anyOf, + "dependencies": _legacy_keywords.dependencies_draft4_draft6_draft7, + "enum": _keywords.enum, + "format": _keywords.format, + "items": _legacy_keywords.items_draft3_draft4, + "maxItems": _keywords.maxItems, + "maxLength": _keywords.maxLength, + "maxProperties": _keywords.maxProperties, + "maximum": _legacy_keywords.maximum_draft3_draft4, + "minItems": _keywords.minItems, + "minLength": _keywords.minLength, + "minProperties": _keywords.minProperties, + "minimum": _legacy_keywords.minimum_draft3_draft4, + "multipleOf": _keywords.multipleOf, + "not": _keywords.not_, + "oneOf": _keywords.oneOf, + "pattern": _keywords.pattern, + "patternProperties": _keywords.patternProperties, + "properties": _keywords.properties, + "required": _keywords.required, + "type": _keywords.type, + "uniqueItems": _keywords.uniqueItems, + }, + type_checker=_types.draft4_type_checker, + format_checker=_format.draft4_format_checker, + version="draft4", + id_of=referencing.jsonschema.DRAFT4.id_of, + applicable_validators=_legacy_keywords.ignore_ref_siblings, +) + +Draft6Validator = create( + meta_schema=SPECIFICATIONS.contents( + "http://json-schema.org/draft-06/schema#", + ), + validators={ + "$ref": _keywords.ref, + "additionalItems": _legacy_keywords.additionalItems, + "additionalProperties": _keywords.additionalProperties, + "allOf": _keywords.allOf, + "anyOf": _keywords.anyOf, + "const": _keywords.const, + "contains": _legacy_keywords.contains_draft6_draft7, + "dependencies": _legacy_keywords.dependencies_draft4_draft6_draft7, + "enum": _keywords.enum, + "exclusiveMaximum": _keywords.exclusiveMaximum, + "exclusiveMinimum": _keywords.exclusiveMinimum, + "format": _keywords.format, + "items": _legacy_keywords.items_draft6_draft7_draft201909, + "maxItems": _keywords.maxItems, + "maxLength": _keywords.maxLength, + "maxProperties": _keywords.maxProperties, + "maximum": _keywords.maximum, + "minItems": _keywords.minItems, + "minLength": _keywords.minLength, + "minProperties": _keywords.minProperties, + "minimum": _keywords.minimum, + "multipleOf": _keywords.multipleOf, + "not": _keywords.not_, + "oneOf": _keywords.oneOf, + "pattern": _keywords.pattern, + "patternProperties": _keywords.patternProperties, + "properties": _keywords.properties, + "propertyNames": _keywords.propertyNames, + "required": _keywords.required, + "type": _keywords.type, + "uniqueItems": _keywords.uniqueItems, + }, + type_checker=_types.draft6_type_checker, + format_checker=_format.draft6_format_checker, + version="draft6", + id_of=referencing.jsonschema.DRAFT6.id_of, + applicable_validators=_legacy_keywords.ignore_ref_siblings, +) + +Draft7Validator = create( + meta_schema=SPECIFICATIONS.contents( + "http://json-schema.org/draft-07/schema#", + ), + validators={ + "$ref": _keywords.ref, + "additionalItems": _legacy_keywords.additionalItems, + "additionalProperties": _keywords.additionalProperties, + "allOf": _keywords.allOf, + "anyOf": _keywords.anyOf, + "const": _keywords.const, + "contains": _legacy_keywords.contains_draft6_draft7, + "dependencies": _legacy_keywords.dependencies_draft4_draft6_draft7, + "enum": _keywords.enum, + "exclusiveMaximum": _keywords.exclusiveMaximum, + "exclusiveMinimum": _keywords.exclusiveMinimum, + "format": _keywords.format, + "if": _keywords.if_, + "items": _legacy_keywords.items_draft6_draft7_draft201909, + "maxItems": _keywords.maxItems, + "maxLength": _keywords.maxLength, + "maxProperties": _keywords.maxProperties, + "maximum": _keywords.maximum, + "minItems": _keywords.minItems, + "minLength": _keywords.minLength, + "minProperties": _keywords.minProperties, + "minimum": _keywords.minimum, + "multipleOf": _keywords.multipleOf, + "not": _keywords.not_, + "oneOf": _keywords.oneOf, + "pattern": _keywords.pattern, + "patternProperties": _keywords.patternProperties, + "properties": _keywords.properties, + "propertyNames": _keywords.propertyNames, + "required": _keywords.required, + "type": _keywords.type, + "uniqueItems": _keywords.uniqueItems, + }, + type_checker=_types.draft7_type_checker, + format_checker=_format.draft7_format_checker, + version="draft7", + id_of=referencing.jsonschema.DRAFT7.id_of, + applicable_validators=_legacy_keywords.ignore_ref_siblings, +) + +Draft201909Validator = create( + meta_schema=SPECIFICATIONS.contents( + "https://json-schema.org/draft/2019-09/schema", + ), + validators={ + "$recursiveRef": _legacy_keywords.recursiveRef, + "$ref": _keywords.ref, + "additionalItems": _legacy_keywords.additionalItems, + "additionalProperties": _keywords.additionalProperties, + "allOf": _keywords.allOf, + "anyOf": _keywords.anyOf, + "const": _keywords.const, + "contains": _keywords.contains, + "dependentRequired": _keywords.dependentRequired, + "dependentSchemas": _keywords.dependentSchemas, + "enum": _keywords.enum, + "exclusiveMaximum": _keywords.exclusiveMaximum, + "exclusiveMinimum": _keywords.exclusiveMinimum, + "format": _keywords.format, + "if": _keywords.if_, + "items": _legacy_keywords.items_draft6_draft7_draft201909, + "maxItems": _keywords.maxItems, + "maxLength": _keywords.maxLength, + "maxProperties": _keywords.maxProperties, + "maximum": _keywords.maximum, + "minItems": _keywords.minItems, + "minLength": _keywords.minLength, + "minProperties": _keywords.minProperties, + "minimum": _keywords.minimum, + "multipleOf": _keywords.multipleOf, + "not": _keywords.not_, + "oneOf": _keywords.oneOf, + "pattern": _keywords.pattern, + "patternProperties": _keywords.patternProperties, + "properties": _keywords.properties, + "propertyNames": _keywords.propertyNames, + "required": _keywords.required, + "type": _keywords.type, + "unevaluatedItems": _legacy_keywords.unevaluatedItems_draft2019, + "unevaluatedProperties": ( + _legacy_keywords.unevaluatedProperties_draft2019 + ), + "uniqueItems": _keywords.uniqueItems, + }, + type_checker=_types.draft201909_type_checker, + format_checker=_format.draft201909_format_checker, + version="draft2019-09", +) + +Draft202012Validator = create( + meta_schema=SPECIFICATIONS.contents( + "https://json-schema.org/draft/2020-12/schema", + ), + validators={ + "$dynamicRef": _keywords.dynamicRef, + "$ref": _keywords.ref, + "additionalProperties": _keywords.additionalProperties, + "allOf": _keywords.allOf, + "anyOf": _keywords.anyOf, + "const": _keywords.const, + "contains": _keywords.contains, + "dependentRequired": _keywords.dependentRequired, + "dependentSchemas": _keywords.dependentSchemas, + "enum": _keywords.enum, + "exclusiveMaximum": _keywords.exclusiveMaximum, + "exclusiveMinimum": _keywords.exclusiveMinimum, + "format": _keywords.format, + "if": _keywords.if_, + "items": _keywords.items, + "maxItems": _keywords.maxItems, + "maxLength": _keywords.maxLength, + "maxProperties": _keywords.maxProperties, + "maximum": _keywords.maximum, + "minItems": _keywords.minItems, + "minLength": _keywords.minLength, + "minProperties": _keywords.minProperties, + "minimum": _keywords.minimum, + "multipleOf": _keywords.multipleOf, + "not": _keywords.not_, + "oneOf": _keywords.oneOf, + "pattern": _keywords.pattern, + "patternProperties": _keywords.patternProperties, + "prefixItems": _keywords.prefixItems, + "properties": _keywords.properties, + "propertyNames": _keywords.propertyNames, + "required": _keywords.required, + "type": _keywords.type, + "unevaluatedItems": _keywords.unevaluatedItems, + "unevaluatedProperties": _keywords.unevaluatedProperties, + "uniqueItems": _keywords.uniqueItems, + }, + type_checker=_types.draft202012_type_checker, + format_checker=_format.draft202012_format_checker, + version="draft2020-12", +) + +_LATEST_VERSION: type[Validator] = Draft202012Validator + + +class _RefResolver: + """ + Resolve JSON References. + + Arguments: + + base_uri (str): + + The URI of the referring document + + referrer: + + The actual referring document + + store (dict): + + A mapping from URIs to documents to cache + + cache_remote (bool): + + Whether remote refs should be cached after first resolution + + handlers (dict): + + A mapping from URI schemes to functions that should be used + to retrieve them + + urljoin_cache (:func:`functools.lru_cache`): + + A cache that will be used for caching the results of joining + the resolution scope to subscopes. + + remote_cache (:func:`functools.lru_cache`): + + A cache that will be used for caching the results of + resolved remote URLs. + + Attributes: + + cache_remote (bool): + + Whether remote refs should be cached after first resolution + + .. deprecated:: v4.18.0 + + ``RefResolver`` has been deprecated in favor of `referencing`. + + """ + + _DEPRECATION_MESSAGE = ( + "jsonschema.RefResolver is deprecated as of v4.18.0, in favor of the " + "https://github.com/python-jsonschema/referencing library, which " + "provides more compliant referencing behavior as well as more " + "flexible APIs for customization. A future release will remove " + "RefResolver. Please file a feature request (on referencing) if you " + "are missing an API for the kind of customization you need." + ) + + def __init__( + self, + base_uri, + referrer, + store=HashTrieMap(), + cache_remote=True, + handlers=(), + urljoin_cache=None, + remote_cache=None, + ): + if urljoin_cache is None: + urljoin_cache = lru_cache(1024)(urljoin) + if remote_cache is None: + remote_cache = lru_cache(1024)(self.resolve_from_url) + + self.referrer = referrer + self.cache_remote = cache_remote + self.handlers = dict(handlers) + + self._scopes_stack = [base_uri] + + self.store = _utils.URIDict( + (uri, each.contents) for uri, each in SPECIFICATIONS.items() + ) + self.store.update( + (id, each.META_SCHEMA) for id, each in _META_SCHEMAS.items() + ) + self.store.update(store) + self.store.update( + (schema["$id"], schema) + for schema in store.values() + if isinstance(schema, Mapping) and "$id" in schema + ) + self.store[base_uri] = referrer + + self._urljoin_cache = urljoin_cache + self._remote_cache = remote_cache + + @classmethod + def from_schema( # noqa: D417 + cls, + schema, + id_of=referencing.jsonschema.DRAFT202012.id_of, + *args, + **kwargs, + ): + """ + Construct a resolver from a JSON schema object. + + Arguments: + + schema: + + the referring schema + + Returns: + + `_RefResolver` + + """ + return cls(base_uri=id_of(schema) or "", referrer=schema, *args, **kwargs) # noqa: B026, E501 + + def push_scope(self, scope): + """ + Enter a given sub-scope. + + Treats further dereferences as being performed underneath the + given scope. + """ + self._scopes_stack.append( + self._urljoin_cache(self.resolution_scope, scope), + ) + + def pop_scope(self): + """ + Exit the most recent entered scope. + + Treats further dereferences as being performed underneath the + original scope. + + Don't call this method more times than `push_scope` has been + called. + """ + try: + self._scopes_stack.pop() + except IndexError: + raise exceptions._RefResolutionError( + "Failed to pop the scope from an empty stack. " + "`pop_scope()` should only be called once for every " + "`push_scope()`", + ) from None + + @property + def resolution_scope(self): + """ + Retrieve the current resolution scope. + """ + return self._scopes_stack[-1] + + @property + def base_uri(self): + """ + Retrieve the current base URI, not including any fragment. + """ + uri, _ = urldefrag(self.resolution_scope) + return uri + + @contextlib.contextmanager + def in_scope(self, scope): + """ + Temporarily enter the given scope for the duration of the context. + + .. deprecated:: v4.0.0 + """ + warnings.warn( + "jsonschema.RefResolver.in_scope is deprecated and will be " + "removed in a future release.", + DeprecationWarning, + stacklevel=3, + ) + self.push_scope(scope) + try: + yield + finally: + self.pop_scope() + + @contextlib.contextmanager + def resolving(self, ref): + """ + Resolve the given ``ref`` and enter its resolution scope. + + Exits the scope on exit of this context manager. + + Arguments: + + ref (str): + + The reference to resolve + + """ + url, resolved = self.resolve(ref) + self.push_scope(url) + try: + yield resolved + finally: + self.pop_scope() + + def _find_in_referrer(self, key): + return self._get_subschemas_cache()[key] + + @lru_cache # noqa: B019 + def _get_subschemas_cache(self): + cache = {key: [] for key in _SUBSCHEMAS_KEYWORDS} + for keyword, subschema in _search_schema( + self.referrer, _match_subschema_keywords, + ): + cache[keyword].append(subschema) + return cache + + @lru_cache # noqa: B019 + def _find_in_subschemas(self, url): + subschemas = self._get_subschemas_cache()["$id"] + if not subschemas: + return None + uri, fragment = urldefrag(url) + for subschema in subschemas: + id = subschema["$id"] + if not isinstance(id, str): + continue + target_uri = self._urljoin_cache(self.resolution_scope, id) + if target_uri.rstrip("/") == uri.rstrip("/"): + if fragment: + subschema = self.resolve_fragment(subschema, fragment) + self.store[url] = subschema + return url, subschema + return None + + def resolve(self, ref): + """ + Resolve the given reference. + """ + url = self._urljoin_cache(self.resolution_scope, ref).rstrip("/") + + match = self._find_in_subschemas(url) + if match is not None: + return match + + return url, self._remote_cache(url) + + def resolve_from_url(self, url): + """ + Resolve the given URL. + """ + url, fragment = urldefrag(url) + if not url: + url = self.base_uri + + try: + document = self.store[url] + except KeyError: + try: + document = self.resolve_remote(url) + except Exception as exc: + raise exceptions._RefResolutionError(exc) from exc + + return self.resolve_fragment(document, fragment) + + def resolve_fragment(self, document, fragment): + """ + Resolve a ``fragment`` within the referenced ``document``. + + Arguments: + + document: + + The referent document + + fragment (str): + + a URI fragment to resolve within it + + """ + fragment = fragment.lstrip("/") + + if not fragment: + return document + + if document is self.referrer: + find = self._find_in_referrer + else: + + def find(key): + yield from _search_schema(document, _match_keyword(key)) + + for keyword in ["$anchor", "$dynamicAnchor"]: + for subschema in find(keyword): + if fragment == subschema[keyword]: + return subschema + for keyword in ["id", "$id"]: + for subschema in find(keyword): + if "#" + fragment == subschema[keyword]: + return subschema + + # Resolve via path + parts = unquote(fragment).split("/") if fragment else [] + for part in parts: + part = part.replace("~1", "/").replace("~0", "~") + + if isinstance(document, Sequence): + try: # noqa: SIM105 + part = int(part) + except ValueError: + pass + try: + document = document[part] + except (TypeError, LookupError) as err: + raise exceptions._RefResolutionError( + f"Unresolvable JSON pointer: {fragment!r}", + ) from err + + return document + + def resolve_remote(self, uri): + """ + Resolve a remote ``uri``. + + If called directly, does not check the store first, but after + retrieving the document at the specified URI it will be saved in + the store if :attr:`cache_remote` is True. + + .. note:: + + If the requests_ library is present, ``jsonschema`` will use it to + request the remote ``uri``, so that the correct encoding is + detected and used. + + If it isn't, or if the scheme of the ``uri`` is not ``http`` or + ``https``, UTF-8 is assumed. + + Arguments: + + uri (str): + + The URI to resolve + + Returns: + + The retrieved document + + .. _requests: https://pypi.org/project/requests/ + + """ + try: + import requests + except ImportError: + requests = None + + scheme = urlsplit(uri).scheme + + if scheme in self.handlers: + result = self.handlers[scheme](uri) + elif scheme in ["http", "https"] and requests: + # Requests has support for detecting the correct encoding of + # json over http + result = requests.get(uri).json() + else: + # Otherwise, pass off to urllib and assume utf-8 + from urllib.request import urlopen + with urlopen(uri) as url: # noqa: S310 + result = json.loads(url.read().decode("utf-8")) + + if self.cache_remote: + self.store[uri] = result + return result + + +_SUBSCHEMAS_KEYWORDS = ("$id", "id", "$anchor", "$dynamicAnchor") + + +def _match_keyword(keyword): + + def matcher(value): + if keyword in value: + yield value + + return matcher + + +def _match_subschema_keywords(value): + for keyword in _SUBSCHEMAS_KEYWORDS: + if keyword in value: + yield keyword, value + + +def _search_schema(schema, matcher): + """Breadth-first search routine.""" + values = deque([schema]) + while values: + value = values.pop() + if not isinstance(value, dict): + continue + yield from matcher(value) + values.extendleft(value.values()) + + +def validate(instance, schema, cls=None, *args, **kwargs): # noqa: D417 + """ + Validate an instance under the given schema. + + >>> validate([2, 3, 4], {"maxItems": 2}) + Traceback (most recent call last): + ... + ValidationError: [2, 3, 4] is too long + + :func:`~jsonschema.validators.validate` will first verify that the + provided schema is itself valid, since not doing so can lead to less + obvious error messages and fail in less obvious or consistent ways. + + If you know you have a valid schema already, especially + if you intend to validate multiple instances with + the same schema, you likely would prefer using the + `jsonschema.protocols.Validator.validate` method directly on a + specific validator (e.g. ``Draft202012Validator.validate``). + + + Arguments: + + instance: + + The instance to validate + + schema: + + The schema to validate with + + cls (jsonschema.protocols.Validator): + + The class that will be used to validate the instance. + + If the ``cls`` argument is not provided, two things will happen + in accordance with the specification. First, if the schema has a + :kw:`$schema` keyword containing a known meta-schema [#]_ then the + proper validator will be used. The specification recommends that + all schemas contain :kw:`$schema` properties for this reason. If no + :kw:`$schema` property is found, the default validator class is the + latest released draft. + + Any other provided positional and keyword arguments will be passed + on when instantiating the ``cls``. + + Raises: + + `jsonschema.exceptions.ValidationError`: + + if the instance is invalid + + `jsonschema.exceptions.SchemaError`: + + if the schema itself is invalid + + .. rubric:: Footnotes + .. [#] known by a validator registered with + `jsonschema.validators.validates` + + """ + if cls is None: + cls = validator_for(schema) + + cls.check_schema(schema) + validator = cls(schema, *args, **kwargs) + error = exceptions.best_match(validator.iter_errors(instance)) + if error is not None: + raise error + + +def validator_for( + schema, + default: type[Validator] | _utils.Unset = _UNSET, +) -> type[Validator]: + """ + Retrieve the validator class appropriate for validating the given schema. + + Uses the :kw:`$schema` keyword that should be present in the given + schema to look up the appropriate validator class. + + Arguments: + + schema (collections.abc.Mapping or bool): + + the schema to look at + + default: + + the default to return if the appropriate validator class + cannot be determined. + + If unprovided, the default is to return the latest supported + draft. + + Examples: + + The :kw:`$schema` JSON Schema keyword will control which validator + class is returned: + + >>> schema = { + ... "$schema": "https://json-schema.org/draft/2020-12/schema", + ... "type": "integer", + ... } + >>> jsonschema.validators.validator_for(schema) + + + + Here, a draft 7 schema instead will return the draft 7 validator: + + >>> schema = { + ... "$schema": "http://json-schema.org/draft-07/schema#", + ... "type": "integer", + ... } + >>> jsonschema.validators.validator_for(schema) + + + + Schemas with no ``$schema`` keyword will fallback to the default + argument: + + >>> schema = {"type": "integer"} + >>> jsonschema.validators.validator_for( + ... schema, default=Draft7Validator, + ... ) + + + or if none is provided, to the latest version supported. + Always including the keyword when authoring schemas is highly + recommended. + + """ + DefaultValidator = _LATEST_VERSION if default is _UNSET else default + + if schema is True or schema is False or "$schema" not in schema: + return DefaultValidator # type: ignore[return-value] + if schema["$schema"] not in _META_SCHEMAS and default is _UNSET: + warn( + ( + "The metaschema specified by $schema was not found. " + "Using the latest draft to validate, but this will raise " + "an error in the future." + ), + DeprecationWarning, + stacklevel=2, + ) + return _META_SCHEMAS.get(schema["$schema"], DefaultValidator) diff --git a/source/target-vendors/cp312/jsonschema_specifications-2025.9.1.dist-info/METADATA b/source/target-vendors/cp312/jsonschema_specifications-2025.9.1.dist-info/METADATA new file mode 100644 index 0000000..ff5daef --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications-2025.9.1.dist-info/METADATA @@ -0,0 +1,54 @@ +Metadata-Version: 2.4 +Name: jsonschema-specifications +Version: 2025.9.1 +Summary: The JSON Schema meta-schemas and vocabularies, exposed as a Registry +Project-URL: Documentation, https://jsonschema-specifications.readthedocs.io/ +Project-URL: Homepage, https://github.com/python-jsonschema/jsonschema-specifications +Project-URL: Issues, https://github.com/python-jsonschema/jsonschema-specifications/issues/ +Project-URL: Funding, https://github.com/sponsors/Julian +Project-URL: Tidelift, https://tidelift.com/subscription/pkg/pypi-jsonschema-specifications?utm_source=pypi-jsonschema-specifications&utm_medium=referral&utm_campaign=pypi-link +Project-URL: Source, https://github.com/python-jsonschema/jsonschema-specifications +Author-email: Julian Berman +License-Expression: MIT +License-File: COPYING +Keywords: data validation,json,json schema,jsonschema,validation +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: File Formats :: JSON +Classifier: Topic :: File Formats :: JSON :: JSON Schema +Requires-Python: >=3.9 +Requires-Dist: referencing>=0.31.0 +Description-Content-Type: text/x-rst + +============================= +``jsonschema-specifications`` +============================= + +|PyPI| |Pythons| |CI| |ReadTheDocs| + +JSON support files from the `JSON Schema Specifications `_ (metaschemas, vocabularies, etc.), packaged for runtime access from Python as a `referencing-based Schema Registry `_. + +.. |PyPI| image:: https://img.shields.io/pypi/v/jsonschema-specifications.svg + :alt: PyPI version + :target: https://pypi.org/project/jsonschema-specifications/ + +.. |Pythons| image:: https://img.shields.io/pypi/pyversions/jsonschema-specifications.svg + :alt: Supported Python versions + :target: https://pypi.org/project/jsonschema-specifications/ + +.. |CI| image:: https://github.com/python-jsonschema/jsonschema-specifications/workflows/CI/badge.svg + :alt: Build status + :target: https://github.com/python-jsonschema/jsonschema-specifications/actions?query=workflow%3ACI + +.. |ReadTheDocs| image:: https://readthedocs.org/projects/jsonschema-specifications/badge/?version=stable&style=flat + :alt: ReadTheDocs status + :target: https://jsonschema-specifications.readthedocs.io/en/stable/ diff --git a/source/target-vendors/cp312/jsonschema_specifications-2025.9.1.dist-info/RECORD b/source/target-vendors/cp312/jsonschema_specifications-2025.9.1.dist-info/RECORD new file mode 100644 index 0000000..11399af --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications-2025.9.1.dist-info/RECORD @@ -0,0 +1,28 @@ +jsonschema_specifications/__init__.py,sha256=qoTB2DKY7qvNrGhMPH6gtmAJRLilmVQ-fFZwT6ryqw0,386 +jsonschema_specifications/_core.py,sha256=tFhc1CMleJ3AJOK_bjxOpFQTdrsUClFGfFxPBU_CebM,1140 +jsonschema_specifications/schemas/draft201909/metaschema.json,sha256=e3YbPhIfCgyh6ioLjizIVrz4AWBLgmjXG6yqICvAwTs,1785 +jsonschema_specifications/schemas/draft201909/vocabularies/applicator,sha256=aJUQDplyb7sQcFhRK77D7P1LJOj9L6zuPlBe5ysNTDE,1860 +jsonschema_specifications/schemas/draft201909/vocabularies/content,sha256=m31PVaTi_bAsQwBo_f-rxzKt3OI42j8d8mkCScM1MnQ,517 +jsonschema_specifications/schemas/draft201909/vocabularies/core,sha256=taLElX9kldClCB8ECevooU5BOayyA_x0hHH47eKvWyw,1531 +jsonschema_specifications/schemas/draft201909/vocabularies/format,sha256=UOu_55BhGoSbjMQAoJwdDg-2q1wNQ6DyIgH9NiUFa_Q,403 +jsonschema_specifications/schemas/draft201909/vocabularies/meta-data,sha256=1H4kRd1qgicaKY2DzGxsuNSuHhXg3Fa-zTehY-zwEoY,892 +jsonschema_specifications/schemas/draft201909/vocabularies/validation,sha256=HlJsHTNac0gF_ILPV5jBK5YK19olF8Zs2lobCTWcPBw,2834 +jsonschema_specifications/schemas/draft202012/metaschema.json,sha256=Qdp29a-3zgYtJI92JGOpL3ykfk4PkFsiS6av7vkd7Q8,2452 +jsonschema_specifications/schemas/draft202012/vocabularies/applicator,sha256=xKbkFHuR_vf-ptwFjLG_k0AvdBS3ZXiosWqvHa1qrO8,1659 +jsonschema_specifications/schemas/draft202012/vocabularies/content,sha256=CDQ3R3ZOSlgUJieTz01lIFenkThjxZUNQyl-jh_axbY,519 +jsonschema_specifications/schemas/draft202012/vocabularies/core,sha256=wtEqjk3RHTNt_IOj9mOqTGnwtJs76wlP_rJbUxb0gD0,1564 +jsonschema_specifications/schemas/draft202012/vocabularies/format-annotation,sha256=q8d1rf79idIjWBcNm_k_Tr0jSVY7u-3WDwK-98gSvMA,448 +jsonschema_specifications/schemas/draft202012/vocabularies/format-assertion,sha256=xSJCuaG7eGsmw-gset1CjDH5yW5XXc6Z5W6l_qptogw,445 +jsonschema_specifications/schemas/draft202012/vocabularies/meta-data,sha256=j3bW4U9Bubku-TO3CM3FFEyLUmhlGtEZGEhfsXVPHHY,892 +jsonschema_specifications/schemas/draft202012/vocabularies/unevaluated,sha256=Lb-8tzmUtnCwl2SSre4f_7RsIWgnhNL1pMpWH54tDLQ,506 +jsonschema_specifications/schemas/draft202012/vocabularies/validation,sha256=cBCjHlQfMtK-ch4t40jfdcmzaHaj7TBId_wKvaHTelg,2834 +jsonschema_specifications/schemas/draft3/metaschema.json,sha256=LPdfZENvtb43Si6qJ6uLfh_WUcm0ba6mxnsC_WTiRYs,2600 +jsonschema_specifications/schemas/draft4/metaschema.json,sha256=4UidC0dV8CeTMCWR0_y48Htok6gqlPJIlfjk7fEbguI,4357 +jsonschema_specifications/schemas/draft6/metaschema.json,sha256=wp386fVINcOgbAOzxdXsDtp3cGVo-cTffPvHVmpRAG0,4437 +jsonschema_specifications/schemas/draft7/metaschema.json,sha256=PVOSCIJhYGxVm2A_OFMpyfGrRbXWZ-uZBodFOwVdQF4,4819 +jsonschema_specifications/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +jsonschema_specifications/tests/test_jsonschema_specifications.py,sha256=WkbYRW6A6FoZ0rivShfqVLSCsAiHJ2x8TxqECJTXPTY,1106 +jsonschema_specifications-2025.9.1.dist-info/METADATA,sha256=NavUF1fzK06iR1aSDe1HtwFz13y8BSpabTq1g7Lo2J0,2907 +jsonschema_specifications-2025.9.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87 +jsonschema_specifications-2025.9.1.dist-info/licenses/COPYING,sha256=QtzWNJX4e063x3V6-jebtVpT-Ur9el9lfZrfVyNuUVw,1057 +jsonschema_specifications-2025.9.1.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/jsonschema_specifications-2025.9.1.dist-info/WHEEL b/source/target-vendors/cp312/jsonschema_specifications-2025.9.1.dist-info/WHEEL new file mode 100644 index 0000000..12228d4 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications-2025.9.1.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.27.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/source/target-vendors/cp312/jsonschema_specifications-2025.9.1.dist-info/licenses/COPYING b/source/target-vendors/cp312/jsonschema_specifications-2025.9.1.dist-info/licenses/COPYING new file mode 100644 index 0000000..a9f853e --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications-2025.9.1.dist-info/licenses/COPYING @@ -0,0 +1,19 @@ +Copyright (c) 2022 Julian Berman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/source/target-vendors/cp312/jsonschema_specifications/__init__.py b/source/target-vendors/cp312/jsonschema_specifications/__init__.py new file mode 100644 index 0000000..a423574 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/__init__.py @@ -0,0 +1,12 @@ +""" +The JSON Schema meta-schemas and vocabularies, exposed as a Registry. +""" + +from referencing.jsonschema import EMPTY_REGISTRY as _EMPTY_REGISTRY + +from jsonschema_specifications._core import _schemas + +#: A `referencing.jsonschema.SchemaRegistry` containing all of the official +#: meta-schemas and vocabularies. +REGISTRY = (_schemas() @ _EMPTY_REGISTRY).crawl() +__all__ = ["REGISTRY"] diff --git a/source/target-vendors/cp312/jsonschema_specifications/_core.py b/source/target-vendors/cp312/jsonschema_specifications/_core.py new file mode 100644 index 0000000..e67bd71 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/_core.py @@ -0,0 +1,38 @@ +""" +Load all the JSON Schema specification's official schemas. +""" + +import json + +try: + from importlib.resources import files +except ImportError: + from importlib_resources import ( # type: ignore[import-not-found, no-redef] + files, + ) + +from referencing import Resource + + +def _schemas(): + """ + All schemas we ship. + """ + # importlib.resources.abc.Traversal doesn't have nice ways to do this that + # I'm aware of... + # + # It can't recurse arbitrarily, e.g. no ``.glob()``. + # + # So this takes some liberties given the real layout of what we ship + # (only 2 levels of nesting, no directories within the second level). + + for version in files(__package__).joinpath("schemas").iterdir(): + if version.name.startswith("."): + continue + for child in version.iterdir(): + children = [child] if child.is_file() else child.iterdir() + for path in children: + if path.name.startswith("."): + continue + contents = json.loads(path.read_text(encoding="utf-8")) + yield Resource.from_contents(contents) diff --git a/source/target-vendors/cp312/jsonschema_specifications/schemas/draft201909/metaschema.json b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft201909/metaschema.json new file mode 100644 index 0000000..2248a0c --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft201909/metaschema.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/schema", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/core": true, + "https://json-schema.org/draft/2019-09/vocab/applicator": true, + "https://json-schema.org/draft/2019-09/vocab/validation": true, + "https://json-schema.org/draft/2019-09/vocab/meta-data": true, + "https://json-schema.org/draft/2019-09/vocab/format": false, + "https://json-schema.org/draft/2019-09/vocab/content": true + }, + "$recursiveAnchor": true, + + "title": "Core and Validation specifications meta-schema", + "allOf": [ + {"$ref": "meta/core"}, + {"$ref": "meta/applicator"}, + {"$ref": "meta/validation"}, + {"$ref": "meta/meta-data"}, + {"$ref": "meta/format"}, + {"$ref": "meta/content"} + ], + "type": ["object", "boolean"], + "properties": { + "definitions": { + "$comment": "While no longer an official keyword as it is replaced by $defs, this keyword is retained in the meta-schema to prevent incompatible extensions as it remains in common use.", + "type": "object", + "additionalProperties": { "$recursiveRef": "#" }, + "default": {} + }, + "dependencies": { + "$comment": "\"dependencies\" is no longer a keyword, but schema authors should avoid redefining it to facilitate a smooth transition to \"dependentSchemas\" and \"dependentRequired\"", + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$recursiveRef": "#" }, + { "$ref": "meta/validation#/$defs/stringArray" } + ] + } + } + } +} diff --git a/source/target-vendors/cp312/jsonschema_specifications/schemas/draft201909/vocabularies/applicator b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft201909/vocabularies/applicator new file mode 100644 index 0000000..24a1cc4 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft201909/vocabularies/applicator @@ -0,0 +1,56 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/applicator", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/applicator": true + }, + "$recursiveAnchor": true, + + "title": "Applicator vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "additionalItems": { "$recursiveRef": "#" }, + "unevaluatedItems": { "$recursiveRef": "#" }, + "items": { + "anyOf": [ + { "$recursiveRef": "#" }, + { "$ref": "#/$defs/schemaArray" } + ] + }, + "contains": { "$recursiveRef": "#" }, + "additionalProperties": { "$recursiveRef": "#" }, + "unevaluatedProperties": { "$recursiveRef": "#" }, + "properties": { + "type": "object", + "additionalProperties": { "$recursiveRef": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$recursiveRef": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependentSchemas": { + "type": "object", + "additionalProperties": { + "$recursiveRef": "#" + } + }, + "propertyNames": { "$recursiveRef": "#" }, + "if": { "$recursiveRef": "#" }, + "then": { "$recursiveRef": "#" }, + "else": { "$recursiveRef": "#" }, + "allOf": { "$ref": "#/$defs/schemaArray" }, + "anyOf": { "$ref": "#/$defs/schemaArray" }, + "oneOf": { "$ref": "#/$defs/schemaArray" }, + "not": { "$recursiveRef": "#" } + }, + "$defs": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$recursiveRef": "#" } + } + } +} diff --git a/source/target-vendors/cp312/jsonschema_specifications/schemas/draft201909/vocabularies/content b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft201909/vocabularies/content new file mode 100644 index 0000000..f6752a8 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft201909/vocabularies/content @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/content", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/content": true + }, + "$recursiveAnchor": true, + + "title": "Content vocabulary meta-schema", + + "type": ["object", "boolean"], + "properties": { + "contentMediaType": { "type": "string" }, + "contentEncoding": { "type": "string" }, + "contentSchema": { "$recursiveRef": "#" } + } +} diff --git a/source/target-vendors/cp312/jsonschema_specifications/schemas/draft201909/vocabularies/core b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft201909/vocabularies/core new file mode 100644 index 0000000..eb708a5 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft201909/vocabularies/core @@ -0,0 +1,57 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/core", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/core": true + }, + "$recursiveAnchor": true, + + "title": "Core vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference", + "$comment": "Non-empty fragments not allowed.", + "pattern": "^[^#]*#?$" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$anchor": { + "type": "string", + "pattern": "^[A-Za-z][-A-Za-z0-9.:_]*$" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "$recursiveRef": { + "type": "string", + "format": "uri-reference" + }, + "$recursiveAnchor": { + "type": "boolean", + "default": false + }, + "$vocabulary": { + "type": "object", + "propertyNames": { + "type": "string", + "format": "uri" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "$comment": { + "type": "string" + }, + "$defs": { + "type": "object", + "additionalProperties": { "$recursiveRef": "#" }, + "default": {} + } + } +} diff --git a/source/target-vendors/cp312/jsonschema_specifications/schemas/draft201909/vocabularies/format b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft201909/vocabularies/format new file mode 100644 index 0000000..09bbfdd --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft201909/vocabularies/format @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/format", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/format": true + }, + "$recursiveAnchor": true, + + "title": "Format vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "format": { "type": "string" } + } +} diff --git a/source/target-vendors/cp312/jsonschema_specifications/schemas/draft201909/vocabularies/meta-data b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft201909/vocabularies/meta-data new file mode 100644 index 0000000..da04cff --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft201909/vocabularies/meta-data @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/meta-data", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/meta-data": true + }, + "$recursiveAnchor": true, + + "title": "Meta-data vocabulary meta-schema", + + "type": ["object", "boolean"], + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "deprecated": { + "type": "boolean", + "default": false + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + } + } +} diff --git a/source/target-vendors/cp312/jsonschema_specifications/schemas/draft201909/vocabularies/validation b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft201909/vocabularies/validation new file mode 100644 index 0000000..9f59677 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft201909/vocabularies/validation @@ -0,0 +1,98 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/validation", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/validation": true + }, + "$recursiveAnchor": true, + + "title": "Validation vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/$defs/nonNegativeInteger" }, + "minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "maxItems": { "$ref": "#/$defs/nonNegativeInteger" }, + "minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxContains": { "$ref": "#/$defs/nonNegativeInteger" }, + "minContains": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 1 + }, + "maxProperties": { "$ref": "#/$defs/nonNegativeInteger" }, + "minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/$defs/stringArray" }, + "dependentRequired": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/stringArray" + } + }, + "const": true, + "enum": { + "type": "array", + "items": true + }, + "type": { + "anyOf": [ + { "$ref": "#/$defs/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/$defs/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + } + }, + "$defs": { + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 0 + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + } +} diff --git a/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/metaschema.json b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/metaschema.json new file mode 100644 index 0000000..d5e2d31 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/metaschema.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/schema", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true, + "https://json-schema.org/draft/2020-12/vocab/meta-data": true, + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true, + "https://json-schema.org/draft/2020-12/vocab/content": true + }, + "$dynamicAnchor": "meta", + + "title": "Core and Validation specifications meta-schema", + "allOf": [ + {"$ref": "meta/core"}, + {"$ref": "meta/applicator"}, + {"$ref": "meta/unevaluated"}, + {"$ref": "meta/validation"}, + {"$ref": "meta/meta-data"}, + {"$ref": "meta/format-annotation"}, + {"$ref": "meta/content"} + ], + "type": ["object", "boolean"], + "$comment": "This meta-schema also defines keywords that have appeared in previous drafts in order to prevent incompatible extensions as they remain in common use.", + "properties": { + "definitions": { + "$comment": "\"definitions\" has been replaced by \"$defs\".", + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" }, + "deprecated": true, + "default": {} + }, + "dependencies": { + "$comment": "\"dependencies\" has been split and replaced by \"dependentSchemas\" and \"dependentRequired\" in order to serve their differing semantics.", + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$dynamicRef": "#meta" }, + { "$ref": "meta/validation#/$defs/stringArray" } + ] + }, + "deprecated": true, + "default": {} + }, + "$recursiveAnchor": { + "$comment": "\"$recursiveAnchor\" has been replaced by \"$dynamicAnchor\".", + "$ref": "meta/core#/$defs/anchorString", + "deprecated": true + }, + "$recursiveRef": { + "$comment": "\"$recursiveRef\" has been replaced by \"$dynamicRef\".", + "$ref": "meta/core#/$defs/uriReferenceString", + "deprecated": true + } + } +} diff --git a/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/applicator b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/applicator new file mode 100644 index 0000000..ca69923 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/applicator @@ -0,0 +1,48 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/applicator", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/applicator": true + }, + "$dynamicAnchor": "meta", + + "title": "Applicator vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "prefixItems": { "$ref": "#/$defs/schemaArray" }, + "items": { "$dynamicRef": "#meta" }, + "contains": { "$dynamicRef": "#meta" }, + "additionalProperties": { "$dynamicRef": "#meta" }, + "properties": { + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependentSchemas": { + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" }, + "default": {} + }, + "propertyNames": { "$dynamicRef": "#meta" }, + "if": { "$dynamicRef": "#meta" }, + "then": { "$dynamicRef": "#meta" }, + "else": { "$dynamicRef": "#meta" }, + "allOf": { "$ref": "#/$defs/schemaArray" }, + "anyOf": { "$ref": "#/$defs/schemaArray" }, + "oneOf": { "$ref": "#/$defs/schemaArray" }, + "not": { "$dynamicRef": "#meta" } + }, + "$defs": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$dynamicRef": "#meta" } + } + } +} diff --git a/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/content b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/content new file mode 100644 index 0000000..2f6e056 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/content @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/content", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/content": true + }, + "$dynamicAnchor": "meta", + + "title": "Content vocabulary meta-schema", + + "type": ["object", "boolean"], + "properties": { + "contentEncoding": { "type": "string" }, + "contentMediaType": { "type": "string" }, + "contentSchema": { "$dynamicRef": "#meta" } + } +} diff --git a/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/core b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/core new file mode 100644 index 0000000..dfc092d --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/core @@ -0,0 +1,51 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/core", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true + }, + "$dynamicAnchor": "meta", + + "title": "Core vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "$id": { + "$ref": "#/$defs/uriReferenceString", + "$comment": "Non-empty fragments not allowed.", + "pattern": "^[^#]*#?$" + }, + "$schema": { "$ref": "#/$defs/uriString" }, + "$ref": { "$ref": "#/$defs/uriReferenceString" }, + "$anchor": { "$ref": "#/$defs/anchorString" }, + "$dynamicRef": { "$ref": "#/$defs/uriReferenceString" }, + "$dynamicAnchor": { "$ref": "#/$defs/anchorString" }, + "$vocabulary": { + "type": "object", + "propertyNames": { "$ref": "#/$defs/uriString" }, + "additionalProperties": { + "type": "boolean" + } + }, + "$comment": { + "type": "string" + }, + "$defs": { + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" } + } + }, + "$defs": { + "anchorString": { + "type": "string", + "pattern": "^[A-Za-z_][-A-Za-z0-9._]*$" + }, + "uriString": { + "type": "string", + "format": "uri" + }, + "uriReferenceString": { + "type": "string", + "format": "uri-reference" + } + } +} diff --git a/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/format-annotation b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/format-annotation new file mode 100644 index 0000000..51ef7ea --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/format-annotation @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/format-annotation", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true + }, + "$dynamicAnchor": "meta", + + "title": "Format vocabulary meta-schema for annotation results", + "type": ["object", "boolean"], + "properties": { + "format": { "type": "string" } + } +} diff --git a/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/format-assertion b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/format-assertion new file mode 100644 index 0000000..5e73fd7 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/format-assertion @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/format-assertion", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/format-assertion": true + }, + "$dynamicAnchor": "meta", + + "title": "Format vocabulary meta-schema for assertion results", + "type": ["object", "boolean"], + "properties": { + "format": { "type": "string" } + } +} diff --git a/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/meta-data b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/meta-data new file mode 100644 index 0000000..05cbc22 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/meta-data @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/meta-data", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/meta-data": true + }, + "$dynamicAnchor": "meta", + + "title": "Meta-data vocabulary meta-schema", + + "type": ["object", "boolean"], + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "deprecated": { + "type": "boolean", + "default": false + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + } + } +} diff --git a/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/unevaluated b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/unevaluated new file mode 100644 index 0000000..5f62a3f --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/unevaluated @@ -0,0 +1,15 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/unevaluated", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true + }, + "$dynamicAnchor": "meta", + + "title": "Unevaluated applicator vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "unevaluatedItems": { "$dynamicRef": "#meta" }, + "unevaluatedProperties": { "$dynamicRef": "#meta" } + } +} diff --git a/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/validation b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/validation new file mode 100644 index 0000000..606b87b --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft202012/vocabularies/validation @@ -0,0 +1,98 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/validation", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/validation": true + }, + "$dynamicAnchor": "meta", + + "title": "Validation vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "type": { + "anyOf": [ + { "$ref": "#/$defs/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/$defs/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "const": true, + "enum": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/$defs/nonNegativeInteger" }, + "minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "maxItems": { "$ref": "#/$defs/nonNegativeInteger" }, + "minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxContains": { "$ref": "#/$defs/nonNegativeInteger" }, + "minContains": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 1 + }, + "maxProperties": { "$ref": "#/$defs/nonNegativeInteger" }, + "minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/$defs/stringArray" }, + "dependentRequired": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/stringArray" + } + } + }, + "$defs": { + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 0 + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + } +} diff --git a/source/target-vendors/cp312/jsonschema_specifications/schemas/draft3/metaschema.json b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft3/metaschema.json new file mode 100644 index 0000000..8b26b1f --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft3/metaschema.json @@ -0,0 +1,172 @@ +{ + "$schema" : "http://json-schema.org/draft-03/schema#", + "id" : "http://json-schema.org/draft-03/schema#", + "type" : "object", + + "properties" : { + "type" : { + "type" : ["string", "array"], + "items" : { + "type" : ["string", {"$ref" : "#"}] + }, + "uniqueItems" : true, + "default" : "any" + }, + + "properties" : { + "type" : "object", + "additionalProperties" : {"$ref" : "#"}, + "default" : {} + }, + + "patternProperties" : { + "type" : "object", + "additionalProperties" : {"$ref" : "#"}, + "default" : {} + }, + + "additionalProperties" : { + "type" : [{"$ref" : "#"}, "boolean"], + "default" : {} + }, + + "items" : { + "type" : [{"$ref" : "#"}, "array"], + "items" : {"$ref" : "#"}, + "default" : {} + }, + + "additionalItems" : { + "type" : [{"$ref" : "#"}, "boolean"], + "default" : {} + }, + + "required" : { + "type" : "boolean", + "default" : false + }, + + "dependencies" : { + "type" : "object", + "additionalProperties" : { + "type" : ["string", "array", {"$ref" : "#"}], + "items" : { + "type" : "string" + } + }, + "default" : {} + }, + + "minimum" : { + "type" : "number" + }, + + "maximum" : { + "type" : "number" + }, + + "exclusiveMinimum" : { + "type" : "boolean", + "default" : false + }, + + "exclusiveMaximum" : { + "type" : "boolean", + "default" : false + }, + + "minItems" : { + "type" : "integer", + "minimum" : 0, + "default" : 0 + }, + + "maxItems" : { + "type" : "integer", + "minimum" : 0 + }, + + "uniqueItems" : { + "type" : "boolean", + "default" : false + }, + + "pattern" : { + "type" : "string", + "format" : "regex" + }, + + "minLength" : { + "type" : "integer", + "minimum" : 0, + "default" : 0 + }, + + "maxLength" : { + "type" : "integer" + }, + + "enum" : { + "type" : "array", + "minItems" : 1, + "uniqueItems" : true + }, + + "default" : { + "type" : "any" + }, + + "title" : { + "type" : "string" + }, + + "description" : { + "type" : "string" + }, + + "format" : { + "type" : "string" + }, + + "divisibleBy" : { + "type" : "number", + "minimum" : 0, + "exclusiveMinimum" : true, + "default" : 1 + }, + + "disallow" : { + "type" : ["string", "array"], + "items" : { + "type" : ["string", {"$ref" : "#"}] + }, + "uniqueItems" : true + }, + + "extends" : { + "type" : [{"$ref" : "#"}, "array"], + "items" : {"$ref" : "#"}, + "default" : {} + }, + + "id" : { + "type" : "string" + }, + + "$ref" : { + "type" : "string" + }, + + "$schema" : { + "type" : "string", + "format" : "uri" + } + }, + + "dependencies" : { + "exclusiveMinimum" : "minimum", + "exclusiveMaximum" : "maximum" + }, + + "default" : {} +} diff --git a/source/target-vendors/cp312/jsonschema_specifications/schemas/draft4/metaschema.json b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft4/metaschema.json new file mode 100644 index 0000000..bcbb847 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft4/metaschema.json @@ -0,0 +1,149 @@ +{ + "id": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "positiveInteger": { + "type": "integer", + "minimum": 0 + }, + "positiveIntegerDefault0": { + "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] + }, + "simpleTypes": { + "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "uniqueItems": true + } + }, + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "$schema": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": {}, + "multipleOf": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + "maxLength": { "$ref": "#/definitions/positiveInteger" }, + "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { + "anyOf": [ + { "type": "boolean" }, + { "$ref": "#" } + ], + "default": {} + }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": {} + }, + "maxItems": { "$ref": "#/definitions/positiveInteger" }, + "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxProperties": { "$ref": "#/definitions/positiveInteger" }, + "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { + "anyOf": [ + { "type": "boolean" }, + { "$ref": "#" } + ], + "default": {} + }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "dependencies": { + "exclusiveMaximum": [ "maximum" ], + "exclusiveMinimum": [ "minimum" ] + }, + "default": {} +} diff --git a/source/target-vendors/cp312/jsonschema_specifications/schemas/draft6/metaschema.json b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft6/metaschema.json new file mode 100644 index 0000000..a0d2bf7 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft6/metaschema.json @@ -0,0 +1,153 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "$id": "http://json-schema.org/draft-06/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { "$ref": "#/definitions/nonNegativeInteger" }, + { "default": 0 } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + }, + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": {}, + "examples": { + "type": "array", + "items": {} + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, + "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { "$ref": "#" }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": {} + }, + "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, + "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { "$ref": "#" }, + "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, + "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { "$ref": "#" }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "propertyNames": { "$ref": "#" }, + "const": {}, + "enum": { + "type": "array" + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "default": {} +} diff --git a/source/target-vendors/cp312/jsonschema_specifications/schemas/draft7/metaschema.json b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft7/metaschema.json new file mode 100644 index 0000000..746cde9 --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/schemas/draft7/metaschema.json @@ -0,0 +1,166 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { "$ref": "#/definitions/nonNegativeInteger" }, + { "default": 0 } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + }, + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "$comment": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "readOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, + "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { "$ref": "#" }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": true + }, + "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, + "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { "$ref": "#" }, + "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, + "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { "$ref": "#" }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "propertyNames": { "$ref": "#" }, + "const": true, + "enum": { + "type": "array", + "items": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "contentMediaType": { "type": "string" }, + "contentEncoding": { "type": "string" }, + "if": {"$ref": "#"}, + "then": {"$ref": "#"}, + "else": {"$ref": "#"}, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "default": true +} diff --git a/source/target-vendors/cp312/jsonschema_specifications/tests/__init__.py b/source/target-vendors/cp312/jsonschema_specifications/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/jsonschema_specifications/tests/test_jsonschema_specifications.py b/source/target-vendors/cp312/jsonschema_specifications/tests/test_jsonschema_specifications.py new file mode 100644 index 0000000..fd2927e --- /dev/null +++ b/source/target-vendors/cp312/jsonschema_specifications/tests/test_jsonschema_specifications.py @@ -0,0 +1,41 @@ +from collections.abc import Mapping +from pathlib import Path + +import pytest + +from jsonschema_specifications import REGISTRY + + +def test_it_contains_metaschemas(): + schema = REGISTRY.contents("http://json-schema.org/draft-07/schema#") + assert isinstance(schema, Mapping) + assert schema["$id"] == "http://json-schema.org/draft-07/schema#" + assert schema["title"] == "Core schema meta-schema" + + +def test_it_is_crawled(): + assert REGISTRY.crawl() == REGISTRY + + +@pytest.mark.parametrize( + "ignored_relative_path", + ["schemas/.DS_Store", "schemas/draft7/.DS_Store"], +) +def test_it_copes_with_dotfiles(ignored_relative_path): + """ + Ignore files like .DS_Store if someone has actually caused one to exist. + + We test here through the private interface as of course the global has + already loaded our schemas. + """ + + import jsonschema_specifications + + package = Path(jsonschema_specifications.__file__).parent + + ignored = package / ignored_relative_path + ignored.touch() + try: + list(jsonschema_specifications._schemas()) + finally: + ignored.unlink() diff --git a/source/target-vendors/cp312/jwt/__init__.py b/source/target-vendors/cp312/jwt/__init__.py new file mode 100644 index 0000000..4f9b685 --- /dev/null +++ b/source/target-vendors/cp312/jwt/__init__.py @@ -0,0 +1,78 @@ +from .api_jwk import PyJWK, PyJWKSet +from .api_jws import ( + PyJWS, + get_algorithm_by_name, + get_unverified_header, + register_algorithm, + unregister_algorithm, +) +from .api_jwt import PyJWT, decode, decode_complete, encode +from .exceptions import ( + DecodeError, + ExpiredSignatureError, + ImmatureSignatureError, + InvalidAlgorithmError, + InvalidAudienceError, + InvalidIssuedAtError, + InvalidIssuerError, + InvalidKeyError, + InvalidSignatureError, + InvalidTokenError, + MissingRequiredClaimError, + PyJWKClientConnectionError, + PyJWKClientError, + PyJWKError, + PyJWKSetError, + PyJWTError, +) +from .jwks_client import PyJWKClient +from .warnings import InsecureKeyLengthWarning + +__version__ = "2.14.0" + +__title__ = "PyJWT" +__description__ = "JSON Web Token implementation in Python" +__url__ = "https://pyjwt.readthedocs.io" +__uri__ = __url__ +__doc__ = f"{__description__} <{__uri__}>" + +__author__ = "José Padilla" +__email__ = "hello@jpadilla.com" + +__license__ = "MIT" +__copyright__ = "Copyright 2015-2026 José Padilla" + + +__all__ = [ + "PyJWS", + "PyJWT", + "PyJWKClient", + "PyJWK", + "PyJWKSet", + "decode", + "decode_complete", + "encode", + "get_unverified_header", + "register_algorithm", + "unregister_algorithm", + "get_algorithm_by_name", + # Warnings + "InsecureKeyLengthWarning", + # Exceptions + "DecodeError", + "ExpiredSignatureError", + "ImmatureSignatureError", + "InvalidAlgorithmError", + "InvalidAudienceError", + "InvalidIssuedAtError", + "InvalidIssuerError", + "InvalidKeyError", + "InvalidSignatureError", + "InvalidTokenError", + "MissingRequiredClaimError", + "PyJWKClientConnectionError", + "PyJWKClientError", + "PyJWKError", + "PyJWKSetError", + "PyJWTError", +] diff --git a/source/target-vendors/cp312/jwt/algorithms.py b/source/target-vendors/cp312/jwt/algorithms.py new file mode 100644 index 0000000..3f350e3 --- /dev/null +++ b/source/target-vendors/cp312/jwt/algorithms.py @@ -0,0 +1,1111 @@ +from __future__ import annotations + +import hashlib +import hmac +import json +import os +import sys +from abc import ABC, abstractmethod +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Literal, + NoReturn, + Union, + cast, + get_args, + overload, +) + +from .exceptions import InvalidKeyError +from .types import HashlibHash, JWKDict +from .utils import ( + base64url_decode, + base64url_encode, + der_to_raw_signature, + force_bytes, + from_base64url_uint, + is_pem_format, + is_ssh_key, + raw_to_der_signature, + to_base64url_uint, +) + +try: + from cryptography import x509 + from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.asymmetric import padding + from cryptography.hazmat.primitives.asymmetric.ec import ( + ECDSA, + SECP256K1, + SECP256R1, + SECP384R1, + SECP521R1, + EllipticCurve, + EllipticCurvePrivateKey, + EllipticCurvePrivateNumbers, + EllipticCurvePublicKey, + EllipticCurvePublicNumbers, + ) + from cryptography.hazmat.primitives.asymmetric.ed448 import ( + Ed448PrivateKey, + Ed448PublicKey, + ) + from cryptography.hazmat.primitives.asymmetric.ed25519 import ( + Ed25519PrivateKey, + Ed25519PublicKey, + ) + from cryptography.hazmat.primitives.asymmetric.rsa import ( + RSAPrivateKey, + RSAPrivateNumbers, + RSAPublicKey, + RSAPublicNumbers, + rsa_crt_dmp1, + rsa_crt_dmq1, + rsa_crt_iqmp, + rsa_recover_prime_factors, + ) + from cryptography.hazmat.primitives.serialization import ( + Encoding, + NoEncryption, + PrivateFormat, + PublicFormat, + load_der_public_key, + load_pem_private_key, + load_pem_public_key, + load_ssh_public_key, + ) + + if sys.version_info >= (3, 10): + from typing import TypeAlias + else: + # Python 3.9 and lower + from typing_extensions import TypeAlias + + # Type aliases for convenience in algorithms method signatures + AllowedRSAKeys: TypeAlias = Union[RSAPrivateKey, RSAPublicKey] + AllowedECKeys: TypeAlias = Union[EllipticCurvePrivateKey, EllipticCurvePublicKey] + AllowedOKPKeys: TypeAlias = Union[ + Ed25519PrivateKey, Ed25519PublicKey, Ed448PrivateKey, Ed448PublicKey + ] + AllowedKeys: TypeAlias = Union[AllowedRSAKeys, AllowedECKeys, AllowedOKPKeys] + #: Type alias for allowed ``cryptography`` private keys (requires ``cryptography`` to be installed) + AllowedPrivateKeys: TypeAlias = Union[ + RSAPrivateKey, EllipticCurvePrivateKey, Ed25519PrivateKey, Ed448PrivateKey + ] + #: Type alias for allowed ``cryptography`` public keys (requires ``cryptography`` to be installed) + AllowedPublicKeys: TypeAlias = Union[ + RSAPublicKey, EllipticCurvePublicKey, Ed25519PublicKey, Ed448PublicKey + ] + + if TYPE_CHECKING or bool(os.getenv("SPHINX_BUILD", "")): + from cryptography.hazmat.primitives.asymmetric.types import ( + PrivateKeyTypes, + PublicKeyTypes, + ) + + has_crypto = True +except ModuleNotFoundError: + if sys.version_info >= (3, 11): + from typing import Never + else: + from typing_extensions import Never + + AllowedRSAKeys = Never # type: ignore[misc] + AllowedECKeys = Never # type: ignore[misc] + AllowedOKPKeys = Never # type: ignore[misc] + AllowedKeys = Never # type: ignore[misc] + AllowedPrivateKeys = Never # type: ignore[misc] + AllowedPublicKeys = Never # type: ignore[misc] + has_crypto = False + + +requires_cryptography = { + "RS256", + "RS384", + "RS512", + "ES256", + "ES256K", + "ES384", + "ES521", + "ES512", + "PS256", + "PS384", + "PS512", + "EdDSA", +} + + +def get_default_algorithms() -> dict[str, Algorithm]: + """ + Returns the algorithms that are implemented by the library. + """ + default_algorithms: dict[str, Algorithm] = { + "none": NoneAlgorithm(), + "HS256": HMACAlgorithm(HMACAlgorithm.SHA256), + "HS384": HMACAlgorithm(HMACAlgorithm.SHA384), + "HS512": HMACAlgorithm(HMACAlgorithm.SHA512), + } + + if has_crypto: + default_algorithms.update( + { + "RS256": RSAAlgorithm(RSAAlgorithm.SHA256), + "RS384": RSAAlgorithm(RSAAlgorithm.SHA384), + "RS512": RSAAlgorithm(RSAAlgorithm.SHA512), + "ES256": ECAlgorithm(ECAlgorithm.SHA256, SECP256R1), + "ES256K": ECAlgorithm(ECAlgorithm.SHA256, SECP256K1), + "ES384": ECAlgorithm(ECAlgorithm.SHA384, SECP384R1), + "ES521": ECAlgorithm(ECAlgorithm.SHA512, SECP521R1), + "ES512": ECAlgorithm( + ECAlgorithm.SHA512, SECP521R1 + ), # Backward compat for #219 fix + "PS256": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256), + "PS384": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA384), + "PS512": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA512), + "EdDSA": OKPAlgorithm(), + } + ) + + return default_algorithms + + +class Algorithm(ABC): + """ + The interface for an algorithm used to sign and verify tokens. + """ + + # pyjwt-964: Validate to ensure the key passed in was decoded to the correct cryptography key family + _crypto_key_types: tuple[type[AllowedKeys], ...] | None = None + + def compute_hash_digest(self, bytestr: bytes) -> bytes: + """ + Compute a hash digest using the specified algorithm's hash algorithm. + + If there is no hash algorithm, raises a NotImplementedError. + """ + # lookup self.hash_alg if defined in a way that mypy can understand + hash_alg = getattr(self, "hash_alg", None) + if hash_alg is None: + raise NotImplementedError + + if ( + has_crypto + and isinstance(hash_alg, type) + and issubclass(hash_alg, hashes.HashAlgorithm) + ): + digest = hashes.Hash(hash_alg(), backend=default_backend()) + digest.update(bytestr) + return bytes(digest.finalize()) + else: + return bytes(hash_alg(bytestr).digest()) + + def check_crypto_key_type(self, key: PublicKeyTypes | PrivateKeyTypes) -> None: + """Check that the key belongs to the right cryptographic family. + + Note that this method only works when ``cryptography`` is installed. + + :param key: Potentially a cryptography key + :type key: :py:data:`PublicKeyTypes ` | :py:data:`PrivateKeyTypes ` + :raises ValueError: if ``cryptography`` is not installed, or this method is called by a non-cryptography algorithm + :raises InvalidKeyError: if the key doesn't match the expected key classes + """ + if not has_crypto or self._crypto_key_types is None: + raise ValueError( + "This method requires the cryptography library, and should only be used by cryptography-based algorithms." + ) + + if not isinstance(key, self._crypto_key_types): + valid_classes = (cls.__name__ for cls in self._crypto_key_types) + actual_class = key.__class__.__name__ + self_class = self.__class__.__name__ + raise InvalidKeyError( + f"Expected one of {valid_classes}, got: {actual_class}. Invalid Key type for {self_class}" + ) + + @abstractmethod + def prepare_key(self, key: Any) -> Any: + """ + Performs necessary validation and conversions on the key and returns + the key value in the proper format for sign() and verify(). + """ + + @abstractmethod + def sign(self, msg: bytes, key: Any) -> bytes: + """ + Returns a digital signature for the specified message + using the specified key value. + """ + + @abstractmethod + def verify(self, msg: bytes, key: Any, sig: bytes) -> bool: + """ + Verifies that the specified digital signature is valid + for the specified message and key values. + """ + + @overload + @staticmethod + @abstractmethod + def to_jwk(key_obj: Any, as_dict: Literal[True]) -> JWKDict: ... # pragma: no cover + + @overload + @staticmethod + @abstractmethod + def to_jwk( + key_obj: Any, as_dict: Literal[False] = False + ) -> str: ... # pragma: no cover + + @staticmethod + @abstractmethod + def to_jwk(key_obj: Any, as_dict: bool = False) -> JWKDict | str: + """ + Serializes a given key into a JWK + """ + + @staticmethod + @abstractmethod + def from_jwk(jwk: str | JWKDict) -> Any: + """ + Deserializes a given key from JWK back into a key object + """ + + def check_key_length(self, key: Any) -> str | None: + """ + Return a warning message if the key is below the minimum + recommended length for this algorithm, or None if adequate. + """ + return None + + +class NoneAlgorithm(Algorithm): + """ + Placeholder for use when no signing or verification + operations are required. + """ + + def prepare_key(self, key: str | None) -> None: + if key == "": + key = None + + if key is not None: + raise InvalidKeyError('When alg = "none", key value must be None.') + + return key + + def sign(self, msg: bytes, key: None) -> bytes: + return b"" + + def verify(self, msg: bytes, key: None, sig: bytes) -> bool: + return False + + @staticmethod + def to_jwk(key_obj: Any, as_dict: bool = False) -> NoReturn: + raise NotImplementedError() + + @staticmethod + def from_jwk(jwk: str | JWKDict) -> NoReturn: + raise NotImplementedError() + + +class HMACAlgorithm(Algorithm): + """ + Performs signing and verification operations using HMAC + and the specified hash function. + """ + + SHA256: ClassVar[HashlibHash] = hashlib.sha256 + SHA384: ClassVar[HashlibHash] = hashlib.sha384 + SHA512: ClassVar[HashlibHash] = hashlib.sha512 + + def __init__(self, hash_alg: HashlibHash) -> None: + self.hash_alg = hash_alg + + @staticmethod + def _is_der_key(key_bytes: bytes) -> bool: + if not has_crypto: + return False + + try: + load_der_public_key(key_bytes) + except (TypeError, ValueError, UnsupportedAlgorithm): + pass + else: + return True + + try: + x509.load_der_x509_certificate(key_bytes) + except (TypeError, ValueError, UnsupportedAlgorithm): + return False + else: + return True + + def prepare_key(self, key: str | bytes) -> bytes: + key_bytes = force_bytes(key) + + if len(key_bytes) == 0: + raise InvalidKeyError("HMAC key must not be empty.") + + if ( + is_pem_format(key_bytes) + or is_ssh_key(key_bytes) + or self._is_der_key(key_bytes) + ): + raise InvalidKeyError( + "The specified key is an asymmetric key or x509 certificate and" + " should not be used as an HMAC secret." + ) + + # Defense against algorithm-confusion attacks: an attacker with + # control over the token header can force this code path by setting + # alg=HS*, and HMACAlgorithm is the only algorithm that accepts + # arbitrary bytes as a valid secret. Other algorithms reject + # non-key-shaped input naturally. Even a symmetric (kty=oct) JWK + # should be loaded via PyJWK / from_jwk rather than fed as raw JSON + # bytes (whose contents are not the secret material). + try: + jwk_obj = json.loads(key_bytes, parse_int=lambda _: 0) + except RecursionError: + try: + decoded_key = key_bytes.decode( + json.detect_encoding(key_bytes), errors="surrogatepass" + ) + except UnicodeError: + decoded_key = "" + stripped_key = decoded_key.lstrip("\ufeff \t\r\n") + has_jwk_member = False + index = 0 + while index < len(decoded_key): + if decoded_key[index] != '"': + index += 1 + continue + end = index + 1 + while end < len(decoded_key): + if decoded_key[end] == "\\": + end += 2 + elif decoded_key[end] == '"': + break + else: + end += 1 + if end >= len(decoded_key): + break + next_index = end + 1 + while ( + next_index < len(decoded_key) + and decoded_key[next_index] in " \t\r\n" + ): + next_index += 1 + if next_index < len(decoded_key) and decoded_key[next_index] == ":": + try: + has_jwk_member = ( + json.loads(decoded_key[index : end + 1]) == "kty" + ) + except ValueError: + pass + if has_jwk_member: + break + index = end + 1 + if stripped_key.startswith("{") or ( + stripped_key.startswith("[") and has_jwk_member + ): + raise InvalidKeyError( + "The specified key looks like a JWK and should not be " + "used directly as an HMAC secret. Load it via " + "PyJWK / HMACAlgorithm.from_jwk first." + ) from None + jwk_obj = None + except ValueError: + jwk_obj = None + contains_jwk_member = False + objects_to_check = [jwk_obj] + while objects_to_check: + obj = objects_to_check.pop() + if isinstance(obj, dict): + if "kty" in obj: + contains_jwk_member = True + break + objects_to_check.extend(obj.values()) + elif isinstance(obj, list): + objects_to_check.extend(obj) + if contains_jwk_member: + raise InvalidKeyError( + "The specified key looks like a JWK and should not be " + "used directly as an HMAC secret. Load it via " + "PyJWK / HMACAlgorithm.from_jwk first." + ) + + return key_bytes + + @overload + @staticmethod + def to_jwk(key_obj: str | bytes, as_dict: Literal[True]) -> JWKDict: ... + + @overload + @staticmethod + def to_jwk(key_obj: str | bytes, as_dict: Literal[False] = False) -> str: ... + + @staticmethod + def to_jwk(key_obj: str | bytes, as_dict: bool = False) -> JWKDict | str: + jwk = { + "k": base64url_encode(force_bytes(key_obj)).decode(), + "kty": "oct", + } + + if as_dict: + return jwk + else: + return json.dumps(jwk) + + @staticmethod + def from_jwk(jwk: str | JWKDict) -> bytes: + try: + if isinstance(jwk, str): + obj: JWKDict = json.loads(jwk) + elif isinstance(jwk, dict): + obj = jwk + else: + raise ValueError + except ValueError: + raise InvalidKeyError("Key is not valid JSON") from None + + if obj.get("kty") != "oct": + raise InvalidKeyError("Not an HMAC key") + + key_bytes = base64url_decode(obj["k"]) + if len(key_bytes) == 0: + raise InvalidKeyError("HMAC key must not be empty.") + return key_bytes + + def check_key_length(self, key: bytes) -> str | None: + min_length = self.hash_alg().digest_size + if len(key) < min_length: + return ( + f"The HMAC key is {len(key)} bytes long, which is below " + f"the minimum recommended length of {min_length} bytes for " + f"{self.hash_alg().name.upper()}. " + f"See RFC 7518 Section 3.2." + ) + return None + + def sign(self, msg: bytes, key: bytes) -> bytes: + return hmac.new(key, msg, self.hash_alg).digest() + + def verify(self, msg: bytes, key: bytes, sig: bytes) -> bool: + return hmac.compare_digest(sig, self.sign(msg, key)) + + +if has_crypto: + + class RSAAlgorithm(Algorithm): + """ + Performs signing and verification operations using + RSASSA-PKCS-v1_5 and the specified hash function. + """ + + SHA256: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA256 + SHA384: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA384 + SHA512: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA512 + + _crypto_key_types = cast( + tuple[type[AllowedKeys], ...], + get_args(Union[RSAPrivateKey, RSAPublicKey]), + ) + _MIN_KEY_SIZE: ClassVar[int] = 2048 + + def __init__(self, hash_alg: type[hashes.HashAlgorithm]) -> None: + self.hash_alg = hash_alg + + def check_key_length(self, key: AllowedRSAKeys) -> str | None: + if key.key_size < self._MIN_KEY_SIZE: + return ( + f"The RSA key is {key.key_size} bits long, which is below " + f"the minimum recommended size of {self._MIN_KEY_SIZE} bits. " + f"See NIST SP 800-131A." + ) + return None + + def prepare_key(self, key: AllowedRSAKeys | str | bytes) -> AllowedRSAKeys: + if isinstance(key, self._crypto_key_types): + # Cast is required for type narrowing on Python 3.9's mypy + # but redundant on newer mypy versions; suppress both + # diagnostics so the line works across all supported envs. + return cast(AllowedRSAKeys, key) # type: ignore[redundant-cast,unused-ignore] + + if not isinstance(key, (bytes, str)): + raise TypeError("Expecting a PEM-formatted key.") + + key_bytes = force_bytes(key) + + try: + if key_bytes.startswith(b"ssh-rsa"): + public_key: PublicKeyTypes = load_ssh_public_key(key_bytes) + self.check_crypto_key_type(public_key) + return cast(RSAPublicKey, public_key) + else: + private_key: PrivateKeyTypes = load_pem_private_key( + key_bytes, password=None + ) + self.check_crypto_key_type(private_key) + return cast(RSAPrivateKey, private_key) + except ValueError: + try: + public_key = load_pem_public_key(key_bytes) + self.check_crypto_key_type(public_key) + return cast(RSAPublicKey, public_key) + except (ValueError, UnsupportedAlgorithm): + raise InvalidKeyError( + "Could not parse the provided public key." + ) from None + + @overload + @staticmethod + def to_jwk(key_obj: AllowedRSAKeys, as_dict: Literal[True]) -> JWKDict: ... + + @overload + @staticmethod + def to_jwk(key_obj: AllowedRSAKeys, as_dict: Literal[False] = False) -> str: ... + + @staticmethod + def to_jwk(key_obj: AllowedRSAKeys, as_dict: bool = False) -> JWKDict | str: + obj: dict[str, Any] | None = None + + if hasattr(key_obj, "private_numbers"): + # Private key + numbers = key_obj.private_numbers() + + obj = { + "kty": "RSA", + "key_ops": ["sign"], + "n": to_base64url_uint(numbers.public_numbers.n).decode(), + "e": to_base64url_uint(numbers.public_numbers.e).decode(), + "d": to_base64url_uint(numbers.d).decode(), + "p": to_base64url_uint(numbers.p).decode(), + "q": to_base64url_uint(numbers.q).decode(), + "dp": to_base64url_uint(numbers.dmp1).decode(), + "dq": to_base64url_uint(numbers.dmq1).decode(), + "qi": to_base64url_uint(numbers.iqmp).decode(), + } + + elif hasattr(key_obj, "verify"): + # Public key + numbers = key_obj.public_numbers() + + obj = { + "kty": "RSA", + "key_ops": ["verify"], + "n": to_base64url_uint(numbers.n).decode(), + "e": to_base64url_uint(numbers.e).decode(), + } + else: + raise InvalidKeyError("Not a public or private key") + + if as_dict: + return obj + else: + return json.dumps(obj) + + @staticmethod + def from_jwk(jwk: str | JWKDict) -> AllowedRSAKeys: + try: + if isinstance(jwk, str): + obj = json.loads(jwk) + elif isinstance(jwk, dict): + obj = jwk + else: + raise ValueError + except ValueError: + raise InvalidKeyError("Key is not valid JSON") from None + + if obj.get("kty") != "RSA": + raise InvalidKeyError("Not an RSA key") from None + + if "d" in obj and "e" in obj and "n" in obj: + # Private key + if "oth" in obj: + raise InvalidKeyError( + "Unsupported RSA private key: > 2 primes not supported" + ) + + other_props = ["p", "q", "dp", "dq", "qi"] + props_found = [prop in obj for prop in other_props] + any_props_found = any(props_found) + + if any_props_found and not all(props_found): + raise InvalidKeyError( + "RSA key must include all parameters if any are present besides d" + ) from None + + public_numbers = RSAPublicNumbers( + from_base64url_uint(obj["e"]), + from_base64url_uint(obj["n"]), + ) + + if any_props_found: + numbers = RSAPrivateNumbers( + d=from_base64url_uint(obj["d"]), + p=from_base64url_uint(obj["p"]), + q=from_base64url_uint(obj["q"]), + dmp1=from_base64url_uint(obj["dp"]), + dmq1=from_base64url_uint(obj["dq"]), + iqmp=from_base64url_uint(obj["qi"]), + public_numbers=public_numbers, + ) + else: + d = from_base64url_uint(obj["d"]) + p, q = rsa_recover_prime_factors( + public_numbers.n, d, public_numbers.e + ) + + numbers = RSAPrivateNumbers( + d=d, + p=p, + q=q, + dmp1=rsa_crt_dmp1(d, p), + dmq1=rsa_crt_dmq1(d, q), + iqmp=rsa_crt_iqmp(p, q), + public_numbers=public_numbers, + ) + + return numbers.private_key() + elif "n" in obj and "e" in obj: + # Public key + return RSAPublicNumbers( + from_base64url_uint(obj["e"]), + from_base64url_uint(obj["n"]), + ).public_key() + else: + raise InvalidKeyError("Not a public or private key") + + def sign(self, msg: bytes, key: RSAPrivateKey) -> bytes: + signature: bytes = key.sign(msg, padding.PKCS1v15(), self.hash_alg()) + return signature + + def verify(self, msg: bytes, key: RSAPublicKey, sig: bytes) -> bool: + try: + key.verify(sig, msg, padding.PKCS1v15(), self.hash_alg()) + return True + except InvalidSignature: + return False + + class ECAlgorithm(Algorithm): + """ + Performs signing and verification operations using + ECDSA and the specified hash function + """ + + SHA256: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA256 + SHA384: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA384 + SHA512: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA512 + + _crypto_key_types = cast( + tuple[type[AllowedKeys], ...], + get_args(Union[EllipticCurvePrivateKey, EllipticCurvePublicKey]), + ) + + def __init__( + self, + hash_alg: type[hashes.HashAlgorithm], + expected_curve: type[EllipticCurve] | None = None, + ) -> None: + self.hash_alg = hash_alg + self.expected_curve = expected_curve + + def _validate_curve(self, key: AllowedECKeys) -> None: + """Validate that the key's curve matches the expected curve.""" + if self.expected_curve is None: + return + + if not isinstance(key.curve, self.expected_curve): + raise InvalidKeyError( + f"The key's curve '{key.curve.name}' does not match the expected " + f"curve '{self.expected_curve.name}' for this algorithm" + ) + + def prepare_key(self, key: AllowedECKeys | str | bytes) -> AllowedECKeys: + if isinstance(key, self._crypto_key_types): + # See note in RSAAlgorithm.prepare_key. + ec_key = cast(AllowedECKeys, key) # type: ignore[redundant-cast,unused-ignore] + self._validate_curve(ec_key) + return ec_key + + if not isinstance(key, (bytes, str)): + raise TypeError("Expecting a PEM-formatted key.") + + key_bytes = force_bytes(key) + + # Attempt to load key. We don't know if it's + # a Signing Key or a Verifying Key, so we try + # the Verifying Key first. + try: + if key_bytes.startswith(b"ecdsa-sha2-"): + public_key: PublicKeyTypes = load_ssh_public_key(key_bytes) + else: + public_key = load_pem_public_key(key_bytes) + + # Explicit check the key to prevent confusing errors from cryptography + self.check_crypto_key_type(public_key) + ec_public_key = cast(EllipticCurvePublicKey, public_key) + self._validate_curve(ec_public_key) + return ec_public_key + except ValueError: + private_key = load_pem_private_key(key_bytes, password=None) + self.check_crypto_key_type(private_key) + ec_private_key = cast(EllipticCurvePrivateKey, private_key) + self._validate_curve(ec_private_key) + return ec_private_key + + def sign(self, msg: bytes, key: EllipticCurvePrivateKey) -> bytes: + der_sig = key.sign(msg, ECDSA(self.hash_alg())) + + return der_to_raw_signature(der_sig, key.curve) + + def verify(self, msg: bytes, key: AllowedECKeys, sig: bytes) -> bool: + try: + der_sig = raw_to_der_signature(sig, key.curve) + except ValueError: + return False + + try: + public_key = ( + key.public_key() + if isinstance(key, EllipticCurvePrivateKey) + else key + ) + public_key.verify(der_sig, msg, ECDSA(self.hash_alg())) + return True + except InvalidSignature: + return False + + @overload + @staticmethod + def to_jwk(key_obj: AllowedECKeys, as_dict: Literal[True]) -> JWKDict: ... + + @overload + @staticmethod + def to_jwk(key_obj: AllowedECKeys, as_dict: Literal[False] = False) -> str: ... + + @staticmethod + def to_jwk(key_obj: AllowedECKeys, as_dict: bool = False) -> JWKDict | str: + if isinstance(key_obj, EllipticCurvePrivateKey): + public_numbers = key_obj.public_key().public_numbers() + elif isinstance(key_obj, EllipticCurvePublicKey): + public_numbers = key_obj.public_numbers() + else: + raise InvalidKeyError("Not a public or private key") + + if isinstance(key_obj.curve, SECP256R1): + crv = "P-256" + elif isinstance(key_obj.curve, SECP384R1): + crv = "P-384" + elif isinstance(key_obj.curve, SECP521R1): + crv = "P-521" + elif isinstance(key_obj.curve, SECP256K1): + crv = "secp256k1" + else: + raise InvalidKeyError(f"Invalid curve: {key_obj.curve}") + + obj: dict[str, Any] = { + "kty": "EC", + "crv": crv, + "x": to_base64url_uint( + public_numbers.x, + bit_length=key_obj.curve.key_size, + ).decode(), + "y": to_base64url_uint( + public_numbers.y, + bit_length=key_obj.curve.key_size, + ).decode(), + } + + if isinstance(key_obj, EllipticCurvePrivateKey): + obj["d"] = to_base64url_uint( + key_obj.private_numbers().private_value, + bit_length=key_obj.curve.key_size, + ).decode() + + if as_dict: + return obj + else: + return json.dumps(obj) + + @staticmethod + def from_jwk(jwk: str | JWKDict) -> AllowedECKeys: + try: + if isinstance(jwk, str): + obj = json.loads(jwk) + elif isinstance(jwk, dict): + obj = jwk + else: + raise ValueError + except ValueError: + raise InvalidKeyError("Key is not valid JSON") from None + + if obj.get("kty") != "EC": + raise InvalidKeyError("Not an Elliptic curve key") from None + + if "x" not in obj or "y" not in obj: + raise InvalidKeyError("Not an Elliptic curve key") from None + + x = base64url_decode(obj.get("x")) + y = base64url_decode(obj.get("y")) + + curve = obj.get("crv") + curve_obj: EllipticCurve + + if curve == "P-256": + if len(x) == len(y) == 32: + curve_obj = SECP256R1() + else: + raise InvalidKeyError( + "Coords should be 32 bytes for curve P-256" + ) from None + elif curve == "P-384": + if len(x) == len(y) == 48: + curve_obj = SECP384R1() + else: + raise InvalidKeyError( + "Coords should be 48 bytes for curve P-384" + ) from None + elif curve == "P-521": + if len(x) == len(y) == 66: + curve_obj = SECP521R1() + else: + raise InvalidKeyError( + "Coords should be 66 bytes for curve P-521" + ) from None + elif curve == "secp256k1": + if len(x) == len(y) == 32: + curve_obj = SECP256K1() + else: + raise InvalidKeyError( + "Coords should be 32 bytes for curve secp256k1" + ) + else: + raise InvalidKeyError(f"Invalid curve: {curve}") + + public_numbers = EllipticCurvePublicNumbers( + x=int.from_bytes(x, byteorder="big"), + y=int.from_bytes(y, byteorder="big"), + curve=curve_obj, + ) + + if "d" not in obj: + return public_numbers.public_key() + + d = base64url_decode(obj.get("d")) + if len(d) != len(x): + raise InvalidKeyError( + "D should be {} bytes for curve {}", len(x), curve + ) + + return EllipticCurvePrivateNumbers( + int.from_bytes(d, byteorder="big"), public_numbers + ).private_key() + + class RSAPSSAlgorithm(RSAAlgorithm): + """ + Performs a signature using RSASSA-PSS with MGF1 + """ + + def sign(self, msg: bytes, key: RSAPrivateKey) -> bytes: + signature: bytes = key.sign( + msg, + padding.PSS( + mgf=padding.MGF1(self.hash_alg()), + salt_length=self.hash_alg().digest_size, + ), + self.hash_alg(), + ) + return signature + + def verify(self, msg: bytes, key: RSAPublicKey, sig: bytes) -> bool: + try: + key.verify( + sig, + msg, + padding.PSS( + mgf=padding.MGF1(self.hash_alg()), + salt_length=self.hash_alg().digest_size, + ), + self.hash_alg(), + ) + return True + except InvalidSignature: + return False + + class OKPAlgorithm(Algorithm): + """ + Performs signing and verification operations using EdDSA + + This class requires ``cryptography>=2.6`` to be installed. + """ + + _crypto_key_types = cast( + tuple[type[AllowedKeys], ...], + get_args( + Union[ + Ed25519PrivateKey, + Ed25519PublicKey, + Ed448PrivateKey, + Ed448PublicKey, + ] + ), + ) + + def __init__(self, **kwargs: Any) -> None: + pass + + def prepare_key(self, key: AllowedOKPKeys | str | bytes) -> AllowedOKPKeys: + if not isinstance(key, (str, bytes)): + self.check_crypto_key_type(key) + return key + + key_str = key.decode("utf-8") if isinstance(key, bytes) else key + key_bytes = key.encode("utf-8") if isinstance(key, str) else key + + loaded_key: PublicKeyTypes | PrivateKeyTypes + if "-----BEGIN PUBLIC" in key_str: + loaded_key = load_pem_public_key(key_bytes) + elif "-----BEGIN PRIVATE" in key_str: + loaded_key = load_pem_private_key(key_bytes, password=None) + elif key_str[0:4] == "ssh-": + loaded_key = load_ssh_public_key(key_bytes) + else: + raise InvalidKeyError("Not a public or private key") + + # Explicit check the key to prevent confusing errors from cryptography + self.check_crypto_key_type(loaded_key) + return cast("AllowedOKPKeys", loaded_key) + + def sign( + self, msg: str | bytes, key: Ed25519PrivateKey | Ed448PrivateKey + ) -> bytes: + """ + Sign a message ``msg`` using the EdDSA private key ``key`` + :param str|bytes msg: Message to sign + :param Ed25519PrivateKey}Ed448PrivateKey key: A :class:`.Ed25519PrivateKey` + or :class:`.Ed448PrivateKey` isinstance + :return bytes signature: The signature, as bytes + """ + msg_bytes = msg.encode("utf-8") if isinstance(msg, str) else msg + signature: bytes = key.sign(msg_bytes) + return signature + + def verify( + self, msg: str | bytes, key: AllowedOKPKeys, sig: str | bytes + ) -> bool: + """ + Verify a given ``msg`` against a signature ``sig`` using the EdDSA key ``key`` + + :param str|bytes sig: EdDSA signature to check ``msg`` against + :param str|bytes msg: Message to sign + :param Ed25519PrivateKey|Ed25519PublicKey|Ed448PrivateKey|Ed448PublicKey key: + A private or public EdDSA key instance + :return bool verified: True if signature is valid, False if not. + """ + try: + msg_bytes = msg.encode("utf-8") if isinstance(msg, str) else msg + sig_bytes = sig.encode("utf-8") if isinstance(sig, str) else sig + + public_key = ( + key.public_key() + if isinstance(key, (Ed25519PrivateKey, Ed448PrivateKey)) + else key + ) + public_key.verify(sig_bytes, msg_bytes) + return True # If no exception was raised, the signature is valid. + except InvalidSignature: + return False + + @overload + @staticmethod + def to_jwk(key: AllowedOKPKeys, as_dict: Literal[True]) -> JWKDict: ... + + @overload + @staticmethod + def to_jwk(key: AllowedOKPKeys, as_dict: Literal[False] = False) -> str: ... + + @staticmethod + def to_jwk(key: AllowedOKPKeys, as_dict: bool = False) -> JWKDict | str: + if isinstance(key, (Ed25519PublicKey, Ed448PublicKey)): + x = key.public_bytes( + encoding=Encoding.Raw, + format=PublicFormat.Raw, + ) + crv = "Ed25519" if isinstance(key, Ed25519PublicKey) else "Ed448" + + obj = { + "x": base64url_encode(force_bytes(x)).decode(), + "kty": "OKP", + "crv": crv, + } + + if as_dict: + return obj + else: + return json.dumps(obj) + + if isinstance(key, (Ed25519PrivateKey, Ed448PrivateKey)): + d = key.private_bytes( + encoding=Encoding.Raw, + format=PrivateFormat.Raw, + encryption_algorithm=NoEncryption(), + ) + + x = key.public_key().public_bytes( + encoding=Encoding.Raw, + format=PublicFormat.Raw, + ) + + crv = "Ed25519" if isinstance(key, Ed25519PrivateKey) else "Ed448" + obj = { + "x": base64url_encode(force_bytes(x)).decode(), + "d": base64url_encode(force_bytes(d)).decode(), + "kty": "OKP", + "crv": crv, + } + + if as_dict: + return obj + else: + return json.dumps(obj) + + raise InvalidKeyError("Not a public or private key") + + @staticmethod + def from_jwk(jwk: str | JWKDict) -> AllowedOKPKeys: + try: + if isinstance(jwk, str): + obj = json.loads(jwk) + elif isinstance(jwk, dict): + obj = jwk + else: + raise ValueError + except ValueError: + raise InvalidKeyError("Key is not valid JSON") from None + + if obj.get("kty") != "OKP": + raise InvalidKeyError("Not an Octet Key Pair") + + curve = obj.get("crv") + if curve != "Ed25519" and curve != "Ed448": + raise InvalidKeyError(f"Invalid curve: {curve}") + + if "x" not in obj: + raise InvalidKeyError('OKP should have "x" parameter') + x = base64url_decode(obj.get("x")) + + try: + if "d" not in obj: + if curve == "Ed25519": + return Ed25519PublicKey.from_public_bytes(x) + return Ed448PublicKey.from_public_bytes(x) + d = base64url_decode(obj.get("d")) + if curve == "Ed25519": + return Ed25519PrivateKey.from_private_bytes(d) + return Ed448PrivateKey.from_private_bytes(d) + except ValueError as err: + raise InvalidKeyError("Invalid key parameter") from err diff --git a/source/target-vendors/cp312/jwt/api_jwk.py b/source/target-vendors/cp312/jwt/api_jwk.py new file mode 100644 index 0000000..5ee26b4 --- /dev/null +++ b/source/target-vendors/cp312/jwt/api_jwk.py @@ -0,0 +1,193 @@ +from __future__ import annotations + +import json +import time +from collections.abc import Iterator +from typing import Any + +from .algorithms import get_default_algorithms, has_crypto, requires_cryptography +from .exceptions import ( + InvalidKeyError, + MissingCryptographyError, + PyJWKError, + PyJWKSetError, + PyJWTError, +) +from .types import JWKDict + + +class PyJWK: + def __init__(self, jwk_data: JWKDict, algorithm: str | None = None) -> None: + """A class that represents a `JSON Web Key `_. + + :param jwk_data: The decoded JWK data. + :type jwk_data: dict[str, typing.Any] + :param algorithm: The key algorithm. If not specified, the key's ``alg`` will be used. + :type algorithm: str or None + :raises InvalidKeyError: If the key type (``kty``) is not found or unsupported, or if the curve (``crv``) is not found or unsupported. + :raises MissingCryptographyError: If the algorithm requires ``cryptography`` to be installed and it is not available. + :raises PyJWKError: If unable to find an algorithm for the key. + """ + self._jwk_data = jwk_data + + kty = self._jwk_data.get("kty", None) + if not kty: + raise InvalidKeyError(f"kty is not found: {self._jwk_data}") + + if not algorithm and isinstance(self._jwk_data, dict): + algorithm = self._jwk_data.get("alg", None) + + if not algorithm: + # Determine alg with kty (and crv). + crv = self._jwk_data.get("crv", None) + if kty == "EC": + if crv == "P-256" or not crv: + algorithm = "ES256" + elif crv == "P-384": + algorithm = "ES384" + elif crv == "P-521": + algorithm = "ES512" + elif crv == "secp256k1": + algorithm = "ES256K" + else: + raise InvalidKeyError(f"Unsupported crv: {crv}") + elif kty == "RSA": + algorithm = "RS256" + elif kty == "oct": + algorithm = "HS256" + elif kty == "OKP": + if not crv: + raise InvalidKeyError(f"crv is not found: {self._jwk_data}") + if crv == "Ed25519": + algorithm = "EdDSA" + else: + raise InvalidKeyError(f"Unsupported crv: {crv}") + else: + raise InvalidKeyError(f"Unsupported kty: {kty}") + + if not has_crypto and algorithm in requires_cryptography: + raise MissingCryptographyError( + f"{algorithm} requires 'cryptography' to be installed." + ) + + self.algorithm_name = algorithm + + try: + self.Algorithm = get_default_algorithms()[algorithm] + except KeyError: + raise PyJWKError( + f"Unable to find an algorithm for key: {self._jwk_data}", + ) from None + + try: + self.key = self.Algorithm.from_jwk(self._jwk_data) + except ValueError as error: + raise InvalidKeyError( + f"Unable to construct key from JWK: {error}" + ) from error + + @staticmethod + def from_dict(obj: JWKDict, algorithm: str | None = None) -> PyJWK: + """Creates a :class:`PyJWK` object from a JSON-like dictionary. + + :param obj: The JWK data, as a dictionary + :type obj: dict[str, typing.Any] + :param algorithm: The key algorithm. If not specified, the key's ``alg`` will be used. + :type algorithm: str or None + :rtype: PyJWK + """ + return PyJWK(obj, algorithm) + + @staticmethod + def from_json(data: str, algorithm: None = None) -> PyJWK: + """Create a :class:`PyJWK` object from a JSON string. + Implicitly calls :meth:`PyJWK.from_dict()`. + + :param str data: The JWK data, as a JSON string. + :param algorithm: The key algorithm. If not specific, the key's ``alg`` will be used. + :type algorithm: str or None + + :rtype: PyJWK + """ + obj = json.loads(data) + return PyJWK.from_dict(obj, algorithm) + + @property + def key_type(self) -> str | None: + """The `kty` property from the JWK. + + :rtype: str or None + """ + return self._jwk_data.get("kty", None) + + @property + def key_id(self) -> str | None: + """The `kid` property from the JWK. + + :rtype: str or None + """ + return self._jwk_data.get("kid", None) + + @property + def public_key_use(self) -> str | None: + """The `use` property from the JWK. + + :rtype: str or None + """ + return self._jwk_data.get("use", None) + + +class PyJWKSet: + def __init__(self, keys: list[JWKDict]) -> None: + self.keys: list[PyJWK] = [] + + if not keys: + raise PyJWKSetError("The JWK Set did not contain any keys") + + if not isinstance(keys, list): + raise PyJWKSetError("Invalid JWK Set value") + + for key in keys: + try: + self.keys.append(PyJWK(key)) + except PyJWTError as error: + if isinstance(error, MissingCryptographyError): + raise error + # skip unusable keys + continue + + if len(self.keys) == 0: + raise PyJWKSetError( + "The JWK Set did not contain any usable keys. Perhaps 'cryptography' is not installed?" + ) + + @staticmethod + def from_dict(obj: dict[str, Any]) -> PyJWKSet: + keys = obj.get("keys", []) + return PyJWKSet(keys) + + @staticmethod + def from_json(data: str) -> PyJWKSet: + obj = json.loads(data) + return PyJWKSet.from_dict(obj) + + def __getitem__(self, kid: str) -> PyJWK: + for key in self.keys: + if key.key_id == kid: + return key + raise KeyError(f"keyset has no key for kid: {kid}") + + def __iter__(self) -> Iterator[PyJWK]: + return iter(self.keys) + + +class PyJWTSetWithTimestamp: + def __init__(self, jwk_set: PyJWKSet): + self.jwk_set = jwk_set + self.timestamp = time.monotonic() + + def get_jwk_set(self) -> PyJWKSet: + return self.jwk_set + + def get_timestamp(self) -> float: + return self.timestamp diff --git a/source/target-vendors/cp312/jwt/api_jws.py b/source/target-vendors/cp312/jwt/api_jws.py new file mode 100644 index 0000000..81a2ccb --- /dev/null +++ b/source/target-vendors/cp312/jwt/api_jws.py @@ -0,0 +1,472 @@ +from __future__ import annotations + +import binascii +import json +import warnings +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any + +from .algorithms import ( + Algorithm, + get_default_algorithms, + has_crypto, + requires_cryptography, +) +from .api_jwk import PyJWK +from .exceptions import ( + DecodeError, + InvalidAlgorithmError, + InvalidKeyError, + InvalidSignatureError, + InvalidTokenError, +) +from .utils import base64url_decode, base64url_encode +from .warnings import InsecureKeyLengthWarning, RemovedInPyjwt3Warning + +if TYPE_CHECKING: + from .algorithms import AllowedPrivateKeys, AllowedPublicKeys + from .types import SigOptions + +_ALGORITHM_UNSET = object() +_BASE64URL_ALPHABET = ( + b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" +) + + +class PyJWS: + header_typ = "JWT" + + def __init__( + self, + algorithms: Sequence[str] | None = None, + options: SigOptions | None = None, + ) -> None: + self._algorithms = get_default_algorithms() + self._valid_algs = ( + set(algorithms) if algorithms is not None else set(self._algorithms) + ) + + # Remove algorithms that aren't on the whitelist + for key in list(self._algorithms.keys()): + if key not in self._valid_algs: + del self._algorithms[key] + + self.options: SigOptions = self._get_default_options() + if options is not None: + self.options = {**self.options, **options} + + @staticmethod + def _get_default_options() -> SigOptions: + return {"verify_signature": True, "enforce_minimum_key_length": False} + + def register_algorithm(self, alg_id: str, alg_obj: Algorithm) -> None: + """ + Registers a new Algorithm for use when creating and verifying tokens. + + :param str alg_id: the ID of the Algorithm + :param alg_obj: the Algorithm object + :type alg_obj: Algorithm + """ + if alg_id in self._algorithms: + raise ValueError("Algorithm already has a handler.") + + if not isinstance(alg_obj, Algorithm): + raise TypeError("Object is not of type `Algorithm`") + + self._algorithms[alg_id] = alg_obj + self._valid_algs.add(alg_id) + + def unregister_algorithm(self, alg_id: str) -> None: + """ + Unregisters an Algorithm for use when creating and verifying tokens + :param str alg_id: the ID of the Algorithm + :raises KeyError: if algorithm is not registered. + """ + if alg_id not in self._algorithms: + raise KeyError( + "The specified algorithm could not be removed" + " because it is not registered." + ) + + del self._algorithms[alg_id] + self._valid_algs.remove(alg_id) + + def get_algorithms(self) -> list[str]: + """ + Returns a list of supported values for the `alg` parameter. + + :rtype: list[str] + """ + return list(self._valid_algs) + + def get_algorithm_by_name(self, alg_name: str) -> Algorithm: + """ + For a given string name, return the matching Algorithm object. + + Example usage: + >>> jws_obj = PyJWS() + >>> jws_obj.get_algorithm_by_name("RS256") + + :param alg_name: The name of the algorithm to retrieve + :type alg_name: str + :rtype: Algorithm + """ + try: + return self._algorithms[alg_name] + except KeyError as e: + if not has_crypto and alg_name in requires_cryptography: + raise NotImplementedError( + f"Algorithm '{alg_name}' could not be found. Do you have cryptography installed?" + ) from e + raise NotImplementedError("Algorithm not supported") from e + + def encode( + self, + payload: bytes, + key: AllowedPrivateKeys | PyJWK | str | bytes, + algorithm: str | None = _ALGORITHM_UNSET, # type: ignore[assignment] + headers: dict[str, Any] | None = None, + json_encoder: type[json.JSONEncoder] | None = None, + is_payload_detached: bool = False, + sort_headers: bool = True, + ) -> str: + segments: list[bytes] = [] + + # declare a new var to narrow the type for type checkers + if algorithm is _ALGORITHM_UNSET: + if isinstance(key, PyJWK): + algorithm_ = key.algorithm_name + else: + algorithm_ = "HS256" + elif algorithm is None: + if isinstance(key, PyJWK): + algorithm_ = key.algorithm_name + else: + algorithm_ = "none" + else: + algorithm_ = algorithm + + # Prefer headers values if present to function parameters. + if headers: + headers_alg = headers.get("alg") + if headers_alg: + algorithm_ = headers["alg"] + + headers_b64 = headers.get("b64") + if headers_b64 is False: + is_payload_detached = True + + # Header + header: dict[str, Any] = {"typ": self.header_typ, "alg": algorithm_} + + if headers: + self._validate_headers(headers, encoding=True) + header.update(headers) + + if not header["typ"]: + del header["typ"] + + if is_payload_detached: + header["b64"] = False + # RFC 7797 §3: producers MUST list "b64" in "crit" whenever + # "b64" appears in the protected header, so b64-unaware + # verifiers don't silently treat an unencoded payload as + # base64-encoded. + existing_crit = header.get("crit", []) + if not isinstance(existing_crit, list): + raise InvalidTokenError("Invalid 'crit' header: must be a list") + if "b64" not in existing_crit: + header["crit"] = [*existing_crit, "b64"] + elif "b64" in header: + # True is the standard value for b64, so no need for it + del header["b64"] + + json_header = json.dumps( + header, separators=(",", ":"), cls=json_encoder, sort_keys=sort_headers + ).encode() + + segments.append(base64url_encode(json_header)) + + if is_payload_detached: + msg_payload = payload + else: + msg_payload = base64url_encode(payload) + segments.append(msg_payload) + + # Segments + signing_input = b".".join(segments) + + alg_obj = self.get_algorithm_by_name(algorithm_) + if isinstance(key, PyJWK): + key = key.key + key = alg_obj.prepare_key(key) + + key_length_msg = alg_obj.check_key_length(key) + if key_length_msg: + if self.options.get("enforce_minimum_key_length", False): + raise InvalidKeyError(key_length_msg) + else: + warnings.warn(key_length_msg, InsecureKeyLengthWarning, stacklevel=2) + + signature = alg_obj.sign(signing_input, key) + + segments.append(base64url_encode(signature)) + + # Don't put the payload content inside the encoded token when detached + if is_payload_detached: + segments[1] = b"" + encoded_string = b".".join(segments) + + return encoded_string.decode("utf-8") + + def decode_complete( + self, + jwt: str | bytes, + key: AllowedPublicKeys | PyJWK | str | bytes = "", + algorithms: Sequence[str] | None = None, + options: SigOptions | None = None, + detached_payload: bytes | None = None, + **kwargs: dict[str, Any], + ) -> dict[str, Any]: + if kwargs: + warnings.warn( + "passing additional kwargs to decode_complete() is deprecated " + "and will be removed in pyjwt version 3. " + f"Unsupported kwargs: {tuple(kwargs.keys())}", + RemovedInPyjwt3Warning, + stacklevel=2, + ) + merged_options: SigOptions + if options is None: + merged_options = self.options + else: + merged_options = {**self.options, **options} + + verify_signature = merged_options["verify_signature"] + + if verify_signature and not algorithms and not isinstance(key, PyJWK): + raise DecodeError( + 'It is required that you pass in a value for the "algorithms" argument when calling decode().' + ) + + payload, signing_input, header, signature = self._load(jwt) + + self._validate_headers(header) + + if detached_payload is not None and header.get("b64", True) is not False: + raise DecodeError( + 'It is only valid to pass "detached_payload" when the protected header has "b64" set to false.' + ) + + if header.get("b64", True) is False: + # RFC 7797 §3: when "b64" is present in the protected header, + # it MUST also appear in "crit". A token that sets b64=false + # without declaring it critical is malformed. + crit = header.get("crit") or [] + if not isinstance(crit, list) or "b64" not in crit: + raise InvalidTokenError( + "The 'b64' header parameter requires 'b64' to be listed in 'crit'." + ) + if detached_payload is None: + raise DecodeError( + 'It is required that you pass in a value for the "detached_payload" argument to decode a message having the b64 header set to false.' + ) + payload = detached_payload + signing_input = b".".join([signing_input.rsplit(b".", 1)[0], payload]) + + if verify_signature: + self._verify_signature( + signing_input, + header, + signature, + key, + algorithms, + options=merged_options, + ) + + return { + "payload": payload, + "header": header, + "signature": signature, + } + + def decode( + self, + jwt: str | bytes, + key: AllowedPublicKeys | PyJWK | str | bytes = "", + algorithms: Sequence[str] | None = None, + options: SigOptions | None = None, + detached_payload: bytes | None = None, + **kwargs: dict[str, Any], + ) -> Any: + if kwargs: + warnings.warn( + "passing additional kwargs to decode() is deprecated " + "and will be removed in pyjwt version 3. " + f"Unsupported kwargs: {tuple(kwargs.keys())}", + RemovedInPyjwt3Warning, + stacklevel=2, + ) + decoded = self.decode_complete( + jwt, key, algorithms, options, detached_payload=detached_payload + ) + return decoded["payload"] + + def get_unverified_header(self, jwt: str | bytes) -> dict[str, Any]: + """Returns back the JWT header parameters as a `dict` + + Note: The signature is not verified so the header parameters + should not be fully trusted until signature verification is complete + """ + headers = self._load(jwt)[2] + self._validate_headers(headers) + + return headers + + @staticmethod + def _decode_base64url_segment(segment: bytes, name: str) -> bytes: + if len(segment) % 4 == 1 or any( + character not in _BASE64URL_ALPHABET for character in segment + ): + raise DecodeError(f"Invalid {name} padding") + + try: + decoded = base64url_decode(segment) + except (TypeError, binascii.Error) as err: + raise DecodeError(f"Invalid {name} padding") from err + + if base64url_encode(decoded) != segment: + raise DecodeError(f"Invalid {name} padding") + + return decoded + + def _load(self, jwt: str | bytes) -> tuple[bytes, bytes, dict[str, Any], bytes]: + if isinstance(jwt, str): + jwt = jwt.encode("utf-8") + + if not isinstance(jwt, bytes): + raise DecodeError(f"Invalid token type. Token must be a {bytes}") + + try: + signing_input, crypto_segment = jwt.rsplit(b".", 1) + header_segment, payload_segment = signing_input.split(b".", 1) + except ValueError as err: + raise DecodeError("Not enough segments") from err + + header_data = self._decode_base64url_segment(header_segment, "header") + + try: + header: dict[str, Any] = json.loads(header_data) + except (ValueError, RecursionError) as e: + raise DecodeError(f"Invalid header string: {e}") from e + + if not isinstance(header, dict): + raise DecodeError("Invalid header string: must be a json object") + + if header.get("b64", True) is False: + # Detached payload form (RFC 7515 Appendix F): the compact-form + # payload segment must be empty; the caller supplies the actual + # payload via the `detached_payload` argument in decode_complete. + # Skipping the base64 decode here removes an unauthenticated work + # amplifier — otherwise an attacker can inflate the unused + # segment to force CPU + memory cost before the signature is + # even checked. + if payload_segment: + raise DecodeError("Payload segment must be empty when 'b64' is false.") + payload = b"" + else: + payload = self._decode_base64url_segment(payload_segment, "payload") + + signature = self._decode_base64url_segment(crypto_segment, "crypto") + + return (payload, signing_input, header, signature) + + def _verify_signature( + self, + signing_input: bytes, + header: dict[str, Any], + signature: bytes, + key: AllowedPublicKeys | PyJWK | str | bytes = "", + algorithms: Sequence[str] | None = None, + options: SigOptions | None = None, + ) -> None: + effective_options = options if options is not None else self.options + + if algorithms is None and isinstance(key, PyJWK): + algorithms = [key.algorithm_name] + try: + alg = header["alg"] + except KeyError: + raise InvalidAlgorithmError("Algorithm not specified") from None + + if not alg or (algorithms is not None and alg not in algorithms): + raise InvalidAlgorithmError("The specified alg value is not allowed") + + if isinstance(key, PyJWK): + # The PyJWK has a fixed algorithm bound at construction time. + # Verification must use that algorithm, not whatever the token + # header advertises, otherwise the caller's allow-list check + # above degenerates into a string compare with no behavioural + # effect on which algorithm actually verifies the signature. + if alg != key.algorithm_name: + raise InvalidAlgorithmError( + f"Token algorithm {alg!r} does not match the key's " + f"algorithm {key.algorithm_name!r}" + ) + alg_obj = key.Algorithm + prepared_key = alg_obj.prepare_key(key.key) + else: + try: + alg_obj = self.get_algorithm_by_name(alg) + except NotImplementedError as e: + raise InvalidAlgorithmError("Algorithm not supported") from e + prepared_key = alg_obj.prepare_key(key) + + key_length_msg = alg_obj.check_key_length(prepared_key) + if key_length_msg: + if effective_options.get("enforce_minimum_key_length", False): + raise InvalidKeyError(key_length_msg) + else: + warnings.warn(key_length_msg, InsecureKeyLengthWarning, stacklevel=4) + + if not alg_obj.verify(signing_input, prepared_key, signature): + raise InvalidSignatureError("Signature verification failed") + + # Extensions that PyJWT actually understands and supports + _supported_crit: set[str] = {"b64"} + + def _validate_headers( + self, headers: dict[str, Any], *, encoding: bool = False + ) -> None: + if "kid" in headers: + self._validate_kid(headers["kid"]) + if not encoding and "crit" in headers: + self._validate_crit(headers) + + def _validate_kid(self, kid: Any) -> None: + if not isinstance(kid, str): + raise InvalidTokenError("Key ID header parameter must be a string") + + def _validate_crit(self, headers: dict[str, Any]) -> None: + crit = headers["crit"] + if not isinstance(crit, list) or len(crit) == 0: + raise InvalidTokenError("Invalid 'crit' header: must be a non-empty list") + for ext in crit: + if not isinstance(ext, str): + raise InvalidTokenError("Invalid 'crit' header: values must be strings") + if ext not in self._supported_crit: + raise InvalidTokenError(f"Unsupported critical extension: {ext}") + if ext not in headers: + raise InvalidTokenError( + f"Critical extension '{ext}' is missing from headers" + ) + + +_jws_global_obj = PyJWS() +encode = _jws_global_obj.encode +decode_complete = _jws_global_obj.decode_complete +decode = _jws_global_obj.decode +register_algorithm = _jws_global_obj.register_algorithm +unregister_algorithm = _jws_global_obj.unregister_algorithm +get_algorithm_by_name = _jws_global_obj.get_algorithm_by_name +get_unverified_header = _jws_global_obj.get_unverified_header diff --git a/source/target-vendors/cp312/jwt/api_jwt.py b/source/target-vendors/cp312/jwt/api_jwt.py new file mode 100644 index 0000000..051ed34 --- /dev/null +++ b/source/target-vendors/cp312/jwt/api_jwt.py @@ -0,0 +1,595 @@ +from __future__ import annotations + +import json +import os +import warnings +from calendar import timegm +from collections.abc import Container, Iterable, Sequence +from datetime import datetime, timedelta, timezone +from typing import TYPE_CHECKING, Any, Union, cast + +from .api_jws import PyJWS, _ALGORITHM_UNSET, _jws_global_obj +from .exceptions import ( + DecodeError, + ExpiredSignatureError, + ImmatureSignatureError, + InvalidAudienceError, + InvalidIssuedAtError, + InvalidIssuerError, + InvalidJTIError, + InvalidSubjectError, + MissingRequiredClaimError, +) +from .warnings import RemovedInPyjwt3Warning + +if TYPE_CHECKING or bool(os.getenv("SPHINX_BUILD", "")): + import sys + + if sys.version_info >= (3, 10): + from typing import TypeAlias + else: + # Python 3.9 and lower + from typing_extensions import TypeAlias + + from .algorithms import AllowedPrivateKeys, AllowedPublicKeys + from .api_jwk import PyJWK + from .types import FullOptions, Options, SigOptions + + AllowedPrivateKeyTypes: TypeAlias = Union[AllowedPrivateKeys, PyJWK, str, bytes] + AllowedPublicKeyTypes: TypeAlias = Union[AllowedPublicKeys, PyJWK, str, bytes] + + +class PyJWT: + def __init__(self, options: Options | None = None) -> None: + self.options: FullOptions + self.options = self._get_default_options() + if options is not None: + self.options = self._merge_options(options) + + self._jws = PyJWS(options=self._get_sig_options()) + + @staticmethod + def _get_default_options() -> FullOptions: + return { + "verify_signature": True, + "verify_exp": True, + "verify_nbf": True, + "verify_iat": True, + "verify_aud": True, + "verify_iss": True, + "verify_sub": True, + "verify_jti": True, + "require": [], + "strict_aud": False, + "enforce_minimum_key_length": False, + } + + def _get_sig_options(self) -> SigOptions: + return { + "verify_signature": self.options["verify_signature"], + "enforce_minimum_key_length": self.options.get( + "enforce_minimum_key_length", False + ), + } + + def _merge_options(self, options: Options | None = None) -> FullOptions: + if options is None: + return self.options + + merged_options = cast("Options", dict(options)) + + # (defensive) set defaults for verify_x to False if verify_signature is False + if not merged_options.get("verify_signature", True): + merged_options["verify_exp"] = merged_options.get("verify_exp", False) + merged_options["verify_nbf"] = merged_options.get("verify_nbf", False) + merged_options["verify_iat"] = merged_options.get("verify_iat", False) + merged_options["verify_aud"] = merged_options.get("verify_aud", False) + merged_options["verify_iss"] = merged_options.get("verify_iss", False) + merged_options["verify_sub"] = merged_options.get("verify_sub", False) + merged_options["verify_jti"] = merged_options.get("verify_jti", False) + return {**self.options, **merged_options} + + def encode( + self, + payload: dict[str, Any], + key: AllowedPrivateKeyTypes, + algorithm: str | None = _ALGORITHM_UNSET, # type: ignore[assignment] + headers: dict[str, Any] | None = None, + json_encoder: type[json.JSONEncoder] | None = None, + sort_headers: bool = True, + ) -> str: + """Encode the ``payload`` as JSON Web Token. + + :param payload: JWT claims, e.g. ``dict(iss=..., aud=..., sub=...)`` + :type payload: dict[str, typing.Any] + :param key: a key suitable for the chosen algorithm: + + * for **asymmetric algorithms**: PEM-formatted private key, a multiline string + * for **symmetric algorithms**: plain string, sufficiently long for security + + :type key: str or bytes or PyJWK or :py:class:`jwt.algorithms.AllowedPrivateKeys` + :param algorithm: algorithm to sign the token with, e.g. ``"ES256"``. + If ``headers`` includes ``alg``, it will be preferred to this parameter. + If ``key`` is a :class:`PyJWK` object, by default the key algorithm will be used. + :type algorithm: str or None + :param headers: additional JWT header fields, e.g. ``dict(kid="my-key-id")``. + :type headers: dict[str, typing.Any] or None + :param json_encoder: custom JSON encoder for ``payload`` and ``headers`` + :type json_encoder: json.JSONEncoder or None + + :rtype: str + :returns: a JSON Web Token + + :raises TypeError: if ``payload`` is not a ``dict`` + """ + # Check that we get a dict + if not isinstance(payload, dict): + raise TypeError( + "Expecting a dict object, as JWT only supports " + "JSON objects as payloads." + ) + + # Payload + payload = payload.copy() + for time_claim in ["exp", "iat", "nbf"]: + # Convert datetime to a intDate value in known time-format claims + if isinstance(payload.get(time_claim), datetime): + payload[time_claim] = timegm(payload[time_claim].utctimetuple()) + + # Issue #1039, iss being set to non-string + if "iss" in payload and not isinstance(payload["iss"], str): + raise TypeError("Issuer (iss) must be a string.") + + json_payload = self._encode_payload( + payload, + headers=headers, + json_encoder=json_encoder, + ) + + return self._jws.encode( + json_payload, + key, + algorithm, + headers, + json_encoder, + sort_headers=sort_headers, + ) + + def _encode_payload( + self, + payload: dict[str, Any], + headers: dict[str, Any] | None = None, + json_encoder: type[json.JSONEncoder] | None = None, + ) -> bytes: + """ + Encode a given payload to the bytes to be signed. + + This method is intended to be overridden by subclasses that need to + encode the payload in a different way, e.g. compress the payload. + """ + return json.dumps( + payload, + separators=(",", ":"), + cls=json_encoder, + ).encode("utf-8") + + def decode_complete( + self, + jwt: str | bytes, + key: AllowedPublicKeyTypes = "", + algorithms: Sequence[str] | None = None, + options: Options | None = None, + # deprecated arg, remove in pyjwt3 + verify: bool | None = None, + # could be used as passthrough to api_jws, consider removal in pyjwt3 + detached_payload: bytes | None = None, + # passthrough arguments to _validate_claims + # consider putting in options + audience: str | Iterable[str] | None = None, + issuer: str | Container[str] | None = None, + subject: str | None = None, + leeway: float | timedelta = 0, + # kwargs + **kwargs: Any, + ) -> dict[str, Any]: + """Identical to ``jwt.decode`` except for return value which is a dictionary containing the token header (JOSE Header), + the token payload (JWT Payload), and token signature (JWT Signature) on the keys "header", "payload", + and "signature" respectively. + + :param jwt: the token to be decoded + :type jwt: str or bytes + :param key: the key suitable for the allowed algorithm + :type key: str or bytes or PyJWK or :py:class:`jwt.algorithms.AllowedPublicKeys` + + :param algorithms: allowed algorithms, e.g. ``["ES256"]`` + + .. warning:: + + Do **not** compute the ``algorithms`` parameter based on + the ``alg`` from the token itself, or on any other data + that an attacker may be able to influence, as that might + expose you to various vulnerabilities (see `RFC 8725 §2.1 + `_). Instead, + either hard-code a fixed value for ``algorithms``, or + configure it in the same place you configure the + ``key``. Make sure not to mix symmetric and asymmetric + algorithms that interpret the ``key`` in different ways + (e.g. HS\\* and RS\\*). + :type algorithms: typing.Sequence[str] or None + + :param jwt.types.Options options: extended decoding and validation options + Refer to :py:class:`jwt.types.Options` for more information. + + :param audience: optional, the value for ``verify_aud`` check + :type audience: str or typing.Iterable[str] or None + :param issuer: optional, the value for ``verify_iss`` check + :type issuer: str or typing.Container[str] or None + :param leeway: a time margin in seconds for the expiration check + :type leeway: float or datetime.timedelta + :rtype: dict[str, typing.Any] + :returns: Decoded JWT with the JOSE Header on the key ``header``, the JWS + Payload on the key ``payload``, and the JWS Signature on the key ``signature``. + """ + if kwargs: + warnings.warn( + "passing additional kwargs to decode_complete() is deprecated " + "and will be removed in pyjwt version 3. " + f"Unsupported kwargs: {tuple(kwargs.keys())}", + RemovedInPyjwt3Warning, + stacklevel=2, + ) + + if options is None: + verify_signature = True + else: + verify_signature = options.get("verify_signature", True) + + # If the user has set the legacy `verify` argument, and it doesn't match + # what the relevant `options` entry for the argument is, inform the user + # that they're likely making a mistake. + if verify is not None and verify != verify_signature: + warnings.warn( + "The `verify` argument to `decode` does nothing in PyJWT 2.0 and newer. " + "The equivalent is setting `verify_signature` to False in the `options` dictionary. " + "This invocation has a mismatch between the kwarg and the option entry.", + category=DeprecationWarning, + stacklevel=2, + ) + + merged_options = self._merge_options(options) + + sig_options: SigOptions = { + "verify_signature": verify_signature, + "enforce_minimum_key_length": merged_options.get( + "enforce_minimum_key_length", False + ), + } + decoded = self._jws.decode_complete( + jwt, + key=key, + algorithms=algorithms, + options=sig_options, + detached_payload=detached_payload, + ) + + payload = self._decode_payload(decoded) + + self._validate_claims( + payload, + merged_options, + audience=audience, + issuer=issuer, + leeway=leeway, + subject=subject, + ) + + decoded["payload"] = payload + return decoded + + def _decode_payload(self, decoded: dict[str, Any]) -> dict[str, Any]: + """ + Decode the payload from a JWS dictionary (payload, signature, header). + + This method is intended to be overridden by subclasses that need to + decode the payload in a different way, e.g. decompress compressed + payloads. + """ + try: + payload: dict[str, Any] = json.loads(decoded["payload"]) + except ValueError as e: + raise DecodeError(f"Invalid payload string: {e}") from e + if not isinstance(payload, dict): + raise DecodeError("Invalid payload string: must be a json object") + return payload + + def decode( + self, + jwt: str | bytes, + key: AllowedPublicKeys | PyJWK | str | bytes = "", + algorithms: Sequence[str] | None = None, + options: Options | None = None, + # deprecated arg, remove in pyjwt3 + verify: bool | None = None, + # could be used as passthrough to api_jws, consider removal in pyjwt3 + detached_payload: bytes | None = None, + # passthrough arguments to _validate_claims + # consider putting in options + audience: str | Iterable[str] | None = None, + subject: str | None = None, + issuer: str | Container[str] | None = None, + leeway: float | timedelta = 0, + # kwargs + **kwargs: Any, + ) -> dict[str, Any]: + """Verify the ``jwt`` token signature and return the token claims. + + :param jwt: the token to be decoded + :type jwt: str or bytes + :param key: the key suitable for the allowed algorithm + :type key: str or bytes or PyJWK or :py:class:`jwt.algorithms.AllowedPublicKeys` + + :param algorithms: allowed algorithms, e.g. ``["ES256"]`` + If ``key`` is a :class:`PyJWK` object, allowed algorithms will default to the key algorithm. + + .. warning:: + + Do **not** compute the ``algorithms`` parameter based on + the ``alg`` from the token itself, or on any other data + that an attacker may be able to influence, as that might + expose you to various vulnerabilities (see `RFC 8725 §2.1 + `_). Instead, + either hard-code a fixed value for ``algorithms``, or + configure it in the same place you configure the + ``key``. Make sure not to mix symmetric and asymmetric + algorithms that interpret the ``key`` in different ways + (e.g. HS\\* and RS\\*). + :type algorithms: typing.Sequence[str] or None + + :param jwt.types.Options options: extended decoding and validation options + Refer to :py:class:`jwt.types.Options` for more information. + + :param audience: optional, the value for ``verify_aud`` check + :type audience: str or typing.Iterable[str] or None + :param subject: optional, the value for ``verify_sub`` check + :type subject: str or None + :param issuer: optional, the value for ``verify_iss`` check + :type issuer: str or typing.Container[str] or None + :param leeway: a time margin in seconds for the expiration check + :type leeway: float or datetime.timedelta + :rtype: dict[str, typing.Any] + :returns: the JWT claims + """ + if kwargs: + warnings.warn( + "passing additional kwargs to decode() is deprecated " + "and will be removed in pyjwt version 3. " + f"Unsupported kwargs: {tuple(kwargs.keys())}", + RemovedInPyjwt3Warning, + stacklevel=2, + ) + decoded = self.decode_complete( + jwt, + key, + algorithms, + options, + verify=verify, + detached_payload=detached_payload, + audience=audience, + subject=subject, + issuer=issuer, + leeway=leeway, + ) + return cast(dict[str, Any], decoded["payload"]) + + def _validate_claims( + self, + payload: dict[str, Any], + options: FullOptions, + audience: Iterable[str] | str | None = None, + issuer: Container[str] | str | None = None, + subject: str | None = None, + leeway: float | timedelta = 0, + ) -> None: + if isinstance(leeway, timedelta): + leeway = leeway.total_seconds() + + if audience is not None and not isinstance(audience, (str, Iterable)): + raise TypeError("audience must be a string, iterable or None") + + self._validate_required_claims(payload, options["require"]) + + now = datetime.now(tz=timezone.utc).timestamp() + + if "iat" in payload and options["verify_iat"]: + self._validate_iat(payload, now, leeway) + + if "nbf" in payload and options["verify_nbf"]: + self._validate_nbf(payload, now, leeway) + + if "exp" in payload and options["verify_exp"]: + self._validate_exp(payload, now, leeway) + + if options["verify_iss"]: + self._validate_iss(payload, issuer) + + if options["verify_aud"]: + self._validate_aud( + payload, audience, strict=options.get("strict_aud", False) + ) + + if options["verify_sub"]: + self._validate_sub(payload, subject) + + if options["verify_jti"]: + self._validate_jti(payload) + + def _validate_required_claims( + self, + payload: dict[str, Any], + claims: Iterable[str], + ) -> None: + for claim in claims: + if payload.get(claim) is None: + raise MissingRequiredClaimError(claim) + + def _validate_sub( + self, payload: dict[str, Any], subject: str | None = None + ) -> None: + """ + Checks whether "sub" if in the payload is valid or not. + This is an Optional claim + + :param payload(dict): The payload which needs to be validated + :param subject(str): The subject of the token + """ + + if "sub" not in payload: + return + + if not isinstance(payload["sub"], str): + raise InvalidSubjectError("Subject must be a string") + + if subject is not None: + if payload.get("sub") != subject: + raise InvalidSubjectError("Invalid subject") + + def _validate_jti(self, payload: dict[str, Any]) -> None: + """ + Checks whether "jti" if in the payload is valid or not + This is an Optional claim + + :param payload(dict): The payload which needs to be validated + """ + + if "jti" not in payload: + return + + if not isinstance(payload.get("jti"), str): + raise InvalidJTIError("JWT ID must be a string") + + def _validate_iat( + self, + payload: dict[str, Any], + now: float, + leeway: float, + ) -> None: + try: + iat = int(payload["iat"]) + except ValueError: + raise InvalidIssuedAtError( + "Issued At claim (iat) must be an integer." + ) from None + if iat > (now + leeway): + raise ImmatureSignatureError("The token is not yet valid (iat)") + + def _validate_nbf( + self, + payload: dict[str, Any], + now: float, + leeway: float, + ) -> None: + try: + nbf = int(payload["nbf"]) + except ValueError: + raise DecodeError("Not Before claim (nbf) must be an integer.") from None + + if nbf > (now + leeway): + raise ImmatureSignatureError("The token is not yet valid (nbf)") + + def _validate_exp( + self, + payload: dict[str, Any], + now: float, + leeway: float, + ) -> None: + try: + exp = int(payload["exp"]) + except ValueError: + raise DecodeError( + "Expiration Time claim (exp) must be an integer." + ) from None + + if exp <= (now - leeway): + raise ExpiredSignatureError("Signature has expired") + + def _validate_aud( + self, + payload: dict[str, Any], + audience: str | Iterable[str] | None, + *, + strict: bool = False, + ) -> None: + if audience is None: + if "aud" not in payload or not payload["aud"]: + return + # Application did not specify an audience, but + # the token has the 'aud' claim + raise InvalidAudienceError("Invalid audience") + + if "aud" not in payload or not payload["aud"]: + # Application specified an audience, but it could not be + # verified since the token does not contain a claim. + raise MissingRequiredClaimError("aud") + + audience_claims = payload["aud"] + + # In strict mode, we forbid list matching: the supplied audience + # must be a string, and it must exactly match the audience claim. + if strict: + # Only a single audience is allowed in strict mode. + if not isinstance(audience, str): + raise InvalidAudienceError("Invalid audience (strict)") + + # Only a single audience claim is allowed in strict mode. + if not isinstance(audience_claims, str): + raise InvalidAudienceError("Invalid claim format in token (strict)") + + if audience != audience_claims: + raise InvalidAudienceError("Audience doesn't match (strict)") + + return + + if isinstance(audience_claims, str): + audience_claims = [audience_claims] + if not isinstance(audience_claims, list): + raise InvalidAudienceError("Invalid claim format in token") + if any(not isinstance(c, str) for c in audience_claims): + raise InvalidAudienceError("Invalid claim format in token") + + if isinstance(audience, str): + audience = [audience] + + if all(aud not in audience_claims for aud in audience): + raise InvalidAudienceError("Audience doesn't match") + + def _validate_iss( + self, payload: dict[str, Any], issuer: Container[str] | str | None + ) -> None: + if issuer is None: + return + + if "iss" not in payload: + raise MissingRequiredClaimError("iss") + + iss = payload["iss"] + if not isinstance(iss, str): + raise InvalidIssuerError("Payload Issuer (iss) must be a string") + + if isinstance(issuer, str): + if iss != issuer: + raise InvalidIssuerError("Invalid issuer") + else: + try: + if iss not in issuer: + raise InvalidIssuerError("Invalid issuer") + except TypeError: + raise InvalidIssuerError( + 'Issuer param must be "str" or "Container[str]"' + ) from None + + +_jwt_global_obj = PyJWT() +_jwt_global_obj._jws = _jws_global_obj +encode = _jwt_global_obj.encode +decode_complete = _jwt_global_obj.decode_complete +decode = _jwt_global_obj.decode diff --git a/source/target-vendors/cp312/jwt/exceptions.py b/source/target-vendors/cp312/jwt/exceptions.py new file mode 100644 index 0000000..57559fc --- /dev/null +++ b/source/target-vendors/cp312/jwt/exceptions.py @@ -0,0 +1,113 @@ +class PyJWTError(Exception): + """ + Base class for all exceptions + """ + + pass + + +class InvalidTokenError(PyJWTError): + """Base exception when ``decode()`` fails on a token""" + + pass + + +class DecodeError(InvalidTokenError): + """Raised when a token cannot be decoded because it failed validation""" + + pass + + +class InvalidSignatureError(DecodeError): + """Raised when a token's signature doesn't match the one provided as part of + the token.""" + + pass + + +class ExpiredSignatureError(InvalidTokenError): + """Raised when a token's ``exp`` claim indicates that it has expired""" + + pass + + +class InvalidAudienceError(InvalidTokenError): + """Raised when a token's ``aud`` claim does not match one of the expected + audience values""" + + pass + + +class InvalidIssuerError(InvalidTokenError): + """Raised when a token's ``iss`` claim does not match the expected issuer""" + + pass + + +class InvalidIssuedAtError(InvalidTokenError): + """Raised when a token's ``iat`` claim is non-numeric""" + + pass + + +class ImmatureSignatureError(InvalidTokenError): + """Raised when a token's ``nbf`` or ``iat`` claims represent a time in the future""" + + pass + + +class InvalidKeyError(PyJWTError): + """Raised when the specified key is not in the proper format""" + + pass + + +class InvalidAlgorithmError(InvalidTokenError): + """Raised when the specified algorithm is not recognized by PyJWT""" + + pass + + +class MissingRequiredClaimError(InvalidTokenError): + """Raised when a claim that is required to be present is not contained + in the claimset""" + + def __init__(self, claim: str) -> None: + self.claim = claim + + def __str__(self) -> str: + return f'Token is missing the "{self.claim}" claim' + + +class PyJWKError(PyJWTError): + pass + + +class MissingCryptographyError(PyJWKError): + """Raised if the algorithm requires ``cryptography`` to be installed and it is not available.""" + + pass + + +class PyJWKSetError(PyJWTError): + pass + + +class PyJWKClientError(PyJWTError): + pass + + +class PyJWKClientConnectionError(PyJWKClientError): + pass + + +class InvalidSubjectError(InvalidTokenError): + """Raised when a token's ``sub`` claim is not a string or doesn't match the expected ``subject``""" + + pass + + +class InvalidJTIError(InvalidTokenError): + """Raised when a token's ``jti`` claim is not a string""" + + pass diff --git a/source/target-vendors/cp312/jwt/help.py b/source/target-vendors/cp312/jwt/help.py new file mode 100644 index 0000000..2f42b5d --- /dev/null +++ b/source/target-vendors/cp312/jwt/help.py @@ -0,0 +1,66 @@ +import json +import platform +import sys + +from . import __version__ as pyjwt_version + +try: + import cryptography + + cryptography_version = cryptography.__version__ +except ModuleNotFoundError: + cryptography_version = "" + + +def info() -> dict[str, dict[str, str]]: + """ + Generate information for a bug report. + Based on the requests package help utility module. + """ + try: + platform_info = { + "system": platform.system(), + "release": platform.release(), + } + except OSError: + platform_info = {"system": "Unknown", "release": "Unknown"} + + implementation = platform.python_implementation() + + if implementation == "CPython": + implementation_version = platform.python_version() + elif implementation == "PyPy": + pypy_version_info = sys.pypy_version_info # type: ignore[attr-defined] + implementation_version = ( + f"{pypy_version_info.major}." + f"{pypy_version_info.minor}." + f"{pypy_version_info.micro}" + ) + if pypy_version_info.releaselevel != "final": + implementation_version = "".join( + [ + implementation_version, + pypy_version_info.releaselevel, + ] + ) + else: + implementation_version = "Unknown" + + return { + "platform": platform_info, + "implementation": { + "name": implementation, + "version": implementation_version, + }, + "cryptography": {"version": cryptography_version}, + "pyjwt": {"version": pyjwt_version}, + } + + +def main() -> None: + """Pretty-print the bug information as JSON.""" + print(json.dumps(info(), sort_keys=True, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/source/target-vendors/cp312/jwt/jwk_set_cache.py b/source/target-vendors/cp312/jwt/jwk_set_cache.py new file mode 100644 index 0000000..f3f3ca5 --- /dev/null +++ b/source/target-vendors/cp312/jwt/jwk_set_cache.py @@ -0,0 +1,31 @@ +import time +from typing import Optional + +from .api_jwk import PyJWKSet, PyJWTSetWithTimestamp + + +class JWKSetCache: + def __init__(self, lifespan: float) -> None: + self.jwk_set_with_timestamp: Optional[PyJWTSetWithTimestamp] = None + self.lifespan = lifespan + + def put(self, jwk_set: PyJWKSet) -> None: + if jwk_set is not None: + self.jwk_set_with_timestamp = PyJWTSetWithTimestamp(jwk_set) + else: + # clear cache + self.jwk_set_with_timestamp = None + + def get(self) -> Optional[PyJWKSet]: + if self.jwk_set_with_timestamp is None or self.is_expired(): + return None + + return self.jwk_set_with_timestamp.get_jwk_set() + + def is_expired(self) -> bool: + return ( + self.jwk_set_with_timestamp is not None + and self.lifespan > -1 + and time.monotonic() + > self.jwk_set_with_timestamp.get_timestamp() + self.lifespan + ) diff --git a/source/target-vendors/cp312/jwt/jwks_client.py b/source/target-vendors/cp312/jwt/jwks_client.py new file mode 100644 index 0000000..4ef34ce --- /dev/null +++ b/source/target-vendors/cp312/jwt/jwks_client.py @@ -0,0 +1,302 @@ +from __future__ import annotations + +import json +import math +import threading +import time +import urllib.request +from functools import lru_cache +from ssl import SSLContext +from typing import Any +from urllib.error import HTTPError, URLError +from urllib.parse import urlparse + +from .api_jwk import PyJWK, PyJWKSet +from .api_jwt import decode_complete as decode_token +from .exceptions import PyJWKClientConnectionError, PyJWKClientError +from .jwk_set_cache import JWKSetCache + + +class _NoRedirectHandler(urllib.request.HTTPRedirectHandler): + def redirect_request( + self, + req: urllib.request.Request, + fp: Any, + code: int, + msg: str, + headers: Any, + newurl: str, + ) -> urllib.request.Request | None: + return None + + +class PyJWKClient: + def __init__( + self, + uri: str, + cache_keys: bool = False, + max_cached_keys: int = 16, + cache_jwk_set: bool = True, + lifespan: float = 300, + headers: dict[str, Any] | None = None, + timeout: float = 30, + ssl_context: SSLContext | None = None, + cooldown_duration: float = 30, + ): + """A client for retrieving signing keys from a JWKS endpoint. + + ``PyJWKClient`` uses a two-tier caching system to avoid unnecessary + network requests: + + **Tier 1 — JWK Set cache** (enabled by default): + Caches the entire JSON Web Key Set response from the endpoint. + Controlled by: + + - ``cache_jwk_set``: Set to ``True`` (the default) to enable this + cache. When enabled, the JWK Set is fetched from the network only + when the cache is empty or expired. + - ``lifespan``: Time in seconds before the cached JWK Set expires. + Defaults to ``300`` (5 minutes). Must be greater than 0. + + Unknown key IDs can trigger one forced refresh after the cooldown + period configured by ``cooldown_duration``. Every successful fetch + starts this cooldown, including the initial fetch and cache-expiry + fetches. A newly rotated key may therefore wait for the cooldown + period before it is fetched; set ``cooldown_duration`` to ``0`` to + disable this behavior. The cooldown is bypassed when + ``cache_jwk_set`` is ``False``. + + **Tier 2 — Signing key cache** (disabled by default): + Caches individual signing keys (looked up by ``kid``) using an LRU + cache with **no time-based expiration**. Keys are evicted only when + the cache reaches its maximum size. Controlled by: + + - ``cache_keys``: Set to ``True`` to enable this cache. + Defaults to ``False``. + - ``max_cached_keys``: Maximum number of signing keys to keep in + the LRU cache. Defaults to ``16``. + + :param uri: The URL of the JWKS endpoint. + :type uri: str + :param cache_keys: Enable the per-key LRU cache (Tier 2). + :type cache_keys: bool + :param max_cached_keys: Max entries in the signing key LRU cache. + :type max_cached_keys: int + :param cache_jwk_set: Enable the JWK Set response cache (Tier 1). + :type cache_jwk_set: bool + :param lifespan: TTL in seconds for the JWK Set cache. + :type lifespan: float + :param headers: Optional HTTP headers to include in requests. + :type headers: dict or None + :param timeout: HTTP request timeout in seconds. + :type timeout: float + :param ssl_context: Optional SSL context for the request. + :type ssl_context: ssl.SSLContext or None + :param cooldown_duration: Minimum time in seconds between forced + refreshes after an unknown key ID. Defaults to ``30``. + :type cooldown_duration: float + """ + if headers is None: + headers = {} + # urllib's default OpenerDirector also handles file://, ftp://, and + # data: URIs. Reject anything that isn't http(s) eagerly so a caller + # passing an attacker-influenced URL (e.g. taken from a `jku` token + # header) can't read local files or reach other unintended schemes. + scheme = urlparse(uri).scheme.lower() + if scheme not in ("http", "https"): + raise PyJWKClientError( + f"Invalid JWKS URI scheme {scheme!r}: only 'http' and 'https' " + f"are supported." + ) + self.uri = uri + self.jwk_set_cache: JWKSetCache | None = None + self.headers = headers + self.timeout = timeout + self.ssl_context = ssl_context + if cooldown_duration < 0: + raise PyJWKClientError( + "Cooldown duration must be greater than or equal to 0, " + f'the input is "{cooldown_duration}"' + ) + if not math.isfinite(cooldown_duration): + raise PyJWKClientError( + f'Cooldown duration must be finite, the input is "{cooldown_duration}"' + ) + self.cooldown_duration = cooldown_duration + self._last_successful_fetch: float | None = None + self._client_lock = threading.RLock() + + if cache_jwk_set: + # Init jwt set cache with default or given lifespan. + # Default lifespan is 300 seconds (5 minutes). + if lifespan <= 0: + raise PyJWKClientError( + f'Lifespan must be greater than 0, the input is "{lifespan}"' + ) + self.jwk_set_cache = JWKSetCache(lifespan) + else: + self.jwk_set_cache = None + + if cache_keys: + # Cache signing keys + get_signing_key = lru_cache(maxsize=max_cached_keys)(self.get_signing_key) + # Ignore mypy (https://github.com/python/mypy/issues/2427) + self.get_signing_key = get_signing_key # type: ignore[method-assign] + + def fetch_data(self) -> Any: + """Fetch the JWK Set from the JWKS endpoint. + + Makes an HTTP request to the configured ``uri`` and returns the + parsed JSON response. If the JWK Set cache is enabled, the + response is stored in the cache. + + :returns: The parsed JWK Set as a dictionary. + :raises PyJWKClientConnectionError: If the HTTP request fails. + """ + try: + r = urllib.request.Request(url=self.uri, headers=self.headers) + handlers: list[Any] = [_NoRedirectHandler()] + if self.ssl_context is not None: + handlers.append(urllib.request.HTTPSHandler(context=self.ssl_context)) + opener = urllib.request.build_opener(*handlers) + with opener.open(r, timeout=self.timeout) as response: + jwk_set = json.load(response) + except (URLError, TimeoutError) as e: + if isinstance(e, HTTPError): + e.close() + raise PyJWKClientConnectionError( + f'Fail to fetch data from the url, err: "{e}"' + ) from e + + # Only update the cache on a successful fetch. Writing in a + # `finally` block with `jwk_set=None` on error clears any + # previously-cached JWKS, turning a transient outage into a cache + # wipe that breaks legitimate auth. + if self.jwk_set_cache is not None: + self.jwk_set_cache.put(jwk_set) + self._last_successful_fetch = time.monotonic() + return jwk_set + + def get_jwk_set(self, refresh: bool = False) -> PyJWKSet: + """Return the JWK Set, using the cache when available. + + :param refresh: Force a fresh fetch from the endpoint, bypassing + the cache. + :type refresh: bool + :returns: The JWK Set. + :rtype: PyJWKSet + :raises PyJWKClientError: If the endpoint does not return a JSON + object. + """ + data = None + if self.jwk_set_cache is not None and not refresh: + data = self.jwk_set_cache.get() + + if data is None: + data = self.fetch_data() + + if not isinstance(data, dict): + raise PyJWKClientError("The JWKS endpoint did not return a JSON object") + + return PyJWKSet.from_dict(data) + + def get_signing_keys(self, refresh: bool = False) -> list[PyJWK]: + """Return all signing keys from the JWK Set. + + Filters the JWK Set to keys whose ``use`` is ``"sig"`` (or + unspecified) and that have a ``kid``. + + :param refresh: Force a fresh fetch from the endpoint, bypassing + the cache. + :type refresh: bool + :returns: A list of signing keys. + :rtype: list[PyJWK] + :raises PyJWKClientError: If no signing keys are found. + """ + jwk_set = self.get_jwk_set(refresh) + return self._get_signing_keys_from_jwk_set(jwk_set) + + @staticmethod + def _get_signing_keys_from_jwk_set(jwk_set: PyJWKSet) -> list[PyJWK]: + signing_keys = [ + jwk_set_key + for jwk_set_key in jwk_set.keys + if jwk_set_key.public_key_use in ["sig", None] and jwk_set_key.key_id + ] + + if not signing_keys: + raise PyJWKClientError("The JWKS endpoint did not contain any signing keys") + + return signing_keys + + def get_signing_key(self, kid: str) -> PyJWK: + """Return the signing key matching the given ``kid``. + + If no match is found in the current JWK Set, the set is refreshed + from the endpoint and the lookup is retried once when the refresh + cooldown permits it. + + :param kid: The key ID to look up. + :type kid: str + :returns: The matching signing key. + :rtype: PyJWK + :raises PyJWKClientError: If no matching key is found after + refreshing. + """ + with self._client_lock: + signing_keys = self.get_signing_keys() + signing_key = self.match_kid(signing_keys, kid) + + if not signing_key: + cooling_down = ( + self.jwk_set_cache is not None + and self._last_successful_fetch is not None + and time.monotonic() - self._last_successful_fetch + < self.cooldown_duration + ) + if not cooling_down: + signing_keys = self.get_signing_keys(refresh=True) + self._last_successful_fetch = time.monotonic() + signing_key = self.match_kid(signing_keys, kid) + + if not signing_key: + raise PyJWKClientError( + f'Unable to find a signing key that matches: "{kid}"' + ) + + return signing_key + + def get_signing_key_from_jwt(self, token: str | bytes) -> PyJWK: + """Return the signing key for a JWT by reading its ``kid`` header. + + Extracts the ``kid`` from the token's unverified header and + delegates to :meth:`get_signing_key`. + + :param token: The encoded JWT. + :type token: str or bytes + :returns: The matching signing key. + :rtype: PyJWK + """ + unverified = decode_token(token, options={"verify_signature": False}) + header = unverified["header"] + return self.get_signing_key(header.get("kid")) + + @staticmethod + def match_kid(signing_keys: list[PyJWK], kid: str) -> PyJWK | None: + """Find a key in *signing_keys* that matches *kid*. + + :param signing_keys: The list of keys to search. + :type signing_keys: list[PyJWK] + :param kid: The key ID to match. + :type kid: str + :returns: The matching key, or ``None`` if not found. + :rtype: PyJWK or None + """ + signing_key = None + + for key in signing_keys: + if key.key_id == kid: + signing_key = key + break + + return signing_key diff --git a/source/target-vendors/cp312/jwt/py.typed b/source/target-vendors/cp312/jwt/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/jwt/types.py b/source/target-vendors/cp312/jwt/types.py new file mode 100644 index 0000000..61bfdf0 --- /dev/null +++ b/source/target-vendors/cp312/jwt/types.py @@ -0,0 +1,69 @@ +from typing import Any, Callable, TypedDict + +JWKDict = dict[str, Any] + +HashlibHash = Callable[..., Any] + + +class SigOptions(TypedDict, total=False): + """Options for PyJWS class (TypedDict). Note that this is a smaller set of options than + for :py:func:`jwt.decode()`.""" + + verify_signature: bool + """verify the JWT cryptographic signature""" + enforce_minimum_key_length: bool + """Default: ``False``. Raise :py:class:`jwt.exceptions.InvalidKeyError` instead of warning when keys are below minimum recommended length.""" + + +class Options(TypedDict, total=False): + """Options for :py:func:`jwt.decode()` and :py:func:`jwt.decode_complete()` (TypedDict). + + .. warning:: + + Some claims, such as ``exp``, ``iat``, ``jti``, ``nbf``, and ``sub``, + will only be verified if present. Please refer to the documentation below + for which ones, and make sure to include them in the ``require`` param + if you want to make sure that they are always present (and therefore always verified + if ``verify_{claim} = True`` for that claim). + """ + + verify_signature: bool + """Default: ``True``. Verify the JWT cryptographic signature.""" + require: list[str] + """Default: ``[]``. List of claims that must be present. + Example: ``require=["exp", "iat", "nbf"]``. + **Only verifies that the claims exists**. Does not verify that the claims are valid.""" + strict_aud: bool + """Default: ``False``. (requires ``verify_aud=True``) Check that the ``aud`` claim is a single value (not a list), and matches ``audience`` exactly.""" + verify_aud: bool + """Default: ``verify_signature``. Check that ``aud`` (audience) claim matches ``audience``.""" + verify_exp: bool + """Default: ``verify_signature``. Check that ``exp`` (expiration) claim value is in the future (if present in payload). """ + verify_iat: bool + """Default: ``verify_signature``. Check that ``iat`` (issued at) claim value is an integer (if present in payload). """ + verify_iss: bool + """Default: ``verify_signature``. Check that ``iss`` (issuer) claim matches ``issuer``. """ + verify_jti: bool + """Default: ``verify_signature``. Check that ``jti`` (JWT ID) claim is a string (if present in payload). """ + verify_nbf: bool + """Default: ``verify_signature``. Check that ``nbf`` (not before) claim value is in the past (if present in payload). """ + verify_sub: bool + """Default: ``verify_signature``. Check that ``sub`` (subject) claim is a string and matches ``subject`` (if present in payload). """ + enforce_minimum_key_length: bool + """Default: ``False``. Raise :py:class:`jwt.exceptions.InvalidKeyError` instead of warning when keys are below minimum recommended length.""" + + +# The only difference between Options and FullOptions is that FullOptions +# required _every_ value to be there; Options doesn't require any +class FullOptions(TypedDict): + verify_signature: bool + require: list[str] + strict_aud: bool + verify_aud: bool + verify_exp: bool + verify_iat: bool + verify_iss: bool + verify_jti: bool + verify_nbf: bool + verify_sub: bool + enforce_minimum_key_length: bool diff --git a/source/target-vendors/cp312/jwt/utils.py b/source/target-vendors/cp312/jwt/utils.py new file mode 100644 index 0000000..914cb50 --- /dev/null +++ b/source/target-vendors/cp312/jwt/utils.py @@ -0,0 +1,146 @@ +import base64 +import binascii +import re +from typing import Optional, Union + +try: + from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve + from cryptography.hazmat.primitives.asymmetric.utils import ( + decode_dss_signature, + encode_dss_signature, + ) +except ModuleNotFoundError: + pass + + +def force_bytes(value: Union[bytes, str]) -> bytes: + if isinstance(value, str): + return value.encode("utf-8") + elif isinstance(value, bytes): + return value + else: + raise TypeError("Expected a string value") + + +def base64url_decode(input: Union[bytes, str]) -> bytes: + input_bytes = force_bytes(input) + + rem = len(input_bytes) % 4 + + if rem > 0: + input_bytes += b"=" * (4 - rem) + + return base64.urlsafe_b64decode(input_bytes) + + +def base64url_encode(input: bytes) -> bytes: + return base64.urlsafe_b64encode(input).replace(b"=", b"") + + +def to_base64url_uint(val: int, *, bit_length: Optional[int] = None) -> bytes: + if val < 0: + raise ValueError("Must be a positive integer") + + int_bytes = bytes_from_int(val, bit_length=bit_length) + + if len(int_bytes) == 0: + int_bytes = b"\x00" + + return base64url_encode(int_bytes) + + +def from_base64url_uint(val: Union[bytes, str]) -> int: + data = base64url_decode(force_bytes(val)) + return int.from_bytes(data, byteorder="big") + + +def number_to_bytes(num: int, num_bytes: int) -> bytes: + padded_hex = "%0*x" % (2 * num_bytes, num) + return binascii.a2b_hex(padded_hex.encode("ascii")) + + +def bytes_to_number(string: bytes) -> int: + return int(binascii.b2a_hex(string), 16) + + +def bytes_from_int(val: int, *, bit_length: Optional[int] = None) -> bytes: + if bit_length is None: + bit_length = val.bit_length() + byte_length = (bit_length + 7) // 8 + + return val.to_bytes(byte_length, "big", signed=False) + + +def der_to_raw_signature(der_sig: bytes, curve: "EllipticCurve") -> bytes: + num_bits = curve.key_size + num_bytes = (num_bits + 7) // 8 + + r, s = decode_dss_signature(der_sig) + + return number_to_bytes(r, num_bytes) + number_to_bytes(s, num_bytes) + + +def raw_to_der_signature(raw_sig: bytes, curve: "EllipticCurve") -> bytes: + num_bits = curve.key_size + num_bytes = (num_bits + 7) // 8 + + if len(raw_sig) != 2 * num_bytes: + raise ValueError("Invalid signature") + + r = bytes_to_number(raw_sig[:num_bytes]) + s = bytes_to_number(raw_sig[num_bytes:]) + + return bytes(encode_dss_signature(r, s)) + + +# Based on https://github.com/hynek/pem/blob/7ad94db26b0bc21d10953f5dbad3acfdfacf57aa/src/pem/_core.py#L224-L252 +_PEMS = { + b"CERTIFICATE", + b"TRUSTED CERTIFICATE", + b"PRIVATE KEY", + b"PUBLIC KEY", + b"ENCRYPTED PRIVATE KEY", + b"OPENSSH PRIVATE KEY", + b"DSA PRIVATE KEY", + b"RSA PRIVATE KEY", + b"RSA PUBLIC KEY", + b"EC PRIVATE KEY", + b"DH PARAMETERS", + b"NEW CERTIFICATE REQUEST", + b"CERTIFICATE REQUEST", + b"SSH2 PUBLIC KEY", + b"SSH2 ENCRYPTED PRIVATE KEY", + b"X509 CRL", +} + +# Accept PEM formatting variants that the cryptography loader accepts, while +# retaining the explicit asymmetric-key marker check. +_PEM_MARKER_RE = re.compile( + b"(?=(----[- ](BEGIN|END) (" + b"|".join(_PEMS) + b")[- ]----))" +) + + +def is_pem_format(key: bytes) -> bool: + begin_markers: dict[bytes, int] = {} + for marker in _PEM_MARKER_RE.finditer(key): + marker_type, label = marker.group(2), marker.group(3) + if marker_type == b"BEGIN": + begin_markers[label] = marker.start(1) + len(marker.group(1)) + elif label in begin_markers and begin_markers[label] < marker.start(1): + return True + return False + + +# Based on https://github.com/pyca/cryptography/blob/bcb70852d577b3f490f015378c75cba74986297b/src/cryptography/hazmat/primitives/serialization/ssh.py#L40-L46 +_SSH_KEY_FORMATS = ( + b"ssh-ed25519", + b"ssh-rsa", + b"ssh-dss", + b"ecdsa-sha2-nistp256", + b"ecdsa-sha2-nistp384", + b"ecdsa-sha2-nistp521", +) + + +def is_ssh_key(key: bytes) -> bool: + return key.startswith(_SSH_KEY_FORMATS) diff --git a/source/target-vendors/cp312/jwt/warnings.py b/source/target-vendors/cp312/jwt/warnings.py new file mode 100644 index 0000000..8a41a36 --- /dev/null +++ b/source/target-vendors/cp312/jwt/warnings.py @@ -0,0 +1,11 @@ +class RemovedInPyjwt3Warning(DeprecationWarning): + """Warning for features that will be removed in PyJWT 3.""" + + pass + + +class InsecureKeyLengthWarning(UserWarning): + """Warning emitted when a cryptographic key is shorter than the minimum + recommended length. See :ref:`key-length-validation` for details.""" + + pass diff --git a/source/target-vendors/cp312/mcp-1.26.0.dist-info/METADATA b/source/target-vendors/cp312/mcp-1.26.0.dist-info/METADATA new file mode 100644 index 0000000..19afdb6 --- /dev/null +++ b/source/target-vendors/cp312/mcp-1.26.0.dist-info/METADATA @@ -0,0 +1,2606 @@ +Metadata-Version: 2.4 +Name: mcp +Version: 1.26.0 +Summary: Model Context Protocol SDK +Project-URL: Homepage, https://modelcontextprotocol.io +Project-URL: Repository, https://github.com/modelcontextprotocol/python-sdk +Project-URL: Issues, https://github.com/modelcontextprotocol/python-sdk/issues +Author: Anthropic, PBC. +Maintainer-email: David Soria Parra , Justin Spahr-Summers +License: MIT +License-File: LICENSE +Keywords: automation,git,llm,mcp +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Requires-Python: >=3.10 +Requires-Dist: anyio>=4.5 +Requires-Dist: httpx-sse>=0.4 +Requires-Dist: httpx>=0.27.1 +Requires-Dist: jsonschema>=4.20.0 +Requires-Dist: pydantic-settings>=2.5.2 +Requires-Dist: pydantic<3.0.0,>=2.11.0 +Requires-Dist: pyjwt[crypto]>=2.10.1 +Requires-Dist: python-multipart>=0.0.9 +Requires-Dist: pywin32>=310; sys_platform == 'win32' +Requires-Dist: sse-starlette>=1.6.1 +Requires-Dist: starlette>=0.27 +Requires-Dist: typing-extensions>=4.9.0 +Requires-Dist: typing-inspection>=0.4.1 +Requires-Dist: uvicorn>=0.31.1; sys_platform != 'emscripten' +Provides-Extra: cli +Requires-Dist: python-dotenv>=1.0.0; extra == 'cli' +Requires-Dist: typer>=0.16.0; extra == 'cli' +Provides-Extra: rich +Requires-Dist: rich>=13.9.4; extra == 'rich' +Provides-Extra: ws +Requires-Dist: websockets>=15.0.1; extra == 'ws' +Description-Content-Type: text/markdown + +# MCP Python SDK + +
+ +Python implementation of the Model Context Protocol (MCP) + +[![PyPI][pypi-badge]][pypi-url] +[![MIT licensed][mit-badge]][mit-url] +[![Python Version][python-badge]][python-url] +[![Documentation][docs-badge]][docs-url] +[![Protocol][protocol-badge]][protocol-url] +[![Specification][spec-badge]][spec-url] + +
+ + +## Table of Contents + +- [MCP Python SDK](#mcp-python-sdk) + - [Overview](#overview) + - [Installation](#installation) + - [Adding MCP to your python project](#adding-mcp-to-your-python-project) + - [Running the standalone MCP development tools](#running-the-standalone-mcp-development-tools) + - [Quickstart](#quickstart) + - [What is MCP?](#what-is-mcp) + - [Core Concepts](#core-concepts) + - [Server](#server) + - [Resources](#resources) + - [Tools](#tools) + - [Structured Output](#structured-output) + - [Prompts](#prompts) + - [Images](#images) + - [Context](#context) + - [Getting Context in Functions](#getting-context-in-functions) + - [Context Properties and Methods](#context-properties-and-methods) + - [Completions](#completions) + - [Elicitation](#elicitation) + - [Sampling](#sampling) + - [Logging and Notifications](#logging-and-notifications) + - [Authentication](#authentication) + - [FastMCP Properties](#fastmcp-properties) + - [Session Properties and Methods](#session-properties-and-methods) + - [Request Context Properties](#request-context-properties) + - [Running Your Server](#running-your-server) + - [Development Mode](#development-mode) + - [Claude Desktop Integration](#claude-desktop-integration) + - [Direct Execution](#direct-execution) + - [Streamable HTTP Transport](#streamable-http-transport) + - [CORS Configuration for Browser-Based Clients](#cors-configuration-for-browser-based-clients) + - [Mounting to an Existing ASGI Server](#mounting-to-an-existing-asgi-server) + - [StreamableHTTP servers](#streamablehttp-servers) + - [Basic mounting](#basic-mounting) + - [Host-based routing](#host-based-routing) + - [Multiple servers with path configuration](#multiple-servers-with-path-configuration) + - [Path configuration at initialization](#path-configuration-at-initialization) + - [SSE servers](#sse-servers) + - [Advanced Usage](#advanced-usage) + - [Low-Level Server](#low-level-server) + - [Structured Output Support](#structured-output-support) + - [Pagination (Advanced)](#pagination-advanced) + - [Writing MCP Clients](#writing-mcp-clients) + - [Client Display Utilities](#client-display-utilities) + - [OAuth Authentication for Clients](#oauth-authentication-for-clients) + - [Parsing Tool Results](#parsing-tool-results) + - [MCP Primitives](#mcp-primitives) + - [Server Capabilities](#server-capabilities) + - [Documentation](#documentation) + - [Contributing](#contributing) + - [License](#license) + +[pypi-badge]: https://img.shields.io/pypi/v/mcp.svg +[pypi-url]: https://pypi.org/project/mcp/ +[mit-badge]: https://img.shields.io/pypi/l/mcp.svg +[mit-url]: https://github.com/modelcontextprotocol/python-sdk/blob/main/LICENSE +[python-badge]: https://img.shields.io/pypi/pyversions/mcp.svg +[python-url]: https://www.python.org/downloads/ +[docs-badge]: https://img.shields.io/badge/docs-python--sdk-blue.svg +[docs-url]: https://modelcontextprotocol.github.io/python-sdk/ +[protocol-badge]: https://img.shields.io/badge/protocol-modelcontextprotocol.io-blue.svg +[protocol-url]: https://modelcontextprotocol.io +[spec-badge]: https://img.shields.io/badge/spec-spec.modelcontextprotocol.io-blue.svg +[spec-url]: https://modelcontextprotocol.io/specification/latest + +## Overview + +The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This Python SDK implements the full MCP specification, making it easy to: + +- Build MCP clients that can connect to any MCP server +- Create MCP servers that expose resources, prompts and tools +- Use standard transports like stdio, SSE, and Streamable HTTP +- Handle all MCP protocol messages and lifecycle events + +## Installation + +### Adding MCP to your python project + +We recommend using [uv](https://docs.astral.sh/uv/) to manage your Python projects. + +If you haven't created a uv-managed project yet, create one: + + ```bash + uv init mcp-server-demo + cd mcp-server-demo + ``` + + Then add MCP to your project dependencies: + + ```bash + uv add "mcp[cli]" + ``` + +Alternatively, for projects using pip for dependencies: + +```bash +pip install "mcp[cli]" +``` + +### Running the standalone MCP development tools + +To run the mcp command with uv: + +```bash +uv run mcp +``` + +## Quickstart + +Let's create a simple MCP server that exposes a calculator tool and some data: + + +```python +""" +FastMCP quickstart example. + +Run from the repository root: + uv run examples/snippets/servers/fastmcp_quickstart.py +""" + +from mcp.server.fastmcp import FastMCP + +# Create an MCP server +mcp = FastMCP("Demo", json_response=True) + + +# Add an addition tool +@mcp.tool() +def add(a: int, b: int) -> int: + """Add two numbers""" + return a + b + + +# Add a dynamic greeting resource +@mcp.resource("greeting://{name}") +def get_greeting(name: str) -> str: + """Get a personalized greeting""" + return f"Hello, {name}!" + + +# Add a prompt +@mcp.prompt() +def greet_user(name: str, style: str = "friendly") -> str: + """Generate a greeting prompt""" + styles = { + "friendly": "Please write a warm, friendly greeting", + "formal": "Please write a formal, professional greeting", + "casual": "Please write a casual, relaxed greeting", + } + + return f"{styles.get(style, styles['friendly'])} for someone named {name}." + + +# Run with streamable HTTP transport +if __name__ == "__main__": + mcp.run(transport="streamable-http") +``` + +_Full example: [examples/snippets/servers/fastmcp_quickstart.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/fastmcp_quickstart.py)_ + + +You can install this server in [Claude Code](https://docs.claude.com/en/docs/claude-code/mcp) and interact with it right away. First, run the server: + +```bash +uv run --with mcp examples/snippets/servers/fastmcp_quickstart.py +``` + +Then add it to Claude Code: + +```bash +claude mcp add --transport http my-server http://localhost:8000/mcp +``` + +Alternatively, you can test it with the MCP Inspector. Start the server as above, then in a separate terminal: + +```bash +npx -y @modelcontextprotocol/inspector +``` + +In the inspector UI, connect to `http://localhost:8000/mcp`. + +## What is MCP? + +The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can: + +- Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context) +- Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect) +- Define interaction patterns through **Prompts** (reusable templates for LLM interactions) +- And more! + +## Core Concepts + +### Server + +The FastMCP server is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing: + + +```python +"""Example showing lifespan support for startup/shutdown with strong typing.""" + +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from dataclasses import dataclass + +from mcp.server.fastmcp import Context, FastMCP +from mcp.server.session import ServerSession + + +# Mock database class for example +class Database: + """Mock database class for example.""" + + @classmethod + async def connect(cls) -> "Database": + """Connect to database.""" + return cls() + + async def disconnect(self) -> None: + """Disconnect from database.""" + pass + + def query(self) -> str: + """Execute a query.""" + return "Query result" + + +@dataclass +class AppContext: + """Application context with typed dependencies.""" + + db: Database + + +@asynccontextmanager +async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: + """Manage application lifecycle with type-safe context.""" + # Initialize on startup + db = await Database.connect() + try: + yield AppContext(db=db) + finally: + # Cleanup on shutdown + await db.disconnect() + + +# Pass lifespan to server +mcp = FastMCP("My App", lifespan=app_lifespan) + + +# Access type-safe lifespan context in tools +@mcp.tool() +def query_db(ctx: Context[ServerSession, AppContext]) -> str: + """Tool that uses initialized resources.""" + db = ctx.request_context.lifespan_context.db + return db.query() +``` + +_Full example: [examples/snippets/servers/lifespan_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lifespan_example.py)_ + + +### Resources + +Resources are how you expose data to LLMs. They're similar to GET endpoints in a REST API - they provide data but shouldn't perform significant computation or have side effects: + + +```python +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP(name="Resource Example") + + +@mcp.resource("file://documents/{name}") +def read_document(name: str) -> str: + """Read a document by name.""" + # This would normally read from disk + return f"Content of {name}" + + +@mcp.resource("config://settings") +def get_settings() -> str: + """Get application settings.""" + return """{ + "theme": "dark", + "language": "en", + "debug": false +}""" +``` + +_Full example: [examples/snippets/servers/basic_resource.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_resource.py)_ + + +### Tools + +Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects: + + +```python +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP(name="Tool Example") + + +@mcp.tool() +def sum(a: int, b: int) -> int: + """Add two numbers together.""" + return a + b + + +@mcp.tool() +def get_weather(city: str, unit: str = "celsius") -> str: + """Get weather for a city.""" + # This would normally call a weather API + return f"Weather in {city}: 22degrees{unit[0].upper()}" +``` + +_Full example: [examples/snippets/servers/basic_tool.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_tool.py)_ + + +Tools can optionally receive a Context object by including a parameter with the `Context` type annotation. This context is automatically injected by the FastMCP framework and provides access to MCP capabilities: + + +```python +from mcp.server.fastmcp import Context, FastMCP +from mcp.server.session import ServerSession + +mcp = FastMCP(name="Progress Example") + + +@mcp.tool() +async def long_running_task(task_name: str, ctx: Context[ServerSession, None], steps: int = 5) -> str: + """Execute a task with progress updates.""" + await ctx.info(f"Starting: {task_name}") + + for i in range(steps): + progress = (i + 1) / steps + await ctx.report_progress( + progress=progress, + total=1.0, + message=f"Step {i + 1}/{steps}", + ) + await ctx.debug(f"Completed step {i + 1}") + + return f"Task '{task_name}' completed" +``` + +_Full example: [examples/snippets/servers/tool_progress.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/tool_progress.py)_ + + +#### Structured Output + +Tools will return structured results by default, if their return type +annotation is compatible. Otherwise, they will return unstructured results. + +Structured output supports these return types: + +- Pydantic models (BaseModel subclasses) +- TypedDicts +- Dataclasses and other classes with type hints +- `dict[str, T]` (where T is any JSON-serializable type) +- Primitive types (str, int, float, bool, bytes, None) - wrapped in `{"result": value}` +- Generic types (list, tuple, Union, Optional, etc.) - wrapped in `{"result": value}` + +Classes without type hints cannot be serialized for structured output. Only +classes with properly annotated attributes will be converted to Pydantic models +for schema generation and validation. + +Structured results are automatically validated against the output schema +generated from the annotation. This ensures the tool returns well-typed, +validated data that clients can easily process. + +**Note:** For backward compatibility, unstructured results are also +returned. Unstructured results are provided for backward compatibility +with previous versions of the MCP specification, and are quirks-compatible +with previous versions of FastMCP in the current version of the SDK. + +**Note:** In cases where a tool function's return type annotation +causes the tool to be classified as structured _and this is undesirable_, +the classification can be suppressed by passing `structured_output=False` +to the `@tool` decorator. + +##### Advanced: Direct CallToolResult + +For full control over tool responses including the `_meta` field (for passing data to client applications without exposing it to the model), you can return `CallToolResult` directly: + + +```python +"""Example showing direct CallToolResult return for advanced control.""" + +from typing import Annotated + +from pydantic import BaseModel + +from mcp.server.fastmcp import FastMCP +from mcp.types import CallToolResult, TextContent + +mcp = FastMCP("CallToolResult Example") + + +class ValidationModel(BaseModel): + """Model for validating structured output.""" + + status: str + data: dict[str, int] + + +@mcp.tool() +def advanced_tool() -> CallToolResult: + """Return CallToolResult directly for full control including _meta field.""" + return CallToolResult( + content=[TextContent(type="text", text="Response visible to the model")], + _meta={"hidden": "data for client applications only"}, + ) + + +@mcp.tool() +def validated_tool() -> Annotated[CallToolResult, ValidationModel]: + """Return CallToolResult with structured output validation.""" + return CallToolResult( + content=[TextContent(type="text", text="Validated response")], + structuredContent={"status": "success", "data": {"result": 42}}, + _meta={"internal": "metadata"}, + ) + + +@mcp.tool() +def empty_result_tool() -> CallToolResult: + """For empty results, return CallToolResult with empty content.""" + return CallToolResult(content=[]) +``` + +_Full example: [examples/snippets/servers/direct_call_tool_result.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/direct_call_tool_result.py)_ + + +**Important:** `CallToolResult` must always be returned (no `Optional` or `Union`). For empty results, use `CallToolResult(content=[])`. For optional simple types, use `str | None` without `CallToolResult`. + + +```python +"""Example showing structured output with tools.""" + +from typing import TypedDict + +from pydantic import BaseModel, Field + +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP("Structured Output Example") + + +# Using Pydantic models for rich structured data +class WeatherData(BaseModel): + """Weather information structure.""" + + temperature: float = Field(description="Temperature in Celsius") + humidity: float = Field(description="Humidity percentage") + condition: str + wind_speed: float + + +@mcp.tool() +def get_weather(city: str) -> WeatherData: + """Get weather for a city - returns structured data.""" + # Simulated weather data + return WeatherData( + temperature=22.5, + humidity=45.0, + condition="sunny", + wind_speed=5.2, + ) + + +# Using TypedDict for simpler structures +class LocationInfo(TypedDict): + latitude: float + longitude: float + name: str + + +@mcp.tool() +def get_location(address: str) -> LocationInfo: + """Get location coordinates""" + return LocationInfo(latitude=51.5074, longitude=-0.1278, name="London, UK") + + +# Using dict[str, Any] for flexible schemas +@mcp.tool() +def get_statistics(data_type: str) -> dict[str, float]: + """Get various statistics""" + return {"mean": 42.5, "median": 40.0, "std_dev": 5.2} + + +# Ordinary classes with type hints work for structured output +class UserProfile: + name: str + age: int + email: str | None = None + + def __init__(self, name: str, age: int, email: str | None = None): + self.name = name + self.age = age + self.email = email + + +@mcp.tool() +def get_user(user_id: str) -> UserProfile: + """Get user profile - returns structured data""" + return UserProfile(name="Alice", age=30, email="alice@example.com") + + +# Classes WITHOUT type hints cannot be used for structured output +class UntypedConfig: + def __init__(self, setting1, setting2): # type: ignore[reportMissingParameterType] + self.setting1 = setting1 + self.setting2 = setting2 + + +@mcp.tool() +def get_config() -> UntypedConfig: + """This returns unstructured output - no schema generated""" + return UntypedConfig("value1", "value2") + + +# Lists and other types are wrapped automatically +@mcp.tool() +def list_cities() -> list[str]: + """Get a list of cities""" + return ["London", "Paris", "Tokyo"] + # Returns: {"result": ["London", "Paris", "Tokyo"]} + + +@mcp.tool() +def get_temperature(city: str) -> float: + """Get temperature as a simple float""" + return 22.5 + # Returns: {"result": 22.5} +``` + +_Full example: [examples/snippets/servers/structured_output.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/structured_output.py)_ + + +### Prompts + +Prompts are reusable templates that help LLMs interact with your server effectively: + + +```python +from mcp.server.fastmcp import FastMCP +from mcp.server.fastmcp.prompts import base + +mcp = FastMCP(name="Prompt Example") + + +@mcp.prompt(title="Code Review") +def review_code(code: str) -> str: + return f"Please review this code:\n\n{code}" + + +@mcp.prompt(title="Debug Assistant") +def debug_error(error: str) -> list[base.Message]: + return [ + base.UserMessage("I'm seeing this error:"), + base.UserMessage(error), + base.AssistantMessage("I'll help debug that. What have you tried so far?"), + ] +``` + +_Full example: [examples/snippets/servers/basic_prompt.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_prompt.py)_ + + +### Icons + +MCP servers can provide icons for UI display. Icons can be added to the server implementation, tools, resources, and prompts: + +```python +from mcp.server.fastmcp import FastMCP, Icon + +# Create an icon from a file path or URL +icon = Icon( + src="icon.png", + mimeType="image/png", + sizes="64x64" +) + +# Add icons to server +mcp = FastMCP( + "My Server", + website_url="https://example.com", + icons=[icon] +) + +# Add icons to tools, resources, and prompts +@mcp.tool(icons=[icon]) +def my_tool(): + """Tool with an icon.""" + return "result" + +@mcp.resource("demo://resource", icons=[icon]) +def my_resource(): + """Resource with an icon.""" + return "content" +``` + +_Full example: [examples/fastmcp/icons_demo.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/fastmcp/icons_demo.py)_ + +### Images + +FastMCP provides an `Image` class that automatically handles image data: + + +```python +"""Example showing image handling with FastMCP.""" + +from PIL import Image as PILImage + +from mcp.server.fastmcp import FastMCP, Image + +mcp = FastMCP("Image Example") + + +@mcp.tool() +def create_thumbnail(image_path: str) -> Image: + """Create a thumbnail from an image""" + img = PILImage.open(image_path) + img.thumbnail((100, 100)) + return Image(data=img.tobytes(), format="png") +``` + +_Full example: [examples/snippets/servers/images.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/images.py)_ + + +### Context + +The Context object is automatically injected into tool and resource functions that request it via type hints. It provides access to MCP capabilities like logging, progress reporting, resource reading, user interaction, and request metadata. + +#### Getting Context in Functions + +To use context in a tool or resource function, add a parameter with the `Context` type annotation: + +```python +from mcp.server.fastmcp import Context, FastMCP + +mcp = FastMCP(name="Context Example") + + +@mcp.tool() +async def my_tool(x: int, ctx: Context) -> str: + """Tool that uses context capabilities.""" + # The context parameter can have any name as long as it's type-annotated + return await process_with_context(x, ctx) +``` + +#### Context Properties and Methods + +The Context object provides the following capabilities: + +- `ctx.request_id` - Unique ID for the current request +- `ctx.client_id` - Client ID if available +- `ctx.fastmcp` - Access to the FastMCP server instance (see [FastMCP Properties](#fastmcp-properties)) +- `ctx.session` - Access to the underlying session for advanced communication (see [Session Properties and Methods](#session-properties-and-methods)) +- `ctx.request_context` - Access to request-specific data and lifespan resources (see [Request Context Properties](#request-context-properties)) +- `await ctx.debug(message)` - Send debug log message +- `await ctx.info(message)` - Send info log message +- `await ctx.warning(message)` - Send warning log message +- `await ctx.error(message)` - Send error log message +- `await ctx.log(level, message, logger_name=None)` - Send log with custom level +- `await ctx.report_progress(progress, total=None, message=None)` - Report operation progress +- `await ctx.read_resource(uri)` - Read a resource by URI +- `await ctx.elicit(message, schema)` - Request additional information from user with validation + + +```python +from mcp.server.fastmcp import Context, FastMCP +from mcp.server.session import ServerSession + +mcp = FastMCP(name="Progress Example") + + +@mcp.tool() +async def long_running_task(task_name: str, ctx: Context[ServerSession, None], steps: int = 5) -> str: + """Execute a task with progress updates.""" + await ctx.info(f"Starting: {task_name}") + + for i in range(steps): + progress = (i + 1) / steps + await ctx.report_progress( + progress=progress, + total=1.0, + message=f"Step {i + 1}/{steps}", + ) + await ctx.debug(f"Completed step {i + 1}") + + return f"Task '{task_name}' completed" +``` + +_Full example: [examples/snippets/servers/tool_progress.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/tool_progress.py)_ + + +### Completions + +MCP supports providing completion suggestions for prompt arguments and resource template parameters. With the context parameter, servers can provide completions based on previously resolved values: + +Client usage: + + +```python +""" +cd to the `examples/snippets` directory and run: + uv run completion-client +""" + +import asyncio +import os + +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client +from mcp.types import PromptReference, ResourceTemplateReference + +# Create server parameters for stdio connection +server_params = StdioServerParameters( + command="uv", # Using uv to run the server + args=["run", "server", "completion", "stdio"], # Server with completion support + env={"UV_INDEX": os.environ.get("UV_INDEX", "")}, +) + + +async def run(): + """Run the completion client example.""" + async with stdio_client(server_params) as (read, write): + async with ClientSession(read, write) as session: + # Initialize the connection + await session.initialize() + + # List available resource templates + templates = await session.list_resource_templates() + print("Available resource templates:") + for template in templates.resourceTemplates: + print(f" - {template.uriTemplate}") + + # List available prompts + prompts = await session.list_prompts() + print("\nAvailable prompts:") + for prompt in prompts.prompts: + print(f" - {prompt.name}") + + # Complete resource template arguments + if templates.resourceTemplates: + template = templates.resourceTemplates[0] + print(f"\nCompleting arguments for resource template: {template.uriTemplate}") + + # Complete without context + result = await session.complete( + ref=ResourceTemplateReference(type="ref/resource", uri=template.uriTemplate), + argument={"name": "owner", "value": "model"}, + ) + print(f"Completions for 'owner' starting with 'model': {result.completion.values}") + + # Complete with context - repo suggestions based on owner + result = await session.complete( + ref=ResourceTemplateReference(type="ref/resource", uri=template.uriTemplate), + argument={"name": "repo", "value": ""}, + context_arguments={"owner": "modelcontextprotocol"}, + ) + print(f"Completions for 'repo' with owner='modelcontextprotocol': {result.completion.values}") + + # Complete prompt arguments + if prompts.prompts: + prompt_name = prompts.prompts[0].name + print(f"\nCompleting arguments for prompt: {prompt_name}") + + result = await session.complete( + ref=PromptReference(type="ref/prompt", name=prompt_name), + argument={"name": "style", "value": ""}, + ) + print(f"Completions for 'style' argument: {result.completion.values}") + + +def main(): + """Entry point for the completion client.""" + asyncio.run(run()) + + +if __name__ == "__main__": + main() +``` + +_Full example: [examples/snippets/clients/completion_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/completion_client.py)_ + +### Elicitation + +Request additional information from users. This example shows an Elicitation during a Tool Call: + + +```python +"""Elicitation examples demonstrating form and URL mode elicitation. + +Form mode elicitation collects structured, non-sensitive data through a schema. +URL mode elicitation directs users to external URLs for sensitive operations +like OAuth flows, credential collection, or payment processing. +""" + +import uuid + +from pydantic import BaseModel, Field + +from mcp.server.fastmcp import Context, FastMCP +from mcp.server.session import ServerSession +from mcp.shared.exceptions import UrlElicitationRequiredError +from mcp.types import ElicitRequestURLParams + +mcp = FastMCP(name="Elicitation Example") + + +class BookingPreferences(BaseModel): + """Schema for collecting user preferences.""" + + checkAlternative: bool = Field(description="Would you like to check another date?") + alternativeDate: str = Field( + default="2024-12-26", + description="Alternative date (YYYY-MM-DD)", + ) + + +@mcp.tool() +async def book_table(date: str, time: str, party_size: int, ctx: Context[ServerSession, None]) -> str: + """Book a table with date availability check. + + This demonstrates form mode elicitation for collecting non-sensitive user input. + """ + # Check if date is available + if date == "2024-12-25": + # Date unavailable - ask user for alternative + result = await ctx.elicit( + message=(f"No tables available for {party_size} on {date}. Would you like to try another date?"), + schema=BookingPreferences, + ) + + if result.action == "accept" and result.data: + if result.data.checkAlternative: + return f"[SUCCESS] Booked for {result.data.alternativeDate}" + return "[CANCELLED] No booking made" + return "[CANCELLED] Booking cancelled" + + # Date available + return f"[SUCCESS] Booked for {date} at {time}" + + +@mcp.tool() +async def secure_payment(amount: float, ctx: Context[ServerSession, None]) -> str: + """Process a secure payment requiring URL confirmation. + + This demonstrates URL mode elicitation using ctx.elicit_url() for + operations that require out-of-band user interaction. + """ + elicitation_id = str(uuid.uuid4()) + + result = await ctx.elicit_url( + message=f"Please confirm payment of ${amount:.2f}", + url=f"https://payments.example.com/confirm?amount={amount}&id={elicitation_id}", + elicitation_id=elicitation_id, + ) + + if result.action == "accept": + # In a real app, the payment confirmation would happen out-of-band + # and you'd verify the payment status from your backend + return f"Payment of ${amount:.2f} initiated - check your browser to complete" + elif result.action == "decline": + return "Payment declined by user" + return "Payment cancelled" + + +@mcp.tool() +async def connect_service(service_name: str, ctx: Context[ServerSession, None]) -> str: + """Connect to a third-party service requiring OAuth authorization. + + This demonstrates the "throw error" pattern using UrlElicitationRequiredError. + Use this pattern when the tool cannot proceed without user authorization. + """ + elicitation_id = str(uuid.uuid4()) + + # Raise UrlElicitationRequiredError to signal that the client must complete + # a URL elicitation before this request can be processed. + # The MCP framework will convert this to a -32042 error response. + raise UrlElicitationRequiredError( + [ + ElicitRequestURLParams( + mode="url", + message=f"Authorization required to connect to {service_name}", + url=f"https://{service_name}.example.com/oauth/authorize?elicit={elicitation_id}", + elicitationId=elicitation_id, + ) + ] + ) +``` + +_Full example: [examples/snippets/servers/elicitation.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/elicitation.py)_ + + +Elicitation schemas support default values for all field types. Default values are automatically included in the JSON schema sent to clients, allowing them to pre-populate forms. + +The `elicit()` method returns an `ElicitationResult` with: + +- `action`: "accept", "decline", or "cancel" +- `data`: The validated response (only when accepted) +- `validation_error`: Any validation error message + +### Sampling + +Tools can interact with LLMs through sampling (generating text): + + +```python +from mcp.server.fastmcp import Context, FastMCP +from mcp.server.session import ServerSession +from mcp.types import SamplingMessage, TextContent + +mcp = FastMCP(name="Sampling Example") + + +@mcp.tool() +async def generate_poem(topic: str, ctx: Context[ServerSession, None]) -> str: + """Generate a poem using LLM sampling.""" + prompt = f"Write a short poem about {topic}" + + result = await ctx.session.create_message( + messages=[ + SamplingMessage( + role="user", + content=TextContent(type="text", text=prompt), + ) + ], + max_tokens=100, + ) + + # Since we're not passing tools param, result.content is single content + if result.content.type == "text": + return result.content.text + return str(result.content) +``` + +_Full example: [examples/snippets/servers/sampling.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/sampling.py)_ + + +### Logging and Notifications + +Tools can send logs and notifications through the context: + + +```python +from mcp.server.fastmcp import Context, FastMCP +from mcp.server.session import ServerSession + +mcp = FastMCP(name="Notifications Example") + + +@mcp.tool() +async def process_data(data: str, ctx: Context[ServerSession, None]) -> str: + """Process data with logging.""" + # Different log levels + await ctx.debug(f"Debug: Processing '{data}'") + await ctx.info("Info: Starting processing") + await ctx.warning("Warning: This is experimental") + await ctx.error("Error: (This is just a demo)") + + # Notify about resource changes + await ctx.session.send_resource_list_changed() + + return f"Processed: {data}" +``` + +_Full example: [examples/snippets/servers/notifications.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/notifications.py)_ + + +### Authentication + +Authentication can be used by servers that want to expose tools accessing protected resources. + +`mcp.server.auth` implements OAuth 2.1 resource server functionality, where MCP servers act as Resource Servers (RS) that validate tokens issued by separate Authorization Servers (AS). This follows the [MCP authorization specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization) and implements RFC 9728 (Protected Resource Metadata) for AS discovery. + +MCP servers can use authentication by providing an implementation of the `TokenVerifier` protocol: + + +```python +""" +Run from the repository root: + uv run examples/snippets/servers/oauth_server.py +""" + +from pydantic import AnyHttpUrl + +from mcp.server.auth.provider import AccessToken, TokenVerifier +from mcp.server.auth.settings import AuthSettings +from mcp.server.fastmcp import FastMCP + + +class SimpleTokenVerifier(TokenVerifier): + """Simple token verifier for demonstration.""" + + async def verify_token(self, token: str) -> AccessToken | None: + pass # This is where you would implement actual token validation + + +# Create FastMCP instance as a Resource Server +mcp = FastMCP( + "Weather Service", + json_response=True, + # Token verifier for authentication + token_verifier=SimpleTokenVerifier(), + # Auth settings for RFC 9728 Protected Resource Metadata + auth=AuthSettings( + issuer_url=AnyHttpUrl("https://auth.example.com"), # Authorization Server URL + resource_server_url=AnyHttpUrl("http://localhost:3001"), # This server's URL + required_scopes=["user"], + ), +) + + +@mcp.tool() +async def get_weather(city: str = "London") -> dict[str, str]: + """Get weather data for a city""" + return { + "city": city, + "temperature": "22", + "condition": "Partly cloudy", + "humidity": "65%", + } + + +if __name__ == "__main__": + mcp.run(transport="streamable-http") +``` + +_Full example: [examples/snippets/servers/oauth_server.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/oauth_server.py)_ + + +For a complete example with separate Authorization Server and Resource Server implementations, see [`examples/servers/simple-auth/`](examples/servers/simple-auth/). + +**Architecture:** + +- **Authorization Server (AS)**: Handles OAuth flows, user authentication, and token issuance +- **Resource Server (RS)**: Your MCP server that validates tokens and serves protected resources +- **Client**: Discovers AS through RFC 9728, obtains tokens, and uses them with the MCP server + +See [TokenVerifier](src/mcp/server/auth/provider.py) for more details on implementing token validation. + +### FastMCP Properties + +The FastMCP server instance accessible via `ctx.fastmcp` provides access to server configuration and metadata: + +- `ctx.fastmcp.name` - The server's name as defined during initialization +- `ctx.fastmcp.instructions` - Server instructions/description provided to clients +- `ctx.fastmcp.website_url` - Optional website URL for the server +- `ctx.fastmcp.icons` - Optional list of icons for UI display +- `ctx.fastmcp.settings` - Complete server configuration object containing: + - `debug` - Debug mode flag + - `log_level` - Current logging level + - `host` and `port` - Server network configuration + - `mount_path`, `sse_path`, `streamable_http_path` - Transport paths + - `stateless_http` - Whether the server operates in stateless mode + - And other configuration options + +```python +@mcp.tool() +def server_info(ctx: Context) -> dict: + """Get information about the current server.""" + return { + "name": ctx.fastmcp.name, + "instructions": ctx.fastmcp.instructions, + "debug_mode": ctx.fastmcp.settings.debug, + "log_level": ctx.fastmcp.settings.log_level, + "host": ctx.fastmcp.settings.host, + "port": ctx.fastmcp.settings.port, + } +``` + +### Session Properties and Methods + +The session object accessible via `ctx.session` provides advanced control over client communication: + +- `ctx.session.client_params` - Client initialization parameters and declared capabilities +- `await ctx.session.send_log_message(level, data, logger)` - Send log messages with full control +- `await ctx.session.create_message(messages, max_tokens)` - Request LLM sampling/completion +- `await ctx.session.send_progress_notification(token, progress, total, message)` - Direct progress updates +- `await ctx.session.send_resource_updated(uri)` - Notify clients that a specific resource changed +- `await ctx.session.send_resource_list_changed()` - Notify clients that the resource list changed +- `await ctx.session.send_tool_list_changed()` - Notify clients that the tool list changed +- `await ctx.session.send_prompt_list_changed()` - Notify clients that the prompt list changed + +```python +@mcp.tool() +async def notify_data_update(resource_uri: str, ctx: Context) -> str: + """Update data and notify clients of the change.""" + # Perform data update logic here + + # Notify clients that this specific resource changed + await ctx.session.send_resource_updated(AnyUrl(resource_uri)) + + # If this affects the overall resource list, notify about that too + await ctx.session.send_resource_list_changed() + + return f"Updated {resource_uri} and notified clients" +``` + +### Request Context Properties + +The request context accessible via `ctx.request_context` contains request-specific information and resources: + +- `ctx.request_context.lifespan_context` - Access to resources initialized during server startup + - Database connections, configuration objects, shared services + - Type-safe access to resources defined in your server's lifespan function +- `ctx.request_context.meta` - Request metadata from the client including: + - `progressToken` - Token for progress notifications + - Other client-provided metadata +- `ctx.request_context.request` - The original MCP request object for advanced processing +- `ctx.request_context.request_id` - Unique identifier for this request + +```python +# Example with typed lifespan context +@dataclass +class AppContext: + db: Database + config: AppConfig + +@mcp.tool() +def query_with_config(query: str, ctx: Context) -> str: + """Execute a query using shared database and configuration.""" + # Access typed lifespan context + app_ctx: AppContext = ctx.request_context.lifespan_context + + # Use shared resources + connection = app_ctx.db + settings = app_ctx.config + + # Execute query with configuration + result = connection.execute(query, timeout=settings.query_timeout) + return str(result) +``` + +_Full lifespan example: [examples/snippets/servers/lifespan_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lifespan_example.py)_ + +## Running Your Server + +### Development Mode + +The fastest way to test and debug your server is with the MCP Inspector: + +```bash +uv run mcp dev server.py + +# Add dependencies +uv run mcp dev server.py --with pandas --with numpy + +# Mount local code +uv run mcp dev server.py --with-editable . +``` + +### Claude Desktop Integration + +Once your server is ready, install it in Claude Desktop: + +```bash +uv run mcp install server.py + +# Custom name +uv run mcp install server.py --name "My Analytics Server" + +# Environment variables +uv run mcp install server.py -v API_KEY=abc123 -v DB_URL=postgres://... +uv run mcp install server.py -f .env +``` + +### Direct Execution + +For advanced scenarios like custom deployments: + + +```python +"""Example showing direct execution of an MCP server. + +This is the simplest way to run an MCP server directly. +cd to the `examples/snippets` directory and run: + uv run direct-execution-server + or + python servers/direct_execution.py +""" + +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP("My App") + + +@mcp.tool() +def hello(name: str = "World") -> str: + """Say hello to someone.""" + return f"Hello, {name}!" + + +def main(): + """Entry point for the direct execution server.""" + mcp.run() + + +if __name__ == "__main__": + main() +``` + +_Full example: [examples/snippets/servers/direct_execution.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/direct_execution.py)_ + + +Run it with: + +```bash +python servers/direct_execution.py +# or +uv run mcp run servers/direct_execution.py +``` + +Note that `uv run mcp run` or `uv run mcp dev` only supports server using FastMCP and not the low-level server variant. + +### Streamable HTTP Transport + +> **Note**: Streamable HTTP transport is the recommended transport for production deployments. Use `stateless_http=True` and `json_response=True` for optimal scalability. + + +```python +""" +Run from the repository root: + uv run examples/snippets/servers/streamable_config.py +""" + +from mcp.server.fastmcp import FastMCP + +# Stateless server with JSON responses (recommended) +mcp = FastMCP("StatelessServer", stateless_http=True, json_response=True) + +# Other configuration options: +# Stateless server with SSE streaming responses +# mcp = FastMCP("StatelessServer", stateless_http=True) + +# Stateful server with session persistence +# mcp = FastMCP("StatefulServer") + + +# Add a simple tool to demonstrate the server +@mcp.tool() +def greet(name: str = "World") -> str: + """Greet someone by name.""" + return f"Hello, {name}!" + + +# Run server with streamable_http transport +if __name__ == "__main__": + mcp.run(transport="streamable-http") +``` + +_Full example: [examples/snippets/servers/streamable_config.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_config.py)_ + + +You can mount multiple FastMCP servers in a Starlette application: + + +```python +""" +Run from the repository root: + uvicorn examples.snippets.servers.streamable_starlette_mount:app --reload +""" + +import contextlib + +from starlette.applications import Starlette +from starlette.routing import Mount + +from mcp.server.fastmcp import FastMCP + +# Create the Echo server +echo_mcp = FastMCP(name="EchoServer", stateless_http=True, json_response=True) + + +@echo_mcp.tool() +def echo(message: str) -> str: + """A simple echo tool""" + return f"Echo: {message}" + + +# Create the Math server +math_mcp = FastMCP(name="MathServer", stateless_http=True, json_response=True) + + +@math_mcp.tool() +def add_two(n: int) -> int: + """Tool to add two to the input""" + return n + 2 + + +# Create a combined lifespan to manage both session managers +@contextlib.asynccontextmanager +async def lifespan(app: Starlette): + async with contextlib.AsyncExitStack() as stack: + await stack.enter_async_context(echo_mcp.session_manager.run()) + await stack.enter_async_context(math_mcp.session_manager.run()) + yield + + +# Create the Starlette app and mount the MCP servers +app = Starlette( + routes=[ + Mount("/echo", echo_mcp.streamable_http_app()), + Mount("/math", math_mcp.streamable_http_app()), + ], + lifespan=lifespan, +) + +# Note: Clients connect to http://localhost:8000/echo/mcp and http://localhost:8000/math/mcp +# To mount at the root of each path (e.g., /echo instead of /echo/mcp): +# echo_mcp.settings.streamable_http_path = "/" +# math_mcp.settings.streamable_http_path = "/" +``` + +_Full example: [examples/snippets/servers/streamable_starlette_mount.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_starlette_mount.py)_ + + +For low level server with Streamable HTTP implementations, see: + +- Stateful server: [`examples/servers/simple-streamablehttp/`](examples/servers/simple-streamablehttp/) +- Stateless server: [`examples/servers/simple-streamablehttp-stateless/`](examples/servers/simple-streamablehttp-stateless/) + +The streamable HTTP transport supports: + +- Stateful and stateless operation modes +- Resumability with event stores +- JSON or SSE response formats +- Better scalability for multi-node deployments + +#### CORS Configuration for Browser-Based Clients + +If you'd like your server to be accessible by browser-based MCP clients, you'll need to configure CORS headers. The `Mcp-Session-Id` header must be exposed for browser clients to access it: + +```python +from starlette.applications import Starlette +from starlette.middleware.cors import CORSMiddleware + +# Create your Starlette app first +starlette_app = Starlette(routes=[...]) + +# Then wrap it with CORS middleware +starlette_app = CORSMiddleware( + starlette_app, + allow_origins=["*"], # Configure appropriately for production + allow_methods=["GET", "POST", "DELETE"], # MCP streamable HTTP methods + expose_headers=["Mcp-Session-Id"], +) +``` + +This configuration is necessary because: + +- The MCP streamable HTTP transport uses the `Mcp-Session-Id` header for session management +- Browsers restrict access to response headers unless explicitly exposed via CORS +- Without this configuration, browser-based clients won't be able to read the session ID from initialization responses + +### Mounting to an Existing ASGI Server + +By default, SSE servers are mounted at `/sse` and Streamable HTTP servers are mounted at `/mcp`. You can customize these paths using the methods described below. + +For more information on mounting applications in Starlette, see the [Starlette documentation](https://www.starlette.io/routing/#submounting-routes). + +#### StreamableHTTP servers + +You can mount the StreamableHTTP server to an existing ASGI server using the `streamable_http_app` method. This allows you to integrate the StreamableHTTP server with other ASGI applications. + +##### Basic mounting + + +```python +""" +Basic example showing how to mount StreamableHTTP server in Starlette. + +Run from the repository root: + uvicorn examples.snippets.servers.streamable_http_basic_mounting:app --reload +""" + +import contextlib + +from starlette.applications import Starlette +from starlette.routing import Mount + +from mcp.server.fastmcp import FastMCP + +# Create MCP server +mcp = FastMCP("My App", json_response=True) + + +@mcp.tool() +def hello() -> str: + """A simple hello tool""" + return "Hello from MCP!" + + +# Create a lifespan context manager to run the session manager +@contextlib.asynccontextmanager +async def lifespan(app: Starlette): + async with mcp.session_manager.run(): + yield + + +# Mount the StreamableHTTP server to the existing ASGI server +app = Starlette( + routes=[ + Mount("/", app=mcp.streamable_http_app()), + ], + lifespan=lifespan, +) +``` + +_Full example: [examples/snippets/servers/streamable_http_basic_mounting.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_http_basic_mounting.py)_ + + +##### Host-based routing + + +```python +""" +Example showing how to mount StreamableHTTP server using Host-based routing. + +Run from the repository root: + uvicorn examples.snippets.servers.streamable_http_host_mounting:app --reload +""" + +import contextlib + +from starlette.applications import Starlette +from starlette.routing import Host + +from mcp.server.fastmcp import FastMCP + +# Create MCP server +mcp = FastMCP("MCP Host App", json_response=True) + + +@mcp.tool() +def domain_info() -> str: + """Get domain-specific information""" + return "This is served from mcp.acme.corp" + + +# Create a lifespan context manager to run the session manager +@contextlib.asynccontextmanager +async def lifespan(app: Starlette): + async with mcp.session_manager.run(): + yield + + +# Mount using Host-based routing +app = Starlette( + routes=[ + Host("mcp.acme.corp", app=mcp.streamable_http_app()), + ], + lifespan=lifespan, +) +``` + +_Full example: [examples/snippets/servers/streamable_http_host_mounting.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_http_host_mounting.py)_ + + +##### Multiple servers with path configuration + + +```python +""" +Example showing how to mount multiple StreamableHTTP servers with path configuration. + +Run from the repository root: + uvicorn examples.snippets.servers.streamable_http_multiple_servers:app --reload +""" + +import contextlib + +from starlette.applications import Starlette +from starlette.routing import Mount + +from mcp.server.fastmcp import FastMCP + +# Create multiple MCP servers +api_mcp = FastMCP("API Server", json_response=True) +chat_mcp = FastMCP("Chat Server", json_response=True) + + +@api_mcp.tool() +def api_status() -> str: + """Get API status""" + return "API is running" + + +@chat_mcp.tool() +def send_message(message: str) -> str: + """Send a chat message""" + return f"Message sent: {message}" + + +# Configure servers to mount at the root of each path +# This means endpoints will be at /api and /chat instead of /api/mcp and /chat/mcp +api_mcp.settings.streamable_http_path = "/" +chat_mcp.settings.streamable_http_path = "/" + + +# Create a combined lifespan to manage both session managers +@contextlib.asynccontextmanager +async def lifespan(app: Starlette): + async with contextlib.AsyncExitStack() as stack: + await stack.enter_async_context(api_mcp.session_manager.run()) + await stack.enter_async_context(chat_mcp.session_manager.run()) + yield + + +# Mount the servers +app = Starlette( + routes=[ + Mount("/api", app=api_mcp.streamable_http_app()), + Mount("/chat", app=chat_mcp.streamable_http_app()), + ], + lifespan=lifespan, +) +``` + +_Full example: [examples/snippets/servers/streamable_http_multiple_servers.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_http_multiple_servers.py)_ + + +##### Path configuration at initialization + + +```python +""" +Example showing path configuration during FastMCP initialization. + +Run from the repository root: + uvicorn examples.snippets.servers.streamable_http_path_config:app --reload +""" + +from starlette.applications import Starlette +from starlette.routing import Mount + +from mcp.server.fastmcp import FastMCP + +# Configure streamable_http_path during initialization +# This server will mount at the root of wherever it's mounted +mcp_at_root = FastMCP( + "My Server", + json_response=True, + streamable_http_path="/", +) + + +@mcp_at_root.tool() +def process_data(data: str) -> str: + """Process some data""" + return f"Processed: {data}" + + +# Mount at /process - endpoints will be at /process instead of /process/mcp +app = Starlette( + routes=[ + Mount("/process", app=mcp_at_root.streamable_http_app()), + ] +) +``` + +_Full example: [examples/snippets/servers/streamable_http_path_config.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_http_path_config.py)_ + + +#### SSE servers + +> **Note**: SSE transport is being superseded by [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http). + +You can mount the SSE server to an existing ASGI server using the `sse_app` method. This allows you to integrate the SSE server with other ASGI applications. + +```python +from starlette.applications import Starlette +from starlette.routing import Mount, Host +from mcp.server.fastmcp import FastMCP + + +mcp = FastMCP("My App") + +# Mount the SSE server to the existing ASGI server +app = Starlette( + routes=[ + Mount('/', app=mcp.sse_app()), + ] +) + +# or dynamically mount as host +app.router.routes.append(Host('mcp.acme.corp', app=mcp.sse_app())) +``` + +When mounting multiple MCP servers under different paths, you can configure the mount path in several ways: + +```python +from starlette.applications import Starlette +from starlette.routing import Mount +from mcp.server.fastmcp import FastMCP + +# Create multiple MCP servers +github_mcp = FastMCP("GitHub API") +browser_mcp = FastMCP("Browser") +curl_mcp = FastMCP("Curl") +search_mcp = FastMCP("Search") + +# Method 1: Configure mount paths via settings (recommended for persistent configuration) +github_mcp.settings.mount_path = "/github" +browser_mcp.settings.mount_path = "/browser" + +# Method 2: Pass mount path directly to sse_app (preferred for ad-hoc mounting) +# This approach doesn't modify the server's settings permanently + +# Create Starlette app with multiple mounted servers +app = Starlette( + routes=[ + # Using settings-based configuration + Mount("/github", app=github_mcp.sse_app()), + Mount("/browser", app=browser_mcp.sse_app()), + # Using direct mount path parameter + Mount("/curl", app=curl_mcp.sse_app("/curl")), + Mount("/search", app=search_mcp.sse_app("/search")), + ] +) + +# Method 3: For direct execution, you can also pass the mount path to run() +if __name__ == "__main__": + search_mcp.run(transport="sse", mount_path="/search") +``` + +For more information on mounting applications in Starlette, see the [Starlette documentation](https://www.starlette.io/routing/#submounting-routes). + +## Advanced Usage + +### Low-Level Server + +For more control, you can use the low-level server implementation directly. This gives you full access to the protocol and allows you to customize every aspect of your server, including lifecycle management through the lifespan API: + + +```python +""" +Run from the repository root: + uv run examples/snippets/servers/lowlevel/lifespan.py +""" + +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from typing import Any + +import mcp.server.stdio +import mcp.types as types +from mcp.server.lowlevel import NotificationOptions, Server +from mcp.server.models import InitializationOptions + + +# Mock database class for example +class Database: + """Mock database class for example.""" + + @classmethod + async def connect(cls) -> "Database": + """Connect to database.""" + print("Database connected") + return cls() + + async def disconnect(self) -> None: + """Disconnect from database.""" + print("Database disconnected") + + async def query(self, query_str: str) -> list[dict[str, str]]: + """Execute a query.""" + # Simulate database query + return [{"id": "1", "name": "Example", "query": query_str}] + + +@asynccontextmanager +async def server_lifespan(_server: Server) -> AsyncIterator[dict[str, Any]]: + """Manage server startup and shutdown lifecycle.""" + # Initialize resources on startup + db = await Database.connect() + try: + yield {"db": db} + finally: + # Clean up on shutdown + await db.disconnect() + + +# Pass lifespan to server +server = Server("example-server", lifespan=server_lifespan) + + +@server.list_tools() +async def handle_list_tools() -> list[types.Tool]: + """List available tools.""" + return [ + types.Tool( + name="query_db", + description="Query the database", + inputSchema={ + "type": "object", + "properties": {"query": {"type": "string", "description": "SQL query to execute"}}, + "required": ["query"], + }, + ) + ] + + +@server.call_tool() +async def query_db(name: str, arguments: dict[str, Any]) -> list[types.TextContent]: + """Handle database query tool call.""" + if name != "query_db": + raise ValueError(f"Unknown tool: {name}") + + # Access lifespan context + ctx = server.request_context + db = ctx.lifespan_context["db"] + + # Execute query + results = await db.query(arguments["query"]) + + return [types.TextContent(type="text", text=f"Query results: {results}")] + + +async def run(): + """Run the server with lifespan management.""" + async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + InitializationOptions( + server_name="example-server", + server_version="0.1.0", + capabilities=server.get_capabilities( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), + ), + ) + + +if __name__ == "__main__": + import asyncio + + asyncio.run(run()) +``` + +_Full example: [examples/snippets/servers/lowlevel/lifespan.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/lifespan.py)_ + + +The lifespan API provides: + +- A way to initialize resources when the server starts and clean them up when it stops +- Access to initialized resources through the request context in handlers +- Type-safe context passing between lifespan and request handlers + + +```python +""" +Run from the repository root: +uv run examples/snippets/servers/lowlevel/basic.py +""" + +import asyncio + +import mcp.server.stdio +import mcp.types as types +from mcp.server.lowlevel import NotificationOptions, Server +from mcp.server.models import InitializationOptions + +# Create a server instance +server = Server("example-server") + + +@server.list_prompts() +async def handle_list_prompts() -> list[types.Prompt]: + """List available prompts.""" + return [ + types.Prompt( + name="example-prompt", + description="An example prompt template", + arguments=[types.PromptArgument(name="arg1", description="Example argument", required=True)], + ) + ] + + +@server.get_prompt() +async def handle_get_prompt(name: str, arguments: dict[str, str] | None) -> types.GetPromptResult: + """Get a specific prompt by name.""" + if name != "example-prompt": + raise ValueError(f"Unknown prompt: {name}") + + arg1_value = (arguments or {}).get("arg1", "default") + + return types.GetPromptResult( + description="Example prompt", + messages=[ + types.PromptMessage( + role="user", + content=types.TextContent(type="text", text=f"Example prompt text with argument: {arg1_value}"), + ) + ], + ) + + +async def run(): + """Run the basic low-level server.""" + async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + InitializationOptions( + server_name="example", + server_version="0.1.0", + capabilities=server.get_capabilities( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), + ), + ) + + +if __name__ == "__main__": + asyncio.run(run()) +``` + +_Full example: [examples/snippets/servers/lowlevel/basic.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/basic.py)_ + + +Caution: The `uv run mcp run` and `uv run mcp dev` tool doesn't support low-level server. + +#### Structured Output Support + +The low-level server supports structured output for tools, allowing you to return both human-readable content and machine-readable structured data. Tools can define an `outputSchema` to validate their structured output: + + +```python +""" +Run from the repository root: + uv run examples/snippets/servers/lowlevel/structured_output.py +""" + +import asyncio +from typing import Any + +import mcp.server.stdio +import mcp.types as types +from mcp.server.lowlevel import NotificationOptions, Server +from mcp.server.models import InitializationOptions + +server = Server("example-server") + + +@server.list_tools() +async def list_tools() -> list[types.Tool]: + """List available tools with structured output schemas.""" + return [ + types.Tool( + name="get_weather", + description="Get current weather for a city", + inputSchema={ + "type": "object", + "properties": {"city": {"type": "string", "description": "City name"}}, + "required": ["city"], + }, + outputSchema={ + "type": "object", + "properties": { + "temperature": {"type": "number", "description": "Temperature in Celsius"}, + "condition": {"type": "string", "description": "Weather condition"}, + "humidity": {"type": "number", "description": "Humidity percentage"}, + "city": {"type": "string", "description": "City name"}, + }, + "required": ["temperature", "condition", "humidity", "city"], + }, + ) + ] + + +@server.call_tool() +async def call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + """Handle tool calls with structured output.""" + if name == "get_weather": + city = arguments["city"] + + # Simulated weather data - in production, call a weather API + weather_data = { + "temperature": 22.5, + "condition": "partly cloudy", + "humidity": 65, + "city": city, # Include the requested city + } + + # low-level server will validate structured output against the tool's + # output schema, and additionally serialize it into a TextContent block + # for backwards compatibility with pre-2025-06-18 clients. + return weather_data + else: + raise ValueError(f"Unknown tool: {name}") + + +async def run(): + """Run the structured output server.""" + async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + InitializationOptions( + server_name="structured-output-example", + server_version="0.1.0", + capabilities=server.get_capabilities( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), + ), + ) + + +if __name__ == "__main__": + asyncio.run(run()) +``` + +_Full example: [examples/snippets/servers/lowlevel/structured_output.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/structured_output.py)_ + + +Tools can return data in four ways: + +1. **Content only**: Return a list of content blocks (default behavior before spec revision 2025-06-18) +2. **Structured data only**: Return a dictionary that will be serialized to JSON (Introduced in spec revision 2025-06-18) +3. **Both**: Return a tuple of (content, structured_data) preferred option to use for backwards compatibility +4. **Direct CallToolResult**: Return `CallToolResult` directly for full control (including `_meta` field) + +When an `outputSchema` is defined, the server automatically validates the structured output against the schema. This ensures type safety and helps catch errors early. + +##### Returning CallToolResult Directly + +For full control over the response including the `_meta` field (for passing data to client applications without exposing it to the model), return `CallToolResult` directly: + + +```python +""" +Run from the repository root: + uv run examples/snippets/servers/lowlevel/direct_call_tool_result.py +""" + +import asyncio +from typing import Any + +import mcp.server.stdio +import mcp.types as types +from mcp.server.lowlevel import NotificationOptions, Server +from mcp.server.models import InitializationOptions + +server = Server("example-server") + + +@server.list_tools() +async def list_tools() -> list[types.Tool]: + """List available tools.""" + return [ + types.Tool( + name="advanced_tool", + description="Tool with full control including _meta field", + inputSchema={ + "type": "object", + "properties": {"message": {"type": "string"}}, + "required": ["message"], + }, + ) + ] + + +@server.call_tool() +async def handle_call_tool(name: str, arguments: dict[str, Any]) -> types.CallToolResult: + """Handle tool calls by returning CallToolResult directly.""" + if name == "advanced_tool": + message = str(arguments.get("message", "")) + return types.CallToolResult( + content=[types.TextContent(type="text", text=f"Processed: {message}")], + structuredContent={"result": "success", "message": message}, + _meta={"hidden": "data for client applications only"}, + ) + + raise ValueError(f"Unknown tool: {name}") + + +async def run(): + """Run the server.""" + async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + InitializationOptions( + server_name="example", + server_version="0.1.0", + capabilities=server.get_capabilities( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), + ), + ) + + +if __name__ == "__main__": + asyncio.run(run()) +``` + +_Full example: [examples/snippets/servers/lowlevel/direct_call_tool_result.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/direct_call_tool_result.py)_ + + +**Note:** When returning `CallToolResult`, you bypass the automatic content/structured conversion. You must construct the complete response yourself. + +### Pagination (Advanced) + +For servers that need to handle large datasets, the low-level server provides paginated versions of list operations. This is an optional optimization - most servers won't need pagination unless they're dealing with hundreds or thousands of items. + +#### Server-side Implementation + + +```python +""" +Example of implementing pagination with MCP server decorators. +""" + +from pydantic import AnyUrl + +import mcp.types as types +from mcp.server.lowlevel import Server + +# Initialize the server +server = Server("paginated-server") + +# Sample data to paginate +ITEMS = [f"Item {i}" for i in range(1, 101)] # 100 items + + +@server.list_resources() +async def list_resources_paginated(request: types.ListResourcesRequest) -> types.ListResourcesResult: + """List resources with pagination support.""" + page_size = 10 + + # Extract cursor from request params + cursor = request.params.cursor if request.params is not None else None + + # Parse cursor to get offset + start = 0 if cursor is None else int(cursor) + end = start + page_size + + # Get page of resources + page_items = [ + types.Resource(uri=AnyUrl(f"resource://items/{item}"), name=item, description=f"Description for {item}") + for item in ITEMS[start:end] + ] + + # Determine next cursor + next_cursor = str(end) if end < len(ITEMS) else None + + return types.ListResourcesResult(resources=page_items, nextCursor=next_cursor) +``` + +_Full example: [examples/snippets/servers/pagination_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/pagination_example.py)_ + + +#### Client-side Consumption + + +```python +""" +Example of consuming paginated MCP endpoints from a client. +""" + +import asyncio + +from mcp.client.session import ClientSession +from mcp.client.stdio import StdioServerParameters, stdio_client +from mcp.types import PaginatedRequestParams, Resource + + +async def list_all_resources() -> None: + """Fetch all resources using pagination.""" + async with stdio_client(StdioServerParameters(command="uv", args=["run", "mcp-simple-pagination"])) as ( + read, + write, + ): + async with ClientSession(read, write) as session: + await session.initialize() + + all_resources: list[Resource] = [] + cursor = None + + while True: + # Fetch a page of resources + result = await session.list_resources(params=PaginatedRequestParams(cursor=cursor)) + all_resources.extend(result.resources) + + print(f"Fetched {len(result.resources)} resources") + + # Check if there are more pages + if result.nextCursor: + cursor = result.nextCursor + else: + break + + print(f"Total resources: {len(all_resources)}") + + +if __name__ == "__main__": + asyncio.run(list_all_resources()) +``` + +_Full example: [examples/snippets/clients/pagination_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/pagination_client.py)_ + + +#### Key Points + +- **Cursors are opaque strings** - the server defines the format (numeric offsets, timestamps, etc.) +- **Return `nextCursor=None`** when there are no more pages +- **Backward compatible** - clients that don't support pagination will still work (they'll just get the first page) +- **Flexible page sizes** - Each endpoint can define its own page size based on data characteristics + +See the [simple-pagination example](examples/servers/simple-pagination) for a complete implementation. + +### Writing MCP Clients + +The SDK provides a high-level client interface for connecting to MCP servers using various [transports](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports): + + +```python +""" +cd to the `examples/snippets/clients` directory and run: + uv run client +""" + +import asyncio +import os + +from pydantic import AnyUrl + +from mcp import ClientSession, StdioServerParameters, types +from mcp.client.stdio import stdio_client +from mcp.shared.context import RequestContext + +# Create server parameters for stdio connection +server_params = StdioServerParameters( + command="uv", # Using uv to run the server + args=["run", "server", "fastmcp_quickstart", "stdio"], # We're already in snippets dir + env={"UV_INDEX": os.environ.get("UV_INDEX", "")}, +) + + +# Optional: create a sampling callback +async def handle_sampling_message( + context: RequestContext[ClientSession, None], params: types.CreateMessageRequestParams +) -> types.CreateMessageResult: + print(f"Sampling request: {params.messages}") + return types.CreateMessageResult( + role="assistant", + content=types.TextContent( + type="text", + text="Hello, world! from model", + ), + model="gpt-3.5-turbo", + stopReason="endTurn", + ) + + +async def run(): + async with stdio_client(server_params) as (read, write): + async with ClientSession(read, write, sampling_callback=handle_sampling_message) as session: + # Initialize the connection + await session.initialize() + + # List available prompts + prompts = await session.list_prompts() + print(f"Available prompts: {[p.name for p in prompts.prompts]}") + + # Get a prompt (greet_user prompt from fastmcp_quickstart) + if prompts.prompts: + prompt = await session.get_prompt("greet_user", arguments={"name": "Alice", "style": "friendly"}) + print(f"Prompt result: {prompt.messages[0].content}") + + # List available resources + resources = await session.list_resources() + print(f"Available resources: {[r.uri for r in resources.resources]}") + + # List available tools + tools = await session.list_tools() + print(f"Available tools: {[t.name for t in tools.tools]}") + + # Read a resource (greeting resource from fastmcp_quickstart) + resource_content = await session.read_resource(AnyUrl("greeting://World")) + content_block = resource_content.contents[0] + if isinstance(content_block, types.TextContent): + print(f"Resource content: {content_block.text}") + + # Call a tool (add tool from fastmcp_quickstart) + result = await session.call_tool("add", arguments={"a": 5, "b": 3}) + result_unstructured = result.content[0] + if isinstance(result_unstructured, types.TextContent): + print(f"Tool result: {result_unstructured.text}") + result_structured = result.structuredContent + print(f"Structured tool result: {result_structured}") + + +def main(): + """Entry point for the client script.""" + asyncio.run(run()) + + +if __name__ == "__main__": + main() +``` + +_Full example: [examples/snippets/clients/stdio_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/stdio_client.py)_ + + +Clients can also connect using [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http): + + +```python +""" +Run from the repository root: + uv run examples/snippets/clients/streamable_basic.py +""" + +import asyncio + +from mcp import ClientSession +from mcp.client.streamable_http import streamable_http_client + + +async def main(): + # Connect to a streamable HTTP server + async with streamable_http_client("http://localhost:8000/mcp") as ( + read_stream, + write_stream, + _, + ): + # Create a session using the client streams + async with ClientSession(read_stream, write_stream) as session: + # Initialize the connection + await session.initialize() + # List available tools + tools = await session.list_tools() + print(f"Available tools: {[tool.name for tool in tools.tools]}") + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +_Full example: [examples/snippets/clients/streamable_basic.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/streamable_basic.py)_ + + +### Client Display Utilities + +When building MCP clients, the SDK provides utilities to help display human-readable names for tools, resources, and prompts: + + +```python +""" +cd to the `examples/snippets` directory and run: + uv run display-utilities-client +""" + +import asyncio +import os + +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client +from mcp.shared.metadata_utils import get_display_name + +# Create server parameters for stdio connection +server_params = StdioServerParameters( + command="uv", # Using uv to run the server + args=["run", "server", "fastmcp_quickstart", "stdio"], + env={"UV_INDEX": os.environ.get("UV_INDEX", "")}, +) + + +async def display_tools(session: ClientSession): + """Display available tools with human-readable names""" + tools_response = await session.list_tools() + + for tool in tools_response.tools: + # get_display_name() returns the title if available, otherwise the name + display_name = get_display_name(tool) + print(f"Tool: {display_name}") + if tool.description: + print(f" {tool.description}") + + +async def display_resources(session: ClientSession): + """Display available resources with human-readable names""" + resources_response = await session.list_resources() + + for resource in resources_response.resources: + display_name = get_display_name(resource) + print(f"Resource: {display_name} ({resource.uri})") + + templates_response = await session.list_resource_templates() + for template in templates_response.resourceTemplates: + display_name = get_display_name(template) + print(f"Resource Template: {display_name}") + + +async def run(): + """Run the display utilities example.""" + async with stdio_client(server_params) as (read, write): + async with ClientSession(read, write) as session: + # Initialize the connection + await session.initialize() + + print("=== Available Tools ===") + await display_tools(session) + + print("\n=== Available Resources ===") + await display_resources(session) + + +def main(): + """Entry point for the display utilities client.""" + asyncio.run(run()) + + +if __name__ == "__main__": + main() +``` + +_Full example: [examples/snippets/clients/display_utilities.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/display_utilities.py)_ + + +The `get_display_name()` function implements the proper precedence rules for displaying names: + +- For tools: `title` > `annotations.title` > `name` +- For other objects: `title` > `name` + +This ensures your client UI shows the most user-friendly names that servers provide. + +### OAuth Authentication for Clients + +The SDK includes [authorization support](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) for connecting to protected MCP servers: + + +```python +""" +Before running, specify running MCP RS server URL. +To spin up RS server locally, see + examples/servers/simple-auth/README.md + +cd to the `examples/snippets` directory and run: + uv run oauth-client +""" + +import asyncio +from urllib.parse import parse_qs, urlparse + +import httpx +from pydantic import AnyUrl + +from mcp import ClientSession +from mcp.client.auth import OAuthClientProvider, TokenStorage +from mcp.client.streamable_http import streamable_http_client +from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken + + +class InMemoryTokenStorage(TokenStorage): + """Demo In-memory token storage implementation.""" + + def __init__(self): + self.tokens: OAuthToken | None = None + self.client_info: OAuthClientInformationFull | None = None + + async def get_tokens(self) -> OAuthToken | None: + """Get stored tokens.""" + return self.tokens + + async def set_tokens(self, tokens: OAuthToken) -> None: + """Store tokens.""" + self.tokens = tokens + + async def get_client_info(self) -> OAuthClientInformationFull | None: + """Get stored client information.""" + return self.client_info + + async def set_client_info(self, client_info: OAuthClientInformationFull) -> None: + """Store client information.""" + self.client_info = client_info + + +async def handle_redirect(auth_url: str) -> None: + print(f"Visit: {auth_url}") + + +async def handle_callback() -> tuple[str, str | None]: + callback_url = input("Paste callback URL: ") + params = parse_qs(urlparse(callback_url).query) + return params["code"][0], params.get("state", [None])[0] + + +async def main(): + """Run the OAuth client example.""" + oauth_auth = OAuthClientProvider( + server_url="http://localhost:8001", + client_metadata=OAuthClientMetadata( + client_name="Example MCP Client", + redirect_uris=[AnyUrl("http://localhost:3000/callback")], + grant_types=["authorization_code", "refresh_token"], + response_types=["code"], + scope="user", + ), + storage=InMemoryTokenStorage(), + redirect_handler=handle_redirect, + callback_handler=handle_callback, + ) + + async with httpx.AsyncClient(auth=oauth_auth, follow_redirects=True) as custom_client: + async with streamable_http_client("http://localhost:8001/mcp", http_client=custom_client) as (read, write, _): + async with ClientSession(read, write) as session: + await session.initialize() + + tools = await session.list_tools() + print(f"Available tools: {[tool.name for tool in tools.tools]}") + + resources = await session.list_resources() + print(f"Available resources: {[r.uri for r in resources.resources]}") + + +def run(): + asyncio.run(main()) + + +if __name__ == "__main__": + run() +``` + +_Full example: [examples/snippets/clients/oauth_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/oauth_client.py)_ + + +For a complete working example, see [`examples/clients/simple-auth-client/`](examples/clients/simple-auth-client/). + +### Parsing Tool Results + +When calling tools through MCP, the `CallToolResult` object contains the tool's response in a structured format. Understanding how to parse this result is essential for properly handling tool outputs. + +```python +"""examples/snippets/clients/parsing_tool_results.py""" + +import asyncio + +from mcp import ClientSession, StdioServerParameters, types +from mcp.client.stdio import stdio_client + + +async def parse_tool_results(): + """Demonstrates how to parse different types of content in CallToolResult.""" + server_params = StdioServerParameters( + command="python", args=["path/to/mcp_server.py"] + ) + + async with stdio_client(server_params) as (read, write): + async with ClientSession(read, write) as session: + await session.initialize() + + # Example 1: Parsing text content + result = await session.call_tool("get_data", {"format": "text"}) + for content in result.content: + if isinstance(content, types.TextContent): + print(f"Text: {content.text}") + + # Example 2: Parsing structured content from JSON tools + result = await session.call_tool("get_user", {"id": "123"}) + if hasattr(result, "structuredContent") and result.structuredContent: + # Access structured data directly + user_data = result.structuredContent + print(f"User: {user_data.get('name')}, Age: {user_data.get('age')}") + + # Example 3: Parsing embedded resources + result = await session.call_tool("read_config", {}) + for content in result.content: + if isinstance(content, types.EmbeddedResource): + resource = content.resource + if isinstance(resource, types.TextResourceContents): + print(f"Config from {resource.uri}: {resource.text}") + elif isinstance(resource, types.BlobResourceContents): + print(f"Binary data from {resource.uri}") + + # Example 4: Parsing image content + result = await session.call_tool("generate_chart", {"data": [1, 2, 3]}) + for content in result.content: + if isinstance(content, types.ImageContent): + print(f"Image ({content.mimeType}): {len(content.data)} bytes") + + # Example 5: Handling errors + result = await session.call_tool("failing_tool", {}) + if result.isError: + print("Tool execution failed!") + for content in result.content: + if isinstance(content, types.TextContent): + print(f"Error: {content.text}") + + +async def main(): + await parse_tool_results() + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +### MCP Primitives + +The MCP protocol defines three core primitives that servers can implement: + +| Primitive | Control | Description | Example Use | +|-----------|-----------------------|-----------------------------------------------------|------------------------------| +| Prompts | User-controlled | Interactive templates invoked by user choice | Slash commands, menu options | +| Resources | Application-controlled| Contextual data managed by the client application | File contents, API responses | +| Tools | Model-controlled | Functions exposed to the LLM to take actions | API calls, data updates | + +### Server Capabilities + +MCP servers declare capabilities during initialization: + +| Capability | Feature Flag | Description | +|--------------|------------------------------|------------------------------------| +| `prompts` | `listChanged` | Prompt template management | +| `resources` | `subscribe`
`listChanged`| Resource exposure and updates | +| `tools` | `listChanged` | Tool discovery and execution | +| `logging` | - | Server logging configuration | +| `completions`| - | Argument completion suggestions | + +## Documentation + +- [API Reference](https://modelcontextprotocol.github.io/python-sdk/api/) +- [Experimental Features (Tasks)](https://modelcontextprotocol.github.io/python-sdk/experimental/tasks/) +- [Model Context Protocol documentation](https://modelcontextprotocol.io) +- [Model Context Protocol specification](https://modelcontextprotocol.io/specification/latest) +- [Officially supported servers](https://github.com/modelcontextprotocol/servers) + +## Contributing + +We are passionate about supporting contributors of all levels of experience and would love to see you get involved in the project. See the [contributing guide](CONTRIBUTING.md) to get started. + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/source/target-vendors/cp312/mcp-1.26.0.dist-info/RECORD b/source/target-vendors/cp312/mcp-1.26.0.dist-info/RECORD new file mode 100644 index 0000000..66e4ac7 --- /dev/null +++ b/source/target-vendors/cp312/mcp-1.26.0.dist-info/RECORD @@ -0,0 +1,115 @@ +mcp/__init__.py,sha256=9mrheVmhQg7bZ2JNzXnaIIYFQ-HVE7fVyG7AJQz4r3I,3224 +mcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +mcp/types.py,sha256=aCQQnNHquvQ8XauCmoMBWxuI7JrhSXZeaBSvryP5veU,64908 +mcp/cli/__init__.py,sha256=NJrJIUQmjc6FtSxvGNt1zeXnSLj8-Mli86laODTtQzE,107 +mcp/cli/claude.py,sha256=MKUuZGrBmMSslD2weLIxU7RdQXZl2OvuMMq-_-EAlgY,5086 +mcp/cli/cli.py,sha256=w6mWBmcy4y7nxLQDa4qSbxKV0BWeJqU_SFZf3qdOubs,15179 +mcp/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +mcp/client/__main__.py,sha256=_aXYJ3xJiQGURxEEfvV-4MAqxRJwIFptFaLHyJgfvRU,2588 +mcp/client/session.py,sha256=A31De5OwhsBRD-iF32lraArJQ_vYHJYkYYrI_XudAX8,23495 +mcp/client/session_group.py,sha256=wXt4sZI-WRXdy5PPv-hNwRhMuzEhKMkQf3bFvaOITEc,17423 +mcp/client/sse.py,sha256=foXGvt297jQOFV0AnFVfQhqRZVlnp1REjW7R1Bl1FFc,8257 +mcp/client/streamable_http.py,sha256=jrAcPU2onG5z1JSpYCnbzy_gXQv__nYHGx5Amv22L1s,28324 +mcp/client/websocket.py,sha256=mH6zD_nRnMr11uSFdDTy2TZ2YeS9S18v-JJ2rQ7tXck,3486 +mcp/client/auth/__init__.py,sha256=hvlPclaDk3EkHr_nrt6tx37iv_rQ8SQgosM0OhJSeQ4,488 +mcp/client/auth/exceptions.py,sha256=uUDHX_P0S3R_tt7oAS8ALQrUgOiV2Dgc0QQAdaaBcW8,264 +mcp/client/auth/oauth2.py,sha256=4n8H8M5-2rCXH9kBX81ZDkKW4XEhY5N8U3B018cIYuk,27638 +mcp/client/auth/utils.py,sha256=Mm443kj9pHY09mUYflmr89W8jexZalvDS96b_31tUIE,12069 +mcp/client/auth/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +mcp/client/auth/extensions/client_credentials.py,sha256=XvCarBQnB4DREuYEVygZbe0P9Wj7z8dSx_3SqajMKzQ,20897 +mcp/client/experimental/__init__.py,sha256=r923QPMrHQbLQYA_F48MLy2Y8ODdPWJhx8SuzhXcaEA,219 +mcp/client/experimental/task_handlers.py,sha256=yDrCoiCha5iBBVpjqZ66uk3ZoDiQznqU29xKGY4Dh_Q,10791 +mcp/client/experimental/tasks.py,sha256=LLEbfmIwCNk0PCpPSnhK6U0dcaqxENbdzLQXeGsqVjI,7055 +mcp/client/stdio/__init__.py,sha256=ojInz-HjRW8Wq0ExFIajmc9Uf8xrZFOZv5vzptxs9lQ,9403 +mcp/os/__init__.py,sha256=uqaGIfCF4I0IPeSsEEZMNwZuDF91U0WKAIbDcKzPRLo,43 +mcp/os/posix/__init__.py,sha256=l_2j8jhhgw69s_-eH4dCDk4QYxNlxKGIpaOCvcbv1Qk,40 +mcp/os/posix/utilities.py,sha256=mTEYV74M9z9s2deXWctpUGTS1MNtkOr7IYCXU3mxlNI,1930 +mcp/os/win32/__init__.py,sha256=7xyI7KWpZhDS3omGEBcu3200Q-DjIsS7c-a-vrn28Y4,42 +mcp/os/win32/utilities.py,sha256=M5j9NF9QhgCHqQeHYpc9z0ksFsNy1F43vYqQl3LXq-Y,10808 +mcp/server/__init__.py,sha256=jeMjAkvChhKLEy1fpeKJ4d_bT1vnnmuGm11otQ7ncBg,202 +mcp/server/__main__.py,sha256=KakUSquF7R44Ugwg3N5VQCJsPnsEVrbwk-UnRHerVww,1328 +mcp/server/elicitation.py,sha256=euqjotipBszsirdZurr2fbKemRYwR7-zlZlijwMdSNY,6917 +mcp/server/models.py,sha256=w-xEPy_qGds2_bEpejN6iaWeh0UvMtKqwlXzPN_ChZ0,422 +mcp/server/session.py,sha256=A0pB1LsQnWgJUowQDhUy45BZHY2_qi29fuJ26jLPc0k,27555 +mcp/server/sse.py,sha256=QqzE9gWpG35HfQo6gbsCmZPV4ud02EtZcewNGTwKvF8,10826 +mcp/server/stdio.py,sha256=LkFigRZC4uIDUA2JjnsKlgOuLh9IWFBNVgHcs97JsM8,3358 +mcp/server/streamable_http.py,sha256=75k3sVYmcBe6t0zmk0XgiOuAq3RSMCr50j8248Wy7W0,46836 +mcp/server/streamable_http_manager.py,sha256=OoFoMo4CUKCVbRgVzsI5-MsLS4qj7rWSMBdrkem8uu4,12183 +mcp/server/transport_security.py,sha256=T8oLE1lh2WXW6cVvPiNUSmG5WmxMvdCCXkm7JdSvmIs,4938 +mcp/server/validation.py,sha256=owYWMCEg5KLesnXmNbyaTifJyJKJYp79FszXgLPsG7c,3633 +mcp/server/websocket.py,sha256=sw9aSmyhOZlqK-jPQd9RS03zn9XaLR4D6HL6vi8DtBY,2352 +mcp/server/auth/__init__.py,sha256=xmxBzGAYvejOEzxqvZxiRkootIdlpp5WrCNiPKvuKdk,51 +mcp/server/auth/errors.py,sha256=8YjlejJPl-qMjZrbJTDzMPBOAOITn0Da0pC5QWn9QdE,224 +mcp/server/auth/json_response.py,sha256=EMv3qAH62vRuw8XBHdFU3kYp0S8Jd6DjRGfwc7Jc9XU,377 +mcp/server/auth/provider.py,sha256=itp10kM6TGcQGL_zMUVpQ28FK3p-q_zFK5V3CJNoHBE,9846 +mcp/server/auth/routes.py,sha256=j4B5b2Pw2VJnCOq3bW2t3mRvgxJ3oI5wzQn0z3AlZ50,9131 +mcp/server/auth/settings.py,sha256=Nj8tNCXANHMCmX6nxCa-GUrA9knJ9f8zCuMxMDFN4D4,1031 +mcp/server/auth/handlers/__init__.py,sha256=T2ueQWVpLDbMIAiOcROyumxRwqa8Po5lA9zd_Cbt2Uc,58 +mcp/server/auth/handlers/authorize.py,sha256=fAz-L-5Qv6KeHjpoBOAUE6sYgTuG0ydPA7y2xjb4pIM,9728 +mcp/server/auth/handlers/metadata.py,sha256=mWPkrs1DfG-Qzq1nZU2HzwxSwk8K_vXe-ZpEDZ0k_QA,853 +mcp/server/auth/handlers/register.py,sha256=CtH9c03oZtJgUoBVr-VgrAoJ6WXBw2RGNk5n70eBlPc,5928 +mcp/server/auth/handlers/revoke.py,sha256=DIVjn09k7qTkAXrkTsRBVlR-XM9rsmTJzKlXyNdJqDE,3113 +mcp/server/auth/handlers/token.py,sha256=yt5kUvB8zhBgONpYZJOQTFyoJZBhC0UKPUE3GQMkots,10223 +mcp/server/auth/middleware/__init__.py,sha256=XhRUi_w-ie_QG7XT5EfqNG-NPZvXtJtgHSB_tPVYNIw,42 +mcp/server/auth/middleware/auth_context.py,sha256=0hYDUr2GL0VqTVQtCCZCDop2JwGox_IuD0XBZQbtpT0,1696 +mcp/server/auth/middleware/bearer_auth.py,sha256=-t3Ar5bNuIJtYSlF2CsvmAozEX2uySo4chp2Rzgbw4s,4423 +mcp/server/auth/middleware/client_auth.py,sha256=lIIGufkj8rwkNVjh63tPH1UOY5h4Bc08zuvGmubUz40,4723 +mcp/server/experimental/__init__.py,sha256=DtyLxeneqNnUc4ZDKeD9KBFEX9ku2iRwOqsME9k5Gvc,373 +mcp/server/experimental/request_context.py,sha256=MAH7Z-Xoe0vENzcPtu4_lVGywDZHp-7FaUJ8bYDt8Jo,8448 +mcp/server/experimental/session_features.py,sha256=fwwIb-Uj2hhjnrE2ZOfEL5Y5UgNa8gg8SiIl5QpN8YA,7870 +mcp/server/experimental/task_context.py,sha256=M970tK_ZM_mle0gRfgSTvd5NztUMW5Wx3yCYExRfo8I,23021 +mcp/server/experimental/task_result_handler.py,sha256=EyTcsYcdwzK49_z8RnBfRX1WQKaFcEzKF3tnpEIUcvg,8379 +mcp/server/experimental/task_support.py,sha256=gP2NiEIaycpRsLa0ThQG8VFOoMGUaoh0mrn4BNfQChQ,3691 +mcp/server/fastmcp/__init__.py,sha256=4EnleAYdPF_hgPexfq2zxGRtU_Fd3ZrjXegKckuj-K8,297 +mcp/server/fastmcp/exceptions.py,sha256=q9djUDmpwmGEWcHI8q4UzJBtf7s7UtgL--OB7OaGzyQ,435 +mcp/server/fastmcp/server.py,sha256=q9Jb2B34oDCO66kM3htygwfYUsfv4Ku54KXGrPW3ubc,52706 +mcp/server/fastmcp/prompts/__init__.py,sha256=4BsMxoYolpoxg74xkkkzCFL8vvdkLVJ5cIPNs1ND1Jo,99 +mcp/server/fastmcp/prompts/base.py,sha256=s5-zpCjfd-LLbqMoa2oJnzUoVUGW4EVhi8ufvWSjHns,7088 +mcp/server/fastmcp/prompts/manager.py,sha256=IXIv__muOQhXgipCmKWJ4cyPjjawWkjmeahe6G29oyU,1835 +mcp/server/fastmcp/resources/__init__.py,sha256=e4S369jBoJt07ez9_ZaJefzGfz4kr9nGwG4KPMzMHc8,464 +mcp/server/fastmcp/resources/base.py,sha256=aw7d46dfMGCz9fYNtShxCJ9PlnNGmoev4JfPpstC8xI,1852 +mcp/server/fastmcp/resources/resource_manager.py,sha256=f_yS7f4-hs-RryiLHlxXmf68bHd4cZnIPkmXbwzhaVg,4007 +mcp/server/fastmcp/resources/templates.py,sha256=dLcTfoEV6nu8CkDq7WsNI1qmwGFan4mMewvYUroLPPA,4947 +mcp/server/fastmcp/resources/types.py,sha256=g6bfXFNGGfoOGiq37TMvG87kxXrgjSgV6UzLS4M6Lz4,7214 +mcp/server/fastmcp/tools/__init__.py,sha256=ZboxhyMJDl87Svjov8YwNYwNZi55P9VhmpTjBZLesnk,96 +mcp/server/fastmcp/tools/base.py,sha256=h2JrpiTTMhj_t1QhQNyIhFPjMTD97-rciQnUe8YAylQ,4740 +mcp/server/fastmcp/tools/tool_manager.py,sha256=rNjJ6_ug6HZEJuYIaN7qqTGgEqiCTha6T0eeSZG9KnY,2972 +mcp/server/fastmcp/utilities/__init__.py,sha256=-imJ8S-rXmbXMWeDamldP-dHDqAPg_wwmPVz-LNX14E,31 +mcp/server/fastmcp/utilities/context_injection.py,sha256=BVm4Svi5RK-nlscwYZT75WuLPccRAJ_INdZxxVSM-lg,2028 +mcp/server/fastmcp/utilities/func_metadata.py,sha256=unwzYeTu8JRVxqYZe-jiCEdtia2KpOnKHWiTEXugBjc,22121 +mcp/server/fastmcp/utilities/logging.py,sha256=e8kBHAYHB00gntMTaWMN6uT4kwiiOdL14qDXEZU0x-k,1063 +mcp/server/fastmcp/utilities/types.py,sha256=xXRshurGRIjVnr1lVPwcZomSSN4_wvxpNb2jS6RUCw4,3546 +mcp/server/lowlevel/__init__.py,sha256=mS7slrVoWzHMYqDHDIJ7oRZxwPZG_I63UVQiJN311YY,93 +mcp/server/lowlevel/experimental.py,sha256=s0BoxBuMKXXmINiShqBOo456PWyKF4Juf9PG8ydE4V0,10545 +mcp/server/lowlevel/func_inspection.py,sha256=uLEpY5IHgWicIfbYbaI4aDYfUgMRxuFdTu20duvH2pw,2466 +mcp/server/lowlevel/helper_types.py,sha256=LVC0N6JMPynVs4fMJSfBmqKIigHNLsRNhBvbU06rg6A,251 +mcp/server/lowlevel/server.py,sha256=kQiQVVOV3oMD3q72SWOwVkQ_DgvdHSL2GlFIHXDDvao,33499 +mcp/shared/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +mcp/shared/_httpx_utils.py,sha256=bfvypq5zsbvL4dgtNfX-lp8R_FqmzDtWEK61K7Y-Sos,2906 +mcp/shared/auth.py,sha256=KndMQ-tXUUGZzXw4fx_2QFLhcGBzQCQi9w8MF6eNSVU,6446 +mcp/shared/auth_utils.py,sha256=urxND9gJd48mHI0XgwjLx5Eq2GlMBrs4y9HebSQtyY4,3006 +mcp/shared/context.py,sha256=QMRHwXCIJsvf4U3vcgqSAXaaUEMKhfj_9XlCI-kUknE,1188 +mcp/shared/exceptions.py,sha256=glHYWFVmdInhHRnlwrpr5w_iALLM0OlNZAM3djNBDXg,2433 +mcp/shared/memory.py,sha256=DtPFbfalB14Nm5hZS5zZLAwrIkqwZFVcBUwFcBAU1TM,3951 +mcp/shared/message.py,sha256=K7wy_fCaPo7p0r8_2Pd223Ab0Q9DMABuCitK3xq0cQI,1550 +mcp/shared/metadata_utils.py,sha256=9Tp9b0PkfjBhm2SlwHnod1W4Bf6vZB0p5J2E7Sv25U4,1712 +mcp/shared/progress.py,sha256=Ys4V_DTQhYsug6KS6Bb3E4T0CDyOOaIecIXAIMFs1tU,1756 +mcp/shared/response_router.py,sha256=en9M764wGHNkWl6dn9Ki7amIRCclHaKsfbnTfpL9KR0,2043 +mcp/shared/session.py,sha256=N5J5m4Y2mnYy1ujy7-yxGLu8fNaau1S7FPawecsJ1_Q,24086 +mcp/shared/tool_name_validation.py,sha256=mTEvgzsMskayyilLaIkP7lIaA4je8cnkj9xYIc6htgo,4531 +mcp/shared/version.py,sha256=GKwM5WHAwd2Dmrq7TyrAHEGWhTO_9UKJJUmC791JcMk,156 +mcp/shared/experimental/__init__.py,sha256=BhhD176Lv_NmLZ3nK6n3zVyCbCxEj5VMwPuqjm3fqcs,209 +mcp/shared/experimental/tasks/__init__.py,sha256=jYCm-mbvvEblzcSWEvQMTlbRD3iUJss-PGF4sMI9OMc,433 +mcp/shared/experimental/tasks/capabilities.py,sha256=wkO_u4698o5yYtV8RPCRj0oaKWfQLMk7l05-ICdjGmo,3571 +mcp/shared/experimental/tasks/context.py,sha256=an3XBnocKUwdlKX8hR71wowzDixBKwsUe9uCBXrsq1w,2875 +mcp/shared/experimental/tasks/helpers.py,sha256=OF7obHXpVoQmrqft-ZcEXOA9bY8xHvnMfd2YGz3xeLo,5517 +mcp/shared/experimental/tasks/in_memory_task_store.py,sha256=cmCPo59XOQPe-kF5uG1Yd-6nHZDaOhrEZJZc35v3I8U,7558 +mcp/shared/experimental/tasks/message_queue.py,sha256=vf5Dmwb-XkpytbK6TixouFWj8ci0wxeN2STpj7IUoiw,7436 +mcp/shared/experimental/tasks/polling.py,sha256=6BCbo7hYGG19bG_RJUYvnYLJ65WHWbWJ9gwri7Aiw80,1369 +mcp/shared/experimental/tasks/resolver.py,sha256=5VvPexlwcL1TZq2j_7fAdMziYlbHDNHk1UW4l1hVH1M,1801 +mcp/shared/experimental/tasks/store.py,sha256=552Kbp7UqLoIruPZnrIkZFu5m0iiQz0quNuHOKI-Yts,3808 +mcp-1.26.0.dist-info/METADATA,sha256=faPMQb0c9UArFqMQZ9WdGFxZdVjfGHy2I71OQ-hBlws,89485 +mcp-1.26.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87 +mcp-1.26.0.dist-info/entry_points.txt,sha256=gnGBbo3yGF0YJNXfZpgkAg1-CQtKxnzxrlA53c8viYA,42 +mcp-1.26.0.dist-info/licenses/LICENSE,sha256=XhPbvB0SD8KgPOzefJFCSuLX3hG2PVje0vRDHiYe5Q0,1071 +mcp-1.26.0.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/mcp-1.26.0.dist-info/WHEEL b/source/target-vendors/cp312/mcp-1.26.0.dist-info/WHEEL new file mode 100644 index 0000000..ae8ec1b --- /dev/null +++ b/source/target-vendors/cp312/mcp-1.26.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.28.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/source/target-vendors/cp312/mcp-1.26.0.dist-info/entry_points.txt b/source/target-vendors/cp312/mcp-1.26.0.dist-info/entry_points.txt new file mode 100644 index 0000000..df7969b --- /dev/null +++ b/source/target-vendors/cp312/mcp-1.26.0.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +mcp = mcp.cli:app [cli] diff --git a/source/target-vendors/cp312/mcp-1.26.0.dist-info/licenses/LICENSE b/source/target-vendors/cp312/mcp-1.26.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000..3d48435 --- /dev/null +++ b/source/target-vendors/cp312/mcp-1.26.0.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Anthropic, PBC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/source/target-vendors/cp312/mcp/__init__.py b/source/target-vendors/cp312/mcp/__init__.py new file mode 100644 index 0000000..fbec40d --- /dev/null +++ b/source/target-vendors/cp312/mcp/__init__.py @@ -0,0 +1,135 @@ +from .client.session import ClientSession +from .client.session_group import ClientSessionGroup +from .client.stdio import StdioServerParameters, stdio_client +from .server.session import ServerSession +from .server.stdio import stdio_server +from .shared.exceptions import McpError, UrlElicitationRequiredError +from .types import ( + CallToolRequest, + ClientCapabilities, + ClientNotification, + ClientRequest, + ClientResult, + CompleteRequest, + CreateMessageRequest, + CreateMessageResult, + CreateMessageResultWithTools, + ErrorData, + GetPromptRequest, + GetPromptResult, + Implementation, + IncludeContext, + InitializedNotification, + InitializeRequest, + InitializeResult, + JSONRPCError, + JSONRPCRequest, + JSONRPCResponse, + ListPromptsRequest, + ListPromptsResult, + ListResourcesRequest, + ListResourcesResult, + ListToolsResult, + LoggingLevel, + LoggingMessageNotification, + Notification, + PingRequest, + ProgressNotification, + PromptsCapability, + ReadResourceRequest, + ReadResourceResult, + Resource, + ResourcesCapability, + ResourceUpdatedNotification, + RootsCapability, + SamplingCapability, + SamplingContent, + SamplingContextCapability, + SamplingMessage, + SamplingMessageContentBlock, + SamplingToolsCapability, + ServerCapabilities, + ServerNotification, + ServerRequest, + ServerResult, + SetLevelRequest, + StopReason, + SubscribeRequest, + Tool, + ToolChoice, + ToolResultContent, + ToolsCapability, + ToolUseContent, + UnsubscribeRequest, +) +from .types import ( + Role as SamplingRole, +) + +__all__ = [ + "CallToolRequest", + "ClientCapabilities", + "ClientNotification", + "ClientRequest", + "ClientResult", + "ClientSession", + "ClientSessionGroup", + "CompleteRequest", + "CreateMessageRequest", + "CreateMessageResult", + "CreateMessageResultWithTools", + "ErrorData", + "GetPromptRequest", + "GetPromptResult", + "Implementation", + "IncludeContext", + "InitializeRequest", + "InitializeResult", + "InitializedNotification", + "JSONRPCError", + "JSONRPCRequest", + "JSONRPCResponse", + "ListPromptsRequest", + "ListPromptsResult", + "ListResourcesRequest", + "ListResourcesResult", + "ListToolsResult", + "LoggingLevel", + "LoggingMessageNotification", + "McpError", + "Notification", + "PingRequest", + "ProgressNotification", + "PromptsCapability", + "ReadResourceRequest", + "ReadResourceResult", + "Resource", + "ResourcesCapability", + "ResourceUpdatedNotification", + "RootsCapability", + "SamplingCapability", + "SamplingContent", + "SamplingContextCapability", + "SamplingMessage", + "SamplingMessageContentBlock", + "SamplingRole", + "SamplingToolsCapability", + "ServerCapabilities", + "ServerNotification", + "ServerRequest", + "ServerResult", + "ServerSession", + "SetLevelRequest", + "StdioServerParameters", + "StopReason", + "SubscribeRequest", + "Tool", + "ToolChoice", + "ToolResultContent", + "ToolsCapability", + "ToolUseContent", + "UnsubscribeRequest", + "UrlElicitationRequiredError", + "stdio_client", + "stdio_server", +] diff --git a/source/target-vendors/cp312/mcp/cli/__init__.py b/source/target-vendors/cp312/mcp/cli/__init__.py new file mode 100644 index 0000000..b29bce8 --- /dev/null +++ b/source/target-vendors/cp312/mcp/cli/__init__.py @@ -0,0 +1,6 @@ +"""FastMCP CLI package.""" + +from .cli import app + +if __name__ == "__main__": # pragma: no cover + app() diff --git a/source/target-vendors/cp312/mcp/cli/claude.py b/source/target-vendors/cp312/mcp/cli/claude.py new file mode 100644 index 0000000..f2dc688 --- /dev/null +++ b/source/target-vendors/cp312/mcp/cli/claude.py @@ -0,0 +1,148 @@ +"""Claude app integration utilities.""" + +import json +import os +import shutil +import sys +from pathlib import Path +from typing import Any + +from mcp.server.fastmcp.utilities.logging import get_logger + +logger = get_logger(__name__) + +MCP_PACKAGE = "mcp[cli]" + + +def get_claude_config_path() -> Path | None: # pragma: no cover + """Get the Claude config directory based on platform.""" + if sys.platform == "win32": + path = Path(Path.home(), "AppData", "Roaming", "Claude") + elif sys.platform == "darwin": + path = Path(Path.home(), "Library", "Application Support", "Claude") + elif sys.platform.startswith("linux"): + path = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"), "Claude") + else: + return None + + if path.exists(): + return path + return None + + +def get_uv_path() -> str: + """Get the full path to the uv executable.""" + uv_path = shutil.which("uv") + if not uv_path: # pragma: no cover + logger.error( + "uv executable not found in PATH, falling back to 'uv'. Please ensure uv is installed and in your PATH" + ) + return "uv" # Fall back to just "uv" if not found + return uv_path + + +def update_claude_config( + file_spec: str, + server_name: str, + *, + with_editable: Path | None = None, + with_packages: list[str] | None = None, + env_vars: dict[str, str] | None = None, +) -> bool: + """Add or update a FastMCP server in Claude's configuration. + + Args: + file_spec: Path to the server file, optionally with :object suffix + server_name: Name for the server in Claude's config + with_editable: Optional directory to install in editable mode + with_packages: Optional list of additional packages to install + env_vars: Optional dictionary of environment variables. These are merged with + any existing variables, with new values taking precedence. + + Raises: + RuntimeError: If Claude Desktop's config directory is not found, indicating + Claude Desktop may not be installed or properly set up. + """ + config_dir = get_claude_config_path() + uv_path = get_uv_path() + if not config_dir: # pragma: no cover + raise RuntimeError( + "Claude Desktop config directory not found. Please ensure Claude Desktop" + " is installed and has been run at least once to initialize its config." + ) + + config_file = config_dir / "claude_desktop_config.json" + if not config_file.exists(): # pragma: no cover + try: + config_file.write_text("{}") + except Exception: + logger.exception( + "Failed to create Claude config file", + extra={ + "config_file": str(config_file), + }, + ) + return False + + try: + config = json.loads(config_file.read_text()) + if "mcpServers" not in config: + config["mcpServers"] = {} + + # Always preserve existing env vars and merge with new ones + if server_name in config["mcpServers"] and "env" in config["mcpServers"][server_name]: # pragma: no cover + existing_env = config["mcpServers"][server_name]["env"] + if env_vars: + # New vars take precedence over existing ones + env_vars = {**existing_env, **env_vars} + else: + env_vars = existing_env + + # Build uv run command + args = ["run", "--frozen"] + + # Collect all packages in a set to deduplicate + packages = {MCP_PACKAGE} + if with_packages: # pragma: no cover + packages.update(pkg for pkg in with_packages if pkg) + + # Add all packages with --with + for pkg in sorted(packages): + args.extend(["--with", pkg]) + + if with_editable: # pragma: no cover + args.extend(["--with-editable", str(with_editable)]) + + # Convert file path to absolute before adding to command + # Split off any :object suffix first + if ":" in file_spec: + file_path, server_object = file_spec.rsplit(":", 1) + file_spec = f"{Path(file_path).resolve()}:{server_object}" + else: # pragma: no cover + file_spec = str(Path(file_spec).resolve()) + + # Add fastmcp run command + args.extend(["mcp", "run", file_spec]) + + server_config: dict[str, Any] = {"command": uv_path, "args": args} + + # Add environment variables if specified + if env_vars: # pragma: no cover + server_config["env"] = env_vars + + config["mcpServers"][server_name] = server_config + + config_file.write_text(json.dumps(config, indent=2)) + logger.info( + f"Added server '{server_name}' to Claude config", + extra={"config_file": str(config_file)}, + ) + return True + except Exception: # pragma: no cover + logger.exception( + "Failed to update Claude config", + extra={ + "config_file": str(config_file), + }, + ) + return False diff --git a/source/target-vendors/cp312/mcp/cli/cli.py b/source/target-vendors/cp312/mcp/cli/cli.py new file mode 100644 index 0000000..c4cae0d --- /dev/null +++ b/source/target-vendors/cp312/mcp/cli/cli.py @@ -0,0 +1,488 @@ +"""MCP CLI tools.""" + +import importlib.metadata +import importlib.util +import os +import subprocess +import sys +from pathlib import Path +from typing import Annotated, Any + +from mcp.server import FastMCP +from mcp.server import Server as LowLevelServer + +try: + import typer +except ImportError: # pragma: no cover + print("Error: typer is required. Install with 'pip install mcp[cli]'") + sys.exit(1) + +try: + from mcp.cli import claude + from mcp.server.fastmcp.utilities.logging import get_logger +except ImportError: # pragma: no cover + print("Error: mcp.server.fastmcp is not installed or not in PYTHONPATH") + sys.exit(1) + +try: + import dotenv +except ImportError: # pragma: no cover + dotenv = None + +logger = get_logger("cli") + +app = typer.Typer( + name="mcp", + help="MCP development tools", + add_completion=False, + no_args_is_help=True, # Show help if no args provided +) + + +def _get_npx_command(): + """Get the correct npx command for the current platform.""" + if sys.platform == "win32": + # Try both npx.cmd and npx.exe on Windows + for cmd in ["npx.cmd", "npx.exe", "npx"]: + try: + subprocess.run([cmd, "--version"], check=True, capture_output=True, shell=True) + return cmd + except subprocess.CalledProcessError: + continue + return None + return "npx" # On Unix-like systems, just use npx + + +def _parse_env_var(env_var: str) -> tuple[str, str]: # pragma: no cover + """Parse environment variable string in format KEY=VALUE.""" + if "=" not in env_var: + logger.error(f"Invalid environment variable format: {env_var}. Must be KEY=VALUE") + sys.exit(1) + key, value = env_var.split("=", 1) + return key.strip(), value.strip() + + +def _build_uv_command( + file_spec: str, + with_editable: Path | None = None, + with_packages: list[str] | None = None, +) -> list[str]: + """Build the uv run command that runs an MCP server through mcp run.""" + cmd = ["uv"] + + cmd.extend(["run", "--with", "mcp"]) + + if with_editable: + cmd.extend(["--with-editable", str(with_editable)]) + + if with_packages: + for pkg in with_packages: + if pkg: # pragma: no cover + cmd.extend(["--with", pkg]) + + # Add mcp run command + cmd.extend(["mcp", "run", file_spec]) + return cmd + + +def _parse_file_path(file_spec: str) -> tuple[Path, str | None]: + """Parse a file path that may include a server object specification. + + Args: + file_spec: Path to file, optionally with :object suffix + + Returns: + Tuple of (file_path, server_object) + """ + # First check if we have a Windows path (e.g., C:\...) + has_windows_drive = len(file_spec) > 1 and file_spec[1] == ":" + + # Split on the last colon, but only if it's not part of the Windows drive letter + # and there's actually another colon in the string after the drive letter + if ":" in (file_spec[2:] if has_windows_drive else file_spec): + file_str, server_object = file_spec.rsplit(":", 1) + else: + file_str, server_object = file_spec, None + + # Resolve the file path + file_path = Path(file_str).expanduser().resolve() + if not file_path.exists(): + logger.error(f"File not found: {file_path}") + sys.exit(1) + if not file_path.is_file(): + logger.error(f"Not a file: {file_path}") + sys.exit(1) + + return file_path, server_object + + +def _import_server(file: Path, server_object: str | None = None): # pragma: no cover + """Import an MCP server from a file. + + Args: + file: Path to the file + server_object: Optional object name in format "module:object" or just "object" + + Returns: + The server object + """ + # Add parent directory to Python path so imports can be resolved + file_dir = str(file.parent) + if file_dir not in sys.path: + sys.path.insert(0, file_dir) + + # Import the module + spec = importlib.util.spec_from_file_location("server_module", file) + if not spec or not spec.loader: + logger.error("Could not load module", extra={"file": str(file)}) + sys.exit(1) + + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + def _check_server_object(server_object: Any, object_name: str): + """Helper function to check that the server object is supported + + Args: + server_object: The server object to check. + + Returns: + True if it's supported. + """ + if not isinstance(server_object, FastMCP): + logger.error(f"The server object {object_name} is of type {type(server_object)} (expecting {FastMCP}).") + if isinstance(server_object, LowLevelServer): + logger.warning( + "Note that only FastMCP server is supported. Low level Server class is not yet supported." + ) + return False + return True + + # If no object specified, try common server names + if not server_object: + # Look for the most common server object names + for name in ["mcp", "server", "app"]: + if hasattr(module, name): + if not _check_server_object(getattr(module, name), f"{file}:{name}"): + logger.error(f"Ignoring object '{file}:{name}' as it's not a valid server object") + continue + return getattr(module, name) + + logger.error( + f"No server object found in {file}. Please either:\n" + "1. Use a standard variable name (mcp, server, or app)\n" + "2. Specify the object name with file:object syntax" + "3. If the server creates the FastMCP object within main() " + " or another function, refactor the FastMCP object to be a " + " global variable named mcp, server, or app.", + extra={"file": str(file)}, + ) + sys.exit(1) + + # Handle module:object syntax + if ":" in server_object: + module_name, object_name = server_object.split(":", 1) + try: + server_module = importlib.import_module(module_name) + server = getattr(server_module, object_name, None) + except ImportError: + logger.error( + f"Could not import module '{module_name}'", + extra={"file": str(file)}, + ) + sys.exit(1) + else: + # Just object name + server = getattr(module, server_object, None) + + if server is None: + logger.error( + f"Server object '{server_object}' not found", + extra={"file": str(file)}, + ) + sys.exit(1) + + if not _check_server_object(server, server_object): + sys.exit(1) + + return server + + +@app.command() +def version() -> None: # pragma: no cover + """Show the MCP version.""" + try: + version = importlib.metadata.version("mcp") + print(f"MCP version {version}") + except importlib.metadata.PackageNotFoundError: + print("MCP version unknown (package not installed)") + sys.exit(1) + + +@app.command() +def dev( + file_spec: str = typer.Argument( + ..., + help="Python file to run, optionally with :object suffix", + ), + with_editable: Annotated[ + Path | None, + typer.Option( + "--with-editable", + "-e", + help="Directory containing pyproject.toml to install in editable mode", + exists=True, + file_okay=False, + resolve_path=True, + ), + ] = None, + with_packages: Annotated[ + list[str], + typer.Option( + "--with", + help="Additional packages to install", + ), + ] = [], +) -> None: # pragma: no cover + """Run an MCP server with the MCP Inspector.""" + file, server_object = _parse_file_path(file_spec) + + logger.debug( + "Starting dev server", + extra={ + "file": str(file), + "server_object": server_object, + "with_editable": str(with_editable) if with_editable else None, + "with_packages": with_packages, + }, + ) + + try: + # Import server to get dependencies + server = _import_server(file, server_object) + if hasattr(server, "dependencies"): + with_packages = list(set(with_packages + server.dependencies)) + + uv_cmd = _build_uv_command(file_spec, with_editable, with_packages) + + # Get the correct npx command + npx_cmd = _get_npx_command() + if not npx_cmd: + logger.error( + "npx not found. Please ensure Node.js and npm are properly installed and added to your system PATH." + ) + sys.exit(1) + + # Run the MCP Inspector command with shell=True on Windows + shell = sys.platform == "win32" + process = subprocess.run( + [npx_cmd, "@modelcontextprotocol/inspector"] + uv_cmd, + check=True, + shell=shell, + env=dict(os.environ.items()), # Convert to list of tuples for env update + ) + sys.exit(process.returncode) + except subprocess.CalledProcessError as e: + logger.error( + "Dev server failed", + extra={ + "file": str(file), + "error": str(e), + "returncode": e.returncode, + }, + ) + sys.exit(e.returncode) + except FileNotFoundError: + logger.error( + "npx not found. Please ensure Node.js and npm are properly installed " + "and added to your system PATH. You may need to restart your terminal " + "after installation.", + extra={"file": str(file)}, + ) + sys.exit(1) + + +@app.command() +def run( + file_spec: str = typer.Argument( + ..., + help="Python file to run, optionally with :object suffix", + ), + transport: Annotated[ + str | None, + typer.Option( + "--transport", + "-t", + help="Transport protocol to use (stdio or sse)", + ), + ] = None, +) -> None: # pragma: no cover + """Run an MCP server. + + The server can be specified in two ways:\n + 1. Module approach: server.py - runs the module directly, expecting a server.run() call.\n + 2. Import approach: server.py:app - imports and runs the specified server object.\n\n + + Note: This command runs the server directly. You are responsible for ensuring + all dependencies are available.\n + For dependency management, use `mcp install` or `mcp dev` instead. + """ # noqa: E501 + file, server_object = _parse_file_path(file_spec) + + logger.debug( + "Running server", + extra={ + "file": str(file), + "server_object": server_object, + "transport": transport, + }, + ) + + try: + # Import and get server object + server = _import_server(file, server_object) + + # Run the server + kwargs = {} + if transport: + kwargs["transport"] = transport + + server.run(**kwargs) + + except Exception: + logger.exception( + "Failed to run server", + extra={ + "file": str(file), + }, + ) + sys.exit(1) + + +@app.command() +def install( + file_spec: str = typer.Argument( + ..., + help="Python file to run, optionally with :object suffix", + ), + server_name: Annotated[ + str | None, + typer.Option( + "--name", + "-n", + help="Custom name for the server (defaults to server's name attribute or file name)", + ), + ] = None, + with_editable: Annotated[ + Path | None, + typer.Option( + "--with-editable", + "-e", + help="Directory containing pyproject.toml to install in editable mode", + exists=True, + file_okay=False, + resolve_path=True, + ), + ] = None, + with_packages: Annotated[ + list[str], + typer.Option( + "--with", + help="Additional packages to install", + ), + ] = [], + env_vars: Annotated[ + list[str], + typer.Option( + "--env-var", + "-v", + help="Environment variables in KEY=VALUE format", + ), + ] = [], + env_file: Annotated[ + Path | None, + typer.Option( + "--env-file", + "-f", + help="Load environment variables from a .env file", + exists=True, + file_okay=True, + dir_okay=False, + resolve_path=True, + ), + ] = None, +) -> None: # pragma: no cover + """Install an MCP server in the Claude desktop app. + + Environment variables are preserved once added and only updated if new values + are explicitly provided. + """ + file, server_object = _parse_file_path(file_spec) + + logger.debug( + "Installing server", + extra={ + "file": str(file), + "server_name": server_name, + "server_object": server_object, + "with_editable": str(with_editable) if with_editable else None, + "with_packages": with_packages, + }, + ) + + if not claude.get_claude_config_path(): + logger.error("Claude app not found") + sys.exit(1) + + # Try to import server to get its name, but fall back to file name if dependencies + # missing + name = server_name + server = None + if not name: + try: + server = _import_server(file, server_object) + name = server.name + except (ImportError, ModuleNotFoundError) as e: + logger.debug( + "Could not import server (likely missing dependencies), using file name", + extra={"error": str(e)}, + ) + name = file.stem + + # Get server dependencies if available + server_dependencies = getattr(server, "dependencies", []) if server else [] + if server_dependencies: + with_packages = list(set(with_packages + server_dependencies)) + + # Process environment variables if provided + env_dict: dict[str, str] | None = None + if env_file or env_vars: + env_dict = {} + # Load from .env file if specified + if env_file: + if dotenv: + try: + env_dict |= {k: v for k, v in dotenv.dotenv_values(env_file).items() if v is not None} + except (OSError, ValueError): + logger.exception("Failed to load .env file") + sys.exit(1) + else: + logger.error("python-dotenv is not installed. Cannot load .env file.") + sys.exit(1) + + # Add command line environment variables + for env_var in env_vars: + key, value = _parse_env_var(env_var) + env_dict[key] = value + + if claude.update_claude_config( + file_spec, + name, + with_editable=with_editable, + with_packages=with_packages, + env_vars=env_dict, + ): + logger.info(f"Successfully installed {name} in Claude app") + else: + logger.error(f"Failed to install {name} in Claude app") + sys.exit(1) diff --git a/source/target-vendors/cp312/mcp/client/__init__.py b/source/target-vendors/cp312/mcp/client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/mcp/client/__main__.py b/source/target-vendors/cp312/mcp/client/__main__.py new file mode 100644 index 0000000..2efe05d --- /dev/null +++ b/source/target-vendors/cp312/mcp/client/__main__.py @@ -0,0 +1,85 @@ +import argparse +import logging +import sys +from functools import partial +from urllib.parse import urlparse + +import anyio +from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream + +import mcp.types as types +from mcp.client.session import ClientSession +from mcp.client.sse import sse_client +from mcp.client.stdio import StdioServerParameters, stdio_client +from mcp.shared.message import SessionMessage +from mcp.shared.session import RequestResponder + +if not sys.warnoptions: + import warnings + + warnings.simplefilter("ignore") + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("client") + + +async def message_handler( + message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception, +) -> None: + if isinstance(message, Exception): + logger.error("Error: %s", message) + return + + logger.info("Received message from server: %s", message) + + +async def run_session( + read_stream: MemoryObjectReceiveStream[SessionMessage | Exception], + write_stream: MemoryObjectSendStream[SessionMessage], + client_info: types.Implementation | None = None, +): + async with ClientSession( + read_stream, + write_stream, + message_handler=message_handler, + client_info=client_info, + ) as session: + logger.info("Initializing session") + await session.initialize() + logger.info("Initialized") + + +async def main(command_or_url: str, args: list[str], env: list[tuple[str, str]]): + env_dict = dict(env) + + if urlparse(command_or_url).scheme in ("http", "https"): + # Use SSE client for HTTP(S) URLs + async with sse_client(command_or_url) as streams: + await run_session(*streams) + else: + # Use stdio client for commands + server_parameters = StdioServerParameters(command=command_or_url, args=args, env=env_dict) + async with stdio_client(server_parameters) as streams: + await run_session(*streams) + + +def cli(): + parser = argparse.ArgumentParser() + parser.add_argument("command_or_url", help="Command or URL to connect to") + parser.add_argument("args", nargs="*", help="Additional arguments") + parser.add_argument( + "-e", + "--env", + nargs=2, + action="append", + metavar=("KEY", "VALUE"), + help="Environment variables to set. Can be used multiple times.", + default=[], + ) + + args = parser.parse_args() + anyio.run(partial(main, args.command_or_url, args.args, args.env), backend="trio") + + +if __name__ == "__main__": + cli() diff --git a/source/target-vendors/cp312/mcp/client/auth/__init__.py b/source/target-vendors/cp312/mcp/client/auth/__init__.py new file mode 100644 index 0000000..252dfd9 --- /dev/null +++ b/source/target-vendors/cp312/mcp/client/auth/__init__.py @@ -0,0 +1,21 @@ +""" +OAuth2 Authentication implementation for HTTPX. + +Implements authorization code flow with PKCE and automatic token refresh. +""" + +from mcp.client.auth.exceptions import OAuthFlowError, OAuthRegistrationError, OAuthTokenError +from mcp.client.auth.oauth2 import ( + OAuthClientProvider, + PKCEParameters, + TokenStorage, +) + +__all__ = [ + "OAuthClientProvider", + "OAuthFlowError", + "OAuthRegistrationError", + "OAuthTokenError", + "PKCEParameters", + "TokenStorage", +] diff --git a/source/target-vendors/cp312/mcp/client/auth/exceptions.py b/source/target-vendors/cp312/mcp/client/auth/exceptions.py new file mode 100644 index 0000000..5ce8777 --- /dev/null +++ b/source/target-vendors/cp312/mcp/client/auth/exceptions.py @@ -0,0 +1,10 @@ +class OAuthFlowError(Exception): + """Base exception for OAuth flow errors.""" + + +class OAuthTokenError(OAuthFlowError): + """Raised when token operations fail.""" + + +class OAuthRegistrationError(OAuthFlowError): + """Raised when client registration fails.""" diff --git a/source/target-vendors/cp312/mcp/client/auth/extensions/__init__.py b/source/target-vendors/cp312/mcp/client/auth/extensions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/mcp/client/auth/extensions/client_credentials.py b/source/target-vendors/cp312/mcp/client/auth/extensions/client_credentials.py new file mode 100644 index 0000000..e2f3f08 --- /dev/null +++ b/source/target-vendors/cp312/mcp/client/auth/extensions/client_credentials.py @@ -0,0 +1,487 @@ +""" +OAuth client credential extensions for MCP. + +Provides OAuth providers for machine-to-machine authentication flows: +- ClientCredentialsOAuthProvider: For client_credentials with client_id + client_secret +- PrivateKeyJWTOAuthProvider: For client_credentials with private_key_jwt authentication + (typically using a pre-built JWT from workload identity federation) +- RFC7523OAuthClientProvider: For jwt-bearer grant (RFC 7523 Section 2.1) +""" + +import time +from collections.abc import Awaitable, Callable +from typing import Any, Literal +from uuid import uuid4 + +import httpx +import jwt +from pydantic import BaseModel, Field + +from mcp.client.auth import OAuthClientProvider, OAuthFlowError, OAuthTokenError, TokenStorage +from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata + + +class ClientCredentialsOAuthProvider(OAuthClientProvider): + """OAuth provider for client_credentials grant with client_id + client_secret. + + This provider sets client_info directly, bypassing dynamic client registration. + Use this when you already have client credentials (client_id and client_secret). + + Example: + ```python + provider = ClientCredentialsOAuthProvider( + server_url="https://api.example.com", + storage=my_token_storage, + client_id="my-client-id", + client_secret="my-client-secret", + ) + ``` + """ + + def __init__( + self, + server_url: str, + storage: TokenStorage, + client_id: str, + client_secret: str, + token_endpoint_auth_method: Literal["client_secret_basic", "client_secret_post"] = "client_secret_basic", + scopes: str | None = None, + ) -> None: + """Initialize client_credentials OAuth provider. + + Args: + server_url: The MCP server URL. + storage: Token storage implementation. + client_id: The OAuth client ID. + client_secret: The OAuth client secret. + token_endpoint_auth_method: Authentication method for token endpoint. + Either "client_secret_basic" (default) or "client_secret_post". + scopes: Optional space-separated list of scopes to request. + """ + # Build minimal client_metadata for the base class + client_metadata = OAuthClientMetadata( + redirect_uris=None, + grant_types=["client_credentials"], + token_endpoint_auth_method=token_endpoint_auth_method, + scope=scopes, + ) + super().__init__(server_url, client_metadata, storage, None, None, 300.0) + # Store client_info to be set during _initialize - no dynamic registration needed + self._fixed_client_info = OAuthClientInformationFull( + redirect_uris=None, + client_id=client_id, + client_secret=client_secret, + grant_types=["client_credentials"], + token_endpoint_auth_method=token_endpoint_auth_method, + scope=scopes, + ) + + async def _initialize(self) -> None: + """Load stored tokens and set pre-configured client_info.""" + self.context.current_tokens = await self.context.storage.get_tokens() + self.context.client_info = self._fixed_client_info + self._initialized = True + + async def _perform_authorization(self) -> httpx.Request: + """Perform client_credentials authorization.""" + return await self._exchange_token_client_credentials() + + async def _exchange_token_client_credentials(self) -> httpx.Request: + """Build token exchange request for client_credentials grant.""" + token_data: dict[str, Any] = { + "grant_type": "client_credentials", + } + + headers: dict[str, str] = {"Content-Type": "application/x-www-form-urlencoded"} + + # Use standard auth methods (client_secret_basic, client_secret_post, none) + token_data, headers = self.context.prepare_token_auth(token_data, headers) + + if self.context.should_include_resource_param(self.context.protocol_version): + token_data["resource"] = self.context.get_resource_url() + + if self.context.client_metadata.scope: + token_data["scope"] = self.context.client_metadata.scope + + token_url = self._get_token_endpoint() + return httpx.Request("POST", token_url, data=token_data, headers=headers) + + +def static_assertion_provider(token: str) -> Callable[[str], Awaitable[str]]: + """Create an assertion provider that returns a static JWT token. + + Use this when you have a pre-built JWT (e.g., from workload identity federation) + that doesn't need the audience parameter. + + Example: + ```python + provider = PrivateKeyJWTOAuthProvider( + server_url="https://api.example.com", + storage=my_token_storage, + client_id="my-client-id", + assertion_provider=static_assertion_provider(my_prebuilt_jwt), + ) + ``` + + Args: + token: The pre-built JWT assertion string. + + Returns: + An async callback suitable for use as an assertion_provider. + """ + + async def provider(audience: str) -> str: + return token + + return provider + + +class SignedJWTParameters(BaseModel): + """Parameters for creating SDK-signed JWT assertions. + + Use `create_assertion_provider()` to create an assertion provider callback + for use with `PrivateKeyJWTOAuthProvider`. + + Example: + ```python + jwt_params = SignedJWTParameters( + issuer="my-client-id", + subject="my-client-id", + signing_key=private_key_pem, + ) + provider = PrivateKeyJWTOAuthProvider( + server_url="https://api.example.com", + storage=my_token_storage, + client_id="my-client-id", + assertion_provider=jwt_params.create_assertion_provider(), + ) + ``` + """ + + issuer: str = Field(description="Issuer for JWT assertions (typically client_id).") + subject: str = Field(description="Subject identifier for JWT assertions (typically client_id).") + signing_key: str = Field(description="Private key for JWT signing (PEM format).") + signing_algorithm: str = Field(default="RS256", description="Algorithm for signing JWT assertions.") + lifetime_seconds: int = Field(default=300, description="Lifetime of generated JWT in seconds.") + additional_claims: dict[str, Any] | None = Field(default=None, description="Additional claims.") + + def create_assertion_provider(self) -> Callable[[str], Awaitable[str]]: + """Create an assertion provider callback for use with PrivateKeyJWTOAuthProvider. + + Returns: + An async callback that takes the audience (authorization server issuer URL) + and returns a signed JWT assertion. + """ + + async def provider(audience: str) -> str: + now = int(time.time()) + claims: dict[str, Any] = { + "iss": self.issuer, + "sub": self.subject, + "aud": audience, + "exp": now + self.lifetime_seconds, + "iat": now, + "jti": str(uuid4()), + } + if self.additional_claims: + claims.update(self.additional_claims) + + return jwt.encode(claims, self.signing_key, algorithm=self.signing_algorithm) + + return provider + + +class PrivateKeyJWTOAuthProvider(OAuthClientProvider): + """OAuth provider for client_credentials grant with private_key_jwt authentication. + + Uses RFC 7523 Section 2.2 for client authentication via JWT assertion. + + The JWT assertion's audience MUST be the authorization server's issuer identifier + (per RFC 7523bis security updates). The `assertion_provider` callback receives + this audience value and must return a JWT with that audience. + + **Option 1: Pre-built JWT via Workload Identity Federation** + + In production scenarios, the JWT assertion is typically obtained from a workload + identity provider (e.g., GCP, AWS IAM, Azure AD): + + ```python + async def get_workload_identity_token(audience: str) -> str: + # Fetch JWT from your identity provider + # The JWT's audience must match the provided audience parameter + return await fetch_token_from_identity_provider(audience=audience) + + provider = PrivateKeyJWTOAuthProvider( + server_url="https://api.example.com", + storage=my_token_storage, + client_id="my-client-id", + assertion_provider=get_workload_identity_token, + ) + ``` + + **Option 2: Static pre-built JWT** + + If you have a static JWT that doesn't need the audience parameter: + + ```python + provider = PrivateKeyJWTOAuthProvider( + server_url="https://api.example.com", + storage=my_token_storage, + client_id="my-client-id", + assertion_provider=static_assertion_provider(my_prebuilt_jwt), + ) + ``` + + **Option 3: SDK-signed JWT (for testing/simple setups)** + + For testing or simple deployments, use `SignedJWTParameters.create_assertion_provider()`: + + ```python + jwt_params = SignedJWTParameters( + issuer="my-client-id", + subject="my-client-id", + signing_key=private_key_pem, + ) + provider = PrivateKeyJWTOAuthProvider( + server_url="https://api.example.com", + storage=my_token_storage, + client_id="my-client-id", + assertion_provider=jwt_params.create_assertion_provider(), + ) + ``` + """ + + def __init__( + self, + server_url: str, + storage: TokenStorage, + client_id: str, + assertion_provider: Callable[[str], Awaitable[str]], + scopes: str | None = None, + ) -> None: + """Initialize private_key_jwt OAuth provider. + + Args: + server_url: The MCP server URL. + storage: Token storage implementation. + client_id: The OAuth client ID. + assertion_provider: Async callback that takes the audience (authorization + server's issuer identifier) and returns a JWT assertion. Use + `SignedJWTParameters.create_assertion_provider()` for SDK-signed JWTs, + `static_assertion_provider()` for pre-built JWTs, or provide your own + callback for workload identity federation. + scopes: Optional space-separated list of scopes to request. + """ + # Build minimal client_metadata for the base class + client_metadata = OAuthClientMetadata( + redirect_uris=None, + grant_types=["client_credentials"], + token_endpoint_auth_method="private_key_jwt", + scope=scopes, + ) + super().__init__(server_url, client_metadata, storage, None, None, 300.0) + self._assertion_provider = assertion_provider + # Store client_info to be set during _initialize - no dynamic registration needed + self._fixed_client_info = OAuthClientInformationFull( + redirect_uris=None, + client_id=client_id, + grant_types=["client_credentials"], + token_endpoint_auth_method="private_key_jwt", + scope=scopes, + ) + + async def _initialize(self) -> None: + """Load stored tokens and set pre-configured client_info.""" + self.context.current_tokens = await self.context.storage.get_tokens() + self.context.client_info = self._fixed_client_info + self._initialized = True + + async def _perform_authorization(self) -> httpx.Request: + """Perform client_credentials authorization with private_key_jwt.""" + return await self._exchange_token_client_credentials() + + async def _add_client_authentication_jwt(self, *, token_data: dict[str, Any]) -> None: + """Add JWT assertion for client authentication to token endpoint parameters.""" + if not self.context.oauth_metadata: + raise OAuthFlowError("Missing OAuth metadata for private_key_jwt flow") # pragma: no cover + + # Audience MUST be the issuer identifier of the authorization server + # https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rfc7523bis-01 + audience = str(self.context.oauth_metadata.issuer) + assertion = await self._assertion_provider(audience) + + # RFC 7523 Section 2.2: client authentication via JWT + token_data["client_assertion"] = assertion + token_data["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + + async def _exchange_token_client_credentials(self) -> httpx.Request: + """Build token exchange request for client_credentials grant with private_key_jwt.""" + token_data: dict[str, Any] = { + "grant_type": "client_credentials", + } + + headers: dict[str, str] = {"Content-Type": "application/x-www-form-urlencoded"} + + # Add JWT client authentication (RFC 7523 Section 2.2) + await self._add_client_authentication_jwt(token_data=token_data) + + if self.context.should_include_resource_param(self.context.protocol_version): + token_data["resource"] = self.context.get_resource_url() + + if self.context.client_metadata.scope: + token_data["scope"] = self.context.client_metadata.scope + + token_url = self._get_token_endpoint() + return httpx.Request("POST", token_url, data=token_data, headers=headers) + + +class JWTParameters(BaseModel): + """JWT parameters.""" + + assertion: str | None = Field( + default=None, + description="JWT assertion for JWT authentication. " + "Will be used instead of generating a new assertion if provided.", + ) + + issuer: str | None = Field(default=None, description="Issuer for JWT assertions.") + subject: str | None = Field(default=None, description="Subject identifier for JWT assertions.") + audience: str | None = Field(default=None, description="Audience for JWT assertions.") + claims: dict[str, Any] | None = Field(default=None, description="Additional claims for JWT assertions.") + jwt_signing_algorithm: str | None = Field(default="RS256", description="Algorithm for signing JWT assertions.") + jwt_signing_key: str | None = Field(default=None, description="Private key for JWT signing.") + jwt_lifetime_seconds: int = Field(default=300, description="Lifetime of generated JWT in seconds.") + + def to_assertion(self, with_audience_fallback: str | None = None) -> str: + if self.assertion is not None: + # Prebuilt JWT (e.g. acquired out-of-band) + assertion = self.assertion + else: + if not self.jwt_signing_key: + raise OAuthFlowError("Missing signing key for JWT bearer grant") # pragma: no cover + if not self.issuer: + raise OAuthFlowError("Missing issuer for JWT bearer grant") # pragma: no cover + if not self.subject: + raise OAuthFlowError("Missing subject for JWT bearer grant") # pragma: no cover + + audience = self.audience if self.audience else with_audience_fallback + if not audience: + raise OAuthFlowError("Missing audience for JWT bearer grant") # pragma: no cover + + now = int(time.time()) + claims: dict[str, Any] = { + "iss": self.issuer, + "sub": self.subject, + "aud": audience, + "exp": now + self.jwt_lifetime_seconds, + "iat": now, + "jti": str(uuid4()), + } + claims.update(self.claims or {}) + + assertion = jwt.encode( + claims, + self.jwt_signing_key, + algorithm=self.jwt_signing_algorithm or "RS256", + ) + return assertion + + +class RFC7523OAuthClientProvider(OAuthClientProvider): + """OAuth client provider for RFC 7523 jwt-bearer grant. + + .. deprecated:: + Use :class:`ClientCredentialsOAuthProvider` for client_credentials with + client_id + client_secret, or :class:`PrivateKeyJWTOAuthProvider` for + client_credentials with private_key_jwt authentication instead. + + This provider supports the jwt-bearer authorization grant (RFC 7523 Section 2.1) + where the JWT itself is the authorization grant. + """ + + def __init__( + self, + server_url: str, + client_metadata: OAuthClientMetadata, + storage: TokenStorage, + redirect_handler: Callable[[str], Awaitable[None]] | None = None, + callback_handler: Callable[[], Awaitable[tuple[str, str | None]]] | None = None, + timeout: float = 300.0, + jwt_parameters: JWTParameters | None = None, + ) -> None: + import warnings + + warnings.warn( + "RFC7523OAuthClientProvider is deprecated. Use ClientCredentialsOAuthProvider " + "or PrivateKeyJWTOAuthProvider instead.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(server_url, client_metadata, storage, redirect_handler, callback_handler, timeout) + self.jwt_parameters = jwt_parameters + + async def _exchange_token_authorization_code( + self, auth_code: str, code_verifier: str, *, token_data: dict[str, Any] | None = None + ) -> httpx.Request: # pragma: no cover + """Build token exchange request for authorization_code flow.""" + token_data = token_data or {} + if self.context.client_metadata.token_endpoint_auth_method == "private_key_jwt": + self._add_client_authentication_jwt(token_data=token_data) + return await super()._exchange_token_authorization_code(auth_code, code_verifier, token_data=token_data) + + async def _perform_authorization(self) -> httpx.Request: # pragma: no cover + """Perform the authorization flow.""" + if "urn:ietf:params:oauth:grant-type:jwt-bearer" in self.context.client_metadata.grant_types: + token_request = await self._exchange_token_jwt_bearer() + return token_request + else: + return await super()._perform_authorization() + + def _add_client_authentication_jwt(self, *, token_data: dict[str, Any]): # pragma: no cover + """Add JWT assertion for client authentication to token endpoint parameters.""" + if not self.jwt_parameters: + raise OAuthTokenError("Missing JWT parameters for private_key_jwt flow") + if not self.context.oauth_metadata: + raise OAuthTokenError("Missing OAuth metadata for private_key_jwt flow") + + # We need to set the audience to the issuer identifier of the authorization server + # https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rfc7523bis-01#name-updates-to-rfc-7523 + issuer = str(self.context.oauth_metadata.issuer) + assertion = self.jwt_parameters.to_assertion(with_audience_fallback=issuer) + + # When using private_key_jwt, in a client_credentials flow, we use RFC 7523 Section 2.2 + token_data["client_assertion"] = assertion + token_data["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + # We need to set the audience to the resource server, the audience is difference from the one in claims + # it represents the resource server that will validate the token + token_data["audience"] = self.context.get_resource_url() + + async def _exchange_token_jwt_bearer(self) -> httpx.Request: + """Build token exchange request for JWT bearer grant.""" + if not self.context.client_info: + raise OAuthFlowError("Missing client info") # pragma: no cover + if not self.jwt_parameters: + raise OAuthFlowError("Missing JWT parameters") # pragma: no cover + if not self.context.oauth_metadata: + raise OAuthTokenError("Missing OAuth metadata") # pragma: no cover + + # We need to set the audience to the issuer identifier of the authorization server + # https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rfc7523bis-01#name-updates-to-rfc-7523 + issuer = str(self.context.oauth_metadata.issuer) + assertion = self.jwt_parameters.to_assertion(with_audience_fallback=issuer) + + token_data = { + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + } + + if self.context.should_include_resource_param(self.context.protocol_version): # pragma: no branch + token_data["resource"] = self.context.get_resource_url() + + if self.context.client_metadata.scope: # pragma: no branch + token_data["scope"] = self.context.client_metadata.scope + + token_url = self._get_token_endpoint() + return httpx.Request( + "POST", token_url, data=token_data, headers={"Content-Type": "application/x-www-form-urlencoded"} + ) diff --git a/source/target-vendors/cp312/mcp/client/auth/oauth2.py b/source/target-vendors/cp312/mcp/client/auth/oauth2.py new file mode 100644 index 0000000..cd96a75 --- /dev/null +++ b/source/target-vendors/cp312/mcp/client/auth/oauth2.py @@ -0,0 +1,616 @@ +""" +OAuth2 Authentication implementation for HTTPX. + +Implements authorization code flow with PKCE and automatic token refresh. +""" + +import base64 +import hashlib +import logging +import secrets +import string +import time +from collections.abc import AsyncGenerator, Awaitable, Callable +from dataclasses import dataclass, field +from typing import Any, Protocol +from urllib.parse import quote, urlencode, urljoin, urlparse + +import anyio +import httpx +from pydantic import BaseModel, Field, ValidationError + +from mcp.client.auth.exceptions import OAuthFlowError, OAuthTokenError +from mcp.client.auth.utils import ( + build_oauth_authorization_server_metadata_discovery_urls, + build_protected_resource_metadata_discovery_urls, + create_client_info_from_metadata_url, + create_client_registration_request, + create_oauth_metadata_request, + extract_field_from_www_auth, + extract_resource_metadata_from_www_auth, + extract_scope_from_www_auth, + get_client_metadata_scopes, + handle_auth_metadata_response, + handle_protected_resource_response, + handle_registration_response, + handle_token_response_scopes, + is_valid_client_metadata_url, + should_use_client_metadata_url, +) +from mcp.client.streamable_http import MCP_PROTOCOL_VERSION +from mcp.shared.auth import ( + OAuthClientInformationFull, + OAuthClientMetadata, + OAuthMetadata, + OAuthToken, + ProtectedResourceMetadata, +) +from mcp.shared.auth_utils import ( + calculate_token_expiry, + check_resource_allowed, + resource_url_from_server_url, +) + +logger = logging.getLogger(__name__) + + +class PKCEParameters(BaseModel): + """PKCE (Proof Key for Code Exchange) parameters.""" + + code_verifier: str = Field(..., min_length=43, max_length=128) + code_challenge: str = Field(..., min_length=43, max_length=128) + + @classmethod + def generate(cls) -> "PKCEParameters": + """Generate new PKCE parameters.""" + code_verifier = "".join(secrets.choice(string.ascii_letters + string.digits + "-._~") for _ in range(128)) + digest = hashlib.sha256(code_verifier.encode()).digest() + code_challenge = base64.urlsafe_b64encode(digest).decode().rstrip("=") + return cls(code_verifier=code_verifier, code_challenge=code_challenge) + + +class TokenStorage(Protocol): + """Protocol for token storage implementations.""" + + async def get_tokens(self) -> OAuthToken | None: + """Get stored tokens.""" + ... + + async def set_tokens(self, tokens: OAuthToken) -> None: + """Store tokens.""" + ... + + async def get_client_info(self) -> OAuthClientInformationFull | None: + """Get stored client information.""" + ... + + async def set_client_info(self, client_info: OAuthClientInformationFull) -> None: + """Store client information.""" + ... + + +@dataclass +class OAuthContext: + """OAuth flow context.""" + + server_url: str + client_metadata: OAuthClientMetadata + storage: TokenStorage + redirect_handler: Callable[[str], Awaitable[None]] | None + callback_handler: Callable[[], Awaitable[tuple[str, str | None]]] | None + timeout: float = 300.0 + client_metadata_url: str | None = None + + # Discovered metadata + protected_resource_metadata: ProtectedResourceMetadata | None = None + oauth_metadata: OAuthMetadata | None = None + auth_server_url: str | None = None + protocol_version: str | None = None + + # Client registration + client_info: OAuthClientInformationFull | None = None + + # Token management + current_tokens: OAuthToken | None = None + token_expiry_time: float | None = None + + # State + lock: anyio.Lock = field(default_factory=anyio.Lock) + + def get_authorization_base_url(self, server_url: str) -> str: + """Extract base URL by removing path component.""" + parsed = urlparse(server_url) + return f"{parsed.scheme}://{parsed.netloc}" + + def update_token_expiry(self, token: OAuthToken) -> None: + """Update token expiry time using shared util function.""" + self.token_expiry_time = calculate_token_expiry(token.expires_in) + + def is_token_valid(self) -> bool: + """Check if current token is valid.""" + return bool( + self.current_tokens + and self.current_tokens.access_token + and (not self.token_expiry_time or time.time() <= self.token_expiry_time) + ) + + def can_refresh_token(self) -> bool: + """Check if token can be refreshed.""" + return bool(self.current_tokens and self.current_tokens.refresh_token and self.client_info) + + def clear_tokens(self) -> None: + """Clear current tokens.""" + self.current_tokens = None + self.token_expiry_time = None + + def get_resource_url(self) -> str: + """Get resource URL for RFC 8707. + + Uses PRM resource if it's a valid parent, otherwise uses canonical server URL. + """ + resource = resource_url_from_server_url(self.server_url) + + # If PRM provides a resource that's a valid parent, use it + if self.protected_resource_metadata and self.protected_resource_metadata.resource: + prm_resource = str(self.protected_resource_metadata.resource) + if check_resource_allowed(requested_resource=resource, configured_resource=prm_resource): + resource = prm_resource + + return resource + + def should_include_resource_param(self, protocol_version: str | None = None) -> bool: + """Determine if the resource parameter should be included in OAuth requests. + + Returns True if: + - Protected resource metadata is available, OR + - MCP-Protocol-Version header is 2025-06-18 or later + """ + # If we have protected resource metadata, include the resource param + if self.protected_resource_metadata is not None: + return True + + # If no protocol version provided, don't include resource param + if not protocol_version: + return False + + # Check if protocol version is 2025-06-18 or later + # Version format is YYYY-MM-DD, so string comparison works + return protocol_version >= "2025-06-18" + + def prepare_token_auth( + self, data: dict[str, str], headers: dict[str, str] | None = None + ) -> tuple[dict[str, str], dict[str, str]]: + """Prepare authentication for token requests. + + Args: + data: The form data to send + headers: Optional headers dict to update + + Returns: + Tuple of (updated_data, updated_headers) + """ + if headers is None: + headers = {} # pragma: no cover + + if not self.client_info: + return data, headers # pragma: no cover + + auth_method = self.client_info.token_endpoint_auth_method + + if auth_method == "client_secret_basic" and self.client_info.client_id and self.client_info.client_secret: + # URL-encode client ID and secret per RFC 6749 Section 2.3.1 + encoded_id = quote(self.client_info.client_id, safe="") + encoded_secret = quote(self.client_info.client_secret, safe="") + credentials = f"{encoded_id}:{encoded_secret}" + encoded_credentials = base64.b64encode(credentials.encode()).decode() + headers["Authorization"] = f"Basic {encoded_credentials}" + # Don't include client_secret in body for basic auth + data = {k: v for k, v in data.items() if k != "client_secret"} + elif auth_method == "client_secret_post" and self.client_info.client_secret: + # Include client_secret in request body + data["client_secret"] = self.client_info.client_secret + # For auth_method == "none", don't add any client_secret + + return data, headers + + +class OAuthClientProvider(httpx.Auth): + """ + OAuth2 authentication for httpx. + Handles OAuth flow with automatic client registration and token storage. + """ + + requires_response_body = True + + def __init__( + self, + server_url: str, + client_metadata: OAuthClientMetadata, + storage: TokenStorage, + redirect_handler: Callable[[str], Awaitable[None]] | None = None, + callback_handler: Callable[[], Awaitable[tuple[str, str | None]]] | None = None, + timeout: float = 300.0, + client_metadata_url: str | None = None, + ): + """Initialize OAuth2 authentication. + + Args: + server_url: The MCP server URL. + client_metadata: OAuth client metadata for registration. + storage: Token storage implementation. + redirect_handler: Handler for authorization redirects. + callback_handler: Handler for authorization callbacks. + timeout: Timeout for the OAuth flow. + client_metadata_url: URL-based client ID. When provided and the server + advertises client_id_metadata_document_supported=true, this URL will be + used as the client_id instead of performing dynamic client registration. + Must be a valid HTTPS URL with a non-root pathname. + + Raises: + ValueError: If client_metadata_url is provided but not a valid HTTPS URL + with a non-root pathname. + """ + # Validate client_metadata_url if provided + if client_metadata_url is not None and not is_valid_client_metadata_url(client_metadata_url): + raise ValueError( + f"client_metadata_url must be a valid HTTPS URL with a non-root pathname, got: {client_metadata_url}" + ) + + self.context = OAuthContext( + server_url=server_url, + client_metadata=client_metadata, + storage=storage, + redirect_handler=redirect_handler, + callback_handler=callback_handler, + timeout=timeout, + client_metadata_url=client_metadata_url, + ) + self._initialized = False + + async def _handle_protected_resource_response(self, response: httpx.Response) -> bool: + """ + Handle protected resource metadata discovery response. + + Per SEP-985, supports fallback when discovery fails at one URL. + + Returns: + True if metadata was successfully discovered, False if we should try next URL + """ + if response.status_code == 200: + try: + content = await response.aread() + metadata = ProtectedResourceMetadata.model_validate_json(content) + self.context.protected_resource_metadata = metadata + if metadata.authorization_servers: # pragma: no branch + self.context.auth_server_url = str(metadata.authorization_servers[0]) + return True + + except ValidationError: # pragma: no cover + # Invalid metadata - try next URL + logger.warning(f"Invalid protected resource metadata at {response.request.url}") + return False + elif response.status_code == 404: # pragma: no cover + # Not found - try next URL in fallback chain + logger.debug(f"Protected resource metadata not found at {response.request.url}, trying next URL") + return False + else: + # Other error - fail immediately + raise OAuthFlowError( + f"Protected Resource Metadata request failed: {response.status_code}" + ) # pragma: no cover + + async def _perform_authorization(self) -> httpx.Request: + """Perform the authorization flow.""" + auth_code, code_verifier = await self._perform_authorization_code_grant() + token_request = await self._exchange_token_authorization_code(auth_code, code_verifier) + return token_request + + async def _perform_authorization_code_grant(self) -> tuple[str, str]: + """Perform the authorization redirect and get auth code.""" + if self.context.client_metadata.redirect_uris is None: + raise OAuthFlowError("No redirect URIs provided for authorization code grant") # pragma: no cover + if not self.context.redirect_handler: + raise OAuthFlowError("No redirect handler provided for authorization code grant") # pragma: no cover + if not self.context.callback_handler: + raise OAuthFlowError("No callback handler provided for authorization code grant") # pragma: no cover + + if self.context.oauth_metadata and self.context.oauth_metadata.authorization_endpoint: + auth_endpoint = str(self.context.oauth_metadata.authorization_endpoint) # pragma: no cover + else: + auth_base_url = self.context.get_authorization_base_url(self.context.server_url) + auth_endpoint = urljoin(auth_base_url, "/authorize") + + if not self.context.client_info: + raise OAuthFlowError("No client info available for authorization") # pragma: no cover + + # Generate PKCE parameters + pkce_params = PKCEParameters.generate() + state = secrets.token_urlsafe(32) + + auth_params = { + "response_type": "code", + "client_id": self.context.client_info.client_id, + "redirect_uri": str(self.context.client_metadata.redirect_uris[0]), + "state": state, + "code_challenge": pkce_params.code_challenge, + "code_challenge_method": "S256", + } + + # Only include resource param if conditions are met + if self.context.should_include_resource_param(self.context.protocol_version): + auth_params["resource"] = self.context.get_resource_url() # RFC 8707 # pragma: no cover + + if self.context.client_metadata.scope: # pragma: no branch + auth_params["scope"] = self.context.client_metadata.scope + + authorization_url = f"{auth_endpoint}?{urlencode(auth_params)}" + await self.context.redirect_handler(authorization_url) + + # Wait for callback + auth_code, returned_state = await self.context.callback_handler() + + if returned_state is None or not secrets.compare_digest(returned_state, state): + raise OAuthFlowError(f"State parameter mismatch: {returned_state} != {state}") # pragma: no cover + + if not auth_code: + raise OAuthFlowError("No authorization code received") # pragma: no cover + + # Return auth code and code verifier for token exchange + return auth_code, pkce_params.code_verifier + + def _get_token_endpoint(self) -> str: + if self.context.oauth_metadata and self.context.oauth_metadata.token_endpoint: + token_url = str(self.context.oauth_metadata.token_endpoint) + else: + auth_base_url = self.context.get_authorization_base_url(self.context.server_url) + token_url = urljoin(auth_base_url, "/token") + return token_url + + async def _exchange_token_authorization_code( + self, auth_code: str, code_verifier: str, *, token_data: dict[str, Any] | None = {} + ) -> httpx.Request: + """Build token exchange request for authorization_code flow.""" + if self.context.client_metadata.redirect_uris is None: + raise OAuthFlowError("No redirect URIs provided for authorization code grant") # pragma: no cover + if not self.context.client_info: + raise OAuthFlowError("Missing client info") # pragma: no cover + + token_url = self._get_token_endpoint() + token_data = token_data or {} + token_data.update( + { + "grant_type": "authorization_code", + "code": auth_code, + "redirect_uri": str(self.context.client_metadata.redirect_uris[0]), + "client_id": self.context.client_info.client_id, + "code_verifier": code_verifier, + } + ) + + # Only include resource param if conditions are met + if self.context.should_include_resource_param(self.context.protocol_version): + token_data["resource"] = self.context.get_resource_url() # RFC 8707 + + # Prepare authentication based on preferred method + headers = {"Content-Type": "application/x-www-form-urlencoded"} + token_data, headers = self.context.prepare_token_auth(token_data, headers) + + return httpx.Request("POST", token_url, data=token_data, headers=headers) + + async def _handle_token_response(self, response: httpx.Response) -> None: + """Handle token exchange response.""" + if response.status_code != 200: + body = await response.aread() # pragma: no cover + body_text = body.decode("utf-8") # pragma: no cover + raise OAuthTokenError(f"Token exchange failed ({response.status_code}): {body_text}") # pragma: no cover + + # Parse and validate response with scope validation + token_response = await handle_token_response_scopes(response) + + # Store tokens in context + self.context.current_tokens = token_response + self.context.update_token_expiry(token_response) + await self.context.storage.set_tokens(token_response) + + async def _refresh_token(self) -> httpx.Request: + """Build token refresh request.""" + if not self.context.current_tokens or not self.context.current_tokens.refresh_token: + raise OAuthTokenError("No refresh token available") # pragma: no cover + + if not self.context.client_info or not self.context.client_info.client_id: + raise OAuthTokenError("No client info available") # pragma: no cover + + if self.context.oauth_metadata and self.context.oauth_metadata.token_endpoint: + token_url = str(self.context.oauth_metadata.token_endpoint) # pragma: no cover + else: + auth_base_url = self.context.get_authorization_base_url(self.context.server_url) + token_url = urljoin(auth_base_url, "/token") + + refresh_data: dict[str, str] = { + "grant_type": "refresh_token", + "refresh_token": self.context.current_tokens.refresh_token, + "client_id": self.context.client_info.client_id, + } + + # Only include resource param if conditions are met + if self.context.should_include_resource_param(self.context.protocol_version): + refresh_data["resource"] = self.context.get_resource_url() # RFC 8707 + + # Prepare authentication based on preferred method + headers = {"Content-Type": "application/x-www-form-urlencoded"} + refresh_data, headers = self.context.prepare_token_auth(refresh_data, headers) + + return httpx.Request("POST", token_url, data=refresh_data, headers=headers) + + async def _handle_refresh_response(self, response: httpx.Response) -> bool: # pragma: no cover + """Handle token refresh response. Returns True if successful.""" + if response.status_code != 200: + logger.warning(f"Token refresh failed: {response.status_code}") + self.context.clear_tokens() + return False + + try: + content = await response.aread() + token_response = OAuthToken.model_validate_json(content) + + self.context.current_tokens = token_response + self.context.update_token_expiry(token_response) + await self.context.storage.set_tokens(token_response) + + return True + except ValidationError: + logger.exception("Invalid refresh response") + self.context.clear_tokens() + return False + + async def _initialize(self) -> None: # pragma: no cover + """Load stored tokens and client info.""" + self.context.current_tokens = await self.context.storage.get_tokens() + self.context.client_info = await self.context.storage.get_client_info() + self._initialized = True + + def _add_auth_header(self, request: httpx.Request) -> None: + """Add authorization header to request if we have valid tokens.""" + if self.context.current_tokens and self.context.current_tokens.access_token: # pragma: no branch + request.headers["Authorization"] = f"Bearer {self.context.current_tokens.access_token}" + + async def _handle_oauth_metadata_response(self, response: httpx.Response) -> None: + content = await response.aread() + metadata = OAuthMetadata.model_validate_json(content) + self.context.oauth_metadata = metadata + + async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.Request, httpx.Response]: + """HTTPX auth flow integration.""" + async with self.context.lock: + if not self._initialized: + await self._initialize() # pragma: no cover + + # Capture protocol version from request headers + self.context.protocol_version = request.headers.get(MCP_PROTOCOL_VERSION) + + if not self.context.is_token_valid() and self.context.can_refresh_token(): + # Try to refresh token + refresh_request = await self._refresh_token() # pragma: no cover + refresh_response = yield refresh_request # pragma: no cover + + if not await self._handle_refresh_response(refresh_response): # pragma: no cover + # Refresh failed, need full re-authentication + self._initialized = False + + if self.context.is_token_valid(): + self._add_auth_header(request) + + response = yield request + + if response.status_code == 401: + # Perform full OAuth flow + try: + # OAuth flow must be inline due to generator constraints + www_auth_resource_metadata_url = extract_resource_metadata_from_www_auth(response) + + # Step 1: Discover protected resource metadata (SEP-985 with fallback support) + prm_discovery_urls = build_protected_resource_metadata_discovery_urls( + www_auth_resource_metadata_url, self.context.server_url + ) + + for url in prm_discovery_urls: # pragma: no branch + discovery_request = create_oauth_metadata_request(url) + + discovery_response = yield discovery_request # sending request + + prm = await handle_protected_resource_response(discovery_response) + if prm: + self.context.protected_resource_metadata = prm + + # todo: try all authorization_servers to find the OASM + assert ( + len(prm.authorization_servers) > 0 + ) # this is always true as authorization_servers has a min length of 1 + + self.context.auth_server_url = str(prm.authorization_servers[0]) + break + else: + logger.debug(f"Protected resource metadata discovery failed: {url}") + + asm_discovery_urls = build_oauth_authorization_server_metadata_discovery_urls( + self.context.auth_server_url, self.context.server_url + ) + + # Step 2: Discover OAuth Authorization Server Metadata (OASM) (with fallback for legacy servers) + for url in asm_discovery_urls: # pragma: no cover + oauth_metadata_request = create_oauth_metadata_request(url) + oauth_metadata_response = yield oauth_metadata_request + + ok, asm = await handle_auth_metadata_response(oauth_metadata_response) + if not ok: + break + if ok and asm: + self.context.oauth_metadata = asm + break + else: + logger.debug(f"OAuth metadata discovery failed: {url}") + + # Step 3: Apply scope selection strategy + self.context.client_metadata.scope = get_client_metadata_scopes( + extract_scope_from_www_auth(response), + self.context.protected_resource_metadata, + self.context.oauth_metadata, + ) + + # Step 4: Register client or use URL-based client ID (CIMD) + if not self.context.client_info: + if should_use_client_metadata_url( + self.context.oauth_metadata, self.context.client_metadata_url + ): + # Use URL-based client ID (CIMD) + logger.debug(f"Using URL-based client ID (CIMD): {self.context.client_metadata_url}") + client_information = create_client_info_from_metadata_url( + self.context.client_metadata_url, # type: ignore[arg-type] + redirect_uris=self.context.client_metadata.redirect_uris, + ) + self.context.client_info = client_information + await self.context.storage.set_client_info(client_information) + else: + # Fallback to Dynamic Client Registration + registration_request = create_client_registration_request( + self.context.oauth_metadata, + self.context.client_metadata, + self.context.get_authorization_base_url(self.context.server_url), + ) + registration_response = yield registration_request + client_information = await handle_registration_response(registration_response) + self.context.client_info = client_information + await self.context.storage.set_client_info(client_information) + + # Step 5: Perform authorization and complete token exchange + token_response = yield await self._perform_authorization() + await self._handle_token_response(token_response) + except Exception: # pragma: no cover + logger.exception("OAuth flow error") + raise + + # Retry with new tokens + self._add_auth_header(request) + yield request + elif response.status_code == 403: + # Step 1: Extract error field from WWW-Authenticate header + error = extract_field_from_www_auth(response, "error") + + # Step 2: Check if we need to step-up authorization + if error == "insufficient_scope": # pragma: no branch + try: + # Step 2a: Update the required scopes + self.context.client_metadata.scope = get_client_metadata_scopes( + extract_scope_from_www_auth(response), self.context.protected_resource_metadata + ) + + # Step 2b: Perform (re-)authorization and token exchange + token_response = yield await self._perform_authorization() + await self._handle_token_response(token_response) + except Exception: # pragma: no cover + logger.exception("OAuth flow error") + raise + + # Retry with new tokens + self._add_auth_header(request) + yield request diff --git a/source/target-vendors/cp312/mcp/client/auth/utils.py b/source/target-vendors/cp312/mcp/client/auth/utils.py new file mode 100644 index 0000000..b4426be --- /dev/null +++ b/source/target-vendors/cp312/mcp/client/auth/utils.py @@ -0,0 +1,336 @@ +import logging +import re +from urllib.parse import urljoin, urlparse + +from httpx import Request, Response +from pydantic import AnyUrl, ValidationError + +from mcp.client.auth import OAuthRegistrationError, OAuthTokenError +from mcp.client.streamable_http import MCP_PROTOCOL_VERSION +from mcp.shared.auth import ( + OAuthClientInformationFull, + OAuthClientMetadata, + OAuthMetadata, + OAuthToken, + ProtectedResourceMetadata, +) +from mcp.types import LATEST_PROTOCOL_VERSION + +logger = logging.getLogger(__name__) + + +def extract_field_from_www_auth(response: Response, field_name: str) -> str | None: + """ + Extract field from WWW-Authenticate header. + + Returns: + Field value if found in WWW-Authenticate header, None otherwise + """ + www_auth_header = response.headers.get("WWW-Authenticate") + if not www_auth_header: + return None + + # Pattern matches: field_name="value" or field_name=value (unquoted) + pattern = rf'{field_name}=(?:"([^"]+)"|([^\s,]+))' + match = re.search(pattern, www_auth_header) + + if match: + # Return quoted value if present, otherwise unquoted value + return match.group(1) or match.group(2) + + return None + + +def extract_scope_from_www_auth(response: Response) -> str | None: + """ + Extract scope parameter from WWW-Authenticate header as per RFC6750. + + Returns: + Scope string if found in WWW-Authenticate header, None otherwise + """ + return extract_field_from_www_auth(response, "scope") + + +def extract_resource_metadata_from_www_auth(response: Response) -> str | None: + """ + Extract protected resource metadata URL from WWW-Authenticate header as per RFC9728. + + Returns: + Resource metadata URL if found in WWW-Authenticate header, None otherwise + """ + if not response or response.status_code != 401: + return None # pragma: no cover + + return extract_field_from_www_auth(response, "resource_metadata") + + +def build_protected_resource_metadata_discovery_urls(www_auth_url: str | None, server_url: str) -> list[str]: + """ + Build ordered list of URLs to try for protected resource metadata discovery. + + Per SEP-985, the client MUST: + 1. Try resource_metadata from WWW-Authenticate header (if present) + 2. Fall back to path-based well-known URI: /.well-known/oauth-protected-resource/{path} + 3. Fall back to root-based well-known URI: /.well-known/oauth-protected-resource + + Args: + www_auth_url: optional resource_metadata url extracted from the WWW-Authenticate header + server_url: server url + + Returns: + Ordered list of URLs to try for discovery + """ + urls: list[str] = [] + + # Priority 1: WWW-Authenticate header with resource_metadata parameter + if www_auth_url: + urls.append(www_auth_url) + + # Priority 2-3: Well-known URIs (RFC 9728) + parsed = urlparse(server_url) + base_url = f"{parsed.scheme}://{parsed.netloc}" + + # Priority 2: Path-based well-known URI (if server has a path component) + if parsed.path and parsed.path != "/": + path_based_url = urljoin(base_url, f"/.well-known/oauth-protected-resource{parsed.path}") + urls.append(path_based_url) + + # Priority 3: Root-based well-known URI + root_based_url = urljoin(base_url, "/.well-known/oauth-protected-resource") + urls.append(root_based_url) + + return urls + + +def get_client_metadata_scopes( + www_authenticate_scope: str | None, + protected_resource_metadata: ProtectedResourceMetadata | None, + authorization_server_metadata: OAuthMetadata | None = None, +) -> str | None: + """Select scopes as outlined in the 'Scope Selection Strategy' in the MCP spec.""" + # Per MCP spec, scope selection priority order: + # 1. Use scope from WWW-Authenticate header (if provided) + # 2. Use all scopes from PRM scopes_supported (if available) + # 3. Omit scope parameter if neither is available + + if www_authenticate_scope is not None: + # Priority 1: WWW-Authenticate header scope + return www_authenticate_scope + elif protected_resource_metadata is not None and protected_resource_metadata.scopes_supported is not None: + # Priority 2: PRM scopes_supported + return " ".join(protected_resource_metadata.scopes_supported) + elif authorization_server_metadata is not None and authorization_server_metadata.scopes_supported is not None: + return " ".join(authorization_server_metadata.scopes_supported) # pragma: no cover + else: + # Priority 3: Omit scope parameter + return None + + +def build_oauth_authorization_server_metadata_discovery_urls(auth_server_url: str | None, server_url: str) -> list[str]: + """ + Generate ordered list of (url, type) tuples for discovery attempts. + + Args: + auth_server_url: URL for the OAuth Authorization Metadata URL if found, otherwise None + server_url: URL for the MCP server, used as a fallback if auth_server_url is None + """ + + if not auth_server_url: + # Legacy path using the 2025-03-26 spec: + # link: https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization + parsed = urlparse(server_url) + return [f"{parsed.scheme}://{parsed.netloc}/.well-known/oauth-authorization-server"] + + urls: list[str] = [] + parsed = urlparse(auth_server_url) + base_url = f"{parsed.scheme}://{parsed.netloc}" + + # RFC 8414: Path-aware OAuth discovery + if parsed.path and parsed.path != "/": + oauth_path = f"/.well-known/oauth-authorization-server{parsed.path.rstrip('/')}" + urls.append(urljoin(base_url, oauth_path)) + + # RFC 8414 section 5: Path-aware OIDC discovery + # See https://www.rfc-editor.org/rfc/rfc8414.html#section-5 + oidc_path = f"/.well-known/openid-configuration{parsed.path.rstrip('/')}" + urls.append(urljoin(base_url, oidc_path)) + + # https://openid.net/specs/openid-connect-discovery-1_0.html + oidc_path = f"{parsed.path.rstrip('/')}/.well-known/openid-configuration" + urls.append(urljoin(base_url, oidc_path)) + return urls + + # OAuth root + urls.append(urljoin(base_url, "/.well-known/oauth-authorization-server")) + + # OIDC 1.0 fallback (appends to full URL per OIDC spec) + # https://openid.net/specs/openid-connect-discovery-1_0.html + urls.append(urljoin(base_url, "/.well-known/openid-configuration")) + + return urls + + +async def handle_protected_resource_response( + response: Response, +) -> ProtectedResourceMetadata | None: + """ + Handle protected resource metadata discovery response. + + Per SEP-985, supports fallback when discovery fails at one URL. + + Returns: + True if metadata was successfully discovered, False if we should try next URL + """ + if response.status_code == 200: + try: + content = await response.aread() + metadata = ProtectedResourceMetadata.model_validate_json(content) + return metadata + + except ValidationError: # pragma: no cover + # Invalid metadata - try next URL + return None + else: + # Not found - try next URL in fallback chain + return None + + +async def handle_auth_metadata_response(response: Response) -> tuple[bool, OAuthMetadata | None]: + if response.status_code == 200: + try: + content = await response.aread() + asm = OAuthMetadata.model_validate_json(content) + return True, asm + except ValidationError: # pragma: no cover + return True, None + elif response.status_code < 400 or response.status_code >= 500: + return False, None # Non-4XX error, stop trying + return True, None + + +def create_oauth_metadata_request(url: str) -> Request: + return Request("GET", url, headers={MCP_PROTOCOL_VERSION: LATEST_PROTOCOL_VERSION}) + + +def create_client_registration_request( + auth_server_metadata: OAuthMetadata | None, client_metadata: OAuthClientMetadata, auth_base_url: str +) -> Request: + """Build registration request or skip if already registered.""" + + if auth_server_metadata and auth_server_metadata.registration_endpoint: + registration_url = str(auth_server_metadata.registration_endpoint) + else: + registration_url = urljoin(auth_base_url, "/register") + + registration_data = client_metadata.model_dump(by_alias=True, mode="json", exclude_none=True) + + return Request("POST", registration_url, json=registration_data, headers={"Content-Type": "application/json"}) + + +async def handle_registration_response(response: Response) -> OAuthClientInformationFull: + """Handle registration response.""" + if response.status_code not in (200, 201): + await response.aread() + raise OAuthRegistrationError(f"Registration failed: {response.status_code} {response.text}") + + try: + content = await response.aread() + client_info = OAuthClientInformationFull.model_validate_json(content) + return client_info + # self.context.client_info = client_info + # await self.context.storage.set_client_info(client_info) + except ValidationError as e: # pragma: no cover + raise OAuthRegistrationError(f"Invalid registration response: {e}") + + +def is_valid_client_metadata_url(url: str | None) -> bool: + """Validate that a URL is suitable for use as a client_id (CIMD). + + The URL must be HTTPS with a non-root pathname. + + Args: + url: The URL to validate + + Returns: + True if the URL is a valid HTTPS URL with a non-root pathname + """ + if not url: + return False + try: + parsed = urlparse(url) + return parsed.scheme == "https" and parsed.path not in ("", "/") + except Exception: + return False + + +def should_use_client_metadata_url( + oauth_metadata: OAuthMetadata | None, + client_metadata_url: str | None, +) -> bool: + """Determine if URL-based client ID (CIMD) should be used instead of DCR. + + URL-based client IDs should be used when: + 1. The server advertises client_id_metadata_document_supported=true + 2. The client has a valid client_metadata_url configured + + Args: + oauth_metadata: OAuth authorization server metadata + client_metadata_url: URL-based client ID (already validated) + + Returns: + True if CIMD should be used, False if DCR should be used + """ + if not client_metadata_url: + return False + + if not oauth_metadata: + return False + + return oauth_metadata.client_id_metadata_document_supported is True + + +def create_client_info_from_metadata_url( + client_metadata_url: str, redirect_uris: list[AnyUrl] | None = None +) -> OAuthClientInformationFull: + """Create client information using a URL-based client ID (CIMD). + + When using URL-based client IDs, the URL itself becomes the client_id + and no client_secret is used (token_endpoint_auth_method="none"). + + Args: + client_metadata_url: The URL to use as the client_id + redirect_uris: The redirect URIs from the client metadata (passed through for + compatibility with OAuthClientInformationFull which inherits from OAuthClientMetadata) + + Returns: + OAuthClientInformationFull with the URL as client_id + """ + return OAuthClientInformationFull( + client_id=client_metadata_url, + token_endpoint_auth_method="none", + redirect_uris=redirect_uris, + ) + + +async def handle_token_response_scopes( + response: Response, +) -> OAuthToken: + """Parse and validate token response with optional scope validation. + + Parses token response JSON. Callers should check response.status_code before calling. + + Args: + response: HTTP response from token endpoint (status already checked by caller) + + Returns: + Validated OAuthToken model + + Raises: + OAuthTokenError: If response JSON is invalid + """ + try: + content = await response.aread() + token_response = OAuthToken.model_validate_json(content) + return token_response + except ValidationError as e: # pragma: no cover + raise OAuthTokenError(f"Invalid token response: {e}") diff --git a/source/target-vendors/cp312/mcp/client/experimental/__init__.py b/source/target-vendors/cp312/mcp/client/experimental/__init__.py new file mode 100644 index 0000000..b6579b1 --- /dev/null +++ b/source/target-vendors/cp312/mcp/client/experimental/__init__.py @@ -0,0 +1,9 @@ +""" +Experimental client features. + +WARNING: These APIs are experimental and may change without notice. +""" + +from mcp.client.experimental.tasks import ExperimentalClientFeatures + +__all__ = ["ExperimentalClientFeatures"] diff --git a/source/target-vendors/cp312/mcp/client/experimental/task_handlers.py b/source/target-vendors/cp312/mcp/client/experimental/task_handlers.py new file mode 100644 index 0000000..a475086 --- /dev/null +++ b/source/target-vendors/cp312/mcp/client/experimental/task_handlers.py @@ -0,0 +1,290 @@ +""" +Experimental task handler protocols for server -> client requests. + +This module provides Protocol types and default handlers for when servers +send task-related requests to clients (the reverse of normal client -> server flow). + +WARNING: These APIs are experimental and may change without notice. + +Use cases: +- Server sends task-augmented sampling/elicitation request to client +- Client creates a local task, spawns background work, returns CreateTaskResult +- Server polls client's task status via tasks/get, tasks/result, etc. +""" + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, Protocol + +from pydantic import TypeAdapter + +import mcp.types as types +from mcp.shared.context import RequestContext +from mcp.shared.session import RequestResponder + +if TYPE_CHECKING: + from mcp.client.session import ClientSession + + +class GetTaskHandlerFnT(Protocol): + """Handler for tasks/get requests from server. + + WARNING: This is experimental and may change without notice. + """ + + async def __call__( + self, + context: RequestContext["ClientSession", Any], + params: types.GetTaskRequestParams, + ) -> types.GetTaskResult | types.ErrorData: ... # pragma: no branch + + +class GetTaskResultHandlerFnT(Protocol): + """Handler for tasks/result requests from server. + + WARNING: This is experimental and may change without notice. + """ + + async def __call__( + self, + context: RequestContext["ClientSession", Any], + params: types.GetTaskPayloadRequestParams, + ) -> types.GetTaskPayloadResult | types.ErrorData: ... # pragma: no branch + + +class ListTasksHandlerFnT(Protocol): + """Handler for tasks/list requests from server. + + WARNING: This is experimental and may change without notice. + """ + + async def __call__( + self, + context: RequestContext["ClientSession", Any], + params: types.PaginatedRequestParams | None, + ) -> types.ListTasksResult | types.ErrorData: ... # pragma: no branch + + +class CancelTaskHandlerFnT(Protocol): + """Handler for tasks/cancel requests from server. + + WARNING: This is experimental and may change without notice. + """ + + async def __call__( + self, + context: RequestContext["ClientSession", Any], + params: types.CancelTaskRequestParams, + ) -> types.CancelTaskResult | types.ErrorData: ... # pragma: no branch + + +class TaskAugmentedSamplingFnT(Protocol): + """Handler for task-augmented sampling/createMessage requests from server. + + When server sends a CreateMessageRequest with task field, this callback + is invoked. The callback should create a task, spawn background work, + and return CreateTaskResult immediately. + + WARNING: This is experimental and may change without notice. + """ + + async def __call__( + self, + context: RequestContext["ClientSession", Any], + params: types.CreateMessageRequestParams, + task_metadata: types.TaskMetadata, + ) -> types.CreateTaskResult | types.ErrorData: ... # pragma: no branch + + +class TaskAugmentedElicitationFnT(Protocol): + """Handler for task-augmented elicitation/create requests from server. + + When server sends an ElicitRequest with task field, this callback + is invoked. The callback should create a task, spawn background work, + and return CreateTaskResult immediately. + + WARNING: This is experimental and may change without notice. + """ + + async def __call__( + self, + context: RequestContext["ClientSession", Any], + params: types.ElicitRequestParams, + task_metadata: types.TaskMetadata, + ) -> types.CreateTaskResult | types.ErrorData: ... # pragma: no branch + + +async def default_get_task_handler( + context: RequestContext["ClientSession", Any], + params: types.GetTaskRequestParams, +) -> types.GetTaskResult | types.ErrorData: + return types.ErrorData( + code=types.METHOD_NOT_FOUND, + message="tasks/get not supported", + ) + + +async def default_get_task_result_handler( + context: RequestContext["ClientSession", Any], + params: types.GetTaskPayloadRequestParams, +) -> types.GetTaskPayloadResult | types.ErrorData: + return types.ErrorData( + code=types.METHOD_NOT_FOUND, + message="tasks/result not supported", + ) + + +async def default_list_tasks_handler( + context: RequestContext["ClientSession", Any], + params: types.PaginatedRequestParams | None, +) -> types.ListTasksResult | types.ErrorData: + return types.ErrorData( + code=types.METHOD_NOT_FOUND, + message="tasks/list not supported", + ) + + +async def default_cancel_task_handler( + context: RequestContext["ClientSession", Any], + params: types.CancelTaskRequestParams, +) -> types.CancelTaskResult | types.ErrorData: + return types.ErrorData( + code=types.METHOD_NOT_FOUND, + message="tasks/cancel not supported", + ) + + +async def default_task_augmented_sampling( + context: RequestContext["ClientSession", Any], + params: types.CreateMessageRequestParams, + task_metadata: types.TaskMetadata, +) -> types.CreateTaskResult | types.ErrorData: + return types.ErrorData( + code=types.INVALID_REQUEST, + message="Task-augmented sampling not supported", + ) + + +async def default_task_augmented_elicitation( + context: RequestContext["ClientSession", Any], + params: types.ElicitRequestParams, + task_metadata: types.TaskMetadata, +) -> types.CreateTaskResult | types.ErrorData: + return types.ErrorData( + code=types.INVALID_REQUEST, + message="Task-augmented elicitation not supported", + ) + + +@dataclass +class ExperimentalTaskHandlers: + """Container for experimental task handlers. + + Groups all task-related handlers that handle server -> client requests. + This includes both pure task requests (get, list, cancel, result) and + task-augmented request handlers (sampling, elicitation with task field). + + WARNING: These APIs are experimental and may change without notice. + + Example: + handlers = ExperimentalTaskHandlers( + get_task=my_get_task_handler, + list_tasks=my_list_tasks_handler, + ) + session = ClientSession(..., experimental_task_handlers=handlers) + """ + + # Pure task request handlers + get_task: GetTaskHandlerFnT = field(default=default_get_task_handler) + get_task_result: GetTaskResultHandlerFnT = field(default=default_get_task_result_handler) + list_tasks: ListTasksHandlerFnT = field(default=default_list_tasks_handler) + cancel_task: CancelTaskHandlerFnT = field(default=default_cancel_task_handler) + + # Task-augmented request handlers + augmented_sampling: TaskAugmentedSamplingFnT = field(default=default_task_augmented_sampling) + augmented_elicitation: TaskAugmentedElicitationFnT = field(default=default_task_augmented_elicitation) + + def build_capability(self) -> types.ClientTasksCapability | None: + """Build ClientTasksCapability from the configured handlers. + + Returns a capability object that reflects which handlers are configured + (i.e., not using the default "not supported" handlers). + + Returns: + ClientTasksCapability if any handlers are provided, None otherwise + """ + has_list = self.list_tasks is not default_list_tasks_handler + has_cancel = self.cancel_task is not default_cancel_task_handler + has_sampling = self.augmented_sampling is not default_task_augmented_sampling + has_elicitation = self.augmented_elicitation is not default_task_augmented_elicitation + + # If no handlers are provided, return None + if not any([has_list, has_cancel, has_sampling, has_elicitation]): + return None + + # Build requests capability if any request handlers are provided + requests_capability: types.ClientTasksRequestsCapability | None = None + if has_sampling or has_elicitation: + requests_capability = types.ClientTasksRequestsCapability( + sampling=types.TasksSamplingCapability(createMessage=types.TasksCreateMessageCapability()) + if has_sampling + else None, + elicitation=types.TasksElicitationCapability(create=types.TasksCreateElicitationCapability()) + if has_elicitation + else None, + ) + + return types.ClientTasksCapability( + list=types.TasksListCapability() if has_list else None, + cancel=types.TasksCancelCapability() if has_cancel else None, + requests=requests_capability, + ) + + @staticmethod + def handles_request(request: types.ServerRequest) -> bool: + """Check if this handler handles the given request type.""" + return isinstance( + request.root, + types.GetTaskRequest | types.GetTaskPayloadRequest | types.ListTasksRequest | types.CancelTaskRequest, + ) + + async def handle_request( + self, + ctx: RequestContext["ClientSession", Any], + responder: RequestResponder[types.ServerRequest, types.ClientResult], + ) -> None: + """Handle a task-related request from the server. + + Call handles_request() first to check if this handler can handle the request. + """ + client_response_type: TypeAdapter[types.ClientResult | types.ErrorData] = TypeAdapter( + types.ClientResult | types.ErrorData + ) + + match responder.request.root: + case types.GetTaskRequest(params=params): + response = await self.get_task(ctx, params) + client_response = client_response_type.validate_python(response) + await responder.respond(client_response) + + case types.GetTaskPayloadRequest(params=params): + response = await self.get_task_result(ctx, params) + client_response = client_response_type.validate_python(response) + await responder.respond(client_response) + + case types.ListTasksRequest(params=params): + response = await self.list_tasks(ctx, params) + client_response = client_response_type.validate_python(response) + await responder.respond(client_response) + + case types.CancelTaskRequest(params=params): + response = await self.cancel_task(ctx, params) + client_response = client_response_type.validate_python(response) + await responder.respond(client_response) + + case _: # pragma: no cover + raise ValueError(f"Unhandled request type: {type(responder.request.root)}") + + +# Backwards compatibility aliases +default_task_augmented_sampling_callback = default_task_augmented_sampling +default_task_augmented_elicitation_callback = default_task_augmented_elicitation diff --git a/source/target-vendors/cp312/mcp/client/experimental/tasks.py b/source/target-vendors/cp312/mcp/client/experimental/tasks.py new file mode 100644 index 0000000..ce9c387 --- /dev/null +++ b/source/target-vendors/cp312/mcp/client/experimental/tasks.py @@ -0,0 +1,224 @@ +""" +Experimental client-side task support. + +This module provides client methods for interacting with MCP tasks. + +WARNING: These APIs are experimental and may change without notice. + +Example: + # Call a tool as a task + result = await session.experimental.call_tool_as_task("tool_name", {"arg": "value"}) + task_id = result.task.taskId + + # Get task status + status = await session.experimental.get_task(task_id) + + # Get task result when complete + if status.status == "completed": + result = await session.experimental.get_task_result(task_id, CallToolResult) + + # List all tasks + tasks = await session.experimental.list_tasks() + + # Cancel a task + await session.experimental.cancel_task(task_id) +""" + +from collections.abc import AsyncIterator +from typing import TYPE_CHECKING, Any, TypeVar + +import mcp.types as types +from mcp.shared.experimental.tasks.polling import poll_until_terminal + +if TYPE_CHECKING: + from mcp.client.session import ClientSession + +ResultT = TypeVar("ResultT", bound=types.Result) + + +class ExperimentalClientFeatures: + """ + Experimental client features for tasks and other experimental APIs. + + WARNING: These APIs are experimental and may change without notice. + + Access via session.experimental: + status = await session.experimental.get_task(task_id) + """ + + def __init__(self, session: "ClientSession") -> None: + self._session = session + + async def call_tool_as_task( + self, + name: str, + arguments: dict[str, Any] | None = None, + *, + ttl: int = 60000, + meta: dict[str, Any] | None = None, + ) -> types.CreateTaskResult: + """Call a tool as a task, returning a CreateTaskResult for polling. + + This is a convenience method for calling tools that support task execution. + The server will return a task reference instead of the immediate result, + which can then be polled via `get_task()` and retrieved via `get_task_result()`. + + Args: + name: The tool name + arguments: Tool arguments + ttl: Task time-to-live in milliseconds (default: 60000 = 1 minute) + meta: Optional metadata to include in the request + + Returns: + CreateTaskResult containing the task reference + + Example: + # Create task + result = await session.experimental.call_tool_as_task( + "long_running_tool", {"input": "data"} + ) + task_id = result.task.taskId + + # Poll for completion + while True: + status = await session.experimental.get_task(task_id) + if status.status == "completed": + break + await asyncio.sleep(0.5) + + # Get result + final = await session.experimental.get_task_result(task_id, CallToolResult) + """ + _meta: types.RequestParams.Meta | None = None + if meta is not None: + _meta = types.RequestParams.Meta(**meta) + + return await self._session.send_request( + types.ClientRequest( + types.CallToolRequest( + params=types.CallToolRequestParams( + name=name, + arguments=arguments, + task=types.TaskMetadata(ttl=ttl), + _meta=_meta, + ), + ) + ), + types.CreateTaskResult, + ) + + async def get_task(self, task_id: str) -> types.GetTaskResult: + """ + Get the current status of a task. + + Args: + task_id: The task identifier + + Returns: + GetTaskResult containing the task status and metadata + """ + return await self._session.send_request( + types.ClientRequest( + types.GetTaskRequest( + params=types.GetTaskRequestParams(taskId=task_id), + ) + ), + types.GetTaskResult, + ) + + async def get_task_result( + self, + task_id: str, + result_type: type[ResultT], + ) -> ResultT: + """ + Get the result of a completed task. + + The result type depends on the original request type: + - tools/call tasks return CallToolResult + - Other request types return their corresponding result type + + Args: + task_id: The task identifier + result_type: The expected result type (e.g., CallToolResult) + + Returns: + The task result, validated against result_type + """ + return await self._session.send_request( + types.ClientRequest( + types.GetTaskPayloadRequest( + params=types.GetTaskPayloadRequestParams(taskId=task_id), + ) + ), + result_type, + ) + + async def list_tasks( + self, + cursor: str | None = None, + ) -> types.ListTasksResult: + """ + List all tasks. + + Args: + cursor: Optional pagination cursor + + Returns: + ListTasksResult containing tasks and optional next cursor + """ + params = types.PaginatedRequestParams(cursor=cursor) if cursor else None + return await self._session.send_request( + types.ClientRequest( + types.ListTasksRequest(params=params), + ), + types.ListTasksResult, + ) + + async def cancel_task(self, task_id: str) -> types.CancelTaskResult: + """ + Cancel a running task. + + Args: + task_id: The task identifier + + Returns: + CancelTaskResult with the updated task state + """ + return await self._session.send_request( + types.ClientRequest( + types.CancelTaskRequest( + params=types.CancelTaskRequestParams(taskId=task_id), + ) + ), + types.CancelTaskResult, + ) + + async def poll_task(self, task_id: str) -> AsyncIterator[types.GetTaskResult]: + """ + Poll a task until it reaches a terminal status. + + Yields GetTaskResult for each poll, allowing the caller to react to + status changes (e.g., handle input_required). Exits when task reaches + a terminal status (completed, failed, cancelled). + + Respects the pollInterval hint from the server. + + Args: + task_id: The task identifier + + Yields: + GetTaskResult for each poll + + Example: + async for status in session.experimental.poll_task(task_id): + print(f"Status: {status.status}") + if status.status == "input_required": + # Handle elicitation request via tasks/result + pass + + # Task is now terminal, get the result + result = await session.experimental.get_task_result(task_id, CallToolResult) + """ + async for status in poll_until_terminal(self.get_task, task_id): + yield status diff --git a/source/target-vendors/cp312/mcp/client/session.py b/source/target-vendors/cp312/mcp/client/session.py new file mode 100644 index 0000000..8519f15 --- /dev/null +++ b/source/target-vendors/cp312/mcp/client/session.py @@ -0,0 +1,615 @@ +import logging +from datetime import timedelta +from typing import Any, Protocol, overload + +import anyio.lowlevel +from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream +from pydantic import AnyUrl, TypeAdapter +from typing_extensions import deprecated + +import mcp.types as types +from mcp.client.experimental import ExperimentalClientFeatures +from mcp.client.experimental.task_handlers import ExperimentalTaskHandlers +from mcp.shared.context import RequestContext +from mcp.shared.message import SessionMessage +from mcp.shared.session import BaseSession, ProgressFnT, RequestResponder +from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS + +DEFAULT_CLIENT_INFO = types.Implementation(name="mcp", version="0.1.0") + +logger = logging.getLogger("client") + + +class SamplingFnT(Protocol): + async def __call__( + self, + context: RequestContext["ClientSession", Any], + params: types.CreateMessageRequestParams, + ) -> types.CreateMessageResult | types.CreateMessageResultWithTools | types.ErrorData: ... # pragma: no branch + + +class ElicitationFnT(Protocol): + async def __call__( + self, + context: RequestContext["ClientSession", Any], + params: types.ElicitRequestParams, + ) -> types.ElicitResult | types.ErrorData: ... # pragma: no branch + + +class ListRootsFnT(Protocol): + async def __call__( + self, context: RequestContext["ClientSession", Any] + ) -> types.ListRootsResult | types.ErrorData: ... # pragma: no branch + + +class LoggingFnT(Protocol): + async def __call__( + self, + params: types.LoggingMessageNotificationParams, + ) -> None: ... # pragma: no branch + + +class MessageHandlerFnT(Protocol): + async def __call__( + self, + message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception, + ) -> None: ... # pragma: no branch + + +async def _default_message_handler( + message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception, +) -> None: + await anyio.lowlevel.checkpoint() + + +async def _default_sampling_callback( + context: RequestContext["ClientSession", Any], + params: types.CreateMessageRequestParams, +) -> types.CreateMessageResult | types.CreateMessageResultWithTools | types.ErrorData: + return types.ErrorData( + code=types.INVALID_REQUEST, + message="Sampling not supported", + ) + + +async def _default_elicitation_callback( + context: RequestContext["ClientSession", Any], + params: types.ElicitRequestParams, +) -> types.ElicitResult | types.ErrorData: + return types.ErrorData( # pragma: no cover + code=types.INVALID_REQUEST, + message="Elicitation not supported", + ) + + +async def _default_list_roots_callback( + context: RequestContext["ClientSession", Any], +) -> types.ListRootsResult | types.ErrorData: + return types.ErrorData( + code=types.INVALID_REQUEST, + message="List roots not supported", + ) + + +async def _default_logging_callback( + params: types.LoggingMessageNotificationParams, +) -> None: + pass + + +ClientResponse: TypeAdapter[types.ClientResult | types.ErrorData] = TypeAdapter(types.ClientResult | types.ErrorData) + + +class ClientSession( + BaseSession[ + types.ClientRequest, + types.ClientNotification, + types.ClientResult, + types.ServerRequest, + types.ServerNotification, + ] +): + def __init__( + self, + read_stream: MemoryObjectReceiveStream[SessionMessage | Exception], + write_stream: MemoryObjectSendStream[SessionMessage], + read_timeout_seconds: timedelta | None = None, + sampling_callback: SamplingFnT | None = None, + elicitation_callback: ElicitationFnT | None = None, + list_roots_callback: ListRootsFnT | None = None, + logging_callback: LoggingFnT | None = None, + message_handler: MessageHandlerFnT | None = None, + client_info: types.Implementation | None = None, + *, + sampling_capabilities: types.SamplingCapability | None = None, + experimental_task_handlers: ExperimentalTaskHandlers | None = None, + ) -> None: + super().__init__( + read_stream, + write_stream, + types.ServerRequest, + types.ServerNotification, + read_timeout_seconds=read_timeout_seconds, + ) + self._client_info = client_info or DEFAULT_CLIENT_INFO + self._sampling_callback = sampling_callback or _default_sampling_callback + self._sampling_capabilities = sampling_capabilities + self._elicitation_callback = elicitation_callback or _default_elicitation_callback + self._list_roots_callback = list_roots_callback or _default_list_roots_callback + self._logging_callback = logging_callback or _default_logging_callback + self._message_handler = message_handler or _default_message_handler + self._tool_output_schemas: dict[str, dict[str, Any] | None] = {} + self._server_capabilities: types.ServerCapabilities | None = None + self._experimental_features: ExperimentalClientFeatures | None = None + + # Experimental: Task handlers (use defaults if not provided) + self._task_handlers = experimental_task_handlers or ExperimentalTaskHandlers() + + async def initialize(self) -> types.InitializeResult: + sampling = ( + (self._sampling_capabilities or types.SamplingCapability()) + if self._sampling_callback is not _default_sampling_callback + else None + ) + elicitation = ( + types.ElicitationCapability( + form=types.FormElicitationCapability(), + url=types.UrlElicitationCapability(), + ) + if self._elicitation_callback is not _default_elicitation_callback + else None + ) + roots = ( + # TODO: Should this be based on whether we + # _will_ send notifications, or only whether + # they're supported? + types.RootsCapability(listChanged=True) + if self._list_roots_callback is not _default_list_roots_callback + else None + ) + + result = await self.send_request( + types.ClientRequest( + types.InitializeRequest( + params=types.InitializeRequestParams( + protocolVersion=types.LATEST_PROTOCOL_VERSION, + capabilities=types.ClientCapabilities( + sampling=sampling, + elicitation=elicitation, + experimental=None, + roots=roots, + tasks=self._task_handlers.build_capability(), + ), + clientInfo=self._client_info, + ), + ) + ), + types.InitializeResult, + ) + + if result.protocolVersion not in SUPPORTED_PROTOCOL_VERSIONS: + raise RuntimeError(f"Unsupported protocol version from the server: {result.protocolVersion}") + + self._server_capabilities = result.capabilities + + await self.send_notification(types.ClientNotification(types.InitializedNotification())) + + return result + + def get_server_capabilities(self) -> types.ServerCapabilities | None: + """Return the server capabilities received during initialization. + + Returns None if the session has not been initialized yet. + """ + return self._server_capabilities + + @property + def experimental(self) -> ExperimentalClientFeatures: + """Experimental APIs for tasks and other features. + + WARNING: These APIs are experimental and may change without notice. + + Example: + status = await session.experimental.get_task(task_id) + result = await session.experimental.get_task_result(task_id, CallToolResult) + """ + if self._experimental_features is None: + self._experimental_features = ExperimentalClientFeatures(self) + return self._experimental_features + + async def send_ping(self) -> types.EmptyResult: + """Send a ping request.""" + return await self.send_request( + types.ClientRequest(types.PingRequest()), + types.EmptyResult, + ) + + async def send_progress_notification( + self, + progress_token: str | int, + progress: float, + total: float | None = None, + message: str | None = None, + ) -> None: + """Send a progress notification.""" + await self.send_notification( + types.ClientNotification( + types.ProgressNotification( + params=types.ProgressNotificationParams( + progressToken=progress_token, + progress=progress, + total=total, + message=message, + ), + ), + ) + ) + + async def set_logging_level(self, level: types.LoggingLevel) -> types.EmptyResult: + """Send a logging/setLevel request.""" + return await self.send_request( # pragma: no cover + types.ClientRequest( + types.SetLevelRequest( + params=types.SetLevelRequestParams(level=level), + ) + ), + types.EmptyResult, + ) + + @overload + @deprecated("Use list_resources(params=PaginatedRequestParams(...)) instead") + async def list_resources(self, cursor: str | None) -> types.ListResourcesResult: ... + + @overload + async def list_resources(self, *, params: types.PaginatedRequestParams | None) -> types.ListResourcesResult: ... + + @overload + async def list_resources(self) -> types.ListResourcesResult: ... + + async def list_resources( + self, + cursor: str | None = None, + *, + params: types.PaginatedRequestParams | None = None, + ) -> types.ListResourcesResult: + """Send a resources/list request. + + Args: + cursor: Simple cursor string for pagination (deprecated, use params instead) + params: Full pagination parameters including cursor and any future fields + """ + if params is not None and cursor is not None: + raise ValueError("Cannot specify both cursor and params") + + if params is not None: + request_params = params + elif cursor is not None: + request_params = types.PaginatedRequestParams(cursor=cursor) + else: + request_params = None + + return await self.send_request( + types.ClientRequest(types.ListResourcesRequest(params=request_params)), + types.ListResourcesResult, + ) + + @overload + @deprecated("Use list_resource_templates(params=PaginatedRequestParams(...)) instead") + async def list_resource_templates(self, cursor: str | None) -> types.ListResourceTemplatesResult: ... + + @overload + async def list_resource_templates( + self, *, params: types.PaginatedRequestParams | None + ) -> types.ListResourceTemplatesResult: ... + + @overload + async def list_resource_templates(self) -> types.ListResourceTemplatesResult: ... + + async def list_resource_templates( + self, + cursor: str | None = None, + *, + params: types.PaginatedRequestParams | None = None, + ) -> types.ListResourceTemplatesResult: + """Send a resources/templates/list request. + + Args: + cursor: Simple cursor string for pagination (deprecated, use params instead) + params: Full pagination parameters including cursor and any future fields + """ + if params is not None and cursor is not None: + raise ValueError("Cannot specify both cursor and params") + + if params is not None: + request_params = params + elif cursor is not None: + request_params = types.PaginatedRequestParams(cursor=cursor) + else: + request_params = None + + return await self.send_request( + types.ClientRequest(types.ListResourceTemplatesRequest(params=request_params)), + types.ListResourceTemplatesResult, + ) + + async def read_resource(self, uri: AnyUrl) -> types.ReadResourceResult: + """Send a resources/read request.""" + return await self.send_request( + types.ClientRequest( + types.ReadResourceRequest( + params=types.ReadResourceRequestParams(uri=uri), + ) + ), + types.ReadResourceResult, + ) + + async def subscribe_resource(self, uri: AnyUrl) -> types.EmptyResult: + """Send a resources/subscribe request.""" + return await self.send_request( # pragma: no cover + types.ClientRequest( + types.SubscribeRequest( + params=types.SubscribeRequestParams(uri=uri), + ) + ), + types.EmptyResult, + ) + + async def unsubscribe_resource(self, uri: AnyUrl) -> types.EmptyResult: + """Send a resources/unsubscribe request.""" + return await self.send_request( # pragma: no cover + types.ClientRequest( + types.UnsubscribeRequest( + params=types.UnsubscribeRequestParams(uri=uri), + ) + ), + types.EmptyResult, + ) + + async def call_tool( + self, + name: str, + arguments: dict[str, Any] | None = None, + read_timeout_seconds: timedelta | None = None, + progress_callback: ProgressFnT | None = None, + *, + meta: dict[str, Any] | None = None, + ) -> types.CallToolResult: + """Send a tools/call request with optional progress callback support.""" + + _meta: types.RequestParams.Meta | None = None + if meta is not None: + _meta = types.RequestParams.Meta(**meta) + + result = await self.send_request( + types.ClientRequest( + types.CallToolRequest( + params=types.CallToolRequestParams(name=name, arguments=arguments, _meta=_meta), + ) + ), + types.CallToolResult, + request_read_timeout_seconds=read_timeout_seconds, + progress_callback=progress_callback, + ) + + if not result.isError: + await self._validate_tool_result(name, result) + + return result + + async def _validate_tool_result(self, name: str, result: types.CallToolResult) -> None: + """Validate the structured content of a tool result against its output schema.""" + if name not in self._tool_output_schemas: + # refresh output schema cache + await self.list_tools() + + output_schema = None + if name in self._tool_output_schemas: + output_schema = self._tool_output_schemas.get(name) + else: + logger.warning(f"Tool {name} not listed by server, cannot validate any structured content") + + if output_schema is not None: + from jsonschema import SchemaError, ValidationError, validate + + if result.structuredContent is None: + raise RuntimeError( + f"Tool {name} has an output schema but did not return structured content" + ) # pragma: no cover + try: + validate(result.structuredContent, output_schema) + except ValidationError as e: + raise RuntimeError(f"Invalid structured content returned by tool {name}: {e}") # pragma: no cover + except SchemaError as e: # pragma: no cover + raise RuntimeError(f"Invalid schema for tool {name}: {e}") # pragma: no cover + + @overload + @deprecated("Use list_prompts(params=PaginatedRequestParams(...)) instead") + async def list_prompts(self, cursor: str | None) -> types.ListPromptsResult: ... + + @overload + async def list_prompts(self, *, params: types.PaginatedRequestParams | None) -> types.ListPromptsResult: ... + + @overload + async def list_prompts(self) -> types.ListPromptsResult: ... + + async def list_prompts( + self, + cursor: str | None = None, + *, + params: types.PaginatedRequestParams | None = None, + ) -> types.ListPromptsResult: + """Send a prompts/list request. + + Args: + cursor: Simple cursor string for pagination (deprecated, use params instead) + params: Full pagination parameters including cursor and any future fields + """ + if params is not None and cursor is not None: + raise ValueError("Cannot specify both cursor and params") + + if params is not None: + request_params = params + elif cursor is not None: + request_params = types.PaginatedRequestParams(cursor=cursor) + else: + request_params = None + + return await self.send_request( + types.ClientRequest(types.ListPromptsRequest(params=request_params)), + types.ListPromptsResult, + ) + + async def get_prompt(self, name: str, arguments: dict[str, str] | None = None) -> types.GetPromptResult: + """Send a prompts/get request.""" + return await self.send_request( + types.ClientRequest( + types.GetPromptRequest( + params=types.GetPromptRequestParams(name=name, arguments=arguments), + ) + ), + types.GetPromptResult, + ) + + async def complete( + self, + ref: types.ResourceTemplateReference | types.PromptReference, + argument: dict[str, str], + context_arguments: dict[str, str] | None = None, + ) -> types.CompleteResult: + """Send a completion/complete request.""" + context = None + if context_arguments is not None: + context = types.CompletionContext(arguments=context_arguments) + + return await self.send_request( + types.ClientRequest( + types.CompleteRequest( + params=types.CompleteRequestParams( + ref=ref, + argument=types.CompletionArgument(**argument), + context=context, + ), + ) + ), + types.CompleteResult, + ) + + @overload + @deprecated("Use list_tools(params=PaginatedRequestParams(...)) instead") + async def list_tools(self, cursor: str | None) -> types.ListToolsResult: ... + + @overload + async def list_tools(self, *, params: types.PaginatedRequestParams | None) -> types.ListToolsResult: ... + + @overload + async def list_tools(self) -> types.ListToolsResult: ... + + async def list_tools( + self, + cursor: str | None = None, + *, + params: types.PaginatedRequestParams | None = None, + ) -> types.ListToolsResult: + """Send a tools/list request. + + Args: + cursor: Simple cursor string for pagination (deprecated, use params instead) + params: Full pagination parameters including cursor and any future fields + """ + if params is not None and cursor is not None: + raise ValueError("Cannot specify both cursor and params") + + if params is not None: + request_params = params + elif cursor is not None: + request_params = types.PaginatedRequestParams(cursor=cursor) + else: + request_params = None + + result = await self.send_request( + types.ClientRequest(types.ListToolsRequest(params=request_params)), + types.ListToolsResult, + ) + + # Cache tool output schemas for future validation + # Note: don't clear the cache, as we may be using a cursor + for tool in result.tools: + self._tool_output_schemas[tool.name] = tool.outputSchema + + return result + + async def send_roots_list_changed(self) -> None: # pragma: no cover + """Send a roots/list_changed notification.""" + await self.send_notification(types.ClientNotification(types.RootsListChangedNotification())) + + async def _received_request(self, responder: RequestResponder[types.ServerRequest, types.ClientResult]) -> None: + ctx = RequestContext[ClientSession, Any]( + request_id=responder.request_id, + meta=responder.request_meta, + session=self, + lifespan_context=None, + ) + + # Delegate to experimental task handler if applicable + if self._task_handlers.handles_request(responder.request): + with responder: + await self._task_handlers.handle_request(ctx, responder) + return None + + # Core request handling + match responder.request.root: + case types.CreateMessageRequest(params=params): + with responder: + # Check if this is a task-augmented request + if params.task is not None: + response = await self._task_handlers.augmented_sampling(ctx, params, params.task) + else: + response = await self._sampling_callback(ctx, params) + client_response = ClientResponse.validate_python(response) + await responder.respond(client_response) + + case types.ElicitRequest(params=params): + with responder: + # Check if this is a task-augmented request + if params.task is not None: + response = await self._task_handlers.augmented_elicitation(ctx, params, params.task) + else: + response = await self._elicitation_callback(ctx, params) + client_response = ClientResponse.validate_python(response) + await responder.respond(client_response) + + case types.ListRootsRequest(): + with responder: + response = await self._list_roots_callback(ctx) + client_response = ClientResponse.validate_python(response) + await responder.respond(client_response) + + case types.PingRequest(): # pragma: no cover + with responder: + return await responder.respond(types.ClientResult(root=types.EmptyResult())) + + case _: # pragma: no cover + pass # Task requests handled above by _task_handlers + + return None + + async def _handle_incoming( + self, + req: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception, + ) -> None: + """Handle incoming messages by forwarding to the message handler.""" + await self._message_handler(req) + + async def _received_notification(self, notification: types.ServerNotification) -> None: + """Handle notifications from the server.""" + # Process specific notification types + match notification.root: + case types.LoggingMessageNotification(params=params): + await self._logging_callback(params) + case types.ElicitCompleteNotification(params=params): + # Handle elicitation completion notification + # Clients MAY use this to retry requests or update UI + # The notification contains the elicitationId of the completed elicitation + pass + case _: + pass diff --git a/source/target-vendors/cp312/mcp/client/session_group.py b/source/target-vendors/cp312/mcp/client/session_group.py new file mode 100644 index 0000000..f82677d --- /dev/null +++ b/source/target-vendors/cp312/mcp/client/session_group.py @@ -0,0 +1,447 @@ +""" +SessionGroup concurrently manages multiple MCP session connections. + +Tools, resources, and prompts are aggregated across servers. Servers may +be connected to or disconnected from at any point after initialization. + +This abstractions can handle naming collisions using a custom user-provided +hook. +""" + +import contextlib +import logging +from collections.abc import Callable +from dataclasses import dataclass +from datetime import timedelta +from types import TracebackType +from typing import Any, TypeAlias, overload + +import anyio +import httpx +from pydantic import BaseModel +from typing_extensions import Self, deprecated + +import mcp +from mcp import types +from mcp.client.session import ElicitationFnT, ListRootsFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT +from mcp.client.sse import sse_client +from mcp.client.stdio import StdioServerParameters +from mcp.client.streamable_http import streamable_http_client +from mcp.shared._httpx_utils import create_mcp_http_client +from mcp.shared.exceptions import McpError +from mcp.shared.session import ProgressFnT + + +class SseServerParameters(BaseModel): + """Parameters for intializing a sse_client.""" + + # The endpoint URL. + url: str + + # Optional headers to include in requests. + headers: dict[str, Any] | None = None + + # HTTP timeout for regular operations. + timeout: float = 5 + + # Timeout for SSE read operations. + sse_read_timeout: float = 60 * 5 + + +class StreamableHttpParameters(BaseModel): + """Parameters for intializing a streamable_http_client.""" + + # The endpoint URL. + url: str + + # Optional headers to include in requests. + headers: dict[str, Any] | None = None + + # HTTP timeout for regular operations. + timeout: timedelta = timedelta(seconds=30) + + # Timeout for SSE read operations. + sse_read_timeout: timedelta = timedelta(seconds=60 * 5) + + # Close the client session when the transport closes. + terminate_on_close: bool = True + + +ServerParameters: TypeAlias = StdioServerParameters | SseServerParameters | StreamableHttpParameters + + +# Use dataclass instead of pydantic BaseModel +# because pydantic BaseModel cannot handle Protocol fields. +@dataclass +class ClientSessionParameters: + """Parameters for establishing a client session to an MCP server.""" + + read_timeout_seconds: timedelta | None = None + sampling_callback: SamplingFnT | None = None + elicitation_callback: ElicitationFnT | None = None + list_roots_callback: ListRootsFnT | None = None + logging_callback: LoggingFnT | None = None + message_handler: MessageHandlerFnT | None = None + client_info: types.Implementation | None = None + + +class ClientSessionGroup: + """Client for managing connections to multiple MCP servers. + + This class is responsible for encapsulating management of server connections. + It aggregates tools, resources, and prompts from all connected servers. + + For auxiliary handlers, such as resource subscription, this is delegated to + the client and can be accessed via the session. + + Example Usage: + name_fn = lambda name, server_info: f"{(server_info.name)}_{name}" + async with ClientSessionGroup(component_name_hook=name_fn) as group: + for server_param in server_params: + await group.connect_to_server(server_param) + ... + + """ + + class _ComponentNames(BaseModel): + """Used for reverse index to find components.""" + + prompts: set[str] = set() + resources: set[str] = set() + tools: set[str] = set() + + # Standard MCP components. + _prompts: dict[str, types.Prompt] + _resources: dict[str, types.Resource] + _tools: dict[str, types.Tool] + + # Client-server connection management. + _sessions: dict[mcp.ClientSession, _ComponentNames] + _tool_to_session: dict[str, mcp.ClientSession] + _exit_stack: contextlib.AsyncExitStack + _session_exit_stacks: dict[mcp.ClientSession, contextlib.AsyncExitStack] + + # Optional fn consuming (component_name, serverInfo) for custom names. + # This is provide a means to mitigate naming conflicts across servers. + # Example: (tool_name, serverInfo) => "{result.serverInfo.name}.{tool_name}" + _ComponentNameHook: TypeAlias = Callable[[str, types.Implementation], str] + _component_name_hook: _ComponentNameHook | None + + def __init__( + self, + exit_stack: contextlib.AsyncExitStack | None = None, + component_name_hook: _ComponentNameHook | None = None, + ) -> None: + """Initializes the MCP client.""" + + self._tools = {} + self._resources = {} + self._prompts = {} + + self._sessions = {} + self._tool_to_session = {} + if exit_stack is None: + self._exit_stack = contextlib.AsyncExitStack() + self._owns_exit_stack = True + else: + self._exit_stack = exit_stack + self._owns_exit_stack = False + self._session_exit_stacks = {} + self._component_name_hook = component_name_hook + + async def __aenter__(self) -> Self: # pragma: no cover + # Enter the exit stack only if we created it ourselves + if self._owns_exit_stack: + await self._exit_stack.__aenter__() + return self + + async def __aexit__( + self, + _exc_type: type[BaseException] | None, + _exc_val: BaseException | None, + _exc_tb: TracebackType | None, + ) -> bool | None: # pragma: no cover + """Closes session exit stacks and main exit stack upon completion.""" + + # Only close the main exit stack if we created it + if self._owns_exit_stack: + await self._exit_stack.aclose() + + # Concurrently close session stacks. + async with anyio.create_task_group() as tg: + for exit_stack in self._session_exit_stacks.values(): + tg.start_soon(exit_stack.aclose) + + @property + def sessions(self) -> list[mcp.ClientSession]: + """Returns the list of sessions being managed.""" + return list(self._sessions.keys()) # pragma: no cover + + @property + def prompts(self) -> dict[str, types.Prompt]: + """Returns the prompts as a dictionary of names to prompts.""" + return self._prompts + + @property + def resources(self) -> dict[str, types.Resource]: + """Returns the resources as a dictionary of names to resources.""" + return self._resources + + @property + def tools(self) -> dict[str, types.Tool]: + """Returns the tools as a dictionary of names to tools.""" + return self._tools + + @overload + async def call_tool( + self, + name: str, + arguments: dict[str, Any], + read_timeout_seconds: timedelta | None = None, + progress_callback: ProgressFnT | None = None, + *, + meta: dict[str, Any] | None = None, + ) -> types.CallToolResult: ... + + @overload + @deprecated("The 'args' parameter is deprecated. Use 'arguments' instead.") + async def call_tool( + self, + name: str, + *, + args: dict[str, Any], + read_timeout_seconds: timedelta | None = None, + progress_callback: ProgressFnT | None = None, + meta: dict[str, Any] | None = None, + ) -> types.CallToolResult: ... + + async def call_tool( + self, + name: str, + arguments: dict[str, Any] | None = None, + read_timeout_seconds: timedelta | None = None, + progress_callback: ProgressFnT | None = None, + *, + meta: dict[str, Any] | None = None, + args: dict[str, Any] | None = None, + ) -> types.CallToolResult: + """Executes a tool given its name and arguments.""" + session = self._tool_to_session[name] + session_tool_name = self.tools[name].name + return await session.call_tool( + session_tool_name, + arguments if args is None else args, + read_timeout_seconds=read_timeout_seconds, + progress_callback=progress_callback, + meta=meta, + ) + + async def disconnect_from_server(self, session: mcp.ClientSession) -> None: + """Disconnects from a single MCP server.""" + + session_known_for_components = session in self._sessions + session_known_for_stack = session in self._session_exit_stacks + + if not session_known_for_components and not session_known_for_stack: + raise McpError( + types.ErrorData( + code=types.INVALID_PARAMS, + message="Provided session is not managed or already disconnected.", + ) + ) + + if session_known_for_components: # pragma: no cover + component_names = self._sessions.pop(session) # Pop from _sessions tracking + + # Remove prompts associated with the session. + for name in component_names.prompts: + if name in self._prompts: + del self._prompts[name] + # Remove resources associated with the session. + for name in component_names.resources: + if name in self._resources: + del self._resources[name] + # Remove tools associated with the session. + for name in component_names.tools: + if name in self._tools: + del self._tools[name] + if name in self._tool_to_session: + del self._tool_to_session[name] + + # Clean up the session's resources via its dedicated exit stack + if session_known_for_stack: + session_stack_to_close = self._session_exit_stacks.pop(session) # pragma: no cover + await session_stack_to_close.aclose() # pragma: no cover + + async def connect_with_session( + self, server_info: types.Implementation, session: mcp.ClientSession + ) -> mcp.ClientSession: + """Connects to a single MCP server.""" + await self._aggregate_components(server_info, session) + return session + + async def connect_to_server( + self, + server_params: ServerParameters, + session_params: ClientSessionParameters | None = None, + ) -> mcp.ClientSession: + """Connects to a single MCP server.""" + server_info, session = await self._establish_session(server_params, session_params or ClientSessionParameters()) + return await self.connect_with_session(server_info, session) + + async def _establish_session( + self, + server_params: ServerParameters, + session_params: ClientSessionParameters, + ) -> tuple[types.Implementation, mcp.ClientSession]: + """Establish a client session to an MCP server.""" + + session_stack = contextlib.AsyncExitStack() + try: + # Create read and write streams that facilitate io with the server. + if isinstance(server_params, StdioServerParameters): + client = mcp.stdio_client(server_params) + read, write = await session_stack.enter_async_context(client) + elif isinstance(server_params, SseServerParameters): + client = sse_client( + url=server_params.url, + headers=server_params.headers, + timeout=server_params.timeout, + sse_read_timeout=server_params.sse_read_timeout, + ) + read, write = await session_stack.enter_async_context(client) + else: + httpx_client = create_mcp_http_client( + headers=server_params.headers, + timeout=httpx.Timeout( + server_params.timeout.total_seconds(), + read=server_params.sse_read_timeout.total_seconds(), + ), + ) + await session_stack.enter_async_context(httpx_client) + + client = streamable_http_client( + url=server_params.url, + http_client=httpx_client, + terminate_on_close=server_params.terminate_on_close, + ) + read, write, _ = await session_stack.enter_async_context(client) + + session = await session_stack.enter_async_context( + mcp.ClientSession( + read, + write, + read_timeout_seconds=session_params.read_timeout_seconds, + sampling_callback=session_params.sampling_callback, + elicitation_callback=session_params.elicitation_callback, + list_roots_callback=session_params.list_roots_callback, + logging_callback=session_params.logging_callback, + message_handler=session_params.message_handler, + client_info=session_params.client_info, + ) + ) + + result = await session.initialize() + + # Session successfully initialized. + # Store its stack and register the stack with the main group stack. + self._session_exit_stacks[session] = session_stack + # session_stack itself becomes a resource managed by the + # main _exit_stack. + await self._exit_stack.enter_async_context(session_stack) + + return result.serverInfo, session + except Exception: # pragma: no cover + # If anything during this setup fails, ensure the session-specific + # stack is closed. + await session_stack.aclose() + raise + + async def _aggregate_components(self, server_info: types.Implementation, session: mcp.ClientSession) -> None: + """Aggregates prompts, resources, and tools from a given session.""" + + # Create a reverse index so we can find all prompts, resources, and + # tools belonging to this session. Used for removing components from + # the session group via self.disconnect_from_server. + component_names = self._ComponentNames() + + # Temporary components dicts. We do not want to modify the aggregate + # lists in case of an intermediate failure. + prompts_temp: dict[str, types.Prompt] = {} + resources_temp: dict[str, types.Resource] = {} + tools_temp: dict[str, types.Tool] = {} + tool_to_session_temp: dict[str, mcp.ClientSession] = {} + + # Query the server for its prompts and aggregate to list. + try: + prompts = (await session.list_prompts()).prompts + for prompt in prompts: + name = self._component_name(prompt.name, server_info) + prompts_temp[name] = prompt + component_names.prompts.add(name) + except McpError as err: # pragma: no cover + logging.warning(f"Could not fetch prompts: {err}") + + # Query the server for its resources and aggregate to list. + try: + resources = (await session.list_resources()).resources + for resource in resources: + name = self._component_name(resource.name, server_info) + resources_temp[name] = resource + component_names.resources.add(name) + except McpError as err: # pragma: no cover + logging.warning(f"Could not fetch resources: {err}") + + # Query the server for its tools and aggregate to list. + try: + tools = (await session.list_tools()).tools + for tool in tools: + name = self._component_name(tool.name, server_info) + tools_temp[name] = tool + tool_to_session_temp[name] = session + component_names.tools.add(name) + except McpError as err: # pragma: no cover + logging.warning(f"Could not fetch tools: {err}") + + # Clean up exit stack for session if we couldn't retrieve anything + # from the server. + if not any((prompts_temp, resources_temp, tools_temp)): + del self._session_exit_stacks[session] # pragma: no cover + + # Check for duplicates. + matching_prompts = prompts_temp.keys() & self._prompts.keys() + if matching_prompts: + raise McpError( # pragma: no cover + types.ErrorData( + code=types.INVALID_PARAMS, + message=f"{matching_prompts} already exist in group prompts.", + ) + ) + matching_resources = resources_temp.keys() & self._resources.keys() + if matching_resources: + raise McpError( # pragma: no cover + types.ErrorData( + code=types.INVALID_PARAMS, + message=f"{matching_resources} already exist in group resources.", + ) + ) + matching_tools = tools_temp.keys() & self._tools.keys() + if matching_tools: + raise McpError( + types.ErrorData( + code=types.INVALID_PARAMS, + message=f"{matching_tools} already exist in group tools.", + ) + ) + + # Aggregate components. + self._sessions[session] = component_names + self._prompts.update(prompts_temp) + self._resources.update(resources_temp) + self._tools.update(tools_temp) + self._tool_to_session.update(tool_to_session_temp) + + def _component_name(self, name: str, server_info: types.Implementation) -> str: + if self._component_name_hook: + return self._component_name_hook(name, server_info) + return name diff --git a/source/target-vendors/cp312/mcp/client/sse.py b/source/target-vendors/cp312/mcp/client/sse.py new file mode 100644 index 0000000..b2ac677 --- /dev/null +++ b/source/target-vendors/cp312/mcp/client/sse.py @@ -0,0 +1,164 @@ +import logging +from collections.abc import Callable +from contextlib import asynccontextmanager +from typing import Any +from urllib.parse import parse_qs, urljoin, urlparse + +import anyio +import httpx +from anyio.abc import TaskStatus +from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream +from httpx_sse import aconnect_sse +from httpx_sse._exceptions import SSEError + +import mcp.types as types +from mcp.shared._httpx_utils import McpHttpClientFactory, create_mcp_http_client +from mcp.shared.message import SessionMessage + +logger = logging.getLogger(__name__) + + +def remove_request_params(url: str) -> str: + return urljoin(url, urlparse(url).path) + + +def _extract_session_id_from_endpoint(endpoint_url: str) -> str | None: + query_params = parse_qs(urlparse(endpoint_url).query) + return query_params.get("sessionId", [None])[0] or query_params.get("session_id", [None])[0] + + +@asynccontextmanager +async def sse_client( + url: str, + headers: dict[str, Any] | None = None, + timeout: float = 5, + sse_read_timeout: float = 60 * 5, + httpx_client_factory: McpHttpClientFactory = create_mcp_http_client, + auth: httpx.Auth | None = None, + on_session_created: Callable[[str], None] | None = None, +): + """ + Client transport for SSE. + + `sse_read_timeout` determines how long (in seconds) the client will wait for a new + event before disconnecting. All other HTTP operations are controlled by `timeout`. + + Args: + url: The SSE endpoint URL. + headers: Optional headers to include in requests. + timeout: HTTP timeout for regular operations. + sse_read_timeout: Timeout for SSE read operations. + auth: Optional HTTPX authentication handler. + on_session_created: Optional callback invoked with the session ID when received. + """ + read_stream: MemoryObjectReceiveStream[SessionMessage | Exception] + read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception] + + write_stream: MemoryObjectSendStream[SessionMessage] + write_stream_reader: MemoryObjectReceiveStream[SessionMessage] + + read_stream_writer, read_stream = anyio.create_memory_object_stream(0) + write_stream, write_stream_reader = anyio.create_memory_object_stream(0) + + async with anyio.create_task_group() as tg: + try: + logger.debug(f"Connecting to SSE endpoint: {remove_request_params(url)}") + async with httpx_client_factory( + headers=headers, auth=auth, timeout=httpx.Timeout(timeout, read=sse_read_timeout) + ) as client: + async with aconnect_sse( + client, + "GET", + url, + ) as event_source: + event_source.response.raise_for_status() + logger.debug("SSE connection established") + + async def sse_reader( + task_status: TaskStatus[str] = anyio.TASK_STATUS_IGNORED, + ): + try: + async for sse in event_source.aiter_sse(): # pragma: no branch + logger.debug(f"Received SSE event: {sse.event}") + match sse.event: + case "endpoint": + endpoint_url = urljoin(url, sse.data) + logger.debug(f"Received endpoint URL: {endpoint_url}") + + url_parsed = urlparse(url) + endpoint_parsed = urlparse(endpoint_url) + if ( # pragma: no cover + url_parsed.netloc != endpoint_parsed.netloc + or url_parsed.scheme != endpoint_parsed.scheme + ): + error_msg = ( # pragma: no cover + f"Endpoint origin does not match connection origin: {endpoint_url}" + ) + logger.error(error_msg) # pragma: no cover + raise ValueError(error_msg) # pragma: no cover + + if on_session_created: + session_id = _extract_session_id_from_endpoint(endpoint_url) + if session_id: + on_session_created(session_id) + + task_status.started(endpoint_url) + + case "message": + # Skip empty data (keep-alive pings) + if not sse.data: + continue + try: + message = types.JSONRPCMessage.model_validate_json( # noqa: E501 + sse.data + ) + logger.debug(f"Received server message: {message}") + except Exception as exc: # pragma: no cover + logger.exception("Error parsing server message") # pragma: no cover + await read_stream_writer.send(exc) # pragma: no cover + continue # pragma: no cover + + session_message = SessionMessage(message) + await read_stream_writer.send(session_message) + case _: # pragma: no cover + logger.warning(f"Unknown SSE event: {sse.event}") # pragma: no cover + except SSEError as sse_exc: # pragma: no cover + logger.exception("Encountered SSE exception") # pragma: no cover + raise sse_exc # pragma: no cover + except Exception as exc: # pragma: no cover + logger.exception("Error in sse_reader") # pragma: no cover + await read_stream_writer.send(exc) # pragma: no cover + finally: + await read_stream_writer.aclose() + + async def post_writer(endpoint_url: str): + try: + async with write_stream_reader: + async for session_message in write_stream_reader: + logger.debug(f"Sending client message: {session_message}") + response = await client.post( + endpoint_url, + json=session_message.message.model_dump( + by_alias=True, + mode="json", + exclude_none=True, + ), + ) + response.raise_for_status() + logger.debug(f"Client message sent successfully: {response.status_code}") + except Exception: # pragma: no cover + logger.exception("Error in post_writer") # pragma: no cover + finally: + await write_stream.aclose() + + endpoint_url = await tg.start(sse_reader) + logger.debug(f"Starting post writer with endpoint URL: {endpoint_url}") + tg.start_soon(post_writer, endpoint_url) + + try: + yield read_stream, write_stream + finally: + tg.cancel_scope.cancel() + finally: + await read_stream_writer.aclose() + await write_stream.aclose() diff --git a/source/target-vendors/cp312/mcp/client/stdio/__init__.py b/source/target-vendors/cp312/mcp/client/stdio/__init__.py new file mode 100644 index 0000000..0d76bb9 --- /dev/null +++ b/source/target-vendors/cp312/mcp/client/stdio/__init__.py @@ -0,0 +1,278 @@ +import logging +import os +import sys +from contextlib import asynccontextmanager +from pathlib import Path +from typing import Literal, TextIO + +import anyio +import anyio.lowlevel +from anyio.abc import Process +from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream +from anyio.streams.text import TextReceiveStream +from pydantic import BaseModel, Field + +import mcp.types as types +from mcp.os.posix.utilities import terminate_posix_process_tree +from mcp.os.win32.utilities import ( + FallbackProcess, + create_windows_process, + get_windows_executable_command, + terminate_windows_process_tree, +) +from mcp.shared.message import SessionMessage + +logger = logging.getLogger(__name__) + +# Environment variables to inherit by default +DEFAULT_INHERITED_ENV_VARS = ( + [ + "APPDATA", + "HOMEDRIVE", + "HOMEPATH", + "LOCALAPPDATA", + "PATH", + "PATHEXT", + "PROCESSOR_ARCHITECTURE", + "SYSTEMDRIVE", + "SYSTEMROOT", + "TEMP", + "USERNAME", + "USERPROFILE", + ] + if sys.platform == "win32" + else ["HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER"] +) + +# Timeout for process termination before falling back to force kill +PROCESS_TERMINATION_TIMEOUT = 2.0 + + +def get_default_environment() -> dict[str, str]: + """ + Returns a default environment object including only environment variables deemed + safe to inherit. + """ + env: dict[str, str] = {} + + for key in DEFAULT_INHERITED_ENV_VARS: + value = os.environ.get(key) + if value is None: + continue # pragma: no cover + + if value.startswith("()"): # pragma: no cover + # Skip functions, which are a security risk + continue # pragma: no cover + + env[key] = value + + return env + + +class StdioServerParameters(BaseModel): + command: str + """The executable to run to start the server.""" + + args: list[str] = Field(default_factory=list) + """Command line arguments to pass to the executable.""" + + env: dict[str, str] | None = None + """ + The environment to use when spawning the process. + + If not specified, the result of get_default_environment() will be used. + """ + + cwd: str | Path | None = None + """The working directory to use when spawning the process.""" + + encoding: str = "utf-8" + """ + The text encoding used when sending/receiving messages to the server + + defaults to utf-8 + """ + + encoding_error_handler: Literal["strict", "ignore", "replace"] = "strict" + """ + The text encoding error handler. + + See https://docs.python.org/3/library/codecs.html#codec-base-classes for + explanations of possible values + """ + + +@asynccontextmanager +async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stderr): + """ + Client transport for stdio: this will connect to a server by spawning a + process and communicating with it over stdin/stdout. + """ + read_stream: MemoryObjectReceiveStream[SessionMessage | Exception] + read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception] + + write_stream: MemoryObjectSendStream[SessionMessage] + write_stream_reader: MemoryObjectReceiveStream[SessionMessage] + + read_stream_writer, read_stream = anyio.create_memory_object_stream(0) + write_stream, write_stream_reader = anyio.create_memory_object_stream(0) + + try: + command = _get_executable_command(server.command) + + # Open process with stderr piped for capture + process = await _create_platform_compatible_process( + command=command, + args=server.args, + env=({**get_default_environment(), **server.env} if server.env is not None else get_default_environment()), + errlog=errlog, + cwd=server.cwd, + ) + except OSError: + # Clean up streams if process creation fails + await read_stream.aclose() + await write_stream.aclose() + await read_stream_writer.aclose() + await write_stream_reader.aclose() + raise + + async def stdout_reader(): + assert process.stdout, "Opened process is missing stdout" + + try: + async with read_stream_writer: + buffer = "" + async for chunk in TextReceiveStream( + process.stdout, + encoding=server.encoding, + errors=server.encoding_error_handler, + ): + lines = (buffer + chunk).split("\n") + buffer = lines.pop() + + for line in lines: + try: + message = types.JSONRPCMessage.model_validate_json(line) + except Exception as exc: # pragma: no cover + logger.exception("Failed to parse JSONRPC message from server") + await read_stream_writer.send(exc) + continue + + session_message = SessionMessage(message) + await read_stream_writer.send(session_message) + except anyio.ClosedResourceError: # pragma: no cover + await anyio.lowlevel.checkpoint() + + async def stdin_writer(): + assert process.stdin, "Opened process is missing stdin" + + try: + async with write_stream_reader: + async for session_message in write_stream_reader: + json = session_message.message.model_dump_json(by_alias=True, exclude_none=True) + await process.stdin.send( + (json + "\n").encode( + encoding=server.encoding, + errors=server.encoding_error_handler, + ) + ) + except anyio.ClosedResourceError: # pragma: no cover + await anyio.lowlevel.checkpoint() + + async with ( + anyio.create_task_group() as tg, + process, + ): + tg.start_soon(stdout_reader) + tg.start_soon(stdin_writer) + try: + yield read_stream, write_stream + finally: + # MCP spec: stdio shutdown sequence + # 1. Close input stream to server + # 2. Wait for server to exit, or send SIGTERM if it doesn't exit in time + # 3. Send SIGKILL if still not exited + if process.stdin: # pragma: no branch + try: + await process.stdin.aclose() + except Exception: # pragma: no cover + # stdin might already be closed, which is fine + pass + + try: + # Give the process time to exit gracefully after stdin closes + with anyio.fail_after(PROCESS_TERMINATION_TIMEOUT): + await process.wait() + except TimeoutError: + # Process didn't exit from stdin closure, use platform-specific termination + # which handles SIGTERM -> SIGKILL escalation + await _terminate_process_tree(process) + except ProcessLookupError: # pragma: no cover + # Process already exited, which is fine + pass + await read_stream.aclose() + await write_stream.aclose() + await read_stream_writer.aclose() + await write_stream_reader.aclose() + + +def _get_executable_command(command: str) -> str: + """ + Get the correct executable command normalized for the current platform. + + Args: + command: Base command (e.g., 'uvx', 'npx') + + Returns: + str: Platform-appropriate command + """ + if sys.platform == "win32": # pragma: no cover + return get_windows_executable_command(command) + else: + return command # pragma: no cover + + +async def _create_platform_compatible_process( + command: str, + args: list[str], + env: dict[str, str] | None = None, + errlog: TextIO = sys.stderr, + cwd: Path | str | None = None, +): + """ + Creates a subprocess in a platform-compatible way. + + Unix: Creates process in a new session/process group for killpg support + Windows: Creates process in a Job Object for reliable child termination + """ + if sys.platform == "win32": # pragma: no cover + process = await create_windows_process(command, args, env, errlog, cwd) + else: + process = await anyio.open_process( + [command, *args], + env=env, + stderr=errlog, + cwd=cwd, + start_new_session=True, + ) # pragma: no cover + + return process + + +async def _terminate_process_tree(process: Process | FallbackProcess, timeout_seconds: float = 2.0) -> None: + """ + Terminate a process and all its children using platform-specific methods. + + Unix: Uses os.killpg() for atomic process group termination + Windows: Uses Job Objects via pywin32 for reliable child process cleanup + + Args: + process: The process to terminate + timeout_seconds: Timeout in seconds before force killing (default: 2.0) + """ + if sys.platform == "win32": # pragma: no cover + await terminate_windows_process_tree(process, timeout_seconds) + else: # pragma: no cover + # FallbackProcess should only be used for Windows compatibility + assert isinstance(process, Process) + await terminate_posix_process_tree(process, timeout_seconds) diff --git a/source/target-vendors/cp312/mcp/client/streamable_http.py b/source/target-vendors/cp312/mcp/client/streamable_http.py new file mode 100644 index 0000000..ed28fcc --- /dev/null +++ b/source/target-vendors/cp312/mcp/client/streamable_http.py @@ -0,0 +1,722 @@ +""" +StreamableHTTP Client Transport Module + +This module implements the StreamableHTTP transport for MCP clients, +providing support for HTTP POST requests with optional SSE streaming responses +and session management. +""" + +import contextlib +import logging +from collections.abc import AsyncGenerator, Awaitable, Callable +from contextlib import asynccontextmanager +from dataclasses import dataclass +from datetime import timedelta +from typing import Any, overload +from warnings import warn + +import anyio +import httpx +from anyio.abc import TaskGroup +from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream +from httpx_sse import EventSource, ServerSentEvent, aconnect_sse +from typing_extensions import deprecated + +from mcp.shared._httpx_utils import ( + McpHttpClientFactory, + create_mcp_http_client, +) +from mcp.shared.message import ClientMessageMetadata, SessionMessage +from mcp.types import ( + ErrorData, + InitializeResult, + JSONRPCError, + JSONRPCMessage, + JSONRPCNotification, + JSONRPCRequest, + JSONRPCResponse, + RequestId, +) + +logger = logging.getLogger(__name__) + + +SessionMessageOrError = SessionMessage | Exception +StreamWriter = MemoryObjectSendStream[SessionMessageOrError] +StreamReader = MemoryObjectReceiveStream[SessionMessage] +GetSessionIdCallback = Callable[[], str | None] + +MCP_SESSION_ID = "mcp-session-id" +MCP_PROTOCOL_VERSION = "mcp-protocol-version" +LAST_EVENT_ID = "last-event-id" + +# Reconnection defaults +DEFAULT_RECONNECTION_DELAY_MS = 1000 # 1 second fallback when server doesn't provide retry +MAX_RECONNECTION_ATTEMPTS = 2 # Max retry attempts before giving up +CONTENT_TYPE = "content-type" +ACCEPT = "accept" + + +JSON = "application/json" +SSE = "text/event-stream" + +# Sentinel value for detecting unset optional parameters +_UNSET = object() + + +class StreamableHTTPError(Exception): + """Base exception for StreamableHTTP transport errors.""" + + +class ResumptionError(StreamableHTTPError): + """Raised when resumption request is invalid.""" + + +@dataclass +class RequestContext: + """Context for a request operation.""" + + client: httpx.AsyncClient + session_id: str | None + session_message: SessionMessage + metadata: ClientMessageMetadata | None + read_stream_writer: StreamWriter + headers: dict[str, str] | None = None # Deprecated - no longer used + sse_read_timeout: float | None = None # Deprecated - no longer used + + +class StreamableHTTPTransport: + """StreamableHTTP client transport implementation.""" + + @overload + def __init__(self, url: str) -> None: ... + + @overload + @deprecated( + "Parameters headers, timeout, sse_read_timeout, and auth are deprecated. " + "Configure these on the httpx.AsyncClient instead." + ) + def __init__( + self, + url: str, + headers: dict[str, str] | None = None, + timeout: float | timedelta = 30, + sse_read_timeout: float | timedelta = 60 * 5, + auth: httpx.Auth | None = None, + ) -> None: ... + + def __init__( + self, + url: str, + headers: Any = _UNSET, + timeout: Any = _UNSET, + sse_read_timeout: Any = _UNSET, + auth: Any = _UNSET, + ) -> None: + """Initialize the StreamableHTTP transport. + + Args: + url: The endpoint URL. + headers: Optional headers to include in requests. + timeout: HTTP timeout for regular operations. + sse_read_timeout: Timeout for SSE read operations. + auth: Optional HTTPX authentication handler. + """ + # Check for deprecated parameters and issue runtime warning + deprecated_params: list[str] = [] + if headers is not _UNSET: + deprecated_params.append("headers") + if timeout is not _UNSET: + deprecated_params.append("timeout") + if sse_read_timeout is not _UNSET: + deprecated_params.append("sse_read_timeout") + if auth is not _UNSET: + deprecated_params.append("auth") + + if deprecated_params: + warn( + f"Parameters {', '.join(deprecated_params)} are deprecated and will be ignored. " + "Configure these on the httpx.AsyncClient instead.", + DeprecationWarning, + stacklevel=2, + ) + + self.url = url + self.session_id = None + self.protocol_version = None + + def _prepare_headers(self) -> dict[str, str]: + """Build MCP-specific request headers. + + These headers will be merged with the httpx.AsyncClient's default headers, + with these MCP-specific headers taking precedence. + """ + headers: dict[str, str] = {} + # Add MCP protocol headers + headers[ACCEPT] = f"{JSON}, {SSE}" + headers[CONTENT_TYPE] = JSON + # Add session headers if available + if self.session_id: + headers[MCP_SESSION_ID] = self.session_id + if self.protocol_version: + headers[MCP_PROTOCOL_VERSION] = self.protocol_version + return headers + + def _is_initialization_request(self, message: JSONRPCMessage) -> bool: + """Check if the message is an initialization request.""" + return isinstance(message.root, JSONRPCRequest) and message.root.method == "initialize" + + def _is_initialized_notification(self, message: JSONRPCMessage) -> bool: + """Check if the message is an initialized notification.""" + return isinstance(message.root, JSONRPCNotification) and message.root.method == "notifications/initialized" + + def _maybe_extract_session_id_from_response( + self, + response: httpx.Response, + ) -> None: + """Extract and store session ID from response headers.""" + new_session_id = response.headers.get(MCP_SESSION_ID) + if new_session_id: + self.session_id = new_session_id + logger.info(f"Received session ID: {self.session_id}") + + def _maybe_extract_protocol_version_from_message( + self, + message: JSONRPCMessage, + ) -> None: + """Extract protocol version from initialization response message.""" + if isinstance(message.root, JSONRPCResponse) and message.root.result: # pragma: no branch + try: + # Parse the result as InitializeResult for type safety + init_result = InitializeResult.model_validate(message.root.result) + self.protocol_version = str(init_result.protocolVersion) + logger.info(f"Negotiated protocol version: {self.protocol_version}") + except Exception as exc: # pragma: no cover + logger.warning( + f"Failed to parse initialization response as InitializeResult: {exc}" + ) # pragma: no cover + logger.warning(f"Raw result: {message.root.result}") + + async def _handle_sse_event( + self, + sse: ServerSentEvent, + read_stream_writer: StreamWriter, + original_request_id: RequestId | None = None, + resumption_callback: Callable[[str], Awaitable[None]] | None = None, + is_initialization: bool = False, + ) -> bool: + """Handle an SSE event, returning True if the response is complete.""" + if sse.event == "message": + # Handle priming events (empty data with ID) for resumability + if not sse.data: + # Call resumption callback for priming events that have an ID + if sse.id and resumption_callback: + await resumption_callback(sse.id) + return False + try: + message = JSONRPCMessage.model_validate_json(sse.data) + logger.debug(f"SSE message: {message}") + + # Extract protocol version from initialization response + if is_initialization: + self._maybe_extract_protocol_version_from_message(message) + + # If this is a response and we have original_request_id, replace it + if original_request_id is not None and isinstance(message.root, JSONRPCResponse | JSONRPCError): + message.root.id = original_request_id + + session_message = SessionMessage(message) + await read_stream_writer.send(session_message) + + # Call resumption token callback if we have an ID + if sse.id and resumption_callback: + await resumption_callback(sse.id) + + # If this is a response or error return True indicating completion + # Otherwise, return False to continue listening + return isinstance(message.root, JSONRPCResponse | JSONRPCError) + + except Exception as exc: # pragma: no cover + logger.exception("Error parsing SSE message") + await read_stream_writer.send(exc) + return False + else: # pragma: no cover + logger.warning(f"Unknown SSE event: {sse.event}") + return False + + async def handle_get_stream( + self, + client: httpx.AsyncClient, + read_stream_writer: StreamWriter, + ) -> None: + """Handle GET stream for server-initiated messages with auto-reconnect.""" + last_event_id: str | None = None + retry_interval_ms: int | None = None + attempt: int = 0 + + while attempt < MAX_RECONNECTION_ATTEMPTS: # pragma: no branch + try: + if not self.session_id: + return + + headers = self._prepare_headers() + if last_event_id: + headers[LAST_EVENT_ID] = last_event_id # pragma: no cover + + async with aconnect_sse( + client, + "GET", + self.url, + headers=headers, + ) as event_source: + event_source.response.raise_for_status() + logger.debug("GET SSE connection established") + + async for sse in event_source.aiter_sse(): + # Track last event ID for reconnection + if sse.id: + last_event_id = sse.id # pragma: no cover + # Track retry interval from server + if sse.retry is not None: + retry_interval_ms = sse.retry # pragma: no cover + + await self._handle_sse_event(sse, read_stream_writer) + + # Stream ended normally (server closed) - reset attempt counter + attempt = 0 + + except Exception as exc: # pragma: no cover + logger.debug(f"GET stream error: {exc}") + attempt += 1 + + if attempt >= MAX_RECONNECTION_ATTEMPTS: # pragma: no cover + logger.debug(f"GET stream max reconnection attempts ({MAX_RECONNECTION_ATTEMPTS}) exceeded") + return + + # Wait before reconnecting + delay_ms = retry_interval_ms if retry_interval_ms is not None else DEFAULT_RECONNECTION_DELAY_MS + logger.info(f"GET stream disconnected, reconnecting in {delay_ms}ms...") + await anyio.sleep(delay_ms / 1000.0) + + async def _handle_resumption_request(self, ctx: RequestContext) -> None: + """Handle a resumption request using GET with SSE.""" + headers = self._prepare_headers() + if ctx.metadata and ctx.metadata.resumption_token: + headers[LAST_EVENT_ID] = ctx.metadata.resumption_token + else: + raise ResumptionError("Resumption request requires a resumption token") # pragma: no cover + + # Extract original request ID to map responses + original_request_id = None + if isinstance(ctx.session_message.message.root, JSONRPCRequest): # pragma: no branch + original_request_id = ctx.session_message.message.root.id + + async with aconnect_sse( + ctx.client, + "GET", + self.url, + headers=headers, + ) as event_source: + event_source.response.raise_for_status() + logger.debug("Resumption GET SSE connection established") + + async for sse in event_source.aiter_sse(): # pragma: no branch + is_complete = await self._handle_sse_event( + sse, + ctx.read_stream_writer, + original_request_id, + ctx.metadata.on_resumption_token_update if ctx.metadata else None, + ) + if is_complete: + await event_source.response.aclose() + break + + async def _handle_post_request(self, ctx: RequestContext) -> None: + """Handle a POST request with response processing.""" + headers = self._prepare_headers() + message = ctx.session_message.message + is_initialization = self._is_initialization_request(message) + + async with ctx.client.stream( + "POST", + self.url, + json=message.model_dump(by_alias=True, mode="json", exclude_none=True), + headers=headers, + ) as response: + if response.status_code == 202: + logger.debug("Received 202 Accepted") + return + + if response.status_code == 404: # pragma: no branch + if isinstance(message.root, JSONRPCRequest): + await self._send_session_terminated_error( # pragma: no cover + ctx.read_stream_writer, # pragma: no cover + message.root.id, # pragma: no cover + ) # pragma: no cover + return # pragma: no cover + + response.raise_for_status() + if is_initialization: + self._maybe_extract_session_id_from_response(response) + + # Per https://modelcontextprotocol.io/specification/2025-06-18/basic#notifications: + # The server MUST NOT send a response to notifications. + if isinstance(message.root, JSONRPCRequest): + content_type = response.headers.get(CONTENT_TYPE, "").lower() + if content_type.startswith(JSON): + await self._handle_json_response(response, ctx.read_stream_writer, is_initialization) + elif content_type.startswith(SSE): + await self._handle_sse_response(response, ctx, is_initialization) + else: + await self._handle_unexpected_content_type( # pragma: no cover + content_type, # pragma: no cover + ctx.read_stream_writer, # pragma: no cover + ) # pragma: no cover + + async def _handle_json_response( + self, + response: httpx.Response, + read_stream_writer: StreamWriter, + is_initialization: bool = False, + ) -> None: + """Handle JSON response from the server.""" + try: + content = await response.aread() + message = JSONRPCMessage.model_validate_json(content) + + # Extract protocol version from initialization response + if is_initialization: + self._maybe_extract_protocol_version_from_message(message) + + session_message = SessionMessage(message) + await read_stream_writer.send(session_message) + except Exception as exc: # pragma: no cover + logger.exception("Error parsing JSON response") + await read_stream_writer.send(exc) + + async def _handle_sse_response( + self, + response: httpx.Response, + ctx: RequestContext, + is_initialization: bool = False, + ) -> None: + """Handle SSE response from the server.""" + last_event_id: str | None = None + retry_interval_ms: int | None = None + + try: + event_source = EventSource(response) + async for sse in event_source.aiter_sse(): # pragma: no branch + # Track last event ID for potential reconnection + if sse.id: + last_event_id = sse.id + + # Track retry interval from server + if sse.retry is not None: + retry_interval_ms = sse.retry + + is_complete = await self._handle_sse_event( + sse, + ctx.read_stream_writer, + resumption_callback=(ctx.metadata.on_resumption_token_update if ctx.metadata else None), + is_initialization=is_initialization, + ) + # If the SSE event indicates completion, like returning respose/error + # break the loop + if is_complete: + await response.aclose() + return # Normal completion, no reconnect needed + except Exception as e: # pragma: no cover + logger.debug(f"SSE stream ended: {e}") + + # Stream ended without response - reconnect if we received an event with ID + if last_event_id is not None: # pragma: no branch + logger.info("SSE stream disconnected, reconnecting...") + await self._handle_reconnection(ctx, last_event_id, retry_interval_ms) + + async def _handle_reconnection( + self, + ctx: RequestContext, + last_event_id: str, + retry_interval_ms: int | None = None, + attempt: int = 0, + ) -> None: + """Reconnect with Last-Event-ID to resume stream after server disconnect.""" + # Bail if max retries exceeded + if attempt >= MAX_RECONNECTION_ATTEMPTS: # pragma: no cover + logger.debug(f"Max reconnection attempts ({MAX_RECONNECTION_ATTEMPTS}) exceeded") + return + + # Always wait - use server value or default + delay_ms = retry_interval_ms if retry_interval_ms is not None else DEFAULT_RECONNECTION_DELAY_MS + await anyio.sleep(delay_ms / 1000.0) + + headers = self._prepare_headers() + headers[LAST_EVENT_ID] = last_event_id + + # Extract original request ID to map responses + original_request_id = None + if isinstance(ctx.session_message.message.root, JSONRPCRequest): # pragma: no branch + original_request_id = ctx.session_message.message.root.id + + try: + async with aconnect_sse( + ctx.client, + "GET", + self.url, + headers=headers, + ) as event_source: + event_source.response.raise_for_status() + logger.info("Reconnected to SSE stream") + + # Track for potential further reconnection + reconnect_last_event_id: str = last_event_id + reconnect_retry_ms = retry_interval_ms + + async for sse in event_source.aiter_sse(): + if sse.id: # pragma: no branch + reconnect_last_event_id = sse.id + if sse.retry is not None: + reconnect_retry_ms = sse.retry + + is_complete = await self._handle_sse_event( + sse, + ctx.read_stream_writer, + original_request_id, + ctx.metadata.on_resumption_token_update if ctx.metadata else None, + ) + if is_complete: + await event_source.response.aclose() + return + + # Stream ended again without response - reconnect again (reset attempt counter) + logger.info("SSE stream disconnected, reconnecting...") + await self._handle_reconnection(ctx, reconnect_last_event_id, reconnect_retry_ms, 0) + except Exception as e: # pragma: no cover + logger.debug(f"Reconnection failed: {e}") + # Try to reconnect again if we still have an event ID + await self._handle_reconnection(ctx, last_event_id, retry_interval_ms, attempt + 1) + + async def _handle_unexpected_content_type( + self, + content_type: str, + read_stream_writer: StreamWriter, + ) -> None: # pragma: no cover + """Handle unexpected content type in response.""" + error_msg = f"Unexpected content type: {content_type}" # pragma: no cover + logger.error(error_msg) # pragma: no cover + await read_stream_writer.send(ValueError(error_msg)) # pragma: no cover + + async def _send_session_terminated_error( + self, + read_stream_writer: StreamWriter, + request_id: RequestId, + ) -> None: + """Send a session terminated error response.""" + jsonrpc_error = JSONRPCError( + jsonrpc="2.0", + id=request_id, + error=ErrorData(code=32600, message="Session terminated"), + ) + session_message = SessionMessage(JSONRPCMessage(jsonrpc_error)) + await read_stream_writer.send(session_message) + + async def post_writer( + self, + client: httpx.AsyncClient, + write_stream_reader: StreamReader, + read_stream_writer: StreamWriter, + write_stream: MemoryObjectSendStream[SessionMessage], + start_get_stream: Callable[[], None], + tg: TaskGroup, + ) -> None: + """Handle writing requests to the server.""" + try: + async with write_stream_reader: + async for session_message in write_stream_reader: + message = session_message.message + metadata = ( + session_message.metadata + if isinstance(session_message.metadata, ClientMessageMetadata) + else None + ) + + # Check if this is a resumption request + is_resumption = bool(metadata and metadata.resumption_token) + + logger.debug(f"Sending client message: {message}") + + # Handle initialized notification + if self._is_initialized_notification(message): + start_get_stream() + + ctx = RequestContext( + client=client, + session_id=self.session_id, + session_message=session_message, + metadata=metadata, + read_stream_writer=read_stream_writer, + ) + + async def handle_request_async(): + if is_resumption: + await self._handle_resumption_request(ctx) + else: + await self._handle_post_request(ctx) + + # If this is a request, start a new task to handle it + if isinstance(message.root, JSONRPCRequest): + tg.start_soon(handle_request_async) + else: + await handle_request_async() + + except Exception: + logger.exception("Error in post_writer") # pragma: no cover + finally: + await read_stream_writer.aclose() + await write_stream.aclose() + + async def terminate_session(self, client: httpx.AsyncClient) -> None: # pragma: no cover + """Terminate the session by sending a DELETE request.""" + if not self.session_id: + return + + try: + headers = self._prepare_headers() + response = await client.delete(self.url, headers=headers) + + if response.status_code == 405: + logger.debug("Server does not allow session termination") + elif response.status_code not in (200, 204): + logger.warning(f"Session termination failed: {response.status_code}") + except Exception as exc: + logger.warning(f"Session termination failed: {exc}") + + def get_session_id(self) -> str | None: + """Get the current session ID.""" + return self.session_id + + +@asynccontextmanager +async def streamable_http_client( + url: str, + *, + http_client: httpx.AsyncClient | None = None, + terminate_on_close: bool = True, +) -> AsyncGenerator[ + tuple[ + MemoryObjectReceiveStream[SessionMessage | Exception], + MemoryObjectSendStream[SessionMessage], + GetSessionIdCallback, + ], + None, +]: + """ + Client transport for StreamableHTTP. + + Args: + url: The MCP server endpoint URL. + http_client: Optional pre-configured httpx.AsyncClient. If None, a default + client with recommended MCP timeouts will be created. To configure headers, + authentication, or other HTTP settings, create an httpx.AsyncClient and pass it here. + terminate_on_close: If True, send a DELETE request to terminate the session + when the context exits. + + Yields: + Tuple containing: + - read_stream: Stream for reading messages from the server + - write_stream: Stream for sending messages to the server + - get_session_id_callback: Function to retrieve the current session ID + + Example: + See examples/snippets/clients/ for usage patterns. + """ + read_stream_writer, read_stream = anyio.create_memory_object_stream[SessionMessage | Exception](0) + write_stream, write_stream_reader = anyio.create_memory_object_stream[SessionMessage](0) + + # Determine if we need to create and manage the client + client_provided = http_client is not None + client = http_client + + if client is None: + # Create default client with recommended MCP timeouts + client = create_mcp_http_client() + + transport = StreamableHTTPTransport(url) + + async with anyio.create_task_group() as tg: + try: + logger.debug(f"Connecting to StreamableHTTP endpoint: {url}") + + async with contextlib.AsyncExitStack() as stack: + # Only manage client lifecycle if we created it + if not client_provided: + await stack.enter_async_context(client) + + def start_get_stream() -> None: + tg.start_soon(transport.handle_get_stream, client, read_stream_writer) + + tg.start_soon( + transport.post_writer, + client, + write_stream_reader, + read_stream_writer, + write_stream, + start_get_stream, + tg, + ) + + try: + yield ( + read_stream, + write_stream, + transport.get_session_id, + ) + finally: + if transport.session_id and terminate_on_close: + await transport.terminate_session(client) + tg.cancel_scope.cancel() + finally: + await read_stream_writer.aclose() + await write_stream.aclose() + + +@asynccontextmanager +@deprecated("Use `streamable_http_client` instead.") +async def streamablehttp_client( + url: str, + headers: dict[str, str] | None = None, + timeout: float | timedelta = 30, + sse_read_timeout: float | timedelta = 60 * 5, + terminate_on_close: bool = True, + httpx_client_factory: McpHttpClientFactory = create_mcp_http_client, + auth: httpx.Auth | None = None, +) -> AsyncGenerator[ + tuple[ + MemoryObjectReceiveStream[SessionMessage | Exception], + MemoryObjectSendStream[SessionMessage], + GetSessionIdCallback, + ], + None, +]: + # Convert timeout parameters + timeout_seconds = timeout.total_seconds() if isinstance(timeout, timedelta) else timeout + sse_read_timeout_seconds = ( + sse_read_timeout.total_seconds() if isinstance(sse_read_timeout, timedelta) else sse_read_timeout + ) + + # Create httpx client using the factory with old-style parameters + client = httpx_client_factory( + headers=headers, + timeout=httpx.Timeout(timeout_seconds, read=sse_read_timeout_seconds), + auth=auth, + ) + + # Manage client lifecycle since we created it + async with client: + async with streamable_http_client( + url, + http_client=client, + terminate_on_close=terminate_on_close, + ) as streams: + yield streams diff --git a/source/target-vendors/cp312/mcp/client/websocket.py b/source/target-vendors/cp312/mcp/client/websocket.py new file mode 100644 index 0000000..e8c8d9a --- /dev/null +++ b/source/target-vendors/cp312/mcp/client/websocket.py @@ -0,0 +1,86 @@ +import json +import logging +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager + +import anyio +from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream +from pydantic import ValidationError +from websockets.asyncio.client import connect as ws_connect +from websockets.typing import Subprotocol + +import mcp.types as types +from mcp.shared.message import SessionMessage + +logger = logging.getLogger(__name__) + + +@asynccontextmanager +async def websocket_client( + url: str, +) -> AsyncGenerator[ + tuple[MemoryObjectReceiveStream[SessionMessage | Exception], MemoryObjectSendStream[SessionMessage]], + None, +]: + """ + WebSocket client transport for MCP, symmetrical to the server version. + + Connects to 'url' using the 'mcp' subprotocol, then yields: + (read_stream, write_stream) + + - read_stream: As you read from this stream, you'll receive either valid + JSONRPCMessage objects or Exception objects (when validation fails). + - write_stream: Write JSONRPCMessage objects to this stream to send them + over the WebSocket to the server. + """ + + # Create two in-memory streams: + # - One for incoming messages (read_stream, written by ws_reader) + # - One for outgoing messages (write_stream, read by ws_writer) + read_stream: MemoryObjectReceiveStream[SessionMessage | Exception] + read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception] + write_stream: MemoryObjectSendStream[SessionMessage] + write_stream_reader: MemoryObjectReceiveStream[SessionMessage] + + read_stream_writer, read_stream = anyio.create_memory_object_stream(0) + write_stream, write_stream_reader = anyio.create_memory_object_stream(0) + + # Connect using websockets, requesting the "mcp" subprotocol + async with ws_connect(url, subprotocols=[Subprotocol("mcp")]) as ws: + + async def ws_reader(): + """ + Reads text messages from the WebSocket, parses them as JSON-RPC messages, + and sends them into read_stream_writer. + """ + async with read_stream_writer: + async for raw_text in ws: + try: + message = types.JSONRPCMessage.model_validate_json(raw_text) + session_message = SessionMessage(message) + await read_stream_writer.send(session_message) + except ValidationError as exc: # pragma: no cover + # If JSON parse or model validation fails, send the exception + await read_stream_writer.send(exc) + + async def ws_writer(): + """ + Reads JSON-RPC messages from write_stream_reader and + sends them to the server. + """ + async with write_stream_reader: + async for session_message in write_stream_reader: + # Convert to a dict, then to JSON + msg_dict = session_message.message.model_dump(by_alias=True, mode="json", exclude_none=True) + await ws.send(json.dumps(msg_dict)) + + async with anyio.create_task_group() as tg: + # Start reader and writer tasks + tg.start_soon(ws_reader) + tg.start_soon(ws_writer) + + # Yield the receive/send streams + yield (read_stream, write_stream) + + # Once the caller's 'async with' block exits, we shut down + tg.cancel_scope.cancel() diff --git a/source/target-vendors/cp312/mcp/os/__init__.py b/source/target-vendors/cp312/mcp/os/__init__.py new file mode 100644 index 0000000..fa5dbc8 --- /dev/null +++ b/source/target-vendors/cp312/mcp/os/__init__.py @@ -0,0 +1 @@ +"""Platform-specific utilities for MCP.""" diff --git a/source/target-vendors/cp312/mcp/os/posix/__init__.py b/source/target-vendors/cp312/mcp/os/posix/__init__.py new file mode 100644 index 0000000..23aff8b --- /dev/null +++ b/source/target-vendors/cp312/mcp/os/posix/__init__.py @@ -0,0 +1 @@ +"""POSIX-specific utilities for MCP.""" diff --git a/source/target-vendors/cp312/mcp/os/posix/utilities.py b/source/target-vendors/cp312/mcp/os/posix/utilities.py new file mode 100644 index 0000000..dd1aea3 --- /dev/null +++ b/source/target-vendors/cp312/mcp/os/posix/utilities.py @@ -0,0 +1,60 @@ +""" +POSIX-specific functionality for stdio client operations. +""" + +import logging +import os +import signal + +import anyio +from anyio.abc import Process + +logger = logging.getLogger(__name__) + + +async def terminate_posix_process_tree(process: Process, timeout_seconds: float = 2.0) -> None: + """ + Terminate a process and all its children on POSIX systems. + + Uses os.killpg() for atomic process group termination. + + Args: + process: The process to terminate + timeout_seconds: Timeout in seconds before force killing (default: 2.0) + """ + pid = getattr(process, "pid", None) or getattr(getattr(process, "popen", None), "pid", None) + if not pid: + # No PID means there's no process to terminate - it either never started, + # already exited, or we have an invalid process object + return + + try: + pgid = os.getpgid(pid) + os.killpg(pgid, signal.SIGTERM) + + with anyio.move_on_after(timeout_seconds): + while True: + try: + # Check if process group still exists (signal 0 = check only) + os.killpg(pgid, 0) + await anyio.sleep(0.1) + except ProcessLookupError: + return + + try: + os.killpg(pgid, signal.SIGKILL) + except ProcessLookupError: + pass + + except (ProcessLookupError, PermissionError, OSError) as e: + logger.warning(f"Process group termination failed for PID {pid}: {e}, falling back to simple terminate") + try: + process.terminate() + with anyio.fail_after(timeout_seconds): + await process.wait() + except Exception: + logger.warning(f"Process termination failed for PID {pid}, attempting force kill") + try: + process.kill() + except Exception: + logger.exception(f"Failed to kill process {pid}") diff --git a/source/target-vendors/cp312/mcp/os/win32/__init__.py b/source/target-vendors/cp312/mcp/os/win32/__init__.py new file mode 100644 index 0000000..f1ebab9 --- /dev/null +++ b/source/target-vendors/cp312/mcp/os/win32/__init__.py @@ -0,0 +1 @@ +"""Windows-specific utilities for MCP.""" diff --git a/source/target-vendors/cp312/mcp/os/win32/utilities.py b/source/target-vendors/cp312/mcp/os/win32/utilities.py new file mode 100644 index 0000000..962be02 --- /dev/null +++ b/source/target-vendors/cp312/mcp/os/win32/utilities.py @@ -0,0 +1,338 @@ +""" +Windows-specific functionality for stdio client operations. +""" + +import logging +import shutil +import subprocess +import sys +from pathlib import Path +from typing import BinaryIO, TextIO, cast + +import anyio +from anyio import to_thread +from anyio.abc import Process +from anyio.streams.file import FileReadStream, FileWriteStream +from typing_extensions import deprecated + +logger = logging.getLogger("client.stdio.win32") + +# Windows-specific imports for Job Objects +if sys.platform == "win32": + import pywintypes + import win32api + import win32con + import win32job +else: + # Type stubs for non-Windows platforms + win32api = None + win32con = None + win32job = None + pywintypes = None + +JobHandle = int + + +def get_windows_executable_command(command: str) -> str: + """ + Get the correct executable command normalized for Windows. + + On Windows, commands might exist with specific extensions (.exe, .cmd, etc.) + that need to be located for proper execution. + + Args: + command: Base command (e.g., 'uvx', 'npx') + + Returns: + str: Windows-appropriate command path + """ + try: + # First check if command exists in PATH as-is + if command_path := shutil.which(command): + return command_path + + # Check for Windows-specific extensions + for ext in [".cmd", ".bat", ".exe", ".ps1"]: + ext_version = f"{command}{ext}" + if ext_path := shutil.which(ext_version): + return ext_path + + # For regular commands or if we couldn't find special versions + return command + except OSError: + # Handle file system errors during path resolution + # (permissions, broken symlinks, etc.) + return command + + +class FallbackProcess: + """ + A fallback process wrapper for Windows to handle async I/O + when using subprocess.Popen, which provides sync-only FileIO objects. + + This wraps stdin and stdout into async-compatible + streams (FileReadStream, FileWriteStream), + so that MCP clients expecting async streams can work properly. + """ + + def __init__(self, popen_obj: subprocess.Popen[bytes]): + self.popen: subprocess.Popen[bytes] = popen_obj + self.stdin_raw = popen_obj.stdin # type: ignore[assignment] + self.stdout_raw = popen_obj.stdout # type: ignore[assignment] + self.stderr = popen_obj.stderr # type: ignore[assignment] + + self.stdin = FileWriteStream(cast(BinaryIO, self.stdin_raw)) if self.stdin_raw else None + self.stdout = FileReadStream(cast(BinaryIO, self.stdout_raw)) if self.stdout_raw else None + + async def __aenter__(self): + """Support async context manager entry.""" + return self + + async def __aexit__( + self, + exc_type: BaseException | None, + exc_val: BaseException | None, + exc_tb: object | None, + ) -> None: + """Terminate and wait on process exit inside a thread.""" + self.popen.terminate() + await to_thread.run_sync(self.popen.wait) + + # Close the file handles to prevent ResourceWarning + if self.stdin: + await self.stdin.aclose() + if self.stdout: + await self.stdout.aclose() + if self.stdin_raw: + self.stdin_raw.close() + if self.stdout_raw: + self.stdout_raw.close() + if self.stderr: + self.stderr.close() + + async def wait(self): + """Async wait for process completion.""" + return await to_thread.run_sync(self.popen.wait) + + def terminate(self): + """Terminate the subprocess immediately.""" + return self.popen.terminate() + + def kill(self) -> None: + """Kill the subprocess immediately (alias for terminate).""" + self.terminate() + + @property + def pid(self) -> int: + """Return the process ID.""" + return self.popen.pid + + +# ------------------------ +# Updated function +# ------------------------ + + +async def create_windows_process( + command: str, + args: list[str], + env: dict[str, str] | None = None, + errlog: TextIO | None = sys.stderr, + cwd: Path | str | None = None, +) -> Process | FallbackProcess: + """ + Creates a subprocess in a Windows-compatible way with Job Object support. + + Attempt to use anyio's open_process for async subprocess creation. + In some cases this will throw NotImplementedError on Windows, e.g. + when using the SelectorEventLoop which does not support async subprocesses. + In that case, we fall back to using subprocess.Popen. + + The process is automatically added to a Job Object to ensure all child + processes are terminated when the parent is terminated. + + Args: + command (str): The executable to run + args (list[str]): List of command line arguments + env (dict[str, str] | None): Environment variables + errlog (TextIO | None): Where to send stderr output (defaults to sys.stderr) + cwd (Path | str | None): Working directory for the subprocess + + Returns: + Process | FallbackProcess: Async-compatible subprocess with stdin and stdout streams + """ + job = _create_job_object() + process = None + + try: + # First try using anyio with Windows-specific flags to hide console window + process = await anyio.open_process( + [command, *args], + env=env, + # Ensure we don't create console windows for each process + creationflags=subprocess.CREATE_NO_WINDOW # type: ignore + if hasattr(subprocess, "CREATE_NO_WINDOW") + else 0, + stderr=errlog, + cwd=cwd, + ) + except NotImplementedError: + # If Windows doesn't support async subprocess creation, use fallback + process = await _create_windows_fallback_process(command, args, env, errlog, cwd) + except Exception: + # Try again without creation flags + process = await anyio.open_process( + [command, *args], + env=env, + stderr=errlog, + cwd=cwd, + ) + + _maybe_assign_process_to_job(process, job) + return process + + +async def _create_windows_fallback_process( + command: str, + args: list[str], + env: dict[str, str] | None = None, + errlog: TextIO | None = sys.stderr, + cwd: Path | str | None = None, +) -> FallbackProcess: + """ + Create a subprocess using subprocess.Popen as a fallback when anyio fails. + + This function wraps the sync subprocess.Popen in an async-compatible interface. + """ + try: + # Try launching with creationflags to avoid opening a new console window + popen_obj = subprocess.Popen( + [command, *args], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=errlog, + env=env, + cwd=cwd, + bufsize=0, # Unbuffered output + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + except Exception: + # If creationflags failed, fallback without them + popen_obj = subprocess.Popen( + [command, *args], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=errlog, + env=env, + cwd=cwd, + bufsize=0, + ) + return FallbackProcess(popen_obj) + + +def _create_job_object() -> int | None: + """ + Create a Windows Job Object configured to terminate all processes when closed. + """ + if sys.platform != "win32" or not win32job: + return None + + try: + job = win32job.CreateJobObject(None, "") + extended_info = win32job.QueryInformationJobObject(job, win32job.JobObjectExtendedLimitInformation) + + extended_info["BasicLimitInformation"]["LimitFlags"] |= win32job.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE + win32job.SetInformationJobObject(job, win32job.JobObjectExtendedLimitInformation, extended_info) + return job + except Exception as e: + logger.warning(f"Failed to create Job Object for process tree management: {e}") + return None + + +def _maybe_assign_process_to_job(process: Process | FallbackProcess, job: JobHandle | None) -> None: + """ + Try to assign a process to a job object. If assignment fails + for any reason, the job handle is closed. + """ + if not job: + return + + if sys.platform != "win32" or not win32api or not win32con or not win32job: + return + + try: + process_handle = win32api.OpenProcess( + win32con.PROCESS_SET_QUOTA | win32con.PROCESS_TERMINATE, False, process.pid + ) + if not process_handle: + raise Exception("Failed to open process handle") + + try: + win32job.AssignProcessToJobObject(job, process_handle) + process._job_object = job + finally: + win32api.CloseHandle(process_handle) + except Exception as e: + logger.warning(f"Failed to assign process {process.pid} to Job Object: {e}") + if win32api: + win32api.CloseHandle(job) + + +async def terminate_windows_process_tree(process: Process | FallbackProcess, timeout_seconds: float = 2.0) -> None: + """ + Terminate a process and all its children on Windows. + + If the process has an associated job object, it will be terminated. + Otherwise, falls back to basic process termination. + + Args: + process: The process to terminate + timeout_seconds: Timeout in seconds before force killing (default: 2.0) + """ + if sys.platform != "win32": + return + + job = getattr(process, "_job_object", None) + if job and win32job: + try: + win32job.TerminateJobObject(job, 1) + except Exception: + # Job might already be terminated + pass + finally: + if win32api: + try: + win32api.CloseHandle(job) + except Exception: + pass + + # Always try to terminate the process itself as well + try: + process.terminate() + except Exception: + pass + + +@deprecated( + "terminate_windows_process is deprecated and will be removed in a future version. " + "Process termination is now handled internally by the stdio_client context manager." +) +async def terminate_windows_process(process: Process | FallbackProcess): + """ + Terminate a Windows process. + + Note: On Windows, terminating a process with process.terminate() doesn't + always guarantee immediate process termination. + So we give it 2s to exit, or we call process.kill() + which sends a SIGKILL equivalent signal. + + Args: + process: The process to terminate + """ + try: + process.terminate() + with anyio.fail_after(2.0): + await process.wait() + except TimeoutError: + # Force kill if it doesn't terminate + process.kill() diff --git a/source/target-vendors/cp312/mcp/py.typed b/source/target-vendors/cp312/mcp/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/mcp/server/__init__.py b/source/target-vendors/cp312/mcp/server/__init__.py new file mode 100644 index 0000000..0feed36 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/__init__.py @@ -0,0 +1,5 @@ +from .fastmcp import FastMCP +from .lowlevel import NotificationOptions, Server +from .models import InitializationOptions + +__all__ = ["Server", "FastMCP", "NotificationOptions", "InitializationOptions"] diff --git a/source/target-vendors/cp312/mcp/server/__main__.py b/source/target-vendors/cp312/mcp/server/__main__.py new file mode 100644 index 0000000..1970eca --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/__main__.py @@ -0,0 +1,50 @@ +import importlib.metadata +import logging +import sys + +import anyio + +from mcp.server.models import InitializationOptions +from mcp.server.session import ServerSession +from mcp.server.stdio import stdio_server +from mcp.types import ServerCapabilities + +if not sys.warnoptions: + import warnings + + warnings.simplefilter("ignore") + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("server") + + +async def receive_loop(session: ServerSession): + logger.info("Starting receive loop") + async for message in session.incoming_messages: + if isinstance(message, Exception): + logger.error("Error: %s", message) + continue + + logger.info("Received message from client: %s", message) + + +async def main(): + version = importlib.metadata.version("mcp") + async with stdio_server() as (read_stream, write_stream): + async with ( + ServerSession( + read_stream, + write_stream, + InitializationOptions( + server_name="mcp", + server_version=version, + capabilities=ServerCapabilities(), + ), + ) as session, + write_stream, + ): + await receive_loop(session) + + +if __name__ == "__main__": + anyio.run(main, backend="trio") diff --git a/source/target-vendors/cp312/mcp/server/auth/__init__.py b/source/target-vendors/cp312/mcp/server/auth/__init__.py new file mode 100644 index 0000000..6888ffe --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/auth/__init__.py @@ -0,0 +1,3 @@ +""" +MCP OAuth server authorization components. +""" diff --git a/source/target-vendors/cp312/mcp/server/auth/errors.py b/source/target-vendors/cp312/mcp/server/auth/errors.py new file mode 100644 index 0000000..117deea --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/auth/errors.py @@ -0,0 +1,5 @@ +from pydantic import ValidationError + + +def stringify_pydantic_error(validation_error: ValidationError) -> str: + return "\n".join(f"{'.'.join(str(loc) for loc in e['loc'])}: {e['msg']}" for e in validation_error.errors()) diff --git a/source/target-vendors/cp312/mcp/server/auth/handlers/__init__.py b/source/target-vendors/cp312/mcp/server/auth/handlers/__init__.py new file mode 100644 index 0000000..e99a62d --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/auth/handlers/__init__.py @@ -0,0 +1,3 @@ +""" +Request handlers for MCP authorization endpoints. +""" diff --git a/source/target-vendors/cp312/mcp/server/auth/handlers/authorize.py b/source/target-vendors/cp312/mcp/server/auth/handlers/authorize.py new file mode 100644 index 0000000..3570d28 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/auth/handlers/authorize.py @@ -0,0 +1,224 @@ +import logging +from dataclasses import dataclass +from typing import Any, Literal + +from pydantic import AnyUrl, BaseModel, Field, RootModel, ValidationError +from starlette.datastructures import FormData, QueryParams +from starlette.requests import Request +from starlette.responses import RedirectResponse, Response + +from mcp.server.auth.errors import stringify_pydantic_error +from mcp.server.auth.json_response import PydanticJSONResponse +from mcp.server.auth.provider import ( + AuthorizationErrorCode, + AuthorizationParams, + AuthorizeError, + OAuthAuthorizationServerProvider, + construct_redirect_uri, +) +from mcp.shared.auth import InvalidRedirectUriError, InvalidScopeError + +logger = logging.getLogger(__name__) + + +class AuthorizationRequest(BaseModel): + # See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1 + client_id: str = Field(..., description="The client ID") + redirect_uri: AnyUrl | None = Field(None, description="URL to redirect to after authorization") + + # see OAuthClientMetadata; we only support `code` + response_type: Literal["code"] = Field(..., description="Must be 'code' for authorization code flow") + code_challenge: str = Field(..., description="PKCE code challenge") + code_challenge_method: Literal["S256"] = Field("S256", description="PKCE code challenge method, must be S256") + state: str | None = Field(None, description="Optional state parameter") + scope: str | None = Field( + None, + description="Optional scope; if specified, should be a space-separated list of scope strings", + ) + resource: str | None = Field( + None, + description="RFC 8707 resource indicator - the MCP server this token will be used with", + ) + + +class AuthorizationErrorResponse(BaseModel): + error: AuthorizationErrorCode + error_description: str | None + error_uri: AnyUrl | None = None + # must be set if provided in the request + state: str | None = None + + +def best_effort_extract_string(key: str, params: None | FormData | QueryParams) -> str | None: + if params is None: # pragma: no cover + return None + value = params.get(key) + if isinstance(value, str): + return value + return None + + +class AnyUrlModel(RootModel[AnyUrl]): + root: AnyUrl + + +@dataclass +class AuthorizationHandler: + provider: OAuthAuthorizationServerProvider[Any, Any, Any] + + async def handle(self, request: Request) -> Response: + # implements authorization requests for grant_type=code; + # see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1 + + state = None + redirect_uri = None + client = None + params = None + + async def error_response( + error: AuthorizationErrorCode, + error_description: str | None, + attempt_load_client: bool = True, + ): + # Error responses take two different formats: + # 1. The request has a valid client ID & redirect_uri: we issue a redirect + # back to the redirect_uri with the error response fields as query + # parameters. This allows the client to be notified of the error. + # 2. Otherwise, we return an error response directly to the end user; + # we choose to do so in JSON, but this is left undefined in the + # specification. + # See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1 + # + # This logic is a bit awkward to handle, because the error might be thrown + # very early in request validation, before we've done the usual Pydantic + # validation, loaded the client, etc. To handle this, error_response() + # contains fallback logic which attempts to load the parameters directly + # from the request. + + nonlocal client, redirect_uri, state + if client is None and attempt_load_client: + # make last-ditch attempt to load the client + client_id = best_effort_extract_string("client_id", params) + client = await self.provider.get_client(client_id) if client_id else None + if redirect_uri is None and client: + # make last-ditch effort to load the redirect uri + try: + if params is not None and "redirect_uri" not in params: + raw_redirect_uri = None + else: + raw_redirect_uri = AnyUrlModel.model_validate( + best_effort_extract_string("redirect_uri", params) + ).root + redirect_uri = client.validate_redirect_uri(raw_redirect_uri) + except (ValidationError, InvalidRedirectUriError): + # if the redirect URI is invalid, ignore it & just return the + # initial error + pass + + # the error response MUST contain the state specified by the client, if any + if state is None: # pragma: no cover + # make last-ditch effort to load state + state = best_effort_extract_string("state", params) + + error_resp = AuthorizationErrorResponse( + error=error, + error_description=error_description, + state=state, + ) + + if redirect_uri and client: + return RedirectResponse( + url=construct_redirect_uri(str(redirect_uri), **error_resp.model_dump(exclude_none=True)), + status_code=302, + headers={"Cache-Control": "no-store"}, + ) + else: + return PydanticJSONResponse( + status_code=400, + content=error_resp, + headers={"Cache-Control": "no-store"}, + ) + + try: + # Parse request parameters + if request.method == "GET": + # Convert query_params to dict for pydantic validation + params = request.query_params + else: + # Parse form data for POST requests + params = await request.form() + + # Save state if it exists, even before validation + state = best_effort_extract_string("state", params) + + try: + auth_request = AuthorizationRequest.model_validate(params) + state = auth_request.state # Update with validated state + except ValidationError as validation_error: + error: AuthorizationErrorCode = "invalid_request" + for e in validation_error.errors(): + if e["loc"] == ("response_type",) and e["type"] == "literal_error": + error = "unsupported_response_type" + break + return await error_response(error, stringify_pydantic_error(validation_error)) + + # Get client information + client = await self.provider.get_client( + auth_request.client_id, + ) + if not client: + # For client_id validation errors, return direct error (no redirect) + return await error_response( + error="invalid_request", + error_description=f"Client ID '{auth_request.client_id}' not found", + attempt_load_client=False, + ) + + # Validate redirect_uri against client's registered URIs + try: + redirect_uri = client.validate_redirect_uri(auth_request.redirect_uri) + except InvalidRedirectUriError as validation_error: + # For redirect_uri validation errors, return direct error (no redirect) + return await error_response( + error="invalid_request", + error_description=validation_error.message, + ) + + # Validate scope - for scope errors, we can redirect + try: + scopes = client.validate_scope(auth_request.scope) + except InvalidScopeError as validation_error: + # For scope errors, redirect with error parameters + return await error_response( + error="invalid_scope", + error_description=validation_error.message, + ) + + # Setup authorization parameters + auth_params = AuthorizationParams( + state=state, + scopes=scopes, + code_challenge=auth_request.code_challenge, + redirect_uri=redirect_uri, + redirect_uri_provided_explicitly=auth_request.redirect_uri is not None, + resource=auth_request.resource, # RFC 8707 + ) + + try: + # Let the provider pick the next URI to redirect to + return RedirectResponse( + url=await self.provider.authorize( + client, + auth_params, + ), + status_code=302, + headers={"Cache-Control": "no-store"}, + ) + except AuthorizeError as e: + # Handle authorization errors as defined in RFC 6749 Section 4.1.2.1 + return await error_response(error=e.error, error_description=e.error_description) + + except Exception as validation_error: # pragma: no cover + # Catch-all for unexpected errors + logger.exception("Unexpected error in authorization_handler", exc_info=validation_error) + return await error_response(error="server_error", error_description="An unexpected error occurred") diff --git a/source/target-vendors/cp312/mcp/server/auth/handlers/metadata.py b/source/target-vendors/cp312/mcp/server/auth/handlers/metadata.py new file mode 100644 index 0000000..f126442 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/auth/handlers/metadata.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass + +from starlette.requests import Request +from starlette.responses import Response + +from mcp.server.auth.json_response import PydanticJSONResponse +from mcp.shared.auth import OAuthMetadata, ProtectedResourceMetadata + + +@dataclass +class MetadataHandler: + metadata: OAuthMetadata + + async def handle(self, request: Request) -> Response: + return PydanticJSONResponse( + content=self.metadata, + headers={"Cache-Control": "public, max-age=3600"}, # Cache for 1 hour + ) + + +@dataclass +class ProtectedResourceMetadataHandler: + metadata: ProtectedResourceMetadata + + async def handle(self, request: Request) -> Response: + return PydanticJSONResponse( + content=self.metadata, + headers={"Cache-Control": "public, max-age=3600"}, # Cache for 1 hour + ) diff --git a/source/target-vendors/cp312/mcp/server/auth/handlers/register.py b/source/target-vendors/cp312/mcp/server/auth/handlers/register.py new file mode 100644 index 0000000..c65473d --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/auth/handlers/register.py @@ -0,0 +1,136 @@ +import secrets +import time +from dataclasses import dataclass +from typing import Any +from uuid import uuid4 + +from pydantic import BaseModel, RootModel, ValidationError +from starlette.requests import Request +from starlette.responses import Response + +from mcp.server.auth.errors import stringify_pydantic_error +from mcp.server.auth.json_response import PydanticJSONResponse +from mcp.server.auth.provider import OAuthAuthorizationServerProvider, RegistrationError, RegistrationErrorCode +from mcp.server.auth.settings import ClientRegistrationOptions +from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata + + +class RegistrationRequest(RootModel[OAuthClientMetadata]): + # this wrapper is a no-op; it's just to separate out the types exposed to the + # provider from what we use in the HTTP handler + root: OAuthClientMetadata + + +class RegistrationErrorResponse(BaseModel): + error: RegistrationErrorCode + error_description: str | None + + +@dataclass +class RegistrationHandler: + provider: OAuthAuthorizationServerProvider[Any, Any, Any] + options: ClientRegistrationOptions + + async def handle(self, request: Request) -> Response: + # Implements dynamic client registration as defined in https://datatracker.ietf.org/doc/html/rfc7591#section-3.1 + try: + # Parse request body as JSON + body = await request.json() + client_metadata = OAuthClientMetadata.model_validate(body) + + # Scope validation is handled below + except ValidationError as validation_error: + return PydanticJSONResponse( + content=RegistrationErrorResponse( + error="invalid_client_metadata", + error_description=stringify_pydantic_error(validation_error), + ), + status_code=400, + ) + + client_id = str(uuid4()) + + # If auth method is None, default to client_secret_post + if client_metadata.token_endpoint_auth_method is None: + client_metadata.token_endpoint_auth_method = "client_secret_post" + + client_secret = None + if client_metadata.token_endpoint_auth_method != "none": # pragma: no branch + # cryptographically secure random 32-byte hex string + client_secret = secrets.token_hex(32) + + if client_metadata.scope is None and self.options.default_scopes is not None: + client_metadata.scope = " ".join(self.options.default_scopes) + elif client_metadata.scope is not None and self.options.valid_scopes is not None: + requested_scopes = set(client_metadata.scope.split()) + valid_scopes = set(self.options.valid_scopes) + if not requested_scopes.issubset(valid_scopes): # pragma: no branch + return PydanticJSONResponse( + content=RegistrationErrorResponse( + error="invalid_client_metadata", + error_description="Requested scopes are not valid: " + f"{', '.join(requested_scopes - valid_scopes)}", + ), + status_code=400, + ) + if not {"authorization_code", "refresh_token"}.issubset(set(client_metadata.grant_types)): + return PydanticJSONResponse( + content=RegistrationErrorResponse( + error="invalid_client_metadata", + error_description="grant_types must be authorization_code and refresh_token", + ), + status_code=400, + ) + + # The MCP spec requires servers to use the authorization `code` flow + # with PKCE + if "code" not in client_metadata.response_types: + return PydanticJSONResponse( + content=RegistrationErrorResponse( + error="invalid_client_metadata", + error_description="response_types must include 'code' for authorization_code grant", + ), + status_code=400, + ) + + client_id_issued_at = int(time.time()) + client_secret_expires_at = ( + client_id_issued_at + self.options.client_secret_expiry_seconds + if self.options.client_secret_expiry_seconds is not None + else None + ) + + client_info = OAuthClientInformationFull( + client_id=client_id, + client_id_issued_at=client_id_issued_at, + client_secret=client_secret, + client_secret_expires_at=client_secret_expires_at, + # passthrough information from the client request + redirect_uris=client_metadata.redirect_uris, + token_endpoint_auth_method=client_metadata.token_endpoint_auth_method, + grant_types=client_metadata.grant_types, + response_types=client_metadata.response_types, + client_name=client_metadata.client_name, + client_uri=client_metadata.client_uri, + logo_uri=client_metadata.logo_uri, + scope=client_metadata.scope, + contacts=client_metadata.contacts, + tos_uri=client_metadata.tos_uri, + policy_uri=client_metadata.policy_uri, + jwks_uri=client_metadata.jwks_uri, + jwks=client_metadata.jwks, + software_id=client_metadata.software_id, + software_version=client_metadata.software_version, + ) + try: + # Register client + await self.provider.register_client(client_info) + + # Return client information + return PydanticJSONResponse(content=client_info, status_code=201) + except RegistrationError as e: + # Handle registration errors as defined in RFC 7591 Section 3.2.2 + return PydanticJSONResponse( + content=RegistrationErrorResponse(error=e.error, error_description=e.error_description), + status_code=400, + ) diff --git a/source/target-vendors/cp312/mcp/server/auth/handlers/revoke.py b/source/target-vendors/cp312/mcp/server/auth/handlers/revoke.py new file mode 100644 index 0000000..fa8cfc9 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/auth/handlers/revoke.py @@ -0,0 +1,91 @@ +from dataclasses import dataclass +from functools import partial +from typing import Any, Literal + +from pydantic import BaseModel, ValidationError +from starlette.requests import Request +from starlette.responses import Response + +from mcp.server.auth.errors import ( + stringify_pydantic_error, +) +from mcp.server.auth.json_response import PydanticJSONResponse +from mcp.server.auth.middleware.client_auth import AuthenticationError, ClientAuthenticator +from mcp.server.auth.provider import AccessToken, OAuthAuthorizationServerProvider, RefreshToken + + +class RevocationRequest(BaseModel): + """ + # See https://datatracker.ietf.org/doc/html/rfc7009#section-2.1 + """ + + token: str + token_type_hint: Literal["access_token", "refresh_token"] | None = None + client_id: str + client_secret: str | None + + +class RevocationErrorResponse(BaseModel): + error: Literal["invalid_request", "unauthorized_client"] + error_description: str | None = None + + +@dataclass +class RevocationHandler: + provider: OAuthAuthorizationServerProvider[Any, Any, Any] + client_authenticator: ClientAuthenticator + + async def handle(self, request: Request) -> Response: + """ + Handler for the OAuth 2.0 Token Revocation endpoint. + """ + try: + client = await self.client_authenticator.authenticate_request(request) + except AuthenticationError as e: # pragma: no cover + return PydanticJSONResponse( + status_code=401, + content=RevocationErrorResponse( + error="unauthorized_client", + error_description=e.message, + ), + ) + + try: + form_data = await request.form() + revocation_request = RevocationRequest.model_validate(dict(form_data)) + except ValidationError as e: + return PydanticJSONResponse( + status_code=400, + content=RevocationErrorResponse( + error="invalid_request", + error_description=stringify_pydantic_error(e), + ), + ) + + loaders = [ + self.provider.load_access_token, + partial(self.provider.load_refresh_token, client), + ] + if revocation_request.token_type_hint == "refresh_token": # pragma: no cover + loaders = reversed(loaders) + + token: None | AccessToken | RefreshToken = None + for loader in loaders: + token = await loader(revocation_request.token) + if token is not None: + break + + # if token is not found, just return HTTP 200 per the RFC + if token and token.client_id == client.client_id: + # Revoke token; provider is not meant to be able to do validation + # at this point that would result in an error + await self.provider.revoke_token(token) + + # Return successful empty response + return Response( + status_code=200, + headers={ + "Cache-Control": "no-store", + "Pragma": "no-cache", + }, + ) diff --git a/source/target-vendors/cp312/mcp/server/auth/handlers/token.py b/source/target-vendors/cp312/mcp/server/auth/handlers/token.py new file mode 100644 index 0000000..4467da6 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/auth/handlers/token.py @@ -0,0 +1,241 @@ +import base64 +import hashlib +import time +from dataclasses import dataclass +from typing import Annotated, Any, Literal + +from pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field, RootModel, ValidationError +from starlette.requests import Request + +from mcp.server.auth.errors import stringify_pydantic_error +from mcp.server.auth.json_response import PydanticJSONResponse +from mcp.server.auth.middleware.client_auth import AuthenticationError, ClientAuthenticator +from mcp.server.auth.provider import OAuthAuthorizationServerProvider, TokenError, TokenErrorCode +from mcp.shared.auth import OAuthToken + + +class AuthorizationCodeRequest(BaseModel): + # See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3 + grant_type: Literal["authorization_code"] + code: str = Field(..., description="The authorization code") + redirect_uri: AnyUrl | None = Field(None, description="Must be the same as redirect URI provided in /authorize") + client_id: str + # we use the client_secret param, per https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1 + client_secret: str | None = None + # See https://datatracker.ietf.org/doc/html/rfc7636#section-4.5 + code_verifier: str = Field(..., description="PKCE code verifier") + # RFC 8707 resource indicator + resource: str | None = Field(None, description="Resource indicator for the token") + + +class RefreshTokenRequest(BaseModel): + # See https://datatracker.ietf.org/doc/html/rfc6749#section-6 + grant_type: Literal["refresh_token"] + refresh_token: str = Field(..., description="The refresh token") + scope: str | None = Field(None, description="Optional scope parameter") + client_id: str + # we use the client_secret param, per https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1 + client_secret: str | None = None + # RFC 8707 resource indicator + resource: str | None = Field(None, description="Resource indicator for the token") + + +class TokenRequest( + RootModel[ + Annotated[ + AuthorizationCodeRequest | RefreshTokenRequest, + Field(discriminator="grant_type"), + ] + ] +): + root: Annotated[ + AuthorizationCodeRequest | RefreshTokenRequest, + Field(discriminator="grant_type"), + ] + + +class TokenErrorResponse(BaseModel): + """ + See https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 + """ + + error: TokenErrorCode + error_description: str | None = None + error_uri: AnyHttpUrl | None = None + + +class TokenSuccessResponse(RootModel[OAuthToken]): + # this is just a wrapper over OAuthToken; the only reason we do this + # is to have some separation between the HTTP response type, and the + # type returned by the provider + root: OAuthToken + + +@dataclass +class TokenHandler: + provider: OAuthAuthorizationServerProvider[Any, Any, Any] + client_authenticator: ClientAuthenticator + + def response(self, obj: TokenSuccessResponse | TokenErrorResponse): + status_code = 200 + if isinstance(obj, TokenErrorResponse): + status_code = 400 + + return PydanticJSONResponse( + content=obj, + status_code=status_code, + headers={ + "Cache-Control": "no-store", + "Pragma": "no-cache", + }, + ) + + async def handle(self, request: Request): + try: + client_info = await self.client_authenticator.authenticate_request(request) + except AuthenticationError as e: + # Authentication failures should return 401 + return PydanticJSONResponse( + content=TokenErrorResponse( + error="unauthorized_client", + error_description=e.message, + ), + status_code=401, + headers={ + "Cache-Control": "no-store", + "Pragma": "no-cache", + }, + ) + + try: + form_data = await request.form() + token_request = TokenRequest.model_validate(dict(form_data)).root + except ValidationError as validation_error: # pragma: no cover + return self.response( + TokenErrorResponse( + error="invalid_request", + error_description=stringify_pydantic_error(validation_error), + ) + ) + + if token_request.grant_type not in client_info.grant_types: # pragma: no cover + return self.response( + TokenErrorResponse( + error="unsupported_grant_type", + error_description=(f"Unsupported grant type (supported grant types are {client_info.grant_types})"), + ) + ) + + tokens: OAuthToken + + match token_request: + case AuthorizationCodeRequest(): + auth_code = await self.provider.load_authorization_code(client_info, token_request.code) + if auth_code is None or auth_code.client_id != token_request.client_id: + # if code belongs to different client, pretend it doesn't exist + return self.response( + TokenErrorResponse( + error="invalid_grant", + error_description="authorization code does not exist", + ) + ) + + # make auth codes expire after a deadline + # see https://datatracker.ietf.org/doc/html/rfc6749#section-10.5 + if auth_code.expires_at < time.time(): + return self.response( + TokenErrorResponse( + error="invalid_grant", + error_description="authorization code has expired", + ) + ) + + # verify redirect_uri doesn't change between /authorize and /tokens + # see https://datatracker.ietf.org/doc/html/rfc6749#section-10.6 + if auth_code.redirect_uri_provided_explicitly: + authorize_request_redirect_uri = auth_code.redirect_uri + else: # pragma: no cover + authorize_request_redirect_uri = None + + # Convert both sides to strings for comparison to handle AnyUrl vs string issues + token_redirect_str = str(token_request.redirect_uri) if token_request.redirect_uri is not None else None + auth_redirect_str = ( + str(authorize_request_redirect_uri) if authorize_request_redirect_uri is not None else None + ) + + if token_redirect_str != auth_redirect_str: + return self.response( + TokenErrorResponse( + error="invalid_request", + error_description=("redirect_uri did not match the one used when creating auth code"), + ) + ) + + # Verify PKCE code verifier + sha256 = hashlib.sha256(token_request.code_verifier.encode()).digest() + hashed_code_verifier = base64.urlsafe_b64encode(sha256).decode().rstrip("=") + + if hashed_code_verifier != auth_code.code_challenge: + # see https://datatracker.ietf.org/doc/html/rfc7636#section-4.6 + return self.response( + TokenErrorResponse( + error="invalid_grant", + error_description="incorrect code_verifier", + ) + ) + + try: + # Exchange authorization code for tokens + tokens = await self.provider.exchange_authorization_code(client_info, auth_code) + except TokenError as e: + return self.response( + TokenErrorResponse( + error=e.error, + error_description=e.error_description, + ) + ) + + case RefreshTokenRequest(): # pragma: no cover + refresh_token = await self.provider.load_refresh_token(client_info, token_request.refresh_token) + if refresh_token is None or refresh_token.client_id != token_request.client_id: + # if token belongs to different client, pretend it doesn't exist + return self.response( + TokenErrorResponse( + error="invalid_grant", + error_description="refresh token does not exist", + ) + ) + + if refresh_token.expires_at and refresh_token.expires_at < time.time(): + # if the refresh token has expired, pretend it doesn't exist + return self.response( + TokenErrorResponse( + error="invalid_grant", + error_description="refresh token has expired", + ) + ) + + # Parse scopes if provided + scopes = token_request.scope.split(" ") if token_request.scope else refresh_token.scopes + + for scope in scopes: + if scope not in refresh_token.scopes: + return self.response( + TokenErrorResponse( + error="invalid_scope", + error_description=(f"cannot request scope `{scope}` not provided by refresh token"), + ) + ) + + try: + # Exchange refresh token for new tokens + tokens = await self.provider.exchange_refresh_token(client_info, refresh_token, scopes) + except TokenError as e: + return self.response( + TokenErrorResponse( + error=e.error, + error_description=e.error_description, + ) + ) + + return self.response(TokenSuccessResponse(root=tokens)) diff --git a/source/target-vendors/cp312/mcp/server/auth/json_response.py b/source/target-vendors/cp312/mcp/server/auth/json_response.py new file mode 100644 index 0000000..bd95bd6 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/auth/json_response.py @@ -0,0 +1,10 @@ +from typing import Any + +from starlette.responses import JSONResponse + + +class PydanticJSONResponse(JSONResponse): + # use pydantic json serialization instead of the stock `json.dumps`, + # so that we can handle serializing pydantic models like AnyHttpUrl + def render(self, content: Any) -> bytes: + return content.model_dump_json(exclude_none=True).encode("utf-8") diff --git a/source/target-vendors/cp312/mcp/server/auth/middleware/__init__.py b/source/target-vendors/cp312/mcp/server/auth/middleware/__init__.py new file mode 100644 index 0000000..ba3ff63 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/auth/middleware/__init__.py @@ -0,0 +1,3 @@ +""" +Middleware for MCP authorization. +""" diff --git a/source/target-vendors/cp312/mcp/server/auth/middleware/auth_context.py b/source/target-vendors/cp312/mcp/server/auth/middleware/auth_context.py new file mode 100644 index 0000000..e2116c3 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/auth/middleware/auth_context.py @@ -0,0 +1,48 @@ +import contextvars + +from starlette.types import ASGIApp, Receive, Scope, Send + +from mcp.server.auth.middleware.bearer_auth import AuthenticatedUser +from mcp.server.auth.provider import AccessToken + +# Create a contextvar to store the authenticated user +# The default is None, indicating no authenticated user is present +auth_context_var = contextvars.ContextVar[AuthenticatedUser | None]("auth_context", default=None) + + +def get_access_token() -> AccessToken | None: + """ + Get the access token from the current context. + + Returns: + The access token if an authenticated user is available, None otherwise. + """ + auth_user = auth_context_var.get() + return auth_user.access_token if auth_user else None + + +class AuthContextMiddleware: + """ + Middleware that extracts the authenticated user from the request + and sets it in a contextvar for easy access throughout the request lifecycle. + + This middleware should be added after the AuthenticationMiddleware in the + middleware stack to ensure that the user is properly authenticated before + being stored in the context. + """ + + def __init__(self, app: ASGIApp): + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send): + user = scope.get("user") + if isinstance(user, AuthenticatedUser): + # Set the authenticated user in the contextvar + token = auth_context_var.set(user) + try: + await self.app(scope, receive, send) + finally: + auth_context_var.reset(token) + else: + # No authenticated user, just process the request + await self.app(scope, receive, send) diff --git a/source/target-vendors/cp312/mcp/server/auth/middleware/bearer_auth.py b/source/target-vendors/cp312/mcp/server/auth/middleware/bearer_auth.py new file mode 100644 index 0000000..64c9b88 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/auth/middleware/bearer_auth.py @@ -0,0 +1,128 @@ +import json +import time +from typing import Any + +from pydantic import AnyHttpUrl +from starlette.authentication import AuthCredentials, AuthenticationBackend, SimpleUser +from starlette.requests import HTTPConnection +from starlette.types import Receive, Scope, Send + +from mcp.server.auth.provider import AccessToken, TokenVerifier + + +class AuthenticatedUser(SimpleUser): + """User with authentication info.""" + + def __init__(self, auth_info: AccessToken): + super().__init__(auth_info.client_id) + self.access_token = auth_info + self.scopes = auth_info.scopes + + +class BearerAuthBackend(AuthenticationBackend): + """ + Authentication backend that validates Bearer tokens using a TokenVerifier. + """ + + def __init__(self, token_verifier: TokenVerifier): + self.token_verifier = token_verifier + + async def authenticate(self, conn: HTTPConnection): + auth_header = next( + (conn.headers.get(key) for key in conn.headers if key.lower() == "authorization"), + None, + ) + if not auth_header or not auth_header.lower().startswith("bearer "): + return None + + token = auth_header[7:] # Remove "Bearer " prefix + + # Validate the token with the verifier + auth_info = await self.token_verifier.verify_token(token) + + if not auth_info: + return None + + if auth_info.expires_at and auth_info.expires_at < int(time.time()): + return None + + return AuthCredentials(auth_info.scopes), AuthenticatedUser(auth_info) + + +class RequireAuthMiddleware: + """ + Middleware that requires a valid Bearer token in the Authorization header. + + This will validate the token with the auth provider and store the resulting + auth info in the request state. + """ + + def __init__( + self, + app: Any, + required_scopes: list[str], + resource_metadata_url: AnyHttpUrl | None = None, + ): + """ + Initialize the middleware. + + Args: + app: ASGI application + required_scopes: List of scopes that the token must have + resource_metadata_url: Optional protected resource metadata URL for WWW-Authenticate header + """ + self.app = app + self.required_scopes = required_scopes + self.resource_metadata_url = resource_metadata_url + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + auth_user = scope.get("user") + if not isinstance(auth_user, AuthenticatedUser): + await self._send_auth_error( + send, status_code=401, error="invalid_token", description="Authentication required" + ) + return + + auth_credentials = scope.get("auth") + + for required_scope in self.required_scopes: + # auth_credentials should always be provided; this is just paranoia + if auth_credentials is None or required_scope not in auth_credentials.scopes: + await self._send_auth_error( + send, status_code=403, error="insufficient_scope", description=f"Required scope: {required_scope}" + ) + return + + await self.app(scope, receive, send) + + async def _send_auth_error(self, send: Send, status_code: int, error: str, description: str) -> None: + """Send an authentication error response with WWW-Authenticate header.""" + # Build WWW-Authenticate header value + www_auth_parts = [f'error="{error}"', f'error_description="{description}"'] + if self.resource_metadata_url: # pragma: no cover + www_auth_parts.append(f'resource_metadata="{self.resource_metadata_url}"') + + www_authenticate = f"Bearer {', '.join(www_auth_parts)}" + + # Send response + body = {"error": error, "error_description": description} + body_bytes = json.dumps(body).encode() + + await send( + { + "type": "http.response.start", + "status": status_code, + "headers": [ + (b"content-type", b"application/json"), + (b"content-length", str(len(body_bytes)).encode()), + (b"www-authenticate", www_authenticate.encode()), + ], + } + ) + + await send( + { + "type": "http.response.body", + "body": body_bytes, + } + ) diff --git a/source/target-vendors/cp312/mcp/server/auth/middleware/client_auth.py b/source/target-vendors/cp312/mcp/server/auth/middleware/client_auth.py new file mode 100644 index 0000000..6126c6e --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/auth/middleware/client_auth.py @@ -0,0 +1,115 @@ +import base64 +import binascii +import hmac +import time +from typing import Any +from urllib.parse import unquote + +from starlette.requests import Request + +from mcp.server.auth.provider import OAuthAuthorizationServerProvider +from mcp.shared.auth import OAuthClientInformationFull + + +class AuthenticationError(Exception): + def __init__(self, message: str): + self.message = message # pragma: no cover + + +class ClientAuthenticator: + """ + ClientAuthenticator is a callable which validates requests from a client + application, used to verify /token calls. + If, during registration, the client requested to be issued a secret, the + authenticator asserts that /token calls must be authenticated with + that same token. + NOTE: clients can opt for no authentication during registration, in which case this + logic is skipped. + """ + + def __init__(self, provider: OAuthAuthorizationServerProvider[Any, Any, Any]): + """ + Initialize the dependency. + + Args: + provider: Provider to look up client information + """ + self.provider = provider + + async def authenticate_request(self, request: Request) -> OAuthClientInformationFull: + """ + Authenticate a client from an HTTP request. + + Extracts client credentials from the appropriate location based on the + client's registered authentication method and validates them. + + Args: + request: The HTTP request containing client credentials + + Returns: + The authenticated client information + + Raises: + AuthenticationError: If authentication fails + """ + form_data = await request.form() + client_id = form_data.get("client_id") + if not client_id: + raise AuthenticationError("Missing client_id") + + client = await self.provider.get_client(str(client_id)) + if not client: + raise AuthenticationError("Invalid client_id") # pragma: no cover + + request_client_secret: str | None = None + auth_header = request.headers.get("Authorization", "") + + if client.token_endpoint_auth_method == "client_secret_basic": + if not auth_header.startswith("Basic "): + raise AuthenticationError("Missing or invalid Basic authentication in Authorization header") + + try: + encoded_credentials = auth_header[6:] # Remove "Basic " prefix + decoded = base64.b64decode(encoded_credentials).decode("utf-8") + if ":" not in decoded: + raise ValueError("Invalid Basic auth format") + basic_client_id, request_client_secret = decoded.split(":", 1) + + # URL-decode both parts per RFC 6749 Section 2.3.1 + basic_client_id = unquote(basic_client_id) + request_client_secret = unquote(request_client_secret) + + if basic_client_id != client_id: + raise AuthenticationError("Client ID mismatch in Basic auth") + except (ValueError, UnicodeDecodeError, binascii.Error): + raise AuthenticationError("Invalid Basic authentication header") + + elif client.token_endpoint_auth_method == "client_secret_post": + raw_form_data = form_data.get("client_secret") + # form_data.get() can return a UploadFile or None, so we need to check if it's a string + if isinstance(raw_form_data, str): + request_client_secret = str(raw_form_data) + + elif client.token_endpoint_auth_method == "none": + request_client_secret = None + else: + raise AuthenticationError( # pragma: no cover + f"Unsupported auth method: {client.token_endpoint_auth_method}" + ) + + # If client from the store expects a secret, validate that the request provides + # that secret + if client.client_secret: # pragma: no branch + if not request_client_secret: + raise AuthenticationError("Client secret is required") # pragma: no cover + + # hmac.compare_digest requires that both arguments are either bytes or a `str` containing + # only ASCII characters. Since we do not control `request_client_secret`, we encode both + # arguments to bytes. + if not hmac.compare_digest(client.client_secret.encode(), request_client_secret.encode()): + raise AuthenticationError("Invalid client_secret") # pragma: no cover + + if client.client_secret_expires_at and client.client_secret_expires_at < int(time.time()): + raise AuthenticationError("Client secret has expired") # pragma: no cover + + return client diff --git a/source/target-vendors/cp312/mcp/server/auth/provider.py b/source/target-vendors/cp312/mcp/server/auth/provider.py new file mode 100644 index 0000000..96296c1 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/auth/provider.py @@ -0,0 +1,301 @@ +from dataclasses import dataclass +from typing import Generic, Literal, Protocol, TypeVar +from urllib.parse import parse_qs, urlencode, urlparse, urlunparse + +from pydantic import AnyUrl, BaseModel + +from mcp.shared.auth import OAuthClientInformationFull, OAuthToken + + +class AuthorizationParams(BaseModel): + state: str | None + scopes: list[str] | None + code_challenge: str + redirect_uri: AnyUrl + redirect_uri_provided_explicitly: bool + resource: str | None = None # RFC 8707 resource indicator + + +class AuthorizationCode(BaseModel): + code: str + scopes: list[str] + expires_at: float + client_id: str + code_challenge: str + redirect_uri: AnyUrl + redirect_uri_provided_explicitly: bool + resource: str | None = None # RFC 8707 resource indicator + + +class RefreshToken(BaseModel): + token: str + client_id: str + scopes: list[str] + expires_at: int | None = None + + +class AccessToken(BaseModel): + token: str + client_id: str + scopes: list[str] + expires_at: int | None = None + resource: str | None = None # RFC 8707 resource indicator + + +RegistrationErrorCode = Literal[ + "invalid_redirect_uri", + "invalid_client_metadata", + "invalid_software_statement", + "unapproved_software_statement", +] + + +@dataclass(frozen=True) +class RegistrationError(Exception): + error: RegistrationErrorCode + error_description: str | None = None + + +AuthorizationErrorCode = Literal[ + "invalid_request", + "unauthorized_client", + "access_denied", + "unsupported_response_type", + "invalid_scope", + "server_error", + "temporarily_unavailable", +] + + +@dataclass(frozen=True) +class AuthorizeError(Exception): + error: AuthorizationErrorCode + error_description: str | None = None + + +TokenErrorCode = Literal[ + "invalid_request", + "invalid_client", + "invalid_grant", + "unauthorized_client", + "unsupported_grant_type", + "invalid_scope", +] + + +@dataclass(frozen=True) +class TokenError(Exception): + error: TokenErrorCode + error_description: str | None = None + + +class TokenVerifier(Protocol): + """Protocol for verifying bearer tokens.""" + + async def verify_token(self, token: str) -> AccessToken | None: + """Verify a bearer token and return access info if valid.""" + + +# NOTE: FastMCP doesn't render any of these types in the user response, so it's +# OK to add fields to subclasses which should not be exposed externally. +AuthorizationCodeT = TypeVar("AuthorizationCodeT", bound=AuthorizationCode) +RefreshTokenT = TypeVar("RefreshTokenT", bound=RefreshToken) +AccessTokenT = TypeVar("AccessTokenT", bound=AccessToken) + + +class OAuthAuthorizationServerProvider(Protocol, Generic[AuthorizationCodeT, RefreshTokenT, AccessTokenT]): + async def get_client(self, client_id: str) -> OAuthClientInformationFull | None: + """ + Retrieves client information by client ID. + + Implementors MAY raise NotImplementedError if dynamic client registration is + disabled in ClientRegistrationOptions. + + Args: + client_id: The ID of the client to retrieve. + + Returns: + The client information, or None if the client does not exist. + """ + + async def register_client(self, client_info: OAuthClientInformationFull) -> None: + """ + Saves client information as part of registering it. + + Implementors MAY raise NotImplementedError if dynamic client registration is + disabled in ClientRegistrationOptions. + + Args: + client_info: The client metadata to register. + + Raises: + RegistrationError: If the client metadata is invalid. + """ + + async def authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str: + """ + Called as part of the /authorize endpoint, and returns a URL that the client + will be redirected to. + Many MCP implementations will redirect to a third-party provider to perform + a second OAuth exchange with that provider. In this sort of setup, the client + has an OAuth connection with the MCP server, and the MCP server has an OAuth + connection with the 3rd-party provider. At the end of this flow, the client + should be redirected to the redirect_uri from params.redirect_uri. + + +--------+ +------------+ +-------------------+ + | | | | | | + | Client | --> | MCP Server | --> | 3rd Party OAuth | + | | | | | Server | + +--------+ +------------+ +-------------------+ + | ^ | + +------------+ | | | + | | | | Redirect | + |redirect_uri|<-----+ +------------------+ + | | + +------------+ + + Implementations will need to define another handler on the MCP server return + flow to perform the second redirect, and generate and store an authorization + code as part of completing the OAuth authorization step. + + Implementations SHOULD generate an authorization code with at least 160 bits of + entropy, + and MUST generate an authorization code with at least 128 bits of entropy. + See https://datatracker.ietf.org/doc/html/rfc6749#section-10.10. + + Args: + client: The client requesting authorization. + params: The parameters of the authorization request. + + Returns: + A URL to redirect the client to for authorization. + + Raises: + AuthorizeError: If the authorization request is invalid. + """ + ... + + async def load_authorization_code( + self, client: OAuthClientInformationFull, authorization_code: str + ) -> AuthorizationCodeT | None: + """ + Loads an AuthorizationCode by its code. + + Args: + client: The client that requested the authorization code. + authorization_code: The authorization code to get the challenge for. + + Returns: + The AuthorizationCode, or None if not found + """ + ... + + async def exchange_authorization_code( + self, client: OAuthClientInformationFull, authorization_code: AuthorizationCodeT + ) -> OAuthToken: + """ + Exchanges an authorization code for an access token and refresh token. + + Args: + client: The client exchanging the authorization code. + authorization_code: The authorization code to exchange. + + Returns: + The OAuth token, containing access and refresh tokens. + + Raises: + TokenError: If the request is invalid + """ + ... + + async def load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshTokenT | None: + """ + Loads a RefreshToken by its token string. + + Args: + client: The client that is requesting to load the refresh token. + refresh_token: The refresh token string to load. + + Returns: + The RefreshToken object if found, or None if not found. + """ + ... + + async def exchange_refresh_token( + self, + client: OAuthClientInformationFull, + refresh_token: RefreshTokenT, + scopes: list[str], + ) -> OAuthToken: + """ + Exchanges a refresh token for an access token and refresh token. + + Implementations SHOULD rotate both the access token and refresh token. + + Args: + client: The client exchanging the refresh token. + refresh_token: The refresh token to exchange. + scopes: Optional scopes to request with the new access token. + + Returns: + The OAuth token, containing access and refresh tokens. + + Raises: + TokenError: If the request is invalid + """ + ... + + async def load_access_token(self, token: str) -> AccessTokenT | None: + """ + Loads an access token by its token. + + Args: + token: The access token to verify. + + Returns: + The AuthInfo, or None if the token is invalid. + """ + + async def revoke_token( + self, + token: AccessTokenT | RefreshTokenT, + ) -> None: + """ + Revokes an access or refresh token. + + If the given token is invalid or already revoked, this method should do nothing. + + Implementations SHOULD revoke both the access token and its corresponding + refresh token, regardless of which of the access token or refresh token is + provided. + + Args: + token: the token to revoke + """ + + +def construct_redirect_uri(redirect_uri_base: str, **params: str | None) -> str: + parsed_uri = urlparse(redirect_uri_base) + query_params = [(k, v) for k, vs in parse_qs(parsed_uri.query).items() for v in vs] + for k, v in params.items(): + if v is not None: + query_params.append((k, v)) + + redirect_uri = urlunparse(parsed_uri._replace(query=urlencode(query_params))) + return redirect_uri + + +class ProviderTokenVerifier(TokenVerifier): + """Token verifier that uses an OAuthAuthorizationServerProvider. + + This is provided for backwards compatibility with existing auth_server_provider + configurations. For new implementations using AS/RS separation, consider using + the TokenVerifier protocol with a dedicated implementation like IntrospectionTokenVerifier. + """ + + def __init__(self, provider: "OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]"): + self.provider = provider + + async def verify_token(self, token: str) -> AccessToken | None: + """Verify token using the provider's load_access_token method.""" + return await self.provider.load_access_token(token) diff --git a/source/target-vendors/cp312/mcp/server/auth/routes.py b/source/target-vendors/cp312/mcp/server/auth/routes.py new file mode 100644 index 0000000..71a9c8b --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/auth/routes.py @@ -0,0 +1,253 @@ +from collections.abc import Awaitable, Callable +from typing import Any +from urllib.parse import urlparse + +from pydantic import AnyHttpUrl +from starlette.middleware.cors import CORSMiddleware +from starlette.requests import Request +from starlette.responses import Response +from starlette.routing import Route, request_response # type: ignore +from starlette.types import ASGIApp + +from mcp.server.auth.handlers.authorize import AuthorizationHandler +from mcp.server.auth.handlers.metadata import MetadataHandler +from mcp.server.auth.handlers.register import RegistrationHandler +from mcp.server.auth.handlers.revoke import RevocationHandler +from mcp.server.auth.handlers.token import TokenHandler +from mcp.server.auth.middleware.client_auth import ClientAuthenticator +from mcp.server.auth.provider import OAuthAuthorizationServerProvider +from mcp.server.auth.settings import ClientRegistrationOptions, RevocationOptions +from mcp.server.streamable_http import MCP_PROTOCOL_VERSION_HEADER +from mcp.shared.auth import OAuthMetadata + + +def validate_issuer_url(url: AnyHttpUrl): + """ + Validate that the issuer URL meets OAuth 2.0 requirements. + + Args: + url: The issuer URL to validate + + Raises: + ValueError: If the issuer URL is invalid + """ + + # RFC 8414 requires HTTPS, but we allow localhost HTTP for testing + if ( + url.scheme != "https" + and url.host != "localhost" + and (url.host is not None and not url.host.startswith("127.0.0.1")) + ): + raise ValueError("Issuer URL must be HTTPS") # pragma: no cover + + # No fragments or query parameters allowed + if url.fragment: + raise ValueError("Issuer URL must not have a fragment") # pragma: no cover + if url.query: + raise ValueError("Issuer URL must not have a query string") # pragma: no cover + + +AUTHORIZATION_PATH = "/authorize" +TOKEN_PATH = "/token" +REGISTRATION_PATH = "/register" +REVOCATION_PATH = "/revoke" + + +def cors_middleware( + handler: Callable[[Request], Response | Awaitable[Response]], + allow_methods: list[str], +) -> ASGIApp: + cors_app = CORSMiddleware( + app=request_response(handler), + allow_origins="*", + allow_methods=allow_methods, + allow_headers=[MCP_PROTOCOL_VERSION_HEADER], + ) + return cors_app + + +def create_auth_routes( + provider: OAuthAuthorizationServerProvider[Any, Any, Any], + issuer_url: AnyHttpUrl, + service_documentation_url: AnyHttpUrl | None = None, + client_registration_options: ClientRegistrationOptions | None = None, + revocation_options: RevocationOptions | None = None, +) -> list[Route]: + validate_issuer_url(issuer_url) + + client_registration_options = client_registration_options or ClientRegistrationOptions() + revocation_options = revocation_options or RevocationOptions() + metadata = build_metadata( + issuer_url, + service_documentation_url, + client_registration_options, + revocation_options, + ) + client_authenticator = ClientAuthenticator(provider) + + # Create routes + # Allow CORS requests for endpoints meant to be hit by the OAuth client + # (with the client secret). This is intended to support things like MCP Inspector, + # where the client runs in a web browser. + routes = [ + Route( + "/.well-known/oauth-authorization-server", + endpoint=cors_middleware( + MetadataHandler(metadata).handle, + ["GET", "OPTIONS"], + ), + methods=["GET", "OPTIONS"], + ), + Route( + AUTHORIZATION_PATH, + # do not allow CORS for authorization endpoint; + # clients should just redirect to this + endpoint=AuthorizationHandler(provider).handle, + methods=["GET", "POST"], + ), + Route( + TOKEN_PATH, + endpoint=cors_middleware( + TokenHandler(provider, client_authenticator).handle, + ["POST", "OPTIONS"], + ), + methods=["POST", "OPTIONS"], + ), + ] + + if client_registration_options.enabled: # pragma: no branch + registration_handler = RegistrationHandler( + provider, + options=client_registration_options, + ) + routes.append( + Route( + REGISTRATION_PATH, + endpoint=cors_middleware( + registration_handler.handle, + ["POST", "OPTIONS"], + ), + methods=["POST", "OPTIONS"], + ) + ) + + if revocation_options.enabled: # pragma: no branch + revocation_handler = RevocationHandler(provider, client_authenticator) + routes.append( + Route( + REVOCATION_PATH, + endpoint=cors_middleware( + revocation_handler.handle, + ["POST", "OPTIONS"], + ), + methods=["POST", "OPTIONS"], + ) + ) + + return routes + + +def build_metadata( + issuer_url: AnyHttpUrl, + service_documentation_url: AnyHttpUrl | None, + client_registration_options: ClientRegistrationOptions, + revocation_options: RevocationOptions, +) -> OAuthMetadata: + authorization_url = AnyHttpUrl(str(issuer_url).rstrip("/") + AUTHORIZATION_PATH) + token_url = AnyHttpUrl(str(issuer_url).rstrip("/") + TOKEN_PATH) + + # Create metadata + metadata = OAuthMetadata( + issuer=issuer_url, + authorization_endpoint=authorization_url, + token_endpoint=token_url, + scopes_supported=client_registration_options.valid_scopes, + response_types_supported=["code"], + response_modes_supported=None, + grant_types_supported=["authorization_code", "refresh_token"], + token_endpoint_auth_methods_supported=["client_secret_post", "client_secret_basic"], + token_endpoint_auth_signing_alg_values_supported=None, + service_documentation=service_documentation_url, + ui_locales_supported=None, + op_policy_uri=None, + op_tos_uri=None, + introspection_endpoint=None, + code_challenge_methods_supported=["S256"], + ) + + # Add registration endpoint if supported + if client_registration_options.enabled: # pragma: no branch + metadata.registration_endpoint = AnyHttpUrl(str(issuer_url).rstrip("/") + REGISTRATION_PATH) + + # Add revocation endpoint if supported + if revocation_options.enabled: # pragma: no branch + metadata.revocation_endpoint = AnyHttpUrl(str(issuer_url).rstrip("/") + REVOCATION_PATH) + metadata.revocation_endpoint_auth_methods_supported = ["client_secret_post", "client_secret_basic"] + + return metadata + + +def build_resource_metadata_url(resource_server_url: AnyHttpUrl) -> AnyHttpUrl: + """ + Build RFC 9728 compliant protected resource metadata URL. + + Inserts /.well-known/oauth-protected-resource between host and resource path + as specified in RFC 9728 §3.1. + + Args: + resource_server_url: The resource server URL (e.g., https://example.com/mcp) + + Returns: + The metadata URL (e.g., https://example.com/.well-known/oauth-protected-resource/mcp) + """ + parsed = urlparse(str(resource_server_url)) + # Handle trailing slash: if path is just "/", treat as empty + resource_path = parsed.path if parsed.path != "/" else "" + return AnyHttpUrl(f"{parsed.scheme}://{parsed.netloc}/.well-known/oauth-protected-resource{resource_path}") + + +def create_protected_resource_routes( + resource_url: AnyHttpUrl, + authorization_servers: list[AnyHttpUrl], + scopes_supported: list[str] | None = None, + resource_name: str | None = None, + resource_documentation: AnyHttpUrl | None = None, +) -> list[Route]: + """ + Create routes for OAuth 2.0 Protected Resource Metadata (RFC 9728). + + Args: + resource_url: The URL of this resource server + authorization_servers: List of authorization servers that can issue tokens + scopes_supported: Optional list of scopes supported by this resource + + Returns: + List of Starlette routes for protected resource metadata + """ + from mcp.server.auth.handlers.metadata import ProtectedResourceMetadataHandler + from mcp.shared.auth import ProtectedResourceMetadata + + metadata = ProtectedResourceMetadata( + resource=resource_url, + authorization_servers=authorization_servers, + scopes_supported=scopes_supported, + resource_name=resource_name, + resource_documentation=resource_documentation, + # bearer_methods_supported defaults to ["header"] in the model + ) + + handler = ProtectedResourceMetadataHandler(metadata) + + # RFC 9728 §3.1: Register route at /.well-known/oauth-protected-resource + resource path + metadata_url = build_resource_metadata_url(resource_url) + # Extract just the path part for route registration + parsed = urlparse(str(metadata_url)) + well_known_path = parsed.path + + return [ + Route( + well_known_path, + endpoint=cors_middleware(handler.handle, ["GET", "OPTIONS"]), + methods=["GET", "OPTIONS"], + ) + ] diff --git a/source/target-vendors/cp312/mcp/server/auth/settings.py b/source/target-vendors/cp312/mcp/server/auth/settings.py new file mode 100644 index 0000000..1649826 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/auth/settings.py @@ -0,0 +1,30 @@ +from pydantic import AnyHttpUrl, BaseModel, Field + + +class ClientRegistrationOptions(BaseModel): + enabled: bool = False + client_secret_expiry_seconds: int | None = None + valid_scopes: list[str] | None = None + default_scopes: list[str] | None = None + + +class RevocationOptions(BaseModel): + enabled: bool = False + + +class AuthSettings(BaseModel): + issuer_url: AnyHttpUrl = Field( + ..., + description="OAuth authorization server URL that issues tokens for this resource server.", + ) + service_documentation_url: AnyHttpUrl | None = None + client_registration_options: ClientRegistrationOptions | None = None + revocation_options: RevocationOptions | None = None + required_scopes: list[str] | None = None + + # Resource Server settings (when operating as RS only) + resource_server_url: AnyHttpUrl | None = Field( + ..., + description="The URL of the MCP server to be used as the resource identifier " + "and base route to look up OAuth Protected Resource Metadata.", + ) diff --git a/source/target-vendors/cp312/mcp/server/elicitation.py b/source/target-vendors/cp312/mcp/server/elicitation.py new file mode 100644 index 0000000..4919541 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/elicitation.py @@ -0,0 +1,190 @@ +"""Elicitation utilities for MCP servers.""" + +from __future__ import annotations + +import types +from collections.abc import Sequence +from typing import Generic, Literal, TypeVar, Union, get_args, get_origin + +from pydantic import BaseModel + +from mcp.server.session import ServerSession +from mcp.types import RequestId + +ElicitSchemaModelT = TypeVar("ElicitSchemaModelT", bound=BaseModel) + + +class AcceptedElicitation(BaseModel, Generic[ElicitSchemaModelT]): + """Result when user accepts the elicitation.""" + + action: Literal["accept"] = "accept" + data: ElicitSchemaModelT + + +class DeclinedElicitation(BaseModel): + """Result when user declines the elicitation.""" + + action: Literal["decline"] = "decline" + + +class CancelledElicitation(BaseModel): + """Result when user cancels the elicitation.""" + + action: Literal["cancel"] = "cancel" + + +ElicitationResult = AcceptedElicitation[ElicitSchemaModelT] | DeclinedElicitation | CancelledElicitation + + +class AcceptedUrlElicitation(BaseModel): + """Result when user accepts a URL mode elicitation.""" + + action: Literal["accept"] = "accept" + + +UrlElicitationResult = AcceptedUrlElicitation | DeclinedElicitation | CancelledElicitation + + +# Primitive types allowed in elicitation schemas +_ELICITATION_PRIMITIVE_TYPES = (str, int, float, bool) + + +def _validate_elicitation_schema(schema: type[BaseModel]) -> None: + """Validate that a Pydantic model only contains primitive field types.""" + for field_name, field_info in schema.model_fields.items(): + annotation = field_info.annotation + + if annotation is None or annotation is types.NoneType: # pragma: no cover + continue + elif _is_primitive_field(annotation): + continue + elif _is_string_sequence(annotation): + continue + else: + raise TypeError( + f"Elicitation schema field '{field_name}' must be a primitive type " + f"{_ELICITATION_PRIMITIVE_TYPES}, a sequence of strings (list[str], etc.), " + f"or Optional of these types. Nested models and complex types are not allowed." + ) + + +def _is_string_sequence(annotation: type) -> bool: + """Check if annotation is a sequence of strings (list[str], Sequence[str], etc).""" + origin = get_origin(annotation) + # Check if it's a sequence-like type with str elements + if origin: + try: + if issubclass(origin, Sequence): + args = get_args(annotation) + # Should have single str type arg + return len(args) == 1 and args[0] is str + except TypeError: # pragma: no cover + # origin is not a class, so it can't be a subclass of Sequence + pass + return False + + +def _is_primitive_field(annotation: type) -> bool: + """Check if a field is a primitive type allowed in elicitation schemas.""" + # Handle basic primitive types + if annotation in _ELICITATION_PRIMITIVE_TYPES: + return True + + # Handle Union types + origin = get_origin(annotation) + if origin is Union or origin is types.UnionType: + args = get_args(annotation) + # All args must be primitive types, None, or string sequences + return all( + arg is types.NoneType or arg in _ELICITATION_PRIMITIVE_TYPES or _is_string_sequence(arg) for arg in args + ) + + return False + + +async def elicit_with_validation( + session: ServerSession, + message: str, + schema: type[ElicitSchemaModelT], + related_request_id: RequestId | None = None, +) -> ElicitationResult[ElicitSchemaModelT]: + """Elicit information from the client/user with schema validation (form mode). + + This method can be used to interactively ask for additional information from the + client within a tool's execution. The client might display the message to the + user and collect a response according to the provided schema. Or in case a + client is an agent, it might decide how to handle the elicitation -- either by asking + the user or automatically generating a response. + + For sensitive data like credentials or OAuth flows, use elicit_url() instead. + """ + # Validate that schema only contains primitive types and fail loudly if not + _validate_elicitation_schema(schema) + + json_schema = schema.model_json_schema() + + result = await session.elicit_form( + message=message, + requestedSchema=json_schema, + related_request_id=related_request_id, + ) + + if result.action == "accept" and result.content is not None: + # Validate and parse the content using the schema + validated_data = schema.model_validate(result.content) + return AcceptedElicitation(data=validated_data) + elif result.action == "decline": + return DeclinedElicitation() + elif result.action == "cancel": # pragma: no cover + return CancelledElicitation() + else: # pragma: no cover + # This should never happen, but handle it just in case + raise ValueError(f"Unexpected elicitation action: {result.action}") + + +async def elicit_url( + session: ServerSession, + message: str, + url: str, + elicitation_id: str, + related_request_id: RequestId | None = None, +) -> UrlElicitationResult: + """Elicit information from the user via out-of-band URL navigation (URL mode). + + This method directs the user to an external URL where sensitive interactions can + occur without passing data through the MCP client. Use this for: + - Collecting sensitive credentials (API keys, passwords) + - OAuth authorization flows with third-party services + - Payment and subscription flows + - Any interaction where data should not pass through the LLM context + + The response indicates whether the user consented to navigate to the URL. + The actual interaction happens out-of-band. When the elicitation completes, + the server should send an ElicitCompleteNotification to notify the client. + + Args: + session: The server session + message: Human-readable explanation of why the interaction is needed + url: The URL the user should navigate to + elicitation_id: Unique identifier for tracking this elicitation + related_request_id: Optional ID of the request that triggered this elicitation + + Returns: + UrlElicitationResult indicating accept, decline, or cancel + """ + result = await session.elicit_url( + message=message, + url=url, + elicitation_id=elicitation_id, + related_request_id=related_request_id, + ) + + if result.action == "accept": + return AcceptedUrlElicitation() + elif result.action == "decline": + return DeclinedElicitation() + elif result.action == "cancel": + return CancelledElicitation() + else: # pragma: no cover + # This should never happen, but handle it just in case + raise ValueError(f"Unexpected elicitation action: {result.action}") diff --git a/source/target-vendors/cp312/mcp/server/experimental/__init__.py b/source/target-vendors/cp312/mcp/server/experimental/__init__.py new file mode 100644 index 0000000..824bb8b --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/experimental/__init__.py @@ -0,0 +1,11 @@ +""" +Server-side experimental features. + +WARNING: These APIs are experimental and may change without notice. + +Import directly from submodules: +- mcp.server.experimental.task_context.ServerTaskContext +- mcp.server.experimental.task_support.TaskSupport +- mcp.server.experimental.task_result_handler.TaskResultHandler +- mcp.server.experimental.request_context.Experimental +""" diff --git a/source/target-vendors/cp312/mcp/server/experimental/request_context.py b/source/target-vendors/cp312/mcp/server/experimental/request_context.py new file mode 100644 index 0000000..78e75be --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/experimental/request_context.py @@ -0,0 +1,238 @@ +""" +Experimental request context features. + +This module provides the Experimental class which gives access to experimental +features within a request context, such as task-augmented request handling. + +WARNING: These APIs are experimental and may change without notice. +""" + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass, field +from typing import Any + +from mcp.server.experimental.task_context import ServerTaskContext +from mcp.server.experimental.task_support import TaskSupport +from mcp.server.session import ServerSession +from mcp.shared.exceptions import McpError +from mcp.shared.experimental.tasks.helpers import MODEL_IMMEDIATE_RESPONSE_KEY, is_terminal +from mcp.types import ( + METHOD_NOT_FOUND, + TASK_FORBIDDEN, + TASK_REQUIRED, + ClientCapabilities, + CreateTaskResult, + ErrorData, + Result, + TaskExecutionMode, + TaskMetadata, + Tool, +) + + +@dataclass +class Experimental: + """ + Experimental features context for task-augmented requests. + + Provides helpers for validating task execution compatibility and + running tasks with automatic lifecycle management. + + WARNING: This API is experimental and may change without notice. + """ + + task_metadata: TaskMetadata | None = None + _client_capabilities: ClientCapabilities | None = field(default=None, repr=False) + _session: ServerSession | None = field(default=None, repr=False) + _task_support: TaskSupport | None = field(default=None, repr=False) + + @property + def is_task(self) -> bool: + """Check if this request is task-augmented.""" + return self.task_metadata is not None + + @property + def client_supports_tasks(self) -> bool: + """Check if the client declared task support.""" + if self._client_capabilities is None: + return False + return self._client_capabilities.tasks is not None + + def validate_task_mode( + self, + tool_task_mode: TaskExecutionMode | None, + *, + raise_error: bool = True, + ) -> ErrorData | None: + """ + Validate that the request is compatible with the tool's task execution mode. + + Per MCP spec: + - "required": Clients MUST invoke as task. Server returns -32601 if not. + - "forbidden" (or None): Clients MUST NOT invoke as task. Server returns -32601 if they do. + - "optional": Either is acceptable. + + Args: + tool_task_mode: The tool's execution.taskSupport value + ("forbidden", "optional", "required", or None) + raise_error: If True, raises McpError on validation failure. If False, returns ErrorData. + + Returns: + None if valid, ErrorData if invalid and raise_error=False + + Raises: + McpError: If invalid and raise_error=True + """ + + mode = tool_task_mode or TASK_FORBIDDEN + + error: ErrorData | None = None + + if mode == TASK_REQUIRED and not self.is_task: + error = ErrorData( + code=METHOD_NOT_FOUND, + message="This tool requires task-augmented invocation", + ) + elif mode == TASK_FORBIDDEN and self.is_task: + error = ErrorData( + code=METHOD_NOT_FOUND, + message="This tool does not support task-augmented invocation", + ) + + if error is not None and raise_error: + raise McpError(error) + + return error + + def validate_for_tool( + self, + tool: Tool, + *, + raise_error: bool = True, + ) -> ErrorData | None: + """ + Validate that the request is compatible with the given tool. + + Convenience wrapper around validate_task_mode that extracts the mode from a Tool. + + Args: + tool: The Tool definition + raise_error: If True, raises McpError on validation failure. + + Returns: + None if valid, ErrorData if invalid and raise_error=False + """ + mode = tool.execution.taskSupport if tool.execution else None + return self.validate_task_mode(mode, raise_error=raise_error) + + def can_use_tool(self, tool_task_mode: TaskExecutionMode | None) -> bool: + """ + Check if this client can use a tool with the given task mode. + + Useful for filtering tool lists or providing warnings. + Returns False if tool requires "required" but client doesn't support tasks. + + Args: + tool_task_mode: The tool's execution.taskSupport value + + Returns: + True if the client can use this tool, False otherwise + """ + mode = tool_task_mode or TASK_FORBIDDEN + if mode == TASK_REQUIRED and not self.client_supports_tasks: + return False + return True + + async def run_task( + self, + work: Callable[[ServerTaskContext], Awaitable[Result]], + *, + task_id: str | None = None, + model_immediate_response: str | None = None, + ) -> CreateTaskResult: + """ + Create a task, spawn background work, and return CreateTaskResult immediately. + + This is the recommended way to handle task-augmented tool calls. It: + 1. Creates a task in the store + 2. Spawns the work function in a background task + 3. Returns CreateTaskResult immediately + + The work function receives a ServerTaskContext with: + - elicit() for sending elicitation requests + - create_message() for sampling requests + - update_status() for progress updates + - complete()/fail() for finishing the task + + When work() returns a Result, the task is auto-completed with that result. + If work() raises an exception, the task is auto-failed. + + Args: + work: Async function that does the actual work + task_id: Optional task ID (generated if not provided) + model_immediate_response: Optional string to include in _meta as + io.modelcontextprotocol/model-immediate-response + + Returns: + CreateTaskResult to return to the client + + Raises: + RuntimeError: If task support is not enabled or task_metadata is missing + + Example: + @server.call_tool() + async def handle_tool(name: str, args: dict): + ctx = server.request_context + + async def work(task: ServerTaskContext) -> CallToolResult: + result = await task.elicit( + message="Are you sure?", + requestedSchema={"type": "object", ...} + ) + confirmed = result.content.get("confirm", False) + return CallToolResult(content=[TextContent(text="Done" if confirmed else "Cancelled")]) + + return await ctx.experimental.run_task(work) + + WARNING: This API is experimental and may change without notice. + """ + if self._task_support is None: + raise RuntimeError("Task support not enabled. Call server.experimental.enable_tasks() first.") + if self._session is None: + raise RuntimeError("Session not available.") + if self.task_metadata is None: + raise RuntimeError( + "Request is not task-augmented (no task field in params). " + "The client must send a task-augmented request." + ) + + support = self._task_support + # Access task_group via TaskSupport - raises if not in run() context + task_group = support.task_group + + task = await support.store.create_task(self.task_metadata, task_id) + + task_ctx = ServerTaskContext( + task=task, + store=support.store, + session=self._session, + queue=support.queue, + handler=support.handler, + ) + + async def execute() -> None: + try: + result = await work(task_ctx) + if not is_terminal(task_ctx.task.status): + await task_ctx.complete(result) + except Exception as e: + if not is_terminal(task_ctx.task.status): + await task_ctx.fail(str(e)) + + task_group.start_soon(execute) + + meta: dict[str, Any] | None = None + if model_immediate_response is not None: + meta = {MODEL_IMMEDIATE_RESPONSE_KEY: model_immediate_response} + + return CreateTaskResult(task=task, **{"_meta": meta} if meta else {}) diff --git a/source/target-vendors/cp312/mcp/server/experimental/session_features.py b/source/target-vendors/cp312/mcp/server/experimental/session_features.py new file mode 100644 index 0000000..4842da5 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/experimental/session_features.py @@ -0,0 +1,220 @@ +""" +Experimental server session features for server→client task operations. + +This module provides the server-side equivalent of ExperimentalClientFeatures, +allowing the server to send task-augmented requests to the client and poll for results. + +WARNING: These APIs are experimental and may change without notice. +""" + +from collections.abc import AsyncIterator +from typing import TYPE_CHECKING, Any, TypeVar + +import mcp.types as types +from mcp.server.validation import validate_sampling_tools, validate_tool_use_result_messages +from mcp.shared.experimental.tasks.capabilities import ( + require_task_augmented_elicitation, + require_task_augmented_sampling, +) +from mcp.shared.experimental.tasks.polling import poll_until_terminal + +if TYPE_CHECKING: + from mcp.server.session import ServerSession + +ResultT = TypeVar("ResultT", bound=types.Result) + + +class ExperimentalServerSessionFeatures: + """ + Experimental server session features for server→client task operations. + + This provides the server-side equivalent of ExperimentalClientFeatures, + allowing the server to send task-augmented requests to the client and + poll for results. + + WARNING: These APIs are experimental and may change without notice. + + Access via session.experimental: + result = await session.experimental.elicit_as_task(...) + """ + + def __init__(self, session: "ServerSession") -> None: + self._session = session + + async def get_task(self, task_id: str) -> types.GetTaskResult: + """ + Send tasks/get to the client to get task status. + + Args: + task_id: The task identifier + + Returns: + GetTaskResult containing the task status + """ + return await self._session.send_request( + types.ServerRequest(types.GetTaskRequest(params=types.GetTaskRequestParams(taskId=task_id))), + types.GetTaskResult, + ) + + async def get_task_result( + self, + task_id: str, + result_type: type[ResultT], + ) -> ResultT: + """ + Send tasks/result to the client to retrieve the final result. + + Args: + task_id: The task identifier + result_type: The expected result type + + Returns: + The task result, validated against result_type + """ + return await self._session.send_request( + types.ServerRequest(types.GetTaskPayloadRequest(params=types.GetTaskPayloadRequestParams(taskId=task_id))), + result_type, + ) + + async def poll_task(self, task_id: str) -> AsyncIterator[types.GetTaskResult]: + """ + Poll a client task until it reaches terminal status. + + Yields GetTaskResult for each poll, allowing the caller to react to + status changes. Exits when task reaches a terminal status. + + Respects the pollInterval hint from the client. + + Args: + task_id: The task identifier + + Yields: + GetTaskResult for each poll + """ + async for status in poll_until_terminal(self.get_task, task_id): + yield status + + async def elicit_as_task( + self, + message: str, + requestedSchema: types.ElicitRequestedSchema, + *, + ttl: int = 60000, + ) -> types.ElicitResult: + """ + Send a task-augmented elicitation to the client and poll until complete. + + The client will create a local task, process the elicitation asynchronously, + and return the result when ready. This method handles the full flow: + 1. Send elicitation with task field + 2. Receive CreateTaskResult from client + 3. Poll client's task until terminal + 4. Retrieve and return the final ElicitResult + + Args: + message: The message to present to the user + requestedSchema: Schema defining the expected response + ttl: Task time-to-live in milliseconds + + Returns: + The client's elicitation response + + Raises: + McpError: If client doesn't support task-augmented elicitation + """ + client_caps = self._session.client_params.capabilities if self._session.client_params else None + require_task_augmented_elicitation(client_caps) + + create_result = await self._session.send_request( + types.ServerRequest( + types.ElicitRequest( + params=types.ElicitRequestFormParams( + message=message, + requestedSchema=requestedSchema, + task=types.TaskMetadata(ttl=ttl), + ) + ) + ), + types.CreateTaskResult, + ) + + task_id = create_result.task.taskId + + async for _ in self.poll_task(task_id): + pass + + return await self.get_task_result(task_id, types.ElicitResult) + + async def create_message_as_task( + self, + messages: list[types.SamplingMessage], + *, + max_tokens: int, + ttl: int = 60000, + system_prompt: str | None = None, + include_context: types.IncludeContext | None = None, + temperature: float | None = None, + stop_sequences: list[str] | None = None, + metadata: dict[str, Any] | None = None, + model_preferences: types.ModelPreferences | None = None, + tools: list[types.Tool] | None = None, + tool_choice: types.ToolChoice | None = None, + ) -> types.CreateMessageResult: + """ + Send a task-augmented sampling request and poll until complete. + + The client will create a local task, process the sampling request + asynchronously, and return the result when ready. + + Args: + messages: The conversation messages for sampling + max_tokens: Maximum tokens in the response + ttl: Task time-to-live in milliseconds + system_prompt: Optional system prompt + include_context: Context inclusion strategy + temperature: Sampling temperature + stop_sequences: Stop sequences + metadata: Additional metadata + model_preferences: Model selection preferences + tools: Optional list of tools the LLM can use during sampling + tool_choice: Optional control over tool usage behavior + + Returns: + The sampling result from the client + + Raises: + McpError: If client doesn't support task-augmented sampling or tools + ValueError: If tool_use or tool_result message structure is invalid + """ + client_caps = self._session.client_params.capabilities if self._session.client_params else None + require_task_augmented_sampling(client_caps) + validate_sampling_tools(client_caps, tools, tool_choice) + validate_tool_use_result_messages(messages) + + create_result = await self._session.send_request( + types.ServerRequest( + types.CreateMessageRequest( + params=types.CreateMessageRequestParams( + messages=messages, + maxTokens=max_tokens, + systemPrompt=system_prompt, + includeContext=include_context, + temperature=temperature, + stopSequences=stop_sequences, + metadata=metadata, + modelPreferences=model_preferences, + tools=tools, + toolChoice=tool_choice, + task=types.TaskMetadata(ttl=ttl), + ) + ) + ), + types.CreateTaskResult, + ) + + task_id = create_result.task.taskId + + async for _ in self.poll_task(task_id): + pass + + return await self.get_task_result(task_id, types.CreateMessageResult) diff --git a/source/target-vendors/cp312/mcp/server/experimental/task_context.py b/source/target-vendors/cp312/mcp/server/experimental/task_context.py new file mode 100644 index 0000000..e6e14fc --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/experimental/task_context.py @@ -0,0 +1,612 @@ +""" +ServerTaskContext - Server-integrated task context with elicitation and sampling. + +This wraps the pure TaskContext and adds server-specific functionality: +- Elicitation (task.elicit()) +- Sampling (task.create_message()) +- Status notifications +""" + +from typing import Any + +import anyio + +from mcp.server.experimental.task_result_handler import TaskResultHandler +from mcp.server.session import ServerSession +from mcp.server.validation import validate_sampling_tools, validate_tool_use_result_messages +from mcp.shared.exceptions import McpError +from mcp.shared.experimental.tasks.capabilities import ( + require_task_augmented_elicitation, + require_task_augmented_sampling, +) +from mcp.shared.experimental.tasks.context import TaskContext +from mcp.shared.experimental.tasks.message_queue import QueuedMessage, TaskMessageQueue +from mcp.shared.experimental.tasks.resolver import Resolver +from mcp.shared.experimental.tasks.store import TaskStore +from mcp.types import ( + INVALID_REQUEST, + TASK_STATUS_INPUT_REQUIRED, + TASK_STATUS_WORKING, + ClientCapabilities, + CreateMessageResult, + CreateTaskResult, + ElicitationCapability, + ElicitRequestedSchema, + ElicitResult, + ErrorData, + IncludeContext, + ModelPreferences, + RequestId, + Result, + SamplingCapability, + SamplingMessage, + ServerNotification, + Task, + TaskMetadata, + TaskStatusNotification, + TaskStatusNotificationParams, + Tool, + ToolChoice, +) + + +class ServerTaskContext: + """ + Server-integrated task context with elicitation and sampling. + + This wraps a pure TaskContext and adds server-specific functionality: + - elicit() for sending elicitation requests to the client + - create_message() for sampling requests + - Status notifications via the session + + Example: + async def my_task_work(task: ServerTaskContext) -> CallToolResult: + await task.update_status("Starting...") + + result = await task.elicit( + message="Continue?", + requestedSchema={"type": "object", "properties": {"ok": {"type": "boolean"}}} + ) + + if result.content.get("ok"): + return CallToolResult(content=[TextContent(text="Done!")]) + else: + return CallToolResult(content=[TextContent(text="Cancelled")]) + """ + + def __init__( + self, + *, + task: Task, + store: TaskStore, + session: ServerSession, + queue: TaskMessageQueue, + handler: TaskResultHandler | None = None, + ): + """ + Create a ServerTaskContext. + + Args: + task: The Task object + store: The task store + session: The server session + queue: The message queue for elicitation/sampling + handler: The result handler for response routing (required for elicit/create_message) + """ + self._ctx = TaskContext(task=task, store=store) + self._session = session + self._queue = queue + self._handler = handler + self._store = store + + # Delegate pure properties to inner context + + @property + def task_id(self) -> str: + """The task identifier.""" + return self._ctx.task_id + + @property + def task(self) -> Task: + """The current task state.""" + return self._ctx.task + + @property + def is_cancelled(self) -> bool: + """Whether cancellation has been requested.""" + return self._ctx.is_cancelled + + def request_cancellation(self) -> None: + """Request cancellation of this task.""" + self._ctx.request_cancellation() + + # Enhanced methods with notifications + + async def update_status(self, message: str, *, notify: bool = True) -> None: + """ + Update the task's status message. + + Args: + message: The new status message + notify: Whether to send a notification to the client + """ + await self._ctx.update_status(message) + if notify: + await self._send_notification() + + async def complete(self, result: Result, *, notify: bool = True) -> None: + """ + Mark the task as completed with the given result. + + Args: + result: The task result + notify: Whether to send a notification to the client + """ + await self._ctx.complete(result) + if notify: + await self._send_notification() + + async def fail(self, error: str, *, notify: bool = True) -> None: + """ + Mark the task as failed with an error message. + + Args: + error: The error message + notify: Whether to send a notification to the client + """ + await self._ctx.fail(error) + if notify: + await self._send_notification() + + async def _send_notification(self) -> None: + """Send a task status notification to the client.""" + task = self._ctx.task + await self._session.send_notification( + ServerNotification( + TaskStatusNotification( + params=TaskStatusNotificationParams( + taskId=task.taskId, + status=task.status, + statusMessage=task.statusMessage, + createdAt=task.createdAt, + lastUpdatedAt=task.lastUpdatedAt, + ttl=task.ttl, + pollInterval=task.pollInterval, + ) + ) + ) + ) + + # Server-specific methods: elicitation and sampling + + def _check_elicitation_capability(self) -> None: + """Check if the client supports elicitation.""" + if not self._session.check_client_capability(ClientCapabilities(elicitation=ElicitationCapability())): + raise McpError( + ErrorData( + code=INVALID_REQUEST, + message="Client does not support elicitation capability", + ) + ) + + def _check_sampling_capability(self) -> None: + """Check if the client supports sampling.""" + if not self._session.check_client_capability(ClientCapabilities(sampling=SamplingCapability())): + raise McpError( + ErrorData( + code=INVALID_REQUEST, + message="Client does not support sampling capability", + ) + ) + + async def elicit( + self, + message: str, + requestedSchema: ElicitRequestedSchema, + ) -> ElicitResult: + """ + Send an elicitation request via the task message queue. + + This method: + 1. Checks client capability + 2. Updates task status to "input_required" + 3. Queues the elicitation request + 4. Waits for the response (delivered via tasks/result round-trip) + 5. Updates task status back to "working" + 6. Returns the result + + Args: + message: The message to present to the user + requestedSchema: Schema defining the expected response structure + + Returns: + The client's response + + Raises: + McpError: If client doesn't support elicitation capability + """ + self._check_elicitation_capability() + + if self._handler is None: + raise RuntimeError("handler is required for elicit(). Pass handler= to ServerTaskContext.") + + # Update status to input_required + await self._store.update_task(self.task_id, status=TASK_STATUS_INPUT_REQUIRED) + + # Build the request using session's helper + request = self._session._build_elicit_form_request( # pyright: ignore[reportPrivateUsage] + message=message, + requestedSchema=requestedSchema, + related_task_id=self.task_id, + ) + request_id: RequestId = request.id + + resolver: Resolver[dict[str, Any]] = Resolver() + self._handler._pending_requests[request_id] = resolver # pyright: ignore[reportPrivateUsage] + + queued = QueuedMessage( + type="request", + message=request, + resolver=resolver, + original_request_id=request_id, + ) + await self._queue.enqueue(self.task_id, queued) + + try: + # Wait for response (routed back via TaskResultHandler) + response_data = await resolver.wait() + await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING) + return ElicitResult.model_validate(response_data) + except anyio.get_cancelled_exc_class(): # pragma: no cover + # Coverage can't track async exception handlers reliably. + # This path is tested in test_elicit_restores_status_on_cancellation + # which verifies status is restored to "working" after cancellation. + await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING) + raise + + async def elicit_url( + self, + message: str, + url: str, + elicitation_id: str, + ) -> ElicitResult: + """ + Send a URL mode elicitation request via the task message queue. + + This directs the user to an external URL for out-of-band interactions + like OAuth flows, credential collection, or payment processing. + + This method: + 1. Checks client capability + 2. Updates task status to "input_required" + 3. Queues the elicitation request + 4. Waits for the response (delivered via tasks/result round-trip) + 5. Updates task status back to "working" + 6. Returns the result + + Args: + message: Human-readable explanation of why the interaction is needed + url: The URL the user should navigate to + elicitation_id: Unique identifier for tracking this elicitation + + Returns: + The client's response indicating acceptance, decline, or cancellation + + Raises: + McpError: If client doesn't support elicitation capability + RuntimeError: If handler is not configured + """ + self._check_elicitation_capability() + + if self._handler is None: + raise RuntimeError("handler is required for elicit_url(). Pass handler= to ServerTaskContext.") + + # Update status to input_required + await self._store.update_task(self.task_id, status=TASK_STATUS_INPUT_REQUIRED) + + # Build the request using session's helper + request = self._session._build_elicit_url_request( # pyright: ignore[reportPrivateUsage] + message=message, + url=url, + elicitation_id=elicitation_id, + related_task_id=self.task_id, + ) + request_id: RequestId = request.id + + resolver: Resolver[dict[str, Any]] = Resolver() + self._handler._pending_requests[request_id] = resolver # pyright: ignore[reportPrivateUsage] + + queued = QueuedMessage( + type="request", + message=request, + resolver=resolver, + original_request_id=request_id, + ) + await self._queue.enqueue(self.task_id, queued) + + try: + # Wait for response (routed back via TaskResultHandler) + response_data = await resolver.wait() + await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING) + return ElicitResult.model_validate(response_data) + except anyio.get_cancelled_exc_class(): # pragma: no cover + await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING) + raise + + async def create_message( + self, + messages: list[SamplingMessage], + *, + max_tokens: int, + system_prompt: str | None = None, + include_context: IncludeContext | None = None, + temperature: float | None = None, + stop_sequences: list[str] | None = None, + metadata: dict[str, Any] | None = None, + model_preferences: ModelPreferences | None = None, + tools: list[Tool] | None = None, + tool_choice: ToolChoice | None = None, + ) -> CreateMessageResult: + """ + Send a sampling request via the task message queue. + + This method: + 1. Checks client capability + 2. Updates task status to "input_required" + 3. Queues the sampling request + 4. Waits for the response (delivered via tasks/result round-trip) + 5. Updates task status back to "working" + 6. Returns the result + + Args: + messages: The conversation messages for sampling + max_tokens: Maximum tokens in the response + system_prompt: Optional system prompt + include_context: Context inclusion strategy + temperature: Sampling temperature + stop_sequences: Stop sequences + metadata: Additional metadata + model_preferences: Model selection preferences + tools: Optional list of tools the LLM can use during sampling + tool_choice: Optional control over tool usage behavior + + Returns: + The sampling result from the client + + Raises: + McpError: If client doesn't support sampling capability or tools + ValueError: If tool_use or tool_result message structure is invalid + """ + self._check_sampling_capability() + client_caps = self._session.client_params.capabilities if self._session.client_params else None + validate_sampling_tools(client_caps, tools, tool_choice) + validate_tool_use_result_messages(messages) + + if self._handler is None: + raise RuntimeError("handler is required for create_message(). Pass handler= to ServerTaskContext.") + + # Update status to input_required + await self._store.update_task(self.task_id, status=TASK_STATUS_INPUT_REQUIRED) + + # Build the request using session's helper + request = self._session._build_create_message_request( # pyright: ignore[reportPrivateUsage] + messages=messages, + max_tokens=max_tokens, + system_prompt=system_prompt, + include_context=include_context, + temperature=temperature, + stop_sequences=stop_sequences, + metadata=metadata, + model_preferences=model_preferences, + tools=tools, + tool_choice=tool_choice, + related_task_id=self.task_id, + ) + request_id: RequestId = request.id + + resolver: Resolver[dict[str, Any]] = Resolver() + self._handler._pending_requests[request_id] = resolver # pyright: ignore[reportPrivateUsage] + + queued = QueuedMessage( + type="request", + message=request, + resolver=resolver, + original_request_id=request_id, + ) + await self._queue.enqueue(self.task_id, queued) + + try: + # Wait for response (routed back via TaskResultHandler) + response_data = await resolver.wait() + await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING) + return CreateMessageResult.model_validate(response_data) + except anyio.get_cancelled_exc_class(): # pragma: no cover + # Coverage can't track async exception handlers reliably. + # This path is tested in test_create_message_restores_status_on_cancellation + # which verifies status is restored to "working" after cancellation. + await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING) + raise + + async def elicit_as_task( + self, + message: str, + requestedSchema: ElicitRequestedSchema, + *, + ttl: int = 60000, + ) -> ElicitResult: + """ + Send a task-augmented elicitation via the queue, then poll client. + + This is for use inside a task-augmented tool call when you want the client + to handle the elicitation as its own task. The elicitation request is queued + and delivered when the client calls tasks/result. After the client responds + with CreateTaskResult, we poll the client's task until complete. + + Args: + message: The message to present to the user + requestedSchema: Schema defining the expected response structure + ttl: Task time-to-live in milliseconds for the client's task + + Returns: + The client's elicitation response + + Raises: + McpError: If client doesn't support task-augmented elicitation + RuntimeError: If handler is not configured + """ + client_caps = self._session.client_params.capabilities if self._session.client_params else None + require_task_augmented_elicitation(client_caps) + + if self._handler is None: + raise RuntimeError("handler is required for elicit_as_task()") + + # Update status to input_required + await self._store.update_task(self.task_id, status=TASK_STATUS_INPUT_REQUIRED) + + request = self._session._build_elicit_form_request( # pyright: ignore[reportPrivateUsage] + message=message, + requestedSchema=requestedSchema, + related_task_id=self.task_id, + task=TaskMetadata(ttl=ttl), + ) + request_id: RequestId = request.id + + resolver: Resolver[dict[str, Any]] = Resolver() + self._handler._pending_requests[request_id] = resolver # pyright: ignore[reportPrivateUsage] + + queued = QueuedMessage( + type="request", + message=request, + resolver=resolver, + original_request_id=request_id, + ) + await self._queue.enqueue(self.task_id, queued) + + try: + # Wait for initial response (CreateTaskResult from client) + response_data = await resolver.wait() + create_result = CreateTaskResult.model_validate(response_data) + client_task_id = create_result.task.taskId + + # Poll the client's task using session.experimental + async for _ in self._session.experimental.poll_task(client_task_id): + pass + + # Get final result from client + result = await self._session.experimental.get_task_result( + client_task_id, + ElicitResult, + ) + + await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING) + return result + + except anyio.get_cancelled_exc_class(): # pragma: no cover + await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING) + raise + + async def create_message_as_task( + self, + messages: list[SamplingMessage], + *, + max_tokens: int, + ttl: int = 60000, + system_prompt: str | None = None, + include_context: IncludeContext | None = None, + temperature: float | None = None, + stop_sequences: list[str] | None = None, + metadata: dict[str, Any] | None = None, + model_preferences: ModelPreferences | None = None, + tools: list[Tool] | None = None, + tool_choice: ToolChoice | None = None, + ) -> CreateMessageResult: + """ + Send a task-augmented sampling request via the queue, then poll client. + + This is for use inside a task-augmented tool call when you want the client + to handle the sampling as its own task. The request is queued and delivered + when the client calls tasks/result. After the client responds with + CreateTaskResult, we poll the client's task until complete. + + Args: + messages: The conversation messages for sampling + max_tokens: Maximum tokens in the response + ttl: Task time-to-live in milliseconds for the client's task + system_prompt: Optional system prompt + include_context: Context inclusion strategy + temperature: Sampling temperature + stop_sequences: Stop sequences + metadata: Additional metadata + model_preferences: Model selection preferences + tools: Optional list of tools the LLM can use during sampling + tool_choice: Optional control over tool usage behavior + + Returns: + The sampling result from the client + + Raises: + McpError: If client doesn't support task-augmented sampling or tools + ValueError: If tool_use or tool_result message structure is invalid + RuntimeError: If handler is not configured + """ + client_caps = self._session.client_params.capabilities if self._session.client_params else None + require_task_augmented_sampling(client_caps) + validate_sampling_tools(client_caps, tools, tool_choice) + validate_tool_use_result_messages(messages) + + if self._handler is None: + raise RuntimeError("handler is required for create_message_as_task()") + + # Update status to input_required + await self._store.update_task(self.task_id, status=TASK_STATUS_INPUT_REQUIRED) + + # Build request WITH task field for task-augmented sampling + request = self._session._build_create_message_request( # pyright: ignore[reportPrivateUsage] + messages=messages, + max_tokens=max_tokens, + system_prompt=system_prompt, + include_context=include_context, + temperature=temperature, + stop_sequences=stop_sequences, + metadata=metadata, + model_preferences=model_preferences, + tools=tools, + tool_choice=tool_choice, + related_task_id=self.task_id, + task=TaskMetadata(ttl=ttl), + ) + request_id: RequestId = request.id + + resolver: Resolver[dict[str, Any]] = Resolver() + self._handler._pending_requests[request_id] = resolver # pyright: ignore[reportPrivateUsage] + + queued = QueuedMessage( + type="request", + message=request, + resolver=resolver, + original_request_id=request_id, + ) + await self._queue.enqueue(self.task_id, queued) + + try: + # Wait for initial response (CreateTaskResult from client) + response_data = await resolver.wait() + create_result = CreateTaskResult.model_validate(response_data) + client_task_id = create_result.task.taskId + + # Poll the client's task using session.experimental + async for _ in self._session.experimental.poll_task(client_task_id): + pass + + # Get final result from client + result = await self._session.experimental.get_task_result( + client_task_id, + CreateMessageResult, + ) + + await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING) + return result + + except anyio.get_cancelled_exc_class(): # pragma: no cover + await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING) + raise diff --git a/source/target-vendors/cp312/mcp/server/experimental/task_result_handler.py b/source/target-vendors/cp312/mcp/server/experimental/task_result_handler.py new file mode 100644 index 0000000..0b86921 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/experimental/task_result_handler.py @@ -0,0 +1,235 @@ +""" +TaskResultHandler - Integrated handler for tasks/result endpoint. + +This implements the dequeue-send-wait pattern from the MCP Tasks spec: +1. Dequeue all pending messages for the task +2. Send them to the client via transport with relatedRequestId routing +3. Wait if task is not in terminal state +4. Return final result when task completes + +This is the core of the task message queue pattern. +""" + +import logging +from typing import Any + +import anyio + +from mcp.server.session import ServerSession +from mcp.shared.exceptions import McpError +from mcp.shared.experimental.tasks.helpers import RELATED_TASK_METADATA_KEY, is_terminal +from mcp.shared.experimental.tasks.message_queue import TaskMessageQueue +from mcp.shared.experimental.tasks.resolver import Resolver +from mcp.shared.experimental.tasks.store import TaskStore +from mcp.shared.message import ServerMessageMetadata, SessionMessage +from mcp.types import ( + INVALID_PARAMS, + ErrorData, + GetTaskPayloadRequest, + GetTaskPayloadResult, + JSONRPCMessage, + RelatedTaskMetadata, + RequestId, +) + +logger = logging.getLogger(__name__) + + +class TaskResultHandler: + """ + Handler for tasks/result that implements the message queue pattern. + + This handler: + 1. Dequeues pending messages (elicitations, notifications) for the task + 2. Sends them to the client via the response stream + 3. Waits for responses and resolves them back to callers + 4. Blocks until task reaches terminal state + 5. Returns the final result + + Usage: + # Create handler with store and queue + handler = TaskResultHandler(task_store, message_queue) + + # Register it with the server + @server.experimental.get_task_result() + async def handle_task_result(req: GetTaskPayloadRequest) -> GetTaskPayloadResult: + ctx = server.request_context + return await handler.handle(req, ctx.session, ctx.request_id) + + # Or use the convenience method + handler.register(server) + """ + + def __init__( + self, + store: TaskStore, + queue: TaskMessageQueue, + ): + self._store = store + self._queue = queue + # Map from internal request ID to resolver for routing responses + self._pending_requests: dict[RequestId, Resolver[dict[str, Any]]] = {} + + async def send_message( + self, + session: ServerSession, + message: SessionMessage, + ) -> None: + """ + Send a message via the session. + + This is a helper for delivering queued task messages. + """ + await session.send_message(message) + + async def handle( + self, + request: GetTaskPayloadRequest, + session: ServerSession, + request_id: RequestId, + ) -> GetTaskPayloadResult: + """ + Handle a tasks/result request. + + This implements the dequeue-send-wait loop: + 1. Dequeue all pending messages + 2. Send each via transport with relatedRequestId = this request's ID + 3. If task not terminal, wait for status change + 4. Loop until task is terminal + 5. Return final result + + Args: + request: The GetTaskPayloadRequest + session: The server session for sending messages + request_id: The request ID for relatedRequestId routing + + Returns: + GetTaskPayloadResult with the task's final payload + """ + task_id = request.params.taskId + + while True: + task = await self._store.get_task(task_id) + if task is None: + raise McpError( + ErrorData( + code=INVALID_PARAMS, + message=f"Task not found: {task_id}", + ) + ) + + await self._deliver_queued_messages(task_id, session, request_id) + + # If task is terminal, return result + if is_terminal(task.status): + result = await self._store.get_result(task_id) + # GetTaskPayloadResult is a Result with extra="allow" + # The stored result contains the actual payload data + # Per spec: tasks/result MUST include _meta with related-task metadata + related_task = RelatedTaskMetadata(taskId=task_id) + related_task_meta: dict[str, Any] = {RELATED_TASK_METADATA_KEY: related_task.model_dump(by_alias=True)} + if result is not None: + result_data = result.model_dump(by_alias=True) + existing_meta: dict[str, Any] = result_data.get("_meta") or {} + result_data["_meta"] = {**existing_meta, **related_task_meta} + return GetTaskPayloadResult.model_validate(result_data) + return GetTaskPayloadResult.model_validate({"_meta": related_task_meta}) + + # Wait for task update (status change or new messages) + await self._wait_for_task_update(task_id) + + async def _deliver_queued_messages( + self, + task_id: str, + session: ServerSession, + request_id: RequestId, + ) -> None: + """ + Dequeue and send all pending messages for a task. + + Each message is sent via the session's write stream with + relatedRequestId set so responses route back to this stream. + """ + while True: + message = await self._queue.dequeue(task_id) + if message is None: + break + + # If this is a request (not notification), wait for response + if message.type == "request" and message.resolver is not None: + # Store the resolver so we can route the response back + original_id = message.original_request_id + if original_id is not None: + self._pending_requests[original_id] = message.resolver + + logger.debug("Delivering queued message for task %s: %s", task_id, message.type) + + # Send the message with relatedRequestId for routing + session_message = SessionMessage( + message=JSONRPCMessage(message.message), + metadata=ServerMessageMetadata(related_request_id=request_id), + ) + await self.send_message(session, session_message) + + async def _wait_for_task_update(self, task_id: str) -> None: + """ + Wait for task to be updated (status change or new message). + + Races between store update and queue message - first one wins. + """ + async with anyio.create_task_group() as tg: + + async def wait_for_store() -> None: + try: + await self._store.wait_for_update(task_id) + except Exception: + pass + finally: + tg.cancel_scope.cancel() + + async def wait_for_queue() -> None: + try: + await self._queue.wait_for_message(task_id) + except Exception: + pass + finally: + tg.cancel_scope.cancel() + + tg.start_soon(wait_for_store) + tg.start_soon(wait_for_queue) + + def route_response(self, request_id: RequestId, response: dict[str, Any]) -> bool: + """ + Route a response back to the waiting resolver. + + This is called when a response arrives for a queued request. + + Args: + request_id: The request ID from the response + response: The response data + + Returns: + True if response was routed, False if no pending request + """ + resolver = self._pending_requests.pop(request_id, None) + if resolver is not None and not resolver.done(): + resolver.set_result(response) + return True + return False + + def route_error(self, request_id: RequestId, error: ErrorData) -> bool: + """ + Route an error back to the waiting resolver. + + Args: + request_id: The request ID from the error response + error: The error data + + Returns: + True if error was routed, False if no pending request + """ + resolver = self._pending_requests.pop(request_id, None) + if resolver is not None and not resolver.done(): + resolver.set_exception(McpError(error)) + return True + return False diff --git a/source/target-vendors/cp312/mcp/server/experimental/task_support.py b/source/target-vendors/cp312/mcp/server/experimental/task_support.py new file mode 100644 index 0000000..dbb2ed6 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/experimental/task_support.py @@ -0,0 +1,115 @@ +""" +TaskSupport - Configuration for experimental task support. + +This module provides the TaskSupport class which encapsulates all the +infrastructure needed for task-augmented requests: store, queue, and handler. +""" + +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from dataclasses import dataclass, field + +import anyio +from anyio.abc import TaskGroup + +from mcp.server.experimental.task_result_handler import TaskResultHandler +from mcp.server.session import ServerSession +from mcp.shared.experimental.tasks.in_memory_task_store import InMemoryTaskStore +from mcp.shared.experimental.tasks.message_queue import InMemoryTaskMessageQueue, TaskMessageQueue +from mcp.shared.experimental.tasks.store import TaskStore + + +@dataclass +class TaskSupport: + """ + Configuration for experimental task support. + + Encapsulates the task store, message queue, result handler, and task group + for spawning background work. + + When enabled on a server, this automatically: + - Configures response routing for each session + - Provides default handlers for task operations + - Manages a task group for background task execution + + Example: + # Simple in-memory setup + server.experimental.enable_tasks() + + # Custom store/queue for distributed systems + server.experimental.enable_tasks( + store=RedisTaskStore(redis_url), + queue=RedisTaskMessageQueue(redis_url), + ) + """ + + store: TaskStore + queue: TaskMessageQueue + handler: TaskResultHandler = field(init=False) + _task_group: TaskGroup | None = field(init=False, default=None) + + def __post_init__(self) -> None: + """Create the result handler from store and queue.""" + self.handler = TaskResultHandler(self.store, self.queue) + + @property + def task_group(self) -> TaskGroup: + """Get the task group for spawning background work. + + Raises: + RuntimeError: If not within a run() context + """ + if self._task_group is None: + raise RuntimeError("TaskSupport not running. Ensure Server.run() is active.") + return self._task_group + + @asynccontextmanager + async def run(self) -> AsyncIterator[None]: + """ + Run the task support lifecycle. + + This creates a task group for spawning background task work. + Called automatically by Server.run(). + + Usage: + async with task_support.run(): + # Task group is now available + ... + """ + async with anyio.create_task_group() as tg: + self._task_group = tg + try: + yield + finally: + self._task_group = None + + def configure_session(self, session: ServerSession) -> None: + """ + Configure a session for task support. + + This registers the result handler as a response router so that + responses to queued requests (elicitation, sampling) are routed + back to the waiting resolvers. + + Called automatically by Server.run() for each new session. + + Args: + session: The session to configure + """ + session.add_response_router(self.handler) + + @classmethod + def in_memory(cls) -> "TaskSupport": + """ + Create in-memory task support. + + Suitable for development, testing, and single-process servers. + For distributed systems, provide custom store and queue implementations. + + Returns: + TaskSupport configured with in-memory store and queue + """ + return cls( + store=InMemoryTaskStore(), + queue=InMemoryTaskMessageQueue(), + ) diff --git a/source/target-vendors/cp312/mcp/server/fastmcp/__init__.py b/source/target-vendors/cp312/mcp/server/fastmcp/__init__.py new file mode 100644 index 0000000..a89902c --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/fastmcp/__init__.py @@ -0,0 +1,11 @@ +"""FastMCP - A more ergonomic interface for MCP servers.""" + +from importlib.metadata import version + +from mcp.types import Icon + +from .server import Context, FastMCP +from .utilities.types import Audio, Image + +__version__ = version("mcp") +__all__ = ["FastMCP", "Context", "Image", "Audio", "Icon"] diff --git a/source/target-vendors/cp312/mcp/server/fastmcp/exceptions.py b/source/target-vendors/cp312/mcp/server/fastmcp/exceptions.py new file mode 100644 index 0000000..fb5bda1 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/fastmcp/exceptions.py @@ -0,0 +1,21 @@ +"""Custom exceptions for FastMCP.""" + + +class FastMCPError(Exception): + """Base error for FastMCP.""" + + +class ValidationError(FastMCPError): + """Error in validating parameters or return values.""" + + +class ResourceError(FastMCPError): + """Error in resource operations.""" + + +class ToolError(FastMCPError): + """Error in tool operations.""" + + +class InvalidSignature(Exception): + """Invalid signature for use with FastMCP.""" diff --git a/source/target-vendors/cp312/mcp/server/fastmcp/prompts/__init__.py b/source/target-vendors/cp312/mcp/server/fastmcp/prompts/__init__.py new file mode 100644 index 0000000..7637269 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/fastmcp/prompts/__init__.py @@ -0,0 +1,4 @@ +from .base import Prompt +from .manager import PromptManager + +__all__ = ["Prompt", "PromptManager"] diff --git a/source/target-vendors/cp312/mcp/server/fastmcp/prompts/base.py b/source/target-vendors/cp312/mcp/server/fastmcp/prompts/base.py new file mode 100644 index 0000000..48c65b5 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/fastmcp/prompts/base.py @@ -0,0 +1,183 @@ +"""Base classes for FastMCP prompts.""" + +from __future__ import annotations + +import inspect +from collections.abc import Awaitable, Callable, Sequence +from typing import TYPE_CHECKING, Any, Literal + +import pydantic_core +from pydantic import BaseModel, Field, TypeAdapter, validate_call + +from mcp.server.fastmcp.utilities.context_injection import find_context_parameter, inject_context +from mcp.server.fastmcp.utilities.func_metadata import func_metadata +from mcp.types import ContentBlock, Icon, TextContent + +if TYPE_CHECKING: + from mcp.server.fastmcp.server import Context + from mcp.server.session import ServerSessionT + from mcp.shared.context import LifespanContextT, RequestT + + +class Message(BaseModel): + """Base class for all prompt messages.""" + + role: Literal["user", "assistant"] + content: ContentBlock + + def __init__(self, content: str | ContentBlock, **kwargs: Any): + if isinstance(content, str): + content = TextContent(type="text", text=content) + super().__init__(content=content, **kwargs) + + +class UserMessage(Message): + """A message from the user.""" + + role: Literal["user", "assistant"] = "user" + + def __init__(self, content: str | ContentBlock, **kwargs: Any): + super().__init__(content=content, **kwargs) + + +class AssistantMessage(Message): + """A message from the assistant.""" + + role: Literal["user", "assistant"] = "assistant" + + def __init__(self, content: str | ContentBlock, **kwargs: Any): + super().__init__(content=content, **kwargs) + + +message_validator = TypeAdapter[UserMessage | AssistantMessage](UserMessage | AssistantMessage) + +SyncPromptResult = str | Message | dict[str, Any] | Sequence[str | Message | dict[str, Any]] +PromptResult = SyncPromptResult | Awaitable[SyncPromptResult] + + +class PromptArgument(BaseModel): + """An argument that can be passed to a prompt.""" + + name: str = Field(description="Name of the argument") + description: str | None = Field(None, description="Description of what the argument does") + required: bool = Field(default=False, description="Whether the argument is required") + + +class Prompt(BaseModel): + """A prompt template that can be rendered with parameters.""" + + name: str = Field(description="Name of the prompt") + title: str | None = Field(None, description="Human-readable title of the prompt") + description: str | None = Field(None, description="Description of what the prompt does") + arguments: list[PromptArgument] | None = Field(None, description="Arguments that can be passed to the prompt") + fn: Callable[..., PromptResult | Awaitable[PromptResult]] = Field(exclude=True) + icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this prompt") + context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context", exclude=True) + + @classmethod + def from_function( + cls, + fn: Callable[..., PromptResult | Awaitable[PromptResult]], + name: str | None = None, + title: str | None = None, + description: str | None = None, + icons: list[Icon] | None = None, + context_kwarg: str | None = None, + ) -> Prompt: + """Create a Prompt from a function. + + The function can return: + - A string (converted to a message) + - A Message object + - A dict (converted to a message) + - A sequence of any of the above + """ + func_name = name or fn.__name__ + + if func_name == "": # pragma: no cover + raise ValueError("You must provide a name for lambda functions") + + # Find context parameter if it exists + if context_kwarg is None: # pragma: no branch + context_kwarg = find_context_parameter(fn) + + # Get schema from func_metadata, excluding context parameter + func_arg_metadata = func_metadata( + fn, + skip_names=[context_kwarg] if context_kwarg is not None else [], + ) + parameters = func_arg_metadata.arg_model.model_json_schema() + + # Convert parameters to PromptArguments + arguments: list[PromptArgument] = [] + if "properties" in parameters: # pragma: no branch + for param_name, param in parameters["properties"].items(): + required = param_name in parameters.get("required", []) + arguments.append( + PromptArgument( + name=param_name, + description=param.get("description"), + required=required, + ) + ) + + # ensure the arguments are properly cast + fn = validate_call(fn) + + return cls( + name=func_name, + title=title, + description=description or fn.__doc__ or "", + arguments=arguments, + fn=fn, + icons=icons, + context_kwarg=context_kwarg, + ) + + async def render( + self, + arguments: dict[str, Any] | None = None, + context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None, + ) -> list[Message]: + """Render the prompt with arguments.""" + # Validate required arguments + if self.arguments: + required = {arg.name for arg in self.arguments if arg.required} + provided = set(arguments or {}) + missing = required - provided + if missing: + raise ValueError(f"Missing required arguments: {missing}") + + try: + # Add context to arguments if needed + call_args = inject_context(self.fn, arguments or {}, context, self.context_kwarg) + + # Call function and check if result is a coroutine + result = self.fn(**call_args) + if inspect.iscoroutine(result): + result = await result + + # Validate messages + if not isinstance(result, list | tuple): + result = [result] + + # Convert result to messages + messages: list[Message] = [] + for msg in result: # type: ignore[reportUnknownVariableType] + try: + if isinstance(msg, Message): + messages.append(msg) + elif isinstance(msg, dict): + messages.append(message_validator.validate_python(msg)) + elif isinstance(msg, str): + content = TextContent(type="text", text=msg) + messages.append(UserMessage(content=content)) + else: # pragma: no cover + content = pydantic_core.to_json(msg, fallback=str, indent=2).decode() + messages.append(Message(role="user", content=content)) + except Exception: # pragma: no cover + raise ValueError(f"Could not convert prompt result to message: {msg}") + + return messages + except Exception as e: # pragma: no cover + raise ValueError(f"Error rendering prompt {self.name}: {e}") diff --git a/source/target-vendors/cp312/mcp/server/fastmcp/prompts/manager.py b/source/target-vendors/cp312/mcp/server/fastmcp/prompts/manager.py new file mode 100644 index 0000000..6d032c7 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/fastmcp/prompts/manager.py @@ -0,0 +1,60 @@ +"""Prompt management functionality.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from mcp.server.fastmcp.prompts.base import Message, Prompt +from mcp.server.fastmcp.utilities.logging import get_logger + +if TYPE_CHECKING: + from mcp.server.fastmcp.server import Context + from mcp.server.session import ServerSessionT + from mcp.shared.context import LifespanContextT, RequestT + +logger = get_logger(__name__) + + +class PromptManager: + """Manages FastMCP prompts.""" + + def __init__(self, warn_on_duplicate_prompts: bool = True): + self._prompts: dict[str, Prompt] = {} + self.warn_on_duplicate_prompts = warn_on_duplicate_prompts + + def get_prompt(self, name: str) -> Prompt | None: + """Get prompt by name.""" + return self._prompts.get(name) + + def list_prompts(self) -> list[Prompt]: + """List all registered prompts.""" + return list(self._prompts.values()) + + def add_prompt( + self, + prompt: Prompt, + ) -> Prompt: + """Add a prompt to the manager.""" + + # Check for duplicates + existing = self._prompts.get(prompt.name) + if existing: + if self.warn_on_duplicate_prompts: + logger.warning(f"Prompt already exists: {prompt.name}") + return existing + + self._prompts[prompt.name] = prompt + return prompt + + async def render_prompt( + self, + name: str, + arguments: dict[str, Any] | None = None, + context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None, + ) -> list[Message]: + """Render a prompt by name with arguments.""" + prompt = self.get_prompt(name) + if not prompt: + raise ValueError(f"Unknown prompt: {name}") + + return await prompt.render(arguments, context=context) diff --git a/source/target-vendors/cp312/mcp/server/fastmcp/resources/__init__.py b/source/target-vendors/cp312/mcp/server/fastmcp/resources/__init__.py new file mode 100644 index 0000000..b5805fb --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/fastmcp/resources/__init__.py @@ -0,0 +1,23 @@ +from .base import Resource +from .resource_manager import ResourceManager +from .templates import ResourceTemplate +from .types import ( + BinaryResource, + DirectoryResource, + FileResource, + FunctionResource, + HttpResource, + TextResource, +) + +__all__ = [ + "Resource", + "TextResource", + "BinaryResource", + "FunctionResource", + "FileResource", + "HttpResource", + "DirectoryResource", + "ResourceTemplate", + "ResourceManager", +] diff --git a/source/target-vendors/cp312/mcp/server/fastmcp/resources/base.py b/source/target-vendors/cp312/mcp/server/fastmcp/resources/base.py new file mode 100644 index 0000000..e34b97a --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/fastmcp/resources/base.py @@ -0,0 +1,50 @@ +"""Base classes and interfaces for FastMCP resources.""" + +import abc +from typing import Annotated, Any + +from pydantic import ( + AnyUrl, + BaseModel, + ConfigDict, + Field, + UrlConstraints, + ValidationInfo, + field_validator, +) + +from mcp.types import Annotations, Icon + + +class Resource(BaseModel, abc.ABC): + """Base class for all resources.""" + + model_config = ConfigDict(validate_default=True) + + uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] = Field(default=..., description="URI of the resource") + name: str | None = Field(description="Name of the resource", default=None) + title: str | None = Field(description="Human-readable title of the resource", default=None) + description: str | None = Field(description="Description of the resource", default=None) + mime_type: str = Field( + default="text/plain", + description="MIME type of the resource content", + pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+(;\s*[a-zA-Z0-9\-_.]+=[a-zA-Z0-9\-_.]+)*$", + ) + icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this resource") + annotations: Annotations | None = Field(default=None, description="Optional annotations for the resource") + meta: dict[str, Any] | None = Field(default=None, description="Optional metadata for this resource") + + @field_validator("name", mode="before") + @classmethod + def set_default_name(cls, name: str | None, info: ValidationInfo) -> str: + """Set default name from URI if not provided.""" + if name: + return name + if uri := info.data.get("uri"): + return str(uri) + raise ValueError("Either name or uri must be provided") + + @abc.abstractmethod + async def read(self) -> str | bytes: + """Read the resource content.""" + pass # pragma: no cover diff --git a/source/target-vendors/cp312/mcp/server/fastmcp/resources/resource_manager.py b/source/target-vendors/cp312/mcp/server/fastmcp/resources/resource_manager.py new file mode 100644 index 0000000..20f67bb --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/fastmcp/resources/resource_manager.py @@ -0,0 +1,115 @@ +"""Resource manager functionality.""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import TYPE_CHECKING, Any + +from pydantic import AnyUrl + +from mcp.server.fastmcp.resources.base import Resource +from mcp.server.fastmcp.resources.templates import ResourceTemplate +from mcp.server.fastmcp.utilities.logging import get_logger +from mcp.types import Annotations, Icon + +if TYPE_CHECKING: + from mcp.server.fastmcp.server import Context + from mcp.server.session import ServerSessionT + from mcp.shared.context import LifespanContextT, RequestT + +logger = get_logger(__name__) + + +class ResourceManager: + """Manages FastMCP resources.""" + + def __init__(self, warn_on_duplicate_resources: bool = True): + self._resources: dict[str, Resource] = {} + self._templates: dict[str, ResourceTemplate] = {} + self.warn_on_duplicate_resources = warn_on_duplicate_resources + + def add_resource(self, resource: Resource) -> Resource: + """Add a resource to the manager. + + Args: + resource: A Resource instance to add + + Returns: + The added resource. If a resource with the same URI already exists, + returns the existing resource. + """ + logger.debug( + "Adding resource", + extra={ + "uri": resource.uri, + "type": type(resource).__name__, + "resource_name": resource.name, + }, + ) + existing = self._resources.get(str(resource.uri)) + if existing: + if self.warn_on_duplicate_resources: + logger.warning(f"Resource already exists: {resource.uri}") + return existing + self._resources[str(resource.uri)] = resource + return resource + + def add_template( + self, + fn: Callable[..., Any], + uri_template: str, + name: str | None = None, + title: str | None = None, + description: str | None = None, + mime_type: str | None = None, + icons: list[Icon] | None = None, + annotations: Annotations | None = None, + meta: dict[str, Any] | None = None, + ) -> ResourceTemplate: + """Add a template from a function.""" + template = ResourceTemplate.from_function( + fn, + uri_template=uri_template, + name=name, + title=title, + description=description, + mime_type=mime_type, + icons=icons, + annotations=annotations, + meta=meta, + ) + self._templates[template.uri_template] = template + return template + + async def get_resource( + self, + uri: AnyUrl | str, + context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None, + ) -> Resource | None: + """Get resource by URI, checking concrete resources first, then templates.""" + uri_str = str(uri) + logger.debug("Getting resource", extra={"uri": uri_str}) + + # First check concrete resources + if resource := self._resources.get(uri_str): + return resource + + # Then check templates + for template in self._templates.values(): + if params := template.matches(uri_str): + try: + return await template.create_resource(uri_str, params, context=context) + except Exception as e: # pragma: no cover + raise ValueError(f"Error creating resource from template: {e}") + + raise ValueError(f"Unknown resource: {uri}") + + def list_resources(self) -> list[Resource]: + """List all registered resources.""" + logger.debug("Listing resources", extra={"count": len(self._resources)}) + return list(self._resources.values()) + + def list_templates(self) -> list[ResourceTemplate]: + """List all registered templates.""" + logger.debug("Listing templates", extra={"count": len(self._templates)}) + return list(self._templates.values()) diff --git a/source/target-vendors/cp312/mcp/server/fastmcp/resources/templates.py b/source/target-vendors/cp312/mcp/server/fastmcp/resources/templates.py new file mode 100644 index 0000000..89a8ceb --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/fastmcp/resources/templates.py @@ -0,0 +1,122 @@ +"""Resource template functionality.""" + +from __future__ import annotations + +import inspect +import re +from collections.abc import Callable +from typing import TYPE_CHECKING, Any + +from pydantic import BaseModel, Field, validate_call + +from mcp.server.fastmcp.resources.types import FunctionResource, Resource +from mcp.server.fastmcp.utilities.context_injection import find_context_parameter, inject_context +from mcp.server.fastmcp.utilities.func_metadata import func_metadata +from mcp.types import Annotations, Icon + +if TYPE_CHECKING: + from mcp.server.fastmcp.server import Context + from mcp.server.session import ServerSessionT + from mcp.shared.context import LifespanContextT, RequestT + + +class ResourceTemplate(BaseModel): + """A template for dynamically creating resources.""" + + uri_template: str = Field(description="URI template with parameters (e.g. weather://{city}/current)") + name: str = Field(description="Name of the resource") + title: str | None = Field(description="Human-readable title of the resource", default=None) + description: str | None = Field(description="Description of what the resource does") + mime_type: str = Field(default="text/plain", description="MIME type of the resource content") + icons: list[Icon] | None = Field(default=None, description="Optional list of icons for the resource template") + annotations: Annotations | None = Field(default=None, description="Optional annotations for the resource template") + meta: dict[str, Any] | None = Field(default=None, description="Optional metadata for this resource template") + fn: Callable[..., Any] = Field(exclude=True) + parameters: dict[str, Any] = Field(description="JSON schema for function parameters") + context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context") + + @classmethod + def from_function( + cls, + fn: Callable[..., Any], + uri_template: str, + name: str | None = None, + title: str | None = None, + description: str | None = None, + mime_type: str | None = None, + icons: list[Icon] | None = None, + annotations: Annotations | None = None, + meta: dict[str, Any] | None = None, + context_kwarg: str | None = None, + ) -> ResourceTemplate: + """Create a template from a function.""" + func_name = name or fn.__name__ + if func_name == "": + raise ValueError("You must provide a name for lambda functions") # pragma: no cover + + # Find context parameter if it exists + if context_kwarg is None: # pragma: no branch + context_kwarg = find_context_parameter(fn) + + # Get schema from func_metadata, excluding context parameter + func_arg_metadata = func_metadata( + fn, + skip_names=[context_kwarg] if context_kwarg is not None else [], + ) + parameters = func_arg_metadata.arg_model.model_json_schema() + + # ensure the arguments are properly cast + fn = validate_call(fn) + + return cls( + uri_template=uri_template, + name=func_name, + title=title, + description=description or fn.__doc__ or "", + mime_type=mime_type or "text/plain", + icons=icons, + annotations=annotations, + meta=meta, + fn=fn, + parameters=parameters, + context_kwarg=context_kwarg, + ) + + def matches(self, uri: str) -> dict[str, Any] | None: + """Check if URI matches template and extract parameters.""" + # Convert template to regex pattern + pattern = self.uri_template.replace("{", "(?P<").replace("}", ">[^/]+)") + match = re.match(f"^{pattern}$", uri) + if match: + return match.groupdict() + return None + + async def create_resource( + self, + uri: str, + params: dict[str, Any], + context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None, + ) -> Resource: + """Create a resource from the template with the given parameters.""" + try: + # Add context to params if needed + params = inject_context(self.fn, params, context, self.context_kwarg) + + # Call function and check if result is a coroutine + result = self.fn(**params) + if inspect.iscoroutine(result): + result = await result + + return FunctionResource( + uri=uri, # type: ignore + name=self.name, + title=self.title, + description=self.description, + mime_type=self.mime_type, + icons=self.icons, + annotations=self.annotations, + meta=self.meta, + fn=lambda: result, # Capture result in closure + ) + except Exception as e: + raise ValueError(f"Error creating resource from template: {e}") diff --git a/source/target-vendors/cp312/mcp/server/fastmcp/resources/types.py b/source/target-vendors/cp312/mcp/server/fastmcp/resources/types.py new file mode 100644 index 0000000..5f72430 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/fastmcp/resources/types.py @@ -0,0 +1,203 @@ +"""Concrete resource implementations.""" + +import inspect +import json +from collections.abc import Callable +from pathlib import Path +from typing import Any + +import anyio +import anyio.to_thread +import httpx +import pydantic +import pydantic_core +from pydantic import AnyUrl, Field, ValidationInfo, validate_call + +from mcp.server.fastmcp.resources.base import Resource +from mcp.types import Annotations, Icon + + +class TextResource(Resource): + """A resource that reads from a string.""" + + text: str = Field(description="Text content of the resource") + + async def read(self) -> str: + """Read the text content.""" + return self.text # pragma: no cover + + +class BinaryResource(Resource): + """A resource that reads from bytes.""" + + data: bytes = Field(description="Binary content of the resource") + + async def read(self) -> bytes: + """Read the binary content.""" + return self.data # pragma: no cover + + +class FunctionResource(Resource): + """A resource that defers data loading by wrapping a function. + + The function is only called when the resource is read, allowing for lazy loading + of potentially expensive data. This is particularly useful when listing resources, + as the function won't be called until the resource is actually accessed. + + The function can return: + - str for text content (default) + - bytes for binary content + - other types will be converted to JSON + """ + + fn: Callable[[], Any] = Field(exclude=True) + + async def read(self) -> str | bytes: + """Read the resource by calling the wrapped function.""" + try: + # Call the function first to see if it returns a coroutine + result = self.fn() + # If it's a coroutine, await it + if inspect.iscoroutine(result): + result = await result + + if isinstance(result, Resource): # pragma: no cover + return await result.read() + elif isinstance(result, bytes): + return result + elif isinstance(result, str): + return result + else: + return pydantic_core.to_json(result, fallback=str, indent=2).decode() + except Exception as e: + raise ValueError(f"Error reading resource {self.uri}: {e}") + + @classmethod + def from_function( + cls, + fn: Callable[..., Any], + uri: str, + name: str | None = None, + title: str | None = None, + description: str | None = None, + mime_type: str | None = None, + icons: list[Icon] | None = None, + annotations: Annotations | None = None, + meta: dict[str, Any] | None = None, + ) -> "FunctionResource": + """Create a FunctionResource from a function.""" + func_name = name or fn.__name__ + if func_name == "": # pragma: no cover + raise ValueError("You must provide a name for lambda functions") + + # ensure the arguments are properly cast + fn = validate_call(fn) + + return cls( + uri=AnyUrl(uri), + name=func_name, + title=title, + description=description or fn.__doc__ or "", + mime_type=mime_type or "text/plain", + fn=fn, + icons=icons, + annotations=annotations, + meta=meta, + ) + + +class FileResource(Resource): + """A resource that reads from a file. + + Set is_binary=True to read file as binary data instead of text. + """ + + path: Path = Field(description="Path to the file") + is_binary: bool = Field( + default=False, + description="Whether to read the file as binary data", + ) + mime_type: str = Field( + default="text/plain", + description="MIME type of the resource content", + ) + + @pydantic.field_validator("path") + @classmethod + def validate_absolute_path(cls, path: Path) -> Path: # pragma: no cover + """Ensure path is absolute.""" + if not path.is_absolute(): + raise ValueError("Path must be absolute") + return path + + @pydantic.field_validator("is_binary") + @classmethod + def set_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> bool: + """Set is_binary based on mime_type if not explicitly set.""" + if is_binary: + return True + mime_type = info.data.get("mime_type", "text/plain") + return not mime_type.startswith("text/") + + async def read(self) -> str | bytes: + """Read the file content.""" + try: + if self.is_binary: + return await anyio.to_thread.run_sync(self.path.read_bytes) + return await anyio.to_thread.run_sync(self.path.read_text) + except Exception as e: + raise ValueError(f"Error reading file {self.path}: {e}") + + +class HttpResource(Resource): + """A resource that reads from an HTTP endpoint.""" + + url: str = Field(description="URL to fetch content from") + mime_type: str = Field(default="application/json", description="MIME type of the resource content") + + async def read(self) -> str | bytes: + """Read the HTTP content.""" + async with httpx.AsyncClient() as client: # pragma: no cover + response = await client.get(self.url) + response.raise_for_status() + return response.text + + +class DirectoryResource(Resource): + """A resource that lists files in a directory.""" + + path: Path = Field(description="Path to the directory") + recursive: bool = Field(default=False, description="Whether to list files recursively") + pattern: str | None = Field(default=None, description="Optional glob pattern to filter files") + mime_type: str = Field(default="application/json", description="MIME type of the resource content") + + @pydantic.field_validator("path") + @classmethod + def validate_absolute_path(cls, path: Path) -> Path: # pragma: no cover + """Ensure path is absolute.""" + if not path.is_absolute(): + raise ValueError("Path must be absolute") + return path + + def list_files(self) -> list[Path]: # pragma: no cover + """List files in the directory.""" + if not self.path.exists(): + raise FileNotFoundError(f"Directory not found: {self.path}") + if not self.path.is_dir(): + raise NotADirectoryError(f"Not a directory: {self.path}") + + try: + if self.pattern: + return list(self.path.glob(self.pattern)) if not self.recursive else list(self.path.rglob(self.pattern)) + return list(self.path.glob("*")) if not self.recursive else list(self.path.rglob("*")) + except Exception as e: + raise ValueError(f"Error listing directory {self.path}: {e}") + + async def read(self) -> str: # Always returns JSON string # pragma: no cover + """Read the directory listing.""" + try: + files = await anyio.to_thread.run_sync(self.list_files) + file_list = [str(f.relative_to(self.path)) for f in files if f.is_file()] + return json.dumps({"files": file_list}, indent=2) + except Exception as e: + raise ValueError(f"Error reading directory {self.path}: {e}") diff --git a/source/target-vendors/cp312/mcp/server/fastmcp/server.py b/source/target-vendors/cp312/mcp/server/fastmcp/server.py new file mode 100644 index 0000000..7a43bd7 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/fastmcp/server.py @@ -0,0 +1,1349 @@ +"""FastMCP - A more ergonomic interface for MCP servers.""" + +from __future__ import annotations as _annotations + +import inspect +import re +from collections.abc import ( + AsyncIterator, + Awaitable, + Callable, + Collection, + Iterable, + Sequence, +) +from contextlib import AbstractAsyncContextManager, asynccontextmanager +from typing import Any, Generic, Literal + +import anyio +import pydantic_core +from pydantic import BaseModel +from pydantic.networks import AnyUrl +from pydantic_settings import BaseSettings, SettingsConfigDict +from starlette.applications import Starlette +from starlette.middleware import Middleware +from starlette.middleware.authentication import AuthenticationMiddleware +from starlette.requests import Request +from starlette.responses import Response +from starlette.routing import Mount, Route +from starlette.types import Receive, Scope, Send + +from mcp.server.auth.middleware.auth_context import AuthContextMiddleware +from mcp.server.auth.middleware.bearer_auth import ( + BearerAuthBackend, + RequireAuthMiddleware, +) +from mcp.server.auth.provider import ( + OAuthAuthorizationServerProvider, + ProviderTokenVerifier, + TokenVerifier, +) +from mcp.server.auth.settings import AuthSettings +from mcp.server.elicitation import ( + ElicitationResult, + ElicitSchemaModelT, + UrlElicitationResult, + elicit_with_validation, +) +from mcp.server.elicitation import ( + elicit_url as _elicit_url, +) +from mcp.server.fastmcp.exceptions import ResourceError +from mcp.server.fastmcp.prompts import Prompt, PromptManager +from mcp.server.fastmcp.resources import FunctionResource, Resource, ResourceManager +from mcp.server.fastmcp.tools import Tool, ToolManager +from mcp.server.fastmcp.utilities.context_injection import find_context_parameter +from mcp.server.fastmcp.utilities.logging import configure_logging, get_logger +from mcp.server.lowlevel.helper_types import ReadResourceContents +from mcp.server.lowlevel.server import LifespanResultT +from mcp.server.lowlevel.server import Server as MCPServer +from mcp.server.lowlevel.server import lifespan as default_lifespan +from mcp.server.session import ServerSession, ServerSessionT +from mcp.server.sse import SseServerTransport +from mcp.server.stdio import stdio_server +from mcp.server.streamable_http import EventStore +from mcp.server.streamable_http_manager import StreamableHTTPSessionManager +from mcp.server.transport_security import TransportSecuritySettings +from mcp.shared.context import LifespanContextT, RequestContext, RequestT +from mcp.types import Annotations, AnyFunction, ContentBlock, GetPromptResult, Icon, ToolAnnotations +from mcp.types import Prompt as MCPPrompt +from mcp.types import PromptArgument as MCPPromptArgument +from mcp.types import Resource as MCPResource +from mcp.types import ResourceTemplate as MCPResourceTemplate +from mcp.types import Tool as MCPTool + +logger = get_logger(__name__) + + +class Settings(BaseSettings, Generic[LifespanResultT]): + """FastMCP server settings. + + All settings can be configured via environment variables with the prefix FASTMCP_. + For example, FASTMCP_DEBUG=true will set debug=True. + """ + + model_config = SettingsConfigDict( + env_prefix="FASTMCP_", + env_file=".env", + env_nested_delimiter="__", + nested_model_default_partial_update=True, + extra="ignore", + ) + + # Server settings + debug: bool + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + + # HTTP settings + host: str + port: int + mount_path: str + sse_path: str + message_path: str + streamable_http_path: str + + # StreamableHTTP settings + json_response: bool + stateless_http: bool + """Define if the server should create a new transport per request.""" + + # resource settings + warn_on_duplicate_resources: bool + + # tool settings + warn_on_duplicate_tools: bool + + # prompt settings + warn_on_duplicate_prompts: bool + + # TODO(Marcelo): Investigate if this is used. If it is, it's probably a good idea to remove it. + dependencies: list[str] + """A list of dependencies to install in the server environment.""" + + lifespan: Callable[[FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]] | None + """A async context manager that will be called when the server is started.""" + + auth: AuthSettings | None + + # Transport security settings (DNS rebinding protection) + transport_security: TransportSecuritySettings | None + + +def lifespan_wrapper( + app: FastMCP[LifespanResultT], + lifespan: Callable[[FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]], +) -> Callable[[MCPServer[LifespanResultT, Request]], AbstractAsyncContextManager[LifespanResultT]]: + @asynccontextmanager + async def wrap( + _: MCPServer[LifespanResultT, Request], + ) -> AsyncIterator[LifespanResultT]: + async with lifespan(app) as context: + yield context + + return wrap + + +class FastMCP(Generic[LifespanResultT]): + def __init__( # noqa: PLR0913 + self, + name: str | None = None, + instructions: str | None = None, + website_url: str | None = None, + icons: list[Icon] | None = None, + auth_server_provider: (OAuthAuthorizationServerProvider[Any, Any, Any] | None) = None, + token_verifier: TokenVerifier | None = None, + event_store: EventStore | None = None, + retry_interval: int | None = None, + *, + tools: list[Tool] | None = None, + debug: bool = False, + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO", + host: str = "127.0.0.1", + port: int = 8000, + mount_path: str = "/", + sse_path: str = "/sse", + message_path: str = "/messages/", + streamable_http_path: str = "/mcp", + json_response: bool = False, + stateless_http: bool = False, + warn_on_duplicate_resources: bool = True, + warn_on_duplicate_tools: bool = True, + warn_on_duplicate_prompts: bool = True, + dependencies: Collection[str] = (), + lifespan: (Callable[[FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]] | None) = None, + auth: AuthSettings | None = None, + transport_security: TransportSecuritySettings | None = None, + ): + # Auto-enable DNS rebinding protection for localhost (IPv4 and IPv6) + if transport_security is None and host in ("127.0.0.1", "localhost", "::1"): + transport_security = TransportSecuritySettings( + enable_dns_rebinding_protection=True, + allowed_hosts=["127.0.0.1:*", "localhost:*", "[::1]:*"], + allowed_origins=["http://127.0.0.1:*", "http://localhost:*", "http://[::1]:*"], + ) + + self.settings = Settings( + debug=debug, + log_level=log_level, + host=host, + port=port, + mount_path=mount_path, + sse_path=sse_path, + message_path=message_path, + streamable_http_path=streamable_http_path, + json_response=json_response, + stateless_http=stateless_http, + warn_on_duplicate_resources=warn_on_duplicate_resources, + warn_on_duplicate_tools=warn_on_duplicate_tools, + warn_on_duplicate_prompts=warn_on_duplicate_prompts, + dependencies=list(dependencies), + lifespan=lifespan, + auth=auth, + transport_security=transport_security, + ) + + self._mcp_server = MCPServer( + name=name or "FastMCP", + instructions=instructions, + website_url=website_url, + icons=icons, + # TODO(Marcelo): It seems there's a type mismatch between the lifespan type from an FastMCP and Server. + # We need to create a Lifespan type that is a generic on the server type, like Starlette does. + lifespan=(lifespan_wrapper(self, self.settings.lifespan) if self.settings.lifespan else default_lifespan), # type: ignore + ) + self._tool_manager = ToolManager(tools=tools, warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools) + self._resource_manager = ResourceManager(warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources) + self._prompt_manager = PromptManager(warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts) + # Validate auth configuration + if self.settings.auth is not None: + if auth_server_provider and token_verifier: # pragma: no cover + raise ValueError("Cannot specify both auth_server_provider and token_verifier") + if not auth_server_provider and not token_verifier: # pragma: no cover + raise ValueError("Must specify either auth_server_provider or token_verifier when auth is enabled") + elif auth_server_provider or token_verifier: # pragma: no cover + raise ValueError("Cannot specify auth_server_provider or token_verifier without auth settings") + + self._auth_server_provider = auth_server_provider + self._token_verifier = token_verifier + + # Create token verifier from provider if needed (backwards compatibility) + if auth_server_provider and not token_verifier: # pragma: no cover + self._token_verifier = ProviderTokenVerifier(auth_server_provider) + self._event_store = event_store + self._retry_interval = retry_interval + self._custom_starlette_routes: list[Route] = [] + self.dependencies = self.settings.dependencies + self._session_manager: StreamableHTTPSessionManager | None = None + + # Set up MCP protocol handlers + self._setup_handlers() + + # Configure logging + configure_logging(self.settings.log_level) + + @property + def name(self) -> str: + return self._mcp_server.name + + @property + def instructions(self) -> str | None: + return self._mcp_server.instructions + + @property + def website_url(self) -> str | None: + return self._mcp_server.website_url + + @property + def icons(self) -> list[Icon] | None: + return self._mcp_server.icons + + @property + def session_manager(self) -> StreamableHTTPSessionManager: + """Get the StreamableHTTP session manager. + + This is exposed to enable advanced use cases like mounting multiple + FastMCP servers in a single FastAPI application. + + Raises: + RuntimeError: If called before streamable_http_app() has been called. + """ + if self._session_manager is None: # pragma: no cover + raise RuntimeError( + "Session manager can only be accessed after" + "calling streamable_http_app()." + "The session manager is created lazily" + "to avoid unnecessary initialization." + ) + return self._session_manager # pragma: no cover + + def run( + self, + transport: Literal["stdio", "sse", "streamable-http"] = "stdio", + mount_path: str | None = None, + ) -> None: + """Run the FastMCP server. Note this is a synchronous function. + + Args: + transport: Transport protocol to use ("stdio", "sse", or "streamable-http") + mount_path: Optional mount path for SSE transport + """ + TRANSPORTS = Literal["stdio", "sse", "streamable-http"] + if transport not in TRANSPORTS.__args__: # type: ignore # pragma: no cover + raise ValueError(f"Unknown transport: {transport}") + + match transport: + case "stdio": + anyio.run(self.run_stdio_async) + case "sse": # pragma: no cover + anyio.run(lambda: self.run_sse_async(mount_path)) + case "streamable-http": # pragma: no cover + anyio.run(self.run_streamable_http_async) + + def _setup_handlers(self) -> None: + """Set up core MCP protocol handlers.""" + self._mcp_server.list_tools()(self.list_tools) + # Note: we disable the lowlevel server's input validation. + # FastMCP does ad hoc conversion of incoming data before validating - + # for now we preserve this for backwards compatibility. + self._mcp_server.call_tool(validate_input=False)(self.call_tool) + self._mcp_server.list_resources()(self.list_resources) + self._mcp_server.read_resource()(self.read_resource) + self._mcp_server.list_prompts()(self.list_prompts) + self._mcp_server.get_prompt()(self.get_prompt) + self._mcp_server.list_resource_templates()(self.list_resource_templates) + + async def list_tools(self) -> list[MCPTool]: + """List all available tools.""" + tools = self._tool_manager.list_tools() + return [ + MCPTool( + name=info.name, + title=info.title, + description=info.description, + inputSchema=info.parameters, + outputSchema=info.output_schema, + annotations=info.annotations, + icons=info.icons, + _meta=info.meta, + ) + for info in tools + ] + + def get_context(self) -> Context[ServerSession, LifespanResultT, Request]: + """ + Returns a Context object. Note that the context will only be valid + during a request; outside a request, most methods will error. + """ + try: + request_context = self._mcp_server.request_context + except LookupError: + request_context = None + return Context(request_context=request_context, fastmcp=self) + + async def call_tool(self, name: str, arguments: dict[str, Any]) -> Sequence[ContentBlock] | dict[str, Any]: + """Call a tool by name with arguments.""" + context = self.get_context() + return await self._tool_manager.call_tool(name, arguments, context=context, convert_result=True) + + async def list_resources(self) -> list[MCPResource]: + """List all available resources.""" + + resources = self._resource_manager.list_resources() + return [ + MCPResource( + uri=resource.uri, + name=resource.name or "", + title=resource.title, + description=resource.description, + mimeType=resource.mime_type, + icons=resource.icons, + annotations=resource.annotations, + _meta=resource.meta, + ) + for resource in resources + ] + + async def list_resource_templates(self) -> list[MCPResourceTemplate]: + templates = self._resource_manager.list_templates() + return [ + MCPResourceTemplate( + uriTemplate=template.uri_template, + name=template.name, + title=template.title, + description=template.description, + mimeType=template.mime_type, + icons=template.icons, + annotations=template.annotations, + _meta=template.meta, + ) + for template in templates + ] + + async def read_resource(self, uri: AnyUrl | str) -> Iterable[ReadResourceContents]: + """Read a resource by URI.""" + + context = self.get_context() + resource = await self._resource_manager.get_resource(uri, context=context) + if not resource: # pragma: no cover + raise ResourceError(f"Unknown resource: {uri}") + + try: + content = await resource.read() + return [ReadResourceContents(content=content, mime_type=resource.mime_type, meta=resource.meta)] + except Exception as e: # pragma: no cover + logger.exception(f"Error reading resource {uri}") + raise ResourceError(str(e)) + + def add_tool( + self, + fn: AnyFunction, + name: str | None = None, + title: str | None = None, + description: str | None = None, + annotations: ToolAnnotations | None = None, + icons: list[Icon] | None = None, + meta: dict[str, Any] | None = None, + structured_output: bool | None = None, + ) -> None: + """Add a tool to the server. + + The tool function can optionally request a Context object by adding a parameter + with the Context type annotation. See the @tool decorator for examples. + + Args: + fn: The function to register as a tool + name: Optional name for the tool (defaults to function name) + title: Optional human-readable title for the tool + description: Optional description of what the tool does + annotations: Optional ToolAnnotations providing additional tool information + structured_output: Controls whether the tool's output is structured or unstructured + - If None, auto-detects based on the function's return type annotation + - If True, creates a structured tool (return type annotation permitting) + - If False, unconditionally creates an unstructured tool + """ + self._tool_manager.add_tool( + fn, + name=name, + title=title, + description=description, + annotations=annotations, + icons=icons, + meta=meta, + structured_output=structured_output, + ) + + def remove_tool(self, name: str) -> None: + """Remove a tool from the server by name. + + Args: + name: The name of the tool to remove + + Raises: + ToolError: If the tool does not exist + """ + self._tool_manager.remove_tool(name) + + def tool( + self, + name: str | None = None, + title: str | None = None, + description: str | None = None, + annotations: ToolAnnotations | None = None, + icons: list[Icon] | None = None, + meta: dict[str, Any] | None = None, + structured_output: bool | None = None, + ) -> Callable[[AnyFunction], AnyFunction]: + """Decorator to register a tool. + + Tools can optionally request a Context object by adding a parameter with the + Context type annotation. The context provides access to MCP capabilities like + logging, progress reporting, and resource access. + + Args: + name: Optional name for the tool (defaults to function name) + title: Optional human-readable title for the tool + description: Optional description of what the tool does + annotations: Optional ToolAnnotations providing additional tool information + structured_output: Controls whether the tool's output is structured or unstructured + - If None, auto-detects based on the function's return type annotation + - If True, creates a structured tool (return type annotation permitting) + - If False, unconditionally creates an unstructured tool + + Example: + @server.tool() + def my_tool(x: int) -> str: + return str(x) + + @server.tool() + def tool_with_context(x: int, ctx: Context) -> str: + ctx.info(f"Processing {x}") + return str(x) + + @server.tool() + async def async_tool(x: int, context: Context) -> str: + await context.report_progress(50, 100) + return str(x) + """ + # Check if user passed function directly instead of calling decorator + if callable(name): + raise TypeError( + "The @tool decorator was used incorrectly. Did you forget to call it? Use @tool() instead of @tool" + ) + + def decorator(fn: AnyFunction) -> AnyFunction: + self.add_tool( + fn, + name=name, + title=title, + description=description, + annotations=annotations, + icons=icons, + meta=meta, + structured_output=structured_output, + ) + return fn + + return decorator + + def completion(self): + """Decorator to register a completion handler. + + The completion handler receives: + - ref: PromptReference or ResourceTemplateReference + - argument: CompletionArgument with name and partial value + - context: Optional CompletionContext with previously resolved arguments + + Example: + @mcp.completion() + async def handle_completion(ref, argument, context): + if isinstance(ref, ResourceTemplateReference): + # Return completions based on ref, argument, and context + return Completion(values=["option1", "option2"]) + return None + """ + return self._mcp_server.completion() + + def add_resource(self, resource: Resource) -> None: + """Add a resource to the server. + + Args: + resource: A Resource instance to add + """ + self._resource_manager.add_resource(resource) + + def resource( + self, + uri: str, + *, + name: str | None = None, + title: str | None = None, + description: str | None = None, + mime_type: str | None = None, + icons: list[Icon] | None = None, + annotations: Annotations | None = None, + meta: dict[str, Any] | None = None, + ) -> Callable[[AnyFunction], AnyFunction]: + """Decorator to register a function as a resource. + + The function will be called when the resource is read to generate its content. + The function can return: + - str for text content + - bytes for binary content + - other types will be converted to JSON + + If the URI contains parameters (e.g. "resource://{param}") or the function + has parameters, it will be registered as a template resource. + + Args: + uri: URI for the resource (e.g. "resource://my-resource" or "resource://{param}") + name: Optional name for the resource + title: Optional human-readable title for the resource + description: Optional description of the resource + mime_type: Optional MIME type for the resource + meta: Optional metadata dictionary for the resource + + Example: + @server.resource("resource://my-resource") + def get_data() -> str: + return "Hello, world!" + + @server.resource("resource://my-resource") + async get_data() -> str: + data = await fetch_data() + return f"Hello, world! {data}" + + @server.resource("resource://{city}/weather") + def get_weather(city: str) -> str: + return f"Weather for {city}" + + @server.resource("resource://{city}/weather") + async def get_weather(city: str) -> str: + data = await fetch_weather(city) + return f"Weather for {city}: {data}" + """ + # Check if user passed function directly instead of calling decorator + if callable(uri): + raise TypeError( + "The @resource decorator was used incorrectly. " + "Did you forget to call it? Use @resource('uri') instead of @resource" + ) + + def decorator(fn: AnyFunction) -> AnyFunction: + # Check if this should be a template + sig = inspect.signature(fn) + has_uri_params = "{" in uri and "}" in uri + has_func_params = bool(sig.parameters) + + if has_uri_params or has_func_params: + # Check for Context parameter to exclude from validation + context_param = find_context_parameter(fn) + + # Validate that URI params match function params (excluding context) + uri_params = set(re.findall(r"{(\w+)}", uri)) + # We need to remove the context_param from the resource function if + # there is any. + func_params = {p for p in sig.parameters.keys() if p != context_param} + + if uri_params != func_params: + raise ValueError( + f"Mismatch between URI parameters {uri_params} and function parameters {func_params}" + ) + + # Register as template + self._resource_manager.add_template( + fn=fn, + uri_template=uri, + name=name, + title=title, + description=description, + mime_type=mime_type, + icons=icons, + annotations=annotations, + meta=meta, + ) + else: + # Register as regular resource + resource = FunctionResource.from_function( + fn=fn, + uri=uri, + name=name, + title=title, + description=description, + mime_type=mime_type, + icons=icons, + annotations=annotations, + meta=meta, + ) + self.add_resource(resource) + return fn + + return decorator + + def add_prompt(self, prompt: Prompt) -> None: + """Add a prompt to the server. + + Args: + prompt: A Prompt instance to add + """ + self._prompt_manager.add_prompt(prompt) + + def prompt( + self, + name: str | None = None, + title: str | None = None, + description: str | None = None, + icons: list[Icon] | None = None, + ) -> Callable[[AnyFunction], AnyFunction]: + """Decorator to register a prompt. + + Args: + name: Optional name for the prompt (defaults to function name) + title: Optional human-readable title for the prompt + description: Optional description of what the prompt does + + Example: + @server.prompt() + def analyze_table(table_name: str) -> list[Message]: + schema = read_table_schema(table_name) + return [ + { + "role": "user", + "content": f"Analyze this schema:\n{schema}" + } + ] + + @server.prompt() + async def analyze_file(path: str) -> list[Message]: + content = await read_file(path) + return [ + { + "role": "user", + "content": { + "type": "resource", + "resource": { + "uri": f"file://{path}", + "text": content + } + } + } + ] + """ + # Check if user passed function directly instead of calling decorator + if callable(name): + raise TypeError( + "The @prompt decorator was used incorrectly. " + "Did you forget to call it? Use @prompt() instead of @prompt" + ) + + def decorator(func: AnyFunction) -> AnyFunction: + prompt = Prompt.from_function(func, name=name, title=title, description=description, icons=icons) + self.add_prompt(prompt) + return func + + return decorator + + def custom_route( + self, + path: str, + methods: list[str], + name: str | None = None, + include_in_schema: bool = True, + ): + """ + Decorator to register a custom HTTP route on the FastMCP server. + + Allows adding arbitrary HTTP endpoints outside the standard MCP protocol, + which can be useful for OAuth callbacks, health checks, or admin APIs. + The handler function must be an async function that accepts a Starlette + Request and returns a Response. + + Routes using this decorator will not require authorization. It is intended + for uses that are either a part of authorization flows or intended to be + public such as health check endpoints. + + Args: + path: URL path for the route (e.g., "/oauth/callback") + methods: List of HTTP methods to support (e.g., ["GET", "POST"]) + name: Optional name for the route (to reference this route with + Starlette's reverse URL lookup feature) + include_in_schema: Whether to include in OpenAPI schema, defaults to True + + Example: + @server.custom_route("/health", methods=["GET"]) + async def health_check(request: Request) -> Response: + return JSONResponse({"status": "ok"}) + """ + + def decorator( # pragma: no cover + func: Callable[[Request], Awaitable[Response]], + ) -> Callable[[Request], Awaitable[Response]]: + self._custom_starlette_routes.append( + Route( + path, + endpoint=func, + methods=methods, + name=name, + include_in_schema=include_in_schema, + ) + ) + return func + + return decorator # pragma: no cover + + async def run_stdio_async(self) -> None: + """Run the server using stdio transport.""" + async with stdio_server() as (read_stream, write_stream): + await self._mcp_server.run( + read_stream, + write_stream, + self._mcp_server.create_initialization_options(), + ) + + async def run_sse_async(self, mount_path: str | None = None) -> None: # pragma: no cover + """Run the server using SSE transport.""" + import uvicorn + + starlette_app = self.sse_app(mount_path) + + config = uvicorn.Config( + starlette_app, + host=self.settings.host, + port=self.settings.port, + log_level=self.settings.log_level.lower(), + ) + server = uvicorn.Server(config) + await server.serve() + + async def run_streamable_http_async(self) -> None: # pragma: no cover + """Run the server using StreamableHTTP transport.""" + import uvicorn + + starlette_app = self.streamable_http_app() + + config = uvicorn.Config( + starlette_app, + host=self.settings.host, + port=self.settings.port, + log_level=self.settings.log_level.lower(), + ) + server = uvicorn.Server(config) + await server.serve() + + def _normalize_path(self, mount_path: str, endpoint: str) -> str: + """ + Combine mount path and endpoint to return a normalized path. + + Args: + mount_path: The mount path (e.g. "/github" or "/") + endpoint: The endpoint path (e.g. "/messages/") + + Returns: + Normalized path (e.g. "/github/messages/") + """ + # Special case: root path + if mount_path == "/": + return endpoint + + # Remove trailing slash from mount path + if mount_path.endswith("/"): + mount_path = mount_path[:-1] + + # Ensure endpoint starts with slash + if not endpoint.startswith("/"): + endpoint = "/" + endpoint + + # Combine paths + return mount_path + endpoint + + def sse_app(self, mount_path: str | None = None) -> Starlette: + """Return an instance of the SSE server app.""" + from starlette.middleware import Middleware + from starlette.routing import Mount, Route + + # Update mount_path in settings if provided + if mount_path is not None: + self.settings.mount_path = mount_path + + # Create normalized endpoint considering the mount path + normalized_message_endpoint = self._normalize_path(self.settings.mount_path, self.settings.message_path) + + # Set up auth context and dependencies + + sse = SseServerTransport( + normalized_message_endpoint, + security_settings=self.settings.transport_security, + ) + + async def handle_sse(scope: Scope, receive: Receive, send: Send): # pragma: no cover + # Add client ID from auth context into request context if available + + async with sse.connect_sse( + scope, + receive, + send, + ) as streams: + await self._mcp_server.run( + streams[0], + streams[1], + self._mcp_server.create_initialization_options(), + ) + return Response() + + # Create routes + routes: list[Route | Mount] = [] + middleware: list[Middleware] = [] + required_scopes = [] + + # Set up auth if configured + if self.settings.auth: # pragma: no cover + required_scopes = self.settings.auth.required_scopes or [] + + # Add auth middleware if token verifier is available + if self._token_verifier: + middleware = [ + # extract auth info from request (but do not require it) + Middleware( + AuthenticationMiddleware, + backend=BearerAuthBackend(self._token_verifier), + ), + # Add the auth context middleware to store + # authenticated user in a contextvar + Middleware(AuthContextMiddleware), + ] + + # Add auth endpoints if auth server provider is configured + if self._auth_server_provider: + from mcp.server.auth.routes import create_auth_routes + + routes.extend( + create_auth_routes( + provider=self._auth_server_provider, + issuer_url=self.settings.auth.issuer_url, + service_documentation_url=self.settings.auth.service_documentation_url, + client_registration_options=self.settings.auth.client_registration_options, + revocation_options=self.settings.auth.revocation_options, + ) + ) + + # When auth is configured, require authentication + if self._token_verifier: # pragma: no cover + # Determine resource metadata URL + resource_metadata_url = None + if self.settings.auth and self.settings.auth.resource_server_url: + from mcp.server.auth.routes import build_resource_metadata_url + + # Build compliant metadata URL for WWW-Authenticate header + resource_metadata_url = build_resource_metadata_url(self.settings.auth.resource_server_url) + + # Auth is enabled, wrap the endpoints with RequireAuthMiddleware + routes.append( + Route( + self.settings.sse_path, + endpoint=RequireAuthMiddleware(handle_sse, required_scopes, resource_metadata_url), + methods=["GET"], + ) + ) + routes.append( + Mount( + self.settings.message_path, + app=RequireAuthMiddleware(sse.handle_post_message, required_scopes, resource_metadata_url), + ) + ) + else: # pragma: no cover + # Auth is disabled, no need for RequireAuthMiddleware + # Since handle_sse is an ASGI app, we need to create a compatible endpoint + async def sse_endpoint(request: Request) -> Response: + # Convert the Starlette request to ASGI parameters + return await handle_sse(request.scope, request.receive, request._send) # type: ignore[reportPrivateUsage] + + routes.append( + Route( + self.settings.sse_path, + endpoint=sse_endpoint, + methods=["GET"], + ) + ) + routes.append( + Mount( + self.settings.message_path, + app=sse.handle_post_message, + ) + ) + # Add protected resource metadata endpoint if configured as RS + if self.settings.auth and self.settings.auth.resource_server_url: # pragma: no cover + from mcp.server.auth.routes import create_protected_resource_routes + + routes.extend( + create_protected_resource_routes( + resource_url=self.settings.auth.resource_server_url, + authorization_servers=[self.settings.auth.issuer_url], + scopes_supported=self.settings.auth.required_scopes, + ) + ) + + # mount these routes last, so they have the lowest route matching precedence + routes.extend(self._custom_starlette_routes) + + # Create Starlette app with routes and middleware + return Starlette(debug=self.settings.debug, routes=routes, middleware=middleware) + + def streamable_http_app(self) -> Starlette: + """Return an instance of the StreamableHTTP server app.""" + from starlette.middleware import Middleware + + # Create session manager on first call (lazy initialization) + if self._session_manager is None: # pragma: no branch + self._session_manager = StreamableHTTPSessionManager( + app=self._mcp_server, + event_store=self._event_store, + retry_interval=self._retry_interval, + json_response=self.settings.json_response, + stateless=self.settings.stateless_http, # Use the stateless setting + security_settings=self.settings.transport_security, + ) + + # Create the ASGI handler + streamable_http_app = StreamableHTTPASGIApp(self._session_manager) + + # Create routes + routes: list[Route | Mount] = [] + middleware: list[Middleware] = [] + required_scopes = [] + + # Set up auth if configured + if self.settings.auth: # pragma: no cover + required_scopes = self.settings.auth.required_scopes or [] + + # Add auth middleware if token verifier is available + if self._token_verifier: + middleware = [ + Middleware( + AuthenticationMiddleware, + backend=BearerAuthBackend(self._token_verifier), + ), + Middleware(AuthContextMiddleware), + ] + + # Add auth endpoints if auth server provider is configured + if self._auth_server_provider: + from mcp.server.auth.routes import create_auth_routes + + routes.extend( + create_auth_routes( + provider=self._auth_server_provider, + issuer_url=self.settings.auth.issuer_url, + service_documentation_url=self.settings.auth.service_documentation_url, + client_registration_options=self.settings.auth.client_registration_options, + revocation_options=self.settings.auth.revocation_options, + ) + ) + + # Set up routes with or without auth + if self._token_verifier: # pragma: no cover + # Determine resource metadata URL + resource_metadata_url = None + if self.settings.auth and self.settings.auth.resource_server_url: + from mcp.server.auth.routes import build_resource_metadata_url + + # Build compliant metadata URL for WWW-Authenticate header + resource_metadata_url = build_resource_metadata_url(self.settings.auth.resource_server_url) + + routes.append( + Route( + self.settings.streamable_http_path, + endpoint=RequireAuthMiddleware(streamable_http_app, required_scopes, resource_metadata_url), + ) + ) + else: + # Auth is disabled, no wrapper needed + routes.append( + Route( + self.settings.streamable_http_path, + endpoint=streamable_http_app, + ) + ) + + # Add protected resource metadata endpoint if configured as RS + if self.settings.auth and self.settings.auth.resource_server_url: # pragma: no cover + from mcp.server.auth.routes import create_protected_resource_routes + + routes.extend( + create_protected_resource_routes( + resource_url=self.settings.auth.resource_server_url, + authorization_servers=[self.settings.auth.issuer_url], + scopes_supported=self.settings.auth.required_scopes, + ) + ) + + routes.extend(self._custom_starlette_routes) + + return Starlette( + debug=self.settings.debug, + routes=routes, + middleware=middleware, + lifespan=lambda app: self.session_manager.run(), + ) + + async def list_prompts(self) -> list[MCPPrompt]: + """List all available prompts.""" + prompts = self._prompt_manager.list_prompts() + return [ + MCPPrompt( + name=prompt.name, + title=prompt.title, + description=prompt.description, + arguments=[ + MCPPromptArgument( + name=arg.name, + description=arg.description, + required=arg.required, + ) + for arg in (prompt.arguments or []) + ], + icons=prompt.icons, + ) + for prompt in prompts + ] + + async def get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult: + """Get a prompt by name with arguments.""" + try: + prompt = self._prompt_manager.get_prompt(name) + if not prompt: + raise ValueError(f"Unknown prompt: {name}") + + messages = await prompt.render(arguments, context=self.get_context()) + + return GetPromptResult( + description=prompt.description, + messages=pydantic_core.to_jsonable_python(messages), + ) + except Exception as e: + logger.exception(f"Error getting prompt {name}") + raise ValueError(str(e)) + + +class StreamableHTTPASGIApp: + """ + ASGI application for Streamable HTTP server transport. + """ + + def __init__(self, session_manager: StreamableHTTPSessionManager): + self.session_manager = session_manager + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: # pragma: no cover + await self.session_manager.handle_request(scope, receive, send) + + +class Context(BaseModel, Generic[ServerSessionT, LifespanContextT, RequestT]): + """Context object providing access to MCP capabilities. + + This provides a cleaner interface to MCP's RequestContext functionality. + It gets injected into tool and resource functions that request it via type hints. + + To use context in a tool function, add a parameter with the Context type annotation: + + ```python + @server.tool() + def my_tool(x: int, ctx: Context) -> str: + # Log messages to the client + ctx.info(f"Processing {x}") + ctx.debug("Debug info") + ctx.warning("Warning message") + ctx.error("Error message") + + # Report progress + ctx.report_progress(50, 100) + + # Access resources + data = ctx.read_resource("resource://data") + + # Get request info + request_id = ctx.request_id + client_id = ctx.client_id + + return str(x) + ``` + + The context parameter name can be anything as long as it's annotated with Context. + The context is optional - tools that don't need it can omit the parameter. + """ + + _request_context: RequestContext[ServerSessionT, LifespanContextT, RequestT] | None + _fastmcp: FastMCP | None + + def __init__( + self, + *, + request_context: (RequestContext[ServerSessionT, LifespanContextT, RequestT] | None) = None, + fastmcp: FastMCP | None = None, + **kwargs: Any, + ): + super().__init__(**kwargs) + self._request_context = request_context + self._fastmcp = fastmcp + + @property + def fastmcp(self) -> FastMCP: + """Access to the FastMCP server.""" + if self._fastmcp is None: # pragma: no cover + raise ValueError("Context is not available outside of a request") + return self._fastmcp # pragma: no cover + + @property + def request_context( + self, + ) -> RequestContext[ServerSessionT, LifespanContextT, RequestT]: + """Access to the underlying request context.""" + if self._request_context is None: # pragma: no cover + raise ValueError("Context is not available outside of a request") + return self._request_context + + async def report_progress(self, progress: float, total: float | None = None, message: str | None = None) -> None: + """Report progress for the current operation. + + Args: + progress: Current progress value e.g. 24 + total: Optional total value e.g. 100 + message: Optional message e.g. Starting render... + """ + progress_token = self.request_context.meta.progressToken if self.request_context.meta else None + + if progress_token is None: # pragma: no cover + return + + await self.request_context.session.send_progress_notification( + progress_token=progress_token, + progress=progress, + total=total, + message=message, + ) + + async def read_resource(self, uri: str | AnyUrl) -> Iterable[ReadResourceContents]: + """Read a resource by URI. + + Args: + uri: Resource URI to read + + Returns: + The resource content as either text or bytes + """ + assert self._fastmcp is not None, "Context is not available outside of a request" + return await self._fastmcp.read_resource(uri) + + async def elicit( + self, + message: str, + schema: type[ElicitSchemaModelT], + ) -> ElicitationResult[ElicitSchemaModelT]: + """Elicit information from the client/user. + + This method can be used to interactively ask for additional information from the + client within a tool's execution. The client might display the message to the + user and collect a response according to the provided schema. Or in case a + client is an agent, it might decide how to handle the elicitation -- either by asking + the user or automatically generating a response. + + Args: + schema: A Pydantic model class defining the expected response structure, according to the specification, + only primive types are allowed. + message: Optional message to present to the user. If not provided, will use + a default message based on the schema + + Returns: + An ElicitationResult containing the action taken and the data if accepted + + Note: + Check the result.action to determine if the user accepted, declined, or cancelled. + The result.data will only be populated if action is "accept" and validation succeeded. + """ + + return await elicit_with_validation( + session=self.request_context.session, + message=message, + schema=schema, + related_request_id=self.request_id, + ) + + async def elicit_url( + self, + message: str, + url: str, + elicitation_id: str, + ) -> UrlElicitationResult: + """Request URL mode elicitation from the client. + + This directs the user to an external URL for out-of-band interactions + that must not pass through the MCP client. Use this for: + - Collecting sensitive credentials (API keys, passwords) + - OAuth authorization flows with third-party services + - Payment and subscription flows + - Any interaction where data should not pass through the LLM context + + The response indicates whether the user consented to navigate to the URL. + The actual interaction happens out-of-band. When the elicitation completes, + call `self.session.send_elicit_complete(elicitation_id)` to notify the client. + + Args: + message: Human-readable explanation of why the interaction is needed + url: The URL the user should navigate to + elicitation_id: Unique identifier for tracking this elicitation + + Returns: + UrlElicitationResult indicating accept, decline, or cancel + """ + return await _elicit_url( + session=self.request_context.session, + message=message, + url=url, + elicitation_id=elicitation_id, + related_request_id=self.request_id, + ) + + async def log( + self, + level: Literal["debug", "info", "warning", "error"], + message: str, + *, + logger_name: str | None = None, + ) -> None: + """Send a log message to the client. + + Args: + level: Log level (debug, info, warning, error) + message: Log message + logger_name: Optional logger name + **extra: Additional structured data to include + """ + await self.request_context.session.send_log_message( + level=level, + data=message, + logger=logger_name, + related_request_id=self.request_id, + ) + + @property + def client_id(self) -> str | None: + """Get the client ID if available.""" + return ( + getattr(self.request_context.meta, "client_id", None) if self.request_context.meta else None + ) # pragma: no cover + + @property + def request_id(self) -> str: + """Get the unique ID for this request.""" + return str(self.request_context.request_id) + + @property + def session(self): + """Access to the underlying session for advanced usage.""" + return self.request_context.session + + async def close_sse_stream(self) -> None: + """Close the SSE stream to trigger client reconnection. + + This method closes the HTTP connection for the current request, triggering + client reconnection. Events continue to be stored in the event store and will + be replayed when the client reconnects with Last-Event-ID. + + Use this to implement polling behavior during long-running operations - + client will reconnect after the retry interval specified in the priming event. + + Note: + This is a no-op if not using StreamableHTTP transport with event_store. + The callback is only available when event_store is configured. + """ + if self._request_context and self._request_context.close_sse_stream: # pragma: no cover + await self._request_context.close_sse_stream() + + async def close_standalone_sse_stream(self) -> None: + """Close the standalone GET SSE stream to trigger client reconnection. + + This method closes the HTTP connection for the standalone GET stream used + for unsolicited server-to-client notifications. The client SHOULD reconnect + with Last-Event-ID to resume receiving notifications. + + Note: + This is a no-op if not using StreamableHTTP transport with event_store. + Currently, client reconnection for standalone GET streams is NOT + implemented - this is a known gap. + """ + if self._request_context and self._request_context.close_standalone_sse_stream: # pragma: no cover + await self._request_context.close_standalone_sse_stream() + + # Convenience methods for common log levels + async def debug(self, message: str, **extra: Any) -> None: + """Send a debug log message.""" + await self.log("debug", message, **extra) + + async def info(self, message: str, **extra: Any) -> None: + """Send an info log message.""" + await self.log("info", message, **extra) + + async def warning(self, message: str, **extra: Any) -> None: + """Send a warning log message.""" + await self.log("warning", message, **extra) + + async def error(self, message: str, **extra: Any) -> None: + """Send an error log message.""" + await self.log("error", message, **extra) diff --git a/source/target-vendors/cp312/mcp/server/fastmcp/tools/__init__.py b/source/target-vendors/cp312/mcp/server/fastmcp/tools/__init__.py new file mode 100644 index 0000000..ae9c656 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/fastmcp/tools/__init__.py @@ -0,0 +1,4 @@ +from .base import Tool +from .tool_manager import ToolManager + +__all__ = ["Tool", "ToolManager"] diff --git a/source/target-vendors/cp312/mcp/server/fastmcp/tools/base.py b/source/target-vendors/cp312/mcp/server/fastmcp/tools/base.py new file mode 100644 index 0000000..1ae6d90 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/fastmcp/tools/base.py @@ -0,0 +1,126 @@ +from __future__ import annotations as _annotations + +import functools +import inspect +from collections.abc import Callable +from functools import cached_property +from typing import TYPE_CHECKING, Any + +from pydantic import BaseModel, Field + +from mcp.server.fastmcp.exceptions import ToolError +from mcp.server.fastmcp.utilities.context_injection import find_context_parameter +from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata +from mcp.shared.exceptions import UrlElicitationRequiredError +from mcp.shared.tool_name_validation import validate_and_warn_tool_name +from mcp.types import Icon, ToolAnnotations + +if TYPE_CHECKING: + from mcp.server.fastmcp.server import Context + from mcp.server.session import ServerSessionT + from mcp.shared.context import LifespanContextT, RequestT + + +class Tool(BaseModel): + """Internal tool registration info.""" + + fn: Callable[..., Any] = Field(exclude=True) + name: str = Field(description="Name of the tool") + title: str | None = Field(None, description="Human-readable title of the tool") + description: str = Field(description="Description of what the tool does") + parameters: dict[str, Any] = Field(description="JSON schema for tool parameters") + fn_metadata: FuncMetadata = Field( + description="Metadata about the function including a pydantic model for tool arguments" + ) + is_async: bool = Field(description="Whether the tool is async") + context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context") + annotations: ToolAnnotations | None = Field(None, description="Optional annotations for the tool") + icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this tool") + meta: dict[str, Any] | None = Field(default=None, description="Optional metadata for this tool") + + @cached_property + def output_schema(self) -> dict[str, Any] | None: + return self.fn_metadata.output_schema + + @classmethod + def from_function( + cls, + fn: Callable[..., Any], + name: str | None = None, + title: str | None = None, + description: str | None = None, + context_kwarg: str | None = None, + annotations: ToolAnnotations | None = None, + icons: list[Icon] | None = None, + meta: dict[str, Any] | None = None, + structured_output: bool | None = None, + ) -> Tool: + """Create a Tool from a function.""" + func_name = name or fn.__name__ + + validate_and_warn_tool_name(func_name) + + if func_name == "": + raise ValueError("You must provide a name for lambda functions") + + func_doc = description or fn.__doc__ or "" + is_async = _is_async_callable(fn) + + if context_kwarg is None: # pragma: no branch + context_kwarg = find_context_parameter(fn) + + func_arg_metadata = func_metadata( + fn, + skip_names=[context_kwarg] if context_kwarg is not None else [], + structured_output=structured_output, + ) + parameters = func_arg_metadata.arg_model.model_json_schema(by_alias=True) + + return cls( + fn=fn, + name=func_name, + title=title, + description=func_doc, + parameters=parameters, + fn_metadata=func_arg_metadata, + is_async=is_async, + context_kwarg=context_kwarg, + annotations=annotations, + icons=icons, + meta=meta, + ) + + async def run( + self, + arguments: dict[str, Any], + context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None, + convert_result: bool = False, + ) -> Any: + """Run the tool with arguments.""" + try: + result = await self.fn_metadata.call_fn_with_arg_validation( + self.fn, + self.is_async, + arguments, + {self.context_kwarg: context} if self.context_kwarg is not None else None, + ) + + if convert_result: + result = self.fn_metadata.convert_result(result) + + return result + except UrlElicitationRequiredError: + # Re-raise UrlElicitationRequiredError so it can be properly handled + # as an MCP error response with code -32042 + raise + except Exception as e: + raise ToolError(f"Error executing tool {self.name}: {e}") from e + + +def _is_async_callable(obj: Any) -> bool: + while isinstance(obj, functools.partial): # pragma: no cover + obj = obj.func + + return inspect.iscoroutinefunction(obj) or ( + callable(obj) and inspect.iscoroutinefunction(getattr(obj, "__call__", None)) + ) diff --git a/source/target-vendors/cp312/mcp/server/fastmcp/tools/tool_manager.py b/source/target-vendors/cp312/mcp/server/fastmcp/tools/tool_manager.py new file mode 100644 index 0000000..095753d --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/fastmcp/tools/tool_manager.py @@ -0,0 +1,93 @@ +from __future__ import annotations as _annotations + +from collections.abc import Callable +from typing import TYPE_CHECKING, Any + +from mcp.server.fastmcp.exceptions import ToolError +from mcp.server.fastmcp.tools.base import Tool +from mcp.server.fastmcp.utilities.logging import get_logger +from mcp.shared.context import LifespanContextT, RequestT +from mcp.types import Icon, ToolAnnotations + +if TYPE_CHECKING: + from mcp.server.fastmcp.server import Context + from mcp.server.session import ServerSessionT + +logger = get_logger(__name__) + + +class ToolManager: + """Manages FastMCP tools.""" + + def __init__( + self, + warn_on_duplicate_tools: bool = True, + *, + tools: list[Tool] | None = None, + ): + self._tools: dict[str, Tool] = {} + if tools is not None: + for tool in tools: + if warn_on_duplicate_tools and tool.name in self._tools: + logger.warning(f"Tool already exists: {tool.name}") + self._tools[tool.name] = tool + + self.warn_on_duplicate_tools = warn_on_duplicate_tools + + def get_tool(self, name: str) -> Tool | None: + """Get tool by name.""" + return self._tools.get(name) + + def list_tools(self) -> list[Tool]: + """List all registered tools.""" + return list(self._tools.values()) + + def add_tool( + self, + fn: Callable[..., Any], + name: str | None = None, + title: str | None = None, + description: str | None = None, + annotations: ToolAnnotations | None = None, + icons: list[Icon] | None = None, + meta: dict[str, Any] | None = None, + structured_output: bool | None = None, + ) -> Tool: + """Add a tool to the server.""" + tool = Tool.from_function( + fn, + name=name, + title=title, + description=description, + annotations=annotations, + icons=icons, + meta=meta, + structured_output=structured_output, + ) + existing = self._tools.get(tool.name) + if existing: + if self.warn_on_duplicate_tools: + logger.warning(f"Tool already exists: {tool.name}") + return existing + self._tools[tool.name] = tool + return tool + + def remove_tool(self, name: str) -> None: + """Remove a tool by name.""" + if name not in self._tools: + raise ToolError(f"Unknown tool: {name}") + del self._tools[name] + + async def call_tool( + self, + name: str, + arguments: dict[str, Any], + context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None, + convert_result: bool = False, + ) -> Any: + """Call a tool by name with arguments.""" + tool = self.get_tool(name) + if not tool: + raise ToolError(f"Unknown tool: {name}") + + return await tool.run(arguments, context=context, convert_result=convert_result) diff --git a/source/target-vendors/cp312/mcp/server/fastmcp/utilities/__init__.py b/source/target-vendors/cp312/mcp/server/fastmcp/utilities/__init__.py new file mode 100644 index 0000000..be448f9 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/fastmcp/utilities/__init__.py @@ -0,0 +1 @@ +"""FastMCP utility modules.""" diff --git a/source/target-vendors/cp312/mcp/server/fastmcp/utilities/context_injection.py b/source/target-vendors/cp312/mcp/server/fastmcp/utilities/context_injection.py new file mode 100644 index 0000000..66d0cba --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/fastmcp/utilities/context_injection.py @@ -0,0 +1,68 @@ +"""Context injection utilities for FastMCP.""" + +from __future__ import annotations + +import inspect +import typing +from collections.abc import Callable +from typing import Any + + +def find_context_parameter(fn: Callable[..., Any]) -> str | None: + """Find the parameter that should receive the Context object. + + Searches through the function's signature to find a parameter + with a Context type annotation. + + Args: + fn: The function to inspect + + Returns: + The name of the context parameter, or None if not found + """ + from mcp.server.fastmcp.server import Context + + # Get type hints to properly resolve string annotations + try: + hints = typing.get_type_hints(fn) + except Exception: + # If we can't resolve type hints, we can't find the context parameter + return None + + # Check each parameter's type hint + for param_name, annotation in hints.items(): + # Handle direct Context type + if inspect.isclass(annotation) and issubclass(annotation, Context): + return param_name + + # Handle generic types like Optional[Context] + origin = typing.get_origin(annotation) + if origin is not None: + args = typing.get_args(annotation) + for arg in args: + if inspect.isclass(arg) and issubclass(arg, Context): + return param_name + + return None + + +def inject_context( + fn: Callable[..., Any], + kwargs: dict[str, Any], + context: Any | None, + context_kwarg: str | None, +) -> dict[str, Any]: + """Inject context into function kwargs if needed. + + Args: + fn: The function that will be called + kwargs: The current keyword arguments + context: The context object to inject (if any) + context_kwarg: The name of the parameter to inject into + + Returns: + Updated kwargs with context injected if applicable + """ + if context_kwarg is not None and context is not None: + return {**kwargs, context_kwarg: context} + return kwargs diff --git a/source/target-vendors/cp312/mcp/server/fastmcp/utilities/func_metadata.py b/source/target-vendors/cp312/mcp/server/fastmcp/utilities/func_metadata.py new file mode 100644 index 0000000..fa443d2 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/fastmcp/utilities/func_metadata.py @@ -0,0 +1,533 @@ +import inspect +import json +from collections.abc import Awaitable, Callable, Sequence +from itertools import chain +from types import GenericAlias +from typing import Annotated, Any, cast, get_args, get_origin, get_type_hints + +import pydantic_core +from pydantic import ( + BaseModel, + ConfigDict, + Field, + RootModel, + WithJsonSchema, + create_model, +) +from pydantic.fields import FieldInfo +from pydantic.json_schema import GenerateJsonSchema, JsonSchemaWarningKind +from typing_extensions import is_typeddict +from typing_inspection.introspection import ( + UNKNOWN, + AnnotationSource, + ForbiddenQualifier, + inspect_annotation, + is_union_origin, +) + +from mcp.server.fastmcp.exceptions import InvalidSignature +from mcp.server.fastmcp.utilities.logging import get_logger +from mcp.server.fastmcp.utilities.types import Audio, Image +from mcp.types import CallToolResult, ContentBlock, TextContent + +logger = get_logger(__name__) + + +class StrictJsonSchema(GenerateJsonSchema): + """A JSON schema generator that raises exceptions instead of emitting warnings. + + This is used to detect non-serializable types during schema generation. + """ + + def emit_warning(self, kind: JsonSchemaWarningKind, detail: str) -> None: + # Raise an exception instead of emitting a warning + raise ValueError(f"JSON schema warning: {kind} - {detail}") + + +class ArgModelBase(BaseModel): + """A model representing the arguments to a function.""" + + def model_dump_one_level(self) -> dict[str, Any]: + """Return a dict of the model's fields, one level deep. + + That is, sub-models etc are not dumped - they are kept as pydantic models. + """ + kwargs: dict[str, Any] = {} + for field_name, field_info in self.__class__.model_fields.items(): + value = getattr(self, field_name) + # Use the alias if it exists, otherwise use the field name + output_name = field_info.alias if field_info.alias else field_name + kwargs[output_name] = value + return kwargs + + model_config = ConfigDict( + arbitrary_types_allowed=True, + ) + + +class FuncMetadata(BaseModel): + arg_model: Annotated[type[ArgModelBase], WithJsonSchema(None)] + output_schema: dict[str, Any] | None = None + output_model: Annotated[type[BaseModel], WithJsonSchema(None)] | None = None + wrap_output: bool = False + + async def call_fn_with_arg_validation( + self, + fn: Callable[..., Any | Awaitable[Any]], + fn_is_async: bool, + arguments_to_validate: dict[str, Any], + arguments_to_pass_directly: dict[str, Any] | None, + ) -> Any: + """Call the given function with arguments validated and injected. + + Arguments are first attempted to be parsed from JSON, then validated against + the argument model, before being passed to the function. + """ + arguments_pre_parsed = self.pre_parse_json(arguments_to_validate) + arguments_parsed_model = self.arg_model.model_validate(arguments_pre_parsed) + arguments_parsed_dict = arguments_parsed_model.model_dump_one_level() + + arguments_parsed_dict |= arguments_to_pass_directly or {} + + if fn_is_async: + return await fn(**arguments_parsed_dict) + else: + return fn(**arguments_parsed_dict) + + def convert_result(self, result: Any) -> Any: + """ + Convert the result of a function call to the appropriate format for + the lowlevel server tool call handler: + + - If output_model is None, return the unstructured content directly. + - If output_model is not None, convert the result to structured output format + (dict[str, Any]) and return both unstructured and structured content. + + Note: we return unstructured content here **even though the lowlevel server + tool call handler provides generic backwards compatibility serialization of + structured content**. This is for FastMCP backwards compatibility: we need to + retain FastMCP's ad hoc conversion logic for constructing unstructured output + from function return values, whereas the lowlevel server simply serializes + the structured output. + """ + if isinstance(result, CallToolResult): + if self.output_schema is not None: + assert self.output_model is not None, "Output model must be set if output schema is defined" + self.output_model.model_validate(result.structuredContent) + return result + + unstructured_content = _convert_to_content(result) + + if self.output_schema is None: + return unstructured_content + else: + if self.wrap_output: + result = {"result": result} + + assert self.output_model is not None, "Output model must be set if output schema is defined" + validated = self.output_model.model_validate(result) + structured_content = validated.model_dump(mode="json", by_alias=True) + + return (unstructured_content, structured_content) + + def pre_parse_json(self, data: dict[str, Any]) -> dict[str, Any]: + """Pre-parse data from JSON. + + Return a dict with same keys as input but with values parsed from JSON + if appropriate. + + This is to handle cases like `["a", "b", "c"]` being passed in as JSON inside + a string rather than an actual list. Claude desktop is prone to this - in fact + it seems incapable of NOT doing this. For sub-models, it tends to pass + dicts (JSON objects) as JSON strings, which can be pre-parsed here. + """ + new_data = data.copy() # Shallow copy + + # Build a mapping from input keys (including aliases) to field info + key_to_field_info: dict[str, FieldInfo] = {} + for field_name, field_info in self.arg_model.model_fields.items(): + # Map both the field name and its alias (if any) to the field info + key_to_field_info[field_name] = field_info + if field_info.alias: + key_to_field_info[field_info.alias] = field_info + + for data_key, data_value in data.items(): + if data_key not in key_to_field_info: # pragma: no cover + continue + + field_info = key_to_field_info[data_key] + if isinstance(data_value, str) and field_info.annotation is not str: + try: + pre_parsed = json.loads(data_value) + except json.JSONDecodeError: + continue # Not JSON - skip + if isinstance(pre_parsed, str | int | float): + # This is likely that the raw value is e.g. `"hello"` which we + # Should really be parsed as '"hello"' in Python - but if we parse + # it as JSON it'll turn into just 'hello'. So we skip it. + continue + new_data[data_key] = pre_parsed + assert new_data.keys() == data.keys() + return new_data + + model_config = ConfigDict( + arbitrary_types_allowed=True, + ) + + +def func_metadata( + func: Callable[..., Any], + skip_names: Sequence[str] = (), + structured_output: bool | None = None, +) -> FuncMetadata: + """Given a function, return metadata including a pydantic model representing its + signature. + + The use case for this is + ``` + meta = func_metadata(func) + validated_args = meta.arg_model.model_validate(some_raw_data_dict) + return func(**validated_args.model_dump_one_level()) + ``` + + **critically** it also provides pre-parse helper to attempt to parse things from + JSON. + + Args: + func: The function to convert to a pydantic model + skip_names: A list of parameter names to skip. These will not be included in + the model. + structured_output: Controls whether the tool's output is structured or unstructured + - If None, auto-detects based on the function's return type annotation + - If True, creates a structured tool (return type annotation permitting) + - If False, unconditionally creates an unstructured tool + + If structured, creates a Pydantic model for the function's result based on its annotation. + Supports various return types: + - BaseModel subclasses (used directly) + - Primitive types (str, int, float, bool, bytes, None) - wrapped in a + model with a 'result' field + - TypedDict - converted to a Pydantic model with same fields + - Dataclasses and other annotated classes - converted to Pydantic models + - Generic types (list, dict, Union, etc.) - wrapped in a model with a 'result' field + + Returns: + A FuncMetadata object containing: + - arg_model: A pydantic model representing the function's arguments + - output_model: A pydantic model for the return type if output is structured + - output_conversion: Records how function output should be converted before returning. + """ + try: + sig = inspect.signature(func, eval_str=True) + except NameError as e: # pragma: no cover + # This raise could perhaps be skipped, and we (FastMCP) just call + # model_rebuild right before using it 🤷 + raise InvalidSignature(f"Unable to evaluate type annotations for callable {func.__name__!r}") from e + params = sig.parameters + dynamic_pydantic_model_params: dict[str, Any] = {} + for param in params.values(): + if param.name.startswith("_"): # pragma: no cover + raise InvalidSignature(f"Parameter {param.name} of {func.__name__} cannot start with '_'") + if param.name in skip_names: + continue + + annotation = param.annotation if param.annotation is not inspect.Parameter.empty else Any + field_name = param.name + field_kwargs: dict[str, Any] = {} + field_metadata: list[Any] = [] + + if param.annotation is inspect.Parameter.empty: + field_metadata.append(WithJsonSchema({"title": param.name, "type": "string"})) + # Check if the parameter name conflicts with BaseModel attributes + # This is necessary because Pydantic warns about shadowing parent attributes + if hasattr(BaseModel, field_name) and callable(getattr(BaseModel, field_name)): + # Use an alias to avoid the shadowing warning + field_kwargs["alias"] = field_name + # Use a prefixed field name + field_name = f"field_{field_name}" + + if param.default is not inspect.Parameter.empty: + dynamic_pydantic_model_params[field_name] = ( + Annotated[(annotation, *field_metadata, Field(**field_kwargs))], + param.default, + ) + else: + dynamic_pydantic_model_params[field_name] = Annotated[(annotation, *field_metadata, Field(**field_kwargs))] + + arguments_model = create_model( + f"{func.__name__}Arguments", + __base__=ArgModelBase, + **dynamic_pydantic_model_params, + ) + + if structured_output is False: + return FuncMetadata(arg_model=arguments_model) + + # set up structured output support based on return type annotation + + if sig.return_annotation is inspect.Parameter.empty and structured_output is True: + raise InvalidSignature(f"Function {func.__name__}: return annotation required for structured output") + + try: + inspected_return_ann = inspect_annotation(sig.return_annotation, annotation_source=AnnotationSource.FUNCTION) + except ForbiddenQualifier as e: + raise InvalidSignature(f"Function {func.__name__}: return annotation contains an invalid type qualifier") from e + + return_type_expr = inspected_return_ann.type + + # `AnnotationSource.FUNCTION` allows no type qualifier to be used, so `return_type_expr` is guaranteed to *not* be + # unknown (i.e. a bare `Final`). + assert return_type_expr is not UNKNOWN + + if is_union_origin(get_origin(return_type_expr)): + args = get_args(return_type_expr) + # Check if CallToolResult appears in the union (excluding None for Optional check) + if any(isinstance(arg, type) and issubclass(arg, CallToolResult) for arg in args if arg is not type(None)): + raise InvalidSignature( + f"Function {func.__name__}: CallToolResult cannot be used in Union or Optional types. " + "To return empty results, use: CallToolResult(content=[])" + ) + + original_annotation: Any + # if the typehint is CallToolResult, the user either intends to return without validation + # or they provided validation as Annotated metadata + if isinstance(return_type_expr, type) and issubclass(return_type_expr, CallToolResult): + if inspected_return_ann.metadata: + return_type_expr = inspected_return_ann.metadata[0] + if len(inspected_return_ann.metadata) >= 2: + # Reconstruct the original annotation, by preserving the remaining metadata, + # i.e. from `Annotated[CallToolResult, ReturnType, Gt(1)]` to + # `Annotated[ReturnType, Gt(1)]`: + original_annotation = Annotated[ + (return_type_expr, *inspected_return_ann.metadata[1:]) + ] # pragma: no cover + else: + # We only had `Annotated[CallToolResult, ReturnType]`, treat the original annotation + # as beging `ReturnType`: + original_annotation = return_type_expr + else: + return FuncMetadata(arg_model=arguments_model) + else: + original_annotation = sig.return_annotation + + output_model, output_schema, wrap_output = _try_create_model_and_schema( + original_annotation, return_type_expr, func.__name__ + ) + + if output_model is None and structured_output is True: + # Model creation failed or produced warnings - no structured output + raise InvalidSignature( + f"Function {func.__name__}: return type {return_type_expr} is not serializable for structured output" + ) + + return FuncMetadata( + arg_model=arguments_model, + output_schema=output_schema, + output_model=output_model, + wrap_output=wrap_output, + ) + + +def _try_create_model_and_schema( + original_annotation: Any, + type_expr: Any, + func_name: str, +) -> tuple[type[BaseModel] | None, dict[str, Any] | None, bool]: + """Try to create a model and schema for the given annotation without warnings. + + Args: + original_annotation: The original return annotation (may be wrapped in `Annotated`). + type_expr: The underlying type expression derived from the return annotation + (`Annotated` and type qualifiers were stripped). + func_name: The name of the function. + + Returns: + tuple of (model or None, schema or None, wrap_output) + Model and schema are None if warnings occur or creation fails. + wrap_output is True if the result needs to be wrapped in {"result": ...} + """ + model = None + wrap_output = False + + # First handle special case: None + if type_expr is None: + model = _create_wrapped_model(func_name, original_annotation) + wrap_output = True + + # Handle GenericAlias types (list[str], dict[str, int], Union[str, int], etc.) + elif isinstance(type_expr, GenericAlias): + origin = get_origin(type_expr) + + # Special case: dict with string keys can use RootModel + if origin is dict: + args = get_args(type_expr) + if len(args) == 2 and args[0] is str: + # TODO: should we use the original annotation? We are loosing any potential `Annotated` + # metadata for Pydantic here: + model = _create_dict_model(func_name, type_expr) + else: + # dict with non-str keys needs wrapping + model = _create_wrapped_model(func_name, original_annotation) + wrap_output = True + else: + # All other generic types need wrapping (list, tuple, Union, Optional, etc.) + model = _create_wrapped_model(func_name, original_annotation) + wrap_output = True + + # Handle regular type objects + elif isinstance(type_expr, type): + type_annotation = cast(type[Any], type_expr) + + # Case 1: BaseModel subclasses (can be used directly) + if issubclass(type_annotation, BaseModel): + model = type_annotation + + # Case 2: TypedDicts: + elif is_typeddict(type_annotation): + model = _create_model_from_typeddict(type_annotation) + + # Case 3: Primitive types that need wrapping + elif type_annotation in (str, int, float, bool, bytes, type(None)): + model = _create_wrapped_model(func_name, original_annotation) + wrap_output = True + + # Case 4: Other class types (dataclasses, regular classes with annotations) + else: + type_hints = get_type_hints(type_annotation) + if type_hints: + # Classes with type hints can be converted to Pydantic models + model = _create_model_from_class(type_annotation, type_hints) + # Classes without type hints are not serializable - model remains None + + # Handle any other types not covered above + else: + # This includes typing constructs that aren't GenericAlias in Python 3.10 + # (e.g., Union, Optional in some Python versions) + model = _create_wrapped_model(func_name, original_annotation) + wrap_output = True + + if model: + # If we successfully created a model, try to get its schema + # Use StrictJsonSchema to raise exceptions instead of warnings + try: + schema = model.model_json_schema(schema_generator=StrictJsonSchema) + except (TypeError, ValueError, pydantic_core.SchemaError, pydantic_core.ValidationError) as e: + # These are expected errors when a type can't be converted to a Pydantic schema + # TypeError: When Pydantic can't handle the type + # ValueError: When there are issues with the type definition (including our custom warnings) + # SchemaError: When Pydantic can't build a schema + # ValidationError: When validation fails + logger.info(f"Cannot create schema for type {type_expr} in {func_name}: {type(e).__name__}: {e}") + return None, None, False + + return model, schema, wrap_output + + return None, None, False + + +_no_default = object() + + +def _create_model_from_class(cls: type[Any], type_hints: dict[str, Any]) -> type[BaseModel]: + """Create a Pydantic model from an ordinary class. + + The created model will: + - Have the same name as the class + - Have fields with the same names and types as the class's fields + - Include all fields whose type does not include None in the set of required fields + + Precondition: cls must have type hints (i.e., `type_hints` is non-empty) + """ + model_fields: dict[str, Any] = {} + for field_name, field_type in type_hints.items(): + if field_name.startswith("_"): # pragma: no cover + continue + + default = getattr(cls, field_name, _no_default) + if default is _no_default: + model_fields[field_name] = field_type + else: + model_fields[field_name] = (field_type, default) + + return create_model(cls.__name__, __config__=ConfigDict(from_attributes=True), **model_fields) + + +def _create_model_from_typeddict(td_type: type[Any]) -> type[BaseModel]: + """Create a Pydantic model from a TypedDict. + + The created model will have the same name and fields as the TypedDict. + """ + type_hints = get_type_hints(td_type) + required_keys = getattr(td_type, "__required_keys__", set(type_hints.keys())) + + model_fields: dict[str, Any] = {} + for field_name, field_type in type_hints.items(): + if field_name not in required_keys: + # For optional TypedDict fields, set default=None + # This makes them not required in the Pydantic model + # The model should use exclude_unset=True when dumping to get TypedDict semantics + model_fields[field_name] = (field_type, None) + else: + model_fields[field_name] = field_type + + return create_model(td_type.__name__, **model_fields) + + +def _create_wrapped_model(func_name: str, annotation: Any) -> type[BaseModel]: + """Create a model that wraps a type in a 'result' field. + + This is used for primitive types, generic types like list/dict, etc. + """ + model_name = f"{func_name}Output" + + return create_model(model_name, result=annotation) + + +def _create_dict_model(func_name: str, dict_annotation: Any) -> type[BaseModel]: + """Create a RootModel for dict[str, T] types.""" + + class DictModel(RootModel[dict_annotation]): + pass + + # Give it a meaningful name + DictModel.__name__ = f"{func_name}DictOutput" + DictModel.__qualname__ = f"{func_name}DictOutput" + + return DictModel + + +def _convert_to_content( + result: Any, +) -> Sequence[ContentBlock]: + """ + Convert a result to a sequence of content objects. + + Note: This conversion logic comes from previous versions of FastMCP and is being + retained for purposes of backwards compatibility. It produces different unstructured + output than the lowlevel server tool call handler, which just serializes structured + content verbatim. + """ + if result is None: # pragma: no cover + return [] + + if isinstance(result, ContentBlock): + return [result] + + if isinstance(result, Image): + return [result.to_image_content()] + + if isinstance(result, Audio): + return [result.to_audio_content()] + + if isinstance(result, list | tuple): + return list( + chain.from_iterable( + _convert_to_content(item) + for item in result # type: ignore + ) + ) + + if not isinstance(result, str): + result = pydantic_core.to_json(result, fallback=str, indent=2).decode() + + return [TextContent(type="text", text=result)] diff --git a/source/target-vendors/cp312/mcp/server/fastmcp/utilities/logging.py b/source/target-vendors/cp312/mcp/server/fastmcp/utilities/logging.py new file mode 100644 index 0000000..4b47d3b --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/fastmcp/utilities/logging.py @@ -0,0 +1,43 @@ +"""Logging utilities for FastMCP.""" + +import logging +from typing import Literal + + +def get_logger(name: str) -> logging.Logger: + """Get a logger nested under MCPnamespace. + + Args: + name: the name of the logger, which will be prefixed with 'FastMCP.' + + Returns: + a configured logger instance + """ + return logging.getLogger(name) + + +def configure_logging( + level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO", +) -> None: + """Configure logging for MCP. + + Args: + level: the log level to use + """ + handlers: list[logging.Handler] = [] + try: # pragma: no cover + from rich.console import Console + from rich.logging import RichHandler + + handlers.append(RichHandler(console=Console(stderr=True), rich_tracebacks=True)) + except ImportError: # pragma: no cover + pass + + if not handlers: # pragma: no cover + handlers.append(logging.StreamHandler()) + + logging.basicConfig( + level=level, + format="%(message)s", + handlers=handlers, + ) diff --git a/source/target-vendors/cp312/mcp/server/fastmcp/utilities/types.py b/source/target-vendors/cp312/mcp/server/fastmcp/utilities/types.py new file mode 100644 index 0000000..d6928ca --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/fastmcp/utilities/types.py @@ -0,0 +1,101 @@ +"""Common types used across FastMCP.""" + +import base64 +from pathlib import Path + +from mcp.types import AudioContent, ImageContent + + +class Image: + """Helper class for returning images from tools.""" + + def __init__( + self, + path: str | Path | None = None, + data: bytes | None = None, + format: str | None = None, + ): + if path is None and data is None: # pragma: no cover + raise ValueError("Either path or data must be provided") + if path is not None and data is not None: # pragma: no cover + raise ValueError("Only one of path or data can be provided") + + self.path = Path(path) if path else None + self.data = data + self._format = format + self._mime_type = self._get_mime_type() + + def _get_mime_type(self) -> str: + """Get MIME type from format or guess from file extension.""" + if self._format: # pragma: no cover + return f"image/{self._format.lower()}" + + if self.path: + suffix = self.path.suffix.lower() + return { + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".webp": "image/webp", + }.get(suffix, "application/octet-stream") + return "image/png" # pragma: no cover # default for raw binary data + + def to_image_content(self) -> ImageContent: + """Convert to MCP ImageContent.""" + if self.path: + with open(self.path, "rb") as f: + data = base64.b64encode(f.read()).decode() + elif self.data is not None: # pragma: no cover + data = base64.b64encode(self.data).decode() + else: # pragma: no cover + raise ValueError("No image data available") + + return ImageContent(type="image", data=data, mimeType=self._mime_type) + + +class Audio: + """Helper class for returning audio from tools.""" + + def __init__( + self, + path: str | Path | None = None, + data: bytes | None = None, + format: str | None = None, + ): + if not bool(path) ^ bool(data): # pragma: no cover + raise ValueError("Either path or data can be provided") + + self.path = Path(path) if path else None + self.data = data + self._format = format + self._mime_type = self._get_mime_type() + + def _get_mime_type(self) -> str: + """Get MIME type from format or guess from file extension.""" + if self._format: # pragma: no cover + return f"audio/{self._format.lower()}" + + if self.path: + suffix = self.path.suffix.lower() + return { + ".wav": "audio/wav", + ".mp3": "audio/mpeg", + ".ogg": "audio/ogg", + ".flac": "audio/flac", + ".aac": "audio/aac", + ".m4a": "audio/mp4", + }.get(suffix, "application/octet-stream") + return "audio/wav" # pragma: no cover # default for raw binary data + + def to_audio_content(self) -> AudioContent: + """Convert to MCP AudioContent.""" + if self.path: + with open(self.path, "rb") as f: + data = base64.b64encode(f.read()).decode() + elif self.data is not None: # pragma: no cover + data = base64.b64encode(self.data).decode() + else: # pragma: no cover + raise ValueError("No audio data available") + + return AudioContent(type="audio", data=data, mimeType=self._mime_type) diff --git a/source/target-vendors/cp312/mcp/server/lowlevel/__init__.py b/source/target-vendors/cp312/mcp/server/lowlevel/__init__.py new file mode 100644 index 0000000..66df389 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/lowlevel/__init__.py @@ -0,0 +1,3 @@ +from .server import NotificationOptions, Server + +__all__ = ["Server", "NotificationOptions"] diff --git a/source/target-vendors/cp312/mcp/server/lowlevel/experimental.py b/source/target-vendors/cp312/mcp/server/lowlevel/experimental.py new file mode 100644 index 0000000..0e6655b --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/lowlevel/experimental.py @@ -0,0 +1,288 @@ +"""Experimental handlers for the low-level MCP server. + +WARNING: These APIs are experimental and may change without notice. +""" + +from __future__ import annotations + +import logging +from collections.abc import Awaitable, Callable +from typing import TYPE_CHECKING + +from mcp.server.experimental.task_support import TaskSupport +from mcp.server.lowlevel.func_inspection import create_call_wrapper +from mcp.shared.exceptions import McpError +from mcp.shared.experimental.tasks.helpers import cancel_task +from mcp.shared.experimental.tasks.in_memory_task_store import InMemoryTaskStore +from mcp.shared.experimental.tasks.message_queue import InMemoryTaskMessageQueue, TaskMessageQueue +from mcp.shared.experimental.tasks.store import TaskStore +from mcp.types import ( + INVALID_PARAMS, + CancelTaskRequest, + CancelTaskResult, + ErrorData, + GetTaskPayloadRequest, + GetTaskPayloadResult, + GetTaskRequest, + GetTaskResult, + ListTasksRequest, + ListTasksResult, + ServerCapabilities, + ServerResult, + ServerTasksCapability, + ServerTasksRequestsCapability, + TasksCancelCapability, + TasksListCapability, + TasksToolsCapability, +) + +if TYPE_CHECKING: + from mcp.server.lowlevel.server import Server + +logger = logging.getLogger(__name__) + + +class ExperimentalHandlers: + """Experimental request/notification handlers. + + WARNING: These APIs are experimental and may change without notice. + """ + + def __init__( + self, + server: Server, + request_handlers: dict[type, Callable[..., Awaitable[ServerResult]]], + notification_handlers: dict[type, Callable[..., Awaitable[None]]], + ): + self._server = server + self._request_handlers = request_handlers + self._notification_handlers = notification_handlers + self._task_support: TaskSupport | None = None + + @property + def task_support(self) -> TaskSupport | None: + """Get the task support configuration, if enabled.""" + return self._task_support + + def update_capabilities(self, capabilities: ServerCapabilities) -> None: + # Only add tasks capability if handlers are registered + if not any( + req_type in self._request_handlers + for req_type in [GetTaskRequest, ListTasksRequest, CancelTaskRequest, GetTaskPayloadRequest] + ): + return + + capabilities.tasks = ServerTasksCapability() + if ListTasksRequest in self._request_handlers: + capabilities.tasks.list = TasksListCapability() + if CancelTaskRequest in self._request_handlers: + capabilities.tasks.cancel = TasksCancelCapability() + + capabilities.tasks.requests = ServerTasksRequestsCapability( + tools=TasksToolsCapability() + ) # assuming always supported for now + + def enable_tasks( + self, + store: TaskStore | None = None, + queue: TaskMessageQueue | None = None, + ) -> TaskSupport: + """ + Enable experimental task support. + + This sets up the task infrastructure and auto-registers default handlers + for tasks/get, tasks/result, tasks/list, and tasks/cancel. + + Args: + store: Custom TaskStore implementation (defaults to InMemoryTaskStore) + queue: Custom TaskMessageQueue implementation (defaults to InMemoryTaskMessageQueue) + + Returns: + The TaskSupport configuration object + + Example: + # Simple in-memory setup + server.experimental.enable_tasks() + + # Custom store/queue for distributed systems + server.experimental.enable_tasks( + store=RedisTaskStore(redis_url), + queue=RedisTaskMessageQueue(redis_url), + ) + + WARNING: This API is experimental and may change without notice. + """ + if store is None: + store = InMemoryTaskStore() + if queue is None: + queue = InMemoryTaskMessageQueue() + + self._task_support = TaskSupport(store=store, queue=queue) + + # Auto-register default handlers + self._register_default_task_handlers() + + return self._task_support + + def _register_default_task_handlers(self) -> None: + """Register default handlers for task operations.""" + assert self._task_support is not None + support = self._task_support + + # Register get_task handler if not already registered + if GetTaskRequest not in self._request_handlers: + + async def _default_get_task(req: GetTaskRequest) -> ServerResult: + task = await support.store.get_task(req.params.taskId) + if task is None: + raise McpError( + ErrorData( + code=INVALID_PARAMS, + message=f"Task not found: {req.params.taskId}", + ) + ) + return ServerResult( + GetTaskResult( + taskId=task.taskId, + status=task.status, + statusMessage=task.statusMessage, + createdAt=task.createdAt, + lastUpdatedAt=task.lastUpdatedAt, + ttl=task.ttl, + pollInterval=task.pollInterval, + ) + ) + + self._request_handlers[GetTaskRequest] = _default_get_task + + # Register get_task_result handler if not already registered + if GetTaskPayloadRequest not in self._request_handlers: + + async def _default_get_task_result(req: GetTaskPayloadRequest) -> ServerResult: + ctx = self._server.request_context + result = await support.handler.handle(req, ctx.session, ctx.request_id) + return ServerResult(result) + + self._request_handlers[GetTaskPayloadRequest] = _default_get_task_result + + # Register list_tasks handler if not already registered + if ListTasksRequest not in self._request_handlers: + + async def _default_list_tasks(req: ListTasksRequest) -> ServerResult: + cursor = req.params.cursor if req.params else None + tasks, next_cursor = await support.store.list_tasks(cursor) + return ServerResult(ListTasksResult(tasks=tasks, nextCursor=next_cursor)) + + self._request_handlers[ListTasksRequest] = _default_list_tasks + + # Register cancel_task handler if not already registered + if CancelTaskRequest not in self._request_handlers: + + async def _default_cancel_task(req: CancelTaskRequest) -> ServerResult: + result = await cancel_task(support.store, req.params.taskId) + return ServerResult(result) + + self._request_handlers[CancelTaskRequest] = _default_cancel_task + + def list_tasks( + self, + ) -> Callable[ + [Callable[[ListTasksRequest], Awaitable[ListTasksResult]]], + Callable[[ListTasksRequest], Awaitable[ListTasksResult]], + ]: + """Register a handler for listing tasks. + + WARNING: This API is experimental and may change without notice. + """ + + def decorator( + func: Callable[[ListTasksRequest], Awaitable[ListTasksResult]], + ) -> Callable[[ListTasksRequest], Awaitable[ListTasksResult]]: + logger.debug("Registering handler for ListTasksRequest") + wrapper = create_call_wrapper(func, ListTasksRequest) + + async def handler(req: ListTasksRequest) -> ServerResult: + result = await wrapper(req) + return ServerResult(result) + + self._request_handlers[ListTasksRequest] = handler + return func + + return decorator + + def get_task( + self, + ) -> Callable[ + [Callable[[GetTaskRequest], Awaitable[GetTaskResult]]], Callable[[GetTaskRequest], Awaitable[GetTaskResult]] + ]: + """Register a handler for getting task status. + + WARNING: This API is experimental and may change without notice. + """ + + def decorator( + func: Callable[[GetTaskRequest], Awaitable[GetTaskResult]], + ) -> Callable[[GetTaskRequest], Awaitable[GetTaskResult]]: + logger.debug("Registering handler for GetTaskRequest") + wrapper = create_call_wrapper(func, GetTaskRequest) + + async def handler(req: GetTaskRequest) -> ServerResult: + result = await wrapper(req) + return ServerResult(result) + + self._request_handlers[GetTaskRequest] = handler + return func + + return decorator + + def get_task_result( + self, + ) -> Callable[ + [Callable[[GetTaskPayloadRequest], Awaitable[GetTaskPayloadResult]]], + Callable[[GetTaskPayloadRequest], Awaitable[GetTaskPayloadResult]], + ]: + """Register a handler for getting task results/payload. + + WARNING: This API is experimental and may change without notice. + """ + + def decorator( + func: Callable[[GetTaskPayloadRequest], Awaitable[GetTaskPayloadResult]], + ) -> Callable[[GetTaskPayloadRequest], Awaitable[GetTaskPayloadResult]]: + logger.debug("Registering handler for GetTaskPayloadRequest") + wrapper = create_call_wrapper(func, GetTaskPayloadRequest) + + async def handler(req: GetTaskPayloadRequest) -> ServerResult: + result = await wrapper(req) + return ServerResult(result) + + self._request_handlers[GetTaskPayloadRequest] = handler + return func + + return decorator + + def cancel_task( + self, + ) -> Callable[ + [Callable[[CancelTaskRequest], Awaitable[CancelTaskResult]]], + Callable[[CancelTaskRequest], Awaitable[CancelTaskResult]], + ]: + """Register a handler for cancelling tasks. + + WARNING: This API is experimental and may change without notice. + """ + + def decorator( + func: Callable[[CancelTaskRequest], Awaitable[CancelTaskResult]], + ) -> Callable[[CancelTaskRequest], Awaitable[CancelTaskResult]]: + logger.debug("Registering handler for CancelTaskRequest") + wrapper = create_call_wrapper(func, CancelTaskRequest) + + async def handler(req: CancelTaskRequest) -> ServerResult: + result = await wrapper(req) + return ServerResult(result) + + self._request_handlers[CancelTaskRequest] = handler + return func + + return decorator diff --git a/source/target-vendors/cp312/mcp/server/lowlevel/func_inspection.py b/source/target-vendors/cp312/mcp/server/lowlevel/func_inspection.py new file mode 100644 index 0000000..6231aa8 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/lowlevel/func_inspection.py @@ -0,0 +1,54 @@ +import inspect +from collections.abc import Callable +from typing import Any, TypeVar, get_type_hints + +T = TypeVar("T") +R = TypeVar("R") + + +def create_call_wrapper(func: Callable[..., R], request_type: type[T]) -> Callable[[T], R]: + """ + Create a wrapper function that knows how to call func with the request object. + + Returns a wrapper function that takes the request and calls func appropriately. + + The wrapper handles three calling patterns: + 1. Positional-only parameter typed as request_type (no default): func(req) + 2. Positional/keyword parameter typed as request_type (no default): func(**{param_name: req}) + 3. No request parameter or parameter with default: func() + """ + try: + sig = inspect.signature(func) + type_hints = get_type_hints(func) + except (ValueError, TypeError, NameError): # pragma: no cover + return lambda _: func() + + # Check for positional-only parameter typed as request_type + for param_name, param in sig.parameters.items(): + if param.kind == inspect.Parameter.POSITIONAL_ONLY: + param_type = type_hints.get(param_name) + if param_type == request_type: # pragma: no branch + # Check if it has a default - if so, treat as old style + if param.default is not inspect.Parameter.empty: # pragma: no cover + return lambda _: func() + # Found positional-only parameter with correct type and no default + return lambda req: func(req) + + # Check for any positional/keyword parameter typed as request_type + for param_name, param in sig.parameters.items(): + if param.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY): # pragma: no branch + param_type = type_hints.get(param_name) + if param_type == request_type: + # Check if it has a default - if so, treat as old style + if param.default is not inspect.Parameter.empty: # pragma: no cover + return lambda _: func() + + # Found keyword parameter with correct type and no default + # Need to capture param_name in closure properly + def make_keyword_wrapper(name: str) -> Callable[[Any], Any]: + return lambda req: func(**{name: req}) + + return make_keyword_wrapper(param_name) + + # No request parameter found - use old style + return lambda _: func() diff --git a/source/target-vendors/cp312/mcp/server/lowlevel/helper_types.py b/source/target-vendors/cp312/mcp/server/lowlevel/helper_types.py new file mode 100644 index 0000000..fecc716 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/lowlevel/helper_types.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass +from typing import Any + + +@dataclass +class ReadResourceContents: + """Contents returned from a read_resource call.""" + + content: str | bytes + mime_type: str | None = None + meta: dict[str, Any] | None = None diff --git a/source/target-vendors/cp312/mcp/server/lowlevel/server.py b/source/target-vendors/cp312/mcp/server/lowlevel/server.py new file mode 100644 index 0000000..6745362 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/lowlevel/server.py @@ -0,0 +1,803 @@ +""" +MCP Server Module + +This module provides a framework for creating an MCP (Model Context Protocol) server. +It allows you to easily define and handle various types of requests and notifications +in an asynchronous manner. + +Usage: +1. Create a Server instance: + server = Server("your_server_name") + +2. Define request handlers using decorators: + @server.list_prompts() + async def handle_list_prompts(request: types.ListPromptsRequest) -> types.ListPromptsResult: + # Implementation + + @server.get_prompt() + async def handle_get_prompt( + name: str, arguments: dict[str, str] | None + ) -> types.GetPromptResult: + # Implementation + + @server.list_tools() + async def handle_list_tools(request: types.ListToolsRequest) -> types.ListToolsResult: + # Implementation + + @server.call_tool() + async def handle_call_tool( + name: str, arguments: dict | None + ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: + # Implementation + + @server.list_resource_templates() + async def handle_list_resource_templates() -> list[types.ResourceTemplate]: + # Implementation + +3. Define notification handlers if needed: + @server.progress_notification() + async def handle_progress( + progress_token: str | int, progress: float, total: float | None, + message: str | None + ) -> None: + # Implementation + +4. Run the server: + async def main(): + async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + InitializationOptions( + server_name="your_server_name", + server_version="your_version", + capabilities=server.get_capabilities( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), + ), + ) + + asyncio.run(main()) + +The Server class provides methods to register handlers for various MCP requests and +notifications. It automatically manages the request context and handles incoming +messages from the client. +""" + +from __future__ import annotations as _annotations + +import base64 +import contextvars +import json +import logging +import warnings +from collections.abc import AsyncIterator, Awaitable, Callable, Iterable +from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager +from typing import Any, Generic, TypeAlias, cast + +import anyio +import jsonschema +from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream +from pydantic import AnyUrl +from typing_extensions import TypeVar + +import mcp.types as types +from mcp.server.experimental.request_context import Experimental +from mcp.server.lowlevel.experimental import ExperimentalHandlers +from mcp.server.lowlevel.func_inspection import create_call_wrapper +from mcp.server.lowlevel.helper_types import ReadResourceContents +from mcp.server.models import InitializationOptions +from mcp.server.session import ServerSession +from mcp.shared.context import RequestContext +from mcp.shared.exceptions import McpError, UrlElicitationRequiredError +from mcp.shared.message import ServerMessageMetadata, SessionMessage +from mcp.shared.session import RequestResponder +from mcp.shared.tool_name_validation import validate_and_warn_tool_name + +logger = logging.getLogger(__name__) + +LifespanResultT = TypeVar("LifespanResultT", default=Any) +RequestT = TypeVar("RequestT", default=Any) + +# type aliases for tool call results +StructuredContent: TypeAlias = dict[str, Any] +UnstructuredContent: TypeAlias = Iterable[types.ContentBlock] +CombinationContent: TypeAlias = tuple[UnstructuredContent, StructuredContent] + +# This will be properly typed in each Server instance's context +request_ctx: contextvars.ContextVar[RequestContext[ServerSession, Any, Any]] = contextvars.ContextVar("request_ctx") + + +class NotificationOptions: + def __init__( + self, + prompts_changed: bool = False, + resources_changed: bool = False, + tools_changed: bool = False, + ): + self.prompts_changed = prompts_changed + self.resources_changed = resources_changed + self.tools_changed = tools_changed + + +@asynccontextmanager +async def lifespan(_: Server[LifespanResultT, RequestT]) -> AsyncIterator[dict[str, Any]]: + """Default lifespan context manager that does nothing. + + Args: + server: The server instance this lifespan is managing + + Returns: + An empty context object + """ + yield {} + + +class Server(Generic[LifespanResultT, RequestT]): + def __init__( + self, + name: str, + version: str | None = None, + instructions: str | None = None, + website_url: str | None = None, + icons: list[types.Icon] | None = None, + lifespan: Callable[ + [Server[LifespanResultT, RequestT]], + AbstractAsyncContextManager[LifespanResultT], + ] = lifespan, + ): + self.name = name + self.version = version + self.instructions = instructions + self.website_url = website_url + self.icons = icons + self.lifespan = lifespan + self.request_handlers: dict[type, Callable[..., Awaitable[types.ServerResult]]] = { + types.PingRequest: _ping_handler, + } + self.notification_handlers: dict[type, Callable[..., Awaitable[None]]] = {} + self._tool_cache: dict[str, types.Tool] = {} + self._experimental_handlers: ExperimentalHandlers | None = None + logger.debug("Initializing server %r", name) + + def create_initialization_options( + self, + notification_options: NotificationOptions | None = None, + experimental_capabilities: dict[str, dict[str, Any]] | None = None, + ) -> InitializationOptions: + """Create initialization options from this server instance.""" + + def pkg_version(package: str) -> str: + try: + from importlib.metadata import version + + return version(package) + except Exception: # pragma: no cover + pass + + return "unknown" # pragma: no cover + + return InitializationOptions( + server_name=self.name, + server_version=self.version if self.version else pkg_version("mcp"), + capabilities=self.get_capabilities( + notification_options or NotificationOptions(), + experimental_capabilities or {}, + ), + instructions=self.instructions, + website_url=self.website_url, + icons=self.icons, + ) + + def get_capabilities( + self, + notification_options: NotificationOptions, + experimental_capabilities: dict[str, dict[str, Any]], + ) -> types.ServerCapabilities: + """Convert existing handlers to a ServerCapabilities object.""" + prompts_capability = None + resources_capability = None + tools_capability = None + logging_capability = None + completions_capability = None + + # Set prompt capabilities if handler exists + if types.ListPromptsRequest in self.request_handlers: + prompts_capability = types.PromptsCapability(listChanged=notification_options.prompts_changed) + + # Set resource capabilities if handler exists + if types.ListResourcesRequest in self.request_handlers: + resources_capability = types.ResourcesCapability( + subscribe=False, listChanged=notification_options.resources_changed + ) + + # Set tool capabilities if handler exists + if types.ListToolsRequest in self.request_handlers: + tools_capability = types.ToolsCapability(listChanged=notification_options.tools_changed) + + # Set logging capabilities if handler exists + if types.SetLevelRequest in self.request_handlers: # pragma: no cover + logging_capability = types.LoggingCapability() + + # Set completions capabilities if handler exists + if types.CompleteRequest in self.request_handlers: + completions_capability = types.CompletionsCapability() + + capabilities = types.ServerCapabilities( + prompts=prompts_capability, + resources=resources_capability, + tools=tools_capability, + logging=logging_capability, + experimental=experimental_capabilities, + completions=completions_capability, + ) + if self._experimental_handlers: + self._experimental_handlers.update_capabilities(capabilities) + return capabilities + + @property + def request_context( + self, + ) -> RequestContext[ServerSession, LifespanResultT, RequestT]: + """If called outside of a request context, this will raise a LookupError.""" + return request_ctx.get() + + @property + def experimental(self) -> ExperimentalHandlers: + """Experimental APIs for tasks and other features. + + WARNING: These APIs are experimental and may change without notice. + """ + + # We create this inline so we only add these capabilities _if_ they're actually used + if self._experimental_handlers is None: + self._experimental_handlers = ExperimentalHandlers(self, self.request_handlers, self.notification_handlers) + return self._experimental_handlers + + def list_prompts(self): + def decorator( + func: Callable[[], Awaitable[list[types.Prompt]]] + | Callable[[types.ListPromptsRequest], Awaitable[types.ListPromptsResult]], + ): + logger.debug("Registering handler for PromptListRequest") + + wrapper = create_call_wrapper(func, types.ListPromptsRequest) + + async def handler(req: types.ListPromptsRequest): + result = await wrapper(req) + # Handle both old style (list[Prompt]) and new style (ListPromptsResult) + if isinstance(result, types.ListPromptsResult): + return types.ServerResult(result) + else: + # Old style returns list[Prompt] + return types.ServerResult(types.ListPromptsResult(prompts=result)) + + self.request_handlers[types.ListPromptsRequest] = handler + return func + + return decorator + + def get_prompt(self): + def decorator( + func: Callable[[str, dict[str, str] | None], Awaitable[types.GetPromptResult]], + ): + logger.debug("Registering handler for GetPromptRequest") + + async def handler(req: types.GetPromptRequest): + prompt_get = await func(req.params.name, req.params.arguments) + return types.ServerResult(prompt_get) + + self.request_handlers[types.GetPromptRequest] = handler + return func + + return decorator + + def list_resources(self): + def decorator( + func: Callable[[], Awaitable[list[types.Resource]]] + | Callable[[types.ListResourcesRequest], Awaitable[types.ListResourcesResult]], + ): + logger.debug("Registering handler for ListResourcesRequest") + + wrapper = create_call_wrapper(func, types.ListResourcesRequest) + + async def handler(req: types.ListResourcesRequest): + result = await wrapper(req) + # Handle both old style (list[Resource]) and new style (ListResourcesResult) + if isinstance(result, types.ListResourcesResult): + return types.ServerResult(result) + else: + # Old style returns list[Resource] + return types.ServerResult(types.ListResourcesResult(resources=result)) + + self.request_handlers[types.ListResourcesRequest] = handler + return func + + return decorator + + def list_resource_templates(self): + def decorator(func: Callable[[], Awaitable[list[types.ResourceTemplate]]]): + logger.debug("Registering handler for ListResourceTemplatesRequest") + + async def handler(_: Any): + templates = await func() + return types.ServerResult(types.ListResourceTemplatesResult(resourceTemplates=templates)) + + self.request_handlers[types.ListResourceTemplatesRequest] = handler + return func + + return decorator + + def read_resource(self): + def decorator( + func: Callable[[AnyUrl], Awaitable[str | bytes | Iterable[ReadResourceContents]]], + ): + logger.debug("Registering handler for ReadResourceRequest") + + async def handler(req: types.ReadResourceRequest): + result = await func(req.params.uri) + + def create_content(data: str | bytes, mime_type: str | None, meta: dict[str, Any] | None = None): + # Note: ResourceContents uses Field(alias="_meta"), so we must use the alias key + meta_kwargs: dict[str, Any] = {"_meta": meta} if meta is not None else {} + match data: + case str() as data: + return types.TextResourceContents( + uri=req.params.uri, + text=data, + mimeType=mime_type or "text/plain", + **meta_kwargs, + ) + case bytes() as data: # pragma: no cover + return types.BlobResourceContents( + uri=req.params.uri, + blob=base64.b64encode(data).decode(), + mimeType=mime_type or "application/octet-stream", + **meta_kwargs, + ) + + match result: + case str() | bytes() as data: # pragma: no cover + warnings.warn( + "Returning str or bytes from read_resource is deprecated. " + "Use Iterable[ReadResourceContents] instead.", + DeprecationWarning, + stacklevel=2, + ) + content = create_content(data, None) + case Iterable() as contents: + contents_list = [ + create_content( + content_item.content, content_item.mime_type, getattr(content_item, "meta", None) + ) + for content_item in contents + ] + return types.ServerResult( + types.ReadResourceResult( + contents=contents_list, + ) + ) + case _: # pragma: no cover + raise ValueError(f"Unexpected return type from read_resource: {type(result)}") + + return types.ServerResult( # pragma: no cover + types.ReadResourceResult( + contents=[content], + ) + ) + + self.request_handlers[types.ReadResourceRequest] = handler + return func + + return decorator + + def set_logging_level(self): # pragma: no cover + def decorator(func: Callable[[types.LoggingLevel], Awaitable[None]]): + logger.debug("Registering handler for SetLevelRequest") + + async def handler(req: types.SetLevelRequest): + await func(req.params.level) + return types.ServerResult(types.EmptyResult()) + + self.request_handlers[types.SetLevelRequest] = handler + return func + + return decorator + + def subscribe_resource(self): # pragma: no cover + def decorator(func: Callable[[AnyUrl], Awaitable[None]]): + logger.debug("Registering handler for SubscribeRequest") + + async def handler(req: types.SubscribeRequest): + await func(req.params.uri) + return types.ServerResult(types.EmptyResult()) + + self.request_handlers[types.SubscribeRequest] = handler + return func + + return decorator + + def unsubscribe_resource(self): # pragma: no cover + def decorator(func: Callable[[AnyUrl], Awaitable[None]]): + logger.debug("Registering handler for UnsubscribeRequest") + + async def handler(req: types.UnsubscribeRequest): + await func(req.params.uri) + return types.ServerResult(types.EmptyResult()) + + self.request_handlers[types.UnsubscribeRequest] = handler + return func + + return decorator + + def list_tools(self): + def decorator( + func: Callable[[], Awaitable[list[types.Tool]]] + | Callable[[types.ListToolsRequest], Awaitable[types.ListToolsResult]], + ): + logger.debug("Registering handler for ListToolsRequest") + + wrapper = create_call_wrapper(func, types.ListToolsRequest) + + async def handler(req: types.ListToolsRequest): + result = await wrapper(req) + + # Handle both old style (list[Tool]) and new style (ListToolsResult) + if isinstance(result, types.ListToolsResult): # pragma: no cover + # Refresh the tool cache with returned tools + for tool in result.tools: + validate_and_warn_tool_name(tool.name) + self._tool_cache[tool.name] = tool + return types.ServerResult(result) + else: + # Old style returns list[Tool] + # Clear and refresh the entire tool cache + self._tool_cache.clear() + for tool in result: + validate_and_warn_tool_name(tool.name) + self._tool_cache[tool.name] = tool + return types.ServerResult(types.ListToolsResult(tools=result)) + + self.request_handlers[types.ListToolsRequest] = handler + return func + + return decorator + + def _make_error_result(self, error_message: str) -> types.ServerResult: + """Create a ServerResult with an error CallToolResult.""" + return types.ServerResult( + types.CallToolResult( + content=[types.TextContent(type="text", text=error_message)], + isError=True, + ) + ) + + async def _get_cached_tool_definition(self, tool_name: str) -> types.Tool | None: + """Get tool definition from cache, refreshing if necessary. + + Returns the Tool object if found, None otherwise. + """ + if tool_name not in self._tool_cache: + if types.ListToolsRequest in self.request_handlers: + logger.debug("Tool cache miss for %s, refreshing cache", tool_name) + await self.request_handlers[types.ListToolsRequest](None) + + tool = self._tool_cache.get(tool_name) + if tool is None: + logger.warning("Tool '%s' not listed, no validation will be performed", tool_name) + + return tool + + def call_tool(self, *, validate_input: bool = True): + """Register a tool call handler. + + Args: + validate_input: If True, validates input against inputSchema. Default is True. + + The handler validates input against inputSchema (if validate_input=True), calls the tool function, + and builds a CallToolResult with the results: + - Unstructured content (iterable of ContentBlock): returned in content + - Structured content (dict): returned in structuredContent, serialized JSON text returned in content + - Both: returned in content and structuredContent + + If outputSchema is defined, validates structuredContent or errors if missing. + """ + + def decorator( + func: Callable[ + ..., + Awaitable[ + UnstructuredContent + | StructuredContent + | CombinationContent + | types.CallToolResult + | types.CreateTaskResult + ], + ], + ): + logger.debug("Registering handler for CallToolRequest") + + async def handler(req: types.CallToolRequest): + try: + tool_name = req.params.name + arguments = req.params.arguments or {} + tool = await self._get_cached_tool_definition(tool_name) + + # input validation + if validate_input and tool: + try: + jsonschema.validate(instance=arguments, schema=tool.inputSchema) + except jsonschema.ValidationError as e: + return self._make_error_result(f"Input validation error: {e.message}") + + # tool call + results = await func(tool_name, arguments) + + # output normalization + unstructured_content: UnstructuredContent + maybe_structured_content: StructuredContent | None + if isinstance(results, types.CallToolResult): + return types.ServerResult(results) + elif isinstance(results, types.CreateTaskResult): + # Task-augmented execution returns task info instead of result + return types.ServerResult(results) + elif isinstance(results, tuple) and len(results) == 2: + # tool returned both structured and unstructured content + unstructured_content, maybe_structured_content = cast(CombinationContent, results) + elif isinstance(results, dict): + # tool returned structured content only + maybe_structured_content = cast(StructuredContent, results) + unstructured_content = [types.TextContent(type="text", text=json.dumps(results, indent=2))] + elif hasattr(results, "__iter__"): # pragma: no cover + # tool returned unstructured content only + unstructured_content = cast(UnstructuredContent, results) + maybe_structured_content = None + else: # pragma: no cover + return self._make_error_result(f"Unexpected return type from tool: {type(results).__name__}") + + # output validation + if tool and tool.outputSchema is not None: + if maybe_structured_content is None: + return self._make_error_result( + "Output validation error: outputSchema defined but no structured output returned" + ) + else: + try: + jsonschema.validate(instance=maybe_structured_content, schema=tool.outputSchema) + except jsonschema.ValidationError as e: + return self._make_error_result(f"Output validation error: {e.message}") + + # result + return types.ServerResult( + types.CallToolResult( + content=list(unstructured_content), + structuredContent=maybe_structured_content, + isError=False, + ) + ) + except UrlElicitationRequiredError: + # Re-raise UrlElicitationRequiredError so it can be properly handled + # by _handle_request, which converts it to an error response with code -32042 + raise + except Exception as e: + return self._make_error_result(str(e)) + + self.request_handlers[types.CallToolRequest] = handler + return func + + return decorator + + def progress_notification(self): + def decorator( + func: Callable[[str | int, float, float | None, str | None], Awaitable[None]], + ): + logger.debug("Registering handler for ProgressNotification") + + async def handler(req: types.ProgressNotification): + await func( + req.params.progressToken, + req.params.progress, + req.params.total, + req.params.message, + ) + + self.notification_handlers[types.ProgressNotification] = handler + return func + + return decorator + + def completion(self): + """Provides completions for prompts and resource templates""" + + def decorator( + func: Callable[ + [ + types.PromptReference | types.ResourceTemplateReference, + types.CompletionArgument, + types.CompletionContext | None, + ], + Awaitable[types.Completion | None], + ], + ): + logger.debug("Registering handler for CompleteRequest") + + async def handler(req: types.CompleteRequest): + completion = await func(req.params.ref, req.params.argument, req.params.context) + return types.ServerResult( + types.CompleteResult( + completion=completion + if completion is not None + else types.Completion(values=[], total=None, hasMore=None), + ) + ) + + self.request_handlers[types.CompleteRequest] = handler + return func + + return decorator + + async def run( + self, + read_stream: MemoryObjectReceiveStream[SessionMessage | Exception], + write_stream: MemoryObjectSendStream[SessionMessage], + initialization_options: InitializationOptions, + # When False, exceptions are returned as messages to the client. + # When True, exceptions are raised, which will cause the server to shut down + # but also make tracing exceptions much easier during testing and when using + # in-process servers. + raise_exceptions: bool = False, + # When True, the server is stateless and + # clients can perform initialization with any node. The client must still follow + # the initialization lifecycle, but can do so with any available node + # rather than requiring initialization for each connection. + stateless: bool = False, + ): + async with AsyncExitStack() as stack: + lifespan_context = await stack.enter_async_context(self.lifespan(self)) + session = await stack.enter_async_context( + ServerSession( + read_stream, + write_stream, + initialization_options, + stateless=stateless, + ) + ) + + # Configure task support for this session if enabled + task_support = self._experimental_handlers.task_support if self._experimental_handlers else None + if task_support is not None: + task_support.configure_session(session) + await stack.enter_async_context(task_support.run()) + + async with anyio.create_task_group() as tg: + async for message in session.incoming_messages: + logger.debug("Received message: %s", message) + + tg.start_soon( + self._handle_message, + message, + session, + lifespan_context, + raise_exceptions, + ) + + async def _handle_message( + self, + message: RequestResponder[types.ClientRequest, types.ServerResult] | types.ClientNotification | Exception, + session: ServerSession, + lifespan_context: LifespanResultT, + raise_exceptions: bool = False, + ): + with warnings.catch_warnings(record=True) as w: + match message: + case RequestResponder(request=types.ClientRequest(root=req)) as responder: + with responder: + await self._handle_request(message, req, session, lifespan_context, raise_exceptions) + case types.ClientNotification(root=notify): + await self._handle_notification(notify) + case Exception(): # pragma: no cover + logger.error(f"Received exception from stream: {message}") + await session.send_log_message( + level="error", + data="Internal Server Error", + logger="mcp.server.exception_handler", + ) + if raise_exceptions: + raise message + + for warning in w: # pragma: no cover + logger.info("Warning: %s: %s", warning.category.__name__, warning.message) + + async def _handle_request( + self, + message: RequestResponder[types.ClientRequest, types.ServerResult], + req: types.ClientRequestType, + session: ServerSession, + lifespan_context: LifespanResultT, + raise_exceptions: bool, + ): + logger.info("Processing request of type %s", type(req).__name__) + + if handler := self.request_handlers.get(type(req)): + logger.debug("Dispatching request of type %s", type(req).__name__) + + token = None + try: + # Extract request context and close_sse_stream from message metadata + request_data = None + close_sse_stream_cb = None + close_standalone_sse_stream_cb = None + if message.message_metadata is not None and isinstance( + message.message_metadata, ServerMessageMetadata + ): # pragma: no cover + request_data = message.message_metadata.request_context + close_sse_stream_cb = message.message_metadata.close_sse_stream + close_standalone_sse_stream_cb = message.message_metadata.close_standalone_sse_stream + + # Set our global state that can be retrieved via + # app.get_request_context() + client_capabilities = session.client_params.capabilities if session.client_params else None + task_support = self._experimental_handlers.task_support if self._experimental_handlers else None + # Get task metadata from request params if present + task_metadata = None + if hasattr(req, "params") and req.params is not None: + task_metadata = getattr(req.params, "task", None) + token = request_ctx.set( + RequestContext( + message.request_id, + message.request_meta, + session, + lifespan_context, + Experimental( + task_metadata=task_metadata, + _client_capabilities=client_capabilities, + _session=session, + _task_support=task_support, + ), + request=request_data, + close_sse_stream=close_sse_stream_cb, + close_standalone_sse_stream=close_standalone_sse_stream_cb, + ) + ) + response = await handler(req) + except McpError as err: # pragma: no cover + response = err.error + except anyio.get_cancelled_exc_class(): # pragma: no cover + logger.info( + "Request %s cancelled - duplicate response suppressed", + message.request_id, + ) + return + except Exception as err: # pragma: no cover + if raise_exceptions: + raise err + response = types.ErrorData(code=0, message=str(err), data=None) + finally: + # Reset the global state after we are done + if token is not None: # pragma: no branch + request_ctx.reset(token) + + await message.respond(response) + else: # pragma: no cover + await message.respond( + types.ErrorData( + code=types.METHOD_NOT_FOUND, + message="Method not found", + ) + ) + + logger.debug("Response sent") + + async def _handle_notification(self, notify: Any): + if handler := self.notification_handlers.get(type(notify)): # type: ignore + logger.debug("Dispatching notification of type %s", type(notify).__name__) + + try: + await handler(notify) + except Exception: # pragma: no cover + logger.exception("Uncaught exception in notification handler") + + +async def _ping_handler(request: types.PingRequest) -> types.ServerResult: + return types.ServerResult(types.EmptyResult()) diff --git a/source/target-vendors/cp312/mcp/server/models.py b/source/target-vendors/cp312/mcp/server/models.py new file mode 100644 index 0000000..ddf716c --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/models.py @@ -0,0 +1,20 @@ +""" +This module provides simpler types to use with the server for managing prompts +and tools. +""" + +from pydantic import BaseModel + +from mcp.types import ( + Icon, + ServerCapabilities, +) + + +class InitializationOptions(BaseModel): + server_name: str + server_version: str + capabilities: ServerCapabilities + instructions: str | None = None + website_url: str | None = None + icons: list[Icon] | None = None diff --git a/source/target-vendors/cp312/mcp/server/session.py b/source/target-vendors/cp312/mcp/server/session.py new file mode 100644 index 0000000..8f0baa3 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/session.py @@ -0,0 +1,691 @@ +""" +ServerSession Module + +This module provides the ServerSession class, which manages communication between the +server and client in the MCP (Model Context Protocol) framework. It is most commonly +used in MCP servers to interact with the client. + +Common usage pattern: +``` + server = Server(name) + + @server.call_tool() + async def handle_tool_call(ctx: RequestContext, arguments: dict[str, Any]) -> Any: + # Check client capabilities before proceeding + if ctx.session.check_client_capability( + types.ClientCapabilities(experimental={"advanced_tools": dict()}) + ): + # Perform advanced tool operations + result = await perform_advanced_tool_operation(arguments) + else: + # Fall back to basic tool operations + result = await perform_basic_tool_operation(arguments) + + return result + + @server.list_prompts() + async def handle_list_prompts(ctx: RequestContext) -> list[types.Prompt]: + # Access session for any necessary checks or operations + if ctx.session.client_params: + # Customize prompts based on client initialization parameters + return generate_custom_prompts(ctx.session.client_params) + else: + return default_prompts +``` + +The ServerSession class is typically used internally by the Server class and should not +be instantiated directly by users of the MCP framework. +""" + +from enum import Enum +from typing import Any, TypeVar, overload + +import anyio +import anyio.lowlevel +from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream +from pydantic import AnyUrl + +import mcp.types as types +from mcp.server.experimental.session_features import ExperimentalServerSessionFeatures +from mcp.server.models import InitializationOptions +from mcp.server.validation import validate_sampling_tools, validate_tool_use_result_messages +from mcp.shared.experimental.tasks.capabilities import check_tasks_capability +from mcp.shared.experimental.tasks.helpers import RELATED_TASK_METADATA_KEY +from mcp.shared.message import ServerMessageMetadata, SessionMessage +from mcp.shared.session import ( + BaseSession, + RequestResponder, +) +from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS + + +class InitializationState(Enum): + NotInitialized = 1 + Initializing = 2 + Initialized = 3 + + +ServerSessionT = TypeVar("ServerSessionT", bound="ServerSession") + +ServerRequestResponder = ( + RequestResponder[types.ClientRequest, types.ServerResult] | types.ClientNotification | Exception +) + + +class ServerSession( + BaseSession[ + types.ServerRequest, + types.ServerNotification, + types.ServerResult, + types.ClientRequest, + types.ClientNotification, + ] +): + _initialized: InitializationState = InitializationState.NotInitialized + _client_params: types.InitializeRequestParams | None = None + _experimental_features: ExperimentalServerSessionFeatures | None = None + + def __init__( + self, + read_stream: MemoryObjectReceiveStream[SessionMessage | Exception], + write_stream: MemoryObjectSendStream[SessionMessage], + init_options: InitializationOptions, + stateless: bool = False, + ) -> None: + super().__init__(read_stream, write_stream, types.ClientRequest, types.ClientNotification) + self._initialization_state = ( + InitializationState.Initialized if stateless else InitializationState.NotInitialized + ) + + self._init_options = init_options + self._incoming_message_stream_writer, self._incoming_message_stream_reader = anyio.create_memory_object_stream[ + ServerRequestResponder + ](0) + self._exit_stack.push_async_callback(lambda: self._incoming_message_stream_reader.aclose()) + + @property + def client_params(self) -> types.InitializeRequestParams | None: + return self._client_params # pragma: no cover + + @property + def experimental(self) -> ExperimentalServerSessionFeatures: + """Experimental APIs for server→client task operations. + + WARNING: These APIs are experimental and may change without notice. + """ + if self._experimental_features is None: + self._experimental_features = ExperimentalServerSessionFeatures(self) + return self._experimental_features + + def check_client_capability(self, capability: types.ClientCapabilities) -> bool: # pragma: no cover + """Check if the client supports a specific capability.""" + if self._client_params is None: + return False + + client_caps = self._client_params.capabilities + + if capability.roots is not None: + if client_caps.roots is None: + return False + if capability.roots.listChanged and not client_caps.roots.listChanged: + return False + + if capability.sampling is not None: + if client_caps.sampling is None: + return False + if capability.sampling.context is not None and client_caps.sampling.context is None: + return False + if capability.sampling.tools is not None and client_caps.sampling.tools is None: + return False + + if capability.elicitation is not None and client_caps.elicitation is None: + return False + + if capability.experimental is not None: + if client_caps.experimental is None: + return False + for exp_key, exp_value in capability.experimental.items(): + if exp_key not in client_caps.experimental or client_caps.experimental[exp_key] != exp_value: + return False + + if capability.tasks is not None: + if client_caps.tasks is None: + return False + if not check_tasks_capability(capability.tasks, client_caps.tasks): + return False + + return True + + async def _receive_loop(self) -> None: + async with self._incoming_message_stream_writer: + await super()._receive_loop() + + async def _received_request(self, responder: RequestResponder[types.ClientRequest, types.ServerResult]): + match responder.request.root: + case types.InitializeRequest(params=params): + requested_version = params.protocolVersion + self._initialization_state = InitializationState.Initializing + self._client_params = params + with responder: + await responder.respond( + types.ServerResult( + types.InitializeResult( + protocolVersion=requested_version + if requested_version in SUPPORTED_PROTOCOL_VERSIONS + else types.LATEST_PROTOCOL_VERSION, + capabilities=self._init_options.capabilities, + serverInfo=types.Implementation( + name=self._init_options.server_name, + version=self._init_options.server_version, + websiteUrl=self._init_options.website_url, + icons=self._init_options.icons, + ), + instructions=self._init_options.instructions, + ) + ) + ) + self._initialization_state = InitializationState.Initialized + case types.PingRequest(): + # Ping requests are allowed at any time + pass + case _: + if self._initialization_state != InitializationState.Initialized: + raise RuntimeError("Received request before initialization was complete") + + async def _received_notification(self, notification: types.ClientNotification) -> None: + # Need this to avoid ASYNC910 + await anyio.lowlevel.checkpoint() + match notification.root: + case types.InitializedNotification(): + self._initialization_state = InitializationState.Initialized + case _: + if self._initialization_state != InitializationState.Initialized: # pragma: no cover + raise RuntimeError("Received notification before initialization was complete") + + async def send_log_message( + self, + level: types.LoggingLevel, + data: Any, + logger: str | None = None, + related_request_id: types.RequestId | None = None, + ) -> None: + """Send a log message notification.""" + await self.send_notification( + types.ServerNotification( + types.LoggingMessageNotification( + params=types.LoggingMessageNotificationParams( + level=level, + data=data, + logger=logger, + ), + ) + ), + related_request_id, + ) + + async def send_resource_updated(self, uri: AnyUrl) -> None: # pragma: no cover + """Send a resource updated notification.""" + await self.send_notification( + types.ServerNotification( + types.ResourceUpdatedNotification( + params=types.ResourceUpdatedNotificationParams(uri=uri), + ) + ) + ) + + @overload + async def create_message( + self, + messages: list[types.SamplingMessage], + *, + max_tokens: int, + system_prompt: str | None = None, + include_context: types.IncludeContext | None = None, + temperature: float | None = None, + stop_sequences: list[str] | None = None, + metadata: dict[str, Any] | None = None, + model_preferences: types.ModelPreferences | None = None, + tools: None = None, + tool_choice: types.ToolChoice | None = None, + related_request_id: types.RequestId | None = None, + ) -> types.CreateMessageResult: + """Overload: Without tools, returns single content.""" + ... + + @overload + async def create_message( + self, + messages: list[types.SamplingMessage], + *, + max_tokens: int, + system_prompt: str | None = None, + include_context: types.IncludeContext | None = None, + temperature: float | None = None, + stop_sequences: list[str] | None = None, + metadata: dict[str, Any] | None = None, + model_preferences: types.ModelPreferences | None = None, + tools: list[types.Tool], + tool_choice: types.ToolChoice | None = None, + related_request_id: types.RequestId | None = None, + ) -> types.CreateMessageResultWithTools: + """Overload: With tools, returns array-capable content.""" + ... + + async def create_message( + self, + messages: list[types.SamplingMessage], + *, + max_tokens: int, + system_prompt: str | None = None, + include_context: types.IncludeContext | None = None, + temperature: float | None = None, + stop_sequences: list[str] | None = None, + metadata: dict[str, Any] | None = None, + model_preferences: types.ModelPreferences | None = None, + tools: list[types.Tool] | None = None, + tool_choice: types.ToolChoice | None = None, + related_request_id: types.RequestId | None = None, + ) -> types.CreateMessageResult | types.CreateMessageResultWithTools: + """Send a sampling/create_message request. + + Args: + messages: The conversation messages to send. + max_tokens: Maximum number of tokens to generate. + system_prompt: Optional system prompt. + include_context: Optional context inclusion setting. + Should only be set to "thisServer" or "allServers" + if the client has sampling.context capability. + temperature: Optional sampling temperature. + stop_sequences: Optional stop sequences. + metadata: Optional metadata to pass through to the LLM provider. + model_preferences: Optional model selection preferences. + tools: Optional list of tools the LLM can use during sampling. + Requires client to have sampling.tools capability. + tool_choice: Optional control over tool usage behavior. + Requires client to have sampling.tools capability. + related_request_id: Optional ID of a related request. + + Returns: + The sampling result from the client. + + Raises: + McpError: If tools are provided but client doesn't support them. + ValueError: If tool_use or tool_result message structure is invalid. + """ + client_caps = self._client_params.capabilities if self._client_params else None + validate_sampling_tools(client_caps, tools, tool_choice) + validate_tool_use_result_messages(messages) + + request = types.ServerRequest( + types.CreateMessageRequest( + params=types.CreateMessageRequestParams( + messages=messages, + systemPrompt=system_prompt, + includeContext=include_context, + temperature=temperature, + maxTokens=max_tokens, + stopSequences=stop_sequences, + metadata=metadata, + modelPreferences=model_preferences, + tools=tools, + toolChoice=tool_choice, + ), + ) + ) + metadata_obj = ServerMessageMetadata(related_request_id=related_request_id) + + # Use different result types based on whether tools are provided + if tools is not None: + return await self.send_request( + request=request, + result_type=types.CreateMessageResultWithTools, + metadata=metadata_obj, + ) + return await self.send_request( + request=request, + result_type=types.CreateMessageResult, + metadata=metadata_obj, + ) + + async def list_roots(self) -> types.ListRootsResult: + """Send a roots/list request.""" + return await self.send_request( + types.ServerRequest(types.ListRootsRequest()), + types.ListRootsResult, + ) + + async def elicit( + self, + message: str, + requestedSchema: types.ElicitRequestedSchema, + related_request_id: types.RequestId | None = None, + ) -> types.ElicitResult: + """Send a form mode elicitation/create request. + + Args: + message: The message to present to the user + requestedSchema: Schema defining the expected response structure + related_request_id: Optional ID of the request that triggered this elicitation + + Returns: + The client's response + + Note: + This method is deprecated in favor of elicit_form(). It remains for + backward compatibility but new code should use elicit_form(). + """ + return await self.elicit_form(message, requestedSchema, related_request_id) + + async def elicit_form( + self, + message: str, + requestedSchema: types.ElicitRequestedSchema, + related_request_id: types.RequestId | None = None, + ) -> types.ElicitResult: + """Send a form mode elicitation/create request. + + Args: + message: The message to present to the user + requestedSchema: Schema defining the expected response structure + related_request_id: Optional ID of the request that triggered this elicitation + + Returns: + The client's response with form data + """ + return await self.send_request( + types.ServerRequest( + types.ElicitRequest( + params=types.ElicitRequestFormParams( + message=message, + requestedSchema=requestedSchema, + ), + ) + ), + types.ElicitResult, + metadata=ServerMessageMetadata(related_request_id=related_request_id), + ) + + async def elicit_url( + self, + message: str, + url: str, + elicitation_id: str, + related_request_id: types.RequestId | None = None, + ) -> types.ElicitResult: + """Send a URL mode elicitation/create request. + + This directs the user to an external URL for out-of-band interactions + like OAuth flows, credential collection, or payment processing. + + Args: + message: Human-readable explanation of why the interaction is needed + url: The URL the user should navigate to + elicitation_id: Unique identifier for tracking this elicitation + related_request_id: Optional ID of the request that triggered this elicitation + + Returns: + The client's response indicating acceptance, decline, or cancellation + """ + return await self.send_request( + types.ServerRequest( + types.ElicitRequest( + params=types.ElicitRequestURLParams( + message=message, + url=url, + elicitationId=elicitation_id, + ), + ) + ), + types.ElicitResult, + metadata=ServerMessageMetadata(related_request_id=related_request_id), + ) + + async def send_ping(self) -> types.EmptyResult: # pragma: no cover + """Send a ping request.""" + return await self.send_request( + types.ServerRequest(types.PingRequest()), + types.EmptyResult, + ) + + async def send_progress_notification( + self, + progress_token: str | int, + progress: float, + total: float | None = None, + message: str | None = None, + related_request_id: str | None = None, + ) -> None: + """Send a progress notification.""" + await self.send_notification( + types.ServerNotification( + types.ProgressNotification( + params=types.ProgressNotificationParams( + progressToken=progress_token, + progress=progress, + total=total, + message=message, + ), + ) + ), + related_request_id, + ) + + async def send_resource_list_changed(self) -> None: # pragma: no cover + """Send a resource list changed notification.""" + await self.send_notification(types.ServerNotification(types.ResourceListChangedNotification())) + + async def send_tool_list_changed(self) -> None: # pragma: no cover + """Send a tool list changed notification.""" + await self.send_notification(types.ServerNotification(types.ToolListChangedNotification())) + + async def send_prompt_list_changed(self) -> None: # pragma: no cover + """Send a prompt list changed notification.""" + await self.send_notification(types.ServerNotification(types.PromptListChangedNotification())) + + async def send_elicit_complete( + self, + elicitation_id: str, + related_request_id: types.RequestId | None = None, + ) -> None: + """Send an elicitation completion notification. + + This should be sent when a URL mode elicitation has been completed + out-of-band to inform the client that it may retry any requests + that were waiting for this elicitation. + + Args: + elicitation_id: The unique identifier of the completed elicitation + related_request_id: Optional ID of the request that triggered this + """ + await self.send_notification( + types.ServerNotification( + types.ElicitCompleteNotification( + params=types.ElicitCompleteNotificationParams(elicitationId=elicitation_id) + ) + ), + related_request_id, + ) + + def _build_elicit_form_request( + self, + message: str, + requestedSchema: types.ElicitRequestedSchema, + related_task_id: str | None = None, + task: types.TaskMetadata | None = None, + ) -> types.JSONRPCRequest: + """Build a form mode elicitation request without sending it. + + Args: + message: The message to present to the user + requestedSchema: Schema defining the expected response structure + related_task_id: If provided, adds io.modelcontextprotocol/related-task metadata + task: If provided, makes this a task-augmented request + + Returns: + A JSONRPCRequest ready to be sent or queued + """ + params = types.ElicitRequestFormParams( + message=message, + requestedSchema=requestedSchema, + task=task, + ) + params_data = params.model_dump(by_alias=True, mode="json", exclude_none=True) + + # Add related-task metadata if associated with a parent task + if related_task_id is not None: + # Defensive: model_dump() never includes _meta, but guard against future changes + if "_meta" not in params_data: # pragma: no cover + params_data["_meta"] = {} + params_data["_meta"][RELATED_TASK_METADATA_KEY] = types.RelatedTaskMetadata( + taskId=related_task_id + ).model_dump(by_alias=True) + + request_id = f"task-{related_task_id}-{id(params)}" if related_task_id else self._request_id + if related_task_id is None: + self._request_id += 1 + + return types.JSONRPCRequest( + jsonrpc="2.0", + id=request_id, + method="elicitation/create", + params=params_data, + ) + + def _build_elicit_url_request( + self, + message: str, + url: str, + elicitation_id: str, + related_task_id: str | None = None, + ) -> types.JSONRPCRequest: + """Build a URL mode elicitation request without sending it. + + Args: + message: Human-readable explanation of why the interaction is needed + url: The URL the user should navigate to + elicitation_id: Unique identifier for tracking this elicitation + related_task_id: If provided, adds io.modelcontextprotocol/related-task metadata + + Returns: + A JSONRPCRequest ready to be sent or queued + """ + params = types.ElicitRequestURLParams( + message=message, + url=url, + elicitationId=elicitation_id, + ) + params_data = params.model_dump(by_alias=True, mode="json", exclude_none=True) + + # Add related-task metadata if associated with a parent task + if related_task_id is not None: + # Defensive: model_dump() never includes _meta, but guard against future changes + if "_meta" not in params_data: # pragma: no cover + params_data["_meta"] = {} + params_data["_meta"][RELATED_TASK_METADATA_KEY] = types.RelatedTaskMetadata( + taskId=related_task_id + ).model_dump(by_alias=True) + + request_id = f"task-{related_task_id}-{id(params)}" if related_task_id else self._request_id + if related_task_id is None: + self._request_id += 1 + + return types.JSONRPCRequest( + jsonrpc="2.0", + id=request_id, + method="elicitation/create", + params=params_data, + ) + + def _build_create_message_request( + self, + messages: list[types.SamplingMessage], + *, + max_tokens: int, + system_prompt: str | None = None, + include_context: types.IncludeContext | None = None, + temperature: float | None = None, + stop_sequences: list[str] | None = None, + metadata: dict[str, Any] | None = None, + model_preferences: types.ModelPreferences | None = None, + tools: list[types.Tool] | None = None, + tool_choice: types.ToolChoice | None = None, + related_task_id: str | None = None, + task: types.TaskMetadata | None = None, + ) -> types.JSONRPCRequest: + """Build a sampling/createMessage request without sending it. + + Args: + messages: The conversation messages to send + max_tokens: Maximum number of tokens to generate + system_prompt: Optional system prompt + include_context: Optional context inclusion setting + temperature: Optional sampling temperature + stop_sequences: Optional stop sequences + metadata: Optional metadata to pass through to the LLM provider + model_preferences: Optional model selection preferences + tools: Optional list of tools the LLM can use during sampling + tool_choice: Optional control over tool usage behavior + related_task_id: If provided, adds io.modelcontextprotocol/related-task metadata + task: If provided, makes this a task-augmented request + + Returns: + A JSONRPCRequest ready to be sent or queued + """ + params = types.CreateMessageRequestParams( + messages=messages, + systemPrompt=system_prompt, + includeContext=include_context, + temperature=temperature, + maxTokens=max_tokens, + stopSequences=stop_sequences, + metadata=metadata, + modelPreferences=model_preferences, + tools=tools, + toolChoice=tool_choice, + task=task, + ) + params_data = params.model_dump(by_alias=True, mode="json", exclude_none=True) + + # Add related-task metadata if associated with a parent task + if related_task_id is not None: + # Defensive: model_dump() never includes _meta, but guard against future changes + if "_meta" not in params_data: # pragma: no cover + params_data["_meta"] = {} + params_data["_meta"][RELATED_TASK_METADATA_KEY] = types.RelatedTaskMetadata( + taskId=related_task_id + ).model_dump(by_alias=True) + + request_id = f"task-{related_task_id}-{id(params)}" if related_task_id else self._request_id + if related_task_id is None: + self._request_id += 1 + + return types.JSONRPCRequest( + jsonrpc="2.0", + id=request_id, + method="sampling/createMessage", + params=params_data, + ) + + async def send_message(self, message: SessionMessage) -> None: + """Send a raw session message. + + This is primarily used by TaskResultHandler to deliver queued messages + (elicitation/sampling requests) to the client during task execution. + + WARNING: This is a low-level experimental method that may change without + notice. Prefer using higher-level methods like send_notification() or + send_request() for normal operations. + + Args: + message: The session message to send + """ + await self._write_stream.send(message) + + async def _handle_incoming(self, req: ServerRequestResponder) -> None: + await self._incoming_message_stream_writer.send(req) + + @property + def incoming_messages( + self, + ) -> MemoryObjectReceiveStream[ServerRequestResponder]: + return self._incoming_message_stream_reader diff --git a/source/target-vendors/cp312/mcp/server/sse.py b/source/target-vendors/cp312/mcp/server/sse.py new file mode 100644 index 0000000..19af93f --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/sse.py @@ -0,0 +1,249 @@ +""" +SSE Server Transport Module + +This module implements a Server-Sent Events (SSE) transport layer for MCP servers. + +Example usage: +``` + # Create an SSE transport at an endpoint + sse = SseServerTransport("/messages/") + + # Create Starlette routes for SSE and message handling + routes = [ + Route("/sse", endpoint=handle_sse, methods=["GET"]), + Mount("/messages/", app=sse.handle_post_message), + ] + + # Define handler functions + async def handle_sse(request): + async with sse.connect_sse( + request.scope, request.receive, request._send + ) as streams: + await app.run( + streams[0], streams[1], app.create_initialization_options() + ) + # Return empty response to avoid NoneType error + return Response() + + # Create and run Starlette app + starlette_app = Starlette(routes=routes) + uvicorn.run(starlette_app, host="127.0.0.1", port=port) +``` + +Note: The handle_sse function must return a Response to avoid a "TypeError: 'NoneType' +object is not callable" error when client disconnects. The example above returns +an empty Response() after the SSE connection ends to fix this. + +See SseServerTransport class documentation for more details. +""" + +import logging +from contextlib import asynccontextmanager +from typing import Any +from urllib.parse import quote +from uuid import UUID, uuid4 + +import anyio +from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream +from pydantic import ValidationError +from sse_starlette import EventSourceResponse +from starlette.requests import Request +from starlette.responses import Response +from starlette.types import Receive, Scope, Send + +import mcp.types as types +from mcp.server.transport_security import ( + TransportSecurityMiddleware, + TransportSecuritySettings, +) +from mcp.shared.message import ServerMessageMetadata, SessionMessage + +logger = logging.getLogger(__name__) + + +class SseServerTransport: + """ + SSE server transport for MCP. This class provides _two_ ASGI applications, + suitable to be used with a framework like Starlette and a server like Hypercorn: + + 1. connect_sse() is an ASGI application which receives incoming GET requests, + and sets up a new SSE stream to send server messages to the client. + 2. handle_post_message() is an ASGI application which receives incoming POST + requests, which should contain client messages that link to a + previously-established SSE session. + """ + + _endpoint: str + _read_stream_writers: dict[UUID, MemoryObjectSendStream[SessionMessage | Exception]] + _security: TransportSecurityMiddleware + + def __init__(self, endpoint: str, security_settings: TransportSecuritySettings | None = None) -> None: + """ + Creates a new SSE server transport, which will direct the client to POST + messages to the relative path given. + + Args: + endpoint: A relative path where messages should be posted + (e.g., "/messages/"). + security_settings: Optional security settings for DNS rebinding protection. + + Note: + We use relative paths instead of full URLs for several reasons: + 1. Security: Prevents cross-origin requests by ensuring clients only connect + to the same origin they established the SSE connection with + 2. Flexibility: The server can be mounted at any path without needing to + know its full URL + 3. Portability: The same endpoint configuration works across different + environments (development, staging, production) + + Raises: + ValueError: If the endpoint is a full URL instead of a relative path + """ + + super().__init__() + + # Validate that endpoint is a relative path and not a full URL + if "://" in endpoint or endpoint.startswith("//") or "?" in endpoint or "#" in endpoint: + raise ValueError( + f"Given endpoint: {endpoint} is not a relative path (e.g., '/messages/'), " + "expecting a relative path (e.g., '/messages/')." + ) + + # Ensure endpoint starts with a forward slash + if not endpoint.startswith("/"): + endpoint = "/" + endpoint + + self._endpoint = endpoint + self._read_stream_writers = {} + self._security = TransportSecurityMiddleware(security_settings) + logger.debug(f"SseServerTransport initialized with endpoint: {endpoint}") + + @asynccontextmanager + async def connect_sse(self, scope: Scope, receive: Receive, send: Send): # pragma: no cover + if scope["type"] != "http": + logger.error("connect_sse received non-HTTP request") + raise ValueError("connect_sse can only handle HTTP requests") + + # Validate request headers for DNS rebinding protection + request = Request(scope, receive) + error_response = await self._security.validate_request(request, is_post=False) + if error_response: + await error_response(scope, receive, send) + raise ValueError("Request validation failed") + + logger.debug("Setting up SSE connection") + read_stream: MemoryObjectReceiveStream[SessionMessage | Exception] + read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception] + + write_stream: MemoryObjectSendStream[SessionMessage] + write_stream_reader: MemoryObjectReceiveStream[SessionMessage] + + read_stream_writer, read_stream = anyio.create_memory_object_stream(0) + write_stream, write_stream_reader = anyio.create_memory_object_stream(0) + + session_id = uuid4() + self._read_stream_writers[session_id] = read_stream_writer + logger.debug(f"Created new session with ID: {session_id}") + + # Determine the full path for the message endpoint to be sent to the client. + # scope['root_path'] is the prefix where the current Starlette app + # instance is mounted. + # e.g., "" if top-level, or "/api_prefix" if mounted under "/api_prefix". + root_path = scope.get("root_path", "") + + # self._endpoint is the path *within* this app, e.g., "/messages". + # Concatenating them gives the full absolute path from the server root. + # e.g., "" + "/messages" -> "/messages" + # e.g., "/api_prefix" + "/messages" -> "/api_prefix/messages" + full_message_path_for_client = root_path.rstrip("/") + self._endpoint + + # This is the URI (path + query) the client will use to POST messages. + client_post_uri_data = f"{quote(full_message_path_for_client)}?session_id={session_id.hex}" + + sse_stream_writer, sse_stream_reader = anyio.create_memory_object_stream[dict[str, Any]](0) + + async def sse_writer(): + logger.debug("Starting SSE writer") + async with sse_stream_writer, write_stream_reader: + await sse_stream_writer.send({"event": "endpoint", "data": client_post_uri_data}) + logger.debug(f"Sent endpoint event: {client_post_uri_data}") + + async for session_message in write_stream_reader: + logger.debug(f"Sending message via SSE: {session_message}") + await sse_stream_writer.send( + { + "event": "message", + "data": session_message.message.model_dump_json(by_alias=True, exclude_none=True), + } + ) + + async with anyio.create_task_group() as tg: + + async def response_wrapper(scope: Scope, receive: Receive, send: Send): + """ + The EventSourceResponse returning signals a client close / disconnect. + In this case we close our side of the streams to signal the client that + the connection has been closed. + """ + await EventSourceResponse(content=sse_stream_reader, data_sender_callable=sse_writer)( + scope, receive, send + ) + await read_stream_writer.aclose() + await write_stream_reader.aclose() + logging.debug(f"Client session disconnected {session_id}") + + logger.debug("Starting SSE response task") + tg.start_soon(response_wrapper, scope, receive, send) + + logger.debug("Yielding read and write streams") + yield (read_stream, write_stream) + + async def handle_post_message(self, scope: Scope, receive: Receive, send: Send) -> None: # pragma: no cover + logger.debug("Handling POST message") + request = Request(scope, receive) + + # Validate request headers for DNS rebinding protection + error_response = await self._security.validate_request(request, is_post=True) + if error_response: + return await error_response(scope, receive, send) + + session_id_param = request.query_params.get("session_id") + if session_id_param is None: + logger.warning("Received request without session_id") + response = Response("session_id is required", status_code=400) + return await response(scope, receive, send) + + try: + session_id = UUID(hex=session_id_param) + logger.debug(f"Parsed session ID: {session_id}") + except ValueError: + logger.warning(f"Received invalid session ID: {session_id_param}") + response = Response("Invalid session ID", status_code=400) + return await response(scope, receive, send) + + writer = self._read_stream_writers.get(session_id) + if not writer: + logger.warning(f"Could not find session for ID: {session_id}") + response = Response("Could not find session", status_code=404) + return await response(scope, receive, send) + + body = await request.body() + logger.debug(f"Received JSON: {body}") + + try: + message = types.JSONRPCMessage.model_validate_json(body) + logger.debug(f"Validated client message: {message}") + except ValidationError as err: + logger.exception("Failed to parse message") + response = Response("Could not parse message", status_code=400) + await response(scope, receive, send) + await writer.send(err) + return + + # Pass the ASGI scope for framework-agnostic access to request data + metadata = ServerMessageMetadata(request_context=request) + session_message = SessionMessage(message, metadata=metadata) + logger.debug(f"Sending session message to writer: {session_message}") + response = Response("Accepted", status_code=202) + await response(scope, receive, send) + await writer.send(session_message) diff --git a/source/target-vendors/cp312/mcp/server/stdio.py b/source/target-vendors/cp312/mcp/server/stdio.py new file mode 100644 index 0000000..bcb9247 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/stdio.py @@ -0,0 +1,88 @@ +""" +Stdio Server Transport Module + +This module provides functionality for creating an stdio-based transport layer +that can be used to communicate with an MCP client through standard input/output +streams. + +Example usage: +``` + async def run_server(): + async with stdio_server() as (read_stream, write_stream): + # read_stream contains incoming JSONRPCMessages from stdin + # write_stream allows sending JSONRPCMessages to stdout + server = await create_my_server() + await server.run(read_stream, write_stream, init_options) + + anyio.run(run_server) +``` +""" + +import sys +from contextlib import asynccontextmanager +from io import TextIOWrapper + +import anyio +import anyio.lowlevel +from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream + +import mcp.types as types +from mcp.shared.message import SessionMessage + + +@asynccontextmanager +async def stdio_server( + stdin: anyio.AsyncFile[str] | None = None, + stdout: anyio.AsyncFile[str] | None = None, +): + """ + Server transport for stdio: this communicates with an MCP client by reading + from the current process' stdin and writing to stdout. + """ + # Purposely not using context managers for these, as we don't want to close + # standard process handles. Encoding of stdin/stdout as text streams on + # python is platform-dependent (Windows is particularly problematic), so we + # re-wrap the underlying binary stream to ensure UTF-8. + if not stdin: + stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8")) + if not stdout: + stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8")) + + read_stream: MemoryObjectReceiveStream[SessionMessage | Exception] + read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception] + + write_stream: MemoryObjectSendStream[SessionMessage] + write_stream_reader: MemoryObjectReceiveStream[SessionMessage] + + read_stream_writer, read_stream = anyio.create_memory_object_stream(0) + write_stream, write_stream_reader = anyio.create_memory_object_stream(0) + + async def stdin_reader(): + try: + async with read_stream_writer: + async for line in stdin: + try: + message = types.JSONRPCMessage.model_validate_json(line) + except Exception as exc: # pragma: no cover + await read_stream_writer.send(exc) + continue + + session_message = SessionMessage(message) + await read_stream_writer.send(session_message) + except anyio.ClosedResourceError: # pragma: no cover + await anyio.lowlevel.checkpoint() + + async def stdout_writer(): + try: + async with write_stream_reader: + async for session_message in write_stream_reader: + json = session_message.message.model_dump_json(by_alias=True, exclude_none=True) + await stdout.write(json + "\n") + await stdout.flush() + except anyio.ClosedResourceError: # pragma: no cover + await anyio.lowlevel.checkpoint() + + async with anyio.create_task_group() as tg: + tg.start_soon(stdin_reader) + tg.start_soon(stdout_writer) + yield read_stream, write_stream diff --git a/source/target-vendors/cp312/mcp/server/streamable_http.py b/source/target-vendors/cp312/mcp/server/streamable_http.py new file mode 100644 index 0000000..2613b53 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/streamable_http.py @@ -0,0 +1,1067 @@ +""" +StreamableHTTP Server Transport Module + +This module implements an HTTP transport layer with Streamable HTTP. + +The transport handles bidirectional communication using HTTP requests and +responses, with streaming support for long-running operations. +""" + +import json +import logging +import re +from abc import ABC, abstractmethod +from collections.abc import AsyncGenerator, Awaitable, Callable +from contextlib import asynccontextmanager +from dataclasses import dataclass +from http import HTTPStatus +from typing import Any + +import anyio +from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream +from pydantic import ValidationError +from sse_starlette import EventSourceResponse +from starlette.requests import Request +from starlette.responses import Response +from starlette.types import Receive, Scope, Send + +from mcp.server.transport_security import ( + TransportSecurityMiddleware, + TransportSecuritySettings, +) +from mcp.shared.message import ServerMessageMetadata, SessionMessage +from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS +from mcp.types import ( + DEFAULT_NEGOTIATED_VERSION, + INTERNAL_ERROR, + INVALID_PARAMS, + INVALID_REQUEST, + PARSE_ERROR, + ErrorData, + JSONRPCError, + JSONRPCMessage, + JSONRPCRequest, + JSONRPCResponse, + RequestId, +) + +logger = logging.getLogger(__name__) + + +# Header names +MCP_SESSION_ID_HEADER = "mcp-session-id" +MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version" +LAST_EVENT_ID_HEADER = "last-event-id" + +# Content types +CONTENT_TYPE_JSON = "application/json" +CONTENT_TYPE_SSE = "text/event-stream" + +# Special key for the standalone GET stream +GET_STREAM_KEY = "_GET_stream" + +# Session ID validation pattern (visible ASCII characters ranging from 0x21 to 0x7E) +# Pattern ensures entire string contains only valid characters by using ^ and $ anchors +SESSION_ID_PATTERN = re.compile(r"^[\x21-\x7E]+$") + +# Type aliases +StreamId = str +EventId = str + + +@dataclass +class EventMessage: + """ + A JSONRPCMessage with an optional event ID for stream resumability. + """ + + message: JSONRPCMessage + event_id: str | None = None + + +EventCallback = Callable[[EventMessage], Awaitable[None]] + + +class EventStore(ABC): + """ + Interface for resumability support via event storage. + """ + + @abstractmethod + async def store_event(self, stream_id: StreamId, message: JSONRPCMessage | None) -> EventId: + """ + Stores an event for later retrieval. + + Args: + stream_id: ID of the stream the event belongs to + message: The JSON-RPC message to store, or None for priming events + + Returns: + The generated event ID for the stored event + """ + pass # pragma: no cover + + @abstractmethod + async def replay_events_after( + self, + last_event_id: EventId, + send_callback: EventCallback, + ) -> StreamId | None: + """ + Replays events that occurred after the specified event ID. + + Args: + last_event_id: The ID of the last event the client received + send_callback: A callback function to send events to the client + + Returns: + The stream ID of the replayed events + """ + pass # pragma: no cover + + +class StreamableHTTPServerTransport: + """ + HTTP server transport with event streaming support for MCP. + + Handles JSON-RPC messages in HTTP POST requests with SSE streaming. + Supports optional JSON responses and session management. + """ + + # Server notification streams for POST requests as well as standalone SSE stream + _read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception] | None = None + _read_stream: MemoryObjectReceiveStream[SessionMessage | Exception] | None = None + _write_stream: MemoryObjectSendStream[SessionMessage] | None = None + _write_stream_reader: MemoryObjectReceiveStream[SessionMessage] | None = None + _security: TransportSecurityMiddleware + + def __init__( + self, + mcp_session_id: str | None, + is_json_response_enabled: bool = False, + event_store: EventStore | None = None, + security_settings: TransportSecuritySettings | None = None, + retry_interval: int | None = None, + ) -> None: + """ + Initialize a new StreamableHTTP server transport. + + Args: + mcp_session_id: Optional session identifier for this connection. + Must contain only visible ASCII characters (0x21-0x7E). + is_json_response_enabled: If True, return JSON responses for requests + instead of SSE streams. Default is False. + event_store: Event store for resumability support. If provided, + resumability will be enabled, allowing clients to + reconnect and resume messages. + security_settings: Optional security settings for DNS rebinding protection. + retry_interval: Retry interval in milliseconds to suggest to clients in SSE + retry field. When set, the server will send a retry field in + SSE priming events to control client reconnection timing for + polling behavior. Only used when event_store is provided. + + Raises: + ValueError: If the session ID contains invalid characters. + """ + if mcp_session_id is not None and not SESSION_ID_PATTERN.fullmatch(mcp_session_id): + raise ValueError("Session ID must only contain visible ASCII characters (0x21-0x7E)") + + self.mcp_session_id = mcp_session_id + self.is_json_response_enabled = is_json_response_enabled + self._event_store = event_store + self._security = TransportSecurityMiddleware(security_settings) + self._retry_interval = retry_interval + self._request_streams: dict[ + RequestId, + tuple[ + MemoryObjectSendStream[EventMessage], + MemoryObjectReceiveStream[EventMessage], + ], + ] = {} + self._sse_stream_writers: dict[RequestId, MemoryObjectSendStream[dict[str, str]]] = {} + self._terminated = False + + @property + def is_terminated(self) -> bool: + """Check if this transport has been explicitly terminated.""" + return self._terminated + + def close_sse_stream(self, request_id: RequestId) -> None: # pragma: no cover + """Close SSE connection for a specific request without terminating the stream. + + This method closes the HTTP connection for the specified request, triggering + client reconnection. Events continue to be stored in the event store and will + be replayed when the client reconnects with Last-Event-ID. + + Use this to implement polling behavior during long-running operations - + client will reconnect after the retry interval specified in the priming event. + + Args: + request_id: The request ID whose SSE stream should be closed. + + Note: + This is a no-op if there is no active stream for the request ID. + Requires event_store to be configured for events to be stored during + the disconnect. + """ + writer = self._sse_stream_writers.pop(request_id, None) + if writer: + writer.close() + + # Also close and remove request streams + if request_id in self._request_streams: + send_stream, receive_stream = self._request_streams.pop(request_id) + send_stream.close() + receive_stream.close() + + def close_standalone_sse_stream(self) -> None: # pragma: no cover + """Close the standalone GET SSE stream, triggering client reconnection. + + This method closes the HTTP connection for the standalone GET stream used + for unsolicited server-to-client notifications. The client SHOULD reconnect + with Last-Event-ID to resume receiving notifications. + + Use this to implement polling behavior for the notification stream - + client will reconnect after the retry interval specified in the priming event. + + Note: + This is a no-op if there is no active standalone SSE stream. + Requires event_store to be configured for events to be stored during + the disconnect. + Currently, client reconnection for standalone GET streams is NOT + implemented - this is a known gap (see test_standalone_get_stream_reconnection). + """ + self.close_sse_stream(GET_STREAM_KEY) + + def _create_session_message( # pragma: no cover + self, + message: JSONRPCMessage, + request: Request, + request_id: RequestId, + protocol_version: str, + ) -> SessionMessage: + """Create a session message with metadata including close_sse_stream callback. + + The close_sse_stream callbacks are only provided when the client supports + resumability (protocol version >= 2025-11-25). Old clients can't resume if + the stream is closed early because they didn't receive a priming event. + """ + # Only provide close callbacks when client supports resumability + if self._event_store and protocol_version >= "2025-11-25": + + async def close_stream_callback() -> None: + self.close_sse_stream(request_id) + + async def close_standalone_stream_callback() -> None: + self.close_standalone_sse_stream() + + metadata = ServerMessageMetadata( + request_context=request, + close_sse_stream=close_stream_callback, + close_standalone_sse_stream=close_standalone_stream_callback, + ) + else: + metadata = ServerMessageMetadata(request_context=request) + + return SessionMessage(message, metadata=metadata) + + async def _maybe_send_priming_event( + self, + request_id: RequestId, + sse_stream_writer: MemoryObjectSendStream[dict[str, Any]], + protocol_version: str, + ) -> None: + """Send priming event for SSE resumability if event_store is configured. + + Only sends priming events to clients with protocol version >= 2025-11-25, + which includes the fix for handling empty SSE data. Older clients would + crash trying to parse empty data as JSON. + """ + if not self._event_store: + return + # Priming events have empty data which older clients cannot handle. + if protocol_version < "2025-11-25": + return + priming_event_id = await self._event_store.store_event( + str(request_id), # Convert RequestId to StreamId (str) + None, # Priming event has no payload + ) + priming_event: dict[str, str | int] = {"id": priming_event_id, "data": ""} + if self._retry_interval is not None: + priming_event["retry"] = self._retry_interval + await sse_stream_writer.send(priming_event) + + def _create_error_response( + self, + error_message: str, + status_code: HTTPStatus, + error_code: int = INVALID_REQUEST, + headers: dict[str, str] | None = None, + ) -> Response: + """Create an error response with a simple string message.""" + response_headers = {"Content-Type": CONTENT_TYPE_JSON} + if headers: # pragma: no cover + response_headers.update(headers) + + if self.mcp_session_id: + response_headers[MCP_SESSION_ID_HEADER] = self.mcp_session_id + + # Return a properly formatted JSON error response + error_response = JSONRPCError( + jsonrpc="2.0", + id="server-error", # We don't have a request ID for general errors + error=ErrorData( + code=error_code, + message=error_message, + ), + ) + + return Response( + error_response.model_dump_json(by_alias=True, exclude_none=True), + status_code=status_code, + headers=response_headers, + ) + + def _create_json_response( # pragma: no cover + self, + response_message: JSONRPCMessage | None, + status_code: HTTPStatus = HTTPStatus.OK, + headers: dict[str, str] | None = None, + ) -> Response: + """Create a JSON response from a JSONRPCMessage""" + response_headers = {"Content-Type": CONTENT_TYPE_JSON} + if headers: + response_headers.update(headers) + + if self.mcp_session_id: + response_headers[MCP_SESSION_ID_HEADER] = self.mcp_session_id + + return Response( + response_message.model_dump_json(by_alias=True, exclude_none=True) if response_message else None, + status_code=status_code, + headers=response_headers, + ) + + def _get_session_id(self, request: Request) -> str | None: # pragma: no cover + """Extract the session ID from request headers.""" + return request.headers.get(MCP_SESSION_ID_HEADER) + + def _create_event_data(self, event_message: EventMessage) -> dict[str, str]: # pragma: no cover + """Create event data dictionary from an EventMessage.""" + event_data = { + "event": "message", + "data": event_message.message.model_dump_json(by_alias=True, exclude_none=True), + } + + # If an event ID was provided, include it + if event_message.event_id: + event_data["id"] = event_message.event_id + + return event_data + + async def _clean_up_memory_streams(self, request_id: RequestId) -> None: # pragma: no cover + """Clean up memory streams for a given request ID.""" + if request_id in self._request_streams: + try: + # Close the request stream + await self._request_streams[request_id][0].aclose() + await self._request_streams[request_id][1].aclose() + except Exception: + # During cleanup, we catch all exceptions since streams might be in various states + logger.debug("Error closing memory streams - may already be closed") + finally: + # Remove the request stream from the mapping + self._request_streams.pop(request_id, None) + + async def handle_request(self, scope: Scope, receive: Receive, send: Send) -> None: + """Application entry point that handles all HTTP requests""" + request = Request(scope, receive) + + # Validate request headers for DNS rebinding protection + is_post = request.method == "POST" + error_response = await self._security.validate_request(request, is_post=is_post) + if error_response: # pragma: no cover + await error_response(scope, receive, send) + return + + if self._terminated: # pragma: no cover + # If the session has been terminated, return 404 Not Found + response = self._create_error_response( + "Not Found: Session has been terminated", + HTTPStatus.NOT_FOUND, + ) + await response(scope, receive, send) + return + + if request.method == "POST": + await self._handle_post_request(scope, request, receive, send) + elif request.method == "GET": # pragma: no cover + await self._handle_get_request(request, send) + elif request.method == "DELETE": # pragma: no cover + await self._handle_delete_request(request, send) + else: # pragma: no cover + await self._handle_unsupported_request(request, send) + + def _check_accept_headers(self, request: Request) -> tuple[bool, bool]: + """Check if the request accepts the required media types.""" + accept_header = request.headers.get("accept", "") + accept_types = [media_type.strip() for media_type in accept_header.split(",")] + + has_json = any(media_type.startswith(CONTENT_TYPE_JSON) for media_type in accept_types) + has_sse = any(media_type.startswith(CONTENT_TYPE_SSE) for media_type in accept_types) + + return has_json, has_sse + + def _check_content_type(self, request: Request) -> bool: + """Check if the request has the correct Content-Type.""" + content_type = request.headers.get("content-type", "") + content_type_parts = [part.strip() for part in content_type.split(";")[0].split(",")] + + return any(part == CONTENT_TYPE_JSON for part in content_type_parts) + + async def _validate_accept_header(self, request: Request, scope: Scope, send: Send) -> bool: # pragma: no cover + """Validate Accept header based on response mode. Returns True if valid.""" + has_json, has_sse = self._check_accept_headers(request) + if self.is_json_response_enabled: + # For JSON-only responses, only require application/json + if not has_json: + response = self._create_error_response( + "Not Acceptable: Client must accept application/json", + HTTPStatus.NOT_ACCEPTABLE, + ) + await response(scope, request.receive, send) + return False + # For SSE responses, require both content types + elif not (has_json and has_sse): + response = self._create_error_response( + "Not Acceptable: Client must accept both application/json and text/event-stream", + HTTPStatus.NOT_ACCEPTABLE, + ) + await response(scope, request.receive, send) + return False + return True + + async def _handle_post_request(self, scope: Scope, request: Request, receive: Receive, send: Send) -> None: + """Handle POST requests containing JSON-RPC messages.""" + writer = self._read_stream_writer + if writer is None: # pragma: no cover + raise ValueError("No read stream writer available. Ensure connect() is called first.") + try: + # Validate Accept header + if not await self._validate_accept_header(request, scope, send): + return + + # Validate Content-Type + if not self._check_content_type(request): # pragma: no cover + response = self._create_error_response( + "Unsupported Media Type: Content-Type must be application/json", + HTTPStatus.UNSUPPORTED_MEDIA_TYPE, + ) + await response(scope, receive, send) + return + + # Parse the body - only read it once + body = await request.body() + + try: + raw_message = json.loads(body) + except json.JSONDecodeError as e: + response = self._create_error_response(f"Parse error: {str(e)}", HTTPStatus.BAD_REQUEST, PARSE_ERROR) + await response(scope, receive, send) + return + + try: # pragma: no cover + message = JSONRPCMessage.model_validate(raw_message) + except ValidationError as e: # pragma: no cover + response = self._create_error_response( + f"Validation error: {str(e)}", + HTTPStatus.BAD_REQUEST, + INVALID_PARAMS, + ) + await response(scope, receive, send) + return + + # Check if this is an initialization request + is_initialization_request = ( + isinstance(message.root, JSONRPCRequest) and message.root.method == "initialize" + ) # pragma: no cover + + if is_initialization_request: # pragma: no cover + # Check if the server already has an established session + if self.mcp_session_id: + # Check if request has a session ID + request_session_id = self._get_session_id(request) + + # If request has a session ID but doesn't match, return 404 + if request_session_id and request_session_id != self.mcp_session_id: + response = self._create_error_response( + "Not Found: Invalid or expired session ID", + HTTPStatus.NOT_FOUND, + ) + await response(scope, receive, send) + return + elif not await self._validate_request_headers(request, send): # pragma: no cover + return + + # For notifications and responses only, return 202 Accepted + if not isinstance(message.root, JSONRPCRequest): # pragma: no cover + # Create response object and send it + response = self._create_json_response( + None, + HTTPStatus.ACCEPTED, + ) + await response(scope, receive, send) + + # Process the message after sending the response + metadata = ServerMessageMetadata(request_context=request) + session_message = SessionMessage(message, metadata=metadata) + await writer.send(session_message) + + return + + # Extract protocol version for priming event decision. + # For initialize requests, get from request params. + # For other requests, get from header (already validated). + protocol_version = ( + str(message.root.params.get("protocolVersion", DEFAULT_NEGOTIATED_VERSION)) + if is_initialization_request and message.root.params + else request.headers.get(MCP_PROTOCOL_VERSION_HEADER, DEFAULT_NEGOTIATED_VERSION) + ) + + # Extract the request ID outside the try block for proper scope + request_id = str(message.root.id) # pragma: no cover + # Register this stream for the request ID + self._request_streams[request_id] = anyio.create_memory_object_stream[EventMessage](0) # pragma: no cover + request_stream_reader = self._request_streams[request_id][1] # pragma: no cover + + if self.is_json_response_enabled: # pragma: no cover + # Process the message + metadata = ServerMessageMetadata(request_context=request) + session_message = SessionMessage(message, metadata=metadata) + await writer.send(session_message) + try: + # Process messages from the request-specific stream + # We need to collect all messages until we get a response + response_message = None + + # Use similar approach to SSE writer for consistency + async for event_message in request_stream_reader: + # If it's a response, this is what we're waiting for + if isinstance(event_message.message.root, JSONRPCResponse | JSONRPCError): + response_message = event_message.message + break + # For notifications and request, keep waiting + else: + logger.debug(f"received: {event_message.message.root.method}") + + # At this point we should have a response + if response_message: + # Create JSON response + response = self._create_json_response(response_message) + await response(scope, receive, send) + else: + # This shouldn't happen in normal operation + logger.error("No response message received before stream closed") + response = self._create_error_response( + "Error processing request: No response received", + HTTPStatus.INTERNAL_SERVER_ERROR, + ) + await response(scope, receive, send) + except Exception: + logger.exception("Error processing JSON response") + response = self._create_error_response( + "Error processing request", + HTTPStatus.INTERNAL_SERVER_ERROR, + INTERNAL_ERROR, + ) + await response(scope, receive, send) + finally: + await self._clean_up_memory_streams(request_id) + else: # pragma: no cover + # Create SSE stream + sse_stream_writer, sse_stream_reader = anyio.create_memory_object_stream[dict[str, str]](0) + + # Store writer reference so close_sse_stream() can close it + self._sse_stream_writers[request_id] = sse_stream_writer + + async def sse_writer(): + # Get the request ID from the incoming request message + try: + async with sse_stream_writer, request_stream_reader: + # Send priming event for SSE resumability + await self._maybe_send_priming_event(request_id, sse_stream_writer, protocol_version) + + # Process messages from the request-specific stream + async for event_message in request_stream_reader: + # Build the event data + event_data = self._create_event_data(event_message) + await sse_stream_writer.send(event_data) + + # If response, remove from pending streams and close + if isinstance( + event_message.message.root, + JSONRPCResponse | JSONRPCError, + ): + break + except anyio.ClosedResourceError: + # Expected when close_sse_stream() is called + logger.debug("SSE stream closed by close_sse_stream()") + except Exception: + logger.exception("Error in SSE writer") + finally: + logger.debug("Closing SSE writer") + self._sse_stream_writers.pop(request_id, None) + await self._clean_up_memory_streams(request_id) + + # Create and start EventSourceResponse + # SSE stream mode (original behavior) + # Set up headers + headers = { + "Cache-Control": "no-cache, no-transform", + "Connection": "keep-alive", + "Content-Type": CONTENT_TYPE_SSE, + **({MCP_SESSION_ID_HEADER: self.mcp_session_id} if self.mcp_session_id else {}), + } + response = EventSourceResponse( + content=sse_stream_reader, + data_sender_callable=sse_writer, + headers=headers, + ) + + # Start the SSE response (this will send headers immediately) + try: + # First send the response to establish the SSE connection + async with anyio.create_task_group() as tg: + tg.start_soon(response, scope, receive, send) + # Then send the message to be processed by the server + session_message = self._create_session_message(message, request, request_id, protocol_version) + await writer.send(session_message) + except Exception: + logger.exception("SSE response error") + await sse_stream_writer.aclose() + await sse_stream_reader.aclose() + await self._clean_up_memory_streams(request_id) + + except Exception as err: # pragma: no cover + logger.exception("Error handling POST request") + response = self._create_error_response( + f"Error handling POST request: {err}", + HTTPStatus.INTERNAL_SERVER_ERROR, + INTERNAL_ERROR, + ) + await response(scope, receive, send) + if writer: + await writer.send(Exception(err)) + return + + async def _handle_get_request(self, request: Request, send: Send) -> None: # pragma: no cover + """ + Handle GET request to establish SSE. + + This allows the server to communicate to the client without the client + first sending data via HTTP POST. The server can send JSON-RPC requests + and notifications on this stream. + """ + writer = self._read_stream_writer + if writer is None: + raise ValueError("No read stream writer available. Ensure connect() is called first.") + + # Validate Accept header - must include text/event-stream + _, has_sse = self._check_accept_headers(request) + + if not has_sse: + response = self._create_error_response( + "Not Acceptable: Client must accept text/event-stream", + HTTPStatus.NOT_ACCEPTABLE, + ) + await response(request.scope, request.receive, send) + return + + if not await self._validate_request_headers(request, send): + return + + # Handle resumability: check for Last-Event-ID header + if last_event_id := request.headers.get(LAST_EVENT_ID_HEADER): + await self._replay_events(last_event_id, request, send) + return + + headers = { + "Cache-Control": "no-cache, no-transform", + "Connection": "keep-alive", + "Content-Type": CONTENT_TYPE_SSE, + } + + if self.mcp_session_id: + headers[MCP_SESSION_ID_HEADER] = self.mcp_session_id + + # Check if we already have an active GET stream + if GET_STREAM_KEY in self._request_streams: + response = self._create_error_response( + "Conflict: Only one SSE stream is allowed per session", + HTTPStatus.CONFLICT, + ) + await response(request.scope, request.receive, send) + return + + # Create SSE stream + sse_stream_writer, sse_stream_reader = anyio.create_memory_object_stream[dict[str, str]](0) + + async def standalone_sse_writer(): + try: + # Create a standalone message stream for server-initiated messages + + self._request_streams[GET_STREAM_KEY] = anyio.create_memory_object_stream[EventMessage](0) + standalone_stream_reader = self._request_streams[GET_STREAM_KEY][1] + + async with sse_stream_writer, standalone_stream_reader: + # Process messages from the standalone stream + async for event_message in standalone_stream_reader: + # For the standalone stream, we handle: + # - JSONRPCNotification (server sends notifications to client) + # - JSONRPCRequest (server sends requests to client) + # We should NOT receive JSONRPCResponse + + # Send the message via SSE + event_data = self._create_event_data(event_message) + await sse_stream_writer.send(event_data) + except Exception: + logger.exception("Error in standalone SSE writer") + finally: + logger.debug("Closing standalone SSE writer") + await self._clean_up_memory_streams(GET_STREAM_KEY) + + # Create and start EventSourceResponse + response = EventSourceResponse( + content=sse_stream_reader, + data_sender_callable=standalone_sse_writer, + headers=headers, + ) + + try: + # This will send headers immediately and establish the SSE connection + await response(request.scope, request.receive, send) + except Exception: + logger.exception("Error in standalone SSE response") + await sse_stream_writer.aclose() + await sse_stream_reader.aclose() + await self._clean_up_memory_streams(GET_STREAM_KEY) + + async def _handle_delete_request(self, request: Request, send: Send) -> None: # pragma: no cover + """Handle DELETE requests for explicit session termination.""" + # Validate session ID + if not self.mcp_session_id: + # If no session ID set, return Method Not Allowed + response = self._create_error_response( + "Method Not Allowed: Session termination not supported", + HTTPStatus.METHOD_NOT_ALLOWED, + ) + await response(request.scope, request.receive, send) + return + + if not await self._validate_request_headers(request, send): + return + + await self.terminate() + + response = self._create_json_response( + None, + HTTPStatus.OK, + ) + await response(request.scope, request.receive, send) + + async def terminate(self) -> None: + """Terminate the current session, closing all streams. + + Once terminated, all requests with this session ID will receive 404 Not Found. + """ + + self._terminated = True + logger.info(f"Terminating session: {self.mcp_session_id}") + + # We need a copy of the keys to avoid modification during iteration + request_stream_keys = list(self._request_streams.keys()) + + # Close all request streams asynchronously + for key in request_stream_keys: # pragma: no cover + await self._clean_up_memory_streams(key) + + # Clear the request streams dictionary immediately + self._request_streams.clear() + try: + if self._read_stream_writer is not None: # pragma: no branch + await self._read_stream_writer.aclose() + if self._read_stream is not None: # pragma: no branch + await self._read_stream.aclose() + if self._write_stream_reader is not None: # pragma: no branch + await self._write_stream_reader.aclose() + if self._write_stream is not None: # pragma: no branch + await self._write_stream.aclose() + except Exception as e: # pragma: no cover + # During cleanup, we catch all exceptions since streams might be in various states + logger.debug(f"Error closing streams: {e}") + + async def _handle_unsupported_request(self, request: Request, send: Send) -> None: # pragma: no cover + """Handle unsupported HTTP methods.""" + headers = { + "Content-Type": CONTENT_TYPE_JSON, + "Allow": "GET, POST, DELETE", + } + if self.mcp_session_id: + headers[MCP_SESSION_ID_HEADER] = self.mcp_session_id + + response = self._create_error_response( + "Method Not Allowed", + HTTPStatus.METHOD_NOT_ALLOWED, + headers=headers, + ) + await response(request.scope, request.receive, send) + + async def _validate_request_headers(self, request: Request, send: Send) -> bool: # pragma: no cover + if not await self._validate_session(request, send): + return False + if not await self._validate_protocol_version(request, send): + return False + return True + + async def _validate_session(self, request: Request, send: Send) -> bool: # pragma: no cover + """Validate the session ID in the request.""" + if not self.mcp_session_id: + # If we're not using session IDs, return True + return True + + # Get the session ID from the request headers + request_session_id = self._get_session_id(request) + + # If no session ID provided but required, return error + if not request_session_id: + response = self._create_error_response( + "Bad Request: Missing session ID", + HTTPStatus.BAD_REQUEST, + ) + await response(request.scope, request.receive, send) + return False + + # If session ID doesn't match, return error + if request_session_id != self.mcp_session_id: + response = self._create_error_response( + "Not Found: Invalid or expired session ID", + HTTPStatus.NOT_FOUND, + ) + await response(request.scope, request.receive, send) + return False + + return True + + async def _validate_protocol_version(self, request: Request, send: Send) -> bool: # pragma: no cover + """Validate the protocol version header in the request.""" + # Get the protocol version from the request headers + protocol_version = request.headers.get(MCP_PROTOCOL_VERSION_HEADER) + + # If no protocol version provided, assume default version + if protocol_version is None: + protocol_version = DEFAULT_NEGOTIATED_VERSION + + # Check if the protocol version is supported + if protocol_version not in SUPPORTED_PROTOCOL_VERSIONS: + supported_versions = ", ".join(SUPPORTED_PROTOCOL_VERSIONS) + response = self._create_error_response( + f"Bad Request: Unsupported protocol version: {protocol_version}. " + + f"Supported versions: {supported_versions}", + HTTPStatus.BAD_REQUEST, + ) + await response(request.scope, request.receive, send) + return False + + return True + + async def _replay_events(self, last_event_id: str, request: Request, send: Send) -> None: # pragma: no cover + """ + Replays events that would have been sent after the specified event ID. + Only used when resumability is enabled. + """ + event_store = self._event_store + if not event_store: + return + + try: + headers = { + "Cache-Control": "no-cache, no-transform", + "Connection": "keep-alive", + "Content-Type": CONTENT_TYPE_SSE, + } + + if self.mcp_session_id: + headers[MCP_SESSION_ID_HEADER] = self.mcp_session_id + + # Get protocol version from header (already validated in _validate_protocol_version) + replay_protocol_version = request.headers.get(MCP_PROTOCOL_VERSION_HEADER, DEFAULT_NEGOTIATED_VERSION) + + # Create SSE stream for replay + sse_stream_writer, sse_stream_reader = anyio.create_memory_object_stream[dict[str, str]](0) + + async def replay_sender(): + try: + async with sse_stream_writer: + # Define an async callback for sending events + async def send_event(event_message: EventMessage) -> None: + event_data = self._create_event_data(event_message) + await sse_stream_writer.send(event_data) + + # Replay past events and get the stream ID + stream_id = await event_store.replay_events_after(last_event_id, send_event) + + # If stream ID not in mapping, create it + if stream_id and stream_id not in self._request_streams: + # Register SSE writer so close_sse_stream() can close it + self._sse_stream_writers[stream_id] = sse_stream_writer + + # Send priming event for this new connection + await self._maybe_send_priming_event(stream_id, sse_stream_writer, replay_protocol_version) + + # Create new request streams for this connection + self._request_streams[stream_id] = anyio.create_memory_object_stream[EventMessage](0) + msg_reader = self._request_streams[stream_id][1] + + # Forward messages to SSE + async with msg_reader: + async for event_message in msg_reader: + event_data = self._create_event_data(event_message) + + await sse_stream_writer.send(event_data) + except anyio.ClosedResourceError: + # Expected when close_sse_stream() is called + logger.debug("Replay SSE stream closed by close_sse_stream()") + except Exception: + logger.exception("Error in replay sender") + + # Create and start EventSourceResponse + response = EventSourceResponse( + content=sse_stream_reader, + data_sender_callable=replay_sender, + headers=headers, + ) + + try: + await response(request.scope, request.receive, send) + except Exception: + logger.exception("Error in replay response") + finally: + await sse_stream_writer.aclose() + await sse_stream_reader.aclose() + + except Exception: + logger.exception("Error replaying events") + response = self._create_error_response( + "Error replaying events", + HTTPStatus.INTERNAL_SERVER_ERROR, + INTERNAL_ERROR, + ) + await response(request.scope, request.receive, send) + + @asynccontextmanager + async def connect( + self, + ) -> AsyncGenerator[ + tuple[ + MemoryObjectReceiveStream[SessionMessage | Exception], + MemoryObjectSendStream[SessionMessage], + ], + None, + ]: + """Context manager that provides read and write streams for a connection. + + Yields: + Tuple of (read_stream, write_stream) for bidirectional communication + """ + + # Create the memory streams for this connection + + read_stream_writer, read_stream = anyio.create_memory_object_stream[SessionMessage | Exception](0) + write_stream, write_stream_reader = anyio.create_memory_object_stream[SessionMessage](0) + + # Store the streams + self._read_stream_writer = read_stream_writer + self._read_stream = read_stream + self._write_stream_reader = write_stream_reader + self._write_stream = write_stream + + # Start a task group for message routing + async with anyio.create_task_group() as tg: + # Create a message router that distributes messages to request streams + async def message_router(): # pragma: no cover + try: + async for session_message in write_stream_reader: + # Determine which request stream(s) should receive this message + message = session_message.message + target_request_id = None + # Check if this is a response + if isinstance(message.root, JSONRPCResponse | JSONRPCError): + response_id = str(message.root.id) + # If this response is for an existing request stream, + # send it there + target_request_id = response_id + # Extract related_request_id from meta if it exists + elif ( + session_message.metadata is not None + and isinstance( + session_message.metadata, + ServerMessageMetadata, + ) + and session_message.metadata.related_request_id is not None + ): + target_request_id = str(session_message.metadata.related_request_id) + + request_stream_id = target_request_id if target_request_id is not None else GET_STREAM_KEY + + # Store the event if we have an event store, + # regardless of whether a client is connected + # messages will be replayed on the re-connect + event_id = None + if self._event_store: + event_id = await self._event_store.store_event(request_stream_id, message) + logger.debug(f"Stored {event_id} from {request_stream_id}") + + if request_stream_id in self._request_streams: + try: + # Send both the message and the event ID + await self._request_streams[request_stream_id][0].send(EventMessage(message, event_id)) + except ( + anyio.BrokenResourceError, + anyio.ClosedResourceError, + ): + # Stream might be closed, remove from registry + self._request_streams.pop(request_stream_id, None) + else: + logger.debug( + f"""Request stream {request_stream_id} not found + for message. Still processing message as the client + might reconnect and replay.""" + ) + except anyio.ClosedResourceError: + if self._terminated: + logger.debug("Read stream closed by client") + else: + logger.exception("Unexpected closure of read stream in message router") + except Exception: + logger.exception("Error in message router") + + # Start the message router + tg.start_soon(message_router) + + try: + # Yield the streams for the caller to use + yield read_stream, write_stream + finally: + for stream_id in list(self._request_streams.keys()): # pragma: no cover + await self._clean_up_memory_streams(stream_id) + self._request_streams.clear() + + # Clean up the read and write streams + try: + await read_stream_writer.aclose() + await read_stream.aclose() + await write_stream_reader.aclose() + await write_stream.aclose() + except Exception as e: # pragma: no cover + # During cleanup, we catch all exceptions since streams might be in various states + logger.debug(f"Error closing streams: {e}") diff --git a/source/target-vendors/cp312/mcp/server/streamable_http_manager.py b/source/target-vendors/cp312/mcp/server/streamable_http_manager.py new file mode 100644 index 0000000..6a16724 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/streamable_http_manager.py @@ -0,0 +1,297 @@ +"""StreamableHTTP Session Manager for MCP servers.""" + +from __future__ import annotations + +import contextlib +import logging +from collections.abc import AsyncIterator +from http import HTTPStatus +from typing import Any +from uuid import uuid4 + +import anyio +from anyio.abc import TaskStatus +from starlette.requests import Request +from starlette.responses import Response +from starlette.types import Receive, Scope, Send + +from mcp.server.lowlevel.server import Server as MCPServer +from mcp.server.streamable_http import ( + MCP_SESSION_ID_HEADER, + EventStore, + StreamableHTTPServerTransport, +) +from mcp.server.transport_security import TransportSecuritySettings +from mcp.types import INVALID_REQUEST, ErrorData, JSONRPCError + +logger = logging.getLogger(__name__) + + +class StreamableHTTPSessionManager: + """ + Manages StreamableHTTP sessions with optional resumability via event store. + + This class abstracts away the complexity of session management, event storage, + and request handling for StreamableHTTP transports. It handles: + + 1. Session tracking for clients + 2. Resumability via an optional event store + 3. Connection management and lifecycle + 4. Request handling and transport setup + + Important: Only one StreamableHTTPSessionManager instance should be created + per application. The instance cannot be reused after its run() context has + completed. If you need to restart the manager, create a new instance. + + Args: + app: The MCP server instance + event_store: Optional event store for resumability support. + If provided, enables resumable connections where clients + can reconnect and receive missed events. + If None, sessions are still tracked but not resumable. + json_response: Whether to use JSON responses instead of SSE streams + stateless: If True, creates a completely fresh transport for each request + with no session tracking or state persistence between requests. + security_settings: Optional transport security settings. + retry_interval: Retry interval in milliseconds to suggest to clients in SSE + retry field. Used for SSE polling behavior. + """ + + def __init__( + self, + app: MCPServer[Any, Any], + event_store: EventStore | None = None, + json_response: bool = False, + stateless: bool = False, + security_settings: TransportSecuritySettings | None = None, + retry_interval: int | None = None, + ): + self.app = app + self.event_store = event_store + self.json_response = json_response + self.stateless = stateless + self.security_settings = security_settings + self.retry_interval = retry_interval + + # Session tracking (only used if not stateless) + self._session_creation_lock = anyio.Lock() + self._server_instances: dict[str, StreamableHTTPServerTransport] = {} + + # The task group will be set during lifespan + self._task_group = None + # Thread-safe tracking of run() calls + self._run_lock = anyio.Lock() + self._has_started = False + + @contextlib.asynccontextmanager + async def run(self) -> AsyncIterator[None]: + """ + Run the session manager with proper lifecycle management. + + This creates and manages the task group for all session operations. + + Important: This method can only be called once per instance. The same + StreamableHTTPSessionManager instance cannot be reused after this + context manager exits. Create a new instance if you need to restart. + + Use this in the lifespan context manager of your Starlette app: + + @contextlib.asynccontextmanager + async def lifespan(app: Starlette) -> AsyncIterator[None]: + async with session_manager.run(): + yield + """ + # Thread-safe check to ensure run() is only called once + async with self._run_lock: + if self._has_started: + raise RuntimeError( + "StreamableHTTPSessionManager .run() can only be called " + "once per instance. Create a new instance if you need to run again." + ) + self._has_started = True + + async with anyio.create_task_group() as tg: + # Store the task group for later use + self._task_group = tg + logger.info("StreamableHTTP session manager started") + try: + yield # Let the application run + finally: + logger.info("StreamableHTTP session manager shutting down") + # Cancel task group to stop all spawned tasks + tg.cancel_scope.cancel() + self._task_group = None + # Clear any remaining server instances + self._server_instances.clear() + + async def handle_request( + self, + scope: Scope, + receive: Receive, + send: Send, + ) -> None: + """ + Process ASGI request with proper session handling and transport setup. + + Dispatches to the appropriate handler based on stateless mode. + + Args: + scope: ASGI scope + receive: ASGI receive function + send: ASGI send function + """ + if self._task_group is None: + raise RuntimeError("Task group is not initialized. Make sure to use run().") + + # Dispatch to the appropriate handler + if self.stateless: + await self._handle_stateless_request(scope, receive, send) + else: + await self._handle_stateful_request(scope, receive, send) + + async def _handle_stateless_request( + self, + scope: Scope, + receive: Receive, + send: Send, + ) -> None: + """ + Process request in stateless mode - creating a new transport for each request. + + Args: + scope: ASGI scope + receive: ASGI receive function + send: ASGI send function + """ + logger.debug("Stateless mode: Creating new transport for this request") + # No session ID needed in stateless mode + http_transport = StreamableHTTPServerTransport( + mcp_session_id=None, # No session tracking in stateless mode + is_json_response_enabled=self.json_response, + event_store=None, # No event store in stateless mode + security_settings=self.security_settings, + ) + + # Start server in a new task + async def run_stateless_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORED): + async with http_transport.connect() as streams: + read_stream, write_stream = streams + task_status.started() + try: + await self.app.run( + read_stream, + write_stream, + self.app.create_initialization_options(), + stateless=True, + ) + except Exception: # pragma: no cover + logger.exception("Stateless session crashed") + + # Assert task group is not None for type checking + assert self._task_group is not None + # Start the server task + await self._task_group.start(run_stateless_server) + + # Handle the HTTP request and return the response + await http_transport.handle_request(scope, receive, send) + + # Terminate the transport after the request is handled + await http_transport.terminate() + + async def _handle_stateful_request( + self, + scope: Scope, + receive: Receive, + send: Send, + ) -> None: + """ + Process request in stateful mode - maintaining session state between requests. + + Args: + scope: ASGI scope + receive: ASGI receive function + send: ASGI send function + """ + request = Request(scope, receive) + request_mcp_session_id = request.headers.get(MCP_SESSION_ID_HEADER) + + # Existing session case + if request_mcp_session_id is not None and request_mcp_session_id in self._server_instances: # pragma: no cover + transport = self._server_instances[request_mcp_session_id] + logger.debug("Session already exists, handling request directly") + await transport.handle_request(scope, receive, send) + return + + if request_mcp_session_id is None: + # New session case + logger.debug("Creating new transport") + async with self._session_creation_lock: + new_session_id = uuid4().hex + http_transport = StreamableHTTPServerTransport( + mcp_session_id=new_session_id, + is_json_response_enabled=self.json_response, + event_store=self.event_store, # May be None (no resumability) + security_settings=self.security_settings, + retry_interval=self.retry_interval, + ) + + assert http_transport.mcp_session_id is not None + self._server_instances[http_transport.mcp_session_id] = http_transport + logger.info(f"Created new transport with session ID: {new_session_id}") + + # Define the server runner + async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORED) -> None: + async with http_transport.connect() as streams: + read_stream, write_stream = streams + task_status.started() + try: + await self.app.run( + read_stream, + write_stream, + self.app.create_initialization_options(), + stateless=False, # Stateful mode + ) + except Exception as e: + logger.error( + f"Session {http_transport.mcp_session_id} crashed: {e}", + exc_info=True, + ) + finally: + # Only remove from instances if not terminated + if ( # pragma: no branch + http_transport.mcp_session_id + and http_transport.mcp_session_id in self._server_instances + and not http_transport.is_terminated + ): + logger.info( + "Cleaning up crashed session " + f"{http_transport.mcp_session_id} from " + "active instances." + ) + del self._server_instances[http_transport.mcp_session_id] + + # Assert task group is not None for type checking + assert self._task_group is not None + # Start the server task + await self._task_group.start(run_server) + + # Handle the HTTP request and return the response + await http_transport.handle_request(scope, receive, send) + else: + # Unknown or expired session ID - return 404 per MCP spec + # TODO: Align error code once spec clarifies + # See: https://github.com/modelcontextprotocol/python-sdk/issues/1821 + error_response = JSONRPCError( + jsonrpc="2.0", + id="server-error", + error=ErrorData( + code=INVALID_REQUEST, + message="Session not found", + ), + ) + response = Response( + content=error_response.model_dump_json(by_alias=True, exclude_none=True), + status_code=HTTPStatus.NOT_FOUND, + media_type="application/json", + ) + await response(scope, receive, send) diff --git a/source/target-vendors/cp312/mcp/server/transport_security.py b/source/target-vendors/cp312/mcp/server/transport_security.py new file mode 100644 index 0000000..ee1e450 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/transport_security.py @@ -0,0 +1,127 @@ +"""DNS rebinding protection for MCP server transports.""" + +import logging + +from pydantic import BaseModel, Field +from starlette.requests import Request +from starlette.responses import Response + +logger = logging.getLogger(__name__) + + +class TransportSecuritySettings(BaseModel): + """Settings for MCP transport security features. + + These settings help protect against DNS rebinding attacks by validating + incoming request headers. + """ + + enable_dns_rebinding_protection: bool = Field( + default=True, + description="Enable DNS rebinding protection (recommended for production)", + ) + + allowed_hosts: list[str] = Field( + default=[], + description="List of allowed Host header values. Only applies when " + + "enable_dns_rebinding_protection is True.", + ) + + allowed_origins: list[str] = Field( + default=[], + description="List of allowed Origin header values. Only applies when " + + "enable_dns_rebinding_protection is True.", + ) + + +class TransportSecurityMiddleware: + """Middleware to enforce DNS rebinding protection for MCP transport endpoints.""" + + def __init__(self, settings: TransportSecuritySettings | None = None): + # If not specified, disable DNS rebinding protection by default + # for backwards compatibility + self.settings = settings or TransportSecuritySettings(enable_dns_rebinding_protection=False) + + def _validate_host(self, host: str | None) -> bool: # pragma: no cover + """Validate the Host header against allowed values.""" + if not host: + logger.warning("Missing Host header in request") + return False + + # Check exact match first + if host in self.settings.allowed_hosts: + return True + + # Check wildcard port patterns + for allowed in self.settings.allowed_hosts: + if allowed.endswith(":*"): + # Extract base host from pattern + base_host = allowed[:-2] + # Check if the actual host starts with base host and has a port + if host.startswith(base_host + ":"): + return True + + logger.warning(f"Invalid Host header: {host}") + return False + + def _validate_origin(self, origin: str | None) -> bool: # pragma: no cover + """Validate the Origin header against allowed values.""" + # Origin can be absent for same-origin requests + if not origin: + return True + + # Check exact match first + if origin in self.settings.allowed_origins: + return True + + # Check wildcard port patterns + for allowed in self.settings.allowed_origins: + if allowed.endswith(":*"): + # Extract base origin from pattern + base_origin = allowed[:-2] + # Check if the actual origin starts with base origin and has a port + if origin.startswith(base_origin + ":"): + return True + + logger.warning(f"Invalid Origin header: {origin}") + return False + + def _validate_content_type(self, content_type: str | None) -> bool: # pragma: no cover + """Validate the Content-Type header for POST requests.""" + if not content_type: + logger.warning("Missing Content-Type header in POST request") + return False + + # Content-Type must start with application/json + if not content_type.lower().startswith("application/json"): + logger.warning(f"Invalid Content-Type header: {content_type}") + return False + + return True + + async def validate_request(self, request: Request, is_post: bool = False) -> Response | None: + """Validate request headers for DNS rebinding protection. + + Returns None if validation passes, or an error Response if validation fails. + """ + # Always validate Content-Type for POST requests + if is_post: # pragma: no branch + content_type = request.headers.get("content-type") + if not self._validate_content_type(content_type): # pragma: no cover + return Response("Invalid Content-Type header", status_code=400) + + # Skip remaining validation if DNS rebinding protection is disabled + if not self.settings.enable_dns_rebinding_protection: + return None + + # Validate Host header # pragma: no cover + host = request.headers.get("host") # pragma: no cover + if not self._validate_host(host): # pragma: no cover + return Response("Invalid Host header", status_code=421) # pragma: no cover + + # Validate Origin header # pragma: no cover + origin = request.headers.get("origin") # pragma: no cover + if not self._validate_origin(origin): # pragma: no cover + return Response("Invalid Origin header", status_code=403) # pragma: no cover + + return None # pragma: no cover diff --git a/source/target-vendors/cp312/mcp/server/validation.py b/source/target-vendors/cp312/mcp/server/validation.py new file mode 100644 index 0000000..2ccd705 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/validation.py @@ -0,0 +1,104 @@ +""" +Shared validation functions for server requests. + +This module provides validation logic for sampling and elicitation requests +that is shared across normal and task-augmented code paths. +""" + +from mcp.shared.exceptions import McpError +from mcp.types import ( + INVALID_PARAMS, + ClientCapabilities, + ErrorData, + SamplingMessage, + Tool, + ToolChoice, +) + + +def check_sampling_tools_capability(client_caps: ClientCapabilities | None) -> bool: + """ + Check if the client supports sampling tools capability. + + Args: + client_caps: The client's declared capabilities + + Returns: + True if client supports sampling.tools, False otherwise + """ + if client_caps is None: + return False + if client_caps.sampling is None: + return False + if client_caps.sampling.tools is None: + return False + return True + + +def validate_sampling_tools( + client_caps: ClientCapabilities | None, + tools: list[Tool] | None, + tool_choice: ToolChoice | None, +) -> None: + """ + Validate that the client supports sampling tools if tools are being used. + + Args: + client_caps: The client's declared capabilities + tools: The tools list, if provided + tool_choice: The tool choice setting, if provided + + Raises: + McpError: If tools/tool_choice are provided but client doesn't support them + """ + if tools is not None or tool_choice is not None: + if not check_sampling_tools_capability(client_caps): + raise McpError( + ErrorData( + code=INVALID_PARAMS, + message="Client does not support sampling tools capability", + ) + ) + + +def validate_tool_use_result_messages(messages: list[SamplingMessage]) -> None: + """ + Validate tool_use/tool_result message structure per SEP-1577. + + This validation ensures: + 1. Messages with tool_result content contain ONLY tool_result content + 2. tool_result messages are preceded by a message with tool_use + 3. tool_result IDs match the tool_use IDs from the previous message + + See: https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1577 + + Args: + messages: The list of sampling messages to validate + + Raises: + ValueError: If the message structure is invalid + """ + if not messages: + return + + last_content = messages[-1].content_as_list + has_tool_results = any(c.type == "tool_result" for c in last_content) + + previous_content = messages[-2].content_as_list if len(messages) >= 2 else None + has_previous_tool_use = previous_content and any(c.type == "tool_use" for c in previous_content) + + if has_tool_results: + # Per spec: "SamplingMessage with tool result content blocks + # MUST NOT contain other content types." + if any(c.type != "tool_result" for c in last_content): + raise ValueError("The last message must contain only tool_result content if any is present") + if previous_content is None: + raise ValueError("tool_result requires a previous message containing tool_use") + if not has_previous_tool_use: + raise ValueError("tool_result blocks do not match any tool_use in the previous message") + + if has_previous_tool_use and previous_content: + tool_use_ids = {c.id for c in previous_content if c.type == "tool_use"} + tool_result_ids = {c.toolUseId for c in last_content if c.type == "tool_result"} + if tool_use_ids != tool_result_ids: + raise ValueError("ids of tool_result blocks and tool_use blocks from previous message do not match") diff --git a/source/target-vendors/cp312/mcp/server/websocket.py b/source/target-vendors/cp312/mcp/server/websocket.py new file mode 100644 index 0000000..5d5efd1 --- /dev/null +++ b/source/target-vendors/cp312/mcp/server/websocket.py @@ -0,0 +1,62 @@ +import logging +from contextlib import asynccontextmanager + +import anyio +from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream +from pydantic_core import ValidationError +from starlette.types import Receive, Scope, Send +from starlette.websockets import WebSocket + +import mcp.types as types +from mcp.shared.message import SessionMessage + +logger = logging.getLogger(__name__) + + +@asynccontextmanager # pragma: no cover +async def websocket_server(scope: Scope, receive: Receive, send: Send): + """ + WebSocket server transport for MCP. This is an ASGI application, suitable to be + used with a framework like Starlette and a server like Hypercorn. + """ + + websocket = WebSocket(scope, receive, send) + await websocket.accept(subprotocol="mcp") + + read_stream: MemoryObjectReceiveStream[SessionMessage | Exception] + read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception] + + write_stream: MemoryObjectSendStream[SessionMessage] + write_stream_reader: MemoryObjectReceiveStream[SessionMessage] + + read_stream_writer, read_stream = anyio.create_memory_object_stream(0) + write_stream, write_stream_reader = anyio.create_memory_object_stream(0) + + async def ws_reader(): + try: + async with read_stream_writer: + async for msg in websocket.iter_text(): + try: + client_message = types.JSONRPCMessage.model_validate_json(msg) + except ValidationError as exc: + await read_stream_writer.send(exc) + continue + + session_message = SessionMessage(client_message) + await read_stream_writer.send(session_message) + except anyio.ClosedResourceError: + await websocket.close() + + async def ws_writer(): + try: + async with write_stream_reader: + async for session_message in write_stream_reader: + obj = session_message.message.model_dump_json(by_alias=True, exclude_none=True) + await websocket.send_text(obj) + except anyio.ClosedResourceError: + await websocket.close() + + async with anyio.create_task_group() as tg: + tg.start_soon(ws_reader) + tg.start_soon(ws_writer) + yield (read_stream, write_stream) diff --git a/source/target-vendors/cp312/mcp/shared/__init__.py b/source/target-vendors/cp312/mcp/shared/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/mcp/shared/_httpx_utils.py b/source/target-vendors/cp312/mcp/shared/_httpx_utils.py new file mode 100644 index 0000000..945ef80 --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/_httpx_utils.py @@ -0,0 +1,87 @@ +"""Utilities for creating standardized httpx AsyncClient instances.""" + +from typing import Any, Protocol + +import httpx + +__all__ = ["create_mcp_http_client", "MCP_DEFAULT_TIMEOUT", "MCP_DEFAULT_SSE_READ_TIMEOUT"] + +# Default MCP timeout configuration +MCP_DEFAULT_TIMEOUT = 30.0 # General operations (seconds) +MCP_DEFAULT_SSE_READ_TIMEOUT = 300.0 # SSE streams - 5 minutes (seconds) + + +class McpHttpClientFactory(Protocol): # pragma: no branch + def __call__( # pragma: no branch + self, + headers: dict[str, str] | None = None, + timeout: httpx.Timeout | None = None, + auth: httpx.Auth | None = None, + ) -> httpx.AsyncClient: ... + + +def create_mcp_http_client( + headers: dict[str, str] | None = None, + timeout: httpx.Timeout | None = None, + auth: httpx.Auth | None = None, +) -> httpx.AsyncClient: + """Create a standardized httpx AsyncClient with MCP defaults. + + This function provides common defaults used throughout the MCP codebase: + - follow_redirects=True (always enabled) + - Default timeout of 30 seconds if not specified + + Args: + headers: Optional headers to include with all requests. + timeout: Request timeout as httpx.Timeout object. + Defaults to 30 seconds if not specified. + auth: Optional authentication handler. + + Returns: + Configured httpx.AsyncClient instance with MCP defaults. + + Note: + The returned AsyncClient must be used as a context manager to ensure + proper cleanup of connections. + + Examples: + # Basic usage with MCP defaults + async with create_mcp_http_client() as client: + response = await client.get("https://api.example.com") + + # With custom headers + headers = {"Authorization": "Bearer token"} + async with create_mcp_http_client(headers) as client: + response = await client.get("/endpoint") + + # With both custom headers and timeout + timeout = httpx.Timeout(60.0, read=300.0) + async with create_mcp_http_client(headers, timeout) as client: + response = await client.get("/long-request") + + # With authentication + from httpx import BasicAuth + auth = BasicAuth(username="user", password="pass") + async with create_mcp_http_client(headers, timeout, auth) as client: + response = await client.get("/protected-endpoint") + """ + # Set MCP defaults + kwargs: dict[str, Any] = { + "follow_redirects": True, + } + + # Handle timeout + if timeout is None: + kwargs["timeout"] = httpx.Timeout(MCP_DEFAULT_TIMEOUT, read=MCP_DEFAULT_SSE_READ_TIMEOUT) + else: + kwargs["timeout"] = timeout + + # Handle headers + if headers is not None: + kwargs["headers"] = headers + + # Handle authentication + if auth is not None: # pragma: no cover + kwargs["auth"] = auth + + return httpx.AsyncClient(**kwargs) diff --git a/source/target-vendors/cp312/mcp/shared/auth.py b/source/target-vendors/cp312/mcp/shared/auth.py new file mode 100644 index 0000000..d329099 --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/auth.py @@ -0,0 +1,159 @@ +from typing import Any, Literal + +from pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field, field_validator + + +class OAuthToken(BaseModel): + """ + See https://datatracker.ietf.org/doc/html/rfc6749#section-5.1 + """ + + access_token: str + token_type: Literal["Bearer"] = "Bearer" + expires_in: int | None = None + scope: str | None = None + refresh_token: str | None = None + + @field_validator("token_type", mode="before") + @classmethod + def normalize_token_type(cls, v: str | None) -> str | None: + if isinstance(v, str): + # Bearer is title-cased in the spec, so we normalize it + # https://datatracker.ietf.org/doc/html/rfc6750#section-4 + return v.title() + return v # pragma: no cover + + +class InvalidScopeError(Exception): + def __init__(self, message: str): + self.message = message + + +class InvalidRedirectUriError(Exception): + def __init__(self, message: str): + self.message = message + + +class OAuthClientMetadata(BaseModel): + """ + RFC 7591 OAuth 2.0 Dynamic Client Registration metadata. + See https://datatracker.ietf.org/doc/html/rfc7591#section-2 + for the full specification. + """ + + redirect_uris: list[AnyUrl] | None = Field(..., min_length=1) + # supported auth methods for the token endpoint + token_endpoint_auth_method: ( + Literal["none", "client_secret_post", "client_secret_basic", "private_key_jwt"] | None + ) = None + # supported grant_types of this implementation + grant_types: list[ + Literal["authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:jwt-bearer"] | str + ] = [ + "authorization_code", + "refresh_token", + ] + # The MCP spec requires the "code" response type, but OAuth + # servers may also return additional types they support + response_types: list[str] = ["code"] + scope: str | None = None + + # these fields are currently unused, but we support & store them for potential + # future use + client_name: str | None = None + client_uri: AnyHttpUrl | None = None + logo_uri: AnyHttpUrl | None = None + contacts: list[str] | None = None + tos_uri: AnyHttpUrl | None = None + policy_uri: AnyHttpUrl | None = None + jwks_uri: AnyHttpUrl | None = None + jwks: Any | None = None + software_id: str | None = None + software_version: str | None = None + + def validate_scope(self, requested_scope: str | None) -> list[str] | None: + if requested_scope is None: + return None + requested_scopes = requested_scope.split(" ") + allowed_scopes = [] if self.scope is None else self.scope.split(" ") + for scope in requested_scopes: + if scope not in allowed_scopes: # pragma: no branch + raise InvalidScopeError(f"Client was not registered with scope {scope}") + return requested_scopes # pragma: no cover + + def validate_redirect_uri(self, redirect_uri: AnyUrl | None) -> AnyUrl: + if redirect_uri is not None: + # Validate redirect_uri against client's registered redirect URIs + if self.redirect_uris is None or redirect_uri not in self.redirect_uris: + raise InvalidRedirectUriError(f"Redirect URI '{redirect_uri}' not registered for client") + return redirect_uri + elif self.redirect_uris is not None and len(self.redirect_uris) == 1: + return self.redirect_uris[0] + else: + raise InvalidRedirectUriError("redirect_uri must be specified when client has multiple registered URIs") + + +class OAuthClientInformationFull(OAuthClientMetadata): + """ + RFC 7591 OAuth 2.0 Dynamic Client Registration full response + (client information plus metadata). + """ + + client_id: str | None = None + client_secret: str | None = None + client_id_issued_at: int | None = None + client_secret_expires_at: int | None = None + + +class OAuthMetadata(BaseModel): + """ + RFC 8414 OAuth 2.0 Authorization Server Metadata. + See https://datatracker.ietf.org/doc/html/rfc8414#section-2 + """ + + issuer: AnyHttpUrl + authorization_endpoint: AnyHttpUrl + token_endpoint: AnyHttpUrl + registration_endpoint: AnyHttpUrl | None = None + scopes_supported: list[str] | None = None + response_types_supported: list[str] = ["code"] + response_modes_supported: list[str] | None = None + grant_types_supported: list[str] | None = None + token_endpoint_auth_methods_supported: list[str] | None = None + token_endpoint_auth_signing_alg_values_supported: list[str] | None = None + service_documentation: AnyHttpUrl | None = None + ui_locales_supported: list[str] | None = None + op_policy_uri: AnyHttpUrl | None = None + op_tos_uri: AnyHttpUrl | None = None + revocation_endpoint: AnyHttpUrl | None = None + revocation_endpoint_auth_methods_supported: list[str] | None = None + revocation_endpoint_auth_signing_alg_values_supported: list[str] | None = None + introspection_endpoint: AnyHttpUrl | None = None + introspection_endpoint_auth_methods_supported: list[str] | None = None + introspection_endpoint_auth_signing_alg_values_supported: list[str] | None = None + code_challenge_methods_supported: list[str] | None = None + client_id_metadata_document_supported: bool | None = None + + +class ProtectedResourceMetadata(BaseModel): + """ + RFC 9728 OAuth 2.0 Protected Resource Metadata. + See https://datatracker.ietf.org/doc/html/rfc9728#section-2 + """ + + resource: AnyHttpUrl + authorization_servers: list[AnyHttpUrl] = Field(..., min_length=1) + jwks_uri: AnyHttpUrl | None = None + scopes_supported: list[str] | None = None + bearer_methods_supported: list[str] | None = Field(default=["header"]) # MCP only supports header method + resource_signing_alg_values_supported: list[str] | None = None + resource_name: str | None = None + resource_documentation: AnyHttpUrl | None = None + resource_policy_uri: AnyHttpUrl | None = None + resource_tos_uri: AnyHttpUrl | None = None + # tls_client_certificate_bound_access_tokens default is False, but ommited here for clarity + tls_client_certificate_bound_access_tokens: bool | None = None + authorization_details_types_supported: list[str] | None = None + dpop_signing_alg_values_supported: list[str] | None = None + # dpop_bound_access_tokens_required default is False, but ommited here for clarity + dpop_bound_access_tokens_required: bool | None = None diff --git a/source/target-vendors/cp312/mcp/shared/auth_utils.py b/source/target-vendors/cp312/mcp/shared/auth_utils.py new file mode 100644 index 0000000..8f3c542 --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/auth_utils.py @@ -0,0 +1,85 @@ +"""Utilities for OAuth 2.0 Resource Indicators (RFC 8707) and PKCE (RFC 7636).""" + +import time +from urllib.parse import urlparse, urlsplit, urlunsplit + +from pydantic import AnyUrl, HttpUrl + + +def resource_url_from_server_url(url: str | HttpUrl | AnyUrl) -> str: + """Convert server URL to canonical resource URL per RFC 8707. + + RFC 8707 section 2 states that resource URIs "MUST NOT include a fragment component". + Returns absolute URI with lowercase scheme/host for canonical form. + + Args: + url: Server URL to convert + + Returns: + Canonical resource URL string + """ + # Convert to string if needed + url_str = str(url) + + # Parse the URL and remove fragment, create canonical form + parsed = urlsplit(url_str) + canonical = urlunsplit(parsed._replace(scheme=parsed.scheme.lower(), netloc=parsed.netloc.lower(), fragment="")) + + return canonical + + +def check_resource_allowed(requested_resource: str, configured_resource: str) -> bool: + """Check if a requested resource URL matches a configured resource URL. + + A requested resource matches if it has the same scheme, domain, port, + and its path starts with the configured resource's path. This allows + hierarchical matching where a token for a parent resource can be used + for child resources. + + Args: + requested_resource: The resource URL being requested + configured_resource: The resource URL that has been configured + + Returns: + True if the requested resource matches the configured resource + """ + # Parse both URLs + requested = urlparse(requested_resource) + configured = urlparse(configured_resource) + + # Compare scheme, host, and port (origin) + if requested.scheme.lower() != configured.scheme.lower() or requested.netloc.lower() != configured.netloc.lower(): + return False + + # Handle cases like requested=/foo and configured=/foo/ + requested_path = requested.path + configured_path = configured.path + + # If requested path is shorter, it cannot be a child + if len(requested_path) < len(configured_path): + return False + + # Check if the requested path starts with the configured path + # Ensure both paths end with / for proper comparison + # This ensures that paths like "/api123" don't incorrectly match "/api" + if not requested_path.endswith("/"): + requested_path += "/" + if not configured_path.endswith("/"): + configured_path += "/" + + return requested_path.startswith(configured_path) + + +def calculate_token_expiry(expires_in: int | str | None) -> float | None: + """Calculate token expiry timestamp from expires_in seconds. + + Args: + expires_in: Seconds until token expiration (may be string from some servers) + + Returns: + Unix timestamp when token expires, or None if no expiry specified + """ + if expires_in is None: + return None # pragma: no cover + # Defensive: handle servers that return expires_in as string + return time.time() + int(expires_in) diff --git a/source/target-vendors/cp312/mcp/shared/context.py b/source/target-vendors/cp312/mcp/shared/context.py new file mode 100644 index 0000000..5cf6588 --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/context.py @@ -0,0 +1,32 @@ +""" +Request context for MCP handlers. +""" + +from dataclasses import dataclass, field +from typing import Any, Generic + +from typing_extensions import TypeVar + +from mcp.shared.message import CloseSSEStreamCallback +from mcp.shared.session import BaseSession +from mcp.types import RequestId, RequestParams + +SessionT = TypeVar("SessionT", bound=BaseSession[Any, Any, Any, Any, Any]) +LifespanContextT = TypeVar("LifespanContextT") +RequestT = TypeVar("RequestT", default=Any) + + +@dataclass +class RequestContext(Generic[SessionT, LifespanContextT, RequestT]): + request_id: RequestId + meta: RequestParams.Meta | None + session: SessionT + lifespan_context: LifespanContextT + # NOTE: This is typed as Any to avoid circular imports. The actual type is + # mcp.server.experimental.request_context.Experimental, but importing it here + # triggers mcp.server.__init__ -> fastmcp -> tools -> back to this module. + # The Server sets this to an Experimental instance at runtime. + experimental: Any = field(default=None) + request: RequestT | None = None + close_sse_stream: CloseSSEStreamCallback | None = None + close_standalone_sse_stream: CloseSSEStreamCallback | None = None diff --git a/source/target-vendors/cp312/mcp/shared/exceptions.py b/source/target-vendors/cp312/mcp/shared/exceptions.py new file mode 100644 index 0000000..4943114 --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/exceptions.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from typing import Any, cast + +from mcp.types import URL_ELICITATION_REQUIRED, ElicitRequestURLParams, ErrorData + + +class McpError(Exception): + """ + Exception type raised when an error arrives over an MCP connection. + """ + + error: ErrorData + + def __init__(self, error: ErrorData): + """Initialize McpError.""" + super().__init__(error.message) + self.error = error + + +class UrlElicitationRequiredError(McpError): + """ + Specialized error for when a tool requires URL mode elicitation(s) before proceeding. + + Servers can raise this error from tool handlers to indicate that the client + must complete one or more URL elicitations before the request can be processed. + + Example: + raise UrlElicitationRequiredError([ + ElicitRequestURLParams( + mode="url", + message="Authorization required for your files", + url="https://example.com/oauth/authorize", + elicitationId="auth-001" + ) + ]) + """ + + def __init__( + self, + elicitations: list[ElicitRequestURLParams], + message: str | None = None, + ): + """Initialize UrlElicitationRequiredError.""" + if message is None: + message = f"URL elicitation{'s' if len(elicitations) > 1 else ''} required" + + self._elicitations = elicitations + + error = ErrorData( + code=URL_ELICITATION_REQUIRED, + message=message, + data={"elicitations": [e.model_dump(by_alias=True, exclude_none=True) for e in elicitations]}, + ) + super().__init__(error) + + @property + def elicitations(self) -> list[ElicitRequestURLParams]: + """The list of URL elicitations required before the request can proceed.""" + return self._elicitations + + @classmethod + def from_error(cls, error: ErrorData) -> UrlElicitationRequiredError: + """Reconstruct from an ErrorData received over the wire.""" + if error.code != URL_ELICITATION_REQUIRED: + raise ValueError(f"Expected error code {URL_ELICITATION_REQUIRED}, got {error.code}") + + data = cast(dict[str, Any], error.data or {}) + raw_elicitations = cast(list[dict[str, Any]], data.get("elicitations", [])) + elicitations = [ElicitRequestURLParams.model_validate(e) for e in raw_elicitations] + return cls(elicitations, error.message) diff --git a/source/target-vendors/cp312/mcp/shared/experimental/__init__.py b/source/target-vendors/cp312/mcp/shared/experimental/__init__.py new file mode 100644 index 0000000..9b1b147 --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/experimental/__init__.py @@ -0,0 +1,7 @@ +""" +Pure experimental MCP features (no server dependencies). + +WARNING: These APIs are experimental and may change without notice. + +For server-integrated experimental features, use mcp.server.experimental. +""" diff --git a/source/target-vendors/cp312/mcp/shared/experimental/tasks/__init__.py b/source/target-vendors/cp312/mcp/shared/experimental/tasks/__init__.py new file mode 100644 index 0000000..37d81af --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/experimental/tasks/__init__.py @@ -0,0 +1,12 @@ +""" +Pure task state management for MCP. + +WARNING: These APIs are experimental and may change without notice. + +Import directly from submodules: +- mcp.shared.experimental.tasks.store.TaskStore +- mcp.shared.experimental.tasks.context.TaskContext +- mcp.shared.experimental.tasks.in_memory_task_store.InMemoryTaskStore +- mcp.shared.experimental.tasks.message_queue.TaskMessageQueue +- mcp.shared.experimental.tasks.helpers.is_terminal +""" diff --git a/source/target-vendors/cp312/mcp/shared/experimental/tasks/capabilities.py b/source/target-vendors/cp312/mcp/shared/experimental/tasks/capabilities.py new file mode 100644 index 0000000..307fcdd --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/experimental/tasks/capabilities.py @@ -0,0 +1,115 @@ +""" +Tasks capability checking utilities. + +This module provides functions for checking and requiring task-related +capabilities. All tasks capability logic is centralized here to keep +the main session code clean. + +WARNING: These APIs are experimental and may change without notice. +""" + +from mcp.shared.exceptions import McpError +from mcp.types import ( + INVALID_REQUEST, + ClientCapabilities, + ClientTasksCapability, + ErrorData, +) + + +def check_tasks_capability( + required: ClientTasksCapability, + client: ClientTasksCapability, +) -> bool: + """ + Check if client's tasks capability matches the required capability. + + Args: + required: The capability being checked for + client: The client's declared capabilities + + Returns: + True if client has the required capability, False otherwise + """ + if required.requests is None: + return True + if client.requests is None: + return False + + # Check elicitation.create + if required.requests.elicitation is not None: + if client.requests.elicitation is None: + return False + if required.requests.elicitation.create is not None: + if client.requests.elicitation.create is None: + return False + + # Check sampling.createMessage + if required.requests.sampling is not None: + if client.requests.sampling is None: + return False + if required.requests.sampling.createMessage is not None: + if client.requests.sampling.createMessage is None: + return False + + return True + + +def has_task_augmented_elicitation(caps: ClientCapabilities) -> bool: + """Check if capabilities include task-augmented elicitation support.""" + if caps.tasks is None: + return False + if caps.tasks.requests is None: + return False + if caps.tasks.requests.elicitation is None: + return False + return caps.tasks.requests.elicitation.create is not None + + +def has_task_augmented_sampling(caps: ClientCapabilities) -> bool: + """Check if capabilities include task-augmented sampling support.""" + if caps.tasks is None: + return False + if caps.tasks.requests is None: + return False + if caps.tasks.requests.sampling is None: + return False + return caps.tasks.requests.sampling.createMessage is not None + + +def require_task_augmented_elicitation(client_caps: ClientCapabilities | None) -> None: + """ + Raise McpError if client doesn't support task-augmented elicitation. + + Args: + client_caps: The client's declared capabilities, or None if not initialized + + Raises: + McpError: If client doesn't support task-augmented elicitation + """ + if client_caps is None or not has_task_augmented_elicitation(client_caps): + raise McpError( + ErrorData( + code=INVALID_REQUEST, + message="Client does not support task-augmented elicitation", + ) + ) + + +def require_task_augmented_sampling(client_caps: ClientCapabilities | None) -> None: + """ + Raise McpError if client doesn't support task-augmented sampling. + + Args: + client_caps: The client's declared capabilities, or None if not initialized + + Raises: + McpError: If client doesn't support task-augmented sampling + """ + if client_caps is None or not has_task_augmented_sampling(client_caps): + raise McpError( + ErrorData( + code=INVALID_REQUEST, + message="Client does not support task-augmented sampling", + ) + ) diff --git a/source/target-vendors/cp312/mcp/shared/experimental/tasks/context.py b/source/target-vendors/cp312/mcp/shared/experimental/tasks/context.py new file mode 100644 index 0000000..12d1595 --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/experimental/tasks/context.py @@ -0,0 +1,101 @@ +""" +TaskContext - Pure task state management. + +This module provides TaskContext, which manages task state without any +server/session dependencies. It can be used standalone for distributed +workers or wrapped by ServerTaskContext for full server integration. +""" + +from mcp.shared.experimental.tasks.store import TaskStore +from mcp.types import TASK_STATUS_COMPLETED, TASK_STATUS_FAILED, Result, Task + + +class TaskContext: + """ + Pure task state management - no session dependencies. + + This class handles: + - Task state (status, result) + - Cancellation tracking + - Store interactions + + For server-integrated features (elicit, create_message, notifications), + use ServerTaskContext from mcp.server.experimental. + + Example (distributed worker): + async def worker_job(task_id: str): + store = RedisTaskStore(redis_url) + task = await store.get_task(task_id) + ctx = TaskContext(task=task, store=store) + + await ctx.update_status("Working...") + result = await do_work() + await ctx.complete(result) + """ + + def __init__(self, task: Task, store: TaskStore): + self._task = task + self._store = store + self._cancelled = False + + @property + def task_id(self) -> str: + """The task identifier.""" + return self._task.taskId + + @property + def task(self) -> Task: + """The current task state.""" + return self._task + + @property + def is_cancelled(self) -> bool: + """Whether cancellation has been requested.""" + return self._cancelled + + def request_cancellation(self) -> None: + """ + Request cancellation of this task. + + This sets is_cancelled=True. Task work should check this + periodically and exit gracefully if set. + """ + self._cancelled = True + + async def update_status(self, message: str) -> None: + """ + Update the task's status message. + + Args: + message: The new status message + """ + self._task = await self._store.update_task( + self.task_id, + status_message=message, + ) + + async def complete(self, result: Result) -> None: + """ + Mark the task as completed with the given result. + + Args: + result: The task result + """ + await self._store.store_result(self.task_id, result) + self._task = await self._store.update_task( + self.task_id, + status=TASK_STATUS_COMPLETED, + ) + + async def fail(self, error: str) -> None: + """ + Mark the task as failed with an error message. + + Args: + error: The error message + """ + self._task = await self._store.update_task( + self.task_id, + status=TASK_STATUS_FAILED, + status_message=error, + ) diff --git a/source/target-vendors/cp312/mcp/shared/experimental/tasks/helpers.py b/source/target-vendors/cp312/mcp/shared/experimental/tasks/helpers.py new file mode 100644 index 0000000..5c87f9e --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/experimental/tasks/helpers.py @@ -0,0 +1,181 @@ +""" +Helper functions for pure task management. + +These helpers work with pure TaskContext and don't require server dependencies. +For server-integrated task helpers, use mcp.server.experimental. +""" + +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from datetime import datetime, timezone +from uuid import uuid4 + +from mcp.shared.exceptions import McpError +from mcp.shared.experimental.tasks.context import TaskContext +from mcp.shared.experimental.tasks.store import TaskStore +from mcp.types import ( + INVALID_PARAMS, + TASK_STATUS_CANCELLED, + TASK_STATUS_COMPLETED, + TASK_STATUS_FAILED, + TASK_STATUS_WORKING, + CancelTaskResult, + ErrorData, + Task, + TaskMetadata, + TaskStatus, +) + +# Metadata key for model-immediate-response (per MCP spec) +# Servers MAY include this in CreateTaskResult._meta to provide an immediate +# response string while the task executes in the background. +MODEL_IMMEDIATE_RESPONSE_KEY = "io.modelcontextprotocol/model-immediate-response" + +# Metadata key for associating requests with a task (per MCP spec) +RELATED_TASK_METADATA_KEY = "io.modelcontextprotocol/related-task" + + +def is_terminal(status: TaskStatus) -> bool: + """ + Check if a task status represents a terminal state. + + Terminal states are those where the task has finished and will not change. + + Args: + status: The task status to check + + Returns: + True if the status is terminal (completed, failed, or cancelled) + """ + return status in (TASK_STATUS_COMPLETED, TASK_STATUS_FAILED, TASK_STATUS_CANCELLED) + + +async def cancel_task( + store: TaskStore, + task_id: str, +) -> CancelTaskResult: + """ + Cancel a task with spec-compliant validation. + + Per spec: "Receivers MUST reject cancellation of terminal status tasks + with -32602 (Invalid params)" + + This helper validates that the task exists and is not in a terminal state + before setting it to "cancelled". + + Args: + store: The task store + task_id: The task identifier to cancel + + Returns: + CancelTaskResult with the cancelled task state + + Raises: + McpError: With INVALID_PARAMS (-32602) if: + - Task does not exist + - Task is already in a terminal state (completed, failed, cancelled) + + Example: + @server.experimental.cancel_task() + async def handle_cancel(request: CancelTaskRequest) -> CancelTaskResult: + return await cancel_task(store, request.params.taskId) + """ + task = await store.get_task(task_id) + if task is None: + raise McpError( + ErrorData( + code=INVALID_PARAMS, + message=f"Task not found: {task_id}", + ) + ) + + if is_terminal(task.status): + raise McpError( + ErrorData( + code=INVALID_PARAMS, + message=f"Cannot cancel task in terminal state '{task.status}'", + ) + ) + + # Update task to cancelled status + cancelled_task = await store.update_task(task_id, status=TASK_STATUS_CANCELLED) + return CancelTaskResult(**cancelled_task.model_dump()) + + +def generate_task_id() -> str: + """Generate a unique task ID.""" + return str(uuid4()) + + +def create_task_state( + metadata: TaskMetadata, + task_id: str | None = None, +) -> Task: + """ + Create a Task object with initial state. + + This is a helper for TaskStore implementations. + + Args: + metadata: Task metadata + task_id: Optional task ID (generated if not provided) + + Returns: + A new Task in "working" status + """ + now = datetime.now(timezone.utc) + return Task( + taskId=task_id or generate_task_id(), + status=TASK_STATUS_WORKING, + createdAt=now, + lastUpdatedAt=now, + ttl=metadata.ttl, + pollInterval=500, # Default 500ms poll interval + ) + + +@asynccontextmanager +async def task_execution( + task_id: str, + store: TaskStore, +) -> AsyncIterator[TaskContext]: + """ + Context manager for safe task execution (pure, no server dependencies). + + Loads a task from the store and provides a TaskContext for the work. + If an unhandled exception occurs, the task is automatically marked as failed + and the exception is suppressed (since the failure is captured in task state). + + This is useful for distributed workers that don't have a server session. + + Args: + task_id: The task identifier to execute + store: The task store (must be accessible by the worker) + + Yields: + TaskContext for updating status and completing/failing the task + + Raises: + ValueError: If the task is not found in the store + + Example (distributed worker): + async def worker_process(task_id: str): + store = RedisTaskStore(redis_url) + async with task_execution(task_id, store) as ctx: + await ctx.update_status("Working...") + result = await do_work() + await ctx.complete(result) + """ + task = await store.get_task(task_id) + if task is None: + raise ValueError(f"Task {task_id} not found") + + ctx = TaskContext(task, store) + try: + yield ctx + except Exception as e: + # Auto-fail the task if an exception occurs and task isn't already terminal + # Exception is suppressed since failure is captured in task state + if not is_terminal(ctx.task.status): + await ctx.fail(str(e)) + # Don't re-raise - the failure is recorded in task state diff --git a/source/target-vendors/cp312/mcp/shared/experimental/tasks/in_memory_task_store.py b/source/target-vendors/cp312/mcp/shared/experimental/tasks/in_memory_task_store.py new file mode 100644 index 0000000..7b630ce --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/experimental/tasks/in_memory_task_store.py @@ -0,0 +1,219 @@ +""" +In-memory implementation of TaskStore for demonstration purposes. + +This implementation stores all tasks in memory and provides automatic cleanup +based on the TTL duration specified in the task metadata using lazy expiration. + +Note: This is not suitable for production use as all data is lost on restart. +For production, consider implementing TaskStore with a database or distributed cache. +""" + +from dataclasses import dataclass, field +from datetime import datetime, timedelta, timezone + +import anyio + +from mcp.shared.experimental.tasks.helpers import create_task_state, is_terminal +from mcp.shared.experimental.tasks.store import TaskStore +from mcp.types import Result, Task, TaskMetadata, TaskStatus + + +@dataclass +class StoredTask: + """Internal storage representation of a task.""" + + task: Task + result: Result | None = None + # Time when this task should be removed (None = never) + expires_at: datetime | None = field(default=None) + + +class InMemoryTaskStore(TaskStore): + """ + A simple in-memory implementation of TaskStore. + + Features: + - Automatic TTL-based cleanup (lazy expiration) + - Thread-safe for single-process async use + - Pagination support for list_tasks + + Limitations: + - All data lost on restart + - Not suitable for distributed systems + - No persistence + + For production, implement TaskStore with Redis, PostgreSQL, etc. + """ + + def __init__(self, page_size: int = 10) -> None: + self._tasks: dict[str, StoredTask] = {} + self._page_size = page_size + self._update_events: dict[str, anyio.Event] = {} + + def _calculate_expiry(self, ttl_ms: int | None) -> datetime | None: + """Calculate expiry time from TTL in milliseconds.""" + if ttl_ms is None: + return None + return datetime.now(timezone.utc) + timedelta(milliseconds=ttl_ms) + + def _is_expired(self, stored: StoredTask) -> bool: + """Check if a task has expired.""" + if stored.expires_at is None: + return False + return datetime.now(timezone.utc) >= stored.expires_at + + def _cleanup_expired(self) -> None: + """Remove all expired tasks. Called lazily during access operations.""" + expired_ids = [task_id for task_id, stored in self._tasks.items() if self._is_expired(stored)] + for task_id in expired_ids: + del self._tasks[task_id] + + async def create_task( + self, + metadata: TaskMetadata, + task_id: str | None = None, + ) -> Task: + """Create a new task with the given metadata.""" + # Cleanup expired tasks on access + self._cleanup_expired() + + task = create_task_state(metadata, task_id) + + if task.taskId in self._tasks: + raise ValueError(f"Task with ID {task.taskId} already exists") + + stored = StoredTask( + task=task, + expires_at=self._calculate_expiry(metadata.ttl), + ) + self._tasks[task.taskId] = stored + + # Return a copy to prevent external modification + return Task(**task.model_dump()) + + async def get_task(self, task_id: str) -> Task | None: + """Get a task by ID.""" + # Cleanup expired tasks on access + self._cleanup_expired() + + stored = self._tasks.get(task_id) + if stored is None: + return None + + # Return a copy to prevent external modification + return Task(**stored.task.model_dump()) + + async def update_task( + self, + task_id: str, + status: TaskStatus | None = None, + status_message: str | None = None, + ) -> Task: + """Update a task's status and/or message.""" + stored = self._tasks.get(task_id) + if stored is None: + raise ValueError(f"Task with ID {task_id} not found") + + # Per spec: Terminal states MUST NOT transition to any other status + if status is not None and status != stored.task.status and is_terminal(stored.task.status): + raise ValueError(f"Cannot transition from terminal status '{stored.task.status}'") + + status_changed = False + if status is not None and stored.task.status != status: + stored.task.status = status + status_changed = True + + if status_message is not None: + stored.task.statusMessage = status_message + + # Update lastUpdatedAt on any change + stored.task.lastUpdatedAt = datetime.now(timezone.utc) + + # If task is now terminal and has TTL, reset expiry timer + if status is not None and is_terminal(status) and stored.task.ttl is not None: + stored.expires_at = self._calculate_expiry(stored.task.ttl) + + # Notify waiters if status changed + if status_changed: + await self.notify_update(task_id) + + return Task(**stored.task.model_dump()) + + async def store_result(self, task_id: str, result: Result) -> None: + """Store the result for a task.""" + stored = self._tasks.get(task_id) + if stored is None: + raise ValueError(f"Task with ID {task_id} not found") + + stored.result = result + + async def get_result(self, task_id: str) -> Result | None: + """Get the stored result for a task.""" + stored = self._tasks.get(task_id) + if stored is None: + return None + + return stored.result + + async def list_tasks( + self, + cursor: str | None = None, + ) -> tuple[list[Task], str | None]: + """List tasks with pagination.""" + # Cleanup expired tasks on access + self._cleanup_expired() + + all_task_ids = list(self._tasks.keys()) + + start_index = 0 + if cursor is not None: + try: + cursor_index = all_task_ids.index(cursor) + start_index = cursor_index + 1 + except ValueError: + raise ValueError(f"Invalid cursor: {cursor}") + + page_task_ids = all_task_ids[start_index : start_index + self._page_size] + tasks = [Task(**self._tasks[tid].task.model_dump()) for tid in page_task_ids] + + # Determine next cursor + next_cursor = None + if start_index + self._page_size < len(all_task_ids) and page_task_ids: + next_cursor = page_task_ids[-1] + + return tasks, next_cursor + + async def delete_task(self, task_id: str) -> bool: + """Delete a task.""" + if task_id not in self._tasks: + return False + + del self._tasks[task_id] + return True + + async def wait_for_update(self, task_id: str) -> None: + """Wait until the task status changes.""" + if task_id not in self._tasks: + raise ValueError(f"Task with ID {task_id} not found") + + # Create a fresh event for waiting (anyio.Event can't be cleared) + self._update_events[task_id] = anyio.Event() + event = self._update_events[task_id] + await event.wait() + + async def notify_update(self, task_id: str) -> None: + """Signal that a task has been updated.""" + if task_id in self._update_events: + self._update_events[task_id].set() + + # --- Testing/debugging helpers --- + + def cleanup(self) -> None: + """Cleanup all tasks (useful for testing or graceful shutdown).""" + self._tasks.clear() + self._update_events.clear() + + def get_all_tasks(self) -> list[Task]: + """Get all tasks (useful for debugging). Returns copies to prevent modification.""" + self._cleanup_expired() + return [Task(**stored.task.model_dump()) for stored in self._tasks.values()] diff --git a/source/target-vendors/cp312/mcp/shared/experimental/tasks/message_queue.py b/source/target-vendors/cp312/mcp/shared/experimental/tasks/message_queue.py new file mode 100644 index 0000000..69b6609 --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/experimental/tasks/message_queue.py @@ -0,0 +1,241 @@ +""" +TaskMessageQueue - FIFO queue for task-related messages. + +This implements the core message queue pattern from the MCP Tasks spec. +When a handler needs to send a request (like elicitation) during a task-augmented +request, the message is enqueued instead of sent directly. Messages are delivered +to the client only through the `tasks/result` endpoint. + +This pattern enables: +1. Decoupling request handling from message delivery +2. Proper bidirectional communication via the tasks/result stream +3. Automatic status management (working <-> input_required) +""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from datetime import datetime, timezone +from typing import Any, Literal + +import anyio + +from mcp.shared.experimental.tasks.resolver import Resolver +from mcp.types import JSONRPCNotification, JSONRPCRequest, RequestId + + +@dataclass +class QueuedMessage: + """ + A message queued for delivery via tasks/result. + + Messages are stored with their type and a resolver for requests + that expect responses. + """ + + type: Literal["request", "notification"] + """Whether this is a request (expects response) or notification (one-way).""" + + message: JSONRPCRequest | JSONRPCNotification + """The JSON-RPC message to send.""" + + timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + """When the message was enqueued.""" + + resolver: Resolver[dict[str, Any]] | None = None + """Resolver to set when response arrives (only for requests).""" + + original_request_id: RequestId | None = None + """The original request ID used internally, for routing responses back.""" + + +class TaskMessageQueue(ABC): + """ + Abstract interface for task message queuing. + + This is a FIFO queue that stores messages to be delivered via `tasks/result`. + When a task-augmented handler calls elicit() or sends a notification, the + message is enqueued here instead of being sent directly to the client. + + The `tasks/result` handler then dequeues and sends these messages through + the transport, with `relatedRequestId` set to the tasks/result request ID + so responses are routed correctly. + + Implementations can use in-memory storage, Redis, etc. + """ + + @abstractmethod + async def enqueue(self, task_id: str, message: QueuedMessage) -> None: + """ + Add a message to the queue for a task. + + Args: + task_id: The task identifier + message: The message to enqueue + """ + + @abstractmethod + async def dequeue(self, task_id: str) -> QueuedMessage | None: + """ + Remove and return the next message from the queue. + + Args: + task_id: The task identifier + + Returns: + The next message, or None if queue is empty + """ + + @abstractmethod + async def peek(self, task_id: str) -> QueuedMessage | None: + """ + Return the next message without removing it. + + Args: + task_id: The task identifier + + Returns: + The next message, or None if queue is empty + """ + + @abstractmethod + async def is_empty(self, task_id: str) -> bool: + """ + Check if the queue is empty for a task. + + Args: + task_id: The task identifier + + Returns: + True if no messages are queued + """ + + @abstractmethod + async def clear(self, task_id: str) -> list[QueuedMessage]: + """ + Remove and return all messages from the queue. + + This is useful for cleanup when a task is cancelled or completed. + + Args: + task_id: The task identifier + + Returns: + All queued messages (may be empty) + """ + + @abstractmethod + async def wait_for_message(self, task_id: str) -> None: + """ + Wait until a message is available in the queue. + + This blocks until either: + 1. A message is enqueued for this task + 2. The wait is cancelled + + Args: + task_id: The task identifier + """ + + @abstractmethod + async def notify_message_available(self, task_id: str) -> None: + """ + Signal that a message is available for a task. + + This wakes up any coroutines waiting in wait_for_message(). + + Args: + task_id: The task identifier + """ + + +class InMemoryTaskMessageQueue(TaskMessageQueue): + """ + In-memory implementation of TaskMessageQueue. + + This is suitable for single-process servers. For distributed systems, + implement TaskMessageQueue with Redis, RabbitMQ, etc. + + Features: + - FIFO ordering per task + - Async wait for message availability + - Thread-safe for single-process async use + """ + + def __init__(self) -> None: + self._queues: dict[str, list[QueuedMessage]] = {} + self._events: dict[str, anyio.Event] = {} + + def _get_queue(self, task_id: str) -> list[QueuedMessage]: + """Get or create the queue for a task.""" + if task_id not in self._queues: + self._queues[task_id] = [] + return self._queues[task_id] + + async def enqueue(self, task_id: str, message: QueuedMessage) -> None: + """Add a message to the queue.""" + queue = self._get_queue(task_id) + queue.append(message) + # Signal that a message is available + await self.notify_message_available(task_id) + + async def dequeue(self, task_id: str) -> QueuedMessage | None: + """Remove and return the next message.""" + queue = self._get_queue(task_id) + if not queue: + return None + return queue.pop(0) + + async def peek(self, task_id: str) -> QueuedMessage | None: + """Return the next message without removing it.""" + queue = self._get_queue(task_id) + if not queue: + return None + return queue[0] + + async def is_empty(self, task_id: str) -> bool: + """Check if the queue is empty.""" + queue = self._get_queue(task_id) + return len(queue) == 0 + + async def clear(self, task_id: str) -> list[QueuedMessage]: + """Remove and return all messages.""" + queue = self._get_queue(task_id) + messages = list(queue) + queue.clear() + return messages + + async def wait_for_message(self, task_id: str) -> None: + """Wait until a message is available.""" + # Check if there are already messages + if not await self.is_empty(task_id): + return + + # Create a fresh event for waiting (anyio.Event can't be cleared) + self._events[task_id] = anyio.Event() + event = self._events[task_id] + + # Double-check after creating event (avoid race condition) + if not await self.is_empty(task_id): + return + + # Wait for a new message + await event.wait() + + async def notify_message_available(self, task_id: str) -> None: + """Signal that a message is available.""" + if task_id in self._events: + self._events[task_id].set() + + def cleanup(self, task_id: str | None = None) -> None: + """ + Clean up queues and events. + + Args: + task_id: If provided, clean up only this task. Otherwise clean up all. + """ + if task_id is not None: + self._queues.pop(task_id, None) + self._events.pop(task_id, None) + else: + self._queues.clear() + self._events.clear() diff --git a/source/target-vendors/cp312/mcp/shared/experimental/tasks/polling.py b/source/target-vendors/cp312/mcp/shared/experimental/tasks/polling.py new file mode 100644 index 0000000..39db2e6 --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/experimental/tasks/polling.py @@ -0,0 +1,45 @@ +""" +Shared polling utilities for task operations. + +This module provides generic polling logic that works for both client→server +and server→client task polling. + +WARNING: These APIs are experimental and may change without notice. +""" + +from collections.abc import AsyncIterator, Awaitable, Callable + +import anyio + +from mcp.shared.experimental.tasks.helpers import is_terminal +from mcp.types import GetTaskResult + + +async def poll_until_terminal( + get_task: Callable[[str], Awaitable[GetTaskResult]], + task_id: str, + default_interval_ms: int = 500, +) -> AsyncIterator[GetTaskResult]: + """ + Poll a task until it reaches terminal status. + + This is a generic utility that works for both client→server and server→client + polling. The caller provides the get_task function appropriate for their direction. + + Args: + get_task: Async function that takes task_id and returns GetTaskResult + task_id: The task to poll + default_interval_ms: Fallback poll interval if server doesn't specify + + Yields: + GetTaskResult for each poll + """ + while True: + status = await get_task(task_id) + yield status + + if is_terminal(status.status): + break + + interval_ms = status.pollInterval if status.pollInterval is not None else default_interval_ms + await anyio.sleep(interval_ms / 1000) diff --git a/source/target-vendors/cp312/mcp/shared/experimental/tasks/resolver.py b/source/target-vendors/cp312/mcp/shared/experimental/tasks/resolver.py new file mode 100644 index 0000000..f27425b --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/experimental/tasks/resolver.py @@ -0,0 +1,60 @@ +""" +Resolver - An anyio-compatible future-like object for async result passing. + +This provides a simple way to pass a result (or exception) from one coroutine +to another without depending on asyncio.Future. +""" + +from typing import Generic, TypeVar, cast + +import anyio + +T = TypeVar("T") + + +class Resolver(Generic[T]): + """ + A simple resolver for passing results between coroutines. + + Unlike asyncio.Future, this works with any anyio-compatible async backend. + + Usage: + resolver: Resolver[str] = Resolver() + + # In one coroutine: + resolver.set_result("hello") + + # In another coroutine: + result = await resolver.wait() # returns "hello" + """ + + def __init__(self) -> None: + self._event = anyio.Event() + self._value: T | None = None + self._exception: BaseException | None = None + + def set_result(self, value: T) -> None: + """Set the result value and wake up waiters.""" + if self._event.is_set(): + raise RuntimeError("Resolver already completed") + self._value = value + self._event.set() + + def set_exception(self, exc: BaseException) -> None: + """Set an exception and wake up waiters.""" + if self._event.is_set(): + raise RuntimeError("Resolver already completed") + self._exception = exc + self._event.set() + + async def wait(self) -> T: + """Wait for the result and return it, or raise the exception.""" + await self._event.wait() + if self._exception is not None: + raise self._exception + # If we reach here, set_result() was called, so _value is set + return cast(T, self._value) + + def done(self) -> bool: + """Return True if the resolver has been completed.""" + return self._event.is_set() diff --git a/source/target-vendors/cp312/mcp/shared/experimental/tasks/store.py b/source/target-vendors/cp312/mcp/shared/experimental/tasks/store.py new file mode 100644 index 0000000..71fb451 --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/experimental/tasks/store.py @@ -0,0 +1,156 @@ +""" +TaskStore - Abstract interface for task state storage. +""" + +from abc import ABC, abstractmethod + +from mcp.types import Result, Task, TaskMetadata, TaskStatus + + +class TaskStore(ABC): + """ + Abstract interface for task state storage. + + This is a pure storage interface - it doesn't manage execution. + Implementations can use in-memory storage, databases, Redis, etc. + + All methods are async to support various backends. + """ + + @abstractmethod + async def create_task( + self, + metadata: TaskMetadata, + task_id: str | None = None, + ) -> Task: + """ + Create a new task. + + Args: + metadata: Task metadata (ttl, etc.) + task_id: Optional task ID. If None, implementation should generate one. + + Returns: + The created Task with status="working" + + Raises: + ValueError: If task_id already exists + """ + + @abstractmethod + async def get_task(self, task_id: str) -> Task | None: + """ + Get a task by ID. + + Args: + task_id: The task identifier + + Returns: + The Task, or None if not found + """ + + @abstractmethod + async def update_task( + self, + task_id: str, + status: TaskStatus | None = None, + status_message: str | None = None, + ) -> Task: + """ + Update a task's status and/or message. + + Args: + task_id: The task identifier + status: New status (if changing) + status_message: New status message (if changing) + + Returns: + The updated Task + + Raises: + ValueError: If task not found + ValueError: If attempting to transition from a terminal status + (completed, failed, cancelled). Per spec, terminal states + MUST NOT transition to any other status. + """ + + @abstractmethod + async def store_result(self, task_id: str, result: Result) -> None: + """ + Store the result for a task. + + Args: + task_id: The task identifier + result: The result to store + + Raises: + ValueError: If task not found + """ + + @abstractmethod + async def get_result(self, task_id: str) -> Result | None: + """ + Get the stored result for a task. + + Args: + task_id: The task identifier + + Returns: + The stored Result, or None if not available + """ + + @abstractmethod + async def list_tasks( + self, + cursor: str | None = None, + ) -> tuple[list[Task], str | None]: + """ + List tasks with pagination. + + Args: + cursor: Optional cursor for pagination + + Returns: + Tuple of (tasks, next_cursor). next_cursor is None if no more pages. + """ + + @abstractmethod + async def delete_task(self, task_id: str) -> bool: + """ + Delete a task. + + Args: + task_id: The task identifier + + Returns: + True if deleted, False if not found + """ + + @abstractmethod + async def wait_for_update(self, task_id: str) -> None: + """ + Wait until the task status changes. + + This blocks until either: + 1. The task status changes + 2. The wait is cancelled + + Used by tasks/result to wait for task completion or status changes. + + Args: + task_id: The task identifier + + Raises: + ValueError: If task not found + """ + + @abstractmethod + async def notify_update(self, task_id: str) -> None: + """ + Signal that a task has been updated. + + This wakes up any coroutines waiting in wait_for_update(). + + Args: + task_id: The task identifier + """ diff --git a/source/target-vendors/cp312/mcp/shared/memory.py b/source/target-vendors/cp312/mcp/shared/memory.py new file mode 100644 index 0000000..06d404e --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/memory.py @@ -0,0 +1,98 @@ +""" +In-memory transports +""" + +from __future__ import annotations + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager +from datetime import timedelta +from typing import Any + +import anyio +from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream + +import mcp.types as types +from mcp.client.session import ClientSession, ElicitationFnT, ListRootsFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT +from mcp.server import Server +from mcp.server.fastmcp import FastMCP +from mcp.shared.message import SessionMessage + +MessageStream = tuple[MemoryObjectReceiveStream[SessionMessage | Exception], MemoryObjectSendStream[SessionMessage]] + + +@asynccontextmanager +async def create_client_server_memory_streams() -> AsyncGenerator[tuple[MessageStream, MessageStream], None]: + """ + Creates a pair of bidirectional memory streams for client-server communication. + + Returns: + A tuple of (client_streams, server_streams) where each is a tuple of + (read_stream, write_stream) + """ + # Create streams for both directions + server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage | Exception](1) + client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage | Exception](1) + + client_streams = (server_to_client_receive, client_to_server_send) + server_streams = (client_to_server_receive, server_to_client_send) + + async with ( + server_to_client_receive, + client_to_server_send, + client_to_server_receive, + server_to_client_send, + ): + yield client_streams, server_streams + + +@asynccontextmanager +async def create_connected_server_and_client_session( + server: Server[Any] | FastMCP, + read_timeout_seconds: timedelta | None = None, + sampling_callback: SamplingFnT | None = None, + list_roots_callback: ListRootsFnT | None = None, + logging_callback: LoggingFnT | None = None, + message_handler: MessageHandlerFnT | None = None, + client_info: types.Implementation | None = None, + raise_exceptions: bool = False, + elicitation_callback: ElicitationFnT | None = None, +) -> AsyncGenerator[ClientSession, None]: + """Creates a ClientSession that is connected to a running MCP server.""" + + # TODO(Marcelo): we should have a proper `Client` that can use this "in-memory transport", + # and we should expose a method in the `FastMCP` so we don't access a private attribute. + if isinstance(server, FastMCP): # pragma: no cover + server = server._mcp_server # type: ignore[reportPrivateUsage] + + async with create_client_server_memory_streams() as (client_streams, server_streams): + client_read, client_write = client_streams + server_read, server_write = server_streams + + # Create a cancel scope for the server task + async with anyio.create_task_group() as tg: + tg.start_soon( + lambda: server.run( + server_read, + server_write, + server.create_initialization_options(), + raise_exceptions=raise_exceptions, + ) + ) + + try: + async with ClientSession( + read_stream=client_read, + write_stream=client_write, + read_timeout_seconds=read_timeout_seconds, + sampling_callback=sampling_callback, + list_roots_callback=list_roots_callback, + logging_callback=logging_callback, + message_handler=message_handler, + client_info=client_info, + elicitation_callback=elicitation_callback, + ) as client_session: + await client_session.initialize() + yield client_session + finally: # pragma: no cover + tg.cancel_scope.cancel() diff --git a/source/target-vendors/cp312/mcp/shared/message.py b/source/target-vendors/cp312/mcp/shared/message.py new file mode 100644 index 0000000..81503ea --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/message.py @@ -0,0 +1,50 @@ +""" +Message wrapper with metadata support. + +This module defines a wrapper type that combines JSONRPCMessage with metadata +to support transport-specific features like resumability. +""" + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from mcp.types import JSONRPCMessage, RequestId + +ResumptionToken = str + +ResumptionTokenUpdateCallback = Callable[[ResumptionToken], Awaitable[None]] + +# Callback type for closing SSE streams without terminating +CloseSSEStreamCallback = Callable[[], Awaitable[None]] + + +@dataclass +class ClientMessageMetadata: + """Metadata specific to client messages.""" + + resumption_token: ResumptionToken | None = None + on_resumption_token_update: Callable[[ResumptionToken], Awaitable[None]] | None = None + + +@dataclass +class ServerMessageMetadata: + """Metadata specific to server messages.""" + + related_request_id: RequestId | None = None + # Request-specific context (e.g., headers, auth info) + request_context: object | None = None + # Callback to close SSE stream for the current request without terminating + close_sse_stream: CloseSSEStreamCallback | None = None + # Callback to close the standalone GET SSE stream (for unsolicited notifications) + close_standalone_sse_stream: CloseSSEStreamCallback | None = None + + +MessageMetadata = ClientMessageMetadata | ServerMessageMetadata | None + + +@dataclass +class SessionMessage: + """A message with specific metadata for transport-specific features.""" + + message: JSONRPCMessage + metadata: MessageMetadata = None diff --git a/source/target-vendors/cp312/mcp/shared/metadata_utils.py b/source/target-vendors/cp312/mcp/shared/metadata_utils.py new file mode 100644 index 0000000..e3f49da --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/metadata_utils.py @@ -0,0 +1,45 @@ +"""Utility functions for working with metadata in MCP types. + +These utilities are primarily intended for client-side usage to properly display +human-readable names in user interfaces in a spec compliant way. +""" + +from mcp.types import Implementation, Prompt, Resource, ResourceTemplate, Tool + + +def get_display_name(obj: Tool | Resource | Prompt | ResourceTemplate | Implementation) -> str: + """ + Get the display name for an MCP object with proper precedence. + + This is a client-side utility function designed to help MCP clients display + human-readable names in their user interfaces. When servers provide a 'title' + field, it should be preferred over the programmatic 'name' field for display. + + For tools: title > annotations.title > name + For other objects: title > name + + Example: + # In a client displaying available tools + tools = await session.list_tools() + for tool in tools.tools: + display_name = get_display_name(tool) + print(f"Available tool: {display_name}") + + Args: + obj: An MCP object with name and optional title fields + + Returns: + The display name to use for UI presentation + """ + if isinstance(obj, Tool): + # Tools have special precedence: title > annotations.title > name + if hasattr(obj, "title") and obj.title is not None: + return obj.title + if obj.annotations and hasattr(obj.annotations, "title") and obj.annotations.title is not None: + return obj.annotations.title + return obj.name + else: + # All other objects: title > name + if hasattr(obj, "title") and obj.title is not None: + return obj.title + return obj.name diff --git a/source/target-vendors/cp312/mcp/shared/progress.py b/source/target-vendors/cp312/mcp/shared/progress.py new file mode 100644 index 0000000..a230c58 --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/progress.py @@ -0,0 +1,58 @@ +from collections.abc import Generator +from contextlib import contextmanager +from dataclasses import dataclass, field +from typing import Generic + +from pydantic import BaseModel + +from mcp.shared.context import LifespanContextT, RequestContext +from mcp.shared.session import ( + BaseSession, + ReceiveNotificationT, + ReceiveRequestT, + SendNotificationT, + SendRequestT, + SendResultT, +) +from mcp.types import ProgressToken + + +class Progress(BaseModel): + progress: float + total: float | None + + +@dataclass +class ProgressContext(Generic[SendRequestT, SendNotificationT, SendResultT, ReceiveRequestT, ReceiveNotificationT]): + session: BaseSession[SendRequestT, SendNotificationT, SendResultT, ReceiveRequestT, ReceiveNotificationT] + progress_token: ProgressToken + total: float | None + current: float = field(default=0.0, init=False) + + async def progress(self, amount: float, message: str | None = None) -> None: + self.current += amount + + await self.session.send_progress_notification( + self.progress_token, self.current, total=self.total, message=message + ) + + +@contextmanager +def progress( + ctx: RequestContext[ + BaseSession[SendRequestT, SendNotificationT, SendResultT, ReceiveRequestT, ReceiveNotificationT], + LifespanContextT, + ], + total: float | None = None, +) -> Generator[ + ProgressContext[SendRequestT, SendNotificationT, SendResultT, ReceiveRequestT, ReceiveNotificationT], + None, +]: + if ctx.meta is None or ctx.meta.progressToken is None: # pragma: no cover + raise ValueError("No progress token provided") + + progress_ctx = ProgressContext(ctx.session, ctx.meta.progressToken, total) + try: + yield progress_ctx + finally: + pass diff --git a/source/target-vendors/cp312/mcp/shared/response_router.py b/source/target-vendors/cp312/mcp/shared/response_router.py new file mode 100644 index 0000000..3179615 --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/response_router.py @@ -0,0 +1,63 @@ +""" +ResponseRouter - Protocol for pluggable response routing. + +This module defines a protocol for routing JSON-RPC responses to alternative +handlers before falling back to the default response stream mechanism. + +The primary use case is task-augmented requests: when a TaskSession enqueues +a request (like elicitation), the response needs to be routed back to the +waiting resolver instead of the normal response stream. + +Design: +- Protocol-based for testability and flexibility +- Returns bool to indicate if response was handled +- Supports both success responses and errors +""" + +from typing import Any, Protocol + +from mcp.types import ErrorData, RequestId + + +class ResponseRouter(Protocol): + """ + Protocol for routing responses to alternative handlers. + + Implementations check if they have a pending request for the given ID + and deliver the response/error to the appropriate handler. + + Example: + class TaskResultHandler(ResponseRouter): + def route_response(self, request_id, response): + resolver = self._pending_requests.pop(request_id, None) + if resolver: + resolver.set_result(response) + return True + return False + """ + + def route_response(self, request_id: RequestId, response: dict[str, Any]) -> bool: + """ + Try to route a response to a pending request handler. + + Args: + request_id: The JSON-RPC request ID from the response + response: The response result data + + Returns: + True if the response was handled, False otherwise + """ + ... # pragma: no cover + + def route_error(self, request_id: RequestId, error: ErrorData) -> bool: + """ + Try to route an error to a pending request handler. + + Args: + request_id: The JSON-RPC request ID from the error response + error: The error data + + Returns: + True if the error was handled, False otherwise + """ + ... # pragma: no cover diff --git a/source/target-vendors/cp312/mcp/shared/session.py b/source/target-vendors/cp312/mcp/shared/session.py new file mode 100644 index 0000000..3033acd --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/session.py @@ -0,0 +1,550 @@ +import logging +from collections.abc import Callable +from contextlib import AsyncExitStack +from datetime import timedelta +from types import TracebackType +from typing import Any, Generic, Protocol, TypeVar + +import anyio +import httpx +from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream +from pydantic import BaseModel +from typing_extensions import Self + +from mcp.shared.exceptions import McpError +from mcp.shared.message import MessageMetadata, ServerMessageMetadata, SessionMessage +from mcp.shared.response_router import ResponseRouter +from mcp.types import ( + CONNECTION_CLOSED, + INVALID_PARAMS, + CancelledNotification, + ClientNotification, + ClientRequest, + ClientResult, + ErrorData, + JSONRPCError, + JSONRPCMessage, + JSONRPCNotification, + JSONRPCRequest, + JSONRPCResponse, + ProgressNotification, + RequestParams, + ServerNotification, + ServerRequest, + ServerResult, +) + +SendRequestT = TypeVar("SendRequestT", ClientRequest, ServerRequest) +SendResultT = TypeVar("SendResultT", ClientResult, ServerResult) +SendNotificationT = TypeVar("SendNotificationT", ClientNotification, ServerNotification) +ReceiveRequestT = TypeVar("ReceiveRequestT", ClientRequest, ServerRequest) +ReceiveResultT = TypeVar("ReceiveResultT", bound=BaseModel) +ReceiveNotificationT = TypeVar("ReceiveNotificationT", ClientNotification, ServerNotification) + +RequestId = str | int + + +class ProgressFnT(Protocol): + """Protocol for progress notification callbacks.""" + + async def __call__( + self, progress: float, total: float | None, message: str | None + ) -> None: ... # pragma: no branch + + +class RequestResponder(Generic[ReceiveRequestT, SendResultT]): + """Handles responding to MCP requests and manages request lifecycle. + + This class MUST be used as a context manager to ensure proper cleanup and + cancellation handling: + + Example: + with request_responder as resp: + await resp.respond(result) + + The context manager ensures: + 1. Proper cancellation scope setup and cleanup + 2. Request completion tracking + 3. Cleanup of in-flight requests + """ + + def __init__( + self, + request_id: RequestId, + request_meta: RequestParams.Meta | None, + request: ReceiveRequestT, + session: """BaseSession[ + SendRequestT, + SendNotificationT, + SendResultT, + ReceiveRequestT, + ReceiveNotificationT + ]""", + on_complete: Callable[["RequestResponder[ReceiveRequestT, SendResultT]"], Any], + message_metadata: MessageMetadata = None, + ) -> None: + self.request_id = request_id + self.request_meta = request_meta + self.request = request + self.message_metadata = message_metadata + self._session = session + self._completed = False + self._cancel_scope = anyio.CancelScope() + self._on_complete = on_complete + self._entered = False # Track if we're in a context manager + + def __enter__(self) -> "RequestResponder[ReceiveRequestT, SendResultT]": + """Enter the context manager, enabling request cancellation tracking.""" + self._entered = True + self._cancel_scope = anyio.CancelScope() + self._cancel_scope.__enter__() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + """Exit the context manager, performing cleanup and notifying completion.""" + try: + if self._completed: # pragma: no branch + self._on_complete(self) + finally: + self._entered = False + if not self._cancel_scope: # pragma: no cover + raise RuntimeError("No active cancel scope") + self._cancel_scope.__exit__(exc_type, exc_val, exc_tb) + + async def respond(self, response: SendResultT | ErrorData) -> None: + """Send a response for this request. + + Must be called within a context manager block. + Raises: + RuntimeError: If not used within a context manager + AssertionError: If request was already responded to + """ + if not self._entered: # pragma: no cover + raise RuntimeError("RequestResponder must be used as a context manager") + assert not self._completed, "Request already responded to" + + if not self.cancelled: # pragma: no branch + self._completed = True + + await self._session._send_response( # type: ignore[reportPrivateUsage] + request_id=self.request_id, response=response + ) + + async def cancel(self) -> None: + """Cancel this request and mark it as completed.""" + if not self._entered: # pragma: no cover + raise RuntimeError("RequestResponder must be used as a context manager") + if not self._cancel_scope: # pragma: no cover + raise RuntimeError("No active cancel scope") + + self._cancel_scope.cancel() + self._completed = True # Mark as completed so it's removed from in_flight + # Send an error response to indicate cancellation + await self._session._send_response( # type: ignore[reportPrivateUsage] + request_id=self.request_id, + response=ErrorData(code=0, message="Request cancelled", data=None), + ) + + @property + def in_flight(self) -> bool: # pragma: no cover + return not self._completed and not self.cancelled + + @property + def cancelled(self) -> bool: # pragma: no cover + return self._cancel_scope.cancel_called + + +class BaseSession( + Generic[ + SendRequestT, + SendNotificationT, + SendResultT, + ReceiveRequestT, + ReceiveNotificationT, + ], +): + """ + Implements an MCP "session" on top of read/write streams, including features + like request/response linking, notifications, and progress. + + This class is an async context manager that automatically starts processing + messages when entered. + """ + + _response_streams: dict[RequestId, MemoryObjectSendStream[JSONRPCResponse | JSONRPCError]] + _request_id: int + _in_flight: dict[RequestId, RequestResponder[ReceiveRequestT, SendResultT]] + _progress_callbacks: dict[RequestId, ProgressFnT] + _response_routers: list["ResponseRouter"] + + def __init__( + self, + read_stream: MemoryObjectReceiveStream[SessionMessage | Exception], + write_stream: MemoryObjectSendStream[SessionMessage], + receive_request_type: type[ReceiveRequestT], + receive_notification_type: type[ReceiveNotificationT], + # If none, reading will never time out + read_timeout_seconds: timedelta | None = None, + ) -> None: + self._read_stream = read_stream + self._write_stream = write_stream + self._response_streams = {} + self._request_id = 0 + self._receive_request_type = receive_request_type + self._receive_notification_type = receive_notification_type + self._session_read_timeout_seconds = read_timeout_seconds + self._in_flight = {} + self._progress_callbacks = {} + self._response_routers = [] + self._exit_stack = AsyncExitStack() + + def add_response_router(self, router: ResponseRouter) -> None: + """ + Register a response router to handle responses for non-standard requests. + + Response routers are checked in order before falling back to the default + response stream mechanism. This is used by TaskResultHandler to route + responses for queued task requests back to their resolvers. + + WARNING: This is an experimental API that may change without notice. + + Args: + router: A ResponseRouter implementation + """ + self._response_routers.append(router) + + async def __aenter__(self) -> Self: + self._task_group = anyio.create_task_group() + await self._task_group.__aenter__() + self._task_group.start_soon(self._receive_loop) + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + await self._exit_stack.aclose() + # Using BaseSession as a context manager should not block on exit (this + # would be very surprising behavior), so make sure to cancel the tasks + # in the task group. + self._task_group.cancel_scope.cancel() + return await self._task_group.__aexit__(exc_type, exc_val, exc_tb) + + async def send_request( + self, + request: SendRequestT, + result_type: type[ReceiveResultT], + request_read_timeout_seconds: timedelta | None = None, + metadata: MessageMetadata = None, + progress_callback: ProgressFnT | None = None, + ) -> ReceiveResultT: + """ + Sends a request and wait for a response. Raises an McpError if the + response contains an error. If a request read timeout is provided, it + will take precedence over the session read timeout. + + Do not use this method to emit notifications! Use send_notification() + instead. + """ + request_id = self._request_id + self._request_id = request_id + 1 + + response_stream, response_stream_reader = anyio.create_memory_object_stream[JSONRPCResponse | JSONRPCError](1) + self._response_streams[request_id] = response_stream + + # Set up progress token if progress callback is provided + request_data = request.model_dump(by_alias=True, mode="json", exclude_none=True) + if progress_callback is not None: # pragma: no cover + # Use request_id as progress token + if "params" not in request_data: + request_data["params"] = {} + if "_meta" not in request_data["params"]: # pragma: no branch + request_data["params"]["_meta"] = {} + request_data["params"]["_meta"]["progressToken"] = request_id + # Store the callback for this request + self._progress_callbacks[request_id] = progress_callback + + try: + jsonrpc_request = JSONRPCRequest( + jsonrpc="2.0", + id=request_id, + **request_data, + ) + + await self._write_stream.send(SessionMessage(message=JSONRPCMessage(jsonrpc_request), metadata=metadata)) + + # request read timeout takes precedence over session read timeout + timeout = None + if request_read_timeout_seconds is not None: # pragma: no cover + timeout = request_read_timeout_seconds.total_seconds() + elif self._session_read_timeout_seconds is not None: # pragma: no cover + timeout = self._session_read_timeout_seconds.total_seconds() + + try: + with anyio.fail_after(timeout): + response_or_error = await response_stream_reader.receive() + except TimeoutError: + raise McpError( + ErrorData( + code=httpx.codes.REQUEST_TIMEOUT, + message=( + f"Timed out while waiting for response to " + f"{request.__class__.__name__}. Waited " + f"{timeout} seconds." + ), + ) + ) + + if isinstance(response_or_error, JSONRPCError): + raise McpError(response_or_error.error) + else: + return result_type.model_validate(response_or_error.result) + + finally: + self._response_streams.pop(request_id, None) + self._progress_callbacks.pop(request_id, None) + await response_stream.aclose() + await response_stream_reader.aclose() + + async def send_notification( + self, + notification: SendNotificationT, + related_request_id: RequestId | None = None, + ) -> None: + """ + Emits a notification, which is a one-way message that does not expect + a response. + """ + # Some transport implementations may need to set the related_request_id + # to attribute to the notifications to the request that triggered them. + jsonrpc_notification = JSONRPCNotification( + jsonrpc="2.0", + **notification.model_dump(by_alias=True, mode="json", exclude_none=True), + ) + session_message = SessionMessage( # pragma: no cover + message=JSONRPCMessage(jsonrpc_notification), + metadata=ServerMessageMetadata(related_request_id=related_request_id) if related_request_id else None, + ) + await self._write_stream.send(session_message) + + async def _send_response(self, request_id: RequestId, response: SendResultT | ErrorData) -> None: + if isinstance(response, ErrorData): + jsonrpc_error = JSONRPCError(jsonrpc="2.0", id=request_id, error=response) + session_message = SessionMessage(message=JSONRPCMessage(jsonrpc_error)) + await self._write_stream.send(session_message) + else: + jsonrpc_response = JSONRPCResponse( + jsonrpc="2.0", + id=request_id, + result=response.model_dump(by_alias=True, mode="json", exclude_none=True), + ) + session_message = SessionMessage(message=JSONRPCMessage(jsonrpc_response)) + await self._write_stream.send(session_message) + + async def _receive_loop(self) -> None: + async with ( + self._read_stream, + self._write_stream, + ): + try: + async for message in self._read_stream: + if isinstance(message, Exception): # pragma: no cover + await self._handle_incoming(message) + elif isinstance(message.message.root, JSONRPCRequest): + try: + validated_request = self._receive_request_type.model_validate( + message.message.root.model_dump(by_alias=True, mode="json", exclude_none=True) + ) + responder = RequestResponder( + request_id=message.message.root.id, + request_meta=validated_request.root.params.meta + if validated_request.root.params + else None, + request=validated_request, + session=self, + on_complete=lambda r: self._in_flight.pop(r.request_id, None), + message_metadata=message.metadata, + ) + self._in_flight[responder.request_id] = responder + await self._received_request(responder) + + if not responder._completed: # type: ignore[reportPrivateUsage] + await self._handle_incoming(responder) + except Exception as e: + # For request validation errors, send a proper JSON-RPC error + # response instead of crashing the server + logging.warning(f"Failed to validate request: {e}") + logging.debug(f"Message that failed validation: {message.message.root}") + error_response = JSONRPCError( + jsonrpc="2.0", + id=message.message.root.id, + error=ErrorData( + code=INVALID_PARAMS, + message="Invalid request parameters", + data="", + ), + ) + session_message = SessionMessage(message=JSONRPCMessage(error_response)) + await self._write_stream.send(session_message) + + elif isinstance(message.message.root, JSONRPCNotification): + try: + notification = self._receive_notification_type.model_validate( + message.message.root.model_dump(by_alias=True, mode="json", exclude_none=True) + ) + # Handle cancellation notifications + if isinstance(notification.root, CancelledNotification): + cancelled_id = notification.root.params.requestId + if cancelled_id in self._in_flight: # pragma: no branch + await self._in_flight[cancelled_id].cancel() + else: + # Handle progress notifications callback + if isinstance(notification.root, ProgressNotification): # pragma: no cover + progress_token = notification.root.params.progressToken + # If there is a progress callback for this token, + # call it with the progress information + if progress_token in self._progress_callbacks: + callback = self._progress_callbacks[progress_token] + try: + await callback( + notification.root.params.progress, + notification.root.params.total, + notification.root.params.message, + ) + except Exception as e: + logging.error( + "Progress callback raised an exception: %s", + e, + ) + await self._received_notification(notification) + await self._handle_incoming(notification) + except Exception as e: # pragma: no cover + # For other validation errors, log and continue + logging.warning( + f"Failed to validate notification: {e}. Message was: {message.message.root}" + ) + else: # Response or error + await self._handle_response(message) + + except anyio.ClosedResourceError: + # This is expected when the client disconnects abruptly. + # Without this handler, the exception would propagate up and + # crash the server's task group. + logging.debug("Read stream closed by client") # pragma: no cover + except Exception as e: # pragma: no cover + # Other exceptions are not expected and should be logged. We purposefully + # catch all exceptions here to avoid crashing the server. + logging.exception(f"Unhandled exception in receive loop: {e}") + finally: + # after the read stream is closed, we need to send errors + # to any pending requests + for id, stream in self._response_streams.items(): + error = ErrorData(code=CONNECTION_CLOSED, message="Connection closed") + try: + await stream.send(JSONRPCError(jsonrpc="2.0", id=id, error=error)) + await stream.aclose() + except Exception: # pragma: no cover + # Stream might already be closed + pass + self._response_streams.clear() + + def _normalize_request_id(self, response_id: RequestId) -> RequestId: + """ + Normalize a response ID to match how request IDs are stored. + + Since the client always sends integer IDs, we normalize string IDs + to integers when possible. This matches the TypeScript SDK approach: + https://github.com/modelcontextprotocol/typescript-sdk/blob/a606fb17909ea454e83aab14c73f14ea45c04448/src/shared/protocol.ts#L861 + + Args: + response_id: The response ID from the incoming message. + + Returns: + The normalized ID (int if possible, otherwise original value). + """ + if isinstance(response_id, str): + try: + return int(response_id) + except ValueError: + logging.warning(f"Response ID {response_id!r} cannot be normalized to match pending requests") + return response_id + + async def _handle_response(self, message: SessionMessage) -> None: + """ + Handle an incoming response or error message. + + Checks response routers first (e.g., for task-related responses), + then falls back to the normal response stream mechanism. + """ + root = message.message.root + + # This check is always true at runtime: the caller (_receive_loop) only invokes + # this method in the else branch after checking for JSONRPCRequest and + # JSONRPCNotification. However, the type checker can't infer this from the + # method signature, so we need this guard for type narrowing. + if not isinstance(root, JSONRPCResponse | JSONRPCError): + return # pragma: no cover + + # Normalize response ID to handle type mismatches (e.g., "0" vs 0) + response_id = self._normalize_request_id(root.id) + + # First, check response routers (e.g., TaskResultHandler) + if isinstance(root, JSONRPCError): + # Route error to routers + for router in self._response_routers: + if router.route_error(response_id, root.error): + return # Handled + else: + # Route success response to routers + response_data: dict[str, Any] = root.result or {} + for router in self._response_routers: + if router.route_response(response_id, response_data): + return # Handled + + # Fall back to normal response streams + stream = self._response_streams.pop(response_id, None) + if stream: # pragma: no cover + await stream.send(root) + else: # pragma: no cover + await self._handle_incoming(RuntimeError(f"Received response with an unknown request ID: {message}")) + + async def _received_request(self, responder: RequestResponder[ReceiveRequestT, SendResultT]) -> None: + """ + Can be overridden by subclasses to handle a request without needing to + listen on the message stream. + + If the request is responded to within this method, it will not be + forwarded on to the message stream. + """ + + async def _received_notification(self, notification: ReceiveNotificationT) -> None: + """ + Can be overridden by subclasses to handle a notification without needing + to listen on the message stream. + """ + + async def send_progress_notification( + self, + progress_token: str | int, + progress: float, + total: float | None = None, + message: str | None = None, + ) -> None: + """ + Sends a progress notification for a request that is currently being + processed. + """ + + async def _handle_incoming( + self, + req: RequestResponder[ReceiveRequestT, SendResultT] | ReceiveNotificationT | Exception, + ) -> None: + """A generic handler for incoming messages. Overwritten by subclasses.""" + pass # pragma: no cover diff --git a/source/target-vendors/cp312/mcp/shared/tool_name_validation.py b/source/target-vendors/cp312/mcp/shared/tool_name_validation.py new file mode 100644 index 0000000..f35efa5 --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/tool_name_validation.py @@ -0,0 +1,129 @@ +"""Tool name validation utilities according to SEP-986. + +Tool names SHOULD be between 1 and 128 characters in length (inclusive). +Tool names are case-sensitive. +Allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), +digits (0-9), underscore (_), dash (-), and dot (.). +Tool names SHOULD NOT contain spaces, commas, or other special characters. + +See: https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names +""" + +from __future__ import annotations + +import logging +import re +from dataclasses import dataclass, field + +logger = logging.getLogger(__name__) + +# Regular expression for valid tool names according to SEP-986 specification +TOOL_NAME_REGEX = re.compile(r"^[A-Za-z0-9._-]{1,128}$") + +# SEP reference URL for warning messages +SEP_986_URL = "https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names" + + +@dataclass +class ToolNameValidationResult: + """Result of tool name validation. + + Attributes: + is_valid: Whether the tool name conforms to SEP-986 requirements. + warnings: List of warning messages for non-conforming aspects. + """ + + is_valid: bool + warnings: list[str] = field(default_factory=lambda: []) + + +def validate_tool_name(name: str) -> ToolNameValidationResult: + """Validate a tool name according to the SEP-986 specification. + + Args: + name: The tool name to validate. + + Returns: + ToolNameValidationResult containing validation status and any warnings. + """ + warnings: list[str] = [] + + # Check for empty name + if not name: + return ToolNameValidationResult( + is_valid=False, + warnings=["Tool name cannot be empty"], + ) + + # Check length + if len(name) > 128: + return ToolNameValidationResult( + is_valid=False, + warnings=[f"Tool name exceeds maximum length of 128 characters (current: {len(name)})"], + ) + + # Check for problematic patterns (warnings, not validation failures) + if " " in name: + warnings.append("Tool name contains spaces, which may cause parsing issues") + + if "," in name: + warnings.append("Tool name contains commas, which may cause parsing issues") + + # Check for potentially confusing leading/trailing characters + if name.startswith("-") or name.endswith("-"): + warnings.append("Tool name starts or ends with a dash, which may cause parsing issues in some contexts") + + if name.startswith(".") or name.endswith("."): + warnings.append("Tool name starts or ends with a dot, which may cause parsing issues in some contexts") + + # Check for invalid characters + if not TOOL_NAME_REGEX.match(name): + # Find all invalid characters (unique, preserving order) + invalid_chars: list[str] = [] + seen: set[str] = set() + for char in name: + if not re.match(r"[A-Za-z0-9._-]", char) and char not in seen: + invalid_chars.append(char) + seen.add(char) + + warnings.append(f"Tool name contains invalid characters: {', '.join(repr(c) for c in invalid_chars)}") + warnings.append("Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)") + + return ToolNameValidationResult(is_valid=False, warnings=warnings) + + return ToolNameValidationResult(is_valid=True, warnings=warnings) + + +def issue_tool_name_warning(name: str, warnings: list[str]) -> None: + """Log warnings for non-conforming tool names. + + Args: + name: The tool name that triggered the warnings. + warnings: List of warning messages to log. + """ + if not warnings: + return + + logger.warning(f'Tool name validation warning for "{name}":') + for warning in warnings: + logger.warning(f" - {warning}") + logger.warning("Tool registration will proceed, but this may cause compatibility issues.") + logger.warning("Consider updating the tool name to conform to the MCP tool naming standard.") + logger.warning(f"See SEP-986 ({SEP_986_URL}) for more details.") + + +def validate_and_warn_tool_name(name: str) -> bool: + """Validate a tool name and issue warnings for non-conforming names. + + This is the primary entry point for tool name validation. It validates + the name and logs any warnings via the logging module. + + Args: + name: The tool name to validate. + + Returns: + True if the name is valid, False otherwise. + """ + result = validate_tool_name(name) + issue_tool_name_warning(name, result.warnings) + return result.is_valid diff --git a/source/target-vendors/cp312/mcp/shared/version.py b/source/target-vendors/cp312/mcp/shared/version.py new file mode 100644 index 0000000..d2a1e46 --- /dev/null +++ b/source/target-vendors/cp312/mcp/shared/version.py @@ -0,0 +1,3 @@ +from mcp.types import LATEST_PROTOCOL_VERSION + +SUPPORTED_PROTOCOL_VERSIONS: list[str] = ["2024-11-05", "2025-03-26", "2025-06-18", LATEST_PROTOCOL_VERSION] diff --git a/source/target-vendors/cp312/mcp/types.py b/source/target-vendors/cp312/mcp/types.py new file mode 100644 index 0000000..654c006 --- /dev/null +++ b/source/target-vendors/cp312/mcp/types.py @@ -0,0 +1,1999 @@ +from collections.abc import Callable +from datetime import datetime +from typing import Annotated, Any, Final, Generic, Literal, TypeAlias, TypeVar + +from pydantic import BaseModel, ConfigDict, Field, FileUrl, RootModel +from pydantic.networks import AnyUrl, UrlConstraints +from typing_extensions import deprecated + +""" +Model Context Protocol bindings for Python + +These bindings were generated from https://github.com/modelcontextprotocol/specification, +using Claude, with a prompt something like the following: + +Generate idiomatic Python bindings for this schema for MCP, or the "Model Context +Protocol." The schema is defined in TypeScript, but there's also a JSON Schema version +for reference. + +* For the bindings, let's use Pydantic V2 models. +* Each model should allow extra fields everywhere, by specifying `model_config = + ConfigDict(extra='allow')`. Do this in every case, instead of a custom base class. +* Union types should be represented with a Pydantic `RootModel`. +* Define additional model classes instead of using dictionaries. Do this even if they're + not separate types in the schema. +""" + +LATEST_PROTOCOL_VERSION = "2025-11-25" + +""" +The default negotiated version of the Model Context Protocol when no version is specified. +We need this to satisfy the MCP specification, which requires the server to assume a +specific version if none is provided by the client. See section "Protocol Version Header" at +https://modelcontextprotocol.io/specification +""" +DEFAULT_NEGOTIATED_VERSION = "2025-03-26" + +ProgressToken = str | int +Cursor = str +Role = Literal["user", "assistant"] +RequestId = Annotated[int, Field(strict=True)] | str +AnyFunction: TypeAlias = Callable[..., Any] + +TaskExecutionMode = Literal["forbidden", "optional", "required"] +TASK_FORBIDDEN: Final[Literal["forbidden"]] = "forbidden" +TASK_OPTIONAL: Final[Literal["optional"]] = "optional" +TASK_REQUIRED: Final[Literal["required"]] = "required" + + +class TaskMetadata(BaseModel): + """ + Metadata for augmenting a request with task execution. + Include this in the `task` field of the request parameters. + """ + + model_config = ConfigDict(extra="allow") + + ttl: Annotated[int, Field(strict=True)] | None = None + """Requested duration in milliseconds to retain task from creation.""" + + +class RequestParams(BaseModel): + class Meta(BaseModel): + progressToken: ProgressToken | None = None + """ + If specified, the caller requests out-of-band progress notifications for + this request (as represented by notifications/progress). The value of this + parameter is an opaque token that will be attached to any subsequent + notifications. The receiver is not obligated to provide these notifications. + """ + + model_config = ConfigDict(extra="allow") + + task: TaskMetadata | None = None + """ + If specified, the caller is requesting task-augmented execution for this request. + The request will return a CreateTaskResult immediately, and the actual result can be + retrieved later via tasks/result. + + Task augmentation is subject to capability negotiation - receivers MUST declare support + for task augmentation of specific request types in their capabilities. + """ + + meta: Meta | None = Field(alias="_meta", default=None) + + +class PaginatedRequestParams(RequestParams): + cursor: Cursor | None = None + """ + An opaque token representing the current pagination position. + If provided, the server should return results starting after this cursor. + """ + + +class NotificationParams(BaseModel): + class Meta(BaseModel): + model_config = ConfigDict(extra="allow") + + meta: Meta | None = Field(alias="_meta", default=None) + """ + See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + for notes on _meta usage. + """ + + +RequestParamsT = TypeVar("RequestParamsT", bound=RequestParams | dict[str, Any] | None) +NotificationParamsT = TypeVar("NotificationParamsT", bound=NotificationParams | dict[str, Any] | None) +MethodT = TypeVar("MethodT", bound=str) + + +class Request(BaseModel, Generic[RequestParamsT, MethodT]): + """Base class for JSON-RPC requests.""" + + method: MethodT + params: RequestParamsT + model_config = ConfigDict(extra="allow") + + +class PaginatedRequest(Request[PaginatedRequestParams | None, MethodT], Generic[MethodT]): + """Base class for paginated requests, + matching the schema's PaginatedRequest interface.""" + + params: PaginatedRequestParams | None = None + + +class Notification(BaseModel, Generic[NotificationParamsT, MethodT]): + """Base class for JSON-RPC notifications.""" + + method: MethodT + params: NotificationParamsT + model_config = ConfigDict(extra="allow") + + +class Result(BaseModel): + """Base class for JSON-RPC results.""" + + meta: dict[str, Any] | None = Field(alias="_meta", default=None) + """ + See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + for notes on _meta usage. + """ + model_config = ConfigDict(extra="allow") + + +class PaginatedResult(Result): + nextCursor: Cursor | None = None + """ + An opaque token representing the pagination position after the last returned result. + If present, there may be more results available. + """ + + +class JSONRPCRequest(Request[dict[str, Any] | None, str]): + """A request that expects a response.""" + + jsonrpc: Literal["2.0"] + id: RequestId + method: str + params: dict[str, Any] | None = None + + +class JSONRPCNotification(Notification[dict[str, Any] | None, str]): + """A notification which does not expect a response.""" + + jsonrpc: Literal["2.0"] + params: dict[str, Any] | None = None + + +class JSONRPCResponse(BaseModel): + """A successful (non-error) response to a request.""" + + jsonrpc: Literal["2.0"] + id: RequestId + result: dict[str, Any] + model_config = ConfigDict(extra="allow") + + +# MCP-specific error codes in the range [-32000, -32099] +URL_ELICITATION_REQUIRED = -32042 +"""Error code indicating that a URL mode elicitation is required before the request can be processed.""" + +# SDK error codes +CONNECTION_CLOSED = -32000 +# REQUEST_TIMEOUT = -32001 # the typescript sdk uses this + +# Standard JSON-RPC error codes +PARSE_ERROR = -32700 +INVALID_REQUEST = -32600 +METHOD_NOT_FOUND = -32601 +INVALID_PARAMS = -32602 +INTERNAL_ERROR = -32603 + + +class ErrorData(BaseModel): + """Error information for JSON-RPC error responses.""" + + code: int + """The error type that occurred.""" + + message: str + """ + A short description of the error. The message SHOULD be limited to a concise single + sentence. + """ + + data: Any | None = None + """ + Additional information about the error. The value of this member is defined by the + sender (e.g. detailed error information, nested errors etc.). + """ + + model_config = ConfigDict(extra="allow") + + +class JSONRPCError(BaseModel): + """A response to a request that indicates an error occurred.""" + + jsonrpc: Literal["2.0"] + id: str | int + error: ErrorData + model_config = ConfigDict(extra="allow") + + +class JSONRPCMessage(RootModel[JSONRPCRequest | JSONRPCNotification | JSONRPCResponse | JSONRPCError]): + pass + + +class EmptyResult(Result): + """A response that indicates success but carries no data.""" + + +class BaseMetadata(BaseModel): + """Base class for entities with name and optional title fields.""" + + name: str + """The programmatic name of the entity.""" + + title: str | None = None + """ + Intended for UI and end-user contexts — optimized to be human-readable and easily understood, + even by those unfamiliar with domain-specific terminology. + + If not provided, the name should be used for display (except for Tool, + where `annotations.title` should be given precedence over using `name`, + if present). + """ + + +class Icon(BaseModel): + """An icon for display in user interfaces.""" + + src: str + """URL or data URI for the icon.""" + + mimeType: str | None = None + """Optional MIME type for the icon.""" + + sizes: list[str] | None = None + """Optional list of strings specifying icon dimensions (e.g., ["48x48", "96x96"]).""" + + model_config = ConfigDict(extra="allow") + + +class Implementation(BaseMetadata): + """Describes the name and version of an MCP implementation.""" + + version: str + + websiteUrl: str | None = None + """An optional URL of the website for this implementation.""" + + icons: list[Icon] | None = None + """An optional list of icons for this implementation.""" + + model_config = ConfigDict(extra="allow") + + +class RootsCapability(BaseModel): + """Capability for root operations.""" + + listChanged: bool | None = None + """Whether the client supports notifications for changes to the roots list.""" + model_config = ConfigDict(extra="allow") + + +class SamplingContextCapability(BaseModel): + """ + Capability for context inclusion during sampling. + + Indicates support for non-'none' values in the includeContext parameter. + SOFT-DEPRECATED: New implementations should use tools parameter instead. + """ + + model_config = ConfigDict(extra="allow") + + +class SamplingToolsCapability(BaseModel): + """ + Capability indicating support for tool calling during sampling. + + When present in ClientCapabilities.sampling, indicates that the client + supports the tools and toolChoice parameters in sampling requests. + """ + + model_config = ConfigDict(extra="allow") + + +class FormElicitationCapability(BaseModel): + """Capability for form mode elicitation.""" + + model_config = ConfigDict(extra="allow") + + +class UrlElicitationCapability(BaseModel): + """Capability for URL mode elicitation.""" + + model_config = ConfigDict(extra="allow") + + +class ElicitationCapability(BaseModel): + """Capability for elicitation operations. + + Clients must support at least one mode (form or url). + """ + + form: FormElicitationCapability | None = None + """Present if the client supports form mode elicitation.""" + + url: UrlElicitationCapability | None = None + """Present if the client supports URL mode elicitation.""" + + model_config = ConfigDict(extra="allow") + + +class SamplingCapability(BaseModel): + """ + Sampling capability structure, allowing fine-grained capability advertisement. + """ + + context: SamplingContextCapability | None = None + """ + Present if the client supports non-'none' values for includeContext parameter. + SOFT-DEPRECATED: New implementations should use tools parameter instead. + """ + tools: SamplingToolsCapability | None = None + """ + Present if the client supports tools and toolChoice parameters in sampling requests. + Presence indicates full tool calling support during sampling. + """ + model_config = ConfigDict(extra="allow") + + +class TasksListCapability(BaseModel): + """Capability for tasks listing operations.""" + + model_config = ConfigDict(extra="allow") + + +class TasksCancelCapability(BaseModel): + """Capability for tasks cancel operations.""" + + model_config = ConfigDict(extra="allow") + + +class TasksCreateMessageCapability(BaseModel): + """Capability for tasks create messages.""" + + model_config = ConfigDict(extra="allow") + + +class TasksSamplingCapability(BaseModel): + """Capability for tasks sampling operations.""" + + model_config = ConfigDict(extra="allow") + + createMessage: TasksCreateMessageCapability | None = None + + +class TasksCreateElicitationCapability(BaseModel): + """Capability for tasks create elicitation operations.""" + + model_config = ConfigDict(extra="allow") + + +class TasksElicitationCapability(BaseModel): + """Capability for tasks elicitation operations.""" + + model_config = ConfigDict(extra="allow") + + create: TasksCreateElicitationCapability | None = None + + +class ClientTasksRequestsCapability(BaseModel): + """Capability for tasks requests operations.""" + + model_config = ConfigDict(extra="allow") + + sampling: TasksSamplingCapability | None = None + + elicitation: TasksElicitationCapability | None = None + + +class ClientTasksCapability(BaseModel): + """Capability for client tasks operations.""" + + model_config = ConfigDict(extra="allow") + + list: TasksListCapability | None = None + """Whether this client supports tasks/list.""" + + cancel: TasksCancelCapability | None = None + """Whether this client supports tasks/cancel.""" + + requests: ClientTasksRequestsCapability | None = None + """Specifies which request types can be augmented with tasks.""" + + +class ClientCapabilities(BaseModel): + """Capabilities a client may support.""" + + experimental: dict[str, dict[str, Any]] | None = None + """Experimental, non-standard capabilities that the client supports.""" + sampling: SamplingCapability | None = None + """ + Present if the client supports sampling from an LLM. + Can contain fine-grained capabilities like context and tools support. + """ + elicitation: ElicitationCapability | None = None + """Present if the client supports elicitation from the user.""" + roots: RootsCapability | None = None + """Present if the client supports listing roots.""" + tasks: ClientTasksCapability | None = None + """Present if the client supports task-augmented requests.""" + + model_config = ConfigDict(extra="allow") + + +class PromptsCapability(BaseModel): + """Capability for prompts operations.""" + + listChanged: bool | None = None + """Whether this server supports notifications for changes to the prompt list.""" + model_config = ConfigDict(extra="allow") + + +class ResourcesCapability(BaseModel): + """Capability for resources operations.""" + + subscribe: bool | None = None + """Whether this server supports subscribing to resource updates.""" + listChanged: bool | None = None + """Whether this server supports notifications for changes to the resource list.""" + model_config = ConfigDict(extra="allow") + + +class ToolsCapability(BaseModel): + """Capability for tools operations.""" + + listChanged: bool | None = None + """Whether this server supports notifications for changes to the tool list.""" + model_config = ConfigDict(extra="allow") + + +class LoggingCapability(BaseModel): + """Capability for logging operations.""" + + model_config = ConfigDict(extra="allow") + + +class CompletionsCapability(BaseModel): + """Capability for completions operations.""" + + model_config = ConfigDict(extra="allow") + + +class TasksCallCapability(BaseModel): + """Capability for tasks call operations.""" + + model_config = ConfigDict(extra="allow") + + +class TasksToolsCapability(BaseModel): + """Capability for tasks tools operations.""" + + model_config = ConfigDict(extra="allow") + call: TasksCallCapability | None = None + + +class ServerTasksRequestsCapability(BaseModel): + """Capability for tasks requests operations.""" + + model_config = ConfigDict(extra="allow") + + tools: TasksToolsCapability | None = None + + +class ServerTasksCapability(BaseModel): + """Capability for server tasks operations.""" + + model_config = ConfigDict(extra="allow") + + list: TasksListCapability | None = None + cancel: TasksCancelCapability | None = None + requests: ServerTasksRequestsCapability | None = None + + +class ServerCapabilities(BaseModel): + """Capabilities that a server may support.""" + + experimental: dict[str, dict[str, Any]] | None = None + """Experimental, non-standard capabilities that the server supports.""" + logging: LoggingCapability | None = None + """Present if the server supports sending log messages to the client.""" + prompts: PromptsCapability | None = None + """Present if the server offers any prompt templates.""" + resources: ResourcesCapability | None = None + """Present if the server offers any resources to read.""" + tools: ToolsCapability | None = None + """Present if the server offers any tools to call.""" + completions: CompletionsCapability | None = None + """Present if the server offers autocompletion suggestions for prompts and resources.""" + tasks: ServerTasksCapability | None = None + """Present if the server supports task-augmented requests.""" + model_config = ConfigDict(extra="allow") + + +TaskStatus = Literal["working", "input_required", "completed", "failed", "cancelled"] + +# Task status constants +TASK_STATUS_WORKING: Final[Literal["working"]] = "working" +TASK_STATUS_INPUT_REQUIRED: Final[Literal["input_required"]] = "input_required" +TASK_STATUS_COMPLETED: Final[Literal["completed"]] = "completed" +TASK_STATUS_FAILED: Final[Literal["failed"]] = "failed" +TASK_STATUS_CANCELLED: Final[Literal["cancelled"]] = "cancelled" + + +class RelatedTaskMetadata(BaseModel): + """ + Metadata for associating messages with a task. + + Include this in the `_meta` field under the key `io.modelcontextprotocol/related-task`. + """ + + model_config = ConfigDict(extra="allow") + taskId: str + """The task identifier this message is associated with.""" + + +class Task(BaseModel): + """Data associated with a task.""" + + model_config = ConfigDict(extra="allow") + + taskId: str + """The task identifier.""" + + status: TaskStatus + """Current task state.""" + + statusMessage: str | None = None + """ + Optional human-readable message describing the current task state. + This can provide context for any status, including: + - Reasons for "cancelled" status + - Summaries for "completed" status + - Diagnostic information for "failed" status (e.g., error details, what went wrong) + """ + + createdAt: datetime # Pydantic will enforce ISO 8601 and re-serialize as a string later + """ISO 8601 timestamp when the task was created.""" + + lastUpdatedAt: datetime + """ISO 8601 timestamp when the task was last updated.""" + + ttl: Annotated[int, Field(strict=True)] | None + """Actual retention duration from creation in milliseconds, null for unlimited.""" + + pollInterval: Annotated[int, Field(strict=True)] | None = None + """Suggested polling interval in milliseconds.""" + + +class CreateTaskResult(Result): + """A response to a task-augmented request.""" + + task: Task + + +class GetTaskRequestParams(RequestParams): + model_config = ConfigDict(extra="allow") + taskId: str + """The task identifier to query.""" + + +class GetTaskRequest(Request[GetTaskRequestParams, Literal["tasks/get"]]): + """A request to retrieve the state of a task.""" + + method: Literal["tasks/get"] = "tasks/get" + + params: GetTaskRequestParams + + +class GetTaskResult(Result, Task): + """The response to a tasks/get request.""" + + +class GetTaskPayloadRequestParams(RequestParams): + model_config = ConfigDict(extra="allow") + + taskId: str + """The task identifier to retrieve results for.""" + + +class GetTaskPayloadRequest(Request[GetTaskPayloadRequestParams, Literal["tasks/result"]]): + """A request to retrieve the result of a completed task.""" + + method: Literal["tasks/result"] = "tasks/result" + params: GetTaskPayloadRequestParams + + +class GetTaskPayloadResult(Result): + """ + The response to a tasks/result request. + The structure matches the result type of the original request. + For example, a tools/call task would return the CallToolResult structure. + """ + + +class CancelTaskRequestParams(RequestParams): + model_config = ConfigDict(extra="allow") + + taskId: str + """The task identifier to cancel.""" + + +class CancelTaskRequest(Request[CancelTaskRequestParams, Literal["tasks/cancel"]]): + """A request to cancel a task.""" + + method: Literal["tasks/cancel"] = "tasks/cancel" + params: CancelTaskRequestParams + + +class CancelTaskResult(Result, Task): + """The response to a tasks/cancel request.""" + + +class ListTasksRequest(PaginatedRequest[Literal["tasks/list"]]): + """A request to retrieve a list of tasks.""" + + method: Literal["tasks/list"] = "tasks/list" + + +class ListTasksResult(PaginatedResult): + """The response to a tasks/list request.""" + + tasks: list[Task] + + +class TaskStatusNotificationParams(NotificationParams, Task): + """Parameters for a `notifications/tasks/status` notification.""" + + +class TaskStatusNotification(Notification[TaskStatusNotificationParams, Literal["notifications/tasks/status"]]): + """ + An optional notification from the receiver to the requestor, informing them that a task's status has changed. + Receivers are not required to send these notifications + """ + + method: Literal["notifications/tasks/status"] = "notifications/tasks/status" + params: TaskStatusNotificationParams + + +class InitializeRequestParams(RequestParams): + """Parameters for the initialize request.""" + + protocolVersion: str | int + """The latest version of the Model Context Protocol that the client supports.""" + capabilities: ClientCapabilities + clientInfo: Implementation + model_config = ConfigDict(extra="allow") + + +class InitializeRequest(Request[InitializeRequestParams, Literal["initialize"]]): + """ + This request is sent from the client to the server when it first connects, asking it + to begin initialization. + """ + + method: Literal["initialize"] = "initialize" + params: InitializeRequestParams + + +class InitializeResult(Result): + """After receiving an initialize request from the client, the server sends this.""" + + protocolVersion: str | int + """The version of the Model Context Protocol that the server wants to use.""" + capabilities: ServerCapabilities + serverInfo: Implementation + instructions: str | None = None + """Instructions describing how to use the server and its features.""" + + +class InitializedNotification(Notification[NotificationParams | None, Literal["notifications/initialized"]]): + """ + This notification is sent from the client to the server after initialization has + finished. + """ + + method: Literal["notifications/initialized"] = "notifications/initialized" + params: NotificationParams | None = None + + +class PingRequest(Request[RequestParams | None, Literal["ping"]]): + """ + A ping, issued by either the server or the client, to check that the other party is + still alive. + """ + + method: Literal["ping"] = "ping" + params: RequestParams | None = None + + +class ProgressNotificationParams(NotificationParams): + """Parameters for progress notifications.""" + + progressToken: ProgressToken + """ + The progress token which was given in the initial request, used to associate this + notification with the request that is proceeding. + """ + progress: float + """ + The progress thus far. This should increase every time progress is made, even if the + total is unknown. + """ + total: float | None = None + """Total number of items to process (or total progress required), if known.""" + message: str | None = None + """ + Message related to progress. This should provide relevant human readable + progress information. + """ + model_config = ConfigDict(extra="allow") + + +class ProgressNotification(Notification[ProgressNotificationParams, Literal["notifications/progress"]]): + """ + An out-of-band notification used to inform the receiver of a progress update for a + long-running request. + """ + + method: Literal["notifications/progress"] = "notifications/progress" + params: ProgressNotificationParams + + +class ListResourcesRequest(PaginatedRequest[Literal["resources/list"]]): + """Sent from the client to request a list of resources the server has.""" + + method: Literal["resources/list"] = "resources/list" + + +class Annotations(BaseModel): + audience: list[Role] | None = None + priority: Annotated[float, Field(ge=0.0, le=1.0)] | None = None + model_config = ConfigDict(extra="allow") + + +class Resource(BaseMetadata): + """A known resource that the server is capable of reading.""" + + uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] + """The URI of this resource.""" + description: str | None = None + """A description of what this resource represents.""" + mimeType: str | None = None + """The MIME type of this resource, if known.""" + size: int | None = None + """ + The size of the raw resource content, in bytes (i.e., before base64 encoding + or any tokenization), if known. + + This can be used by Hosts to display file sizes and estimate context window usage. + """ + icons: list[Icon] | None = None + """An optional list of icons for this resource.""" + annotations: Annotations | None = None + meta: dict[str, Any] | None = Field(alias="_meta", default=None) + """ + See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + for notes on _meta usage. + """ + model_config = ConfigDict(extra="allow") + + +class ResourceTemplate(BaseMetadata): + """A template description for resources available on the server.""" + + uriTemplate: str + """ + A URI template (according to RFC 6570) that can be used to construct resource + URIs. + """ + description: str | None = None + """A human-readable description of what this template is for.""" + mimeType: str | None = None + """ + The MIME type for all resources that match this template. This should only be + included if all resources matching this template have the same type. + """ + icons: list[Icon] | None = None + """An optional list of icons for this resource template.""" + annotations: Annotations | None = None + meta: dict[str, Any] | None = Field(alias="_meta", default=None) + """ + See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + for notes on _meta usage. + """ + model_config = ConfigDict(extra="allow") + + +class ListResourcesResult(PaginatedResult): + """The server's response to a resources/list request from the client.""" + + resources: list[Resource] + + +class ListResourceTemplatesRequest(PaginatedRequest[Literal["resources/templates/list"]]): + """Sent from the client to request a list of resource templates the server has.""" + + method: Literal["resources/templates/list"] = "resources/templates/list" + + +class ListResourceTemplatesResult(PaginatedResult): + """The server's response to a resources/templates/list request from the client.""" + + resourceTemplates: list[ResourceTemplate] + + +class ReadResourceRequestParams(RequestParams): + """Parameters for reading a resource.""" + + uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] + """ + The URI of the resource to read. The URI can use any protocol; it is up to the + server how to interpret it. + """ + model_config = ConfigDict(extra="allow") + + +class ReadResourceRequest(Request[ReadResourceRequestParams, Literal["resources/read"]]): + """Sent from the client to the server, to read a specific resource URI.""" + + method: Literal["resources/read"] = "resources/read" + params: ReadResourceRequestParams + + +class ResourceContents(BaseModel): + """The contents of a specific resource or sub-resource.""" + + uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] + """The URI of this resource.""" + mimeType: str | None = None + """The MIME type of this resource, if known.""" + meta: dict[str, Any] | None = Field(alias="_meta", default=None) + """ + See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + for notes on _meta usage. + """ + model_config = ConfigDict(extra="allow") + + +class TextResourceContents(ResourceContents): + """Text contents of a resource.""" + + text: str + """ + The text of the item. This must only be set if the item can actually be represented + as text (not binary data). + """ + + +class BlobResourceContents(ResourceContents): + """Binary contents of a resource.""" + + blob: str + """A base64-encoded string representing the binary data of the item.""" + + +class ReadResourceResult(Result): + """The server's response to a resources/read request from the client.""" + + contents: list[TextResourceContents | BlobResourceContents] + + +class ResourceListChangedNotification( + Notification[NotificationParams | None, Literal["notifications/resources/list_changed"]] +): + """ + An optional notification from the server to the client, informing it that the list + of resources it can read from has changed. + """ + + method: Literal["notifications/resources/list_changed"] = "notifications/resources/list_changed" + params: NotificationParams | None = None + + +class SubscribeRequestParams(RequestParams): + """Parameters for subscribing to a resource.""" + + uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] + """ + The URI of the resource to subscribe to. The URI can use any protocol; it is up to + the server how to interpret it. + """ + model_config = ConfigDict(extra="allow") + + +class SubscribeRequest(Request[SubscribeRequestParams, Literal["resources/subscribe"]]): + """ + Sent from the client to request resources/updated notifications from the server + whenever a particular resource changes. + """ + + method: Literal["resources/subscribe"] = "resources/subscribe" + params: SubscribeRequestParams + + +class UnsubscribeRequestParams(RequestParams): + """Parameters for unsubscribing from a resource.""" + + uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] + """The URI of the resource to unsubscribe from.""" + model_config = ConfigDict(extra="allow") + + +class UnsubscribeRequest(Request[UnsubscribeRequestParams, Literal["resources/unsubscribe"]]): + """ + Sent from the client to request cancellation of resources/updated notifications from + the server. + """ + + method: Literal["resources/unsubscribe"] = "resources/unsubscribe" + params: UnsubscribeRequestParams + + +class ResourceUpdatedNotificationParams(NotificationParams): + """Parameters for resource update notifications.""" + + uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] + """ + The URI of the resource that has been updated. This might be a sub-resource of the + one that the client actually subscribed to. + """ + model_config = ConfigDict(extra="allow") + + +class ResourceUpdatedNotification( + Notification[ResourceUpdatedNotificationParams, Literal["notifications/resources/updated"]] +): + """ + A notification from the server to the client, informing it that a resource has + changed and may need to be read again. + """ + + method: Literal["notifications/resources/updated"] = "notifications/resources/updated" + params: ResourceUpdatedNotificationParams + + +class ListPromptsRequest(PaginatedRequest[Literal["prompts/list"]]): + """Sent from the client to request a list of prompts and prompt templates.""" + + method: Literal["prompts/list"] = "prompts/list" + + +class PromptArgument(BaseModel): + """An argument for a prompt template.""" + + name: str + """The name of the argument.""" + description: str | None = None + """A human-readable description of the argument.""" + required: bool | None = None + """Whether this argument must be provided.""" + model_config = ConfigDict(extra="allow") + + +class Prompt(BaseMetadata): + """A prompt or prompt template that the server offers.""" + + description: str | None = None + """An optional description of what this prompt provides.""" + arguments: list[PromptArgument] | None = None + """A list of arguments to use for templating the prompt.""" + icons: list[Icon] | None = None + """An optional list of icons for this prompt.""" + meta: dict[str, Any] | None = Field(alias="_meta", default=None) + """ + See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + for notes on _meta usage. + """ + model_config = ConfigDict(extra="allow") + + +class ListPromptsResult(PaginatedResult): + """The server's response to a prompts/list request from the client.""" + + prompts: list[Prompt] + + +class GetPromptRequestParams(RequestParams): + """Parameters for getting a prompt.""" + + name: str + """The name of the prompt or prompt template.""" + arguments: dict[str, str] | None = None + """Arguments to use for templating the prompt.""" + model_config = ConfigDict(extra="allow") + + +class GetPromptRequest(Request[GetPromptRequestParams, Literal["prompts/get"]]): + """Used by the client to get a prompt provided by the server.""" + + method: Literal["prompts/get"] = "prompts/get" + params: GetPromptRequestParams + + +class TextContent(BaseModel): + """Text content for a message.""" + + type: Literal["text"] + text: str + """The text content of the message.""" + annotations: Annotations | None = None + meta: dict[str, Any] | None = Field(alias="_meta", default=None) + """ + See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + for notes on _meta usage. + """ + model_config = ConfigDict(extra="allow") + + +class ImageContent(BaseModel): + """Image content for a message.""" + + type: Literal["image"] + data: str + """The base64-encoded image data.""" + mimeType: str + """ + The MIME type of the image. Different providers may support different + image types. + """ + annotations: Annotations | None = None + meta: dict[str, Any] | None = Field(alias="_meta", default=None) + """ + See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + for notes on _meta usage. + """ + model_config = ConfigDict(extra="allow") + + +class AudioContent(BaseModel): + """Audio content for a message.""" + + type: Literal["audio"] + data: str + """The base64-encoded audio data.""" + mimeType: str + """ + The MIME type of the audio. Different providers may support different + audio types. + """ + annotations: Annotations | None = None + meta: dict[str, Any] | None = Field(alias="_meta", default=None) + """ + See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + for notes on _meta usage. + """ + model_config = ConfigDict(extra="allow") + + +class ToolUseContent(BaseModel): + """ + Content representing an assistant's request to invoke a tool. + + This content type appears in assistant messages when the LLM wants to call a tool + during sampling. The server should execute the tool and return a ToolResultContent + in the next user message. + """ + + type: Literal["tool_use"] + """Discriminator for tool use content.""" + + name: str + """The name of the tool to invoke. Must match a tool name from the request's tools array.""" + + id: str + """Unique identifier for this tool call, used to correlate with ToolResultContent.""" + + input: dict[str, Any] + """Arguments to pass to the tool. Must conform to the tool's inputSchema.""" + + meta: dict[str, Any] | None = Field(alias="_meta", default=None) + """ + See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + for notes on _meta usage. + """ + model_config = ConfigDict(extra="allow") + + +class ToolResultContent(BaseModel): + """ + Content representing the result of a tool execution. + + This content type appears in user messages as a response to a ToolUseContent + from the assistant. It contains the output of executing the requested tool. + """ + + type: Literal["tool_result"] + """Discriminator for tool result content.""" + + toolUseId: str + """The unique identifier that corresponds to the tool call's id field.""" + + content: list["ContentBlock"] = [] + """ + A list of content objects representing the tool result. + Defaults to empty list if not provided. + """ + + structuredContent: dict[str, Any] | None = None + """ + Optional structured tool output that matches the tool's outputSchema (if defined). + """ + + isError: bool | None = None + """Whether the tool execution resulted in an error.""" + + meta: dict[str, Any] | None = Field(alias="_meta", default=None) + """ + See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + for notes on _meta usage. + """ + model_config = ConfigDict(extra="allow") + + +SamplingMessageContentBlock: TypeAlias = TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent +"""Content block types allowed in sampling messages.""" + +SamplingContent: TypeAlias = TextContent | ImageContent | AudioContent +"""Basic content types for sampling responses (without tool use). +Used for backwards-compatible CreateMessageResult when tools are not used.""" + + +class SamplingMessage(BaseModel): + """Describes a message issued to or received from an LLM API.""" + + role: Role + content: SamplingMessageContentBlock | list[SamplingMessageContentBlock] + """ + Message content. Can be a single content block or an array of content blocks + for multi-modal messages and tool interactions. + """ + meta: dict[str, Any] | None = Field(alias="_meta", default=None) + """ + See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + for notes on _meta usage. + """ + model_config = ConfigDict(extra="allow") + + @property + def content_as_list(self) -> list[SamplingMessageContentBlock]: + """Returns the content as a list of content blocks, regardless of whether + it was originally a single block or a list.""" + return self.content if isinstance(self.content, list) else [self.content] + + +class EmbeddedResource(BaseModel): + """ + The contents of a resource, embedded into a prompt or tool call result. + + It is up to the client how best to render embedded resources for the benefit + of the LLM and/or the user. + """ + + type: Literal["resource"] + resource: TextResourceContents | BlobResourceContents + annotations: Annotations | None = None + meta: dict[str, Any] | None = Field(alias="_meta", default=None) + """ + See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + for notes on _meta usage. + """ + model_config = ConfigDict(extra="allow") + + +class ResourceLink(Resource): + """ + A resource that the server is capable of reading, included in a prompt or tool call result. + + Note: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests. + """ + + type: Literal["resource_link"] + + +ContentBlock = TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource +"""A content block that can be used in prompts and tool results.""" + +Content: TypeAlias = ContentBlock +# """DEPRECATED: Content is deprecated, you should use ContentBlock directly.""" + + +class PromptMessage(BaseModel): + """Describes a message returned as part of a prompt.""" + + role: Role + content: ContentBlock + model_config = ConfigDict(extra="allow") + + +class GetPromptResult(Result): + """The server's response to a prompts/get request from the client.""" + + description: str | None = None + """An optional description for the prompt.""" + messages: list[PromptMessage] + + +class PromptListChangedNotification( + Notification[NotificationParams | None, Literal["notifications/prompts/list_changed"]] +): + """ + An optional notification from the server to the client, informing it that the list + of prompts it offers has changed. + """ + + method: Literal["notifications/prompts/list_changed"] = "notifications/prompts/list_changed" + params: NotificationParams | None = None + + +class ListToolsRequest(PaginatedRequest[Literal["tools/list"]]): + """Sent from the client to request a list of tools the server has.""" + + method: Literal["tools/list"] = "tools/list" + + +class ToolAnnotations(BaseModel): + """ + Additional properties describing a Tool to clients. + + NOTE: all properties in ToolAnnotations are **hints**. + They are not guaranteed to provide a faithful description of + tool behavior (including descriptive properties like `title`). + + Clients should never make tool use decisions based on ToolAnnotations + received from untrusted servers. + """ + + title: str | None = None + """A human-readable title for the tool.""" + + readOnlyHint: bool | None = None + """ + If true, the tool does not modify its environment. + Default: false + """ + + destructiveHint: bool | None = None + """ + If true, the tool may perform destructive updates to its environment. + If false, the tool performs only additive updates. + (This property is meaningful only when `readOnlyHint == false`) + Default: true + """ + + idempotentHint: bool | None = None + """ + If true, calling the tool repeatedly with the same arguments + will have no additional effect on the its environment. + (This property is meaningful only when `readOnlyHint == false`) + Default: false + """ + + openWorldHint: bool | None = None + """ + If true, this tool may interact with an "open world" of external + entities. If false, the tool's domain of interaction is closed. + For example, the world of a web search tool is open, whereas that + of a memory tool is not. + Default: true + """ + + model_config = ConfigDict(extra="allow") + + +class ToolExecution(BaseModel): + """Execution-related properties for a tool.""" + + model_config = ConfigDict(extra="allow") + + taskSupport: TaskExecutionMode | None = None + """ + Indicates whether this tool supports task-augmented execution. + This allows clients to handle long-running operations through polling + the task system. + + - "forbidden": Tool does not support task-augmented execution (default when absent) + - "optional": Tool may support task-augmented execution + - "required": Tool requires task-augmented execution + + Default: "forbidden" + """ + + +class Tool(BaseMetadata): + """Definition for a tool the client can call.""" + + description: str | None = None + """A human-readable description of the tool.""" + inputSchema: dict[str, Any] + """A JSON Schema object defining the expected parameters for the tool.""" + outputSchema: dict[str, Any] | None = None + """ + An optional JSON Schema object defining the structure of the tool's output + returned in the structuredContent field of a CallToolResult. + """ + icons: list[Icon] | None = None + """An optional list of icons for this tool.""" + annotations: ToolAnnotations | None = None + """Optional additional tool information.""" + meta: dict[str, Any] | None = Field(alias="_meta", default=None) + """ + See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + for notes on _meta usage. + """ + + execution: ToolExecution | None = None + + model_config = ConfigDict(extra="allow") + + +class ListToolsResult(PaginatedResult): + """The server's response to a tools/list request from the client.""" + + tools: list[Tool] + + +class CallToolRequestParams(RequestParams): + """Parameters for calling a tool.""" + + name: str + arguments: dict[str, Any] | None = None + model_config = ConfigDict(extra="allow") + + +class CallToolRequest(Request[CallToolRequestParams, Literal["tools/call"]]): + """Used by the client to invoke a tool provided by the server.""" + + method: Literal["tools/call"] = "tools/call" + params: CallToolRequestParams + + +class CallToolResult(Result): + """The server's response to a tool call.""" + + content: list[ContentBlock] + structuredContent: dict[str, Any] | None = None + """An optional JSON object that represents the structured result of the tool call.""" + isError: bool = False + + +class ToolListChangedNotification(Notification[NotificationParams | None, Literal["notifications/tools/list_changed"]]): + """ + An optional notification from the server to the client, informing it that the list + of tools it offers has changed. + """ + + method: Literal["notifications/tools/list_changed"] = "notifications/tools/list_changed" + params: NotificationParams | None = None + + +LoggingLevel = Literal["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"] + + +class SetLevelRequestParams(RequestParams): + """Parameters for setting the logging level.""" + + level: LoggingLevel + """The level of logging that the client wants to receive from the server.""" + model_config = ConfigDict(extra="allow") + + +class SetLevelRequest(Request[SetLevelRequestParams, Literal["logging/setLevel"]]): + """A request from the client to the server, to enable or adjust logging.""" + + method: Literal["logging/setLevel"] = "logging/setLevel" + params: SetLevelRequestParams + + +class LoggingMessageNotificationParams(NotificationParams): + """Parameters for logging message notifications.""" + + level: LoggingLevel + """The severity of this log message.""" + logger: str | None = None + """An optional name of the logger issuing this message.""" + data: Any + """ + The data to be logged, such as a string message or an object. Any JSON serializable + type is allowed here. + """ + model_config = ConfigDict(extra="allow") + + +class LoggingMessageNotification(Notification[LoggingMessageNotificationParams, Literal["notifications/message"]]): + """Notification of a log message passed from server to client.""" + + method: Literal["notifications/message"] = "notifications/message" + params: LoggingMessageNotificationParams + + +IncludeContext = Literal["none", "thisServer", "allServers"] + + +class ModelHint(BaseModel): + """Hints to use for model selection.""" + + name: str | None = None + """A hint for a model name.""" + + model_config = ConfigDict(extra="allow") + + +class ModelPreferences(BaseModel): + """ + The server's preferences for model selection, requested by the client during + sampling. + + Because LLMs can vary along multiple dimensions, choosing the "best" model is + rarely straightforward. Different models excel in different areas—some are + faster but less capable, others are more capable but more expensive, and so + on. This interface allows servers to express their priorities across multiple + dimensions to help clients make an appropriate selection for their use case. + + These preferences are always advisory. The client MAY ignore them. It is also + up to the client to decide how to interpret these preferences and how to + balance them against other considerations. + """ + + hints: list[ModelHint] | None = None + """ + Optional hints to use for model selection. + + If multiple hints are specified, the client MUST evaluate them in order + (such that the first match is taken). + + The client SHOULD prioritize these hints over the numeric priorities, but + MAY still use the priorities to select from ambiguous matches. + """ + + costPriority: float | None = None + """ + How much to prioritize cost when selecting a model. A value of 0 means cost + is not important, while a value of 1 means cost is the most important + factor. + """ + + speedPriority: float | None = None + """ + How much to prioritize sampling speed (latency) when selecting a model. A + value of 0 means speed is not important, while a value of 1 means speed is + the most important factor. + """ + + intelligencePriority: float | None = None + """ + How much to prioritize intelligence and capabilities when selecting a + model. A value of 0 means intelligence is not important, while a value of 1 + means intelligence is the most important factor. + """ + + model_config = ConfigDict(extra="allow") + + +class ToolChoice(BaseModel): + """ + Controls tool usage behavior during sampling. + + Allows the server to specify whether and how the LLM should use tools + in its response. + """ + + mode: Literal["auto", "required", "none"] | None = None + """ + Controls when tools are used: + - "auto": Model decides whether to use tools (default) + - "required": Model MUST use at least one tool before completing + - "none": Model should not use tools + """ + + model_config = ConfigDict(extra="allow") + + +class CreateMessageRequestParams(RequestParams): + """Parameters for creating a message.""" + + messages: list[SamplingMessage] + modelPreferences: ModelPreferences | None = None + """ + The server's preferences for which model to select. The client MAY ignore + these preferences. + """ + systemPrompt: str | None = None + """An optional system prompt the server wants to use for sampling.""" + includeContext: IncludeContext | None = None + """ + A request to include context from one or more MCP servers (including the caller), to + be attached to the prompt. + """ + temperature: float | None = None + maxTokens: int + """The maximum number of tokens to sample, as requested by the server.""" + stopSequences: list[str] | None = None + metadata: dict[str, Any] | None = None + """Optional metadata to pass through to the LLM provider.""" + tools: list["Tool"] | None = None + """ + Tool definitions for the LLM to use during sampling. + Requires clientCapabilities.sampling.tools to be present. + """ + toolChoice: ToolChoice | None = None + """ + Controls tool usage behavior. + Requires clientCapabilities.sampling.tools and the tools parameter to be present. + """ + model_config = ConfigDict(extra="allow") + + +class CreateMessageRequest(Request[CreateMessageRequestParams, Literal["sampling/createMessage"]]): + """A request from the server to sample an LLM via the client.""" + + method: Literal["sampling/createMessage"] = "sampling/createMessage" + params: CreateMessageRequestParams + + +StopReason = Literal["endTurn", "stopSequence", "maxTokens", "toolUse"] | str + + +class CreateMessageResult(Result): + """The client's response to a sampling/create_message request from the server. + + This is the backwards-compatible version that returns single content (no arrays). + Used when the request does not include tools. + """ + + role: Role + """The role of the message sender (typically 'assistant' for LLM responses).""" + content: SamplingContent + """Response content. Single content block (text, image, or audio).""" + model: str + """The name of the model that generated the message.""" + stopReason: StopReason | None = None + """The reason why sampling stopped, if known.""" + + +class CreateMessageResultWithTools(Result): + """The client's response to a sampling/create_message request when tools were provided. + + This version supports array content for tool use flows. + """ + + role: Role + """The role of the message sender (typically 'assistant' for LLM responses).""" + content: SamplingMessageContentBlock | list[SamplingMessageContentBlock] + """ + Response content. May be a single content block or an array. + May include ToolUseContent if stopReason is 'toolUse'. + """ + model: str + """The name of the model that generated the message.""" + stopReason: StopReason | None = None + """ + The reason why sampling stopped, if known. + 'toolUse' indicates the model wants to use a tool. + """ + + @property + def content_as_list(self) -> list[SamplingMessageContentBlock]: + """Returns the content as a list of content blocks, regardless of whether + it was originally a single block or a list.""" + return self.content if isinstance(self.content, list) else [self.content] + + +class ResourceTemplateReference(BaseModel): + """A reference to a resource or resource template definition.""" + + type: Literal["ref/resource"] + uri: str + """The URI or URI template of the resource.""" + model_config = ConfigDict(extra="allow") + + +@deprecated("`ResourceReference` is deprecated, you should use `ResourceTemplateReference`.") +class ResourceReference(ResourceTemplateReference): + pass + + +class PromptReference(BaseModel): + """Identifies a prompt.""" + + type: Literal["ref/prompt"] + name: str + """The name of the prompt or prompt template""" + model_config = ConfigDict(extra="allow") + + +class CompletionArgument(BaseModel): + """The argument's information for completion requests.""" + + name: str + """The name of the argument""" + value: str + """The value of the argument to use for completion matching.""" + model_config = ConfigDict(extra="allow") + + +class CompletionContext(BaseModel): + """Additional, optional context for completions.""" + + arguments: dict[str, str] | None = None + """Previously-resolved variables in a URI template or prompt.""" + model_config = ConfigDict(extra="allow") + + +class CompleteRequestParams(RequestParams): + """Parameters for completion requests.""" + + ref: ResourceTemplateReference | PromptReference + argument: CompletionArgument + context: CompletionContext | None = None + """Additional, optional context for completions""" + model_config = ConfigDict(extra="allow") + + +class CompleteRequest(Request[CompleteRequestParams, Literal["completion/complete"]]): + """A request from the client to the server, to ask for completion options.""" + + method: Literal["completion/complete"] = "completion/complete" + params: CompleteRequestParams + + +class Completion(BaseModel): + """Completion information.""" + + values: list[str] + """An array of completion values. Must not exceed 100 items.""" + total: int | None = None + """ + The total number of completion options available. This can exceed the number of + values actually sent in the response. + """ + hasMore: bool | None = None + """ + Indicates whether there are additional completion options beyond those provided in + the current response, even if the exact total is unknown. + """ + model_config = ConfigDict(extra="allow") + + +class CompleteResult(Result): + """The server's response to a completion/complete request""" + + completion: Completion + + +class ListRootsRequest(Request[RequestParams | None, Literal["roots/list"]]): + """ + Sent from the server to request a list of root URIs from the client. Roots allow + servers to ask for specific directories or files to operate on. A common example + for roots is providing a set of repositories or directories a server should operate + on. + + This request is typically used when the server needs to understand the file system + structure or access specific locations that the client has permission to read from. + """ + + method: Literal["roots/list"] = "roots/list" + params: RequestParams | None = None + + +class Root(BaseModel): + """Represents a root directory or file that the server can operate on.""" + + uri: FileUrl + """ + The URI identifying the root. This *must* start with file:// for now. + This restriction may be relaxed in future versions of the protocol to allow + other URI schemes. + """ + name: str | None = None + """ + An optional name for the root. This can be used to provide a human-readable + identifier for the root, which may be useful for display purposes or for + referencing the root in other parts of the application. + """ + meta: dict[str, Any] | None = Field(alias="_meta", default=None) + """ + See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + for notes on _meta usage. + """ + model_config = ConfigDict(extra="allow") + + +class ListRootsResult(Result): + """ + The client's response to a roots/list request from the server. + This result contains an array of Root objects, each representing a root directory + or file that the server can operate on. + """ + + roots: list[Root] + + +class RootsListChangedNotification( + Notification[NotificationParams | None, Literal["notifications/roots/list_changed"]] +): + """ + A notification from the client to the server, informing it that the list of + roots has changed. + + This notification should be sent whenever the client adds, removes, or + modifies any root. The server should then request an updated list of roots + using the ListRootsRequest. + """ + + method: Literal["notifications/roots/list_changed"] = "notifications/roots/list_changed" + params: NotificationParams | None = None + + +class CancelledNotificationParams(NotificationParams): + """Parameters for cancellation notifications.""" + + requestId: RequestId | None = None + """ + The ID of the request to cancel. + + This MUST correspond to the ID of a request previously issued in the same direction. + This MUST be provided for cancelling non-task requests. + This MUST NOT be used for cancelling tasks (use the `tasks/cancel` request instead). + """ + reason: str | None = None + """An optional string describing the reason for the cancellation.""" + + model_config = ConfigDict(extra="allow") + + +class CancelledNotification(Notification[CancelledNotificationParams, Literal["notifications/cancelled"]]): + """ + This notification can be sent by either side to indicate that it is canceling a + previously-issued request. + """ + + method: Literal["notifications/cancelled"] = "notifications/cancelled" + params: CancelledNotificationParams + + +class ElicitCompleteNotificationParams(NotificationParams): + """Parameters for elicitation completion notifications.""" + + elicitationId: str + """The unique identifier of the elicitation that was completed.""" + + model_config = ConfigDict(extra="allow") + + +class ElicitCompleteNotification( + Notification[ElicitCompleteNotificationParams, Literal["notifications/elicitation/complete"]] +): + """ + A notification from the server to the client, informing it that a URL mode + elicitation has been completed. + + Clients MAY use the notification to automatically retry requests that received a + URLElicitationRequiredError, update the user interface, or otherwise continue + an interaction. However, because delivery of the notification is not guaranteed, + clients must not wait indefinitely for a notification from the server. + """ + + method: Literal["notifications/elicitation/complete"] = "notifications/elicitation/complete" + params: ElicitCompleteNotificationParams + + +ClientRequestType: TypeAlias = ( + PingRequest + | InitializeRequest + | CompleteRequest + | SetLevelRequest + | GetPromptRequest + | ListPromptsRequest + | ListResourcesRequest + | ListResourceTemplatesRequest + | ReadResourceRequest + | SubscribeRequest + | UnsubscribeRequest + | CallToolRequest + | ListToolsRequest + | GetTaskRequest + | GetTaskPayloadRequest + | ListTasksRequest + | CancelTaskRequest +) + + +class ClientRequest(RootModel[ClientRequestType]): + pass + + +ClientNotificationType: TypeAlias = ( + CancelledNotification + | ProgressNotification + | InitializedNotification + | RootsListChangedNotification + | TaskStatusNotification +) + + +class ClientNotification(RootModel[ClientNotificationType]): + pass + + +# Type for elicitation schema - a JSON Schema dict +ElicitRequestedSchema: TypeAlias = dict[str, Any] +"""Schema for elicitation requests.""" + + +class ElicitRequestFormParams(RequestParams): + """Parameters for form mode elicitation requests. + + Form mode collects non-sensitive information from the user via an in-band form + rendered by the client. + """ + + mode: Literal["form"] = "form" + """The elicitation mode (always "form" for this type).""" + + message: str + """The message to present to the user describing what information is being requested.""" + + requestedSchema: ElicitRequestedSchema + """ + A restricted subset of JSON Schema defining the structure of expected response. + Only top-level properties are allowed, without nesting. + """ + + model_config = ConfigDict(extra="allow") + + +class ElicitRequestURLParams(RequestParams): + """Parameters for URL mode elicitation requests. + + URL mode directs users to external URLs for sensitive out-of-band interactions + like OAuth flows, credential collection, or payment processing. + """ + + mode: Literal["url"] = "url" + """The elicitation mode (always "url" for this type).""" + + message: str + """The message to present to the user explaining why the interaction is needed.""" + + url: str + """The URL that the user should navigate to.""" + + elicitationId: str + """ + The ID of the elicitation, which must be unique within the context of the server. + The client MUST treat this ID as an opaque value. + """ + + model_config = ConfigDict(extra="allow") + + +# Union type for elicitation request parameters +ElicitRequestParams: TypeAlias = ElicitRequestURLParams | ElicitRequestFormParams +"""Parameters for elicitation requests - either form or URL mode.""" + + +class ElicitRequest(Request[ElicitRequestParams, Literal["elicitation/create"]]): + """A request from the server to elicit information from the client.""" + + method: Literal["elicitation/create"] = "elicitation/create" + params: ElicitRequestParams + + +class ElicitResult(Result): + """The client's response to an elicitation request.""" + + action: Literal["accept", "decline", "cancel"] + """ + The user action in response to the elicitation. + - "accept": User submitted the form/confirmed the action (or consented to URL navigation) + - "decline": User explicitly declined the action + - "cancel": User dismissed without making an explicit choice + """ + + content: dict[str, str | int | float | bool | list[str] | None] | None = None + """ + The submitted form data, only present when action is "accept" in form mode. + Contains values matching the requested schema. Values can be strings, integers, + booleans, or arrays of strings. + For URL mode, this field is omitted. + """ + + +class ElicitationRequiredErrorData(BaseModel): + """Error data for URLElicitationRequiredError. + + Servers return this when a request cannot be processed until one or more + URL mode elicitations are completed. + """ + + elicitations: list[ElicitRequestURLParams] + """List of URL mode elicitations that must be completed.""" + + model_config = ConfigDict(extra="allow") + + +ClientResultType: TypeAlias = ( + EmptyResult + | CreateMessageResult + | CreateMessageResultWithTools + | ListRootsResult + | ElicitResult + | GetTaskResult + | GetTaskPayloadResult + | ListTasksResult + | CancelTaskResult + | CreateTaskResult +) + + +class ClientResult(RootModel[ClientResultType]): + pass + + +ServerRequestType: TypeAlias = ( + PingRequest + | CreateMessageRequest + | ListRootsRequest + | ElicitRequest + | GetTaskRequest + | GetTaskPayloadRequest + | ListTasksRequest + | CancelTaskRequest +) + + +class ServerRequest(RootModel[ServerRequestType]): + pass + + +ServerNotificationType: TypeAlias = ( + CancelledNotification + | ProgressNotification + | LoggingMessageNotification + | ResourceUpdatedNotification + | ResourceListChangedNotification + | ToolListChangedNotification + | PromptListChangedNotification + | ElicitCompleteNotification + | TaskStatusNotification +) + + +class ServerNotification(RootModel[ServerNotificationType]): + pass + + +ServerResultType: TypeAlias = ( + EmptyResult + | InitializeResult + | CompleteResult + | GetPromptResult + | ListPromptsResult + | ListResourcesResult + | ListResourceTemplatesResult + | ReadResourceResult + | CallToolResult + | ListToolsResult + | GetTaskResult + | GetTaskPayloadResult + | ListTasksResult + | CancelTaskResult + | CreateTaskResult +) + + +class ServerResult(RootModel[ServerResultType]): + pass diff --git a/source/target-vendors/cp312/multipart/__init__.py b/source/target-vendors/cp312/multipart/__init__.py new file mode 100644 index 0000000..53c08d2 --- /dev/null +++ b/source/target-vendors/cp312/multipart/__init__.py @@ -0,0 +1,38 @@ +# Copyright 2012, Andrew Dunham +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This only works if using a file system, other loaders not implemented. + +import importlib.util +import sys +import warnings +from pathlib import Path + +for p in sys.path: + file_path = Path(p, "multipart.py") + try: + if file_path.is_file(): + spec = importlib.util.spec_from_file_location("multipart", file_path) + assert spec is not None, f"{file_path} found but not loadable!" + module = importlib.util.module_from_spec(spec) + sys.modules["multipart"] = module + assert spec.loader is not None, f"{file_path} must be loadable!" + spec.loader.exec_module(module) + break + except PermissionError: + pass +else: + warnings.warn("Please use `import python_multipart` instead.", PendingDeprecationWarning, stacklevel=2) + from python_multipart import * + from python_multipart import __all__, __version__ diff --git a/source/target-vendors/cp312/multipart/decoders.py b/source/target-vendors/cp312/multipart/decoders.py new file mode 100644 index 0000000..31acdfb --- /dev/null +++ b/source/target-vendors/cp312/multipart/decoders.py @@ -0,0 +1 @@ +from python_multipart.decoders import * diff --git a/source/target-vendors/cp312/multipart/exceptions.py b/source/target-vendors/cp312/multipart/exceptions.py new file mode 100644 index 0000000..36815d1 --- /dev/null +++ b/source/target-vendors/cp312/multipart/exceptions.py @@ -0,0 +1 @@ +from python_multipart.exceptions import * diff --git a/source/target-vendors/cp312/multipart/multipart.py b/source/target-vendors/cp312/multipart/multipart.py new file mode 100644 index 0000000..7bf567d --- /dev/null +++ b/source/target-vendors/cp312/multipart/multipart.py @@ -0,0 +1 @@ +from python_multipart.multipart import * diff --git a/source/target-vendors/cp312/pycparser-3.0.dist-info/METADATA b/source/target-vendors/cp312/pycparser-3.0.dist-info/METADATA new file mode 100644 index 0000000..b94a88b --- /dev/null +++ b/source/target-vendors/cp312/pycparser-3.0.dist-info/METADATA @@ -0,0 +1,244 @@ +Metadata-Version: 2.4 +Name: pycparser +Version: 3.0 +Summary: C parser in Python +Author-email: Eli Bendersky +Maintainer-email: Eli Bendersky +License-Expression: BSD-3-Clause +Project-URL: Homepage, https://github.com/eliben/pycparser +Classifier: Development Status :: 5 - Production/Stable +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Requires-Python: >=3.10 +Description-Content-Type: text/x-rst +License-File: LICENSE +Dynamic: license-file + +=============== +pycparser v3.00 +=============== + + +.. image:: https://github.com/eliben/pycparser/workflows/pycparser-tests/badge.svg + :align: center + :target: https://github.com/eliben/pycparser/actions + +---- + +.. contents:: + :backlinks: none + +.. sectnum:: + +Introduction +============ + +What is pycparser? +------------------ + +**pycparser** is a parser for the C language, written in pure Python. It is a +module designed to be easily integrated into applications that need to parse +C source code. + +What is it good for? +-------------------- + +Anything that needs C code to be parsed. The following are some uses for +**pycparser**, taken from real user reports: + +* C code obfuscator +* Front-end for various specialized C compilers +* Static code checker +* Automatic unit-test discovery +* Adding specialized extensions to the C language + +One of the most popular uses of **pycparser** is in the `cffi +`_ library, which uses it to parse the +declarations of C functions and types in order to auto-generate FFIs. + +**pycparser** is unique in the sense that it's written in pure Python - a very +high level language that's easy to experiment with and tweak. To people familiar +with Lex and Yacc, **pycparser**'s code will be simple to understand. It also +has no external dependencies (except for a Python interpreter), making it very +simple to install and deploy. + +Which version of C does pycparser support? +------------------------------------------ + +**pycparser** aims to support the full C99 language (according to the standard +ISO/IEC 9899). Some features from C11 are also supported, and patches to support +more are welcome. + +**pycparser** supports very few GCC extensions, but it's fairly easy to set +things up so that it parses code with a lot of GCC-isms successfully. See the +`FAQ `_ for more details. + +What grammar does pycparser follow? +----------------------------------- + +**pycparser** very closely follows the C grammar provided in Annex A of the C99 +standard (ISO/IEC 9899). + +How is pycparser licensed? +-------------------------- + +`BSD license `_. + +Contact details +--------------- + +For reporting problems with **pycparser** or submitting feature requests, please +open an `issue `_, or submit a +pull request. + + +Installing +========== + +Prerequisites +------------- + +**pycparser** is being tested with modern versions of Python on +Linux, macOS and Windows. See `the CI dashboard `__ +for details. + +**pycparser** has no external dependencies. + +Installation process +-------------------- + +The recommended way to install **pycparser** is with ``pip``:: + + > pip install pycparser + +Using +===== + +Interaction with the C preprocessor +----------------------------------- + +In order to be compilable, C code must be preprocessed by the C preprocessor - +``cpp``. A compatible ``cpp`` handles preprocessing directives like ``#include`` and +``#define``, removes comments, and performs other minor tasks that prepare the C +code for compilation. + +For all but the most trivial snippets of C code **pycparser**, like a C +compiler, must receive preprocessed C code in order to function correctly. If +you import the top-level ``parse_file`` function from the **pycparser** package, +it will interact with ``cpp`` for you, as long as it's in your PATH, or you +provide a path to it. + +Note also that you can use ``gcc -E`` or ``clang -E`` instead of ``cpp``. See +the ``using_gcc_E_libc.py`` example for more details. Windows users can download +and install a binary build of Clang for Windows `from this website +`_. + +What about the standard C library headers? +------------------------------------------ + +C code almost always ``#include``\s various header files from the standard C +library, like ``stdio.h``. While (with some effort) **pycparser** can be made to +parse the standard headers from any C compiler, it's much simpler to use the +provided "fake" standard includes for C11 in ``utils/fake_libc_include``. These +are standard C header files that contain only the bare necessities to allow +valid parsing of the files that use them. As a bonus, since they're minimal, it +can significantly improve the performance of parsing large C files. + +The key point to understand here is that **pycparser** doesn't really care about +the semantics of types. It only needs to know whether some token encountered in +the source is a previously defined type. This is essential in order to be able +to parse C correctly. + +See `this blog post +`_ +for more details. + +Note that the fake headers are not included in the ``pip`` package nor installed +via the package build (`#224 `_). + +Basic usage +----------- + +Take a look at the |examples|_ directory of the distribution for a few examples +of using **pycparser**. These should be enough to get you started. Please note +that most realistic C code samples would require running the C preprocessor +before passing the code to **pycparser**; see the previous sections for more +details. + +.. |examples| replace:: ``examples`` +.. _examples: examples + + +Advanced usage +-------------- + +The public interface of **pycparser** is well documented with comments in +``pycparser/c_parser.py``. For a detailed overview of the various AST nodes +created by the parser, see ``pycparser/_c_ast.cfg``. + +There's also a `FAQ available here `_. +In any case, you can always drop me an `email `_ for help. + + +Modifying +========= + +There are a few points to keep in mind when modifying **pycparser**: + +* The code for **pycparser**'s AST nodes is automatically generated from a + configuration file - ``_c_ast.cfg``, by ``_ast_gen.py``. If you modify the AST + configuration, make sure to re-generate the code. This can be done by running + the ``_ast_gen.py`` script (from the repository root or the + ``pycparser`` directory). +* Read the docstring in the constructor of the ``CParser`` class for details + on configuration and compatibility arguments. + + +Package contents +================ + +Once you unzip the ``pycparser`` package, you'll see the following files and +directories: + +README.rst: + This README file. + +LICENSE: + The pycparser license + +setup.py: + Legacy installation script (build metadata lives in ``pyproject.toml``). + +pyproject.toml: + Package metadata and build configuration. + +examples/: + A directory with some examples of using **pycparser** + +pycparser/: + The **pycparser** module source code. + +tests/: + Unit tests. + +utils/fake_libc_include: + Minimal standard C library include files that should allow to parse any C code. + Note that these headers now include C11 code, so they may not work when the + preprocessor is configured to an earlier C standard (like ``-std=c99``). + +utils/internal/: + Internal utilities for my own use. You probably don't need them. + + +Contributors +============ + +Some people have contributed to **pycparser** by opening issues on bugs they've +found and/or submitting patches. The list of contributors is in the CONTRIBUTORS +file in the source distribution. After **pycparser** moved to Github I stopped +updating this list because Github does a much better job at tracking +contributions. diff --git a/source/target-vendors/cp312/pycparser-3.0.dist-info/RECORD b/source/target-vendors/cp312/pycparser-3.0.dist-info/RECORD new file mode 100644 index 0000000..b3383a3 --- /dev/null +++ b/source/target-vendors/cp312/pycparser-3.0.dist-info/RECORD @@ -0,0 +1,13 @@ +pycparser/__init__.py,sha256=phViRyAuUmgqE4kNmaCqpm5WVEBIvzUSFapBv4XX3xo,2829 +pycparser/_ast_gen.py,sha256=ExH5Ym4pk7dQPEIkQr9RJim5feztdBQwSBPvpvE-5BM,11292 +pycparser/_c_ast.cfg,sha256=ld5ezE9yzIJFIVAUfw7ezJSlMi4nXKNCzfmqjOyQTNo,4255 +pycparser/ast_transforms.py,sha256=XwMsarc5aDddNWgIiKm4-jOWMRYib96yNQUo0_u28WA,5899 +pycparser/c_ast.py,sha256=uwkcZWHfXDQIw6WDvCL17iWM_-0R-URDqEMmPjXLOAc,32954 +pycparser/c_generator.py,sha256=RVKJPguv2CvovHHSfQkqimckUr9wGU9PofxCGj251QA,20661 +pycparser/c_lexer.py,sha256=B1VoqbYhPWkOJJWCem4OY4zj0IxrPktBZZFe2Y87kUg,25155 +pycparser/c_parser.py,sha256=3FBKGLjjlC3v8afwD_cnMR67ImoYIy73a4WKQR6hX7g,89798 +pycparser-3.0.dist-info/licenses/LICENSE,sha256=DIRjmTaep23de1xE_m0WSXQV_PAV9cu1CMJL-YuBxbE,1543 +pycparser-3.0.dist-info/METADATA,sha256=9UemCwq1TMLyQiE9H4eWCtgN991d_rmNF6RE1Iv7a5M,8229 +pycparser-3.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92 +pycparser-3.0.dist-info/top_level.txt,sha256=c-lPcS74L_8KoH7IE6PQF5ofyirRQNV4VhkbSFIPeWM,10 +pycparser-3.0.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/pycparser-3.0.dist-info/WHEEL b/source/target-vendors/cp312/pycparser-3.0.dist-info/WHEEL new file mode 100644 index 0000000..fbbd86c --- /dev/null +++ b/source/target-vendors/cp312/pycparser-3.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (80.10.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/source/target-vendors/cp312/pycparser-3.0.dist-info/licenses/LICENSE b/source/target-vendors/cp312/pycparser-3.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000..bee14a4 --- /dev/null +++ b/source/target-vendors/cp312/pycparser-3.0.dist-info/licenses/LICENSE @@ -0,0 +1,27 @@ +pycparser -- A C parser in Python + +Copyright (c) 2008-2022, Eli Bendersky +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of the copyright holder nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/source/target-vendors/cp312/pycparser-3.0.dist-info/top_level.txt b/source/target-vendors/cp312/pycparser-3.0.dist-info/top_level.txt new file mode 100644 index 0000000..dc1c9e1 --- /dev/null +++ b/source/target-vendors/cp312/pycparser-3.0.dist-info/top_level.txt @@ -0,0 +1 @@ +pycparser diff --git a/source/target-vendors/cp312/pycparser/__init__.py b/source/target-vendors/cp312/pycparser/__init__.py new file mode 100644 index 0000000..0606c03 --- /dev/null +++ b/source/target-vendors/cp312/pycparser/__init__.py @@ -0,0 +1,99 @@ +# ----------------------------------------------------------------- +# pycparser: __init__.py +# +# This package file exports some convenience functions for +# interacting with pycparser +# +# Eli Bendersky [https://eli.thegreenplace.net/] +# License: BSD +# ----------------------------------------------------------------- +__all__ = ["c_lexer", "c_parser", "c_ast"] +__version__ = "3.00" + +import io +from subprocess import check_output + +from . import c_parser + +CParser = c_parser.CParser + + +def preprocess_file(filename, cpp_path="cpp", cpp_args=""): + """Preprocess a file using cpp. + + filename: + Name of the file you want to preprocess. + + cpp_path: + cpp_args: + Refer to the documentation of parse_file for the meaning of these + arguments. + + When successful, returns the preprocessed file's contents. + Errors from cpp will be printed out. + """ + path_list = [cpp_path] + if isinstance(cpp_args, list): + path_list += cpp_args + elif cpp_args != "": + path_list += [cpp_args] + path_list += [filename] + + try: + # Note the use of universal_newlines to treat all newlines + # as \n for Python's purpose + text = check_output(path_list, universal_newlines=True) + except OSError as e: + raise RuntimeError( + "Unable to invoke 'cpp'. " + + "Make sure its path was passed correctly\n" + + f"Original error: {e}" + ) + + return text + + +def parse_file( + filename, use_cpp=False, cpp_path="cpp", cpp_args="", parser=None, encoding=None +): + """Parse a C file using pycparser. + + filename: + Name of the file you want to parse. + + use_cpp: + Set to True if you want to execute the C pre-processor + on the file prior to parsing it. + + cpp_path: + If use_cpp is True, this is the path to 'cpp' on your + system. If no path is provided, it attempts to just + execute 'cpp', so it must be in your PATH. + + cpp_args: + If use_cpp is True, set this to the command line arguments strings + to cpp. Be careful with quotes - it's best to pass a raw string + (r'') here. For example: + r'-I../utils/fake_libc_include' + If several arguments are required, pass a list of strings. + + encoding: + Encoding to use for the file to parse + + parser: + Optional parser object to be used instead of the default CParser + + When successful, an AST is returned. ParseError can be + thrown if the file doesn't parse successfully. + + Errors from cpp will be printed out. + """ + if use_cpp: + text = preprocess_file(filename, cpp_path, cpp_args) + else: + with io.open(filename, encoding=encoding) as f: + text = f.read() + + if parser is None: + parser = CParser() + return parser.parse(text, filename) diff --git a/source/target-vendors/cp312/pycparser/_ast_gen.py b/source/target-vendors/cp312/pycparser/_ast_gen.py new file mode 100644 index 0000000..6df0b6a --- /dev/null +++ b/source/target-vendors/cp312/pycparser/_ast_gen.py @@ -0,0 +1,355 @@ +# ----------------------------------------------------------------- +# _ast_gen.py +# +# Generates the AST Node classes from a specification given in +# a configuration file. This module can also be run as a script to +# regenerate c_ast.py from _c_ast.cfg (from the repo root or the +# pycparser/ directory). Use 'make check' to reformat the generated +# file after running this script. +# +# The design of this module was inspired by astgen.py from the +# Python 2.5 code-base. +# +# Eli Bendersky [https://eli.thegreenplace.net/] +# License: BSD +# ----------------------------------------------------------------- +from string import Template +import os +from typing import IO + + +class ASTCodeGenerator: + def __init__(self, cfg_filename="_c_ast.cfg"): + """Initialize the code generator from a configuration + file. + """ + self.cfg_filename = cfg_filename + self.node_cfg = [ + NodeCfg(name, contents) + for (name, contents) in self.parse_cfgfile(cfg_filename) + ] + + def generate(self, file: IO[str]) -> None: + """Generates the code into file, an open file buffer.""" + src = Template(_PROLOGUE_COMMENT).substitute(cfg_filename=self.cfg_filename) + + src += _PROLOGUE_CODE + for node_cfg in self.node_cfg: + src += node_cfg.generate_source() + "\n\n" + + file.write(src) + + def parse_cfgfile(self, filename): + """Parse the configuration file and yield pairs of + (name, contents) for each node. + """ + with open(filename, "r") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + colon_i = line.find(":") + lbracket_i = line.find("[") + rbracket_i = line.find("]") + if colon_i < 1 or lbracket_i <= colon_i or rbracket_i <= lbracket_i: + raise RuntimeError(f"Invalid line in {filename}:\n{line}\n") + + name = line[:colon_i] + val = line[lbracket_i + 1 : rbracket_i] + vallist = [v.strip() for v in val.split(",")] if val else [] + yield name, vallist + + +class NodeCfg: + """Node configuration. + + name: node name + contents: a list of contents - attributes and child nodes + See comment at the top of the configuration file for details. + """ + + def __init__(self, name, contents): + self.name = name + self.all_entries = [] + self.attr = [] + self.child = [] + self.seq_child = [] + + for entry in contents: + clean_entry = entry.rstrip("*") + self.all_entries.append(clean_entry) + + if entry.endswith("**"): + self.seq_child.append(clean_entry) + elif entry.endswith("*"): + self.child.append(clean_entry) + else: + self.attr.append(entry) + + def generate_source(self): + src = self._gen_init() + src += "\n" + self._gen_children() + src += "\n" + self._gen_iter() + src += "\n" + self._gen_attr_names() + return src + + def _gen_init(self): + src = f"class {self.name}(Node):\n" + + if self.all_entries: + args = ", ".join(self.all_entries) + slots = ", ".join(f"'{e}'" for e in self.all_entries) + slots += ", 'coord', '__weakref__'" + arglist = f"(self, {args}, coord=None)" + else: + slots = "'coord', '__weakref__'" + arglist = "(self, coord=None)" + + src += f" __slots__ = ({slots})\n" + src += f" def __init__{arglist}:\n" + + for name in self.all_entries + ["coord"]: + src += f" self.{name} = {name}\n" + + return src + + def _gen_children(self): + src = " def children(self):\n" + + if self.all_entries: + src += " nodelist = []\n" + + for child in self.child: + src += f" if self.{child} is not None:\n" + src += f' nodelist.append(("{child}", self.{child}))\n' + + for seq_child in self.seq_child: + src += f" for i, child in enumerate(self.{seq_child} or []):\n" + src += f' nodelist.append((f"{seq_child}[{{i}}]", child))\n' + + src += " return tuple(nodelist)\n" + else: + src += " return ()\n" + + return src + + def _gen_iter(self): + src = " def __iter__(self):\n" + + if self.all_entries: + for child in self.child: + src += f" if self.{child} is not None:\n" + src += f" yield self.{child}\n" + + for seq_child in self.seq_child: + src += f" for child in (self.{seq_child} or []):\n" + src += " yield child\n" + + if not (self.child or self.seq_child): + # Empty generator + src += " return\n" + " yield\n" + else: + # Empty generator + src += " return\n" + " yield\n" + + return src + + def _gen_attr_names(self): + src = " attr_names = (" + "".join(f"{nm!r}, " for nm in self.attr) + ")" + return src + + +_PROLOGUE_COMMENT = r"""#----------------------------------------------------------------- +# ** ATTENTION ** +# This code was automatically generated from _c_ast.cfg +# +# Do not modify it directly. Modify the configuration file and +# run the generator again. +# ** ** *** ** ** +# +# pycparser: c_ast.py +# +# AST Node classes. +# +# Eli Bendersky [https://eli.thegreenplace.net/] +# License: BSD +#----------------------------------------------------------------- + +""" +_PROLOGUE_CODE = r''' +import sys +from typing import Any, ClassVar, IO, Optional + +def _repr(obj): + """ + Get the representation of an object, with dedicated pprint-like format for lists. + """ + if isinstance(obj, list): + return '[' + (',\n '.join((_repr(e).replace('\n', '\n ') for e in obj))) + '\n]' + else: + return repr(obj) + +class Node: + __slots__ = () + """ Abstract base class for AST nodes. + """ + attr_names: ClassVar[tuple[str, ...]] = () + coord: Optional[Any] + def __repr__(self): + """ Generates a python representation of the current node + """ + result = self.__class__.__name__ + '(' + + indent = '' + separator = '' + for name in self.__slots__[:-2]: + result += separator + result += indent + result += name + '=' + (_repr(getattr(self, name)).replace('\n', '\n ' + (' ' * (len(name) + len(self.__class__.__name__))))) + + separator = ',' + indent = '\n ' + (' ' * len(self.__class__.__name__)) + + result += indent + ')' + + return result + + def children(self): + """ A sequence of all children that are Nodes + """ + pass + + def show( + self, + buf: IO[str] = sys.stdout, + offset: int = 0, + attrnames: bool = False, + showemptyattrs: bool = True, + nodenames: bool = False, + showcoord: bool = False, + _my_node_name: Optional[str] = None, + ): + """ Pretty print the Node and all its attributes and + children (recursively) to a buffer. + + buf: + Open IO buffer into which the Node is printed. + + offset: + Initial offset (amount of leading spaces) + + attrnames: + True if you want to see the attribute names in + name=value pairs. False to only see the values. + + showemptyattrs: + False if you want to suppress printing empty attributes. + + nodenames: + True if you want to see the actual node names + within their parents. + + showcoord: + Do you want the coordinates of each Node to be + displayed. + """ + lead = ' ' * offset + if nodenames and _my_node_name is not None: + buf.write(lead + self.__class__.__name__+ ' <' + _my_node_name + '>: ') + else: + buf.write(lead + self.__class__.__name__+ ': ') + + if self.attr_names: + def is_empty(v): + v is None or (hasattr(v, '__len__') and len(v) == 0) + nvlist = [(n, getattr(self,n)) for n in self.attr_names \ + if showemptyattrs or not is_empty(getattr(self,n))] + if attrnames: + attrstr = ', '.join(f'{name}={value}' for name, value in nvlist) + else: + attrstr = ', '.join(f'{value}' for _, value in nvlist) + buf.write(attrstr) + + if showcoord: + buf.write(f' (at {self.coord})') + buf.write('\n') + + for (child_name, child) in self.children(): + child.show( + buf, + offset=offset + 2, + attrnames=attrnames, + showemptyattrs=showemptyattrs, + nodenames=nodenames, + showcoord=showcoord, + _my_node_name=child_name) + + +class NodeVisitor: + """ A base NodeVisitor class for visiting c_ast nodes. + Subclass it and define your own visit_XXX methods, where + XXX is the class name you want to visit with these + methods. + + For example: + + class ConstantVisitor(NodeVisitor): + def __init__(self): + self.values = [] + + def visit_Constant(self, node): + self.values.append(node.value) + + Creates a list of values of all the constant nodes + encountered below the given node. To use it: + + cv = ConstantVisitor() + cv.visit(node) + + Notes: + + * generic_visit() will be called for AST nodes for which + no visit_XXX method was defined. + * The children of nodes for which a visit_XXX was + defined will not be visited - if you need this, call + generic_visit() on the node. + You can use: + NodeVisitor.generic_visit(self, node) + * Modeled after Python's own AST visiting facilities + (the ast module of Python 3.0) + """ + + _method_cache = None + + def visit(self, node: Node): + """ Visit a node. + """ + + if self._method_cache is None: + self._method_cache = {} + + visitor = self._method_cache.get(node.__class__.__name__, None) + if visitor is None: + method = 'visit_' + node.__class__.__name__ + visitor = getattr(self, method, self.generic_visit) + self._method_cache[node.__class__.__name__] = visitor + + return visitor(node) + + def generic_visit(self, node: Node): + """ Called if no explicit visitor function exists for a + node. Implements preorder visiting of the node. + """ + for _, c in node.children(): + self.visit(c) + +''' + + +if __name__ == "__main__": + base_dir = os.path.dirname(os.path.abspath(__file__)) + cfg_path = os.path.join(base_dir, "_c_ast.cfg") + out_path = os.path.join(base_dir, "c_ast.py") + ast_gen = ASTCodeGenerator(cfg_path) + with open(out_path, "w") as out: + ast_gen.generate(out) diff --git a/source/target-vendors/cp312/pycparser/_c_ast.cfg b/source/target-vendors/cp312/pycparser/_c_ast.cfg new file mode 100644 index 0000000..0626533 --- /dev/null +++ b/source/target-vendors/cp312/pycparser/_c_ast.cfg @@ -0,0 +1,195 @@ +#----------------------------------------------------------------- +# pycparser: _c_ast.cfg +# +# Defines the AST Node classes used in pycparser. +# +# Each entry is a Node sub-class name, listing the attributes +# and child nodes of the class: +# * - a child node +# ** - a sequence of child nodes +# - an attribute +# +# Eli Bendersky [https://eli.thegreenplace.net/] +# License: BSD +#----------------------------------------------------------------- + +# ArrayDecl is a nested declaration of an array with the given type. +# dim: the dimension (for example, constant 42) +# dim_quals: list of dimension qualifiers, to support C99's allowing 'const' +# and 'static' within the array dimension in function declarations. +ArrayDecl: [type*, dim*, dim_quals] + +ArrayRef: [name*, subscript*] + +# op: =, +=, /= etc. +# +Assignment: [op, lvalue*, rvalue*] + +Alignas: [alignment*] + +BinaryOp: [op, left*, right*] + +Break: [] + +Case: [expr*, stmts**] + +Cast: [to_type*, expr*] + +# Compound statement in C99 is a list of block items (declarations or +# statements). +# +Compound: [block_items**] + +# Compound literal (anonymous aggregate) for C99. +# (type-name) {initializer_list} +# type: the typename +# init: InitList for the initializer list +# +CompoundLiteral: [type*, init*] + +# type: int, char, float, string, etc. +# +Constant: [type, value] + +Continue: [] + +# name: the variable being declared +# quals: list of qualifiers (const, volatile) +# funcspec: list function specifiers (i.e. inline in C99) +# storage: list of storage specifiers (extern, register, etc.) +# type: declaration type (probably nested with all the modifiers) +# init: initialization value, or None +# bitsize: bit field size, or None +# +Decl: [name, quals, align, storage, funcspec, type*, init*, bitsize*] + +DeclList: [decls**] + +Default: [stmts**] + +DoWhile: [cond*, stmt*] + +# Represents the ellipsis (...) parameter in a function +# declaration +# +EllipsisParam: [] + +# An empty statement (a semicolon ';' on its own) +# +EmptyStatement: [] + +# Enumeration type specifier +# name: an optional ID +# values: an EnumeratorList +# +Enum: [name, values*] + +# A name/value pair for enumeration values +# +Enumerator: [name, value*] + +# A list of enumerators +# +EnumeratorList: [enumerators**] + +# A list of expressions separated by the comma operator. +# +ExprList: [exprs**] + +# This is the top of the AST, representing a single C file (a +# translation unit in K&R jargon). It contains a list of +# "external-declaration"s, which is either declarations (Decl), +# Typedef or function definitions (FuncDef). +# +FileAST: [ext**] + +# for (init; cond; next) stmt +# +For: [init*, cond*, next*, stmt*] + +# name: Id +# args: ExprList +# +FuncCall: [name*, args*] + +# type (args) +# +FuncDecl: [args*, type*] + +# Function definition: a declarator for the function name and +# a body, which is a compound statement. +# There's an optional list of parameter declarations for old +# K&R-style definitions +# +FuncDef: [decl*, param_decls**, body*] + +Goto: [name] + +ID: [name] + +# Holder for types that are a simple identifier (e.g. the built +# ins void, char etc. and typedef-defined types) +# +IdentifierType: [names] + +If: [cond*, iftrue*, iffalse*] + +# An initialization list used for compound literals. +# +InitList: [exprs**] + +Label: [name, stmt*] + +# A named initializer for C99. +# The name of a NamedInitializer is a sequence of Nodes, because +# names can be hierarchical and contain constant expressions. +# +NamedInitializer: [name**, expr*] + +# a list of comma separated function parameter declarations +# +ParamList: [params**] + +PtrDecl: [quals, type*] + +Return: [expr*] + +StaticAssert: [cond*, message*] + +# name: struct tag name +# decls: declaration of members +# +Struct: [name, decls**] + +# type: . or -> +# name.field or name->field +# +StructRef: [name*, type, field*] + +Switch: [cond*, stmt*] + +# cond ? iftrue : iffalse +# +TernaryOp: [cond*, iftrue*, iffalse*] + +# A base type declaration +# +TypeDecl: [declname, quals, align, type*] + +# A typedef declaration. +# Very similar to Decl, but without some attributes +# +Typedef: [name, quals, storage, type*] + +Typename: [name, quals, align, type*] + +UnaryOp: [op, expr*] + +# name: union tag name +# decls: declaration of members +# +Union: [name, decls**] + +While: [cond*, stmt*] + +Pragma: [string] diff --git a/source/target-vendors/cp312/pycparser/ast_transforms.py b/source/target-vendors/cp312/pycparser/ast_transforms.py new file mode 100644 index 0000000..1051737 --- /dev/null +++ b/source/target-vendors/cp312/pycparser/ast_transforms.py @@ -0,0 +1,174 @@ +# ------------------------------------------------------------------------------ +# pycparser: ast_transforms.py +# +# Some utilities used by the parser to create a friendlier AST. +# +# Eli Bendersky [https://eli.thegreenplace.net/] +# License: BSD +# ------------------------------------------------------------------------------ + +from typing import Any, List, Tuple, cast + +from . import c_ast + + +def fix_switch_cases(switch_node: c_ast.Switch) -> c_ast.Switch: + """The 'case' statements in a 'switch' come out of parsing with one + child node, so subsequent statements are just tucked to the parent + Compound. Additionally, consecutive (fall-through) case statements + come out messy. This is a peculiarity of the C grammar. The following: + + switch (myvar) { + case 10: + k = 10; + p = k + 1; + return 10; + case 20: + case 30: + return 20; + default: + break; + } + + Creates this tree (pseudo-dump): + + Switch + ID: myvar + Compound: + Case 10: + k = 10 + p = k + 1 + return 10 + Case 20: + Case 30: + return 20 + Default: + break + + The goal of this transform is to fix this mess, turning it into the + following: + + Switch + ID: myvar + Compound: + Case 10: + k = 10 + p = k + 1 + return 10 + Case 20: + Case 30: + return 20 + Default: + break + + A fixed AST node is returned. The argument may be modified. + """ + assert isinstance(switch_node, c_ast.Switch) + if not isinstance(switch_node.stmt, c_ast.Compound): + return switch_node + + # The new Compound child for the Switch, which will collect children in the + # correct order + new_compound = c_ast.Compound([], switch_node.stmt.coord) + + # The last Case/Default node + last_case: c_ast.Case | c_ast.Default | None = None + + # Goes over the children of the Compound below the Switch, adding them + # either directly below new_compound or below the last Case as appropriate + # (for `switch(cond) {}`, block_items would have been None) + for child in switch_node.stmt.block_items or []: + if isinstance(child, (c_ast.Case, c_ast.Default)): + # If it's a Case/Default: + # 1. Add it to the Compound and mark as "last case" + # 2. If its immediate child is also a Case or Default, promote it + # to a sibling. + new_compound.block_items.append(child) + _extract_nested_case(child, new_compound.block_items) + last_case = new_compound.block_items[-1] + else: + # Other statements are added as children to the last case, if it + # exists. + if last_case is None: + new_compound.block_items.append(child) + else: + last_case.stmts.append(child) + + switch_node.stmt = new_compound + return switch_node + + +def _extract_nested_case( + case_node: c_ast.Case | c_ast.Default, stmts_list: List[c_ast.Node] +) -> None: + """Recursively extract consecutive Case statements that are made nested + by the parser and add them to the stmts_list. + """ + if isinstance(case_node.stmts[0], (c_ast.Case, c_ast.Default)): + nested = case_node.stmts.pop() + stmts_list.append(nested) + _extract_nested_case(cast(Any, nested), stmts_list) + + +def fix_atomic_specifiers( + decl: c_ast.Decl | c_ast.Typedef, +) -> c_ast.Decl | c_ast.Typedef: + """Atomic specifiers like _Atomic(type) are unusually structured, + conferring a qualifier upon the contained type. + + This function fixes a decl with atomic specifiers to have a sane AST + structure, by removing spurious Typename->TypeDecl pairs and attaching + the _Atomic qualifier in the right place. + """ + # There can be multiple levels of _Atomic in a decl; fix them until a + # fixed point is reached. + while True: + decl, found = _fix_atomic_specifiers_once(decl) + if not found: + break + + # Make sure to add an _Atomic qual on the topmost decl if needed. Also + # restore the declname on the innermost TypeDecl (it gets placed in the + # wrong place during construction). + typ: Any = decl + while not isinstance(typ, c_ast.TypeDecl): + try: + typ = typ.type + except AttributeError: + return decl + if "_Atomic" in typ.quals and "_Atomic" not in decl.quals: + decl.quals.append("_Atomic") + if typ.declname is None: + typ.declname = decl.name + + return decl + + +def _fix_atomic_specifiers_once( + decl: c_ast.Decl | c_ast.Typedef, +) -> Tuple[c_ast.Decl | c_ast.Typedef, bool]: + """Performs one 'fix' round of atomic specifiers. + Returns (modified_decl, found) where found is True iff a fix was made. + """ + parent: Any = decl + grandparent: Any = None + node: Any = decl.type + while node is not None: + if isinstance(node, c_ast.Typename) and "_Atomic" in node.quals: + break + try: + grandparent = parent + parent = node + node = node.type + except AttributeError: + # If we've reached a node without a `type` field, it means we won't + # find what we're looking for at this point; give up the search + # and return the original decl unmodified. + return decl, False + + assert isinstance(parent, c_ast.TypeDecl) + assert grandparent is not None + cast(Any, grandparent).type = node.type + if "_Atomic" not in node.type.quals: + node.type.quals.append("_Atomic") + return decl, True diff --git a/source/target-vendors/cp312/pycparser/c_ast.py b/source/target-vendors/cp312/pycparser/c_ast.py new file mode 100644 index 0000000..b6f42af --- /dev/null +++ b/source/target-vendors/cp312/pycparser/c_ast.py @@ -0,0 +1,1341 @@ +# ----------------------------------------------------------------- +# ** ATTENTION ** +# This code was automatically generated from _c_ast.cfg +# +# Do not modify it directly. Modify the configuration file and +# run the generator again. +# ** ** *** ** ** +# +# pycparser: c_ast.py +# +# AST Node classes. +# +# Eli Bendersky [https://eli.thegreenplace.net/] +# License: BSD +# ----------------------------------------------------------------- + + +import sys +from typing import Any, ClassVar, IO, Optional + + +def _repr(obj): + """ + Get the representation of an object, with dedicated pprint-like format for lists. + """ + if isinstance(obj, list): + return "[" + (",\n ".join((_repr(e).replace("\n", "\n ") for e in obj))) + "\n]" + else: + return repr(obj) + + +class Node: + __slots__ = () + """ Abstract base class for AST nodes. + """ + attr_names: ClassVar[tuple[str, ...]] = () + coord: Optional[Any] + + def __repr__(self): + """Generates a python representation of the current node""" + result = self.__class__.__name__ + "(" + + indent = "" + separator = "" + for name in self.__slots__[:-2]: + result += separator + result += indent + result += ( + name + + "=" + + ( + _repr(getattr(self, name)).replace( + "\n", + "\n " + (" " * (len(name) + len(self.__class__.__name__))), + ) + ) + ) + + separator = "," + indent = "\n " + (" " * len(self.__class__.__name__)) + + result += indent + ")" + + return result + + def children(self): + """A sequence of all children that are Nodes""" + pass + + def show( + self, + buf: IO[str] = sys.stdout, + offset: int = 0, + attrnames: bool = False, + showemptyattrs: bool = True, + nodenames: bool = False, + showcoord: bool = False, + _my_node_name: Optional[str] = None, + ): + """Pretty print the Node and all its attributes and + children (recursively) to a buffer. + + buf: + Open IO buffer into which the Node is printed. + + offset: + Initial offset (amount of leading spaces) + + attrnames: + True if you want to see the attribute names in + name=value pairs. False to only see the values. + + showemptyattrs: + False if you want to suppress printing empty attributes. + + nodenames: + True if you want to see the actual node names + within their parents. + + showcoord: + Do you want the coordinates of each Node to be + displayed. + """ + lead = " " * offset + if nodenames and _my_node_name is not None: + buf.write(lead + self.__class__.__name__ + " <" + _my_node_name + ">: ") + else: + buf.write(lead + self.__class__.__name__ + ": ") + + if self.attr_names: + + def is_empty(v): + v is None or (hasattr(v, "__len__") and len(v) == 0) + + nvlist = [ + (n, getattr(self, n)) + for n in self.attr_names + if showemptyattrs or not is_empty(getattr(self, n)) + ] + if attrnames: + attrstr = ", ".join(f"{name}={value}" for name, value in nvlist) + else: + attrstr = ", ".join(f"{value}" for _, value in nvlist) + buf.write(attrstr) + + if showcoord: + buf.write(f" (at {self.coord})") + buf.write("\n") + + for child_name, child in self.children(): + child.show( + buf, + offset=offset + 2, + attrnames=attrnames, + showemptyattrs=showemptyattrs, + nodenames=nodenames, + showcoord=showcoord, + _my_node_name=child_name, + ) + + +class NodeVisitor: + """A base NodeVisitor class for visiting c_ast nodes. + Subclass it and define your own visit_XXX methods, where + XXX is the class name you want to visit with these + methods. + + For example: + + class ConstantVisitor(NodeVisitor): + def __init__(self): + self.values = [] + + def visit_Constant(self, node): + self.values.append(node.value) + + Creates a list of values of all the constant nodes + encountered below the given node. To use it: + + cv = ConstantVisitor() + cv.visit(node) + + Notes: + + * generic_visit() will be called for AST nodes for which + no visit_XXX method was defined. + * The children of nodes for which a visit_XXX was + defined will not be visited - if you need this, call + generic_visit() on the node. + You can use: + NodeVisitor.generic_visit(self, node) + * Modeled after Python's own AST visiting facilities + (the ast module of Python 3.0) + """ + + _method_cache = None + + def visit(self, node: Node): + """Visit a node.""" + + if self._method_cache is None: + self._method_cache = {} + + visitor = self._method_cache.get(node.__class__.__name__, None) + if visitor is None: + method = "visit_" + node.__class__.__name__ + visitor = getattr(self, method, self.generic_visit) + self._method_cache[node.__class__.__name__] = visitor + + return visitor(node) + + def generic_visit(self, node: Node): + """Called if no explicit visitor function exists for a + node. Implements preorder visiting of the node. + """ + for _, c in node.children(): + self.visit(c) + + +class ArrayDecl(Node): + __slots__ = ("type", "dim", "dim_quals", "coord", "__weakref__") + + def __init__(self, type, dim, dim_quals, coord=None): + self.type = type + self.dim = dim + self.dim_quals = dim_quals + self.coord = coord + + def children(self): + nodelist = [] + if self.type is not None: + nodelist.append(("type", self.type)) + if self.dim is not None: + nodelist.append(("dim", self.dim)) + return tuple(nodelist) + + def __iter__(self): + if self.type is not None: + yield self.type + if self.dim is not None: + yield self.dim + + attr_names = ("dim_quals",) + + +class ArrayRef(Node): + __slots__ = ("name", "subscript", "coord", "__weakref__") + + def __init__(self, name, subscript, coord=None): + self.name = name + self.subscript = subscript + self.coord = coord + + def children(self): + nodelist = [] + if self.name is not None: + nodelist.append(("name", self.name)) + if self.subscript is not None: + nodelist.append(("subscript", self.subscript)) + return tuple(nodelist) + + def __iter__(self): + if self.name is not None: + yield self.name + if self.subscript is not None: + yield self.subscript + + attr_names = () + + +class Assignment(Node): + __slots__ = ("op", "lvalue", "rvalue", "coord", "__weakref__") + + def __init__(self, op, lvalue, rvalue, coord=None): + self.op = op + self.lvalue = lvalue + self.rvalue = rvalue + self.coord = coord + + def children(self): + nodelist = [] + if self.lvalue is not None: + nodelist.append(("lvalue", self.lvalue)) + if self.rvalue is not None: + nodelist.append(("rvalue", self.rvalue)) + return tuple(nodelist) + + def __iter__(self): + if self.lvalue is not None: + yield self.lvalue + if self.rvalue is not None: + yield self.rvalue + + attr_names = ("op",) + + +class Alignas(Node): + __slots__ = ("alignment", "coord", "__weakref__") + + def __init__(self, alignment, coord=None): + self.alignment = alignment + self.coord = coord + + def children(self): + nodelist = [] + if self.alignment is not None: + nodelist.append(("alignment", self.alignment)) + return tuple(nodelist) + + def __iter__(self): + if self.alignment is not None: + yield self.alignment + + attr_names = () + + +class BinaryOp(Node): + __slots__ = ("op", "left", "right", "coord", "__weakref__") + + def __init__(self, op, left, right, coord=None): + self.op = op + self.left = left + self.right = right + self.coord = coord + + def children(self): + nodelist = [] + if self.left is not None: + nodelist.append(("left", self.left)) + if self.right is not None: + nodelist.append(("right", self.right)) + return tuple(nodelist) + + def __iter__(self): + if self.left is not None: + yield self.left + if self.right is not None: + yield self.right + + attr_names = ("op",) + + +class Break(Node): + __slots__ = ("coord", "__weakref__") + + def __init__(self, coord=None): + self.coord = coord + + def children(self): + return () + + def __iter__(self): + return + yield + + attr_names = () + + +class Case(Node): + __slots__ = ("expr", "stmts", "coord", "__weakref__") + + def __init__(self, expr, stmts, coord=None): + self.expr = expr + self.stmts = stmts + self.coord = coord + + def children(self): + nodelist = [] + if self.expr is not None: + nodelist.append(("expr", self.expr)) + for i, child in enumerate(self.stmts or []): + nodelist.append((f"stmts[{i}]", child)) + return tuple(nodelist) + + def __iter__(self): + if self.expr is not None: + yield self.expr + for child in self.stmts or []: + yield child + + attr_names = () + + +class Cast(Node): + __slots__ = ("to_type", "expr", "coord", "__weakref__") + + def __init__(self, to_type, expr, coord=None): + self.to_type = to_type + self.expr = expr + self.coord = coord + + def children(self): + nodelist = [] + if self.to_type is not None: + nodelist.append(("to_type", self.to_type)) + if self.expr is not None: + nodelist.append(("expr", self.expr)) + return tuple(nodelist) + + def __iter__(self): + if self.to_type is not None: + yield self.to_type + if self.expr is not None: + yield self.expr + + attr_names = () + + +class Compound(Node): + __slots__ = ("block_items", "coord", "__weakref__") + + def __init__(self, block_items, coord=None): + self.block_items = block_items + self.coord = coord + + def children(self): + nodelist = [] + for i, child in enumerate(self.block_items or []): + nodelist.append((f"block_items[{i}]", child)) + return tuple(nodelist) + + def __iter__(self): + for child in self.block_items or []: + yield child + + attr_names = () + + +class CompoundLiteral(Node): + __slots__ = ("type", "init", "coord", "__weakref__") + + def __init__(self, type, init, coord=None): + self.type = type + self.init = init + self.coord = coord + + def children(self): + nodelist = [] + if self.type is not None: + nodelist.append(("type", self.type)) + if self.init is not None: + nodelist.append(("init", self.init)) + return tuple(nodelist) + + def __iter__(self): + if self.type is not None: + yield self.type + if self.init is not None: + yield self.init + + attr_names = () + + +class Constant(Node): + __slots__ = ("type", "value", "coord", "__weakref__") + + def __init__(self, type, value, coord=None): + self.type = type + self.value = value + self.coord = coord + + def children(self): + nodelist = [] + return tuple(nodelist) + + def __iter__(self): + return + yield + + attr_names = ( + "type", + "value", + ) + + +class Continue(Node): + __slots__ = ("coord", "__weakref__") + + def __init__(self, coord=None): + self.coord = coord + + def children(self): + return () + + def __iter__(self): + return + yield + + attr_names = () + + +class Decl(Node): + __slots__ = ( + "name", + "quals", + "align", + "storage", + "funcspec", + "type", + "init", + "bitsize", + "coord", + "__weakref__", + ) + + def __init__( + self, name, quals, align, storage, funcspec, type, init, bitsize, coord=None + ): + self.name = name + self.quals = quals + self.align = align + self.storage = storage + self.funcspec = funcspec + self.type = type + self.init = init + self.bitsize = bitsize + self.coord = coord + + def children(self): + nodelist = [] + if self.type is not None: + nodelist.append(("type", self.type)) + if self.init is not None: + nodelist.append(("init", self.init)) + if self.bitsize is not None: + nodelist.append(("bitsize", self.bitsize)) + return tuple(nodelist) + + def __iter__(self): + if self.type is not None: + yield self.type + if self.init is not None: + yield self.init + if self.bitsize is not None: + yield self.bitsize + + attr_names = ( + "name", + "quals", + "align", + "storage", + "funcspec", + ) + + +class DeclList(Node): + __slots__ = ("decls", "coord", "__weakref__") + + def __init__(self, decls, coord=None): + self.decls = decls + self.coord = coord + + def children(self): + nodelist = [] + for i, child in enumerate(self.decls or []): + nodelist.append((f"decls[{i}]", child)) + return tuple(nodelist) + + def __iter__(self): + for child in self.decls or []: + yield child + + attr_names = () + + +class Default(Node): + __slots__ = ("stmts", "coord", "__weakref__") + + def __init__(self, stmts, coord=None): + self.stmts = stmts + self.coord = coord + + def children(self): + nodelist = [] + for i, child in enumerate(self.stmts or []): + nodelist.append((f"stmts[{i}]", child)) + return tuple(nodelist) + + def __iter__(self): + for child in self.stmts or []: + yield child + + attr_names = () + + +class DoWhile(Node): + __slots__ = ("cond", "stmt", "coord", "__weakref__") + + def __init__(self, cond, stmt, coord=None): + self.cond = cond + self.stmt = stmt + self.coord = coord + + def children(self): + nodelist = [] + if self.cond is not None: + nodelist.append(("cond", self.cond)) + if self.stmt is not None: + nodelist.append(("stmt", self.stmt)) + return tuple(nodelist) + + def __iter__(self): + if self.cond is not None: + yield self.cond + if self.stmt is not None: + yield self.stmt + + attr_names = () + + +class EllipsisParam(Node): + __slots__ = ("coord", "__weakref__") + + def __init__(self, coord=None): + self.coord = coord + + def children(self): + return () + + def __iter__(self): + return + yield + + attr_names = () + + +class EmptyStatement(Node): + __slots__ = ("coord", "__weakref__") + + def __init__(self, coord=None): + self.coord = coord + + def children(self): + return () + + def __iter__(self): + return + yield + + attr_names = () + + +class Enum(Node): + __slots__ = ("name", "values", "coord", "__weakref__") + + def __init__(self, name, values, coord=None): + self.name = name + self.values = values + self.coord = coord + + def children(self): + nodelist = [] + if self.values is not None: + nodelist.append(("values", self.values)) + return tuple(nodelist) + + def __iter__(self): + if self.values is not None: + yield self.values + + attr_names = ("name",) + + +class Enumerator(Node): + __slots__ = ("name", "value", "coord", "__weakref__") + + def __init__(self, name, value, coord=None): + self.name = name + self.value = value + self.coord = coord + + def children(self): + nodelist = [] + if self.value is not None: + nodelist.append(("value", self.value)) + return tuple(nodelist) + + def __iter__(self): + if self.value is not None: + yield self.value + + attr_names = ("name",) + + +class EnumeratorList(Node): + __slots__ = ("enumerators", "coord", "__weakref__") + + def __init__(self, enumerators, coord=None): + self.enumerators = enumerators + self.coord = coord + + def children(self): + nodelist = [] + for i, child in enumerate(self.enumerators or []): + nodelist.append((f"enumerators[{i}]", child)) + return tuple(nodelist) + + def __iter__(self): + for child in self.enumerators or []: + yield child + + attr_names = () + + +class ExprList(Node): + __slots__ = ("exprs", "coord", "__weakref__") + + def __init__(self, exprs, coord=None): + self.exprs = exprs + self.coord = coord + + def children(self): + nodelist = [] + for i, child in enumerate(self.exprs or []): + nodelist.append((f"exprs[{i}]", child)) + return tuple(nodelist) + + def __iter__(self): + for child in self.exprs or []: + yield child + + attr_names = () + + +class FileAST(Node): + __slots__ = ("ext", "coord", "__weakref__") + + def __init__(self, ext, coord=None): + self.ext = ext + self.coord = coord + + def children(self): + nodelist = [] + for i, child in enumerate(self.ext or []): + nodelist.append((f"ext[{i}]", child)) + return tuple(nodelist) + + def __iter__(self): + for child in self.ext or []: + yield child + + attr_names = () + + +class For(Node): + __slots__ = ("init", "cond", "next", "stmt", "coord", "__weakref__") + + def __init__(self, init, cond, next, stmt, coord=None): + self.init = init + self.cond = cond + self.next = next + self.stmt = stmt + self.coord = coord + + def children(self): + nodelist = [] + if self.init is not None: + nodelist.append(("init", self.init)) + if self.cond is not None: + nodelist.append(("cond", self.cond)) + if self.next is not None: + nodelist.append(("next", self.next)) + if self.stmt is not None: + nodelist.append(("stmt", self.stmt)) + return tuple(nodelist) + + def __iter__(self): + if self.init is not None: + yield self.init + if self.cond is not None: + yield self.cond + if self.next is not None: + yield self.next + if self.stmt is not None: + yield self.stmt + + attr_names = () + + +class FuncCall(Node): + __slots__ = ("name", "args", "coord", "__weakref__") + + def __init__(self, name, args, coord=None): + self.name = name + self.args = args + self.coord = coord + + def children(self): + nodelist = [] + if self.name is not None: + nodelist.append(("name", self.name)) + if self.args is not None: + nodelist.append(("args", self.args)) + return tuple(nodelist) + + def __iter__(self): + if self.name is not None: + yield self.name + if self.args is not None: + yield self.args + + attr_names = () + + +class FuncDecl(Node): + __slots__ = ("args", "type", "coord", "__weakref__") + + def __init__(self, args, type, coord=None): + self.args = args + self.type = type + self.coord = coord + + def children(self): + nodelist = [] + if self.args is not None: + nodelist.append(("args", self.args)) + if self.type is not None: + nodelist.append(("type", self.type)) + return tuple(nodelist) + + def __iter__(self): + if self.args is not None: + yield self.args + if self.type is not None: + yield self.type + + attr_names = () + + +class FuncDef(Node): + __slots__ = ("decl", "param_decls", "body", "coord", "__weakref__") + + def __init__(self, decl, param_decls, body, coord=None): + self.decl = decl + self.param_decls = param_decls + self.body = body + self.coord = coord + + def children(self): + nodelist = [] + if self.decl is not None: + nodelist.append(("decl", self.decl)) + if self.body is not None: + nodelist.append(("body", self.body)) + for i, child in enumerate(self.param_decls or []): + nodelist.append((f"param_decls[{i}]", child)) + return tuple(nodelist) + + def __iter__(self): + if self.decl is not None: + yield self.decl + if self.body is not None: + yield self.body + for child in self.param_decls or []: + yield child + + attr_names = () + + +class Goto(Node): + __slots__ = ("name", "coord", "__weakref__") + + def __init__(self, name, coord=None): + self.name = name + self.coord = coord + + def children(self): + nodelist = [] + return tuple(nodelist) + + def __iter__(self): + return + yield + + attr_names = ("name",) + + +class ID(Node): + __slots__ = ("name", "coord", "__weakref__") + + def __init__(self, name, coord=None): + self.name = name + self.coord = coord + + def children(self): + nodelist = [] + return tuple(nodelist) + + def __iter__(self): + return + yield + + attr_names = ("name",) + + +class IdentifierType(Node): + __slots__ = ("names", "coord", "__weakref__") + + def __init__(self, names, coord=None): + self.names = names + self.coord = coord + + def children(self): + nodelist = [] + return tuple(nodelist) + + def __iter__(self): + return + yield + + attr_names = ("names",) + + +class If(Node): + __slots__ = ("cond", "iftrue", "iffalse", "coord", "__weakref__") + + def __init__(self, cond, iftrue, iffalse, coord=None): + self.cond = cond + self.iftrue = iftrue + self.iffalse = iffalse + self.coord = coord + + def children(self): + nodelist = [] + if self.cond is not None: + nodelist.append(("cond", self.cond)) + if self.iftrue is not None: + nodelist.append(("iftrue", self.iftrue)) + if self.iffalse is not None: + nodelist.append(("iffalse", self.iffalse)) + return tuple(nodelist) + + def __iter__(self): + if self.cond is not None: + yield self.cond + if self.iftrue is not None: + yield self.iftrue + if self.iffalse is not None: + yield self.iffalse + + attr_names = () + + +class InitList(Node): + __slots__ = ("exprs", "coord", "__weakref__") + + def __init__(self, exprs, coord=None): + self.exprs = exprs + self.coord = coord + + def children(self): + nodelist = [] + for i, child in enumerate(self.exprs or []): + nodelist.append((f"exprs[{i}]", child)) + return tuple(nodelist) + + def __iter__(self): + for child in self.exprs or []: + yield child + + attr_names = () + + +class Label(Node): + __slots__ = ("name", "stmt", "coord", "__weakref__") + + def __init__(self, name, stmt, coord=None): + self.name = name + self.stmt = stmt + self.coord = coord + + def children(self): + nodelist = [] + if self.stmt is not None: + nodelist.append(("stmt", self.stmt)) + return tuple(nodelist) + + def __iter__(self): + if self.stmt is not None: + yield self.stmt + + attr_names = ("name",) + + +class NamedInitializer(Node): + __slots__ = ("name", "expr", "coord", "__weakref__") + + def __init__(self, name, expr, coord=None): + self.name = name + self.expr = expr + self.coord = coord + + def children(self): + nodelist = [] + if self.expr is not None: + nodelist.append(("expr", self.expr)) + for i, child in enumerate(self.name or []): + nodelist.append((f"name[{i}]", child)) + return tuple(nodelist) + + def __iter__(self): + if self.expr is not None: + yield self.expr + for child in self.name or []: + yield child + + attr_names = () + + +class ParamList(Node): + __slots__ = ("params", "coord", "__weakref__") + + def __init__(self, params, coord=None): + self.params = params + self.coord = coord + + def children(self): + nodelist = [] + for i, child in enumerate(self.params or []): + nodelist.append((f"params[{i}]", child)) + return tuple(nodelist) + + def __iter__(self): + for child in self.params or []: + yield child + + attr_names = () + + +class PtrDecl(Node): + __slots__ = ("quals", "type", "coord", "__weakref__") + + def __init__(self, quals, type, coord=None): + self.quals = quals + self.type = type + self.coord = coord + + def children(self): + nodelist = [] + if self.type is not None: + nodelist.append(("type", self.type)) + return tuple(nodelist) + + def __iter__(self): + if self.type is not None: + yield self.type + + attr_names = ("quals",) + + +class Return(Node): + __slots__ = ("expr", "coord", "__weakref__") + + def __init__(self, expr, coord=None): + self.expr = expr + self.coord = coord + + def children(self): + nodelist = [] + if self.expr is not None: + nodelist.append(("expr", self.expr)) + return tuple(nodelist) + + def __iter__(self): + if self.expr is not None: + yield self.expr + + attr_names = () + + +class StaticAssert(Node): + __slots__ = ("cond", "message", "coord", "__weakref__") + + def __init__(self, cond, message, coord=None): + self.cond = cond + self.message = message + self.coord = coord + + def children(self): + nodelist = [] + if self.cond is not None: + nodelist.append(("cond", self.cond)) + if self.message is not None: + nodelist.append(("message", self.message)) + return tuple(nodelist) + + def __iter__(self): + if self.cond is not None: + yield self.cond + if self.message is not None: + yield self.message + + attr_names = () + + +class Struct(Node): + __slots__ = ("name", "decls", "coord", "__weakref__") + + def __init__(self, name, decls, coord=None): + self.name = name + self.decls = decls + self.coord = coord + + def children(self): + nodelist = [] + for i, child in enumerate(self.decls or []): + nodelist.append((f"decls[{i}]", child)) + return tuple(nodelist) + + def __iter__(self): + for child in self.decls or []: + yield child + + attr_names = ("name",) + + +class StructRef(Node): + __slots__ = ("name", "type", "field", "coord", "__weakref__") + + def __init__(self, name, type, field, coord=None): + self.name = name + self.type = type + self.field = field + self.coord = coord + + def children(self): + nodelist = [] + if self.name is not None: + nodelist.append(("name", self.name)) + if self.field is not None: + nodelist.append(("field", self.field)) + return tuple(nodelist) + + def __iter__(self): + if self.name is not None: + yield self.name + if self.field is not None: + yield self.field + + attr_names = ("type",) + + +class Switch(Node): + __slots__ = ("cond", "stmt", "coord", "__weakref__") + + def __init__(self, cond, stmt, coord=None): + self.cond = cond + self.stmt = stmt + self.coord = coord + + def children(self): + nodelist = [] + if self.cond is not None: + nodelist.append(("cond", self.cond)) + if self.stmt is not None: + nodelist.append(("stmt", self.stmt)) + return tuple(nodelist) + + def __iter__(self): + if self.cond is not None: + yield self.cond + if self.stmt is not None: + yield self.stmt + + attr_names = () + + +class TernaryOp(Node): + __slots__ = ("cond", "iftrue", "iffalse", "coord", "__weakref__") + + def __init__(self, cond, iftrue, iffalse, coord=None): + self.cond = cond + self.iftrue = iftrue + self.iffalse = iffalse + self.coord = coord + + def children(self): + nodelist = [] + if self.cond is not None: + nodelist.append(("cond", self.cond)) + if self.iftrue is not None: + nodelist.append(("iftrue", self.iftrue)) + if self.iffalse is not None: + nodelist.append(("iffalse", self.iffalse)) + return tuple(nodelist) + + def __iter__(self): + if self.cond is not None: + yield self.cond + if self.iftrue is not None: + yield self.iftrue + if self.iffalse is not None: + yield self.iffalse + + attr_names = () + + +class TypeDecl(Node): + __slots__ = ("declname", "quals", "align", "type", "coord", "__weakref__") + + def __init__(self, declname, quals, align, type, coord=None): + self.declname = declname + self.quals = quals + self.align = align + self.type = type + self.coord = coord + + def children(self): + nodelist = [] + if self.type is not None: + nodelist.append(("type", self.type)) + return tuple(nodelist) + + def __iter__(self): + if self.type is not None: + yield self.type + + attr_names = ( + "declname", + "quals", + "align", + ) + + +class Typedef(Node): + __slots__ = ("name", "quals", "storage", "type", "coord", "__weakref__") + + def __init__(self, name, quals, storage, type, coord=None): + self.name = name + self.quals = quals + self.storage = storage + self.type = type + self.coord = coord + + def children(self): + nodelist = [] + if self.type is not None: + nodelist.append(("type", self.type)) + return tuple(nodelist) + + def __iter__(self): + if self.type is not None: + yield self.type + + attr_names = ( + "name", + "quals", + "storage", + ) + + +class Typename(Node): + __slots__ = ("name", "quals", "align", "type", "coord", "__weakref__") + + def __init__(self, name, quals, align, type, coord=None): + self.name = name + self.quals = quals + self.align = align + self.type = type + self.coord = coord + + def children(self): + nodelist = [] + if self.type is not None: + nodelist.append(("type", self.type)) + return tuple(nodelist) + + def __iter__(self): + if self.type is not None: + yield self.type + + attr_names = ( + "name", + "quals", + "align", + ) + + +class UnaryOp(Node): + __slots__ = ("op", "expr", "coord", "__weakref__") + + def __init__(self, op, expr, coord=None): + self.op = op + self.expr = expr + self.coord = coord + + def children(self): + nodelist = [] + if self.expr is not None: + nodelist.append(("expr", self.expr)) + return tuple(nodelist) + + def __iter__(self): + if self.expr is not None: + yield self.expr + + attr_names = ("op",) + + +class Union(Node): + __slots__ = ("name", "decls", "coord", "__weakref__") + + def __init__(self, name, decls, coord=None): + self.name = name + self.decls = decls + self.coord = coord + + def children(self): + nodelist = [] + for i, child in enumerate(self.decls or []): + nodelist.append((f"decls[{i}]", child)) + return tuple(nodelist) + + def __iter__(self): + for child in self.decls or []: + yield child + + attr_names = ("name",) + + +class While(Node): + __slots__ = ("cond", "stmt", "coord", "__weakref__") + + def __init__(self, cond, stmt, coord=None): + self.cond = cond + self.stmt = stmt + self.coord = coord + + def children(self): + nodelist = [] + if self.cond is not None: + nodelist.append(("cond", self.cond)) + if self.stmt is not None: + nodelist.append(("stmt", self.stmt)) + return tuple(nodelist) + + def __iter__(self): + if self.cond is not None: + yield self.cond + if self.stmt is not None: + yield self.stmt + + attr_names = () + + +class Pragma(Node): + __slots__ = ("string", "coord", "__weakref__") + + def __init__(self, string, coord=None): + self.string = string + self.coord = coord + + def children(self): + nodelist = [] + return tuple(nodelist) + + def __iter__(self): + return + yield + + attr_names = ("string",) diff --git a/source/target-vendors/cp312/pycparser/c_generator.py b/source/target-vendors/cp312/pycparser/c_generator.py new file mode 100644 index 0000000..424e00e --- /dev/null +++ b/source/target-vendors/cp312/pycparser/c_generator.py @@ -0,0 +1,573 @@ +# ------------------------------------------------------------------------------ +# pycparser: c_generator.py +# +# C code generator from pycparser AST nodes. +# +# Eli Bendersky [https://eli.thegreenplace.net/] +# License: BSD +# ------------------------------------------------------------------------------ +from typing import Callable, List, Optional + +from . import c_ast + + +class CGenerator: + """Uses the same visitor pattern as c_ast.NodeVisitor, but modified to + return a value from each visit method, using string accumulation in + generic_visit. + """ + + indent_level: int + reduce_parentheses: bool + + def __init__(self, reduce_parentheses: bool = False) -> None: + """Constructs C-code generator + + reduce_parentheses: + if True, eliminates needless parentheses on binary operators + """ + # Statements start with indentation of self.indent_level spaces, using + # the _make_indent method. + self.indent_level = 0 + self.reduce_parentheses = reduce_parentheses + + def _make_indent(self) -> str: + return " " * self.indent_level + + def visit(self, node: c_ast.Node) -> str: + method = "visit_" + node.__class__.__name__ + return getattr(self, method, self.generic_visit)(node) + + def generic_visit(self, node: Optional[c_ast.Node]) -> str: + if node is None: + return "" + else: + return "".join(self.visit(c) for c_name, c in node.children()) + + def visit_Constant(self, n: c_ast.Constant) -> str: + return n.value + + def visit_ID(self, n: c_ast.ID) -> str: + return n.name + + def visit_Pragma(self, n: c_ast.Pragma) -> str: + ret = "#pragma" + if n.string: + ret += " " + n.string + return ret + + def visit_ArrayRef(self, n: c_ast.ArrayRef) -> str: + arrref = self._parenthesize_unless_simple(n.name) + return arrref + "[" + self.visit(n.subscript) + "]" + + def visit_StructRef(self, n: c_ast.StructRef) -> str: + sref = self._parenthesize_unless_simple(n.name) + return sref + n.type + self.visit(n.field) + + def visit_FuncCall(self, n: c_ast.FuncCall) -> str: + fref = self._parenthesize_unless_simple(n.name) + args = self.visit(n.args) if n.args is not None else "" + return fref + "(" + args + ")" + + def visit_UnaryOp(self, n: c_ast.UnaryOp) -> str: + match n.op: + case "sizeof": + # Always parenthesize the argument of sizeof since it can be + # a name. + return f"sizeof({self.visit(n.expr)})" + case "p++": + operand = self._parenthesize_unless_simple(n.expr) + return f"{operand}++" + case "p--": + operand = self._parenthesize_unless_simple(n.expr) + return f"{operand}--" + case _: + operand = self._parenthesize_unless_simple(n.expr) + return f"{n.op}{operand}" + + # Precedence map of binary operators: + precedence_map = { + # Should be in sync with c_parser.CParser.precedence + # Higher numbers are stronger binding + "||": 0, # weakest binding + "&&": 1, + "|": 2, + "^": 3, + "&": 4, + "==": 5, + "!=": 5, + ">": 6, + ">=": 6, + "<": 6, + "<=": 6, + ">>": 7, + "<<": 7, + "+": 8, + "-": 8, + "*": 9, + "/": 9, + "%": 9, # strongest binding + } + + def visit_BinaryOp(self, n: c_ast.BinaryOp) -> str: + # Note: all binary operators are left-to-right associative + # + # If `n.left.op` has a stronger or equally binding precedence in + # comparison to `n.op`, no parenthesis are needed for the left: + # e.g., `(a*b) + c` is equivalent to `a*b + c`, as well as + # `(a+b) - c` is equivalent to `a+b - c` (same precedence). + # If the left operator is weaker binding than the current, then + # parentheses are necessary: + # e.g., `(a+b) * c` is NOT equivalent to `a+b * c`. + lval_str = self._parenthesize_if( + n.left, + lambda d: not ( + self._is_simple_node(d) + or self.reduce_parentheses + and isinstance(d, c_ast.BinaryOp) + and self.precedence_map[d.op] >= self.precedence_map[n.op] + ), + ) + # If `n.right.op` has a stronger -but not equal- binding precedence, + # parenthesis can be omitted on the right: + # e.g., `a + (b*c)` is equivalent to `a + b*c`. + # If the right operator is weaker or equally binding, then parentheses + # are necessary: + # e.g., `a * (b+c)` is NOT equivalent to `a * b+c` and + # `a - (b+c)` is NOT equivalent to `a - b+c` (same precedence). + rval_str = self._parenthesize_if( + n.right, + lambda d: not ( + self._is_simple_node(d) + or self.reduce_parentheses + and isinstance(d, c_ast.BinaryOp) + and self.precedence_map[d.op] > self.precedence_map[n.op] + ), + ) + return f"{lval_str} {n.op} {rval_str}" + + def visit_Assignment(self, n: c_ast.Assignment) -> str: + rval_str = self._parenthesize_if( + n.rvalue, lambda n: isinstance(n, c_ast.Assignment) + ) + return f"{self.visit(n.lvalue)} {n.op} {rval_str}" + + def visit_IdentifierType(self, n: c_ast.IdentifierType) -> str: + return " ".join(n.names) + + def _visit_expr(self, n: c_ast.Node) -> str: + match n: + case c_ast.InitList(): + return "{" + self.visit(n) + "}" + case c_ast.ExprList() | c_ast.Compound(): + return "(" + self.visit(n) + ")" + case _: + return self.visit(n) + + def visit_Decl(self, n: c_ast.Decl, no_type: bool = False) -> str: + # no_type is used when a Decl is part of a DeclList, where the type is + # explicitly only for the first declaration in a list. + # + s = n.name if no_type else self._generate_decl(n) + if n.bitsize: + s += " : " + self.visit(n.bitsize) + if n.init: + s += " = " + self._visit_expr(n.init) + return s + + def visit_DeclList(self, n: c_ast.DeclList) -> str: + s = self.visit(n.decls[0]) + if len(n.decls) > 1: + s += ", " + ", ".join( + self.visit_Decl(decl, no_type=True) for decl in n.decls[1:] + ) + return s + + def visit_Typedef(self, n: c_ast.Typedef) -> str: + s = "" + if n.storage: + s += " ".join(n.storage) + " " + s += self._generate_type(n.type) + return s + + def visit_Cast(self, n: c_ast.Cast) -> str: + s = "(" + self._generate_type(n.to_type, emit_declname=False) + ")" + return s + " " + self._parenthesize_unless_simple(n.expr) + + def visit_ExprList(self, n: c_ast.ExprList) -> str: + visited_subexprs = [] + for expr in n.exprs: + visited_subexprs.append(self._visit_expr(expr)) + return ", ".join(visited_subexprs) + + def visit_InitList(self, n: c_ast.InitList) -> str: + visited_subexprs = [] + for expr in n.exprs: + visited_subexprs.append(self._visit_expr(expr)) + return ", ".join(visited_subexprs) + + def visit_Enum(self, n: c_ast.Enum) -> str: + return self._generate_struct_union_enum(n, name="enum") + + def visit_Alignas(self, n: c_ast.Alignas) -> str: + return "_Alignas({})".format(self.visit(n.alignment)) + + def visit_Enumerator(self, n: c_ast.Enumerator) -> str: + if not n.value: + return "{indent}{name},\n".format( + indent=self._make_indent(), + name=n.name, + ) + else: + return "{indent}{name} = {value},\n".format( + indent=self._make_indent(), + name=n.name, + value=self.visit(n.value), + ) + + def visit_FuncDef(self, n: c_ast.FuncDef) -> str: + decl = self.visit(n.decl) + self.indent_level = 0 + body = self.visit(n.body) + if n.param_decls: + knrdecls = ";\n".join(self.visit(p) for p in n.param_decls) + return decl + "\n" + knrdecls + ";\n" + body + "\n" + else: + return decl + "\n" + body + "\n" + + def visit_FileAST(self, n: c_ast.FileAST) -> str: + s = "" + for ext in n.ext: + match ext: + case c_ast.FuncDef(): + s += self.visit(ext) + case c_ast.Pragma(): + s += self.visit(ext) + "\n" + case _: + s += self.visit(ext) + ";\n" + return s + + def visit_Compound(self, n: c_ast.Compound) -> str: + s = self._make_indent() + "{\n" + self.indent_level += 2 + if n.block_items: + s += "".join(self._generate_stmt(stmt) for stmt in n.block_items) + self.indent_level -= 2 + s += self._make_indent() + "}\n" + return s + + def visit_CompoundLiteral(self, n: c_ast.CompoundLiteral) -> str: + return "(" + self.visit(n.type) + "){" + self.visit(n.init) + "}" + + def visit_EmptyStatement(self, n: c_ast.EmptyStatement) -> str: + return ";" + + def visit_ParamList(self, n: c_ast.ParamList) -> str: + return ", ".join(self.visit(param) for param in n.params) + + def visit_Return(self, n: c_ast.Return) -> str: + s = "return" + if n.expr: + s += " " + self.visit(n.expr) + return s + ";" + + def visit_Break(self, n: c_ast.Break) -> str: + return "break;" + + def visit_Continue(self, n: c_ast.Continue) -> str: + return "continue;" + + def visit_TernaryOp(self, n: c_ast.TernaryOp) -> str: + s = "(" + self._visit_expr(n.cond) + ") ? " + s += "(" + self._visit_expr(n.iftrue) + ") : " + s += "(" + self._visit_expr(n.iffalse) + ")" + return s + + def visit_If(self, n: c_ast.If) -> str: + s = "if (" + if n.cond: + s += self.visit(n.cond) + s += ")\n" + s += self._generate_stmt(n.iftrue, add_indent=True) + if n.iffalse: + s += self._make_indent() + "else\n" + s += self._generate_stmt(n.iffalse, add_indent=True) + return s + + def visit_For(self, n: c_ast.For) -> str: + s = "for (" + if n.init: + s += self.visit(n.init) + s += ";" + if n.cond: + s += " " + self.visit(n.cond) + s += ";" + if n.next: + s += " " + self.visit(n.next) + s += ")\n" + s += self._generate_stmt(n.stmt, add_indent=True) + return s + + def visit_While(self, n: c_ast.While) -> str: + s = "while (" + if n.cond: + s += self.visit(n.cond) + s += ")\n" + s += self._generate_stmt(n.stmt, add_indent=True) + return s + + def visit_DoWhile(self, n: c_ast.DoWhile) -> str: + s = "do\n" + s += self._generate_stmt(n.stmt, add_indent=True) + s += self._make_indent() + "while (" + if n.cond: + s += self.visit(n.cond) + s += ");" + return s + + def visit_StaticAssert(self, n: c_ast.StaticAssert) -> str: + s = "_Static_assert(" + s += self.visit(n.cond) + if n.message: + s += "," + s += self.visit(n.message) + s += ")" + return s + + def visit_Switch(self, n: c_ast.Switch) -> str: + s = "switch (" + self.visit(n.cond) + ")\n" + s += self._generate_stmt(n.stmt, add_indent=True) + return s + + def visit_Case(self, n: c_ast.Case) -> str: + s = "case " + self.visit(n.expr) + ":\n" + for stmt in n.stmts: + s += self._generate_stmt(stmt, add_indent=True) + return s + + def visit_Default(self, n: c_ast.Default) -> str: + s = "default:\n" + for stmt in n.stmts: + s += self._generate_stmt(stmt, add_indent=True) + return s + + def visit_Label(self, n: c_ast.Label) -> str: + return n.name + ":\n" + self._generate_stmt(n.stmt) + + def visit_Goto(self, n: c_ast.Goto) -> str: + return "goto " + n.name + ";" + + def visit_EllipsisParam(self, n: c_ast.EllipsisParam) -> str: + return "..." + + def visit_Struct(self, n: c_ast.Struct) -> str: + return self._generate_struct_union_enum(n, "struct") + + def visit_Typename(self, n: c_ast.Typename) -> str: + return self._generate_type(n.type) + + def visit_Union(self, n: c_ast.Union) -> str: + return self._generate_struct_union_enum(n, "union") + + def visit_NamedInitializer(self, n: c_ast.NamedInitializer) -> str: + s = "" + for name in n.name: + if isinstance(name, c_ast.ID): + s += "." + name.name + else: + s += "[" + self.visit(name) + "]" + s += " = " + self._visit_expr(n.expr) + return s + + def visit_FuncDecl(self, n: c_ast.FuncDecl) -> str: + return self._generate_type(n) + + def visit_ArrayDecl(self, n: c_ast.ArrayDecl) -> str: + return self._generate_type(n, emit_declname=False) + + def visit_TypeDecl(self, n: c_ast.TypeDecl) -> str: + return self._generate_type(n, emit_declname=False) + + def visit_PtrDecl(self, n: c_ast.PtrDecl) -> str: + return self._generate_type(n, emit_declname=False) + + def _generate_struct_union_enum( + self, n: c_ast.Struct | c_ast.Union | c_ast.Enum, name: str + ) -> str: + """Generates code for structs, unions, and enums. name should be + 'struct', 'union', or 'enum'. + """ + if name in ("struct", "union"): + assert isinstance(n, (c_ast.Struct, c_ast.Union)) + members = n.decls + body_function = self._generate_struct_union_body + else: + assert name == "enum" + assert isinstance(n, c_ast.Enum) + members = None if n.values is None else n.values.enumerators + body_function = self._generate_enum_body + s = name + " " + (n.name or "") + if members is not None: + # None means no members + # Empty sequence means an empty list of members + s += "\n" + s += self._make_indent() + self.indent_level += 2 + s += "{\n" + s += body_function(members) + self.indent_level -= 2 + s += self._make_indent() + "}" + return s + + def _generate_struct_union_body(self, members: List[c_ast.Node]) -> str: + return "".join(self._generate_stmt(decl) for decl in members) + + def _generate_enum_body(self, members: List[c_ast.Enumerator]) -> str: + # `[:-2] + '\n'` removes the final `,` from the enumerator list + return "".join(self.visit(value) for value in members)[:-2] + "\n" + + def _generate_stmt(self, n: c_ast.Node, add_indent: bool = False) -> str: + """Generation from a statement node. This method exists as a wrapper + for individual visit_* methods to handle different treatment of + some statements in this context. + """ + if add_indent: + self.indent_level += 2 + indent = self._make_indent() + if add_indent: + self.indent_level -= 2 + + match n: + case ( + c_ast.Decl() + | c_ast.Assignment() + | c_ast.Cast() + | c_ast.UnaryOp() + | c_ast.BinaryOp() + | c_ast.TernaryOp() + | c_ast.FuncCall() + | c_ast.ArrayRef() + | c_ast.StructRef() + | c_ast.Constant() + | c_ast.ID() + | c_ast.Typedef() + | c_ast.ExprList() + ): + # These can also appear in an expression context so no semicolon + # is added to them automatically + # + return indent + self.visit(n) + ";\n" + case c_ast.Compound(): + # No extra indentation required before the opening brace of a + # compound - because it consists of multiple lines it has to + # compute its own indentation. + # + return self.visit(n) + case c_ast.If(): + return indent + self.visit(n) + case _: + return indent + self.visit(n) + "\n" + + def _generate_decl(self, n: c_ast.Decl) -> str: + """Generation from a Decl node.""" + s = "" + if n.funcspec: + s = " ".join(n.funcspec) + " " + if n.storage: + s += " ".join(n.storage) + " " + if n.align: + s += self.visit(n.align[0]) + " " + s += self._generate_type(n.type) + return s + + def _generate_type( + self, + n: c_ast.Node, + modifiers: List[c_ast.Node] = [], + emit_declname: bool = True, + ) -> str: + """Recursive generation from a type node. n is the type node. + modifiers collects the PtrDecl, ArrayDecl and FuncDecl modifiers + encountered on the way down to a TypeDecl, to allow proper + generation from it. + """ + # ~ print(n, modifiers) + match n: + case c_ast.TypeDecl(): + s = "" + if n.quals: + s += " ".join(n.quals) + " " + s += self.visit(n.type) + + nstr = n.declname if n.declname and emit_declname else "" + # Resolve modifiers. + # Wrap in parens to distinguish pointer to array and pointer to + # function syntax. + # + for i, modifier in enumerate(modifiers): + match modifier: + case c_ast.ArrayDecl(): + if i != 0 and isinstance(modifiers[i - 1], c_ast.PtrDecl): + nstr = "(" + nstr + ")" + nstr += "[" + if modifier.dim_quals: + nstr += " ".join(modifier.dim_quals) + " " + if modifier.dim is not None: + nstr += self.visit(modifier.dim) + nstr += "]" + case c_ast.FuncDecl(): + if i != 0 and isinstance(modifiers[i - 1], c_ast.PtrDecl): + nstr = "(" + nstr + ")" + args = ( + self.visit(modifier.args) + if modifier.args is not None + else "" + ) + nstr += "(" + args + ")" + case c_ast.PtrDecl(): + if modifier.quals: + quals = " ".join(modifier.quals) + suffix = f" {nstr}" if nstr else "" + nstr = f"* {quals}{suffix}" + else: + nstr = "*" + nstr + if nstr: + s += " " + nstr + return s + case c_ast.Decl(): + return self._generate_decl(n.type) + case c_ast.Typename(): + return self._generate_type(n.type, emit_declname=emit_declname) + case c_ast.IdentifierType(): + return " ".join(n.names) + " " + case c_ast.ArrayDecl() | c_ast.PtrDecl() | c_ast.FuncDecl(): + return self._generate_type( + n.type, modifiers + [n], emit_declname=emit_declname + ) + case _: + return self.visit(n) + + def _parenthesize_if( + self, n: c_ast.Node, condition: Callable[[c_ast.Node], bool] + ) -> str: + """Visits 'n' and returns its string representation, parenthesized + if the condition function applied to the node returns True. + """ + s = self._visit_expr(n) + if condition(n): + return "(" + s + ")" + else: + return s + + def _parenthesize_unless_simple(self, n: c_ast.Node) -> str: + """Common use case for _parenthesize_if""" + return self._parenthesize_if(n, lambda d: not self._is_simple_node(d)) + + def _is_simple_node(self, n: c_ast.Node) -> bool: + """Returns True for nodes that are "simple" - i.e. nodes that always + have higher precedence than operators. + """ + return isinstance( + n, + (c_ast.Constant, c_ast.ID, c_ast.ArrayRef, c_ast.StructRef, c_ast.FuncCall), + ) diff --git a/source/target-vendors/cp312/pycparser/c_lexer.py b/source/target-vendors/cp312/pycparser/c_lexer.py new file mode 100644 index 0000000..ef59d69 --- /dev/null +++ b/source/target-vendors/cp312/pycparser/c_lexer.py @@ -0,0 +1,706 @@ +# ------------------------------------------------------------------------------ +# pycparser: c_lexer.py +# +# CLexer class: lexer for the C language +# +# Eli Bendersky [https://eli.thegreenplace.net/] +# License: BSD +# ------------------------------------------------------------------------------ +import re +from dataclasses import dataclass +from enum import Enum +from typing import Callable, Dict, List, Optional, Tuple + + +@dataclass(slots=True) +class _Token: + type: str + value: str + lineno: int + column: int + + +class CLexer: + """A standalone lexer for C. + + Parameters for construction: + error_func: + Called with (msg, line, column) on lexing errors. + on_lbrace_func: + Called when an LBRACE token is produced (used for scope tracking). + on_rbrace_func: + Called when an RBRACE token is produced (used for scope tracking). + type_lookup_func: + Called with an identifier name; expected to return True if it is + a typedef name and should be tokenized as TYPEID. + + Call input(text) to initialize lexing, and then keep calling token() to + get the next token, until it returns None (at end of input). + """ + + def __init__( + self, + error_func: Callable[[str, int, int], None], + on_lbrace_func: Callable[[], None], + on_rbrace_func: Callable[[], None], + type_lookup_func: Callable[[str], bool], + ) -> None: + self.error_func = error_func + self.on_lbrace_func = on_lbrace_func + self.on_rbrace_func = on_rbrace_func + self.type_lookup_func = type_lookup_func + self._init_state() + + def input(self, text: str, filename: str = "") -> None: + """Initialize the lexer to the given input text. + + filename is an optional name identifying the file from which the input + comes. The lexer can modify it if #line directives are encountered. + """ + self._init_state() + self._lexdata = text + self._filename = filename + + def _init_state(self) -> None: + self._lexdata = "" + self._filename = "" + self._pos = 0 + self._line_start = 0 + self._pending_tok: Optional[_Token] = None + self._lineno = 1 + + @property + def filename(self) -> str: + return self._filename + + def token(self) -> Optional[_Token]: + # Lexing strategy overview: + # + # - We maintain a current position (self._pos), line number, and the + # byte offset of the current line start. The lexer is a simple loop + # that skips whitespace/newlines and emits one token per call. + # - A small amount of logic is handled manually before regex matching: + # + # * Preprocessor-style directives: if we see '#', we check whether + # it's a #line or #pragma directive and consume it inline. #line + # updates lineno/filename and produces no tokens. #pragma can yield + # both PPPRAGMA and PPPRAGMASTR, but token() returns a single token, + # so we stash the PPPRAGMASTR as _pending_tok to return on the next + # token() call. Otherwise we return PPHASH. + # * Newlines update lineno/line-start tracking so tokens can record + # accurate columns. + # + # - The bulk of tokens are recognized in _match_token: + # + # * _regex_rules: regex patterns for identifiers, literals, and other + # complex tokens (including error-producing patterns). The lexer + # uses a combined _regex_master to scan options at the same time. + # * _fixed_tokens: exact string matches for operators and punctuation, + # resolved by longest match. + # + # - Error patterns call the error callback and advance minimally, which + # keeps lexing resilient while reporting useful diagnostics. + text = self._lexdata + n = len(text) + + if self._pending_tok is not None: + tok = self._pending_tok + self._pending_tok = None + return tok + + while self._pos < n: + match text[self._pos]: + case " " | "\t": + self._pos += 1 + case "\n": + self._lineno += 1 + self._pos += 1 + self._line_start = self._pos + case "#": + if _line_pattern.match(text, self._pos + 1): + self._pos += 1 + self._handle_ppline() + continue + if _pragma_pattern.match(text, self._pos + 1): + self._pos += 1 + toks = self._handle_pppragma() + if len(toks) > 1: + self._pending_tok = toks[1] + if len(toks) > 0: + return toks[0] + continue + tok = self._make_token("PPHASH", "#", self._pos) + self._pos += 1 + return tok + case _: + if tok := self._match_token(): + return tok + else: + continue + + def _match_token(self) -> Optional[_Token]: + """Match one token at the current position. + + Returns a Token on success, or None if no token could be matched and + an error was reported. This method always advances _pos by the matched + length, or by 1 on error/no-match. + """ + text = self._lexdata + pos = self._pos + # We pick the longest match between: + # - the master regex (identifiers, literals, error patterns, etc.) + # - fixed operator/punctuator literals from the bucket for text[pos] + # + # The longest match is required to ensure we properly lex something + # like ".123" (a floating-point constant) as a single entity (with + # FLOAT_CONST), rather than a PERIOD followed by a number. + # + # The fixed-literal buckets are already length-sorted, so within that + # bucket we can take the first match. However, we still compare its + # length to the regex match because the regex may have matched a longer + # token that should take precedence. + best = None + + if m := _regex_master.match(text, pos): + tok_type = m.lastgroup + # All master-regex alternatives are named; lastgroup shouldn't be None. + assert tok_type is not None + value = m.group(tok_type) + length = len(value) + action, msg = _regex_actions[tok_type] + best = (length, tok_type, value, action, msg) + + if bucket := _fixed_tokens_by_first.get(text[pos]): + for entry in bucket: + if text.startswith(entry.literal, pos): + length = len(entry.literal) + if best is None or length > best[0]: + best = ( + length, + entry.tok_type, + entry.literal, + _RegexAction.TOKEN, + None, + ) + break + + if best is None: + msg = f"Illegal character {repr(text[pos])}" + self._error(msg, pos) + self._pos += 1 + return None + + length, tok_type, value, action, msg = best + if action == _RegexAction.ERROR: + if tok_type == "BAD_CHAR_CONST": + msg = f"Invalid char constant {value}" + # All other ERROR rules provide a message. + assert msg is not None + self._error(msg, pos) + self._pos += max(1, length) + return None + + if action == _RegexAction.ID: + tok_type = _keyword_map.get(value, "ID") + if tok_type == "ID" and self.type_lookup_func(value): + tok_type = "TYPEID" + + tok = self._make_token(tok_type, value, pos) + self._pos += length + + if tok.type == "LBRACE": + self.on_lbrace_func() + elif tok.type == "RBRACE": + self.on_rbrace_func() + + return tok + + def _make_token(self, tok_type: str, value: str, pos: int) -> _Token: + """Create a Token at an absolute input position. + + Expects tok_type/value and the absolute byte offset pos in the current + input. Does not advance lexer state; callers manage _pos themselves. + Returns a Token with lineno/column computed from current line tracking. + """ + column = pos - self._line_start + 1 + tok = _Token(tok_type, value, self._lineno, column) + return tok + + def _error(self, msg: str, pos: int) -> None: + column = pos - self._line_start + 1 + self.error_func(msg, self._lineno, column) + + def _handle_ppline(self) -> None: + # Since #line directives aren't supposed to return tokens but should + # only affect the lexer's state (update line/filename for coords), this + # method does a bit of parsing on its own. It doesn't return anything, + # but its side effect is to update self._pos past the directive, and + # potentially update self._lineno and self._filename, based on the + # directive's contents. + # + # Accepted #line forms from preprocessors: + # - "#line 66 \"kwas\\df.h\"" + # - "# 9" + # - "#line 10 \"include/me.h\" 1 2 3" (extra numeric flags) + # - "# 1 \"file.h\" 3" + # Errors we must report: + # - "#line \"file.h\"" (filename before line number) + # - "#line df" (garbage instead of number/string) + # + # We scan the directive line once (after an optional 'line' keyword), + # validating the order: NUMBER, optional STRING, then any NUMBERs. + # The NUMBERs tail is only accepted if a filename STRING was present. + text = self._lexdata + n = len(text) + line_end = text.find("\n", self._pos) + if line_end == -1: + line_end = n + line = text[self._pos : line_end] + pos = 0 + line_len = len(line) + + def skip_ws() -> None: + nonlocal pos + while pos < line_len and line[pos] in " \t": + pos += 1 + + skip_ws() + if line.startswith("line", pos): + pos += 4 + + def success(pp_line: Optional[str], pp_filename: Optional[str]) -> None: + if pp_line is None: + self._error("line number missing in #line", self._pos + line_len) + else: + self._lineno = int(pp_line) + if pp_filename is not None: + self._filename = pp_filename + self._pos = line_end + 1 + self._line_start = self._pos + + def fail(msg: str, offset: int) -> None: + self._error(msg, self._pos + offset) + self._pos = line_end + 1 + self._line_start = self._pos + + skip_ws() + if pos >= line_len: + success(None, None) + return + if line[pos] == '"': + fail("filename before line number in #line", pos) + return + + m = re.match(_decimal_constant, line[pos:]) + if not m: + fail("invalid #line directive", pos) + return + + pp_line = m.group(0) + pos += len(pp_line) + skip_ws() + if pos >= line_len: + success(pp_line, None) + return + + if line[pos] != '"': + fail("invalid #line directive", pos) + return + + m = re.match(_string_literal, line[pos:]) + if not m: + fail("invalid #line directive", pos) + return + + pp_filename = m.group(0).lstrip('"').rstrip('"') + pos += len(m.group(0)) + + # Consume arbitrary sequence of numeric flags after the directive + while True: + skip_ws() + if pos >= line_len: + break + m = re.match(_decimal_constant, line[pos:]) + if not m: + fail("invalid #line directive", pos) + return + pos += len(m.group(0)) + + success(pp_line, pp_filename) + + def _handle_pppragma(self) -> List[_Token]: + # Parse a full #pragma line; returns a list of tokens with 1 or 2 + # tokens - PPPRAGMA and an optional PPPRAGMASTR. If an empty list is + # returned, it means an error occurred, or we're at the end of input. + # + # Examples: + # - "#pragma" -> PPPRAGMA only + # - "#pragma once" -> PPPRAGMA, PPPRAGMASTR("once") + # - "# pragma omp parallel private(th_id)" -> PPPRAGMA, PPPRAGMASTR("omp parallel private(th_id)") + # - "#\tpragma {pack: 2, smack: 3}" -> PPPRAGMA, PPPRAGMASTR("{pack: 2, smack: 3}") + text = self._lexdata + n = len(text) + pos = self._pos + + while pos < n and text[pos] in " \t": + pos += 1 + if pos >= n: + self._pos = pos + return [] + + if not text.startswith("pragma", pos): + self._error("invalid #pragma directive", pos) + self._pos = pos + 1 + return [] + + pragma_pos = pos + pos += len("pragma") + toks = [self._make_token("PPPRAGMA", "pragma", pragma_pos)] + + while pos < n and text[pos] in " \t": + pos += 1 + + start = pos + while pos < n and text[pos] != "\n": + pos += 1 + if pos > start: + toks.append(self._make_token("PPPRAGMASTR", text[start:pos], start)) + if pos < n and text[pos] == "\n": + self._lineno += 1 + pos += 1 + self._line_start = pos + self._pos = pos + return toks + + +## +## Reserved keywords +## +_keywords: Tuple[str, ...] = ( + "AUTO", + "BREAK", + "CASE", + "CHAR", + "CONST", + "CONTINUE", + "DEFAULT", + "DO", + "DOUBLE", + "ELSE", + "ENUM", + "EXTERN", + "FLOAT", + "FOR", + "GOTO", + "IF", + "INLINE", + "INT", + "LONG", + "REGISTER", + "OFFSETOF", + "RESTRICT", + "RETURN", + "SHORT", + "SIGNED", + "SIZEOF", + "STATIC", + "STRUCT", + "SWITCH", + "TYPEDEF", + "UNION", + "UNSIGNED", + "VOID", + "VOLATILE", + "WHILE", + "__INT128", + "_BOOL", + "_COMPLEX", + "_NORETURN", + "_THREAD_LOCAL", + "_STATIC_ASSERT", + "_ATOMIC", + "_ALIGNOF", + "_ALIGNAS", + "_PRAGMA", +) + +_keyword_map: Dict[str, str] = {} + +for keyword in _keywords: + # Keywords from new C standard are mixed-case, like _Bool, _Alignas, etc. + if keyword.startswith("_") and len(keyword) > 1 and keyword[1].isalpha(): + _keyword_map[keyword[:2].upper() + keyword[2:].lower()] = keyword + else: + _keyword_map[keyword.lower()] = keyword + +## +## Regexes for use in tokens +## + +# valid C identifiers (K&R2: A.2.3), plus '$' (supported by some compilers) +_identifier = r"[a-zA-Z_$][0-9a-zA-Z_$]*" + +_hex_prefix = "0[xX]" +_hex_digits = "[0-9a-fA-F]+" +_bin_prefix = "0[bB]" +_bin_digits = "[01]+" + +# integer constants (K&R2: A.2.5.1) +_integer_suffix_opt = ( + r"(([uU]ll)|([uU]LL)|(ll[uU]?)|(LL[uU]?)|([uU][lL])|([lL][uU]?)|[uU])?" +) +_decimal_constant = ( + "(0" + _integer_suffix_opt + ")|([1-9][0-9]*" + _integer_suffix_opt + ")" +) +_octal_constant = "0[0-7]*" + _integer_suffix_opt +_hex_constant = _hex_prefix + _hex_digits + _integer_suffix_opt +_bin_constant = _bin_prefix + _bin_digits + _integer_suffix_opt + +_bad_octal_constant = "0[0-7]*[89]" + +# comments are not supported +_unsupported_c_style_comment = r"\/\*" +_unsupported_cxx_style_comment = r"\/\/" + +# character constants (K&R2: A.2.5.2) +# Note: a-zA-Z and '.-~^_!=&;,' are allowed as escape chars to support #line +# directives with Windows paths as filenames (..\..\dir\file) +# For the same reason, decimal_escape allows all digit sequences. We want to +# parse all correct code, even if it means to sometimes parse incorrect +# code. +# +# The original regexes were taken verbatim from the C syntax definition, +# and were later modified to avoid worst-case exponential running time. +# +# simple_escape = r"""([a-zA-Z._~!=&\^\-\\?'"])""" +# decimal_escape = r"""(\d+)""" +# hex_escape = r"""(x[0-9a-fA-F]+)""" +# bad_escape = r"""([\\][^a-zA-Z._~^!=&\^\-\\?'"x0-7])""" +# +# The following modifications were made to avoid the ambiguity that allowed +# backtracking: (https://github.com/eliben/pycparser/issues/61) +# +# - \x was removed from simple_escape, unless it was not followed by a hex +# digit, to avoid ambiguity with hex_escape. +# - hex_escape allows one or more hex characters, but requires that the next +# character(if any) is not hex +# - decimal_escape allows one or more decimal characters, but requires that the +# next character(if any) is not a decimal +# - bad_escape does not allow any decimals (8-9), to avoid conflicting with the +# permissive decimal_escape. +# +# Without this change, python's `re` module would recursively try parsing each +# ambiguous escape sequence in multiple ways. e.g. `\123` could be parsed as +# `\1`+`23`, `\12`+`3`, and `\123`. + +_simple_escape = r"""([a-wyzA-Z._~!=&\^\-\\?'"]|x(?![0-9a-fA-F]))""" +_decimal_escape = r"""(\d+)(?!\d)""" +_hex_escape = r"""(x[0-9a-fA-F]+)(?![0-9a-fA-F])""" +_bad_escape = r"""([\\][^a-zA-Z._~^!=&\^\-\\?'"x0-9])""" + +_escape_sequence = ( + r"""(\\(""" + _simple_escape + "|" + _decimal_escape + "|" + _hex_escape + "))" +) + +# This complicated regex with lookahead might be slow for strings, so because +# all of the valid escapes (including \x) allowed +# 0 or more non-escaped characters after the first character, +# simple_escape+decimal_escape+hex_escape got simplified to + +_escape_sequence_start_in_string = r"""(\\[0-9a-zA-Z._~!=&\^\-\\?'"])""" + +_cconst_char = r"""([^'\\\n]|""" + _escape_sequence + ")" +_char_const = "'" + _cconst_char + "'" +_wchar_const = "L" + _char_const +_u8char_const = "u8" + _char_const +_u16char_const = "u" + _char_const +_u32char_const = "U" + _char_const +_multicharacter_constant = "'" + _cconst_char + "{2,4}'" +_unmatched_quote = "('" + _cconst_char + "*\\n)|('" + _cconst_char + "*$)" +_bad_char_const = ( + r"""('""" + _cconst_char + """[^'\n]+')|('')|('""" + _bad_escape + r"""[^'\n]*')""" +) + +# string literals (K&R2: A.2.6) +_string_char = r"""([^"\\\n]|""" + _escape_sequence_start_in_string + ")" +_string_literal = '"' + _string_char + '*"' +_wstring_literal = "L" + _string_literal +_u8string_literal = "u8" + _string_literal +_u16string_literal = "u" + _string_literal +_u32string_literal = "U" + _string_literal +_bad_string_literal = '"' + _string_char + "*" + _bad_escape + _string_char + '*"' + +# floating constants (K&R2: A.2.5.3) +_exponent_part = r"""([eE][-+]?[0-9]+)""" +_fractional_constant = r"""([0-9]*\.[0-9]+)|([0-9]+\.)""" +_floating_constant = ( + "((((" + + _fractional_constant + + ")" + + _exponent_part + + "?)|([0-9]+" + + _exponent_part + + "))[FfLl]?)" +) +_binary_exponent_part = r"""([pP][+-]?[0-9]+)""" +_hex_fractional_constant = ( + "(((" + _hex_digits + r""")?\.""" + _hex_digits + ")|(" + _hex_digits + r"""\.))""" +) +_hex_floating_constant = ( + "(" + + _hex_prefix + + "(" + + _hex_digits + + "|" + + _hex_fractional_constant + + ")" + + _binary_exponent_part + + "[FfLl]?)" +) + + +class _RegexAction(Enum): + TOKEN = 0 + ID = 1 + ERROR = 2 + + +@dataclass(frozen=True) +class _RegexRule: + # tok_type: name of the token emitted for a match + # regex_pattern: the raw regex (no anchors) to match at the current position + # action: TOKEN for normal tokens, ID for identifiers, ERROR to report + # error_message: message used for ERROR entries + tok_type: str + regex_pattern: str + action: _RegexAction + error_message: Optional[str] + + +_regex_rules: List[_RegexRule] = [ + _RegexRule( + "UNSUPPORTED_C_STYLE_COMMENT", + _unsupported_c_style_comment, + _RegexAction.ERROR, + "Comments are not supported, see https://github.com/eliben/pycparser#3using.", + ), + _RegexRule( + "UNSUPPORTED_CXX_STYLE_COMMENT", + _unsupported_cxx_style_comment, + _RegexAction.ERROR, + "Comments are not supported, see https://github.com/eliben/pycparser#3using.", + ), + _RegexRule( + "BAD_STRING_LITERAL", + _bad_string_literal, + _RegexAction.ERROR, + "String contains invalid escape code", + ), + _RegexRule("WSTRING_LITERAL", _wstring_literal, _RegexAction.TOKEN, None), + _RegexRule("U8STRING_LITERAL", _u8string_literal, _RegexAction.TOKEN, None), + _RegexRule("U16STRING_LITERAL", _u16string_literal, _RegexAction.TOKEN, None), + _RegexRule("U32STRING_LITERAL", _u32string_literal, _RegexAction.TOKEN, None), + _RegexRule("STRING_LITERAL", _string_literal, _RegexAction.TOKEN, None), + _RegexRule("HEX_FLOAT_CONST", _hex_floating_constant, _RegexAction.TOKEN, None), + _RegexRule("FLOAT_CONST", _floating_constant, _RegexAction.TOKEN, None), + _RegexRule("INT_CONST_HEX", _hex_constant, _RegexAction.TOKEN, None), + _RegexRule("INT_CONST_BIN", _bin_constant, _RegexAction.TOKEN, None), + _RegexRule( + "BAD_CONST_OCT", + _bad_octal_constant, + _RegexAction.ERROR, + "Invalid octal constant", + ), + _RegexRule("INT_CONST_OCT", _octal_constant, _RegexAction.TOKEN, None), + _RegexRule("INT_CONST_DEC", _decimal_constant, _RegexAction.TOKEN, None), + _RegexRule("INT_CONST_CHAR", _multicharacter_constant, _RegexAction.TOKEN, None), + _RegexRule("CHAR_CONST", _char_const, _RegexAction.TOKEN, None), + _RegexRule("WCHAR_CONST", _wchar_const, _RegexAction.TOKEN, None), + _RegexRule("U8CHAR_CONST", _u8char_const, _RegexAction.TOKEN, None), + _RegexRule("U16CHAR_CONST", _u16char_const, _RegexAction.TOKEN, None), + _RegexRule("U32CHAR_CONST", _u32char_const, _RegexAction.TOKEN, None), + _RegexRule("UNMATCHED_QUOTE", _unmatched_quote, _RegexAction.ERROR, "Unmatched '"), + _RegexRule("BAD_CHAR_CONST", _bad_char_const, _RegexAction.ERROR, None), + _RegexRule("ID", _identifier, _RegexAction.ID, None), +] + +_regex_actions: Dict[str, Tuple[_RegexAction, Optional[str]]] = {} +_regex_pattern_parts: List[str] = [] +for _rule in _regex_rules: + _regex_actions[_rule.tok_type] = (_rule.action, _rule.error_message) + _regex_pattern_parts.append(f"(?P<{_rule.tok_type}>{_rule.regex_pattern})") +# The master regex is a single alternation of all token patterns, each wrapped +# in a named group. We match once at the current position and then use +# `lastgroup` to recover which token kind fired; this avoids iterating over all +# regexes on every character while keeping the same token-level semantics. +_regex_master: re.Pattern[str] = re.compile("|".join(_regex_pattern_parts)) + + +@dataclass(frozen=True) +class _FixedToken: + tok_type: str + literal: str + + +_fixed_tokens: List[_FixedToken] = [ + _FixedToken("ELLIPSIS", "..."), + _FixedToken("LSHIFTEQUAL", "<<="), + _FixedToken("RSHIFTEQUAL", ">>="), + _FixedToken("PLUSPLUS", "++"), + _FixedToken("MINUSMINUS", "--"), + _FixedToken("ARROW", "->"), + _FixedToken("LAND", "&&"), + _FixedToken("LOR", "||"), + _FixedToken("LSHIFT", "<<"), + _FixedToken("RSHIFT", ">>"), + _FixedToken("LE", "<="), + _FixedToken("GE", ">="), + _FixedToken("EQ", "=="), + _FixedToken("NE", "!="), + _FixedToken("TIMESEQUAL", "*="), + _FixedToken("DIVEQUAL", "/="), + _FixedToken("MODEQUAL", "%="), + _FixedToken("PLUSEQUAL", "+="), + _FixedToken("MINUSEQUAL", "-="), + _FixedToken("ANDEQUAL", "&="), + _FixedToken("OREQUAL", "|="), + _FixedToken("XOREQUAL", "^="), + _FixedToken("EQUALS", "="), + _FixedToken("PLUS", "+"), + _FixedToken("MINUS", "-"), + _FixedToken("TIMES", "*"), + _FixedToken("DIVIDE", "/"), + _FixedToken("MOD", "%"), + _FixedToken("OR", "|"), + _FixedToken("AND", "&"), + _FixedToken("NOT", "~"), + _FixedToken("XOR", "^"), + _FixedToken("LNOT", "!"), + _FixedToken("LT", "<"), + _FixedToken("GT", ">"), + _FixedToken("CONDOP", "?"), + _FixedToken("LPAREN", "("), + _FixedToken("RPAREN", ")"), + _FixedToken("LBRACKET", "["), + _FixedToken("RBRACKET", "]"), + _FixedToken("LBRACE", "{"), + _FixedToken("RBRACE", "}"), + _FixedToken("COMMA", ","), + _FixedToken("PERIOD", "."), + _FixedToken("SEMI", ";"), + _FixedToken("COLON", ":"), +] + +# To avoid scanning all fixed tokens on every character, we bucket them by the +# first character. When matching at position i, we only look at the bucket for +# text[i], and we pre-sort that bucket by token length so the first match is +# also the longest. This preserves longest-match semantics (e.g. '>>=' before +# '>>' before '>') while reducing the number of comparisons. +_fixed_tokens_by_first: Dict[str, List[_FixedToken]] = {} +for _entry in _fixed_tokens: + _fixed_tokens_by_first.setdefault(_entry.literal[0], []).append(_entry) +for _bucket in _fixed_tokens_by_first.values(): + _bucket.sort(key=lambda item: len(item.literal), reverse=True) + +_line_pattern: re.Pattern[str] = re.compile(r"([ \t]*line\W)|([ \t]*\d+)") +_pragma_pattern: re.Pattern[str] = re.compile(r"[ \t]*pragma\W") diff --git a/source/target-vendors/cp312/pycparser/c_parser.py b/source/target-vendors/cp312/pycparser/c_parser.py new file mode 100644 index 0000000..f980672 --- /dev/null +++ b/source/target-vendors/cp312/pycparser/c_parser.py @@ -0,0 +1,2376 @@ +# ------------------------------------------------------------------------------ +# pycparser: c_parser.py +# +# Recursive-descent parser for the C language. +# +# Eli Bendersky [https://eli.thegreenplace.net/] +# License: BSD +# ------------------------------------------------------------------------------ +from dataclasses import dataclass +from typing import ( + Any, + Dict, + List, + Literal, + NoReturn, + Optional, + Tuple, + TypedDict, + cast, +) + +from . import c_ast +from .c_lexer import CLexer, _Token +from .ast_transforms import fix_switch_cases, fix_atomic_specifiers + + +@dataclass +class Coord: + """Coordinates of a syntactic element. Consists of: + - File name + - Line number + - Column number + """ + + file: str + line: int + column: Optional[int] = None + + def __str__(self) -> str: + text = f"{self.file}:{self.line}" + if self.column: + text += f":{self.column}" + return text + + +class ParseError(Exception): + pass + + +class CParser: + """Recursive-descent C parser. + + Usage: + parser = CParser() + ast = parser.parse(text, filename) + + The `lexer` parameter lets you inject a lexer class (defaults to CLexer). + The parameters after `lexer` are accepted for backward compatibility with + the old PLY-based parser and are otherwise unused. + """ + + def __init__( + self, + lex_optimize: bool = True, + lexer: type[CLexer] = CLexer, + lextab: str = "pycparser.lextab", + yacc_optimize: bool = True, + yacctab: str = "pycparser.yacctab", + yacc_debug: bool = False, + taboutputdir: str = "", + ) -> None: + self.clex: CLexer = lexer( + error_func=self._lex_error_func, + on_lbrace_func=self._lex_on_lbrace_func, + on_rbrace_func=self._lex_on_rbrace_func, + type_lookup_func=self._lex_type_lookup_func, + ) + + # Stack of scopes for keeping track of symbols. _scope_stack[-1] is + # the current (topmost) scope. Each scope is a dictionary that + # specifies whether a name is a type. If _scope_stack[n][name] is + # True, 'name' is currently a type in the scope. If it's False, + # 'name' is used in the scope but not as a type (for instance, if we + # saw: int name; + # If 'name' is not a key in _scope_stack[n] then 'name' was not defined + # in this scope at all. + self._scope_stack: List[Dict[str, bool]] = [dict()] + self._tokens: _TokenStream = _TokenStream(self.clex) + + def parse( + self, text: str, filename: str = "", debug: bool = False + ) -> c_ast.FileAST: + """Parses C code and returns an AST. + + text: + A string containing the C source code + + filename: + Name of the file being parsed (for meaningful + error messages) + + debug: + Deprecated debug flag (unused); for backwards compatibility. + """ + self._scope_stack = [dict()] + self.clex.input(text, filename) + self._tokens = _TokenStream(self.clex) + + ast = self._parse_translation_unit_or_empty() + tok = self._peek() + if tok is not None: + self._parse_error(f"before: {tok.value}", self._tok_coord(tok)) + return ast + + # ------------------------------------------------------------------ + # Scope and declaration helpers + # ------------------------------------------------------------------ + def _coord(self, lineno: int, column: Optional[int] = None) -> Coord: + return Coord(file=self.clex.filename, line=lineno, column=column) + + def _parse_error(self, msg: str, coord: Coord | str | None) -> NoReturn: + raise ParseError(f"{coord}: {msg}") + + def _push_scope(self) -> None: + self._scope_stack.append(dict()) + + def _pop_scope(self) -> None: + assert len(self._scope_stack) > 1 + self._scope_stack.pop() + + def _add_typedef_name(self, name: str, coord: Optional[Coord]) -> None: + """Add a new typedef name (ie a TYPEID) to the current scope""" + if not self._scope_stack[-1].get(name, True): + self._parse_error( + f"Typedef {name!r} previously declared as non-typedef in this scope", + coord, + ) + self._scope_stack[-1][name] = True + + def _add_identifier(self, name: str, coord: Optional[Coord]) -> None: + """Add a new object, function, or enum member name (ie an ID) to the + current scope + """ + if self._scope_stack[-1].get(name, False): + self._parse_error( + f"Non-typedef {name!r} previously declared as typedef in this scope", + coord, + ) + self._scope_stack[-1][name] = False + + def _is_type_in_scope(self, name: str) -> bool: + """Is *name* a typedef-name in the current scope?""" + for scope in reversed(self._scope_stack): + # If name is an identifier in this scope it shadows typedefs in + # higher scopes. + in_scope = scope.get(name) + if in_scope is not None: + return in_scope + return False + + def _lex_error_func(self, msg: str, line: int, column: int) -> None: + self._parse_error(msg, self._coord(line, column)) + + def _lex_on_lbrace_func(self) -> None: + self._push_scope() + + def _lex_on_rbrace_func(self) -> None: + self._pop_scope() + + def _lex_type_lookup_func(self, name: str) -> bool: + """Looks up types that were previously defined with + typedef. + Passed to the lexer for recognizing identifiers that + are types. + """ + return self._is_type_in_scope(name) + + # To understand what's going on here, read sections A.8.5 and + # A.8.6 of K&R2 very carefully. + # + # A C type consists of a basic type declaration, with a list + # of modifiers. For example: + # + # int *c[5]; + # + # The basic declaration here is 'int c', and the pointer and + # the array are the modifiers. + # + # Basic declarations are represented by TypeDecl (from module c_ast) and the + # modifiers are FuncDecl, PtrDecl and ArrayDecl. + # + # The standard states that whenever a new modifier is parsed, it should be + # added to the end of the list of modifiers. For example: + # + # K&R2 A.8.6.2: Array Declarators + # + # In a declaration T D where D has the form + # D1 [constant-expression-opt] + # and the type of the identifier in the declaration T D1 is + # "type-modifier T", the type of the + # identifier of D is "type-modifier array of T" + # + # This is what this method does. The declarator it receives + # can be a list of declarators ending with TypeDecl. It + # tacks the modifier to the end of this list, just before + # the TypeDecl. + # + # Additionally, the modifier may be a list itself. This is + # useful for pointers, that can come as a chain from the rule + # p_pointer. In this case, the whole modifier list is spliced + # into the new location. + def _type_modify_decl(self, decl: Any, modifier: Any) -> c_ast.Node: + """Tacks a type modifier on a declarator, and returns + the modified declarator. + + Note: the declarator and modifier may be modified + """ + modifier_head = modifier + modifier_tail = modifier + + # The modifier may be a nested list. Reach its tail. + while modifier_tail.type: + modifier_tail = modifier_tail.type + + # If the decl is a basic type, just tack the modifier onto it. + if isinstance(decl, c_ast.TypeDecl): + modifier_tail.type = decl + return modifier + else: + # Otherwise, the decl is a list of modifiers. Reach + # its tail and splice the modifier onto the tail, + # pointing to the underlying basic type. + decl_tail = decl + while not isinstance(decl_tail.type, c_ast.TypeDecl): + decl_tail = decl_tail.type + + modifier_tail.type = decl_tail.type + decl_tail.type = modifier_head + return decl + + # Due to the order in which declarators are constructed, + # they have to be fixed in order to look like a normal AST. + # + # When a declaration arrives from syntax construction, it has + # these problems: + # * The innermost TypeDecl has no type (because the basic + # type is only known at the uppermost declaration level) + # * The declaration has no variable name, since that is saved + # in the innermost TypeDecl + # * The typename of the declaration is a list of type + # specifiers, and not a node. Here, basic identifier types + # should be separated from more complex types like enums + # and structs. + # + # This method fixes these problems. + def _fix_decl_name_type( + self, + decl: c_ast.Decl | c_ast.Typedef | c_ast.Typename, + typename: List[Any], + ) -> c_ast.Decl | c_ast.Typedef | c_ast.Typename: + """Fixes a declaration. Modifies decl.""" + # Reach the underlying basic type + typ = decl + while not isinstance(typ, c_ast.TypeDecl): + typ = typ.type + + decl.name = typ.declname + typ.quals = decl.quals[:] + + # The typename is a list of types. If any type in this + # list isn't an IdentifierType, it must be the only + # type in the list (it's illegal to declare "int enum ..") + # If all the types are basic, they're collected in the + # IdentifierType holder. + for tn in typename: + if not isinstance(tn, c_ast.IdentifierType): + if len(typename) > 1: + self._parse_error("Invalid multiple types specified", tn.coord) + else: + typ.type = tn + return decl + + if not typename: + # Functions default to returning int + if not isinstance(decl.type, c_ast.FuncDecl): + self._parse_error("Missing type in declaration", decl.coord) + typ.type = c_ast.IdentifierType(["int"], coord=decl.coord) + else: + # At this point, we know that typename is a list of IdentifierType + # nodes. Concatenate all the names into a single list. + typ.type = c_ast.IdentifierType( + [name for id in typename for name in id.names], coord=typename[0].coord + ) + return decl + + def _add_declaration_specifier( + self, + declspec: Optional["_DeclSpec"], + newspec: Any, + kind: "_DeclSpecKind", + append: bool = False, + ) -> "_DeclSpec": + """See _DeclSpec for the specifier dictionary layout.""" + if declspec is None: + spec: _DeclSpec = dict( + qual=[], storage=[], type=[], function=[], alignment=[] + ) + else: + spec = declspec + + if append: + spec[kind].append(newspec) + else: + spec[kind].insert(0, newspec) + + return spec + + def _build_declarations( + self, + spec: "_DeclSpec", + decls: List["_DeclInfo"], + typedef_namespace: bool = False, + ) -> List[c_ast.Node]: + """Builds a list of declarations all sharing the given specifiers. + If typedef_namespace is true, each declared name is added + to the "typedef namespace", which also includes objects, + functions, and enum constants. + """ + is_typedef = "typedef" in spec["storage"] + declarations = [] + + # Bit-fields are allowed to be unnamed. + if decls[0].get("bitsize") is None: + # When redeclaring typedef names as identifiers in inner scopes, a + # problem can occur where the identifier gets grouped into + # spec['type'], leaving decl as None. This can only occur for the + # first declarator. + if decls[0]["decl"] is None: + if ( + len(spec["type"]) < 2 + or len(spec["type"][-1].names) != 1 + or not self._is_type_in_scope(spec["type"][-1].names[0]) + ): + coord = "?" + for t in spec["type"]: + if hasattr(t, "coord"): + coord = t.coord + break + self._parse_error("Invalid declaration", coord) + + # Make this look as if it came from "direct_declarator:ID" + decls[0]["decl"] = c_ast.TypeDecl( + declname=spec["type"][-1].names[0], + type=None, + quals=None, + align=spec["alignment"], + coord=spec["type"][-1].coord, + ) + # Remove the "new" type's name from the end of spec['type'] + del spec["type"][-1] + # A similar problem can occur where the declaration ends up + # looking like an abstract declarator. Give it a name if this is + # the case. + elif not isinstance( + decls[0]["decl"], + (c_ast.Enum, c_ast.Struct, c_ast.Union, c_ast.IdentifierType), + ): + decls_0_tail = cast(Any, decls[0]["decl"]) + while not isinstance(decls_0_tail, c_ast.TypeDecl): + decls_0_tail = decls_0_tail.type + if decls_0_tail.declname is None: + decls_0_tail.declname = spec["type"][-1].names[0] + del spec["type"][-1] + + for decl in decls: + assert decl["decl"] is not None + if is_typedef: + declaration = c_ast.Typedef( + name=None, + quals=spec["qual"], + storage=spec["storage"], + type=decl["decl"], + coord=decl["decl"].coord, + ) + else: + declaration = c_ast.Decl( + name=None, + quals=spec["qual"], + align=spec["alignment"], + storage=spec["storage"], + funcspec=spec["function"], + type=decl["decl"], + init=decl.get("init"), + bitsize=decl.get("bitsize"), + coord=decl["decl"].coord, + ) + + if isinstance( + declaration.type, + (c_ast.Enum, c_ast.Struct, c_ast.Union, c_ast.IdentifierType), + ): + fixed_decl = declaration + else: + fixed_decl = self._fix_decl_name_type(declaration, spec["type"]) + + # Add the type name defined by typedef to a + # symbol table (for usage in the lexer) + if typedef_namespace: + if is_typedef: + self._add_typedef_name(fixed_decl.name, fixed_decl.coord) + else: + self._add_identifier(fixed_decl.name, fixed_decl.coord) + + fixed_decl = fix_atomic_specifiers( + cast(c_ast.Decl | c_ast.Typedef, fixed_decl) + ) + declarations.append(fixed_decl) + + return declarations + + def _build_function_definition( + self, + spec: "_DeclSpec", + decl: c_ast.Node, + param_decls: Optional[List[c_ast.Node]], + body: c_ast.Node, + ) -> c_ast.Node: + """Builds a function definition.""" + if "typedef" in spec["storage"]: + self._parse_error("Invalid typedef", decl.coord) + + declaration = self._build_declarations( + spec=spec, + decls=[dict(decl=decl, init=None, bitsize=None)], + typedef_namespace=True, + )[0] + + return c_ast.FuncDef( + decl=declaration, param_decls=param_decls, body=body, coord=decl.coord + ) + + def _select_struct_union_class(self, token: str) -> type: + """Given a token (either STRUCT or UNION), selects the + appropriate AST class. + """ + if token == "struct": + return c_ast.Struct + else: + return c_ast.Union + + # ------------------------------------------------------------------ + # Token helpers + # ------------------------------------------------------------------ + def _peek(self, k: int = 1) -> Optional[_Token]: + """Return the k-th next token without consuming it (1-based).""" + return self._tokens.peek(k) + + def _peek_type(self, k: int = 1) -> Optional[str]: + """Return the type of the k-th next token, or None if absent (1-based).""" + tok = self._peek(k) + return tok.type if tok is not None else None + + def _advance(self) -> _Token: + tok = self._tokens.next() + if tok is None: + self._parse_error("At end of input", self.clex.filename) + else: + return tok + + def _accept(self, token_type: str) -> Optional[_Token]: + """Conditionally consume next token, only if it's of token_type. + + If it is of the expected type, consume and return it. + Otherwise, leaves the token intact and returns None. + """ + tok = self._peek() + if tok is not None and tok.type == token_type: + return self._advance() + return None + + def _expect(self, token_type: str) -> _Token: + tok = self._advance() + if tok.type != token_type: + self._parse_error(f"before: {tok.value}", self._tok_coord(tok)) + return tok + + def _mark(self) -> int: + return self._tokens.mark() + + def _reset(self, mark: int) -> None: + self._tokens.reset(mark) + + def _tok_coord(self, tok: _Token) -> Coord: + return self._coord(tok.lineno, tok.column) + + def _starts_declaration(self, tok: Optional[_Token] = None) -> bool: + tok = tok or self._peek() + if tok is None: + return False + return tok.type in _DECL_START + + def _starts_expression(self, tok: Optional[_Token] = None) -> bool: + tok = tok or self._peek() + if tok is None: + return False + return tok.type in _STARTS_EXPRESSION + + def _starts_statement(self) -> bool: + tok_type = self._peek_type() + if tok_type is None: + return False + if tok_type in _STARTS_STATEMENT: + return True + return self._starts_expression() + + def _starts_declarator(self, id_only: bool = False) -> bool: + tok_type = self._peek_type() + if tok_type is None: + return False + if tok_type in {"TIMES", "LPAREN"}: + return True + if id_only: + return tok_type == "ID" + return tok_type in {"ID", "TYPEID"} + + def _peek_declarator_name_info(self) -> Tuple[Optional[str], bool]: + mark = self._mark() + tok_type, saw_paren = self._scan_declarator_name_info() + self._reset(mark) + return tok_type, saw_paren + + def _parse_any_declarator( + self, allow_abstract: bool = False, typeid_paren_as_abstract: bool = False + ) -> Tuple[Optional[c_ast.Node], bool]: + # C declarators are ambiguous without lookahead. For example: + # int foo(int (aa)); -> aa is a name (ID) + # typedef char TT; + # int bar(int (TT)); -> TT is a type (TYPEID) in parens + name_type, saw_paren = self._peek_declarator_name_info() + if name_type is None or ( + typeid_paren_as_abstract and name_type == "TYPEID" and saw_paren + ): + if not allow_abstract: + tok = self._peek() + coord = self._tok_coord(tok) if tok is not None else self.clex.filename + self._parse_error("Invalid declarator", coord) + decl = self._parse_abstract_declarator_opt() + return decl, False + + if name_type == "TYPEID": + if typeid_paren_as_abstract: + decl = self._parse_typeid_noparen_declarator() + else: + decl = self._parse_typeid_declarator() + else: + decl = self._parse_id_declarator() + return decl, True + + def _scan_declarator_name_info(self) -> Tuple[Optional[str], bool]: + saw_paren = False + while self._accept("TIMES"): + while self._peek_type() in _TYPE_QUALIFIER: + self._advance() + + tok = self._peek() + if tok is None: + return None, saw_paren + if tok.type in {"ID", "TYPEID"}: + self._advance() + return tok.type, saw_paren + if tok.type == "LPAREN": + saw_paren = True + self._advance() + tok_type, nested_paren = self._scan_declarator_name_info() + if nested_paren: + saw_paren = True + depth = 1 + while True: + tok = self._peek() + if tok is None: + return None, saw_paren + if tok.type == "LPAREN": + depth += 1 + elif tok.type == "RPAREN": + depth -= 1 + self._advance() + if depth == 0: + break + continue + self._advance() + return tok_type, saw_paren + return None, saw_paren + + def _starts_direct_abstract_declarator(self) -> bool: + return self._peek_type() in {"LPAREN", "LBRACKET"} + + def _is_assignment_op(self) -> bool: + tok = self._peek() + return tok is not None and tok.type in _ASSIGNMENT_OPS + + def _try_parse_paren_type_name( + self, + ) -> Optional[Tuple[c_ast.Typename, int, _Token]]: + """Parse and return a parenthesized type name if present. + + Returns (typ, mark, lparen_tok) when the next tokens look like + '(' type_name ')', where typ is the parsed type name, mark is the + token-stream position before parsing, and lparen_tok is the LPAREN + token. Returns None if no parenthesized type name is present. + """ + mark = self._mark() + lparen_tok = self._accept("LPAREN") + if lparen_tok is None: + return None + if not self._starts_declaration(): + self._reset(mark) + return None + typ = self._parse_type_name() + if self._accept("RPAREN") is None: + self._reset(mark) + return None + return typ, mark, lparen_tok + + # ------------------------------------------------------------------ + # Top-level + # ------------------------------------------------------------------ + # BNF: translation_unit_or_empty : translation_unit | empty + def _parse_translation_unit_or_empty(self) -> c_ast.FileAST: + if self._peek() is None: + return c_ast.FileAST([]) + return c_ast.FileAST(self._parse_translation_unit()) + + # BNF: translation_unit : external_declaration+ + def _parse_translation_unit(self) -> List[c_ast.Node]: + ext = [] + while self._peek() is not None: + ext.extend(self._parse_external_declaration()) + return ext + + # BNF: external_declaration : function_definition + # | declaration + # | pp_directive + # | pppragma_directive + # | static_assert + # | ';' + def _parse_external_declaration(self) -> List[c_ast.Node]: + tok = self._peek() + if tok is None: + return [] + if tok.type == "PPHASH": + self._parse_pp_directive() + return [] + if tok.type in {"PPPRAGMA", "_PRAGMA"}: + return [self._parse_pppragma_directive()] + if self._accept("SEMI"): + return [] + if tok.type == "_STATIC_ASSERT": + return self._parse_static_assert() + + if not self._starts_declaration(tok): + # Special handling for old-style function definitions that have an + # implicit return type, e.g. + # + # foo() { + # return 5; + # } + # + # These get an implicit 'int' return type. + decl = self._parse_id_declarator() + param_decls = None + if self._peek_type() != "LBRACE": + self._parse_error("Invalid function definition", decl.coord) + spec: _DeclSpec = dict( + qual=[], + alignment=[], + storage=[], + type=[c_ast.IdentifierType(["int"], coord=decl.coord)], + function=[], + ) + func = self._build_function_definition( + spec=spec, + decl=decl, + param_decls=param_decls, + body=self._parse_compound_statement(), + ) + return [func] + + # From here on, parsing a standard declatation/definition. + spec, saw_type, spec_coord = self._parse_declaration_specifiers( + allow_no_type=True + ) + + name_type, _ = self._peek_declarator_name_info() + if name_type != "ID": + decls = self._parse_decl_body_with_spec(spec, saw_type) + self._expect("SEMI") + return decls + + decl = self._parse_id_declarator() + + if self._peek_type() == "LBRACE" or self._starts_declaration(): + param_decls = None + if self._starts_declaration(): + param_decls = self._parse_declaration_list() + if self._peek_type() != "LBRACE": + self._parse_error("Invalid function definition", decl.coord) + if not spec["type"]: + spec["type"] = [c_ast.IdentifierType(["int"], coord=spec_coord)] + func = self._build_function_definition( + spec=spec, + decl=decl, + param_decls=param_decls, + body=self._parse_compound_statement(), + ) + return [func] + + decl_dict: "_DeclInfo" = dict(decl=decl, init=None, bitsize=None) + if self._accept("EQUALS"): + decl_dict["init"] = self._parse_initializer() + decls = self._parse_init_declarator_list(first=decl_dict) + decls = self._build_declarations(spec=spec, decls=decls, typedef_namespace=True) + self._expect("SEMI") + return decls + + # ------------------------------------------------------------------ + # Declarations + # + # Declarations always come as lists (because they can be several in one + # line). When returning parsed declarations, a list is always returned - + # even if it contains a single element. + # ------------------------------------------------------------------ + def _parse_declaration(self) -> List[c_ast.Node]: + decls = self._parse_decl_body() + self._expect("SEMI") + return decls + + # BNF: decl_body : declaration_specifiers decl_body_with_spec + def _parse_decl_body(self) -> List[c_ast.Node]: + spec, saw_type, _ = self._parse_declaration_specifiers(allow_no_type=True) + return self._parse_decl_body_with_spec(spec, saw_type) + + # BNF: decl_body_with_spec : init_declarator_list + # | struct_or_union_or_enum_only + def _parse_decl_body_with_spec( + self, spec: "_DeclSpec", saw_type: bool + ) -> List[c_ast.Node]: + decls = None + if saw_type: + if self._starts_declarator(): + decls = self._parse_init_declarator_list() + else: + if self._starts_declarator(id_only=True): + decls = self._parse_init_declarator_list(id_only=True) + + if decls is None: + ty = spec["type"] + s_u_or_e = (c_ast.Struct, c_ast.Union, c_ast.Enum) + if len(ty) == 1 and isinstance(ty[0], s_u_or_e): + decls = [ + c_ast.Decl( + name=None, + quals=spec["qual"], + align=spec["alignment"], + storage=spec["storage"], + funcspec=spec["function"], + type=ty[0], + init=None, + bitsize=None, + coord=ty[0].coord, + ) + ] + else: + decls = self._build_declarations( + spec=spec, + decls=[dict(decl=None, init=None, bitsize=None)], + typedef_namespace=True, + ) + else: + decls = self._build_declarations( + spec=spec, decls=decls, typedef_namespace=True + ) + + return decls + + # BNF: declaration_list : declaration+ + def _parse_declaration_list(self) -> List[c_ast.Node]: + decls = [] + while self._starts_declaration(): + decls.extend(self._parse_declaration()) + return decls + + # BNF: declaration_specifiers : (storage_class_specifier + # | type_specifier + # | type_qualifier + # | function_specifier + # | alignment_specifier)+ + def _parse_declaration_specifiers( + self, allow_no_type: bool = False + ) -> Tuple["_DeclSpec", bool, Optional[Coord]]: + """Parse declaration-specifier sequence. + + allow_no_type: + If True, allow a missing type specifier without error. + + Returns: + (spec, saw_type, first_coord) where spec is a dict with + qual/storage/type/function/alignment entries, saw_type is True + if a type specifier was consumed, and first_coord is the coord + of the first specifier token (used for diagnostics). + """ + spec = None + saw_type = False + first_coord = None + + while True: + tok = self._peek() + if tok is None: + break + + if tok.type == "_ALIGNAS": + if first_coord is None: + first_coord = self._tok_coord(tok) + spec = self._add_declaration_specifier( + spec, self._parse_alignment_specifier(), "alignment", append=True + ) + continue + + if tok.type == "_ATOMIC" and self._peek_type(2) == "LPAREN": + if first_coord is None: + first_coord = self._tok_coord(tok) + spec = self._add_declaration_specifier( + spec, self._parse_atomic_specifier(), "type", append=True + ) + saw_type = True + continue + + if tok.type in _TYPE_QUALIFIER: + if first_coord is None: + first_coord = self._tok_coord(tok) + spec = self._add_declaration_specifier( + spec, self._advance().value, "qual", append=True + ) + continue + + if tok.type in _STORAGE_CLASS: + if first_coord is None: + first_coord = self._tok_coord(tok) + spec = self._add_declaration_specifier( + spec, self._advance().value, "storage", append=True + ) + continue + + if tok.type in _FUNCTION_SPEC: + if first_coord is None: + first_coord = self._tok_coord(tok) + spec = self._add_declaration_specifier( + spec, self._advance().value, "function", append=True + ) + continue + + if tok.type in _TYPE_SPEC_SIMPLE: + if first_coord is None: + first_coord = self._tok_coord(tok) + tok = self._advance() + spec = self._add_declaration_specifier( + spec, + c_ast.IdentifierType([tok.value], coord=self._tok_coord(tok)), + "type", + append=True, + ) + saw_type = True + continue + + if tok.type == "TYPEID": + if saw_type: + break + if first_coord is None: + first_coord = self._tok_coord(tok) + tok = self._advance() + spec = self._add_declaration_specifier( + spec, + c_ast.IdentifierType([tok.value], coord=self._tok_coord(tok)), + "type", + append=True, + ) + saw_type = True + continue + + if tok.type in {"STRUCT", "UNION"}: + if first_coord is None: + first_coord = self._tok_coord(tok) + spec = self._add_declaration_specifier( + spec, self._parse_struct_or_union_specifier(), "type", append=True + ) + saw_type = True + continue + + if tok.type == "ENUM": + if first_coord is None: + first_coord = self._tok_coord(tok) + spec = self._add_declaration_specifier( + spec, self._parse_enum_specifier(), "type", append=True + ) + saw_type = True + continue + + break + + if spec is None: + self._parse_error("Invalid declaration", self.clex.filename) + + if not saw_type and not allow_no_type: + self._parse_error("Missing type in declaration", first_coord) + + return spec, saw_type, first_coord + + # BNF: specifier_qualifier_list : (type_specifier + # | type_qualifier + # | alignment_specifier)+ + def _parse_specifier_qualifier_list(self) -> "_DeclSpec": + spec = None + saw_type = False + saw_alignment = False + first_coord = None + + while True: + tok = self._peek() + if tok is None: + break + + if tok.type == "_ALIGNAS": + if first_coord is None: + first_coord = self._tok_coord(tok) + spec = self._add_declaration_specifier( + spec, self._parse_alignment_specifier(), "alignment", append=True + ) + saw_alignment = True + continue + + if tok.type == "_ATOMIC" and self._peek_type(2) == "LPAREN": + if first_coord is None: + first_coord = self._tok_coord(tok) + spec = self._add_declaration_specifier( + spec, self._parse_atomic_specifier(), "type", append=True + ) + saw_type = True + continue + + if tok.type in _TYPE_QUALIFIER: + if first_coord is None: + first_coord = self._tok_coord(tok) + spec = self._add_declaration_specifier( + spec, self._advance().value, "qual", append=True + ) + continue + + if tok.type in _TYPE_SPEC_SIMPLE: + if first_coord is None: + first_coord = self._tok_coord(tok) + tok = self._advance() + spec = self._add_declaration_specifier( + spec, + c_ast.IdentifierType([tok.value], coord=self._tok_coord(tok)), + "type", + append=True, + ) + saw_type = True + continue + + if tok.type == "TYPEID": + if saw_type: + break + if first_coord is None: + first_coord = self._tok_coord(tok) + tok = self._advance() + spec = self._add_declaration_specifier( + spec, + c_ast.IdentifierType([tok.value], coord=self._tok_coord(tok)), + "type", + append=True, + ) + saw_type = True + continue + + if tok.type in {"STRUCT", "UNION"}: + if first_coord is None: + first_coord = self._tok_coord(tok) + spec = self._add_declaration_specifier( + spec, self._parse_struct_or_union_specifier(), "type", append=True + ) + saw_type = True + continue + + if tok.type == "ENUM": + if first_coord is None: + first_coord = self._tok_coord(tok) + spec = self._add_declaration_specifier( + spec, self._parse_enum_specifier(), "type", append=True + ) + saw_type = True + continue + + break + + if spec is None: + self._parse_error("Invalid specifier list", self.clex.filename) + + if not saw_type and not saw_alignment: + self._parse_error("Missing type in declaration", first_coord) + + if spec.get("storage") is None: + spec["storage"] = [] + if spec.get("function") is None: + spec["function"] = [] + + return spec + + # BNF: type_qualifier_list : type_qualifier+ + def _parse_type_qualifier_list(self) -> List[str]: + quals = [] + while self._peek_type() in _TYPE_QUALIFIER: + quals.append(self._advance().value) + return quals + + # BNF: alignment_specifier : _ALIGNAS '(' type_name | constant_expression ')' + def _parse_alignment_specifier(self) -> c_ast.Node: + tok = self._expect("_ALIGNAS") + self._expect("LPAREN") + + if self._starts_declaration(): + typ = self._parse_type_name() + self._expect("RPAREN") + return c_ast.Alignas(typ, self._tok_coord(tok)) + + expr = self._parse_constant_expression() + self._expect("RPAREN") + return c_ast.Alignas(expr, self._tok_coord(tok)) + + # BNF: atomic_specifier : _ATOMIC '(' type_name ')' + def _parse_atomic_specifier(self) -> c_ast.Node: + self._expect("_ATOMIC") + self._expect("LPAREN") + typ = self._parse_type_name() + self._expect("RPAREN") + typ.quals.append("_Atomic") + return typ + + # BNF: init_declarator_list : init_declarator (',' init_declarator)* + def _parse_init_declarator_list( + self, first: Optional["_DeclInfo"] = None, id_only: bool = False + ) -> List["_DeclInfo"]: + decls = ( + [first] + if first is not None + else [self._parse_init_declarator(id_only=id_only)] + ) + + while self._accept("COMMA"): + decls.append(self._parse_init_declarator(id_only=id_only)) + return decls + + # BNF: init_declarator : declarator ('=' initializer)? + def _parse_init_declarator(self, id_only: bool = False) -> "_DeclInfo": + decl = self._parse_id_declarator() if id_only else self._parse_declarator() + init = None + if self._accept("EQUALS"): + init = self._parse_initializer() + return dict(decl=decl, init=init, bitsize=None) + + # ------------------------------------------------------------------ + # Structs/unions/enums + # ------------------------------------------------------------------ + # BNF: struct_or_union_specifier : struct_or_union ID? '{' struct_declaration_list? '}' + # | struct_or_union ID + def _parse_struct_or_union_specifier(self) -> c_ast.Node: + tok = self._advance() + klass = self._select_struct_union_class(tok.value) + + if self._peek_type() in {"ID", "TYPEID"}: + name_tok = self._advance() + if self._peek_type() == "LBRACE": + self._advance() + if self._accept("RBRACE"): + return klass( + name=name_tok.value, decls=[], coord=self._tok_coord(name_tok) + ) + decls = self._parse_struct_declaration_list() + self._expect("RBRACE") + return klass( + name=name_tok.value, decls=decls, coord=self._tok_coord(name_tok) + ) + + return klass( + name=name_tok.value, decls=None, coord=self._tok_coord(name_tok) + ) + + if self._peek_type() == "LBRACE": + brace_tok = self._advance() + if self._accept("RBRACE"): + return klass(name=None, decls=[], coord=self._tok_coord(brace_tok)) + decls = self._parse_struct_declaration_list() + self._expect("RBRACE") + return klass(name=None, decls=decls, coord=self._tok_coord(brace_tok)) + + self._parse_error("Invalid struct/union declaration", self._tok_coord(tok)) + + # BNF: struct_declaration_list : struct_declaration+ + def _parse_struct_declaration_list(self) -> List[c_ast.Node]: + decls = [] + while self._peek_type() not in {None, "RBRACE"}: + items = self._parse_struct_declaration() + if items is None: + continue + decls.extend(items) + return decls + + # BNF: struct_declaration : specifier_qualifier_list struct_declarator_list? ';' + # | static_assert + # | pppragma_directive + def _parse_struct_declaration(self) -> Optional[List[c_ast.Node]]: + if self._peek_type() == "SEMI": + self._advance() + return None + if self._peek_type() in {"PPPRAGMA", "_PRAGMA"}: + return [self._parse_pppragma_directive()] + + spec = self._parse_specifier_qualifier_list() + assert "typedef" not in spec.get("storage", []) + + decls = None + if self._starts_declarator() or self._peek_type() == "COLON": + decls = self._parse_struct_declarator_list() + if decls is not None: + self._expect("SEMI") + return self._build_declarations(spec=spec, decls=decls) + + if len(spec["type"]) == 1: + node = spec["type"][0] + if isinstance(node, c_ast.Node): + decl_type = node + else: + decl_type = c_ast.IdentifierType(node) + self._expect("SEMI") + return self._build_declarations( + spec=spec, decls=[dict(decl=decl_type, init=None, bitsize=None)] + ) + + self._expect("SEMI") + return self._build_declarations( + spec=spec, decls=[dict(decl=None, init=None, bitsize=None)] + ) + + # BNF: struct_declarator_list : struct_declarator (',' struct_declarator)* + def _parse_struct_declarator_list(self) -> List["_DeclInfo"]: + decls = [self._parse_struct_declarator()] + while self._accept("COMMA"): + decls.append(self._parse_struct_declarator()) + return decls + + # BNF: struct_declarator : declarator? ':' constant_expression + # | declarator (':' constant_expression)? + def _parse_struct_declarator(self) -> "_DeclInfo": + if self._accept("COLON"): + bitsize = self._parse_constant_expression() + return { + "decl": c_ast.TypeDecl(None, None, None, None), + "init": None, + "bitsize": bitsize, + } + + decl = self._parse_declarator() + if self._accept("COLON"): + bitsize = self._parse_constant_expression() + return {"decl": decl, "init": None, "bitsize": bitsize} + + return {"decl": decl, "init": None, "bitsize": None} + + # BNF: enum_specifier : ENUM ID? '{' enumerator_list? '}' + # | ENUM ID + def _parse_enum_specifier(self) -> c_ast.Node: + tok = self._expect("ENUM") + if self._peek_type() in {"ID", "TYPEID"}: + name_tok = self._advance() + if self._peek_type() == "LBRACE": + self._advance() + enums = self._parse_enumerator_list() + self._expect("RBRACE") + return c_ast.Enum(name_tok.value, enums, self._tok_coord(tok)) + return c_ast.Enum(name_tok.value, None, self._tok_coord(tok)) + + self._expect("LBRACE") + enums = self._parse_enumerator_list() + self._expect("RBRACE") + return c_ast.Enum(None, enums, self._tok_coord(tok)) + + # BNF: enumerator_list : enumerator (',' enumerator)* ','? + def _parse_enumerator_list(self) -> c_ast.Node: + enum = self._parse_enumerator() + enum_list = c_ast.EnumeratorList([enum], enum.coord) + while self._accept("COMMA"): + if self._peek_type() == "RBRACE": + break + enum = self._parse_enumerator() + enum_list.enumerators.append(enum) + return enum_list + + # BNF: enumerator : ID ('=' constant_expression)? + def _parse_enumerator(self) -> c_ast.Node: + name_tok = self._expect("ID") + if self._accept("EQUALS"): + value = self._parse_constant_expression() + else: + value = None + enum = c_ast.Enumerator(name_tok.value, value, self._tok_coord(name_tok)) + self._add_identifier(enum.name, enum.coord) + return enum + + # ------------------------------------------------------------------ + # Declarators + # ------------------------------------------------------------------ + # BNF: declarator : pointer? direct_declarator + def _parse_declarator(self) -> c_ast.Node: + decl, _ = self._parse_any_declarator( + allow_abstract=False, typeid_paren_as_abstract=False + ) + assert decl is not None + return decl + + # BNF: id_declarator : declarator with ID name + def _parse_id_declarator(self) -> c_ast.Node: + return self._parse_declarator_kind(kind="id", allow_paren=True) + + # BNF: typeid_declarator : declarator with TYPEID name + def _parse_typeid_declarator(self) -> c_ast.Node: + return self._parse_declarator_kind(kind="typeid", allow_paren=True) + + # BNF: typeid_noparen_declarator : declarator without parenthesized name + def _parse_typeid_noparen_declarator(self) -> c_ast.Node: + return self._parse_declarator_kind(kind="typeid", allow_paren=False) + + # BNF: declarator_kind : pointer? direct_declarator(kind) + def _parse_declarator_kind(self, kind: str, allow_paren: bool) -> c_ast.Node: + ptr = None + if self._peek_type() == "TIMES": + ptr = self._parse_pointer() + direct = self._parse_direct_declarator(kind, allow_paren=allow_paren) + if ptr is not None: + return self._type_modify_decl(direct, ptr) + return direct + + # BNF: direct_declarator : ID | TYPEID | '(' declarator ')' + # | direct_declarator '[' ... ']' + # | direct_declarator '(' ... ')' + def _parse_direct_declarator( + self, kind: str, allow_paren: bool = True + ) -> c_ast.Node: + if allow_paren and self._accept("LPAREN"): + decl = self._parse_declarator_kind(kind, allow_paren=True) + self._expect("RPAREN") + else: + if kind == "id": + name_tok = self._expect("ID") + else: + name_tok = self._expect("TYPEID") + decl = c_ast.TypeDecl( + declname=name_tok.value, + type=None, + quals=None, + align=None, + coord=self._tok_coord(name_tok), + ) + + return self._parse_decl_suffixes(decl) + + def _parse_decl_suffixes(self, decl: c_ast.Node) -> c_ast.Node: + """Parse a chain of array/function suffixes and attach them to decl.""" + while True: + if self._peek_type() == "LBRACKET": + decl = self._type_modify_decl(decl, self._parse_array_decl(decl)) + continue + if self._peek_type() == "LPAREN": + func = self._parse_function_decl(decl) + decl = self._type_modify_decl(decl, func) + continue + break + return decl + + # BNF: array_decl : '[' array_specifiers? assignment_expression? ']' + def _parse_array_decl(self, base_decl: c_ast.Node) -> c_ast.Node: + return self._parse_array_decl_common(base_type=None, coord=base_decl.coord) + + def _parse_array_decl_common( + self, base_type: Optional[c_ast.Node], coord: Optional[Coord] = None + ) -> c_ast.Node: + """Parse an array declarator suffix and return an ArrayDecl node. + + base_type: + Base declarator node to attach (None for direct-declarator parsing, + TypeDecl for abstract declarators). + + coord: + Coordinate to use for the ArrayDecl. If None, uses the '[' token. + """ + lbrack_tok = self._expect("LBRACKET") + if coord is None: + coord = self._tok_coord(lbrack_tok) + + def make_array_decl(dim, dim_quals): + return c_ast.ArrayDecl( + type=base_type, dim=dim, dim_quals=dim_quals, coord=coord + ) + + if self._accept("STATIC"): + dim_quals = ["static"] + (self._parse_type_qualifier_list() or []) + dim = self._parse_assignment_expression() + self._expect("RBRACKET") + return make_array_decl(dim, dim_quals) + + if self._peek_type() in _TYPE_QUALIFIER: + dim_quals = self._parse_type_qualifier_list() or [] + if self._accept("STATIC"): + dim_quals = dim_quals + ["static"] + dim = self._parse_assignment_expression() + self._expect("RBRACKET") + return make_array_decl(dim, dim_quals) + times_tok = self._accept("TIMES") + if times_tok: + self._expect("RBRACKET") + dim = c_ast.ID(times_tok.value, self._tok_coord(times_tok)) + return make_array_decl(dim, dim_quals) + dim = None + if self._starts_expression(): + dim = self._parse_assignment_expression() + self._expect("RBRACKET") + return make_array_decl(dim, dim_quals) + + times_tok = self._accept("TIMES") + if times_tok: + self._expect("RBRACKET") + dim = c_ast.ID(times_tok.value, self._tok_coord(times_tok)) + return make_array_decl(dim, []) + + dim = None + if self._starts_expression(): + dim = self._parse_assignment_expression() + self._expect("RBRACKET") + return make_array_decl(dim, []) + + # BNF: function_decl : '(' parameter_type_list_opt | identifier_list_opt ')' + def _parse_function_decl(self, base_decl: c_ast.Node) -> c_ast.Node: + self._expect("LPAREN") + if self._accept("RPAREN"): + args = None + else: + args = ( + self._parse_parameter_type_list() + if self._starts_declaration() + else self._parse_identifier_list_opt() + ) + self._expect("RPAREN") + + func = c_ast.FuncDecl(args=args, type=None, coord=base_decl.coord) + + if self._peek_type() == "LBRACE": + if func.args is not None: + for param in func.args.params: + if isinstance(param, c_ast.EllipsisParam): + break + name = getattr(param, "name", None) + if name: + self._add_identifier(name, param.coord) + + return func + + # BNF: pointer : '*' type_qualifier_list? pointer? + def _parse_pointer(self) -> Optional[c_ast.Node]: + stars = [] + times_tok = self._accept("TIMES") + while times_tok: + quals = self._parse_type_qualifier_list() or [] + stars.append((quals, self._tok_coord(times_tok))) + times_tok = self._accept("TIMES") + + if not stars: + return None + + ptr = None + for quals, coord in stars: + ptr = c_ast.PtrDecl(quals=quals, type=ptr, coord=coord) + return ptr + + # BNF: parameter_type_list : parameter_list (',' ELLIPSIS)? + def _parse_parameter_type_list(self) -> c_ast.ParamList: + params = self._parse_parameter_list() + if self._peek_type() == "COMMA" and self._peek_type(2) == "ELLIPSIS": + self._advance() + ell_tok = self._advance() + params.params.append(c_ast.EllipsisParam(self._tok_coord(ell_tok))) + return params + + # BNF: parameter_list : parameter_declaration (',' parameter_declaration)* + def _parse_parameter_list(self) -> c_ast.ParamList: + first = self._parse_parameter_declaration() + params = c_ast.ParamList([first], first.coord) + while self._peek_type() == "COMMA" and self._peek_type(2) != "ELLIPSIS": + self._advance() + params.params.append(self._parse_parameter_declaration()) + return params + + # BNF: parameter_declaration : declaration_specifiers declarator? + # | declaration_specifiers abstract_declarator_opt + def _parse_parameter_declaration(self) -> c_ast.Node: + spec, _, spec_coord = self._parse_declaration_specifiers(allow_no_type=True) + + if not spec["type"]: + spec["type"] = [c_ast.IdentifierType(["int"], coord=spec_coord)] + + if self._starts_declarator(): + decl, is_named = self._parse_any_declarator( + allow_abstract=True, typeid_paren_as_abstract=True + ) + if is_named: + return self._build_declarations( + spec=spec, decls=[dict(decl=decl, init=None, bitsize=None)] + )[0] + return self._build_parameter_declaration(spec, decl, spec_coord) + + decl = self._parse_abstract_declarator_opt() + return self._build_parameter_declaration(spec, decl, spec_coord) + + def _build_parameter_declaration( + self, spec: "_DeclSpec", decl: Optional[c_ast.Node], spec_coord: Optional[Coord] + ) -> c_ast.Node: + if ( + len(spec["type"]) > 1 + and len(spec["type"][-1].names) == 1 + and self._is_type_in_scope(spec["type"][-1].names[0]) + ): + return self._build_declarations( + spec=spec, decls=[dict(decl=decl, init=None, bitsize=None)] + )[0] + + decl = c_ast.Typename( + name="", + quals=spec["qual"], + align=None, + type=decl or c_ast.TypeDecl(None, None, None, None), + coord=spec_coord, + ) + return self._fix_decl_name_type(decl, spec["type"]) + + # BNF: identifier_list_opt : identifier_list | empty + def _parse_identifier_list_opt(self) -> Optional[c_ast.Node]: + if self._peek_type() == "RPAREN": + return None + return self._parse_identifier_list() + + # BNF: identifier_list : identifier (',' identifier)* + def _parse_identifier_list(self) -> c_ast.Node: + first = self._parse_identifier() + params = c_ast.ParamList([first], first.coord) + while self._accept("COMMA"): + params.params.append(self._parse_identifier()) + return params + + # ------------------------------------------------------------------ + # Abstract declarators + # ------------------------------------------------------------------ + # BNF: type_name : specifier_qualifier_list abstract_declarator_opt + def _parse_type_name(self) -> c_ast.Typename: + spec = self._parse_specifier_qualifier_list() + decl = self._parse_abstract_declarator_opt() + + coord = None + if decl is not None: + coord = decl.coord + elif spec["type"]: + coord = spec["type"][0].coord + + typename = c_ast.Typename( + name="", + quals=spec["qual"][:], + align=None, + type=decl or c_ast.TypeDecl(None, None, None, None), + coord=coord, + ) + return cast(c_ast.Typename, self._fix_decl_name_type(typename, spec["type"])) + + # BNF: abstract_declarator_opt : pointer? direct_abstract_declarator? + def _parse_abstract_declarator_opt(self) -> Optional[c_ast.Node]: + if self._peek_type() == "TIMES": + ptr = self._parse_pointer() + if self._starts_direct_abstract_declarator(): + decl = self._parse_direct_abstract_declarator() + else: + decl = c_ast.TypeDecl(None, None, None, None) + assert ptr is not None + return self._type_modify_decl(decl, ptr) + + if self._starts_direct_abstract_declarator(): + return self._parse_direct_abstract_declarator() + + return None + + # BNF: direct_abstract_declarator : '(' parameter_type_list_opt ')' + # | '(' abstract_declarator ')' + # | '[' ... ']' + def _parse_direct_abstract_declarator(self) -> c_ast.Node: + lparen_tok = self._accept("LPAREN") + if lparen_tok: + if self._starts_declaration() or self._peek_type() == "RPAREN": + params = self._parse_parameter_type_list_opt() + self._expect("RPAREN") + decl = c_ast.FuncDecl( + args=params, + type=c_ast.TypeDecl(None, None, None, None), + coord=self._tok_coord(lparen_tok), + ) + else: + decl = self._parse_abstract_declarator_opt() + self._expect("RPAREN") + assert decl is not None + elif self._peek_type() == "LBRACKET": + decl = self._parse_abstract_array_base() + else: + self._parse_error("Invalid abstract declarator", self.clex.filename) + + return self._parse_decl_suffixes(decl) + + # BNF: parameter_type_list_opt : parameter_type_list | empty + def _parse_parameter_type_list_opt(self) -> Optional[c_ast.ParamList]: + if self._peek_type() == "RPAREN": + return None + return self._parse_parameter_type_list() + + # BNF: abstract_array_base : '[' array_specifiers? assignment_expression? ']' + def _parse_abstract_array_base(self) -> c_ast.Node: + return self._parse_array_decl_common( + base_type=c_ast.TypeDecl(None, None, None, None), coord=None + ) + + # ------------------------------------------------------------------ + # Statements + # ------------------------------------------------------------------ + # BNF: statement : labeled_statement | compound_statement + # | selection_statement | iteration_statement + # | jump_statement | expression_statement + # | static_assert | pppragma_directive + def _parse_statement(self) -> c_ast.Node | List[c_ast.Node]: + tok_type = self._peek_type() + match tok_type: + case "CASE" | "DEFAULT": + return self._parse_labeled_statement() + case "ID" if self._peek_type(2) == "COLON": + return self._parse_labeled_statement() + case "LBRACE": + return self._parse_compound_statement() + case "IF" | "SWITCH": + return self._parse_selection_statement() + case "WHILE" | "DO" | "FOR": + return self._parse_iteration_statement() + case "GOTO" | "BREAK" | "CONTINUE" | "RETURN": + return self._parse_jump_statement() + case "PPPRAGMA" | "_PRAGMA": + return self._parse_pppragma_directive() + case "_STATIC_ASSERT": + return self._parse_static_assert() + case _: + return self._parse_expression_statement() + + # BNF: pragmacomp_or_statement : pppragma_directive* statement + def _parse_pragmacomp_or_statement(self) -> c_ast.Node | List[c_ast.Node]: + if self._peek_type() in {"PPPRAGMA", "_PRAGMA"}: + pragmas = self._parse_pppragma_directive_list() + stmt = self._parse_statement() + return c_ast.Compound(block_items=pragmas + [stmt], coord=pragmas[0].coord) + return self._parse_statement() + + # BNF: block_item : declaration | statement + def _parse_block_item(self) -> c_ast.Node | List[c_ast.Node]: + if self._starts_declaration(): + return self._parse_declaration() + return self._parse_statement() + + # BNF: block_item_list : block_item+ + def _parse_block_item_list(self) -> List[c_ast.Node]: + items = [] + while self._peek_type() not in {"RBRACE", None}: + item = self._parse_block_item() + if isinstance(item, list): + if item == [None]: + continue + items.extend(item) + else: + items.append(item) + return items + + # BNF: compound_statement : '{' block_item_list? '}' + def _parse_compound_statement(self) -> c_ast.Node: + lbrace_tok = self._expect("LBRACE") + if self._accept("RBRACE"): + return c_ast.Compound(block_items=None, coord=self._tok_coord(lbrace_tok)) + block_items = self._parse_block_item_list() + self._expect("RBRACE") + return c_ast.Compound( + block_items=block_items, coord=self._tok_coord(lbrace_tok) + ) + + # BNF: labeled_statement : ID ':' statement + # | CASE constant_expression ':' statement + # | DEFAULT ':' statement + def _parse_labeled_statement(self) -> c_ast.Node: + tok_type = self._peek_type() + match tok_type: + case "ID": + name_tok = self._advance() + self._expect("COLON") + if self._starts_statement(): + stmt = self._parse_pragmacomp_or_statement() + else: + stmt = c_ast.EmptyStatement(self._tok_coord(name_tok)) + return c_ast.Label(name_tok.value, stmt, self._tok_coord(name_tok)) + case "CASE": + case_tok = self._advance() + expr = self._parse_constant_expression() + self._expect("COLON") + if self._starts_statement(): + stmt = self._parse_pragmacomp_or_statement() + else: + stmt = c_ast.EmptyStatement(self._tok_coord(case_tok)) + return c_ast.Case(expr, [stmt], self._tok_coord(case_tok)) + case "DEFAULT": + def_tok = self._advance() + self._expect("COLON") + if self._starts_statement(): + stmt = self._parse_pragmacomp_or_statement() + else: + stmt = c_ast.EmptyStatement(self._tok_coord(def_tok)) + return c_ast.Default([stmt], self._tok_coord(def_tok)) + case _: + self._parse_error("Invalid labeled statement", self.clex.filename) + + # BNF: selection_statement : IF '(' expression ')' statement (ELSE statement)? + # | SWITCH '(' expression ')' statement + def _parse_selection_statement(self) -> c_ast.Node: + tok = self._advance() + match tok.type: + case "IF": + self._expect("LPAREN") + cond = self._parse_expression() + self._expect("RPAREN") + then_stmt = self._parse_pragmacomp_or_statement() + if self._accept("ELSE"): + else_stmt = self._parse_pragmacomp_or_statement() + return c_ast.If(cond, then_stmt, else_stmt, self._tok_coord(tok)) + return c_ast.If(cond, then_stmt, None, self._tok_coord(tok)) + case "SWITCH": + self._expect("LPAREN") + expr = self._parse_expression() + self._expect("RPAREN") + stmt = self._parse_pragmacomp_or_statement() + return fix_switch_cases(c_ast.Switch(expr, stmt, self._tok_coord(tok))) + case _: + self._parse_error("Invalid selection statement", self._tok_coord(tok)) + + # BNF: iteration_statement : WHILE '(' expression ')' statement + # | DO statement WHILE '(' expression ')' ';' + # | FOR '(' (declaration | expression_opt) ';' + # expression_opt ';' expression_opt ')' statement + def _parse_iteration_statement(self) -> c_ast.Node: + tok = self._advance() + match tok.type: + case "WHILE": + self._expect("LPAREN") + cond = self._parse_expression() + self._expect("RPAREN") + stmt = self._parse_pragmacomp_or_statement() + return c_ast.While(cond, stmt, self._tok_coord(tok)) + case "DO": + stmt = self._parse_pragmacomp_or_statement() + self._expect("WHILE") + self._expect("LPAREN") + cond = self._parse_expression() + self._expect("RPAREN") + self._expect("SEMI") + return c_ast.DoWhile(cond, stmt, self._tok_coord(tok)) + case "FOR": + self._expect("LPAREN") + if self._starts_declaration(): + decls = self._parse_declaration() + init = c_ast.DeclList(decls, self._tok_coord(tok)) + cond = self._parse_expression_opt() + self._expect("SEMI") + next_expr = self._parse_expression_opt() + self._expect("RPAREN") + stmt = self._parse_pragmacomp_or_statement() + return c_ast.For(init, cond, next_expr, stmt, self._tok_coord(tok)) + + init = self._parse_expression_opt() + self._expect("SEMI") + cond = self._parse_expression_opt() + self._expect("SEMI") + next_expr = self._parse_expression_opt() + self._expect("RPAREN") + stmt = self._parse_pragmacomp_or_statement() + return c_ast.For(init, cond, next_expr, stmt, self._tok_coord(tok)) + case _: + self._parse_error("Invalid iteration statement", self._tok_coord(tok)) + + # BNF: jump_statement : GOTO ID ';' | BREAK ';' | CONTINUE ';' + # | RETURN expression? ';' + def _parse_jump_statement(self) -> c_ast.Node: + tok = self._advance() + match tok.type: + case "GOTO": + name_tok = self._expect("ID") + self._expect("SEMI") + return c_ast.Goto(name_tok.value, self._tok_coord(tok)) + case "BREAK": + self._expect("SEMI") + return c_ast.Break(self._tok_coord(tok)) + case "CONTINUE": + self._expect("SEMI") + return c_ast.Continue(self._tok_coord(tok)) + case "RETURN": + if self._accept("SEMI"): + return c_ast.Return(None, self._tok_coord(tok)) + expr = self._parse_expression() + self._expect("SEMI") + return c_ast.Return(expr, self._tok_coord(tok)) + case _: + self._parse_error("Invalid jump statement", self._tok_coord(tok)) + + # BNF: expression_statement : expression_opt ';' + def _parse_expression_statement(self) -> c_ast.Node: + expr = self._parse_expression_opt() + semi_tok = self._expect("SEMI") + if expr is None: + return c_ast.EmptyStatement(self._tok_coord(semi_tok)) + return expr + + # ------------------------------------------------------------------ + # Expressions + # ------------------------------------------------------------------ + # BNF: expression_opt : expression | empty + def _parse_expression_opt(self) -> Optional[c_ast.Node]: + if self._starts_expression(): + return self._parse_expression() + return None + + # BNF: expression : assignment_expression (',' assignment_expression)* + def _parse_expression(self) -> c_ast.Node: + expr = self._parse_assignment_expression() + if not self._accept("COMMA"): + return expr + exprs = [expr, self._parse_assignment_expression()] + while self._accept("COMMA"): + exprs.append(self._parse_assignment_expression()) + return c_ast.ExprList(exprs, expr.coord) + + # BNF: assignment_expression : conditional_expression + # | unary_expression assignment_op assignment_expression + def _parse_assignment_expression(self) -> c_ast.Node: + if self._peek_type() == "LPAREN" and self._peek_type(2) == "LBRACE": + self._advance() + comp = self._parse_compound_statement() + self._expect("RPAREN") + return comp + + expr = self._parse_conditional_expression() + if self._is_assignment_op(): + op = self._advance().value + rhs = self._parse_assignment_expression() + return c_ast.Assignment(op, expr, rhs, expr.coord) + return expr + + # BNF: conditional_expression : binary_expression + # | binary_expression '?' expression ':' conditional_expression + def _parse_conditional_expression(self) -> c_ast.Node: + expr = self._parse_binary_expression() + if self._accept("CONDOP"): + iftrue = self._parse_expression() + self._expect("COLON") + iffalse = self._parse_conditional_expression() + return c_ast.TernaryOp(expr, iftrue, iffalse, expr.coord) + return expr + + # BNF: binary_expression : cast_expression (binary_op cast_expression)* + def _parse_binary_expression( + self, min_prec: int = 0, lhs: Optional[c_ast.Node] = None + ) -> c_ast.Node: + if lhs is None: + lhs = self._parse_cast_expression() + + while True: + tok = self._peek() + if tok is None or tok.type not in _BINARY_PRECEDENCE: + break + prec = _BINARY_PRECEDENCE[tok.type] + if prec < min_prec: + break + + op = tok.value + self._advance() + rhs = self._parse_cast_expression() + + while True: + next_tok = self._peek() + if next_tok is None or next_tok.type not in _BINARY_PRECEDENCE: + break + next_prec = _BINARY_PRECEDENCE[next_tok.type] + if next_prec > prec: + rhs = self._parse_binary_expression(next_prec, rhs) + else: + break + + lhs = c_ast.BinaryOp(op, lhs, rhs, lhs.coord) + + return lhs + + # BNF: cast_expression : '(' type_name ')' cast_expression + # | unary_expression + def _parse_cast_expression(self) -> c_ast.Node: + result = self._try_parse_paren_type_name() + if result is not None: + typ, mark, lparen_tok = result + if self._peek_type() == "LBRACE": + # (type){...} is a compound literal, not a cast. Examples: + # (int){1} -> compound literal, handled in postfix + # (int) x -> cast, handled below + self._reset(mark) + else: + expr = self._parse_cast_expression() + return c_ast.Cast(typ, expr, self._tok_coord(lparen_tok)) + return self._parse_unary_expression() + + # BNF: unary_expression : postfix_expression + # | '++' unary_expression + # | '--' unary_expression + # | unary_op cast_expression + # | 'sizeof' unary_expression + # | 'sizeof' '(' type_name ')' + # | '_Alignof' '(' type_name ')' + def _parse_unary_expression(self) -> c_ast.Node: + tok_type = self._peek_type() + if tok_type in {"PLUSPLUS", "MINUSMINUS"}: + tok = self._advance() + expr = self._parse_unary_expression() + return c_ast.UnaryOp(tok.value, expr, expr.coord) + + if tok_type in {"AND", "TIMES", "PLUS", "MINUS", "NOT", "LNOT"}: + tok = self._advance() + expr = self._parse_cast_expression() + return c_ast.UnaryOp(tok.value, expr, expr.coord) + + if tok_type == "SIZEOF": + tok = self._advance() + result = self._try_parse_paren_type_name() + if result is not None: + typ, _, _ = result + return c_ast.UnaryOp(tok.value, typ, self._tok_coord(tok)) + expr = self._parse_unary_expression() + return c_ast.UnaryOp(tok.value, expr, self._tok_coord(tok)) + + if tok_type == "_ALIGNOF": + tok = self._advance() + self._expect("LPAREN") + typ = self._parse_type_name() + self._expect("RPAREN") + return c_ast.UnaryOp(tok.value, typ, self._tok_coord(tok)) + + return self._parse_postfix_expression() + + # BNF: postfix_expression : primary_expression postfix_suffix* + # | '(' type_name ')' '{' initializer_list ','? '}' + def _parse_postfix_expression(self) -> c_ast.Node: + result = self._try_parse_paren_type_name() + if result is not None: + typ, mark, _ = result + # Disambiguate between casts and compound literals: + # (int) x -> cast + # (int) {1} -> compound literal + if self._accept("LBRACE"): + init = self._parse_initializer_list() + self._accept("COMMA") + self._expect("RBRACE") + return c_ast.CompoundLiteral(typ, init) + else: + self._reset(mark) + + expr = self._parse_primary_expression() + while True: + if self._accept("LBRACKET"): + sub = self._parse_expression() + self._expect("RBRACKET") + expr = c_ast.ArrayRef(expr, sub, expr.coord) + continue + if self._accept("LPAREN"): + if self._peek_type() == "RPAREN": + self._advance() + args = None + else: + args = self._parse_argument_expression_list() + self._expect("RPAREN") + expr = c_ast.FuncCall(expr, args, expr.coord) + continue + if self._peek_type() in {"PERIOD", "ARROW"}: + op_tok = self._advance() + name_tok = self._advance() + if name_tok.type not in {"ID", "TYPEID"}: + self._parse_error( + "Invalid struct reference", self._tok_coord(name_tok) + ) + field = c_ast.ID(name_tok.value, self._tok_coord(name_tok)) + expr = c_ast.StructRef(expr, op_tok.value, field, expr.coord) + continue + if self._peek_type() in {"PLUSPLUS", "MINUSMINUS"}: + tok = self._advance() + expr = c_ast.UnaryOp("p" + tok.value, expr, expr.coord) + continue + break + return expr + + # BNF: primary_expression : ID | constant | string_literal + # | '(' expression ')' | offsetof + def _parse_primary_expression(self) -> c_ast.Node: + tok_type = self._peek_type() + if tok_type == "ID": + return self._parse_identifier() + if ( + tok_type in _INT_CONST + or tok_type in _FLOAT_CONST + or tok_type in _CHAR_CONST + ): + return self._parse_constant() + if tok_type in _STRING_LITERAL: + return self._parse_unified_string_literal() + if tok_type in _WSTR_LITERAL: + return self._parse_unified_wstring_literal() + if tok_type == "LPAREN": + self._advance() + expr = self._parse_expression() + self._expect("RPAREN") + return expr + if tok_type == "OFFSETOF": + off_tok = self._advance() + self._expect("LPAREN") + typ = self._parse_type_name() + self._expect("COMMA") + designator = self._parse_offsetof_member_designator() + self._expect("RPAREN") + coord = self._tok_coord(off_tok) + return c_ast.FuncCall( + c_ast.ID(off_tok.value, coord), + c_ast.ExprList([typ, designator], coord), + coord, + ) + + self._parse_error("Invalid expression", self.clex.filename) + + # BNF: offsetof_member_designator : identifier_or_typeid + # ('.' identifier_or_typeid | '[' expression ']')* + def _parse_offsetof_member_designator(self) -> c_ast.Node: + node = self._parse_identifier_or_typeid() + while True: + if self._accept("PERIOD"): + field = self._parse_identifier_or_typeid() + node = c_ast.StructRef(node, ".", field, node.coord) + continue + if self._accept("LBRACKET"): + expr = self._parse_expression() + self._expect("RBRACKET") + node = c_ast.ArrayRef(node, expr, node.coord) + continue + break + return node + + # BNF: argument_expression_list : assignment_expression (',' assignment_expression)* + def _parse_argument_expression_list(self) -> c_ast.Node: + expr = self._parse_assignment_expression() + exprs = [expr] + while self._accept("COMMA"): + exprs.append(self._parse_assignment_expression()) + return c_ast.ExprList(exprs, expr.coord) + + # BNF: constant_expression : conditional_expression + def _parse_constant_expression(self) -> c_ast.Node: + return self._parse_conditional_expression() + + # ------------------------------------------------------------------ + # Terminals + # ------------------------------------------------------------------ + # BNF: identifier : ID + def _parse_identifier(self) -> c_ast.Node: + tok = self._expect("ID") + return c_ast.ID(tok.value, self._tok_coord(tok)) + + # BNF: identifier_or_typeid : ID | TYPEID + def _parse_identifier_or_typeid(self) -> c_ast.Node: + tok = self._advance() + if tok.type not in {"ID", "TYPEID"}: + self._parse_error("Expected identifier", self._tok_coord(tok)) + return c_ast.ID(tok.value, self._tok_coord(tok)) + + # BNF: constant : INT_CONST | FLOAT_CONST | CHAR_CONST + def _parse_constant(self) -> c_ast.Node: + tok = self._advance() + if tok.type in _INT_CONST: + u_count = 0 + l_count = 0 + for ch in tok.value[-3:]: + if ch in ("l", "L"): + l_count += 1 + elif ch in ("u", "U"): + u_count += 1 + if u_count > 1: + raise ValueError("Constant cannot have more than one u/U suffix.") + if l_count > 2: + raise ValueError("Constant cannot have more than two l/L suffix.") + prefix = "unsigned " * u_count + "long " * l_count + return c_ast.Constant(prefix + "int", tok.value, self._tok_coord(tok)) + + if tok.type in _FLOAT_CONST: + if tok.value[-1] in ("f", "F"): + t = "float" + elif tok.value[-1] in ("l", "L"): + t = "long double" + else: + t = "double" + return c_ast.Constant(t, tok.value, self._tok_coord(tok)) + + if tok.type in _CHAR_CONST: + return c_ast.Constant("char", tok.value, self._tok_coord(tok)) + + self._parse_error("Invalid constant", self._tok_coord(tok)) + + # BNF: unified_string_literal : STRING_LITERAL+ + def _parse_unified_string_literal(self) -> c_ast.Node: + tok = self._expect("STRING_LITERAL") + node = c_ast.Constant("string", tok.value, self._tok_coord(tok)) + while self._peek_type() == "STRING_LITERAL": + tok2 = self._advance() + node.value = node.value[:-1] + tok2.value[1:] + return node + + # BNF: unified_wstring_literal : WSTRING_LITERAL+ + def _parse_unified_wstring_literal(self) -> c_ast.Node: + tok = self._advance() + if tok.type not in _WSTR_LITERAL: + self._parse_error("Invalid string literal", self._tok_coord(tok)) + node = c_ast.Constant("string", tok.value, self._tok_coord(tok)) + while self._peek_type() in _WSTR_LITERAL: + tok2 = self._advance() + node.value = node.value.rstrip()[:-1] + tok2.value[2:] + return node + + # ------------------------------------------------------------------ + # Initializers + # ------------------------------------------------------------------ + # BNF: initializer : assignment_expression + # | '{' initializer_list ','? '}' + # | '{' '}' + def _parse_initializer(self) -> c_ast.Node: + lbrace_tok = self._accept("LBRACE") + if lbrace_tok: + if self._accept("RBRACE"): + return c_ast.InitList([], self._tok_coord(lbrace_tok)) + init_list = self._parse_initializer_list() + self._accept("COMMA") + self._expect("RBRACE") + return init_list + + return self._parse_assignment_expression() + + # BNF: initializer_list : initializer_item (',' initializer_item)* ','? + def _parse_initializer_list(self) -> c_ast.Node: + items = [self._parse_initializer_item()] + while self._accept("COMMA"): + if self._peek_type() == "RBRACE": + break + items.append(self._parse_initializer_item()) + return c_ast.InitList(items, items[0].coord) + + # BNF: initializer_item : designation? initializer + def _parse_initializer_item(self) -> c_ast.Node: + designation = None + if self._peek_type() in {"LBRACKET", "PERIOD"}: + designation = self._parse_designation() + init = self._parse_initializer() + if designation is not None: + return c_ast.NamedInitializer(designation, init) + return init + + # BNF: designation : designator_list '=' + def _parse_designation(self) -> List[c_ast.Node]: + designators = self._parse_designator_list() + self._expect("EQUALS") + return designators + + # BNF: designator_list : designator+ + def _parse_designator_list(self) -> List[c_ast.Node]: + designators = [] + while self._peek_type() in {"LBRACKET", "PERIOD"}: + designators.append(self._parse_designator()) + return designators + + # BNF: designator : '[' constant_expression ']' + # | '.' identifier_or_typeid + def _parse_designator(self) -> c_ast.Node: + if self._accept("LBRACKET"): + expr = self._parse_constant_expression() + self._expect("RBRACKET") + return expr + if self._accept("PERIOD"): + return self._parse_identifier_or_typeid() + self._parse_error("Invalid designator", self.clex.filename) + + # ------------------------------------------------------------------ + # Preprocessor-like directives + # ------------------------------------------------------------------ + # BNF: pp_directive : '#' ... (unsupported) + def _parse_pp_directive(self) -> NoReturn: + tok = self._expect("PPHASH") + self._parse_error("Directives not supported yet", self._tok_coord(tok)) + + # BNF: pppragma_directive : PPPRAGMA PPPRAGMASTR? + # | _PRAGMA '(' string_literal ')' + def _parse_pppragma_directive(self) -> c_ast.Node: + if self._peek_type() == "PPPRAGMA": + tok = self._advance() + if self._peek_type() == "PPPRAGMASTR": + str_tok = self._advance() + return c_ast.Pragma(str_tok.value, self._tok_coord(str_tok)) + return c_ast.Pragma("", self._tok_coord(tok)) + + if self._peek_type() == "_PRAGMA": + tok = self._advance() + lparen = self._expect("LPAREN") + literal = self._parse_unified_string_literal() + self._expect("RPAREN") + return c_ast.Pragma(literal, self._tok_coord(lparen)) + + self._parse_error("Invalid pragma", self.clex.filename) + + # BNF: pppragma_directive_list : pppragma_directive+ + def _parse_pppragma_directive_list(self) -> List[c_ast.Node]: + pragmas = [] + while self._peek_type() in {"PPPRAGMA", "_PRAGMA"}: + pragmas.append(self._parse_pppragma_directive()) + return pragmas + + # BNF: static_assert : _STATIC_ASSERT '(' constant_expression (',' string_literal)? ')' + def _parse_static_assert(self) -> List[c_ast.Node]: + tok = self._expect("_STATIC_ASSERT") + self._expect("LPAREN") + cond = self._parse_constant_expression() + msg = None + if self._accept("COMMA"): + msg = self._parse_unified_string_literal() + self._expect("RPAREN") + return [c_ast.StaticAssert(cond, msg, self._tok_coord(tok))] + + +_ASSIGNMENT_OPS = { + "EQUALS", + "XOREQUAL", + "TIMESEQUAL", + "DIVEQUAL", + "MODEQUAL", + "PLUSEQUAL", + "MINUSEQUAL", + "LSHIFTEQUAL", + "RSHIFTEQUAL", + "ANDEQUAL", + "OREQUAL", +} + +# Precedence of operators (lower number = weather binding) +# If this changes, c_generator.CGenerator.precedence_map needs to change as +# well +_BINARY_PRECEDENCE = { + "LOR": 0, + "LAND": 1, + "OR": 2, + "XOR": 3, + "AND": 4, + "EQ": 5, + "NE": 5, + "GT": 6, + "GE": 6, + "LT": 6, + "LE": 6, + "RSHIFT": 7, + "LSHIFT": 7, + "PLUS": 8, + "MINUS": 8, + "TIMES": 9, + "DIVIDE": 9, + "MOD": 9, +} + +_STORAGE_CLASS = {"AUTO", "REGISTER", "STATIC", "EXTERN", "TYPEDEF", "_THREAD_LOCAL"} + +_FUNCTION_SPEC = {"INLINE", "_NORETURN"} + +_TYPE_QUALIFIER = {"CONST", "RESTRICT", "VOLATILE", "_ATOMIC"} + +_TYPE_SPEC_SIMPLE = { + "VOID", + "_BOOL", + "CHAR", + "SHORT", + "INT", + "LONG", + "FLOAT", + "DOUBLE", + "_COMPLEX", + "SIGNED", + "UNSIGNED", + "__INT128", +} + +_DECL_START = ( + _STORAGE_CLASS + | _FUNCTION_SPEC + | _TYPE_QUALIFIER + | _TYPE_SPEC_SIMPLE + | {"TYPEID", "STRUCT", "UNION", "ENUM", "_ALIGNAS", "_ATOMIC"} +) + +_EXPR_START = { + "ID", + "LPAREN", + "PLUSPLUS", + "MINUSMINUS", + "PLUS", + "MINUS", + "TIMES", + "AND", + "NOT", + "LNOT", + "SIZEOF", + "_ALIGNOF", + "OFFSETOF", +} + +_INT_CONST = { + "INT_CONST_DEC", + "INT_CONST_OCT", + "INT_CONST_HEX", + "INT_CONST_BIN", + "INT_CONST_CHAR", +} + +_FLOAT_CONST = {"FLOAT_CONST", "HEX_FLOAT_CONST"} + +_CHAR_CONST = { + "CHAR_CONST", + "WCHAR_CONST", + "U8CHAR_CONST", + "U16CHAR_CONST", + "U32CHAR_CONST", +} + +_STRING_LITERAL = {"STRING_LITERAL"} + +_WSTR_LITERAL = { + "WSTRING_LITERAL", + "U8STRING_LITERAL", + "U16STRING_LITERAL", + "U32STRING_LITERAL", +} + +_STARTS_EXPRESSION = ( + _EXPR_START + | _INT_CONST + | _FLOAT_CONST + | _CHAR_CONST + | _STRING_LITERAL + | _WSTR_LITERAL +) + +_STARTS_STATEMENT = { + "LBRACE", + "IF", + "SWITCH", + "WHILE", + "DO", + "FOR", + "GOTO", + "BREAK", + "CONTINUE", + "RETURN", + "CASE", + "DEFAULT", + "PPPRAGMA", + "_PRAGMA", + "_STATIC_ASSERT", + "SEMI", +} + + +class _TokenStream: + """Wraps a lexer to provide convenient, buffered access to the underlying + token stream. The lexer is expected to be initialized with the input + string already. + """ + + def __init__(self, lexer: CLexer) -> None: + self._lexer = lexer + self._buffer: List[Optional[_Token]] = [] + self._index = 0 + + def peek(self, k: int = 1) -> Optional[_Token]: + """Peek at the k-th next token in the stream, without consuming it. + + Examples: + k=1 returns the immediate next token. + k=2 returns the token after that. + """ + if k <= 0: + return None + self._fill(k) + return self._buffer[self._index + k - 1] + + def next(self) -> Optional[_Token]: + """Consume a single token and return it.""" + self._fill(1) + tok = self._buffer[self._index] + self._index += 1 + return tok + + # The 'mark' and 'reset' methods are useful for speculative parsing with + # backtracking; when the parser needs to examine a sequence of tokens + # and potentially decide to try a different path on the same sequence, it + # can call 'mark' to obtain the current token position, and if the first + # path fails restore the position with `reset(pos)`. + def mark(self) -> int: + return self._index + + def reset(self, mark: int) -> None: + self._index = mark + + def _fill(self, n: int) -> None: + while len(self._buffer) < self._index + n: + tok = self._lexer.token() + self._buffer.append(tok) + if tok is None: + break + + +# Declaration specifiers are represented by a dictionary with entries: +# - qual: a list of type qualifiers +# - storage: a list of storage class specifiers +# - type: a list of type specifiers +# - function: a list of function specifiers +# - alignment: a list of alignment specifiers +class _DeclSpec(TypedDict): + qual: List[Any] + storage: List[Any] + type: List[Any] + function: List[Any] + alignment: List[Any] + + +_DeclSpecKind = Literal["qual", "storage", "type", "function", "alignment"] + + +class _DeclInfo(TypedDict): + # Declarator payloads used by declaration/initializer parsing: + # - decl: the declarator node (may be None for abstract/implicit cases) + # - init: optional initializer expression + # - bitsize: optional bit-field width expression (for struct declarators) + decl: Optional[c_ast.Node] + init: Optional[c_ast.Node] + bitsize: Optional[c_ast.Node] diff --git a/source/target-vendors/cp312/pydantic-2.13.5.dist-info/METADATA b/source/target-vendors/cp312/pydantic-2.13.5.dist-info/METADATA new file mode 100644 index 0000000..10d0af0 --- /dev/null +++ b/source/target-vendors/cp312/pydantic-2.13.5.dist-info/METADATA @@ -0,0 +1,1307 @@ +Metadata-Version: 2.5 +Name: pydantic +Version: 2.13.5 +Summary: Data validation using Python type hints +Project-URL: Homepage, https://github.com/pydantic/pydantic +Project-URL: Documentation, https://docs.pydantic.dev +Project-URL: Funding, https://github.com/sponsors/samuelcolvin +Project-URL: Source, https://github.com/pydantic/pydantic +Project-URL: Changelog, https://docs.pydantic.dev/latest/changelog/ +Author-email: Samuel Colvin , Eric Jolibois , Hasan Ramezani , Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>, Terrence Dorsey , David Montague , Serge Matveenko , Marcelo Trylesinski , Sydney Runkle , David Hewitt , Alex Hall , Victorien Plot +License-Expression: MIT +License-File: LICENSE +Classifier: Development Status :: 5 - Production/Stable +Classifier: Framework :: Hypothesis +Classifier: Framework :: Pydantic +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Information Technology +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Internet +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires-Python: >=3.9 +Requires-Dist: annotated-types>=0.6.0 +Requires-Dist: pydantic-core==2.46.5 +Requires-Dist: typing-extensions>=4.14.1 +Requires-Dist: typing-inspection>=0.4.2 +Provides-Extra: email +Requires-Dist: email-validator>=2.0.0; extra == 'email' +Provides-Extra: timezone +Requires-Dist: tzdata; (python_version >= '3.9' and platform_system == 'Windows') and extra == 'timezone' +Description-Content-Type: text/markdown + +# Pydantic Validation + +[![CI](https://img.shields.io/github/actions/workflow/status/pydantic/pydantic/ci.yml?branch=main&logo=github&label=CI)](https://github.com/pydantic/pydantic/actions?query=event%3Apush+branch%3Amain+workflow%3ACI) +[![Coverage](https://coverage-badge.samuelcolvin.workers.dev/pydantic/pydantic.svg)](https://coverage-badge.samuelcolvin.workers.dev/redirect/pydantic/pydantic) +[![pypi](https://img.shields.io/pypi/v/pydantic.svg)](https://pypi.python.org/pypi/pydantic) +[![CondaForge](https://img.shields.io/conda/v/conda-forge/pydantic.svg)](https://anaconda.org/conda-forge/pydantic) +[![downloads](https://static.pepy.tech/badge/pydantic/month)](https://pepy.tech/project/pydantic) +[![versions](https://img.shields.io/pypi/pyversions/pydantic.svg)](https://github.com/pydantic/pydantic) +[![license](https://img.shields.io/github/license/pydantic/pydantic.svg)](https://github.com/pydantic/pydantic/blob/main/LICENSE) +[![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json)](https://docs.pydantic.dev/latest/contributing/#badges) +[![llms.txt](https://img.shields.io/badge/llms.txt-green)](https://docs.pydantic.dev/latest/llms.txt) + +Data validation using Python type hints. + +Fast and extensible, Pydantic plays nicely with your linters/IDE/brain. +Define how data should be in pure, canonical Python 3.9+; validate it with Pydantic. + +## Pydantic Logfire :fire: + +We've launched Pydantic Logfire to help you monitor your applications. +[Learn more](https://pydantic.dev/logfire/?utm_source=pydantic_validation) + +## Pydantic V1.10 vs. V2 + +Pydantic V2 is a ground-up rewrite that offers many new features, performance improvements, and some breaking changes compared to Pydantic V1. + +If you're using Pydantic V1 you may want to look at the +[pydantic V1.10 Documentation](https://docs.pydantic.dev/) or, +[`1.10.X-fixes` git branch](https://github.com/pydantic/pydantic/tree/1.10.X-fixes). Pydantic V2 also ships with the latest version of Pydantic V1 built in so that you can incrementally upgrade your code base and projects: `from pydantic import v1 as pydantic_v1`. + +## Help + +See [documentation](https://docs.pydantic.dev/) for more details. + +## Installation + +Install using `pip install -U pydantic` or `conda install pydantic -c conda-forge`. +For more installation options to make Pydantic even faster, +see the [Install](https://docs.pydantic.dev/install/) section in the documentation. + +## A Simple Example + +```python +from datetime import datetime +from typing import Optional +from pydantic import BaseModel + +class User(BaseModel): + id: int + name: str = 'John Doe' + signup_ts: Optional[datetime] = None + friends: list[int] = [] + +external_data = {'id': '123', 'signup_ts': '2017-06-01 12:22', 'friends': [1, '2', b'3']} +user = User(**external_data) +print(user) +#> User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3] +print(user.id) +#> 123 +``` + +## Contributing + +For guidance on setting up a development environment and how to make a +contribution to Pydantic, see +[Contributing to Pydantic](https://docs.pydantic.dev/contributing/). + +## Reporting a Security Vulnerability + +See our [security policy](https://github.com/pydantic/pydantic/security/policy). + +## Changelog + + + + + +## v2.13.5 (2026-08-28) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.13.5) + +### What's Changed + +#### Fixes + +* Allow reuse of validators when plugins are set by [@Viicos](https://github.com/Viicos) in [#13535](https://github.com/pydantic/pydantic/pull/13535) +* Fix missing GC traversal on some `pydantic-core` struct fields by [@Viicos](https://github.com/Viicos) in [#13624](https://github.com/pydantic/pydantic/pull/13624) +* Fix missing GC traversal in `pydantic-core` for `GeneralFieldsSerializer` by [@Viicos](https://github.com/Viicos) in [#13629](https://github.com/pydantic/pydantic/pull/13629) +* Count validated model fields once in smart unions by [@tamird](https://github.com/tamird) in [#13731](https://github.com/pydantic/pydantic/pull/13731) + +## v2.13.4 (2026-05-06) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.13.4) + +### What's Changed + +#### Packaging + +* Bump libc from 0.2.155 to 0.2.185 by [@Viicos](https://github.com/Viicos) in [#13109](https://github.com/pydantic/pydantic/pull/13109) +* Adapt `pydantic-core` linker flags on macOS by [@washingtoneg](https://github.com/washingtoneg) and [@Viicos](https://github.com/Viicos) in [#13147](https://github.com/pydantic/pydantic/pull/13147) + +#### Fixes + +* Preserve `RootModel` core metadata by [@Viicos](https://github.com/Viicos) in [#13129](https://github.com/pydantic/pydantic/pull/13129) + +## v2.13.3 (2026-04-20) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.13.3) + +### What's Changed + +#### Fixes + +* Handle `AttributeError` subclasses with `from_attributes` by [@Viicos](https://github.com/Viicos) in [#13096](https://github.com/pydantic/pydantic/pull/13096) + +## v2.13.2 (2026-04-17) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.13.2) + +### What's Changed + +#### Fixes + +* Fix `ValidationInfo.field_name` missing with `model_validate_json()` by [@Viicos](https://github.com/Viicos) in [#13084](https://github.com/pydantic/pydantic/pull/13084) + +## v2.13.1 (2026-04-15) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.13.1) + +### What's Changed + +#### Fixes + +* Fix `ValidationInfo.data` missing with `model_validate_json()` by [@davidhewitt](https://github.com/davidhewitt) in [#13079](https://github.com/pydantic/pydantic/pull/13079) + +## v2.13.0 (2026-04-13) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.13.0) + +The highlights of the v2.13 release are available in the [blog post](https://pydantic.dev/articles/pydantic-v2-13-release). +Several minor changes (considered non-breaking changes according to our [versioning policy](https://pydantic.dev/docs/validation/2.13/get-started/version-policy/#pydantic-v2)) +are also included in this release. Make sure to look into them before upgrading. + +This release contains the updated `pydantic.v1` namespace, matching version 1.10.26 which includes support for Python 3.14. + +### What's Changed + +See the beta releases for all changes sinces 2.12. + +#### New Features + +* Allow default factories of private attributes to take validated model data by [@Viicos](https://github.com/Viicos) in [#13013](https://github.com/pydantic/pydantic/pull/13013) + +#### Changes + +* Warn when serializing fixed length tuples with too few items by [@arvindsaripalli](https://github.com/arvindsaripalli) in [#13016](https://github.com/pydantic/pydantic/pull/13016) + +#### Fixes + +* Change type of `Any` when synthesizing `_build_sources` for `BaseSettings.__init__()` signature in the mypy plugin by [@Viicos](https://github.com/Viicos) in [#13049](https://github.com/pydantic/pydantic/pull/13049) +* Fix model equality when using runtime `extra` configuration by [@Viicos](https://github.com/Viicos) in [#13062](https://github.com/pydantic/pydantic/pull/13062) + +#### Packaging + +* Add zizmor for GitHub Actions workflow linting by [@Viicos](https://github.com/Viicos) in [#13039](https://github.com/pydantic/pydantic/pull/13039) +* Update jiter to v0.14.0 to fix a segmentation fault on musl Linux by [@Viicos](https://github.com/Viicos) in [#13064](https://github.com/pydantic/pydantic/pull/13064) + +### New Contributors + +* [@arvindsaripalli](https://github.com/arvindsaripalli) made their first contribution in [#13016](https://github.com/pydantic/pydantic/pull/13016) + +## v2.13.0b3 (2026-03-31) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.13.0b3) + +### What's Changed + +#### New Features + +* Add `ascii_only` option to `StringConstraints` by [@ai-man-codes](https://github.com/ai-man-codes) in [#12907](https://github.com/pydantic/pydantic/pull/12907) +* Support `exclude_if` in computed fields by [@andresliszt](https://github.com/andresliszt) in [#12748](https://github.com/pydantic/pydantic/pull/12748) +* Push down constraints in unions involving `MISSING` sentinel by [@Viicos](https://github.com/Viicos) in [#12908](https://github.com/pydantic/pydantic/pull/12908) + +#### Changes + +* Track extra fields set after init in `model_fields_set` by [@navalprakhar](https://github.com/navalprakhar) in [#12817](https://github.com/pydantic/pydantic/pull/12817) +* Do not include annotations that are not part of named tuple fields by [@galuszkak](https://github.com/galuszkak) in [#12951](https://github.com/pydantic/pydantic/pull/12951) +* No longer fall back to trying all union members when the variant selected by discriminator fails to serialize by [@navalprakhar](https://github.com/navalprakhar) in [#12825](https://github.com/pydantic/pydantic/pull/12825) + +#### Fixes + +* Support discriminator metadata outside union type alias by [@Viicos](https://github.com/Viicos) in [#12785](https://github.com/pydantic/pydantic/pull/12785) +* Respect `extras_schema` when only `extra_fields_behavior` is set on the config in JSON Schema generation for typed dictionaries by [@Viicos](https://github.com/Viicos) in [#12810](https://github.com/pydantic/pydantic/pull/12810) +* Ensure `__pydantic_private__` is set in `model_construct()` with user-defined `model_post_init()` by [@nightcityblade](https://github.com/nightcityblade) in [#12816](https://github.com/pydantic/pydantic/pull/12816) +* Handle all schema generation errors in `InstanceOf` by [@Viicos](https://github.com/Viicos) in [#12705](https://github.com/pydantic/pydantic/pull/12705) +* Allow dynamic models created with `create_model()` to be used as annotations in the Mypy plugin by [@Br1an67](https://github.com/Br1an67) in [#12879](https://github.com/pydantic/pydantic/pull/12879) +* Check for `PlaceholderNode` in Mypy plugin by [@Viicos](https://github.com/Viicos) in [#12929](https://github.com/pydantic/pydantic/pull/12929) +* Try other branches in smart union in case of omit errors by [@mikeedjones](https://github.com/mikeedjones) in [#12758](https://github.com/pydantic/pydantic/pull/12758) +* Patch unset attributes with `MISSING` during model serialization with `exclude_unset` by [@davidhewitt](https://github.com/davidhewitt) in [#12905](https://github.com/pydantic/pydantic/pull/12905) +* Ensure custom `__init__()` is called when using `model_validate_strings()` by [@siewcapital](https://github.com/siewcapital) in [#12897](https://github.com/pydantic/pydantic/pull/12897) + +#### Packaging + +* Add riscv64 build target for manylinux by [@boosterl](https://github.com/boosterl) in [#12723](https://github.com/pydantic/pydantic/pull/12723) + +### New Contributors + +* [@kelsonbrito50](https://github.com/kelsonbrito50) made their first contribution in [#12860](https://github.com/pydantic/pydantic/pull/12860) +* [@boosterl](https://github.com/boosterl) made their first contribution in [#12723](https://github.com/pydantic/pydantic/pull/12723) +* [@adityagiri3600](https://github.com/adityagiri3600) made their first contribution in [#12868](https://github.com/pydantic/pydantic/pull/12868) +* [@navalprakhar](https://github.com/navalprakhar) made their first contribution in [#12817](https://github.com/pydantic/pydantic/pull/12817) +* [@Br1an67](https://github.com/Br1an67) made their first contribution in [#12879](https://github.com/pydantic/pydantic/pull/12879) +* [@rmorshea](https://github.com/rmorshea) made their first contribution in [#12910](https://github.com/pydantic/pydantic/pull/12910) +* [@N3XT3R1337](https://github.com/N3XT3R1337) made their first contribution in [#12922](https://github.com/pydantic/pydantic/pull/12922) +* [@ai-man-codes](https://github.com/ai-man-codes) made their first contribution in [#12907](https://github.com/pydantic/pydantic/pull/12907) +* [@Yume05-dev](https://github.com/Yume05-dev) made their first contribution in [#12953](https://github.com/pydantic/pydantic/pull/12953) +* [@galuszkak](https://github.com/galuszkak) made their first contribution in [#12951](https://github.com/pydantic/pydantic/pull/12951) +* [@siewcapital](https://github.com/siewcapital) made their first contribution in [#12897](https://github.com/pydantic/pydantic/pull/12897) + +## v2.13.0b2 (2026-02-24) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.13.0b2) + +### What's Changed + +#### Fixes + +* Fix backported V1 namespace by [@Viicos](https://github.com/Viicos) in [#12855](https://github.com/pydantic/pydantic/pull/12855) +* Allow any type form to be used in `validate_as()` by [@bledden](https://github.com/bledden) in [#12846](https://github.com/pydantic/pydantic/pull/12846) +* Fix walrus operator precedence in `UrlConstraints.__get_pydantic_core_schema__()` by [@bysiber](https://github.com/bysiber) in [#12826](https://github.com/pydantic/pydantic/pull/12826) + +### New Contributors + +* [@bledden](https://github.com/bledden) made their first contribution in [#12846](https://github.com/pydantic/pydantic/pull/12846) +* [@bysiber](https://github.com/bysiber) made their first contribution in [#12827](https://github.com/pydantic/pydantic/pull/12827) + +## v2.13.0b1 (2026-02-23) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.13.0b1) + +This is the first beta release of the 2.13 version, mainly providing bug fixes and performance improvements +for validation and serialization. + +Notable changes include: + +* Add a new `polymorphic_serialization` option, solving issues with `serialize_as_any` introduced in 2.12. +* Latest V1.10.26 release under the `pydantic.v1` namespace. This version includes support for Python 3.14. +* The [`pydantic-core`](https://github.com/pydantic/pydantic-core/) repository was merged inside the main `pydantic` one. + +### What's Changed + +#### New Features + +* Add `polymorphic_serialization` option by [@davidhewitt](https://github.com/davidhewitt) in [#12518](https://github.com/pydantic/pydantic/pull/12518) +* Support Root models with `Literal` root types as discriminator field types by [@YassinNouh21](https://github.com/YassinNouh21) in [#12680](https://github.com/pydantic/pydantic/pull/12680) + +#### Changes + +* Migrate `pydantic-core` CI by [@Viicos](https://github.com/Viicos) in [#12752](https://github.com/pydantic/pydantic/pull/12752) +* Import `pydantic-core` into pydantic by [@davidhewitt](https://github.com/davidhewitt) in [#12481](https://github.com/pydantic/pydantic/pull/12481) +* Backport V1 changes up to v1.10.26 by [@Viicos](https://github.com/Viicos) in [#12663](https://github.com/pydantic/pydantic/pull/12663) +* Use the `complex()` constructor unconditionally when validating `complex` Python data by [@tanmaymunjal](https://github.com/tanmaymunjal) in [#12498](https://github.com/pydantic/pydantic/pull/12498) +* Add support for three-tuple input for `Decimal` by [@tanmaymunjal](https://github.com/tanmaymunjal) in [#12500](https://github.com/pydantic/pydantic/pull/12500) +* Align `@field_serializer` logic with `@field_validator` by [@Viicos](https://github.com/Viicos) in [#12577](https://github.com/pydantic/pydantic/pull/12577) +* Make `PydanticUserError` a `RuntimeError` instead of a `TypeError` by [@poliakovva](https://github.com/poliakovva) in [#12579](https://github.com/pydantic/pydantic/pull/12579) +* Remove redundant serialization attempts in nested unions by [@davidhewitt](https://github.com/davidhewitt) in [#12604](https://github.com/pydantic/pydantic/pull/12604) +* Copy `root` value when making root model shallow copies by [@YassinNouh21](https://github.com/YassinNouh21) in [#12679](https://github.com/pydantic/pydantic/pull/12679) +* Ensure deterministic JSON schema defaults by sorting sets by [@drshvik](https://github.com/drshvik) in [#12760](https://github.com/pydantic/pydantic/pull/12760) + +#### Performance + +* Refactor `DecoratorInfos.build()` implementation by [@Viicos](https://github.com/Viicos) in [#12536](https://github.com/pydantic/pydantic/pull/12536) +* Cache compiled regex in `pydantic-core` by [@Viicos](https://github.com/Viicos) in [#12549](https://github.com/pydantic/pydantic/pull/12549) +* Optimize creation of `Literal` validators by [@davidhewitt](https://github.com/davidhewitt) in [#12569](https://github.com/pydantic/pydantic/pull/12569) +* Optimize implementation of `LookupKey` by [@davidhewitt](https://github.com/davidhewitt) in [#12571](https://github.com/pydantic/pydantic/pull/12571) +* Use python strings for field names by [@davidhewitt](https://github.com/davidhewitt) in [#12631](https://github.com/pydantic/pydantic/pull/12631) +* Optimize datetime formatting code by [@davidhewitt](https://github.com/davidhewitt) in [#12626](https://github.com/pydantic/pydantic/pull/12626) +* Validate JSON model data by iteration by [@davidhewitt](https://github.com/davidhewitt) in [#12550](https://github.com/pydantic/pydantic/pull/12550) +* Optimize annotations evaluation of Pydantic models by [@Viicos](https://github.com/Viicos) in [#12681](https://github.com/pydantic/pydantic/pull/12681) +* Optimize `FieldInfo._copy()` by [@Viicos](https://github.com/Viicos) in [#12727](https://github.com/pydantic/pydantic/pull/12727) + +#### Fixes + +* Fix `FieldInfo` rebuilding when parameterizing generic models with an `Annotated` type by [@Viicos](https://github.com/Viicos) in [#12463](https://github.com/pydantic/pydantic/pull/12463) +* Fix nested model schema deduplication in JSON schema generation by [@marwan-alloreview](https://github.com/marwan-alloreview) in [#12494](https://github.com/pydantic/pydantic/pull/12494) +* Fix `InitVar` being ignored when using with the `pydantic.Field()` function by [@Viicos](https://github.com/Viicos) in [#12495](https://github.com/pydantic/pydantic/pull/12495) +* Fix support for enums with `NamedTuple` as values by [@Viicos](https://github.com/Viicos) in [#12506](https://github.com/pydantic/pydantic/pull/12506) +* Do not delete mock validator/serializer in `rebuild_dataclass()` by [@Viicos](https://github.com/Viicos) in [#12513](https://github.com/pydantic/pydantic/pull/12513) +* Require test suite to pass with free threading, switch back to global generic types cache by [@davidhewitt](https://github.com/davidhewitt) in [#12537](https://github.com/pydantic/pydantic/pull/12537) +* Refactor `__pydantic_extra__` annotation handling by [@Viicos](https://github.com/Viicos) in [#12563](https://github.com/pydantic/pydantic/pull/12563) +* Do not add claim of UUID "safety" provision by [@davidhewitt](https://github.com/davidhewitt) in [#12567](https://github.com/pydantic/pydantic/pull/12567) +* Use Python hash to perform lookup in tagged union serializer by [@davidhewitt](https://github.com/davidhewitt) in [#12594](https://github.com/pydantic/pydantic/pull/12594) +* Do not emit serialization warning `MISSING` sentinel is present in a nested model by [@Viicos](https://github.com/Viicos) in [#12635](https://github.com/pydantic/pydantic/pull/12635) +* Do not eagerly evaluate annotations in signature logic by [@Viicos](https://github.com/Viicos) in [#12660](https://github.com/pydantic/pydantic/pull/12660) +* Fix serialization of typed dict unions when `exclude_none` is set by [@davidhewitt](https://github.com/davidhewitt) in [#12677](https://github.com/pydantic/pydantic/pull/12677) +* Do not reuse prebuilt serializers/validators on rebuilds by [@lmmx](https://github.com/lmmx) in [#12689](https://github.com/pydantic/pydantic/pull/12689) +* Fix type annotation of `field_definitions` in `create_model()` by [@lehmann-hqs](https://github.com/lehmann-hqs) in [#12734](https://github.com/pydantic/pydantic/pull/12734) +* Fix incorrect dataclass constructor signature when overriding class `kw_only` with `Field()` by [@jfadia](https://github.com/jfadia) in [#12741](https://github.com/pydantic/pydantic/pull/12741) +* Use `typing.Union` when replacing types under Python 3.14 by [@Viicos](https://github.com/Viicos) in [#12733](https://github.com/pydantic/pydantic/pull/12733) +* Improve ImportString error when internal imports fail by [@tsembp](https://github.com/tsembp) in [#12740](https://github.com/pydantic/pydantic/pull/12740) +* Fix serializing complex numbers with negative zero imaginary part by [@lhnwrk](https://github.com/lhnwrk) in [#12770](https://github.com/pydantic/pydantic/pull/12770) +* Preserve custom docstrings on stdlib dataclasses in JSON schema by [@nightcityblade](https://github.com/nightcityblade) in [#12815](https://github.com/pydantic/pydantic/pull/12815) + +#### Packaging + +* Bump Rust url dependency from 2.5.4 to 2.5.7 in `pydantic-core` by [@dependabot](https://github.com/dependabot)[bot] in [#12508](https://github.com/pydantic/pydantic/pull/12508) +* Bump Rust minimum version to 1.88, use edition 2024 by [@davidhewitt](https://github.com/davidhewitt) and [@Viicos](https://github.com/Viicos) in [#12551](https://github.com/pydantic/pydantic/pull/12551) and [#12752](https://github.com/pydantic/pydantic/pull/12752) +* Bump PyO3 to 0.28, jiter to 0.13 by [@davidhewitt](https://github.com/davidhewitt) in [#12767](https://github.com/pydantic/pydantic/pull/12767) + +### New Contributors + +* [@marwan-alloreview](https://github.com/marwan-alloreview) made their first contribution in [#12494](https://github.com/pydantic/pydantic/pull/12494) +* [@tanmaymunjal](https://github.com/tanmaymunjal) made their first contribution in [#12498](https://github.com/pydantic/pydantic/pull/12498) +* [@poliakovva](https://github.com/poliakovva) made their first contribution in [#12579](https://github.com/pydantic/pydantic/pull/12579) +* [@lehmann-hqs](https://github.com/lehmann-hqs) made their first contribution in [#12734](https://github.com/pydantic/pydantic/pull/12734) +* [@jfadia](https://github.com/jfadia) made their first contribution in [#12741](https://github.com/pydantic/pydantic/pull/12741) +* [@tsembp](https://github.com/tsembp) made their first contribution in [#12740](https://github.com/pydantic/pydantic/pull/12740) +* [@drshvik](https://github.com/drshvik) made their first contribution in [#12760](https://github.com/pydantic/pydantic/pull/12760) +* [@lhnwrk](https://github.com/lhnwrk) made their first contribution in [#12770](https://github.com/pydantic/pydantic/pull/12770) +* [@nightcityblade](https://github.com/nightcityblade) made their first contribution in [#12815](https://github.com/pydantic/pydantic/pull/12811) + +## v2.12.5 (2025-11-26) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.12.5) + +This is the fifth 2.12 patch release, addressing an issue with the `MISSING` sentinel and providing several documentation improvements. + +The next 2.13 minor release will be published in a couple weeks, and will include a new *polymorphic serialization* feature addressing +the remaining unexpected changes to the *serialize as any* behavior. + +* Fix pickle error when using `model_construct()` on a model with `MISSING` as a default value by [@ornariece](https://github.com/ornariece) in [#12522](https://github.com/pydantic/pydantic/pull/12522). +* Several updates to the documentation by [@Viicos](https://github.com/Viicos). + +## v2.12.4 (2025-11-05) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.12.4) + +This is the fourth 2.12 patch release, fixing more regressions, and reverting a change in the `build()` method +of the [`AnyUrl` and Dsn types](https://docs.pydantic.dev/latest/api/networks/). + +This patch release also fixes an issue with the serialization of IP address types, when `serialize_as_any` is used. The next patch release +will try to address the remaining issues with *serialize as any* behavior by introducing a new *polymorphic serialization* feature, that +should be used in most cases in place of *serialize as any*. + +* Fix issue with forward references in parent `TypedDict` classes by [@Viicos](https://github.com/Viicos) in [#12427](https://github.com/pydantic/pydantic/pull/12427). + + This issue is only relevant on Python 3.14 and greater. + +* Exclude fields with `exclude_if` from JSON Schema required fields by [@Viicos](https://github.com/Viicos) in [#12430](https://github.com/pydantic/pydantic/pull/12430) +* Revert URL percent-encoding of credentials in the `build()` method + of the [`AnyUrl` and Dsn types](https://docs.pydantic.dev/latest/api/networks/) by [@davidhewitt](https://github.com/davidhewitt) in + [pydantic-core#1833](https://github.com/pydantic/pydantic-core/pull/1833). + + This was initially considered as a bugfix, but caused regressions and as such was fully reverted. The next release will include + an opt-in option to percent-encode components of the URL. + +* Add type inference for IP address types by [@davidhewitt](https://github.com/davidhewitt) in [pydantic-core#1868](https://github.com/pydantic/pydantic-core/pull/1868). + + The 2.12 changes to the `serialize_as_any` behavior made it so that IP address types could not properly serialize to JSON. + +* Avoid getting default values from defaultdict by [@davidhewitt](https://github.com/davidhewitt) in [pydantic-core#1853](https://github.com/pydantic/pydantic-core/pull/1853). + + This fixes a subtle regression in the validation behavior of the [`collections.defaultdict`](https://docs.python.org/3/library/collections.html#collections.defaultdict) + type. + +* Fix issue with field serializers on nested typed dictionaries by [@davidhewitt](https://github.com/davidhewitt) in [pydantic-core#1879](https://github.com/pydantic/pydantic-core/pull/1879). +* Add more `pydantic-core` builds for the three-threaded version of Python 3.14 by [@davidhewitt](https://github.com/davidhewitt) in [pydantic-core#1864](https://github.com/pydantic/pydantic-core/pull/1864). + +## v2.12.3 (2025-10-17) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.12.3) + +### What's Changed + +This is the third 2.12 patch release, fixing issues related to the `FieldInfo` class, and reverting a change to the supported +[*after* model validator](https://docs.pydantic.dev/latest/concepts/validators/#model-validators) function signatures. + +* Raise a warning when an invalid after model validator function signature is raised by [@Viicos](https://github.com/Viicos) in [#12414](https://github.com/pydantic/pydantic/pull/12414). + Starting in 2.12.0, using class methods for *after* model validators raised an error, but the error wasn't raised concistently. We decided + to emit a deprecation warning instead. +* Add [`FieldInfo.asdict()`](https://docs.pydantic.dev/latest/api/fields/#pydantic.fields.FieldInfo.asdict) method, improve documentation around `FieldInfo` by [@Viicos](https://github.com/Viicos) in [#12411](https://github.com/pydantic/pydantic/pull/12411). + This also add back support for mutations on `FieldInfo` classes, that are reused as `Annotated` metadata. **However**, note that this is still + *not* a supported pattern. Instead, please refer to the [added example](https://docs.pydantic.dev/latest/examples/dynamic_models/) in the documentation. + +The [blog post](https://pydantic.dev/articles/pydantic-v2-12-release#changes) section on changes was also updated to document the changes related to `serialize_as_any`. + +## v2.12.2 (2025-10-14) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.12.2) + +### What's Changed + +#### Fixes + +* Release a new `pydantic-core` version, as a corrupted CPython 3.10 `manylinux2014_aarch64` wheel got uploaded ([pydantic-core#1843](https://github.com/pydantic/pydantic-core/pull/1843)). +* Fix issue with recursive generic models with a parent model class by [@Viicos](https://github.com/Viicos) in [#12398](https://github.com/pydantic/pydantic/pull/12398) + +## v2.12.1 (2025-10-13) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.12.1) + +### What's Changed + +This is the first 2.12 patch release, addressing most (but not all yet) regressions from the initial 2.12.0 release. + +#### Fixes + +* Do not evaluate annotations when inspecting validators and serializers by [@Viicos](https://github.com/Viicos) in [#12355](https://github.com/pydantic/pydantic/pull/12355) +* Make sure `None` is converted as `NoneType` in Python 3.14 by [@Viicos](https://github.com/Viicos) in [#12370](https://github.com/pydantic/pydantic/pull/12370) +* Backport V1 runtime warning when using Python 3.14 by [@Viicos](https://github.com/Viicos) in [#12367](https://github.com/pydantic/pydantic/pull/12367) +* Fix error message for invalid validator signatures by [@Viicos](https://github.com/Viicos) in [#12366](https://github.com/pydantic/pydantic/pull/12366) +* Populate field name in `ValidationInfo` for validation of default value by [@Viicos](https://github.com/Viicos) in [pydantic-core#1826](https://github.com/pydantic/pydantic-core/pull/1826) +* Encode credentials in `MultiHostUrl` builder by [@willswire](https://github.com/willswire) in [pydantic-core#1829](https://github.com/pydantic/pydantic-core/pull/1829) +* Respect field serializers when using `serialize_as_any` serialization flag by [@davidhewitt](https://github.com/davidhewitt) in [pydantic-core#1829](https://github.com/pydantic/pydantic-core/pull/1829) +* Fix various `RootModel` serialization issues by [@davidhewitt](https://github.com/davidhewitt) in [pydantic-core#1836](https://github.com/pydantic/pydantic-core/pull/1836) + +### New Contributors + +* [@willswire](https://github.com/willswire) made their first contribution in [pydantic-core#1829](https://github.com/pydantic/pydantic-core/pull/1829) + +## v2.12.0 (2025-10-07) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.12.0) + +This is the final 2.12 release. It features the work of 20 external contributors and provides useful new features, along with initial Python 3.14 support. +Several minor changes (considered non-breaking changes according to our [versioning policy](https://docs.pydantic.dev/2.12/version-policy/#pydantic-v2)) +are also included in this release. Make sure to look into them before upgrading. + +**Note that Pydantic V1 is not compatible with Python 3.14 and greater**. + +### What's Changed + +See the beta releases for all changes sinces 2.11. + +#### New Features + +* Add `extra` parameter to the validate functions by [@anvilpete](https://github.com/anvilpete) in [#12233](https://github.com/pydantic/pydantic/pull/12233) +* Add `exclude_computed_fields` serialization option by [@Viicos](https://github.com/Viicos) in [#12334](https://github.com/pydantic/pydantic/pull/12334) +* Add `preverse_empty_path` URL options by [@Viicos](https://github.com/Viicos) in [#12336](https://github.com/pydantic/pydantic/pull/12336) +* Add `union_format` parameter to JSON Schema generation by [@Viicos](https://github.com/Viicos) in [#12147](https://github.com/pydantic/pydantic/pull/12147) +* Add `__qualname__` parameter for `create_model` by [@Atry](https://github.com/Atry) in [#12001](https://github.com/pydantic/pydantic/pull/12001) + +#### Fixes + +* Do not try to infer name from lambda definitions in pipelines API by [@Viicos](https://github.com/Viicos) in [#12289](https://github.com/pydantic/pydantic/pull/12289) +* Use proper namespace for functions in `TypeAdapter` by [@Viicos](https://github.com/Viicos) in [#12324](https://github.com/pydantic/pydantic/pull/12324) +* Use `Any` for context type annotation in `TypeAdapter` by [@inducer](https://github.com/inducer) in [#12279](https://github.com/pydantic/pydantic/pull/12279) +* Expose `FieldInfo` in `pydantic.fields.__all__` by [@Viicos](https://github.com/Viicos) in [#12339](https://github.com/pydantic/pydantic/pull/12339) +* Respect `validation_alias` in `@validate_call` by [@Viicos](https://github.com/Viicos) in [#12340](https://github.com/pydantic/pydantic/pull/12340) +* Use `Any` as context annotation in plugin API by [@Viicos](https://github.com/Viicos) in [#12341](https://github.com/pydantic/pydantic/pull/12341) +* Use proper `stacklevel` in warnings when possible by [@Viicos](https://github.com/Viicos) in [#12342](https://github.com/pydantic/pydantic/pull/12342) + +#### Packaging + +* Update V1 copy to v1.10.24 by [@Viicos](https://github.com/Viicos) in [#12338](https://github.com/pydantic/pydantic/pull/12338) + +### New Contributors + +* [@anvilpete](https://github.com/anvilpete) made their first contribution in [#12233](https://github.com/pydantic/pydantic/pull/12233) +* [@JonathanWindell](https://github.com/JonathanWindell) made their first contribution in [#12327](https://github.com/pydantic/pydantic/pull/12327) +* [@inducer](https://github.com/inducer) made their first contribution in [#12279](https://github.com/pydantic/pydantic/pull/12279) +* [@Atry](https://github.com/Atry) made their first contribution in [#12001](https://github.com/pydantic/pydantic/pull/12001) + +## v2.12.0b1 (2025-10-03) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.12.0b1) + +This is the first beta release of the upcoming 2.12 release. + +### What's Changed + +#### New Features + +* Add support for `exclude_if` at the field level by [@andresliszt](https://github.com/andresliszt) in [#12141](https://github.com/pydantic/pydantic/pull/12141) +* Add `ValidateAs` annotation helper by [@Viicos](https://github.com/Viicos) in [#11942](https://github.com/pydantic/pydantic/pull/11942) +* Add configuration options for validation and JSON serialization of temporal types by [@ollz272](https://github.com/ollz272) in [#12068](https://github.com/pydantic/pydantic/pull/12068) +* Add support for PEP 728 by [@Viicos](https://github.com/Viicos) in [#12179](https://github.com/pydantic/pydantic/pull/12179) +* Add field name in serialization error by [@NicolasPllr1](https://github.com/NicolasPllr1) in [pydantic-core#1799](https://github.com/pydantic/pydantic-core/pull/1799) +* Add option to preserve empty URL paths by [@davidhewitt](https://github.com/davidhewitt) in [pydantic-core#1789](https://github.com/pydantic/pydantic-core/pull/1789) + +#### Changes + +* Raise error if an incompatible `pydantic-core` version is installed by [@Viicos](https://github.com/Viicos) in [#12196](https://github.com/pydantic/pydantic/pull/12196) +* Remove runtime warning for experimental features by [@Viicos](https://github.com/Viicos) in [#12265](https://github.com/pydantic/pydantic/pull/12265) +* Warn if registering virtual subclasses on Pydantic models by [@Viicos](https://github.com/Viicos) in [#11669](https://github.com/pydantic/pydantic/pull/11669) + +#### Fixes + +* Fix `__getattr__()` behavior on Pydantic models when a property raised an `AttributeError` and extra values are present by [@raspuchin](https://github.com/raspuchin) in [#12106](https://github.com/pydantic/pydantic/pull/12106) +* Add test to prevent regression with Pydantic models used as annotated metadata by [@Viicos](https://github.com/Viicos) in [#12133](https://github.com/pydantic/pydantic/pull/12133) +* Allow to use property setters on Pydantic dataclasses with `validate_assignment` set by [@Viicos](https://github.com/Viicos) in [#12173](https://github.com/pydantic/pydantic/pull/12173) +* Fix mypy v2 plugin for upcoming mypy release by [@cdce8p](https://github.com/cdce8p) in [#12209](https://github.com/pydantic/pydantic/pull/12209) +* Respect custom title in functions JSON Schema by [@Viicos](https://github.com/Viicos) in [#11892](https://github.com/pydantic/pydantic/pull/11892) +* Fix `ImportString` JSON serialization for objects with a `name` attribute by [@chr1sj0nes](https://github.com/chr1sj0nes) in [#12219](https://github.com/pydantic/pydantic/pull/12219) +* Do not error on fields overridden by methods in the mypy plugin by [@Viicos](https://github.com/Viicos) in [#12290](https://github.com/pydantic/pydantic/pull/12290) + +#### Packaging + +* Bump `pydantic-core` to v2.40.1 by [@Viicos](https://github.com/Viicos) in [#12314](https://github.com/pydantic/pydantic/pull/12314) + +### New Contributors + +* [@raspuchin](https://github.com/raspuchin) made their first contribution in [#12106](https://github.com/pydantic/pydantic/pull/12106) +* [@chr1sj0nes](https://github.com/chr1sj0nes) made their first contribution in [#12219](https://github.com/pydantic/pydantic/pull/12219) + +## v2.12.0a1 (2025-07-26) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.12.0a1) + +This is the first alpha release of the upcoming 2.12 release, which adds initial support for Python 3.14. + +### What's Changed + +#### New Features + +* Add `__pydantic_on_complete__()` hook that is called once model is fully ready to be used by [@DouweM](https://github.com/DouweM) in [#11762](https://github.com/pydantic/pydantic/pull/11762) +* Add initial support for Python 3.14 by [@Viicos](https://github.com/Viicos) in [#11991](https://github.com/pydantic/pydantic/pull/11991) +* Add regex patterns to JSON schema for `Decimal` type by [@Dima-Bulavenko](https://github.com/Dima-Bulavenko) in [#11987](https://github.com/pydantic/pydantic/pull/11987) +* Add support for `doc` attribute on dataclass fields by [@Viicos](https://github.com/Viicos) in [#12077](https://github.com/pydantic/pydantic/pull/12077) +* Add experimental `MISSING` sentinel by [@Viicos](https://github.com/Viicos) in [#11883](https://github.com/pydantic/pydantic/pull/11883) + +#### Changes + +* Allow config and bases to be specified together in `create_model()` by [@Viicos](https://github.com/Viicos) in [#11714](https://github.com/pydantic/pydantic/pull/11714) +* Move some field logic out of the `GenerateSchema` class by [@Viicos](https://github.com/Viicos) in [#11733](https://github.com/pydantic/pydantic/pull/11733) +* Always make use of `inspect.getsourcelines()` for docstring extraction on Python 3.13 and greater by [@Viicos](https://github.com/Viicos) in [#11829](https://github.com/pydantic/pydantic/pull/11829) +* Only support the latest Mypy version by [@Viicos](https://github.com/Viicos) in [#11832](https://github.com/pydantic/pydantic/pull/11832) +* Do not implicitly convert after model validators to class methods by [@Viicos](https://github.com/Viicos) in [#11957](https://github.com/pydantic/pydantic/pull/11957) +* Refactor `FieldInfo` creation implementation by [@Viicos](https://github.com/Viicos) in [#11898](https://github.com/pydantic/pydantic/pull/11898) +* Make `Secret` covariant by [@bluenote10](https://github.com/bluenote10) in [#12008](https://github.com/pydantic/pydantic/pull/12008) +* Emit warning when field-specific metadata is used in invalid contexts by [@Viicos](https://github.com/Viicos) in [#12028](https://github.com/pydantic/pydantic/pull/12028) + +#### Fixes + +* Properly fetch plain serializer function when serializing default value in JSON Schema by [@Viicos](https://github.com/Viicos) in [#11721](https://github.com/pydantic/pydantic/pull/11721) +* Remove generics cache workaround by [@Viicos](https://github.com/Viicos) in [#11755](https://github.com/pydantic/pydantic/pull/11755) +* Remove coercion of decimal constraints by [@Viicos](https://github.com/Viicos) in [#11772](https://github.com/pydantic/pydantic/pull/11772) +* Fix crash when expanding root type in the mypy plugin by [@Viicos](https://github.com/Viicos) in [#11735](https://github.com/pydantic/pydantic/pull/11735) +* Only mark model as complete once all fields are complete by [@DouweM](https://github.com/DouweM) in [#11759](https://github.com/pydantic/pydantic/pull/11759) +* Do not provide `field_name` in validator core schemas by [@DouweM](https://github.com/DouweM) in [#11761](https://github.com/pydantic/pydantic/pull/11761) +* Fix issue with recursive generic models by [@Viicos](https://github.com/Viicos) in [#11775](https://github.com/pydantic/pydantic/pull/11775) +* Fix qualified name comparison of private attributes during namespace inspection by [@karta9821](https://github.com/karta9821) in [#11803](https://github.com/pydantic/pydantic/pull/11803) +* Make sure Pydantic dataclasses with slots and `validate_assignment` can be unpickled by [@Viicos](https://github.com/Viicos) in [#11769](https://github.com/pydantic/pydantic/pull/11769) +* Traverse `function-before` schemas during schema gathering by [@Viicos](https://github.com/Viicos) in [#11801](https://github.com/pydantic/pydantic/pull/11801) +* Fix check for stdlib dataclasses by [@Viicos](https://github.com/Viicos) in [#11822](https://github.com/pydantic/pydantic/pull/11822) +* Check if `FieldInfo` is complete after applying type variable map by [@Viicos](https://github.com/Viicos) in [#11855](https://github.com/pydantic/pydantic/pull/11855) +* Do not delete mock validator/serializer in `model_rebuild()` by [@Viicos](https://github.com/Viicos) in [#11890](https://github.com/pydantic/pydantic/pull/11890) +* Rebuild dataclass fields before schema generation by [@Viicos](https://github.com/Viicos) in [#11949](https://github.com/pydantic/pydantic/pull/11949) +* Always store the original field assignment on `FieldInfo` by [@Viicos](https://github.com/Viicos) in [#11946](https://github.com/pydantic/pydantic/pull/11946) +* Do not use deprecated methods as default field values by [@Viicos](https://github.com/Viicos) in [#11914](https://github.com/pydantic/pydantic/pull/11914) +* Allow callable discriminator to be applied on PEP 695 type aliases by [@Viicos](https://github.com/Viicos) in [#11941](https://github.com/pydantic/pydantic/pull/11941) +* Suppress core schema generation warning when using `SkipValidation` by [@ygsh0816](https://github.com/ygsh0816) in [#12002](https://github.com/pydantic/pydantic/pull/12002) +* Do not emit typechecking error for invalid `Field()` default with `validate_default` set to `True` by [@Viicos](https://github.com/Viicos) in [#11988](https://github.com/pydantic/pydantic/pull/11988) +* Refactor logic to support Pydantic's `Field()` function in dataclasses by [@Viicos](https://github.com/Viicos) in [#12051](https://github.com/pydantic/pydantic/pull/12051) + +#### Packaging + +* Update project metadata to use PEP 639 by [@Viicos](https://github.com/Viicos) in [#11694](https://github.com/pydantic/pydantic/pull/11694) +* Bump `mkdocs-llmstxt` to v0.2.0 by [@Viicos](https://github.com/Viicos) in [#11725](https://github.com/pydantic/pydantic/pull/11725) +* Bump `pydantic-core` to v2.35.1 by [@Viicos](https://github.com/Viicos) in [#11963](https://github.com/pydantic/pydantic/pull/11963) +* Bump dawidd6/action-download-artifact from 10 to 11 by [@dependabot](https://github.com/dependabot)[bot] in [#12033](https://github.com/pydantic/pydantic/pull/12033) +* Bump astral-sh/setup-uv from 5 to 6 by [@dependabot](https://github.com/dependabot)[bot] in [#11826](https://github.com/pydantic/pydantic/pull/11826) +* Update mypy to 1.17.0 by [@Viicos](https://github.com/Viicos) in [#12076](https://github.com/pydantic/pydantic/pull/12076) + +### New Contributors + +* [@parth-paradkar](https://github.com/parth-paradkar) made their first contribution in [#11695](https://github.com/pydantic/pydantic/pull/11695) +* [@dqkqd](https://github.com/dqkqd) made their first contribution in [#11739](https://github.com/pydantic/pydantic/pull/11739) +* [@fhightower](https://github.com/fhightower) made their first contribution in [#11722](https://github.com/pydantic/pydantic/pull/11722) +* [@gbaian10](https://github.com/gbaian10) made their first contribution in [#11766](https://github.com/pydantic/pydantic/pull/11766) +* [@DouweM](https://github.com/DouweM) made their first contribution in [#11759](https://github.com/pydantic/pydantic/pull/11759) +* [@bowenliang123](https://github.com/bowenliang123) made their first contribution in [#11719](https://github.com/pydantic/pydantic/pull/11719) +* [@rawwar](https://github.com/rawwar) made their first contribution in [#11799](https://github.com/pydantic/pydantic/pull/11799) +* [@karta9821](https://github.com/karta9821) made their first contribution in [#11803](https://github.com/pydantic/pydantic/pull/11803) +* [@jinnovation](https://github.com/jinnovation) made their first contribution in [#11834](https://github.com/pydantic/pydantic/pull/11834) +* [@zmievsa](https://github.com/zmievsa) made their first contribution in [#11861](https://github.com/pydantic/pydantic/pull/11861) +* [@Otto-AA](https://github.com/Otto-AA) made their first contribution in [#11860](https://github.com/pydantic/pydantic/pull/11860) +* [@ygsh0816](https://github.com/ygsh0816) made their first contribution in [#12002](https://github.com/pydantic/pydantic/pull/12002) +* [@lukland](https://github.com/lukland) made their first contribution in [#12015](https://github.com/pydantic/pydantic/pull/12015) +* [@Dima-Bulavenko](https://github.com/Dima-Bulavenko) made their first contribution in [#11987](https://github.com/pydantic/pydantic/pull/11987) +* [@GSemikozov](https://github.com/GSemikozov) made their first contribution in [#12050](https://github.com/pydantic/pydantic/pull/12050) +* [@hannah-heywa](https://github.com/hannah-heywa) made their first contribution in [#12082](https://github.com/pydantic/pydantic/pull/12082) + +## v2.11.10 (2025-10-04) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.11.10) + +### What's Changed + +#### Fixes + +* Backport v1.10.24 changes by [@Viicos](https://github.com/Viicos) + +## v2.11.9 (2025-09-13) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.11.9) + +### What's Changed + +#### Fixes + +* Backport v1.10.23 changes by [@Viicos](https://github.com/Viicos) + +## v2.11.8 (2025-09-13) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.11.8) + +### What's Changed + +#### Fixes + +* Fix mypy plugin for mypy 1.18 by [@cdce8p](https://github.com/cdce8p) in [#12209](https://github.com/pydantic/pydantic/pull/12209) + +## v2.11.7 (2025-06-14) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.11.7) + +### What's Changed + +#### Fixes + +* Copy `FieldInfo` instance if necessary during `FieldInfo` build by [@Viicos](https://github.com/Viicos) in [#11898](https://github.com/pydantic/pydantic/pull/11898) + +## v2.11.6 (2025-06-13) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.11.6) + +### What's Changed + +#### Fixes + +* Rebuild dataclass fields before schema generation by [@Viicos](https://github.com/Viicos) in [#11949](https://github.com/pydantic/pydantic/pull/11949) +* Always store the original field assignment on `FieldInfo` by [@Viicos](https://github.com/Viicos) in [#11946](https://github.com/pydantic/pydantic/pull/11946) + +## v2.11.5 (2025-05-22) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.11.5) + +### What's Changed + +#### Fixes + +* Check if `FieldInfo` is complete after applying type variable map by [@Viicos](https://github.com/Viicos) in [#11855](https://github.com/pydantic/pydantic/pull/11855) +* Do not delete mock validator/serializer in `model_rebuild()` by [@Viicos](https://github.com/Viicos) in [#11890](https://github.com/pydantic/pydantic/pull/11890) +* Do not duplicate metadata on model rebuild by [@Viicos](https://github.com/Viicos) in [#11902](https://github.com/pydantic/pydantic/pull/11902) + +## v2.11.4 (2025-04-29) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.11.4) + +### What's Changed + +#### Changes + +* Allow config and bases to be specified together in `create_model()` by [@Viicos](https://github.com/Viicos) in [#11714](https://github.com/pydantic/pydantic/pull/11714). + This change was backported as it was previously possible (although not meant to be supported) + to provide `model_config` as a field, which would make it possible to provide both configuration + and bases. + +#### Fixes + +* Remove generics cache workaround by [@Viicos](https://github.com/Viicos) in [#11755](https://github.com/pydantic/pydantic/pull/11755) +* Remove coercion of decimal constraints by [@Viicos](https://github.com/Viicos) in [#11772](https://github.com/pydantic/pydantic/pull/11772) +* Fix crash when expanding root type in the mypy plugin by [@Viicos](https://github.com/Viicos) in [#11735](https://github.com/pydantic/pydantic/pull/11735) +* Fix issue with recursive generic models by [@Viicos](https://github.com/Viicos) in [#11775](https://github.com/pydantic/pydantic/pull/11775) +* Traverse `function-before` schemas during schema gathering by [@Viicos](https://github.com/Viicos) in [#11801](https://github.com/pydantic/pydantic/pull/11801) + +#### Packaging + +* Bump `mkdocs-llmstxt` to v0.2.0 by [@Viicos](https://github.com/Viicos) in [#11725](https://github.com/pydantic/pydantic/pull/11725) + +## v2.11.3 (2025-04-08) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.11.3) + +### What's Changed + +#### Fixes + +* Preserve field description when rebuilding model fields by [@Viicos](https://github.com/Viicos) in [#11698](https://github.com/pydantic/pydantic/pull/11698) + +#### Packaging + +* Update V1 copy to v1.10.21 by [@Viicos](https://github.com/Viicos) in [#11706](https://github.com/pydantic/pydantic/pull/11706) + +## v2.11.2 (2025-04-03) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.11.2) + +### What's Changed + +#### Fixes + +* Bump `pydantic-core` to v2.33.1 by [@Viicos](https://github.com/Viicos) in [#11678](https://github.com/pydantic/pydantic/pull/11678) +* Make sure `__pydantic_private__` exists before setting private attributes by [@Viicos](https://github.com/Viicos) in [#11666](https://github.com/pydantic/pydantic/pull/11666) +* Do not override `FieldInfo._complete` when using field from parent class by [@Viicos](https://github.com/Viicos) in [#11668](https://github.com/pydantic/pydantic/pull/11668) +* Provide the available definitions when applying discriminated unions by [@Viicos](https://github.com/Viicos) in [#11670](https://github.com/pydantic/pydantic/pull/11670) +* Do not expand root type in the mypy plugin for variables by [@Viicos](https://github.com/Viicos) in [#11676](https://github.com/pydantic/pydantic/pull/11676) +* Mention the attribute name in model fields deprecation message by [@Viicos](https://github.com/Viicos) in [#11674](https://github.com/pydantic/pydantic/pull/11674) +* Properly validate parameterized mappings by [@Viicos](https://github.com/Viicos) in [#11658](https://github.com/pydantic/pydantic/pull/11658) + +## v2.11.1 (2025-03-28) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.11.1) + +### What's Changed + +#### Fixes + +* Do not override `'definitions-ref'` schemas containing serialization schemas or metadata by [@Viicos](https://github.com/Viicos) in [#11644](https://github.com/pydantic/pydantic/pull/11644) + +## v2.11.0 (2025-03-27) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.11.0) + +### What's Changed + +Pydantic v2.11 is a version strongly focused on build time performance of Pydantic models (and core schema generation in general). +See the [blog post](https://pydantic.dev/articles/pydantic-v2-11-release) for more details. + +#### New Features + +* Add `encoded_string()` method to the URL types by [@YassinNouh21](https://github.com/YassinNouh21) in [#11580](https://github.com/pydantic/pydantic/pull/11580) +* Add support for `defer_build` with `@validate_call` decorator by [@Viicos](https://github.com/Viicos) in [#11584](https://github.com/pydantic/pydantic/pull/11584) +* Allow `@with_config` decorator to be used with keyword arguments by [@Viicos](https://github.com/Viicos) in [#11608](https://github.com/pydantic/pydantic/pull/11608) +* Simplify customization of default value inclusion in JSON Schema generation by [@Viicos](https://github.com/Viicos) in [#11634](https://github.com/pydantic/pydantic/pull/11634) +* Add `generate_arguments_schema()` function by [@Viicos](https://github.com/Viicos) in [#11572](https://github.com/pydantic/pydantic/pull/11572) + +#### Fixes + +* Allow generic typed dictionaries to be used for unpacked variadic keyword parameters by [@Viicos](https://github.com/Viicos) in [#11571](https://github.com/pydantic/pydantic/pull/11571) +* Fix runtime error when computing model string representation involving cached properties and self-referenced models by [@Viicos](https://github.com/Viicos) in [#11579](https://github.com/pydantic/pydantic/pull/11579) +* Preserve other steps when using the ellipsis in the pipeline API by [@Viicos](https://github.com/Viicos) in [#11626](https://github.com/pydantic/pydantic/pull/11626) +* Fix deferred discriminator application logic by [@Viicos](https://github.com/Viicos) in [#11591](https://github.com/pydantic/pydantic/pull/11591) + +#### Packaging + +* Bump `pydantic-core` to v2.33.0 by [@Viicos](https://github.com/Viicos) in [#11631](https://github.com/pydantic/pydantic/pull/11631) + +### New Contributors + +* [@cmenon12](https://github.com/cmenon12) made their first contribution in [#11562](https://github.com/pydantic/pydantic/pull/11562) +* [@Jeukoh](https://github.com/Jeukoh) made their first contribution in [#11611](https://github.com/pydantic/pydantic/pull/11611) + +## v2.11.0b2 (2025-03-17) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.11.0b2) + +### What's Changed + +#### New Features + +* Add experimental support for free threading by [@Viicos](https://github.com/Viicos) in [#11516](https://github.com/pydantic/pydantic/pull/11516) + +#### Fixes + +* Fix `NotRequired` qualifier not taken into account in stringified annotation by [@Viicos](https://github.com/Viicos) in [#11559](https://github.com/pydantic/pydantic/pull/11559) + +#### Packaging + +* Bump `pydantic-core` to v2.32.0 by [@Viicos](https://github.com/Viicos) in [#11567](https://github.com/pydantic/pydantic/pull/11567) + +### New Contributors + +* [@joren485](https://github.com/joren485) made their first contribution in [#11547](https://github.com/pydantic/pydantic/pull/11547) + +## v2.11.0b1 (2025-03-06) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.11.0b1) + +### What's Changed + +#### New Features + +* Support unsubstituted type variables with both a default and a bound or constraints by [@FyZzyss](https://github.com/FyZzyss) in https://github.com/pydantic/pydantic/pull/10789 +* Add a `default_factory_takes_validated_data` property to `FieldInfo` by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11034 +* Raise a better error when a generic alias is used inside `type[]` by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11088 +* Properly support PEP 695 generics syntax by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11189 +* Properly support type variable defaults by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11332 +* Add support for validating v6, v7, v8 UUIDs by [@astei](https://github.com/astei) in https://github.com/pydantic/pydantic/pull/11436 +* Improve alias configuration APIs by [@sydney-runkle](https://github.com/sydney-runkle) in https://github.com/pydantic/pydantic/pull/11468 + +#### Changes + +* Rework `create_model` field definitions format by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11032 +* Raise a deprecation warning when a field is annotated as final with a default value by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11168 +* Deprecate accessing `model_fields` and `model_computed_fields` on instances by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11169 +* **Breaking Change:** Move core schema generation logic for path types inside the `GenerateSchema` class by [@sydney-runkle](https://github.com/sydney-runkle) in https://github.com/pydantic/pydantic/pull/10846 +* Remove Python 3.8 Support by [@sydney-runkle](https://github.com/sydney-runkle) in https://github.com/pydantic/pydantic/pull/11258 +* Optimize calls to `get_type_ref` by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/10863 +* Disable `pydantic-core` core schema validation by [@sydney-runkle](https://github.com/sydney-runkle) in https://github.com/pydantic/pydantic/pull/11271 + +#### Performance + +* Only evaluate `FieldInfo` annotations if required during schema building by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/10769 +* Improve `__setattr__` performance of Pydantic models by caching setter functions by [@MarkusSintonen](https://github.com/MarkusSintonen) in https://github.com/pydantic/pydantic/pull/10868 +* Improve annotation application performance by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11186 +* Improve performance of `_typing_extra` module by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11255 +* Refactor and optimize schema cleaning logic by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11244 +* Create a single dictionary when creating a `CoreConfig` instance by [@sydney-runkle](https://github.com/sydney-runkle) in https://github.com/pydantic/pydantic/pull/11384 +* Bump `pydantic-core` and thus use `SchemaValidator` and `SchemaSerializer` caching by [@sydney-runkle](https://github.com/sydney-runkle) in https://github.com/pydantic/pydantic/pull/11402 +* Reuse cached core schemas for parametrized generic Pydantic models by [@MarkusSintonen](https://github.com/MarkusSintonen) in https://github.com/pydantic/pydantic/pull/11434 + +#### Fixes + +* Improve `TypeAdapter` instance repr by [@sydney-runkle](https://github.com/sydney-runkle) in https://github.com/pydantic/pydantic/pull/10872 +* Use the correct frame when instantiating a parametrized `TypeAdapter` by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/10893 +* Infer final fields with a default value as class variables in the mypy plugin by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11121 +* Recursively unpack `Literal` values if using PEP 695 type aliases by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11114 +* Override `__subclasscheck__` on `ModelMetaclass` to avoid memory leak and performance issues by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11116 +* Remove unused `_extract_get_pydantic_json_schema()` parameter by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11155 +* Improve discriminated union error message for invalid union variants by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11161 +* Unpack PEP 695 type aliases if using the `Annotated` form by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11109 +* Add missing stacklevel in `deprecated_instance_property` warning by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11200 +* Copy `WithJsonSchema` schema to avoid sharing mutated data by [@thejcannon](https://github.com/thejcannon) in https://github.com/pydantic/pydantic/pull/11014 +* Do not cache parametrized models when in the process of parametrizing another model by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/10704 +* Add discriminated union related metadata entries to the `CoreMetadata` definition by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11216 +* Consolidate schema definitions logic in the `_Definitions` class by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11208 +* Support initializing root model fields with values of the `root` type in the mypy plugin by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11212 +* Fix various issues with dataclasses and `use_attribute_docstrings` by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11246 +* Only compute normalized decimal places if necessary in `decimal_places_validator` by [@misrasaurabh1](https://github.com/misrasaurabh1) in https://github.com/pydantic/pydantic/pull/11281 +* Add support for `validation_alias` in the mypy plugin by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11295 +* Fix JSON Schema reference collection with `"examples"` keys by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11305 +* Do not transform model serializer functions as class methods in the mypy plugin by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11298 +* Simplify `GenerateJsonSchema.literal_schema()` implementation by [@misrasaurabh1](https://github.com/misrasaurabh1) in https://github.com/pydantic/pydantic/pull/11321 +* Add additional allowed schemes for `ClickHouseDsn` by [@Maze21127](https://github.com/Maze21127) in https://github.com/pydantic/pydantic/pull/11319 +* Coerce decimal constraints to `Decimal` instances by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11350 +* Use the correct JSON Schema mode when handling function schemas by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11367 +* Improve exception message when encountering recursion errors during type evaluation by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11356 +* Always include `additionalProperties: True` for arbitrary dictionary schemas by [@austinyu](https://github.com/austinyu) in https://github.com/pydantic/pydantic/pull/11392 +* Expose `fallback` parameter in serialization methods by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11398 +* Fix path serialization behavior by [@sydney-runkle](https://github.com/sydney-runkle) in https://github.com/pydantic/pydantic/pull/11416 +* Do not reuse validators and serializers during model rebuild by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11429 +* Collect model fields when rebuilding a model by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11388 +* Allow cached properties to be altered on frozen models by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11432 +* Fix tuple serialization for `Sequence` types by [@sydney-runkle](https://github.com/sydney-runkle) in https://github.com/pydantic/pydantic/pull/11435 +* Fix: do not check for `__get_validators__` on classes where `__get_pydantic_core_schema__` is also defined by [@tlambert03](https://github.com/tlambert03) in https://github.com/pydantic/pydantic/pull/11444 +* Allow callable instances to be used as serializers by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11451 +* Improve error thrown when overriding field with a property by [@sydney-runkle](https://github.com/sydney-runkle) in https://github.com/pydantic/pydantic/pull/11459 +* Fix JSON Schema generation with referenceable core schemas holding JSON metadata by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11475 +* Support strict specification on union member types by [@sydney-runkle](https://github.com/sydney-runkle) in https://github.com/pydantic/pydantic/pull/11481 +* Implicitly set `validate_by_name` to `True` when `validate_by_alias` is `False` by [@sydney-runkle](https://github.com/sydney-runkle) in https://github.com/pydantic/pydantic/pull/11503 +* Change type of `Any` when synthesizing `BaseSettings.__init__` signature in the mypy plugin by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11497 +* Support type variable defaults referencing other type variables by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11520 +* Fix `ValueError` on year zero by [@davidhewitt](https://github.com/davidhewitt) in https://github.com/pydantic/pydantic-core/pull/1583 +* `dataclass` `InitVar` shouldn't be required on serialization by [@sydney-runkle](https://github.com/sydney-runkle) in https://github.com/pydantic/pydantic-core/pull/1602 + +#### Packaging + +* Add a `check_pydantic_core_version()` function by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11324 +* Remove `greenlet` development dependency by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11351 +* Use the `typing-inspection` library by [@Viicos](https://github.com/Viicos) in https://github.com/pydantic/pydantic/pull/11479 +* Bump `pydantic-core` to `v2.31.1` by [@sydney-runkle](https://github.com/sydney-runkle) in https://github.com/pydantic/pydantic/pull/11526 + +## New Contributors + +* [@FyZzyss](https://github.com/FyZzyss) made their first contribution in https://github.com/pydantic/pydantic/pull/10789 +* [@tamird](https://github.com/tamird) made their first contribution in https://github.com/pydantic/pydantic/pull/10948 +* [@felixxm](https://github.com/felixxm) made their first contribution in https://github.com/pydantic/pydantic/pull/11077 +* [@alexprabhat99](https://github.com/alexprabhat99) made their first contribution in https://github.com/pydantic/pydantic/pull/11082 +* [@Kharianne](https://github.com/Kharianne) made their first contribution in https://github.com/pydantic/pydantic/pull/11111 +* [@mdaffad](https://github.com/mdaffad) made their first contribution in https://github.com/pydantic/pydantic/pull/11177 +* [@thejcannon](https://github.com/thejcannon) made their first contribution in https://github.com/pydantic/pydantic/pull/11014 +* [@thomasfrimannkoren](https://github.com/thomasfrimannkoren) made their first contribution in https://github.com/pydantic/pydantic/pull/11251 +* [@usernameMAI](https://github.com/usernameMAI) made their first contribution in https://github.com/pydantic/pydantic/pull/11275 +* [@ananiavito](https://github.com/ananiavito) made their first contribution in https://github.com/pydantic/pydantic/pull/11302 +* [@pawamoy](https://github.com/pawamoy) made their first contribution in https://github.com/pydantic/pydantic/pull/11311 +* [@Maze21127](https://github.com/Maze21127) made their first contribution in https://github.com/pydantic/pydantic/pull/11319 +* [@kauabh](https://github.com/kauabh) made their first contribution in https://github.com/pydantic/pydantic/pull/11369 +* [@jaceklaskowski](https://github.com/jaceklaskowski) made their first contribution in https://github.com/pydantic/pydantic/pull/11353 +* [@tmpbeing](https://github.com/tmpbeing) made their first contribution in https://github.com/pydantic/pydantic/pull/11375 +* [@petyosi](https://github.com/petyosi) made their first contribution in https://github.com/pydantic/pydantic/pull/11405 +* [@austinyu](https://github.com/austinyu) made their first contribution in https://github.com/pydantic/pydantic/pull/11392 +* [@mikeedjones](https://github.com/mikeedjones) made their first contribution in https://github.com/pydantic/pydantic/pull/11402 +* [@astei](https://github.com/astei) made their first contribution in https://github.com/pydantic/pydantic/pull/11436 +* [@dsayling](https://github.com/dsayling) made their first contribution in https://github.com/pydantic/pydantic/pull/11522 +* [@sobolevn](https://github.com/sobolevn) made their first contribution in https://github.com/pydantic/pydantic-core/pull/1645 + +## v2.11.0a2 (2025-02-10) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.11.0a2) + +### What's Changed + +Pydantic v2.11 is a version strongly focused on build time performance of Pydantic models (and core schema generation in general). +This is another early alpha release, meant to collect early feedback from users having issues with core schema builds. + +#### Performance + +* Create a single dictionary when creating a `CoreConfig` instance by [@sydney-runkle](https://github.com/sydney-runkle) in [#11384](https://github.com/pydantic/pydantic/pull/11384) + +#### Fixes + +* Use the correct JSON Schema mode when handling function schemas by [@Viicos](https://github.com/Viicos) in [#11367](https://github.com/pydantic/pydantic/pull/11367) +* Fix JSON Schema reference logic with `examples` keys by [@Viicos](https://github.com/Viicos) in [#11366](https://github.com/pydantic/pydantic/pull/11366) +* Improve exception message when encountering recursion errors during type evaluation by [@Viicos](https://github.com/Viicos) in [#11356](https://github.com/pydantic/pydantic/pull/11356) +* Always include `additionalProperties: True` for arbitrary dictionary schemas by [@austinyu](https://github.com/austinyu) in [#11392](https://github.com/pydantic/pydantic/pull/11392) +* Expose `fallback` parameter in serialization methods by [@Viicos](https://github.com/Viicos) in [#11398](https://github.com/pydantic/pydantic/pull/11398) +* Fix path serialization behavior by [@sydney-runkle](https://github.com/sydney-runkle) in [#11416](https://github.com/pydantic/pydantic/pull/11416) + +#### Packaging + +* Bump `ruff` from 0.9.2 to 0.9.5 by [@Viicos](https://github.com/Viicos) in [#11407](https://github.com/pydantic/pydantic/pull/11407) +* Bump `pydantic-core` to v2.29.0 by [@mikeedjones](https://github.com/mikeedjones) in [#11402](https://github.com/pydantic/pydantic/pull/11402) +* Use locally-built rust with symbols & pgo by [@davidhewitt](https://github.com/davidhewitt) in [#11403](https://github.com/pydantic/pydantic/pull/11403) + +### New Contributors + +* [@kauabh](https://github.com/kauabh) made their first contribution in [#11369](https://github.com/pydantic/pydantic/pull/11369) +* [@jaceklaskowski](https://github.com/jaceklaskowski) made their first contribution in [#11353](https://github.com/pydantic/pydantic/pull/11353) +* [@tmpbeing](https://github.com/tmpbeing) made their first contribution in [#11375](https://github.com/pydantic/pydantic/pull/11375) +* [@petyosi](https://github.com/petyosi) made their first contribution in [#11405](https://github.com/pydantic/pydantic/pull/11405) +* [@austinyu](https://github.com/austinyu) made their first contribution in [#11392](https://github.com/pydantic/pydantic/pull/11392) +* [@mikeedjones](https://github.com/mikeedjones) made their first contribution in [#11402](https://github.com/pydantic/pydantic/pull/11402) + +## v2.11.0a1 (2025-01-30) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.11.0a1) + +### What's Changed + +Pydantic v2.11 is a version strongly focused on build time performance of Pydantic models (and core schema generation in general). +This is an early alpha release, meant to collect early feedback from users having issues with core schema builds. + +#### New Features + +* Support unsubstituted type variables with both a default and a bound or constraints by [@FyZzyss](https://github.com/FyZzyss) in [#10789](https://github.com/pydantic/pydantic/pull/10789) +* Add a `default_factory_takes_validated_data` property to `FieldInfo` by [@Viicos](https://github.com/Viicos) in [#11034](https://github.com/pydantic/pydantic/pull/11034) +* Raise a better error when a generic alias is used inside `type[]` by [@Viicos](https://github.com/Viicos) in [#11088](https://github.com/pydantic/pydantic/pull/11088) +* Properly support PEP 695 generics syntax by [@Viicos](https://github.com/Viicos) in [#11189](https://github.com/pydantic/pydantic/pull/11189) +* Properly support type variable defaults by [@Viicos](https://github.com/Viicos) in [#11332](https://github.com/pydantic/pydantic/pull/11332) + +#### Changes + +* Rework `create_model` field definitions format by [@Viicos](https://github.com/Viicos) in [#11032](https://github.com/pydantic/pydantic/pull/11032) +* Raise a deprecation warning when a field is annotated as final with a default value by [@Viicos](https://github.com/Viicos) in [#11168](https://github.com/pydantic/pydantic/pull/11168) +* Deprecate accessing `model_fields` and `model_computed_fields` on instances by [@Viicos](https://github.com/Viicos) in [#11169](https://github.com/pydantic/pydantic/pull/11169) +* Move core schema generation logic for path types inside the `GenerateSchema` class by [@sydney-runkle](https://github.com/sydney-runkle) in [#10846](https://github.com/pydantic/pydantic/pull/10846) +* Move `deque` schema gen to `GenerateSchema` class by [@sydney-runkle](https://github.com/sydney-runkle) in [#11239](https://github.com/pydantic/pydantic/pull/11239) +* Move `Mapping` schema gen to `GenerateSchema` to complete removal of `prepare_annotations_for_known_type` workaround by [@sydney-runkle](https://github.com/sydney-runkle) in [#11247](https://github.com/pydantic/pydantic/pull/11247) +* Remove Python 3.8 Support by [@sydney-runkle](https://github.com/sydney-runkle) in [#11258](https://github.com/pydantic/pydantic/pull/11258) +* Disable `pydantic-core` core schema validation by [@sydney-runkle](https://github.com/sydney-runkle) in [#11271](https://github.com/pydantic/pydantic/pull/11271) + +#### Performance + +* Only evaluate `FieldInfo` annotations if required during schema building by [@Viicos](https://github.com/Viicos) in [#10769](https://github.com/pydantic/pydantic/pull/10769) +* Optimize calls to `get_type_ref` by [@Viicos](https://github.com/Viicos) in [#10863](https://github.com/pydantic/pydantic/pull/10863) +* Improve `__setattr__` performance of Pydantic models by caching setter functions by [@MarkusSintonen](https://github.com/MarkusSintonen) in [#10868](https://github.com/pydantic/pydantic/pull/10868) +* Improve annotation application performance by [@Viicos](https://github.com/Viicos) in [#11186](https://github.com/pydantic/pydantic/pull/11186) +* Improve performance of `_typing_extra` module by [@Viicos](https://github.com/Viicos) in [#11255](https://github.com/pydantic/pydantic/pull/11255) +* Refactor and optimize schema cleaning logic by [@Viicos](https://github.com/Viicos) and [@MarkusSintonen](https://github.com/MarkusSintonen) in [#11244](https://github.com/pydantic/pydantic/pull/11244) + +#### Fixes + +* Add validation tests for `_internal/_validators.py` by [@tkasuz](https://github.com/tkasuz) in [#10763](https://github.com/pydantic/pydantic/pull/10763) +* Improve `TypeAdapter` instance repr by [@sydney-runkle](https://github.com/sydney-runkle) in [#10872](https://github.com/pydantic/pydantic/pull/10872) +* Revert "ci: use locally built pydantic-core with debug symbols by [@sydney-runkle](https://github.com/sydney-runkle) in [#10942](https://github.com/pydantic/pydantic/pull/10942) +* Re-enable all FastAPI tests by [@tamird](https://github.com/tamird) in [#10948](https://github.com/pydantic/pydantic/pull/10948) +* Fix typo in HISTORY.md. by [@felixxm](https://github.com/felixxm) in [#11077](https://github.com/pydantic/pydantic/pull/11077) +* Infer final fields with a default value as class variables in the mypy plugin by [@Viicos](https://github.com/Viicos) in [#11121](https://github.com/pydantic/pydantic/pull/11121) +* Recursively unpack `Literal` values if using PEP 695 type aliases by [@Viicos](https://github.com/Viicos) in [#11114](https://github.com/pydantic/pydantic/pull/11114) +* Override `__subclasscheck__` on `ModelMetaclass` to avoid memory leak and performance issues by [@Viicos](https://github.com/Viicos) in [#11116](https://github.com/pydantic/pydantic/pull/11116) +* Remove unused `_extract_get_pydantic_json_schema()` parameter by [@Viicos](https://github.com/Viicos) in [#11155](https://github.com/pydantic/pydantic/pull/11155) +* Add FastAPI and SQLModel to third-party tests by [@sydney-runkle](https://github.com/sydney-runkle) in [#11044](https://github.com/pydantic/pydantic/pull/11044) +* Fix conditional expressions syntax for third-party tests by [@Viicos](https://github.com/Viicos) in [#11162](https://github.com/pydantic/pydantic/pull/11162) +* Move FastAPI tests to third-party workflow by [@Viicos](https://github.com/Viicos) in [#11164](https://github.com/pydantic/pydantic/pull/11164) +* Improve discriminated union error message for invalid union variants by [@Viicos](https://github.com/Viicos) in [#11161](https://github.com/pydantic/pydantic/pull/11161) +* Unpack PEP 695 type aliases if using the `Annotated` form by [@Viicos](https://github.com/Viicos) in [#11109](https://github.com/pydantic/pydantic/pull/11109) +* Include `openapi-python-client` check in issue creation for third-party failures, use `main` branch by [@sydney-runkle](https://github.com/sydney-runkle) in [#11182](https://github.com/pydantic/pydantic/pull/11182) +* Add pandera third-party tests by [@Viicos](https://github.com/Viicos) in [#11193](https://github.com/pydantic/pydantic/pull/11193) +* Add ODMantic third-party tests by [@sydney-runkle](https://github.com/sydney-runkle) in [#11197](https://github.com/pydantic/pydantic/pull/11197) +* Add missing stacklevel in `deprecated_instance_property` warning by [@Viicos](https://github.com/Viicos) in [#11200](https://github.com/pydantic/pydantic/pull/11200) +* Copy `WithJsonSchema` schema to avoid sharing mutated data by [@thejcannon](https://github.com/thejcannon) in [#11014](https://github.com/pydantic/pydantic/pull/11014) +* Do not cache parametrized models when in the process of parametrizing another model by [@Viicos](https://github.com/Viicos) in [#10704](https://github.com/pydantic/pydantic/pull/10704) +* Re-enable Beanie third-party tests by [@Viicos](https://github.com/Viicos) in [#11214](https://github.com/pydantic/pydantic/pull/11214) +* Add discriminated union related metadata entries to the `CoreMetadata` definition by [@Viicos](https://github.com/Viicos) in [#11216](https://github.com/pydantic/pydantic/pull/11216) +* Consolidate schema definitions logic in the `_Definitions` class by [@Viicos](https://github.com/Viicos) in [#11208](https://github.com/pydantic/pydantic/pull/11208) +* Support initializing root model fields with values of the `root` type in the mypy plugin by [@Viicos](https://github.com/Viicos) in [#11212](https://github.com/pydantic/pydantic/pull/11212) +* Fix various issues with dataclasses and `use_attribute_docstrings` by [@Viicos](https://github.com/Viicos) in [#11246](https://github.com/pydantic/pydantic/pull/11246) +* Only compute normalized decimal places if necessary in `decimal_places_validator` by [@misrasaurabh1](https://github.com/misrasaurabh1) in [#11281](https://github.com/pydantic/pydantic/pull/11281) +* Fix two misplaced sentences in validation errors documentation by [@ananiavito](https://github.com/ananiavito) in [#11302](https://github.com/pydantic/pydantic/pull/11302) +* Fix mkdocstrings inventory example in documentation by [@pawamoy](https://github.com/pawamoy) in [#11311](https://github.com/pydantic/pydantic/pull/11311) +* Add support for `validation_alias` in the mypy plugin by [@Viicos](https://github.com/Viicos) in [#11295](https://github.com/pydantic/pydantic/pull/11295) +* Do not transform model serializer functions as class methods in the mypy plugin by [@Viicos](https://github.com/Viicos) in [#11298](https://github.com/pydantic/pydantic/pull/11298) +* Simplify `GenerateJsonSchema.literal_schema()` implementation by [@misrasaurabh1](https://github.com/misrasaurabh1) in [#11321](https://github.com/pydantic/pydantic/pull/11321) +* Add additional allowed schemes for `ClickHouseDsn` by [@Maze21127](https://github.com/Maze21127) in [#11319](https://github.com/pydantic/pydantic/pull/11319) +* Coerce decimal constraints to `Decimal` instances by [@Viicos](https://github.com/Viicos) in [#11350](https://github.com/pydantic/pydantic/pull/11350) +* Fix `ValueError` on year zero by [@davidhewitt](https://github.com/davidhewitt) in [pydantic-core#1583](https://github.com/pydantic/pydantic-core/pull/1583) + +#### Packaging + +* Bump dawidd6/action-download-artifact from 6 to 7 by [@dependabot](https://github.com/dependabot) in [#11018](https://github.com/pydantic/pydantic/pull/11018) +* Re-enable memray related tests on Python 3.12+ by [@Viicos](https://github.com/Viicos) in [#11191](https://github.com/pydantic/pydantic/pull/11191) +* Bump astral-sh/setup-uv to 5 by [@dependabot](https://github.com/dependabot) in [#11205](https://github.com/pydantic/pydantic/pull/11205) +* Bump `ruff` to v0.9.0 by [@sydney-runkle](https://github.com/sydney-runkle) in [#11254](https://github.com/pydantic/pydantic/pull/11254) +* Regular `uv.lock` deps update by [@sydney-runkle](https://github.com/sydney-runkle) in [#11333](https://github.com/pydantic/pydantic/pull/11333) +* Add a `check_pydantic_core_version()` function by [@Viicos](https://github.com/Viicos) in [#11324](https://github.com/pydantic/pydantic/pull/11324) +* Remove `greenlet` development dependency by [@Viicos](https://github.com/Viicos) in [#11351](https://github.com/pydantic/pydantic/pull/11351) +* Bump `pydantic-core` to v2.28.0 by [@Viicos](https://github.com/Viicos) in [#11364](https://github.com/pydantic/pydantic/pull/11364) + +### New Contributors + +* [@FyZzyss](https://github.com/FyZzyss) made their first contribution in [#10789](https://github.com/pydantic/pydantic/pull/10789) +* [@tamird](https://github.com/tamird) made their first contribution in [#10948](https://github.com/pydantic/pydantic/pull/10948) +* [@felixxm](https://github.com/felixxm) made their first contribution in [#11077](https://github.com/pydantic/pydantic/pull/11077) +* [@alexprabhat99](https://github.com/alexprabhat99) made their first contribution in [#11082](https://github.com/pydantic/pydantic/pull/11082) +* [@Kharianne](https://github.com/Kharianne) made their first contribution in [#11111](https://github.com/pydantic/pydantic/pull/11111) +* [@mdaffad](https://github.com/mdaffad) made their first contribution in [#11177](https://github.com/pydantic/pydantic/pull/11177) +* [@thejcannon](https://github.com/thejcannon) made their first contribution in [#11014](https://github.com/pydantic/pydantic/pull/11014) +* [@thomasfrimannkoren](https://github.com/thomasfrimannkoren) made their first contribution in [#11251](https://github.com/pydantic/pydantic/pull/11251) +* [@usernameMAI](https://github.com/usernameMAI) made their first contribution in [#11275](https://github.com/pydantic/pydantic/pull/11275) +* [@ananiavito](https://github.com/ananiavito) made their first contribution in [#11302](https://github.com/pydantic/pydantic/pull/11302) +* [@pawamoy](https://github.com/pawamoy) made their first contribution in [#11311](https://github.com/pydantic/pydantic/pull/11311) +* [@Maze21127](https://github.com/Maze21127) made their first contribution in [#11319](https://github.com/pydantic/pydantic/pull/11319) + +## v2.10.6 (2025-01-23) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.10.6) + +### What's Changed + +#### Fixes + +* Fix JSON Schema reference collection with `'examples'` keys by [@Viicos](https://github.com/Viicos) in [#11325](https://github.com/pydantic/pydantic/pull/11325) +* Fix url python serialization by [@sydney-runkle](https://github.com/sydney-runkle) in [#11331](https://github.com/pydantic/pydantic/pull/11331) + +## v2.10.5 (2025-01-08) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.10.5) + +### What's Changed + +#### Fixes + +* Remove custom MRO implementation of Pydantic models by [@Viicos](https://github.com/Viicos) in [#11184](https://github.com/pydantic/pydantic/pull/11184) +* Fix URL serialization for unions by [@sydney-runkle](https://github.com/sydney-runkle) in [#11233](https://github.com/pydantic/pydantic/pull/11233) + +## v2.10.4 (2024-12-18) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.10.4) + +### What's Changed + +#### Fixes + +* Fix for comparison of `AnyUrl` objects by [@alexprabhat99](https://github.com/alexprabhat99) in [#11082](https://github.com/pydantic/pydantic/pull/11082) +* Properly fetch PEP 695 type params for functions, do not fetch annotations from signature by [@Viicos](https://github.com/Viicos) in [#11093](https://github.com/pydantic/pydantic/pull/11093) +* Include JSON Schema input core schema in function schemas by [@Viicos](https://github.com/Viicos) in [#11085](https://github.com/pydantic/pydantic/pull/11085) +* Add `len` to `_BaseUrl` to avoid TypeError by [@Kharianne](https://github.com/Kharianne) in [#11111](https://github.com/pydantic/pydantic/pull/11111) +* Make sure the type reference is removed from the seen references by [@Viicos](https://github.com/Viicos) in [#11143](https://github.com/pydantic/pydantic/pull/11143) + +### New Contributors + +* [@FyZzyss](https://github.com/FyZzyss) made their first contribution in [#10789](https://github.com/pydantic/pydantic/pull/10789) +* [@tamird](https://github.com/tamird) made their first contribution in [#10948](https://github.com/pydantic/pydantic/pull/10948) +* [@felixxm](https://github.com/felixxm) made their first contribution in [#11077](https://github.com/pydantic/pydantic/pull/11077) +* [@alexprabhat99](https://github.com/alexprabhat99) made their first contribution in [#11082](https://github.com/pydantic/pydantic/pull/11082) +* [@Kharianne](https://github.com/Kharianne) made their first contribution in [#11111](https://github.com/pydantic/pydantic/pull/11111) + +#### Packaging + +* Bump `pydantic-core` to v2.27.2 by [@davidhewitt](https://github.com/davidhewitt) in [#11138](https://github.com/pydantic/pydantic/pull/11138) + +## v2.10.3 (2024-12-03) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.10.3) + +### What's Changed + +#### Fixes + +* Set fields when `defer_build` is set on Pydantic dataclasses by [@Viicos](https://github.com/Viicos) in [#10984](https://github.com/pydantic/pydantic/pull/10984) +* Do not resolve the JSON Schema reference for `dict` core schema keys by [@Viicos](https://github.com/Viicos) in [#10989](https://github.com/pydantic/pydantic/pull/10989) +* Use the globals of the function when evaluating the return type for `PlainSerializer` and `WrapSerializer` functions by [@Viicos](https://github.com/Viicos) in [#11008](https://github.com/pydantic/pydantic/pull/11008) +* Fix host required enforcement for urls to be compatible with v2.9 behavior by [@sydney-runkle](https://github.com/sydney-runkle) in [#11027](https://github.com/pydantic/pydantic/pull/11027) +* Add a `default_factory_takes_validated_data` property to `FieldInfo` by [@Viicos](https://github.com/Viicos) in [#11034](https://github.com/pydantic/pydantic/pull/11034) +* Fix url json schema in `serialization` mode by [@sydney-runkle](https://github.com/sydney-runkle) in [#11035](https://github.com/pydantic/pydantic/pull/11035) + +## v2.10.2 (2024-11-25) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.10.2) + +### What's Changed + +#### Fixes + +* Only evaluate FieldInfo annotations if required during schema building by [@Viicos](https://github.com/Viicos) in [#10769](https://github.com/pydantic/pydantic/pull/10769) +* Do not evaluate annotations for private fields by [@Viicos](https://github.com/Viicos) in [#10962](https://github.com/pydantic/pydantic/pull/10962) +* Support serialization as any for `Secret` types and `Url` types by [@sydney-runkle](https://github.com/sydney-runkle) in [#10947](https://github.com/pydantic/pydantic/pull/10947) +* Fix type hint of `Field.default` to be compatible with Python 3.8 and 3.9 by [@Viicos](https://github.com/Viicos) in [#10972](https://github.com/pydantic/pydantic/pull/10972) +* Add hashing support for URL types by [@sydney-runkle](https://github.com/sydney-runkle) in [#10975](https://github.com/pydantic/pydantic/pull/10975) +* Hide `BaseModel.__replace__` definition from type checkers by [@Viicos](https://github.com/Viicos) in [#10979](https://github.com/pydantic/pydantic/pull/10979) + +## v2.10.1 (2024-11-21) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.10.1) + +### What's Changed + +#### Fixes + +* Use the correct frame when instantiating a parametrized `TypeAdapter` by [@Viicos](https://github.com/Viicos) in [#10893](https://github.com/pydantic/pydantic/pull/10893) +* Relax check for validated data in `default_factory` utils by [@sydney-runkle](https://github.com/sydney-runkle) in [#10909](https://github.com/pydantic/pydantic/pull/10909) +* Fix type checking issue with `model_fields` and `model_computed_fields` by [@sydney-runkle](https://github.com/sydney-runkle) in [#10911](https://github.com/pydantic/pydantic/pull/10911) +* Use the parent configuration during schema generation for stdlib `dataclass`es by [@sydney-runkle](https://github.com/sydney-runkle) in [#10928](https://github.com/pydantic/pydantic/pull/10928) +* Use the `globals` of the function when evaluating the return type of serializers and `computed_field`s by [@Viicos](https://github.com/Viicos) in [#10929](https://github.com/pydantic/pydantic/pull/10929) +* Fix URL constraint application by [@sydney-runkle](https://github.com/sydney-runkle) in [#10922](https://github.com/pydantic/pydantic/pull/10922) +* Fix URL equality with different validation methods by [@sydney-runkle](https://github.com/sydney-runkle) in [#10934](https://github.com/pydantic/pydantic/pull/10934) +* Fix JSON schema title when specified as `''` by [@sydney-runkle](https://github.com/sydney-runkle) in [#10936](https://github.com/pydantic/pydantic/pull/10936) +* Fix `python` mode serialization for `complex` inference by [@sydney-runkle](https://github.com/sydney-runkle) in [pydantic-core#1549](https://github.com/pydantic/pydantic-core/pull/1549) + +#### Packaging + +* Bump `pydantic-core` version to `v2.27.1` by [@sydney-runkle](https://github.com/sydney-runkle) in [#10938](https://github.com/pydantic/pydantic/pull/10938) + +### New Contributors + +## v2.10.0 (2024-11-20) + +The code released in v2.10.0 is practically identical to that of v2.10.0b2. + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.10.0) + +See the [v2.10 release blog post](https://pydantic.dev/articles/pydantic-v2-10-release) for the highlights! + +### What's Changed + +#### New Features + +* Support `fractions.Fraction` by [@sydney-runkle](https://github.com/sydney-runkle) in [#10318](https://github.com/pydantic/pydantic/pull/10318) +* Support `Hashable` for json validation by [@sydney-runkle](https://github.com/sydney-runkle) in [#10324](https://github.com/pydantic/pydantic/pull/10324) +* Add a `SocketPath` type for `linux` systems by [@theunkn0wn1](https://github.com/theunkn0wn1) in [#10378](https://github.com/pydantic/pydantic/pull/10378) +* Allow arbitrary refs in JSON schema `examples` by [@sydney-runkle](https://github.com/sydney-runkle) in [#10417](https://github.com/pydantic/pydantic/pull/10417) +* Support `defer_build` for Pydantic dataclasses by [@Viicos](https://github.com/Viicos) in [#10313](https://github.com/pydantic/pydantic/pull/10313) +* Adding v1 / v2 incompatibility warning for nested v1 model by [@sydney-runkle](https://github.com/sydney-runkle) in [#10431](https://github.com/pydantic/pydantic/pull/10431) +* Add support for unpacked `TypedDict` to type hint variadic keyword arguments with `@validate_call` by [@Viicos](https://github.com/Viicos) in [#10416](https://github.com/pydantic/pydantic/pull/10416) +* Support compiled patterns in `protected_namespaces` by [@sydney-runkle](https://github.com/sydney-runkle) in [#10522](https://github.com/pydantic/pydantic/pull/10522) +* Add support for `propertyNames` in JSON schema by [@FlorianSW](https://github.com/FlorianSW) in [#10478](https://github.com/pydantic/pydantic/pull/10478) +* Adding `__replace__` protocol for Python 3.13+ support by [@sydney-runkle](https://github.com/sydney-runkle) in [#10596](https://github.com/pydantic/pydantic/pull/10596) +* Expose public `sort` method for JSON schema generation by [@sydney-runkle](https://github.com/sydney-runkle) in [#10595](https://github.com/pydantic/pydantic/pull/10595) +* Add runtime validation of `@validate_call` callable argument by [@kc0506](https://github.com/kc0506) in [#10627](https://github.com/pydantic/pydantic/pull/10627) +* Add `experimental_allow_partial` support by [@samuelcolvin](https://github.com/samuelcolvin) in [#10748](https://github.com/pydantic/pydantic/pull/10748) +* Support default factories taking validated data as an argument by [@Viicos](https://github.com/Viicos) in [#10678](https://github.com/pydantic/pydantic/pull/10678) +* Allow subclassing `ValidationError` and `PydanticCustomError` by [@Youssefares](https://github.com/Youssefares) in [pydantic/pydantic-core#1413](https://github.com/pydantic/pydantic-core/pull/1413) +* Add `trailing-strings` support to `experimental_allow_partial` by [@sydney-runkle](https://github.com/sydney-runkle) in [#10825](https://github.com/pydantic/pydantic/pull/10825) +* Add `rebuild()` method for `TypeAdapter` and simplify `defer_build` patterns by [@sydney-runkle](https://github.com/sydney-runkle) in [#10537](https://github.com/pydantic/pydantic/pull/10537) +* Improve `TypeAdapter` instance repr by [@sydney-runkle](https://github.com/sydney-runkle) in [#10872](https://github.com/pydantic/pydantic/pull/10872) + +#### Changes + +* Don't allow customization of `SchemaGenerator` until interface is more stable by [@sydney-runkle](https://github.com/sydney-runkle) in [#10303](https://github.com/pydantic/pydantic/pull/10303) +* Cleanly `defer_build` on `TypeAdapters`, removing experimental flag by [@sydney-runkle](https://github.com/sydney-runkle) in [#10329](https://github.com/pydantic/pydantic/pull/10329) +* Fix `mro` of generic subclass by [@kc0506](https://github.com/kc0506) in [#10100](https://github.com/pydantic/pydantic/pull/10100) +* Strip whitespaces on JSON Schema title generation by [@sydney-runkle](https://github.com/sydney-runkle) in [#10404](https://github.com/pydantic/pydantic/pull/10404) +* Use `b64decode` and `b64encode` for `Base64Bytes` type by [@sydney-runkle](https://github.com/sydney-runkle) in [#10486](https://github.com/pydantic/pydantic/pull/10486) +* Relax protected namespace config default by [@sydney-runkle](https://github.com/sydney-runkle) in [#10441](https://github.com/pydantic/pydantic/pull/10441) +* Revalidate parametrized generics if instance's origin is subclass of OG class by [@sydney-runkle](https://github.com/sydney-runkle) in [#10666](https://github.com/pydantic/pydantic/pull/10666) +* Warn if configuration is specified on the `@dataclass` decorator and with the `__pydantic_config__` attribute by [@sydney-runkle](https://github.com/sydney-runkle) in [#10406](https://github.com/pydantic/pydantic/pull/10406) +* Recommend against using `Ellipsis` (...) with `Field` by [@Viicos](https://github.com/Viicos) in [#10661](https://github.com/pydantic/pydantic/pull/10661) +* Migrate to subclassing instead of annotated approach for pydantic url types by [@sydney-runkle](https://github.com/sydney-runkle) in [#10662](https://github.com/pydantic/pydantic/pull/10662) +* Change JSON schema generation of `Literal`s and `Enums` by [@Viicos](https://github.com/Viicos) in [#10692](https://github.com/pydantic/pydantic/pull/10692) +* Simplify unions involving `Any` or `Never` when replacing type variables by [@Viicos](https://github.com/Viicos) in [#10338](https://github.com/pydantic/pydantic/pull/10338) +* Do not require padding when decoding `base64` bytes by [@bschoenmaeckers](https://github.com/bschoenmaeckers) in [pydantic/pydantic-core#1448](https://github.com/pydantic/pydantic-core/pull/1448) +* Support dates all the way to 1BC by [@changhc](https://github.com/changhc) in [pydantic/speedate#77](https://github.com/pydantic/speedate/pull/77) + +#### Performance + +* Schema cleaning: skip unnecessary copies during schema walking by [@Viicos](https://github.com/Viicos) in [#10286](https://github.com/pydantic/pydantic/pull/10286) +* Refactor namespace logic for annotations evaluation by [@Viicos](https://github.com/Viicos) in [#10530](https://github.com/pydantic/pydantic/pull/10530) +* Improve email regexp on edge cases by [@AlekseyLobanov](https://github.com/AlekseyLobanov) in [#10601](https://github.com/pydantic/pydantic/pull/10601) +* `CoreMetadata` refactor with an emphasis on documentation, schema build time performance, and reducing complexity by [@sydney-runkle](https://github.com/sydney-runkle) in [#10675](https://github.com/pydantic/pydantic/pull/10675) + +#### Packaging + +* Bump `pydantic-core` to `v2.27.0` by [@sydney-runkle](https://github.com/sydney-runkle) in [#10825](https://github.com/pydantic/pydantic/pull/10825) +* Replaced pdm with uv by [@frfahim](https://github.com/frfahim) in [#10727](https://github.com/pydantic/pydantic/pull/10727) + +#### Fixes + +* Remove guarding check on `computed_field` with `field_serializer` by [@nix010](https://github.com/nix010) in [#10390](https://github.com/pydantic/pydantic/pull/10390) +* Fix `Predicate` issue in `v2.9.0` by [@sydney-runkle](https://github.com/sydney-runkle) in [#10321](https://github.com/pydantic/pydantic/pull/10321) +* Fixing `annotated-types` bound by [@sydney-runkle](https://github.com/sydney-runkle) in [#10327](https://github.com/pydantic/pydantic/pull/10327) +* Turn `tzdata` install requirement into optional `timezone` dependency by [@jakob-keller](https://github.com/jakob-keller) in [#10331](https://github.com/pydantic/pydantic/pull/10331) +* Use correct types namespace when building `namedtuple` core schemas by [@Viicos](https://github.com/Viicos) in [#10337](https://github.com/pydantic/pydantic/pull/10337) +* Fix evaluation of stringified annotations during namespace inspection by [@Viicos](https://github.com/Viicos) in [#10347](https://github.com/pydantic/pydantic/pull/10347) +* Fix `IncEx` type alias definition by [@Viicos](https://github.com/Viicos) in [#10339](https://github.com/pydantic/pydantic/pull/10339) +* Do not error when trying to evaluate annotations of private attributes by [@Viicos](https://github.com/Viicos) in [#10358](https://github.com/pydantic/pydantic/pull/10358) +* Fix nested type statement by [@kc0506](https://github.com/kc0506) in [#10369](https://github.com/pydantic/pydantic/pull/10369) +* Improve typing of `ModelMetaclass.mro` by [@Viicos](https://github.com/Viicos) in [#10372](https://github.com/pydantic/pydantic/pull/10372) +* Fix class access of deprecated `computed_field`s by [@Viicos](https://github.com/Viicos) in [#10391](https://github.com/pydantic/pydantic/pull/10391) +* Make sure `inspect.iscoroutinefunction` works on coroutines decorated with `@validate_call` by [@MovisLi](https://github.com/MovisLi) in [#10374](https://github.com/pydantic/pydantic/pull/10374) +* Fix `NameError` when using `validate_call` with PEP 695 on a class by [@kc0506](https://github.com/kc0506) in [#10380](https://github.com/pydantic/pydantic/pull/10380) +* Fix `ZoneInfo` with various invalid types by [@sydney-runkle](https://github.com/sydney-runkle) in [#10408](https://github.com/pydantic/pydantic/pull/10408) +* Fix `PydanticUserError` on empty `model_config` with annotations by [@cdwilson](https://github.com/cdwilson) in [#10412](https://github.com/pydantic/pydantic/pull/10412) +* Fix variance issue in `_IncEx` type alias, only allow `True` by [@Viicos](https://github.com/Viicos) in [#10414](https://github.com/pydantic/pydantic/pull/10414) +* Fix serialization schema generation when using `PlainValidator` by [@Viicos](https://github.com/Viicos) in [#10427](https://github.com/pydantic/pydantic/pull/10427) +* Fix schema generation error when serialization schema holds references by [@Viicos](https://github.com/Viicos) in [#10444](https://github.com/pydantic/pydantic/pull/10444) +* Inline references if possible when generating schema for `json_schema_input_type` by [@Viicos](https://github.com/Viicos) in [#10439](https://github.com/pydantic/pydantic/pull/10439) +* Fix recursive arguments in `Representation` by [@Viicos](https://github.com/Viicos) in [#10480](https://github.com/pydantic/pydantic/pull/10480) +* Fix representation for builtin function types by [@kschwab](https://github.com/kschwab) in [#10479](https://github.com/pydantic/pydantic/pull/10479) +* Add python validators for decimal constraints (`max_digits` and `decimal_places`) by [@sydney-runkle](https://github.com/sydney-runkle) in [#10506](https://github.com/pydantic/pydantic/pull/10506) +* Only fetch `__pydantic_core_schema__` from the current class during schema generation by [@Viicos](https://github.com/Viicos) in [#10518](https://github.com/pydantic/pydantic/pull/10518) +* Fix `stacklevel` on deprecation warnings for `BaseModel` by [@sydney-runkle](https://github.com/sydney-runkle) in [#10520](https://github.com/pydantic/pydantic/pull/10520) +* Fix warning `stacklevel` in `BaseModel.__init__` by [@Viicos](https://github.com/Viicos) in [#10526](https://github.com/pydantic/pydantic/pull/10526) +* Improve error handling for in-evaluable refs for discriminator application by [@sydney-runkle](https://github.com/sydney-runkle) in [#10440](https://github.com/pydantic/pydantic/pull/10440) +* Change the signature of `ConfigWrapper.core_config` to take the title directly by [@Viicos](https://github.com/Viicos) in [#10562](https://github.com/pydantic/pydantic/pull/10562) +* Do not use the previous config from the stack for dataclasses without config by [@Viicos](https://github.com/Viicos) in [#10576](https://github.com/pydantic/pydantic/pull/10576) +* Fix serialization for IP types with `mode='python'` by [@sydney-runkle](https://github.com/sydney-runkle) in [#10594](https://github.com/pydantic/pydantic/pull/10594) +* Support constraint application for `Base64Etc` types by [@sydney-runkle](https://github.com/sydney-runkle) in [#10584](https://github.com/pydantic/pydantic/pull/10584) +* Fix `validate_call` ignoring `Field` in `Annotated` by [@kc0506](https://github.com/kc0506) in [#10610](https://github.com/pydantic/pydantic/pull/10610) +* Raise an error when `Self` is invalid by [@kc0506](https://github.com/kc0506) in [#10609](https://github.com/pydantic/pydantic/pull/10609) +* Using `core_schema.InvalidSchema` instead of metadata injection + checks by [@sydney-runkle](https://github.com/sydney-runkle) in [#10523](https://github.com/pydantic/pydantic/pull/10523) +* Tweak type alias logic by [@kc0506](https://github.com/kc0506) in [#10643](https://github.com/pydantic/pydantic/pull/10643) +* Support usage of `type` with `typing.Self` and type aliases by [@kc0506](https://github.com/kc0506) in [#10621](https://github.com/pydantic/pydantic/pull/10621) +* Use overloads for `Field` and `PrivateAttr` functions by [@Viicos](https://github.com/Viicos) in [#10651](https://github.com/pydantic/pydantic/pull/10651) +* Clean up the `mypy` plugin implementation by [@Viicos](https://github.com/Viicos) in [#10669](https://github.com/pydantic/pydantic/pull/10669) +* Properly check for `typing_extensions` variant of `TypeAliasType` by [@Daraan](https://github.com/Daraan) in [#10713](https://github.com/pydantic/pydantic/pull/10713) +* Allow any mapping in `BaseModel.model_copy()` by [@Viicos](https://github.com/Viicos) in [#10751](https://github.com/pydantic/pydantic/pull/10751) +* Fix `isinstance` behavior for urls by [@sydney-runkle](https://github.com/sydney-runkle) in [#10766](https://github.com/pydantic/pydantic/pull/10766) +* Ensure `cached_property` can be set on Pydantic models by [@Viicos](https://github.com/Viicos) in [#10774](https://github.com/pydantic/pydantic/pull/10774) +* Fix equality checks for primitives in literals by [@sydney-runkle](https://github.com/sydney-runkle) in [pydantic/pydantic-core#1459](https://github.com/pydantic/pydantic-core/pull/1459) +* Properly enforce `host_required` for URLs by [@Viicos](https://github.com/Viicos) in [pydantic/pydantic-core#1488](https://github.com/pydantic/pydantic-core/pull/1488) +* Fix when `coerce_numbers_to_str` enabled and string has invalid Unicode character by [@andrey-berenda](https://github.com/andrey-berenda) in [pydantic/pydantic-core#1515](https://github.com/pydantic/pydantic-core/pull/1515) +* Fix serializing `complex` values in `Enum`s by [@changhc](https://github.com/changhc) in [pydantic/pydantic-core#1524](https://github.com/pydantic/pydantic-core/pull/1524) +* Refactor `_typing_extra` module by [@Viicos](https://github.com/Viicos) in [#10725](https://github.com/pydantic/pydantic/pull/10725) +* Support intuitive equality for urls by [@sydney-runkle](https://github.com/sydney-runkle) in [#10798](https://github.com/pydantic/pydantic/pull/10798) +* Add `bytearray` to `TypeAdapter.validate_json` signature by [@samuelcolvin](https://github.com/samuelcolvin) in [#10802](https://github.com/pydantic/pydantic/pull/10802) +* Ensure class access of method descriptors is performed when used as a default with `Field` by [@Viicos](https://github.com/Viicos) in [#10816](https://github.com/pydantic/pydantic/pull/10816) +* Fix circular import with `validate_call` by [@sydney-runkle](https://github.com/sydney-runkle) in [#10807](https://github.com/pydantic/pydantic/pull/10807) +* Fix error when using type aliases referencing other type aliases by [@Viicos](https://github.com/Viicos) in [#10809](https://github.com/pydantic/pydantic/pull/10809) +* Fix `IncEx` type alias to be compatible with mypy by [@Viicos](https://github.com/Viicos) in [#10813](https://github.com/pydantic/pydantic/pull/10813) +* Make `__signature__` a lazy property, do not deepcopy defaults by [@Viicos](https://github.com/Viicos) in [#10818](https://github.com/pydantic/pydantic/pull/10818) +* Make `__signature__` lazy for dataclasses, too by [@sydney-runkle](https://github.com/sydney-runkle) in [#10832](https://github.com/pydantic/pydantic/pull/10832) +* Subclass all single host url classes from `AnyUrl` to preserve behavior from v2.9 by [@sydney-runkle](https://github.com/sydney-runkle) in [#10856](https://github.com/pydantic/pydantic/pull/10856) + +### New Contributors + +* [@jakob-keller](https://github.com/jakob-keller) made their first contribution in [#10331](https://github.com/pydantic/pydantic/pull/10331) +* [@MovisLi](https://github.com/MovisLi) made their first contribution in [#10374](https://github.com/pydantic/pydantic/pull/10374) +* [@joaopalmeiro](https://github.com/joaopalmeiro) made their first contribution in [#10405](https://github.com/pydantic/pydantic/pull/10405) +* [@theunkn0wn1](https://github.com/theunkn0wn1) made their first contribution in [#10378](https://github.com/pydantic/pydantic/pull/10378) +* [@cdwilson](https://github.com/cdwilson) made their first contribution in [#10412](https://github.com/pydantic/pydantic/pull/10412) +* [@dlax](https://github.com/dlax) made their first contribution in [#10421](https://github.com/pydantic/pydantic/pull/10421) +* [@kschwab](https://github.com/kschwab) made their first contribution in [#10479](https://github.com/pydantic/pydantic/pull/10479) +* [@santibreo](https://github.com/santibreo) made their first contribution in [#10453](https://github.com/pydantic/pydantic/pull/10453) +* [@FlorianSW](https://github.com/FlorianSW) made their first contribution in [#10478](https://github.com/pydantic/pydantic/pull/10478) +* [@tkasuz](https://github.com/tkasuz) made their first contribution in [#10555](https://github.com/pydantic/pydantic/pull/10555) +* [@AlekseyLobanov](https://github.com/AlekseyLobanov) made their first contribution in [#10601](https://github.com/pydantic/pydantic/pull/10601) +* [@NiclasvanEyk](https://github.com/NiclasvanEyk) made their first contribution in [#10667](https://github.com/pydantic/pydantic/pull/10667) +* [@mschoettle](https://github.com/mschoettle) made their first contribution in [#10677](https://github.com/pydantic/pydantic/pull/10677) +* [@Daraan](https://github.com/Daraan) made their first contribution in [#10713](https://github.com/pydantic/pydantic/pull/10713) +* [@k4nar](https://github.com/k4nar) made their first contribution in [#10736](https://github.com/pydantic/pydantic/pull/10736) +* [@UriyaHarpeness](https://github.com/UriyaHarpeness) made their first contribution in [#10740](https://github.com/pydantic/pydantic/pull/10740) +* [@frfahim](https://github.com/frfahim) made their first contribution in [#10727](https://github.com/pydantic/pydantic/pull/10727) + +## v2.10.0b2 (2024-11-13) + +Pre-release, see [the GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.10.0b2) for details. + +## v2.10.0b1 (2024-11-06) + +Pre-release, see [the GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.10.0b1) for details. + + +... see [here](https://docs.pydantic.dev/changelog/#v0322-2019-08-17) for earlier changes. diff --git a/source/target-vendors/cp312/pydantic-2.13.5.dist-info/RECORD b/source/target-vendors/cp312/pydantic-2.13.5.dist-info/RECORD new file mode 100644 index 0000000..51852ce --- /dev/null +++ b/source/target-vendors/cp312/pydantic-2.13.5.dist-info/RECORD @@ -0,0 +1,111 @@ +pydantic/__init__.py,sha256=5iEnJ4wHv1OEzdKQPzaKaZKfO4pSQAC65ODrYI6_S8Y,15812 +pydantic/_migration.py,sha256=VF73LRCUz3Irb5xVt13jb3NAcXVnEF6T1-J0OLfeZ5A,12160 +pydantic/alias_generators.py,sha256=KM1n3u4JfLSBl1UuYg3hoYHzXJD-yvgrnq8u1ccwh_A,2124 +pydantic/aliases.py,sha256=vhCHyoSWnX-EJ-wWb5qj4xyRssgGWnTQfzQp4GSZ9ug,4937 +pydantic/annotated_handlers.py,sha256=WfyFSqwoEIFXBh7T73PycKloI1DiX45GWi0-JOsCR4Y,4407 +pydantic/class_validators.py,sha256=i_V3j-PYdGLSLmj_IJZekTRjunO8SIVz8LMlquPyP7E,148 +pydantic/color.py,sha256=AzqGfVQHF92_ZctDcue0DM4yTp2P6tekkwRINTWrLIo,21481 +pydantic/config.py,sha256=o1P67FMWIQG-_RfGtKislkHwo4pXm_6jfsR41P88v78,44533 +pydantic/dataclasses.py,sha256=4X9We0jj1KLwBtvYSkAFXNon46zrpBmRZANf4LfwbXg,18963 +pydantic/datetime_parse.py,sha256=QC-WgMxMr_wQ_mNXUS7AVf-2hLEhvvsPY1PQyhSGOdk,150 +pydantic/decorator.py,sha256=YX-jUApu5AKaVWKPoaV-n-4l7UbS69GEt9Ra3hszmKI,145 +pydantic/env_settings.py,sha256=6IHeeWEqlUPRUv3V-AXiF_W91fg2Jw_M3O0l34J_eyA,148 +pydantic/error_wrappers.py,sha256=RK6mqATc9yMD-KBD9IJS9HpKCprWHd8wo84Bnm-3fR8,150 +pydantic/errors.py,sha256=DrECPCWhSYrQ8Ba4O8hKzIAM2i9GBHTXWALzaFDpLf4,6013 +pydantic/fields.py,sha256=a8ZhJfI8FD6TQDD7-cWMm1ZXlQva0wMdT2Ey5VvVe-M,82023 +pydantic/functional_serializers.py,sha256=zwRAjZusORtEbtxpVU20kg8FpemyZz4Fq6wJk1mpwYQ,18117 +pydantic/functional_validators.py,sha256=7p-4jvP__9jZyvcdXiide5pAa_JlM7fJ1BgkqbjxSWM,31724 +pydantic/generics.py,sha256=0ZqZ9O9annIj_3mGBRqps4htey3b5lV1-d2tUxPMMnA,144 +pydantic/json.py,sha256=ZH8RkI7h4Bz-zp8OdTAxbJUoVvcoU-jhMdRZ0B-k0xc,140 +pydantic/json_schema.py,sha256=da3hQ9vQPLEhPsrDnUlijfQ1fuCCViGVnUzOrAZLgDs,125955 +pydantic/main.py,sha256=NbhCz-ku8wDgYLQMBi75Ov7hywdc8LewA3oUNIZ8JVQ,85334 +pydantic/mypy.py,sha256=ypnhh7hn-L-mk6sW4CcrdV2C5lyMzcM6FTBj21GufsE,61074 +pydantic/networks.py,sha256=gCB96gt0G7tiVDhVnJfpKr1ARL5qkH-SPZkuNkmG2O4,42102 +pydantic/parse.py,sha256=wkd82dgtvWtD895U_I6E1htqMlGhBSYEV39cuBSeo3A,141 +pydantic/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pydantic/root_model.py,sha256=cOMoeWdp536KF85uRIcW-oz1O7d5GRCpx9HTS4-1uf8,6394 +pydantic/schema.py,sha256=Vqqjvq_LnapVknebUd3Bp_J1p2gXZZnZRgL48bVEG7o,142 +pydantic/tools.py,sha256=iHQpd8SJ5DCTtPV5atAV06T89bjSaMFeZZ2LX9lasZY,141 +pydantic/type_adapter.py,sha256=T05g8WQczBsVU_35RdKxgjIi7Y7LIip67pF3NI7X4GE,36123 +pydantic/types.py,sha256=fpYcGnAncK4QjaFm3jZtqYwxJYzB7rJJPfAl89emzyQ,105961 +pydantic/typing.py,sha256=P7feA35MwTcLsR1uL7db0S-oydBxobmXa55YDoBgajQ,138 +pydantic/utils.py,sha256=15nR2QpqTBFlQV4TNtTItMyTJx_fbyV-gPmIEY1Gooc,141 +pydantic/validate_call_decorator.py,sha256=VLAi4hoFpjC-1eL0HixYaaWaEFO6htGcDctgsqa5VII,4416 +pydantic/validators.py,sha256=pwbIJXVb1CV2mAE4w_EGfNj7DwzsKaWw_tTL6cviTus,146 +pydantic/version.py,sha256=uKOxh0LjiZkZ0KfLmlpPYRAY-_NpIX8ijxOLDNhW8m8,3985 +pydantic/warnings.py,sha256=3QyQo6lN35cO7OXDbFEXWcNaPlGCRaregVZA-G-lZwI,4822 +pydantic/_internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pydantic/_internal/_config.py,sha256=Rzys1Joffn4JczElcYDqsZLRgBgHn2lYWqDR55oASPA,14839 +pydantic/_internal/_core_metadata.py,sha256=Y_g2t3i7uluK-wXCZvzJfRFMPUM23aBYLfae4FzBPy0,5162 +pydantic/_internal/_core_utils.py,sha256=1jru4VbJ0x63R6dtVcuOI-dKQTC_d_lSnJWEBQzGNEQ,6487 +pydantic/_internal/_dataclasses.py,sha256=Zgqcm1WaJLBwTQQC5mGKNowjlTgX3mfX_J5e2vd24lM,13188 +pydantic/_internal/_decorators.py,sha256=RDEG_Jau5NiJcfO0xgdT7EOgsU1LgWIYlX7wN5rYtVs,33620 +pydantic/_internal/_decorators_v1.py,sha256=tfdfdpQKY4R2XCOwqHbZeoQMur6VNigRrfhudXBHx38,6185 +pydantic/_internal/_discriminated_union.py,sha256=JLx_MVLep7Mxl1zbpdNZjvHDcz-J3OEW6WcdV184dcM,26255 +pydantic/_internal/_docs_extraction.py,sha256=fyznSAHh5AzohnXZStV0HvH-nRbavNHPyg-knx-S_EE,4127 +pydantic/_internal/_fields.py,sha256=hXeb-zodGwTDvG9OK1um18P64b7cqHb0GOngP4jrgcY,31557 +pydantic/_internal/_forward_ref.py,sha256=5n3Y7-3AKLn8_FS3Yc7KutLiPUhyXmAtkEZOaFnonwM,611 +pydantic/_internal/_generate_schema.py,sha256=PjogUawIXmf8LuLNe9seJph7WLW4MJ7-GBsXwCsQC9Q,136348 +pydantic/_internal/_generics.py,sha256=CXjcInlvci8VejaWn1f39kv0AcfJL0R523qRLFZVD-s,23393 +pydantic/_internal/_git.py,sha256=IwPh3DPfa2Xq3rBuB9Nx8luR2A1i69QdeTfWWXIuCVg,809 +pydantic/_internal/_import_utils.py,sha256=TRhxD5OuY6CUosioBdBcJUs0om7IIONiZdYAV7zQ8jM,402 +pydantic/_internal/_internal_dataclass.py,sha256=_bedc1XbuuygRGiLZqkUkwwFpQaoR1hKLlR501nyySY,144 +pydantic/_internal/_known_annotated_metadata.py,sha256=PynQIFQ61__4Gcrzn0D5ENllg7jPq_cxoLTmuFQBY88,16805 +pydantic/_internal/_mock_val_ser.py,sha256=wmRRFSBvqfcLbI41PsFliB4u2AZ3mJpZeiERbD3xKTo,8885 +pydantic/_internal/_model_construction.py,sha256=JoKmY4JrDBu3nG_tCIrJgtJJE1uq6v29TvTz5ElHE5g,38928 +pydantic/_internal/_namespace_utils.py,sha256=hl3-TRAr82U2jTyPP3t-QqsvKLirxtkLfNfrN-fp0x8,12878 +pydantic/_internal/_repr.py,sha256=jQfnJuyDxQpSRNhG29II9PX8e4Nv2qWZrEw2lqih3UE,5172 +pydantic/_internal/_schema_gather.py,sha256=8nJ-uM6Y4z6xpasnGonEMubtNVX_mxeeRDFmd_qMVLA,9052 +pydantic/_internal/_schema_generation_shared.py,sha256=F_rbQbrkoomgxsskdHpP0jUJ7TCfe0BADAEkq6CJ4nM,4842 +pydantic/_internal/_serializers.py,sha256=YIWvSmAR5fnbGSWCOQduWt1yB4ZQY42eAruc-enrb6c,1491 +pydantic/_internal/_signature.py,sha256=i_b6wtluiVWZRh1ZY8UvB2UZziP1KjqSXZgC-HxwOT0,6808 +pydantic/_internal/_typing_extra.py,sha256=dDxqF46lzuqCoKLrAH_k95EDbayEeKb2lHjuTJ5OBoY,31574 +pydantic/_internal/_utils.py,sha256=gN48BsR-FDrJDibCmo69ttQg67WbuFrdy_1NQL3cvLI,15959 +pydantic/_internal/_validate_call.py,sha256=OD_BspHaL9FKzZ9XrndhiEuMnjF3SRIJUHtwv6yUffU,5366 +pydantic/_internal/_validators.py,sha256=7GTjXXWFMLib4dxQ-HeaiHlAZiR2B2G8byCYMGrmQ48,20563 +pydantic/deprecated/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pydantic/deprecated/class_validators.py,sha256=n0jYQOcb5YQiw0b7YXyi7NPiYdV7ujWR4KyZumjTPok,10281 +pydantic/deprecated/config.py,sha256=k_lsVk57paxLJOcBueH07cu1OgEgWdVBxm6lfaC3CCU,2663 +pydantic/deprecated/copy_internals.py,sha256=Ghd-vkMd5EYCCgyCGtPKO58np9cEKBQC6qkBeIEFI2g,7618 +pydantic/deprecated/decorator.py,sha256=TBm6bJ7wJsNih_8Wq5IzDcwP32m9_vfxs96desLuk00,10845 +pydantic/deprecated/json.py,sha256=HlWCG35RRrxyzuTS6LTQiZBwRhmDZWmeqQH8rLW6wA8,4657 +pydantic/deprecated/parse.py,sha256=Gzd6b_g8zJXcuE7QRq5adhx_EMJahXfcpXCF0RgrqqI,2511 +pydantic/deprecated/tools.py,sha256=Nrm9oFRZWp8-jlfvPgJILEsywp4YzZD52XIGPDLxHcI,3330 +pydantic/experimental/__init__.py,sha256=QT7rKYdDsCiTJ9GEjmsQdWHScwpKrrNkGq6vqONP6RQ,104 +pydantic/experimental/arguments_schema.py,sha256=EFnjX_ulp-tPyUjQX5pmQtug1OFL_Acc8bcMbLd-fVY,1866 +pydantic/experimental/missing_sentinel.py,sha256=hQejgtF00wUuQMni9429evg-eXyIwpKvjsD8ofqfj-w,127 +pydantic/experimental/pipeline.py,sha256=auoW6l6g1FC41LciPmsI1M6ncf00Szde9B3C-yvb9mI,23956 +pydantic/plugin/__init__.py,sha256=a7Tw366U6K3kltCCNZY76nc9ss-7uGGQ40TXad9OypQ,7333 +pydantic/plugin/_loader.py,sha256=hAjgSljoIhGx3AVpIpuqw5SPttBNNeGBSTrqSMnNiJk,2213 +pydantic/plugin/_schema_validator.py,sha256=thbZz_-5sYPzxsC5uWK5GDfYcmmrsecC8Vw_A02mhwU,5815 +pydantic/v1/__init__.py,sha256=SxQPklgBs4XHJwE6BZ9qoewYoGiNyYUnmHzEFCZbfnI,2946 +pydantic/v1/_hypothesis_plugin.py,sha256=5ES5xWuw1FQAsymLezy8QgnVz0ZpVfU3jkmT74H27VQ,14847 +pydantic/v1/annotated_types.py,sha256=uk2NAAxqiNELKjiHhyhxKaIOh8F1lYW_LzrW3X7oZBc,3157 +pydantic/v1/class_validators.py,sha256=ULOaIUgYUDBsHL7EEVEarcM-UubKUggoN8hSbDonsFE,14672 +pydantic/v1/color.py,sha256=iZABLYp6OVoo2AFkP9Ipri_wSc6-Kklu8YuhSartd5g,16844 +pydantic/v1/config.py,sha256=a6P0Wer9x4cbwKW7Xv8poSUqM4WP-RLWwX6YMpYq9AA,6532 +pydantic/v1/dataclasses.py,sha256=784cqvInbwIPWr9usfpX3ch7z4t3J2tTK6N067_wk1o,18172 +pydantic/v1/datetime_parse.py,sha256=4Qy1kQpq3rNVZJeIHeSPDpuS2Bvhp1KPtzJG1xu-H00,7724 +pydantic/v1/decorator.py,sha256=zaaxxxoWPCm818D1bs0yhapRjXm32V8G0ZHWCdM1uXA,10339 +pydantic/v1/env_settings.py,sha256=A9VXwtRl02AY-jH0C0ouy5VNw3fi6F_pkzuHDjgAAOM,14105 +pydantic/v1/error_wrappers.py,sha256=6625Mfw9qkC2NwitB_JFAWe8B-Xv6zBU7rL9k28tfyo,5196 +pydantic/v1/errors.py,sha256=mIwPED5vGM5Q5v4C4Z1JPldTRH-omvEylH6ksMhOmPw,17726 +pydantic/v1/fields.py,sha256=VqWJCriUNiEyptXroDVJ501JpVA0en2VANcksqXL2b8,50649 +pydantic/v1/generics.py,sha256=YzyKTZN6x5Q1RGJ3WQ9jN-uwHJxL3W4qoZqwcZXqxWg,17829 +pydantic/v1/json.py,sha256=WQ5Hy_hIpfdR3YS8k6N2E6KMJzsdbBi_ldWOPJaV81M,3390 +pydantic/v1/main.py,sha256=vRB1TbpkzPN3P5ijJlc-cjNuO-HciNOpC4b8K3zZnfc,45697 +pydantic/v1/mypy.py,sha256=Cl8XRfCmIcVE3j5AEU52C8iDh8lcX__D3hz2jIWxMAs,38860 +pydantic/v1/networks.py,sha256=HYNtKAfOmOnKJpsDg1g6SIkj9WPhU_-i8l5e2JKBpG4,22124 +pydantic/v1/parse.py,sha256=BJtdqiZRtav9VRFCmOxoY-KImQmjPy-A_NoojiFUZxY,1821 +pydantic/v1/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pydantic/v1/schema.py,sha256=aqBuA--cq8gAVkim5BJPFASHzOZ8dFtmFX_fNGr6ip4,47801 +pydantic/v1/tools.py,sha256=1lDdXHk0jL5uP3u5RCYAvUAlGClgAO-45lkq9j7fyBA,2881 +pydantic/v1/types.py,sha256=Bzl-RcnitPBHnqwwj9iv7JjHuN1GpnWH24dKkF3l9e8,35455 +pydantic/v1/typing.py,sha256=ovwtLpEZCbnghZaHfSNJupzetzHNkLXjn_66kgTnIV4,20102 +pydantic/v1/utils.py,sha256=1PqOIlz6OVWwGds3HBKlw4Et6asFou0UUpAto7jFOCs,26014 +pydantic/v1/validators.py,sha256=lyUkn1MWhHxlCX5ZfEgFj_CAHojoiPcaQeMdEM9XviU,22187 +pydantic/v1/version.py,sha256=YpHWOQKtGoxfyikzGrcmXJVKUVYB9EBdoCR994QxSnE,1039 +pydantic-2.13.5.dist-info/METADATA,sha256=BoWDDRNke9bz8FUmBD0yqLKiIdhrU3cwU9bFK7F6nAc,110178 +pydantic-2.13.5.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87 +pydantic-2.13.5.dist-info/licenses/LICENSE,sha256=qeGG88oWte74QxjnpwFyE1GgDLe4rjpDlLZ7SeNSnvM,1129 +pydantic-2.13.5.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/pydantic-2.13.5.dist-info/WHEEL b/source/target-vendors/cp312/pydantic-2.13.5.dist-info/WHEEL new file mode 100644 index 0000000..51abe1f --- /dev/null +++ b/source/target-vendors/cp312/pydantic-2.13.5.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.32.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/source/target-vendors/cp312/pydantic-2.13.5.dist-info/licenses/LICENSE b/source/target-vendors/cp312/pydantic-2.13.5.dist-info/licenses/LICENSE new file mode 100644 index 0000000..488c626 --- /dev/null +++ b/source/target-vendors/cp312/pydantic-2.13.5.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 to present Pydantic Services Inc. and individual contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/source/target-vendors/cp312/pydantic/__init__.py b/source/target-vendors/cp312/pydantic/__init__.py new file mode 100644 index 0000000..0121284 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/__init__.py @@ -0,0 +1,456 @@ +from importlib import import_module +from typing import TYPE_CHECKING +from warnings import warn + +from ._migration import getattr_migration +from .version import VERSION, _ensure_pydantic_core_version + +_ensure_pydantic_core_version() +del _ensure_pydantic_core_version + +if TYPE_CHECKING: + # import of virtually everything is supported via `__getattr__` below, + # but we need them here for type checking and IDE support + import pydantic_core + from pydantic_core.core_schema import ( + FieldSerializationInfo, + SerializationInfo, + SerializerFunctionWrapHandler, + ValidationInfo, + ValidatorFunctionWrapHandler, + ) + + from . import dataclasses + from .aliases import AliasChoices, AliasGenerator, AliasPath + from .annotated_handlers import GetCoreSchemaHandler, GetJsonSchemaHandler + from .config import ConfigDict, with_config + from .errors import * + from .fields import Field, PrivateAttr, computed_field + from .functional_serializers import ( + PlainSerializer, + SerializeAsAny, + WrapSerializer, + field_serializer, + model_serializer, + ) + from .functional_validators import ( + AfterValidator, + BeforeValidator, + InstanceOf, + ModelWrapValidatorHandler, + PlainValidator, + SkipValidation, + ValidateAs, + WrapValidator, + field_validator, + model_validator, + ) + from .json_schema import WithJsonSchema + from .main import * + from .networks import * + from .type_adapter import TypeAdapter + from .types import * + from .validate_call_decorator import validate_call + from .warnings import ( + PydanticDeprecatedSince20, + PydanticDeprecatedSince26, + PydanticDeprecatedSince29, + PydanticDeprecatedSince210, + PydanticDeprecatedSince211, + PydanticDeprecatedSince212, + PydanticDeprecationWarning, + PydanticExperimentalWarning, + ) + + # this encourages pycharm to import `ValidationError` from here, not pydantic_core + ValidationError = pydantic_core.ValidationError + from .deprecated.class_validators import root_validator, validator + from .deprecated.config import BaseConfig, Extra + from .deprecated.tools import * + from .root_model import RootModel + +__version__ = VERSION +__all__ = ( + # dataclasses + 'dataclasses', + # functional validators + 'field_validator', + 'model_validator', + 'AfterValidator', + 'BeforeValidator', + 'PlainValidator', + 'WrapValidator', + 'SkipValidation', + 'ValidateAs', + 'InstanceOf', + 'ModelWrapValidatorHandler', + # JSON Schema + 'WithJsonSchema', + # deprecated V1 functional validators, these are imported via `__getattr__` below + 'root_validator', + 'validator', + # functional serializers + 'field_serializer', + 'model_serializer', + 'PlainSerializer', + 'SerializeAsAny', + 'WrapSerializer', + # config + 'ConfigDict', + 'with_config', + # deprecated V1 config, these are imported via `__getattr__` below + 'BaseConfig', + 'Extra', + # validate_call + 'validate_call', + # errors + 'PydanticErrorCodes', + 'PydanticUserError', + 'PydanticSchemaGenerationError', + 'PydanticImportError', + 'PydanticUndefinedAnnotation', + 'PydanticInvalidForJsonSchema', + 'PydanticForbiddenQualifier', + # fields + 'Field', + 'computed_field', + 'PrivateAttr', + # alias + 'AliasChoices', + 'AliasGenerator', + 'AliasPath', + # main + 'BaseModel', + 'create_model', + # network + 'AnyUrl', + 'AnyHttpUrl', + 'FileUrl', + 'HttpUrl', + 'FtpUrl', + 'WebsocketUrl', + 'AnyWebsocketUrl', + 'UrlConstraints', + 'EmailStr', + 'NameEmail', + 'IPvAnyAddress', + 'IPvAnyInterface', + 'IPvAnyNetwork', + 'PostgresDsn', + 'CockroachDsn', + 'AmqpDsn', + 'RedisDsn', + 'MongoDsn', + 'KafkaDsn', + 'NatsDsn', + 'MySQLDsn', + 'MariaDBDsn', + 'ClickHouseDsn', + 'SnowflakeDsn', + 'validate_email', + # root_model + 'RootModel', + # deprecated tools, these are imported via `__getattr__` below + 'parse_obj_as', + 'schema_of', + 'schema_json_of', + # types + 'Strict', + 'StrictStr', + 'conbytes', + 'conlist', + 'conset', + 'confrozenset', + 'constr', + 'StringConstraints', + 'ImportString', + 'conint', + 'PositiveInt', + 'NegativeInt', + 'NonNegativeInt', + 'NonPositiveInt', + 'confloat', + 'PositiveFloat', + 'NegativeFloat', + 'NonNegativeFloat', + 'NonPositiveFloat', + 'FiniteFloat', + 'condecimal', + 'condate', + 'UUID1', + 'UUID3', + 'UUID4', + 'UUID5', + 'UUID6', + 'UUID7', + 'UUID8', + 'FilePath', + 'DirectoryPath', + 'NewPath', + 'Json', + 'Secret', + 'SecretStr', + 'SecretBytes', + 'SocketPath', + 'StrictBool', + 'StrictBytes', + 'StrictInt', + 'StrictFloat', + 'PaymentCardNumber', + 'ByteSize', + 'PastDate', + 'FutureDate', + 'PastDatetime', + 'FutureDatetime', + 'AwareDatetime', + 'NaiveDatetime', + 'AllowInfNan', + 'EncoderProtocol', + 'EncodedBytes', + 'EncodedStr', + 'Base64Encoder', + 'Base64Bytes', + 'Base64Str', + 'Base64UrlBytes', + 'Base64UrlStr', + 'GetPydanticSchema', + 'Tag', + 'Discriminator', + 'JsonValue', + 'FailFast', + # type_adapter + 'TypeAdapter', + # version + '__version__', + 'VERSION', + # warnings + 'PydanticDeprecatedSince20', + 'PydanticDeprecatedSince26', + 'PydanticDeprecatedSince29', + 'PydanticDeprecatedSince210', + 'PydanticDeprecatedSince211', + 'PydanticDeprecatedSince212', + 'PydanticDeprecationWarning', + 'PydanticExperimentalWarning', + # annotated handlers + 'GetCoreSchemaHandler', + 'GetJsonSchemaHandler', + # pydantic_core + 'ValidationError', + 'ValidationInfo', + 'SerializationInfo', + 'ValidatorFunctionWrapHandler', + 'FieldSerializationInfo', + 'SerializerFunctionWrapHandler', + 'OnErrorOmit', +) + +# A mapping of {: (package, )} defining dynamic imports +_dynamic_imports: 'dict[str, tuple[str, str]]' = { + 'dataclasses': (__spec__.parent, '__module__'), + # functional validators + 'field_validator': (__spec__.parent, '.functional_validators'), + 'model_validator': (__spec__.parent, '.functional_validators'), + 'AfterValidator': (__spec__.parent, '.functional_validators'), + 'BeforeValidator': (__spec__.parent, '.functional_validators'), + 'PlainValidator': (__spec__.parent, '.functional_validators'), + 'WrapValidator': (__spec__.parent, '.functional_validators'), + 'SkipValidation': (__spec__.parent, '.functional_validators'), + 'InstanceOf': (__spec__.parent, '.functional_validators'), + 'ValidateAs': (__spec__.parent, '.functional_validators'), + 'ModelWrapValidatorHandler': (__spec__.parent, '.functional_validators'), + # JSON Schema + 'WithJsonSchema': (__spec__.parent, '.json_schema'), + # functional serializers + 'field_serializer': (__spec__.parent, '.functional_serializers'), + 'model_serializer': (__spec__.parent, '.functional_serializers'), + 'PlainSerializer': (__spec__.parent, '.functional_serializers'), + 'SerializeAsAny': (__spec__.parent, '.functional_serializers'), + 'WrapSerializer': (__spec__.parent, '.functional_serializers'), + # config + 'ConfigDict': (__spec__.parent, '.config'), + 'with_config': (__spec__.parent, '.config'), + # validate call + 'validate_call': (__spec__.parent, '.validate_call_decorator'), + # errors + 'PydanticErrorCodes': (__spec__.parent, '.errors'), + 'PydanticUserError': (__spec__.parent, '.errors'), + 'PydanticSchemaGenerationError': (__spec__.parent, '.errors'), + 'PydanticImportError': (__spec__.parent, '.errors'), + 'PydanticUndefinedAnnotation': (__spec__.parent, '.errors'), + 'PydanticInvalidForJsonSchema': (__spec__.parent, '.errors'), + 'PydanticForbiddenQualifier': (__spec__.parent, '.errors'), + # fields + 'Field': (__spec__.parent, '.fields'), + 'computed_field': (__spec__.parent, '.fields'), + 'PrivateAttr': (__spec__.parent, '.fields'), + # alias + 'AliasChoices': (__spec__.parent, '.aliases'), + 'AliasGenerator': (__spec__.parent, '.aliases'), + 'AliasPath': (__spec__.parent, '.aliases'), + # main + 'BaseModel': (__spec__.parent, '.main'), + 'create_model': (__spec__.parent, '.main'), + # network + 'AnyUrl': (__spec__.parent, '.networks'), + 'AnyHttpUrl': (__spec__.parent, '.networks'), + 'FileUrl': (__spec__.parent, '.networks'), + 'HttpUrl': (__spec__.parent, '.networks'), + 'FtpUrl': (__spec__.parent, '.networks'), + 'WebsocketUrl': (__spec__.parent, '.networks'), + 'AnyWebsocketUrl': (__spec__.parent, '.networks'), + 'UrlConstraints': (__spec__.parent, '.networks'), + 'EmailStr': (__spec__.parent, '.networks'), + 'NameEmail': (__spec__.parent, '.networks'), + 'IPvAnyAddress': (__spec__.parent, '.networks'), + 'IPvAnyInterface': (__spec__.parent, '.networks'), + 'IPvAnyNetwork': (__spec__.parent, '.networks'), + 'PostgresDsn': (__spec__.parent, '.networks'), + 'CockroachDsn': (__spec__.parent, '.networks'), + 'AmqpDsn': (__spec__.parent, '.networks'), + 'RedisDsn': (__spec__.parent, '.networks'), + 'MongoDsn': (__spec__.parent, '.networks'), + 'KafkaDsn': (__spec__.parent, '.networks'), + 'NatsDsn': (__spec__.parent, '.networks'), + 'MySQLDsn': (__spec__.parent, '.networks'), + 'MariaDBDsn': (__spec__.parent, '.networks'), + 'ClickHouseDsn': (__spec__.parent, '.networks'), + 'SnowflakeDsn': (__spec__.parent, '.networks'), + 'validate_email': (__spec__.parent, '.networks'), + # root_model + 'RootModel': (__spec__.parent, '.root_model'), + # types + 'Strict': (__spec__.parent, '.types'), + 'StrictStr': (__spec__.parent, '.types'), + 'conbytes': (__spec__.parent, '.types'), + 'conlist': (__spec__.parent, '.types'), + 'conset': (__spec__.parent, '.types'), + 'confrozenset': (__spec__.parent, '.types'), + 'constr': (__spec__.parent, '.types'), + 'StringConstraints': (__spec__.parent, '.types'), + 'ImportString': (__spec__.parent, '.types'), + 'conint': (__spec__.parent, '.types'), + 'PositiveInt': (__spec__.parent, '.types'), + 'NegativeInt': (__spec__.parent, '.types'), + 'NonNegativeInt': (__spec__.parent, '.types'), + 'NonPositiveInt': (__spec__.parent, '.types'), + 'confloat': (__spec__.parent, '.types'), + 'PositiveFloat': (__spec__.parent, '.types'), + 'NegativeFloat': (__spec__.parent, '.types'), + 'NonNegativeFloat': (__spec__.parent, '.types'), + 'NonPositiveFloat': (__spec__.parent, '.types'), + 'FiniteFloat': (__spec__.parent, '.types'), + 'condecimal': (__spec__.parent, '.types'), + 'condate': (__spec__.parent, '.types'), + 'UUID1': (__spec__.parent, '.types'), + 'UUID3': (__spec__.parent, '.types'), + 'UUID4': (__spec__.parent, '.types'), + 'UUID5': (__spec__.parent, '.types'), + 'UUID6': (__spec__.parent, '.types'), + 'UUID7': (__spec__.parent, '.types'), + 'UUID8': (__spec__.parent, '.types'), + 'FilePath': (__spec__.parent, '.types'), + 'DirectoryPath': (__spec__.parent, '.types'), + 'NewPath': (__spec__.parent, '.types'), + 'Json': (__spec__.parent, '.types'), + 'Secret': (__spec__.parent, '.types'), + 'SecretStr': (__spec__.parent, '.types'), + 'SecretBytes': (__spec__.parent, '.types'), + 'StrictBool': (__spec__.parent, '.types'), + 'StrictBytes': (__spec__.parent, '.types'), + 'StrictInt': (__spec__.parent, '.types'), + 'StrictFloat': (__spec__.parent, '.types'), + 'PaymentCardNumber': (__spec__.parent, '.types'), + 'ByteSize': (__spec__.parent, '.types'), + 'PastDate': (__spec__.parent, '.types'), + 'SocketPath': (__spec__.parent, '.types'), + 'FutureDate': (__spec__.parent, '.types'), + 'PastDatetime': (__spec__.parent, '.types'), + 'FutureDatetime': (__spec__.parent, '.types'), + 'AwareDatetime': (__spec__.parent, '.types'), + 'NaiveDatetime': (__spec__.parent, '.types'), + 'AllowInfNan': (__spec__.parent, '.types'), + 'EncoderProtocol': (__spec__.parent, '.types'), + 'EncodedBytes': (__spec__.parent, '.types'), + 'EncodedStr': (__spec__.parent, '.types'), + 'Base64Encoder': (__spec__.parent, '.types'), + 'Base64Bytes': (__spec__.parent, '.types'), + 'Base64Str': (__spec__.parent, '.types'), + 'Base64UrlBytes': (__spec__.parent, '.types'), + 'Base64UrlStr': (__spec__.parent, '.types'), + 'GetPydanticSchema': (__spec__.parent, '.types'), + 'Tag': (__spec__.parent, '.types'), + 'Discriminator': (__spec__.parent, '.types'), + 'JsonValue': (__spec__.parent, '.types'), + 'OnErrorOmit': (__spec__.parent, '.types'), + 'FailFast': (__spec__.parent, '.types'), + # type_adapter + 'TypeAdapter': (__spec__.parent, '.type_adapter'), + # warnings + 'PydanticDeprecatedSince20': (__spec__.parent, '.warnings'), + 'PydanticDeprecatedSince26': (__spec__.parent, '.warnings'), + 'PydanticDeprecatedSince29': (__spec__.parent, '.warnings'), + 'PydanticDeprecatedSince210': (__spec__.parent, '.warnings'), + 'PydanticDeprecatedSince211': (__spec__.parent, '.warnings'), + 'PydanticDeprecatedSince212': (__spec__.parent, '.warnings'), + 'PydanticDeprecationWarning': (__spec__.parent, '.warnings'), + 'PydanticExperimentalWarning': (__spec__.parent, '.warnings'), + # annotated handlers + 'GetCoreSchemaHandler': (__spec__.parent, '.annotated_handlers'), + 'GetJsonSchemaHandler': (__spec__.parent, '.annotated_handlers'), + # pydantic_core stuff + 'ValidationError': ('pydantic_core', '.'), + 'ValidationInfo': ('pydantic_core', '.core_schema'), + 'SerializationInfo': ('pydantic_core', '.core_schema'), + 'ValidatorFunctionWrapHandler': ('pydantic_core', '.core_schema'), + 'FieldSerializationInfo': ('pydantic_core', '.core_schema'), + 'SerializerFunctionWrapHandler': ('pydantic_core', '.core_schema'), + # deprecated, mostly not included in __all__ + 'root_validator': (__spec__.parent, '.deprecated.class_validators'), + 'validator': (__spec__.parent, '.deprecated.class_validators'), + 'BaseConfig': (__spec__.parent, '.deprecated.config'), + 'Extra': (__spec__.parent, '.deprecated.config'), + 'parse_obj_as': (__spec__.parent, '.deprecated.tools'), + 'schema_of': (__spec__.parent, '.deprecated.tools'), + 'schema_json_of': (__spec__.parent, '.deprecated.tools'), + # deprecated dynamic imports + 'FieldValidationInfo': ('pydantic_core', '.core_schema'), + 'GenerateSchema': (__spec__.parent, '._internal._generate_schema'), +} +_deprecated_dynamic_imports = {'FieldValidationInfo', 'GenerateSchema'} + +_getattr_migration = getattr_migration(__name__) + + +def __getattr__(attr_name: str) -> object: + if attr_name in _deprecated_dynamic_imports: + from pydantic.warnings import PydanticDeprecatedSince20 + + warn( + f'Importing {attr_name} from `pydantic` is deprecated. This feature is either no longer supported, or is not public.', + PydanticDeprecatedSince20, + stacklevel=2, + ) + + dynamic_attr = _dynamic_imports.get(attr_name) + if dynamic_attr is None: + return _getattr_migration(attr_name) + + package, module_name = dynamic_attr + + if module_name == '__module__': + result = import_module(f'.{attr_name}', package=package) + globals()[attr_name] = result + return result + else: + module = import_module(module_name, package=package) + result = getattr(module, attr_name) + g = globals() + for k, (_, v_module_name) in _dynamic_imports.items(): + if v_module_name == module_name and k not in _deprecated_dynamic_imports: + g[k] = getattr(module, k) + return result + + +def __dir__() -> list[str]: + return list(__all__) diff --git a/source/target-vendors/cp312/pydantic/_internal/__init__.py b/source/target-vendors/cp312/pydantic/_internal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/pydantic/_internal/_config.py b/source/target-vendors/cp312/pydantic/_internal/_config.py new file mode 100644 index 0000000..73b5870 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_config.py @@ -0,0 +1,386 @@ +from __future__ import annotations as _annotations + +import warnings +from contextlib import contextmanager +from re import Pattern +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Literal, + cast, +) + +from pydantic_core import core_schema +from typing_extensions import Self + +from ..aliases import AliasGenerator +from ..config import ConfigDict, ExtraValues, JsonDict, JsonEncoder, JsonSchemaExtraCallable +from ..errors import PydanticUserError +from ..warnings import PydanticDeprecatedSince20, PydanticDeprecatedSince210 + +if TYPE_CHECKING: + from .._internal._schema_generation_shared import GenerateSchema + from ..fields import ComputedFieldInfo, FieldInfo + +DEPRECATION_MESSAGE = 'Support for class-based `config` is deprecated, use ConfigDict instead.' + + +class ConfigWrapper: + """Internal wrapper for Config which exposes ConfigDict items as attributes.""" + + __slots__ = ('config_dict',) + + config_dict: ConfigDict + + # all annotations are copied directly from ConfigDict, and should be kept up to date, a test will fail if they + # stop matching + title: str | None + str_to_lower: bool + str_to_upper: bool + str_strip_whitespace: bool + str_min_length: int + str_max_length: int | None + extra: ExtraValues | None + frozen: bool + populate_by_name: bool + use_enum_values: bool + validate_assignment: bool + arbitrary_types_allowed: bool + from_attributes: bool + # whether to use the actual key provided in the data (e.g. alias or first alias for "field required" errors) instead of field_names + # to construct error `loc`s, default `True` + loc_by_alias: bool + alias_generator: Callable[[str], str] | AliasGenerator | None + model_title_generator: Callable[[type], str] | None + field_title_generator: Callable[[str, FieldInfo | ComputedFieldInfo], str] | None + ignored_types: tuple[type, ...] + allow_inf_nan: bool + json_schema_extra: JsonDict | JsonSchemaExtraCallable | None + json_encoders: dict[type[object], JsonEncoder] | None + + # new in V2 + strict: bool + # whether instances of models and dataclasses (including subclass instances) should re-validate, default 'never' + revalidate_instances: Literal['always', 'never', 'subclass-instances'] + ser_json_timedelta: Literal['iso8601', 'float'] + ser_json_temporal: Literal['iso8601', 'seconds', 'milliseconds'] + val_temporal_unit: Literal['seconds', 'milliseconds', 'infer'] + ser_json_bytes: Literal['utf8', 'base64', 'hex'] + val_json_bytes: Literal['utf8', 'base64', 'hex'] + ser_json_inf_nan: Literal['null', 'constants', 'strings'] + # whether to validate default values during validation, default False + validate_default: bool + validate_return: bool + protected_namespaces: tuple[str | Pattern[str], ...] + hide_input_in_errors: bool + defer_build: bool + plugin_settings: dict[str, object] | None + schema_generator: type[GenerateSchema] | None + json_schema_serialization_defaults_required: bool + json_schema_mode_override: Literal['validation', 'serialization', None] + coerce_numbers_to_str: bool + regex_engine: Literal['rust-regex', 'python-re'] + validation_error_cause: bool + use_attribute_docstrings: bool + cache_strings: bool | Literal['all', 'keys', 'none'] + validate_by_alias: bool + validate_by_name: bool + serialize_by_alias: bool + url_preserve_empty_path: bool + polymorphic_serialization: bool + + def __init__(self, config: ConfigDict | dict[str, Any] | type[Any] | None, *, check: bool = True): + if check: + self.config_dict = prepare_config(config) + else: + self.config_dict = cast(ConfigDict, config) + + @classmethod + def for_model( + cls, + bases: tuple[type[Any], ...], + namespace: dict[str, Any], + raw_annotations: dict[str, Any], + kwargs: dict[str, Any], + ) -> Self: + """Build a new `ConfigWrapper` instance for a `BaseModel`. + + The config wrapper built based on (in descending order of priority): + - options from `kwargs` + - options from the `namespace` + - options from the base classes (`bases`) + + Args: + bases: A tuple of base classes. + namespace: The namespace of the class being created. + raw_annotations: The (non-evaluated) annotations of the model. + kwargs: The kwargs passed to the class being created. + + Returns: + A `ConfigWrapper` instance for `BaseModel`. + """ + config_new = ConfigDict() + for base in bases: + config = getattr(base, 'model_config', None) + if config: + config_new.update(config.copy()) + + config_class_from_namespace = namespace.get('Config') + config_dict_from_namespace = namespace.get('model_config') + + if raw_annotations.get('model_config') and config_dict_from_namespace is None: + raise PydanticUserError( + '`model_config` cannot be used as a model field name. Use `model_config` for model configuration.', + code='model-config-invalid-field-name', + ) + + if config_class_from_namespace and config_dict_from_namespace: + raise PydanticUserError('"Config" and "model_config" cannot be used together', code='config-both') + + config_from_namespace = config_dict_from_namespace or prepare_config(config_class_from_namespace) + + config_new.update(config_from_namespace) + + for k in list(kwargs.keys()): + if k in config_keys: + config_new[k] = kwargs.pop(k) + + return cls(config_new) + + # we don't show `__getattr__` to type checkers so missing attributes cause errors + if not TYPE_CHECKING: # pragma: no branch + + def __getattr__(self, name: str) -> Any: + try: + return self.config_dict[name] + except KeyError: + try: + return config_defaults[name] + except KeyError: + raise AttributeError(f'Config has no attribute {name!r}') from None + + def core_config(self, title: str | None) -> core_schema.CoreConfig: + """Create a pydantic-core config. + + We don't use getattr here since we don't want to populate with defaults. + + Args: + title: The title to use if not set in config. + + Returns: + A `CoreConfig` object created from config. + """ + config = self.config_dict + + if config.get('schema_generator') is not None: + warnings.warn( + 'The `schema_generator` setting has been deprecated since v2.10. This setting no longer has any effect.', + PydanticDeprecatedSince210, + stacklevel=2, + ) + + if (populate_by_name := config.get('populate_by_name')) is not None: + # We include this patch for backwards compatibility purposes, but this config setting will be deprecated in v3.0, and likely removed in v4.0. + # Thus, the above warning and this patch can be removed then as well. + if config.get('validate_by_name') is None: + config['validate_by_alias'] = True + config['validate_by_name'] = populate_by_name + + # We dynamically patch validate_by_name to be True if validate_by_alias is set to False + # and validate_by_name is not explicitly set. + if config.get('validate_by_alias') is False and config.get('validate_by_name') is None: + config['validate_by_name'] = True + + if (not config.get('validate_by_alias', True)) and (not config.get('validate_by_name', False)): + raise PydanticUserError( + 'At least one of `validate_by_alias` or `validate_by_name` must be set to True.', + code='validate-by-alias-and-name-false', + ) + + return core_schema.CoreConfig( + **{ # pyright: ignore[reportArgumentType] + k: v + for k, v in ( + ('title', config.get('title') or title or None), + ('extra_fields_behavior', config.get('extra')), + ('allow_inf_nan', config.get('allow_inf_nan')), + ('str_strip_whitespace', config.get('str_strip_whitespace')), + ('str_to_lower', config.get('str_to_lower')), + ('str_to_upper', config.get('str_to_upper')), + ('strict', config.get('strict')), + ('ser_json_timedelta', config.get('ser_json_timedelta')), + ('ser_json_temporal', config.get('ser_json_temporal')), + ('val_temporal_unit', config.get('val_temporal_unit')), + ('ser_json_bytes', config.get('ser_json_bytes')), + ('val_json_bytes', config.get('val_json_bytes')), + ('ser_json_inf_nan', config.get('ser_json_inf_nan')), + ('from_attributes', config.get('from_attributes')), + ('loc_by_alias', config.get('loc_by_alias')), + ('revalidate_instances', config.get('revalidate_instances')), + ('validate_default', config.get('validate_default')), + ('str_max_length', config.get('str_max_length')), + ('str_min_length', config.get('str_min_length')), + ('hide_input_in_errors', config.get('hide_input_in_errors')), + ('coerce_numbers_to_str', config.get('coerce_numbers_to_str')), + ('regex_engine', config.get('regex_engine')), + ('validation_error_cause', config.get('validation_error_cause')), + ('cache_strings', config.get('cache_strings')), + ('validate_by_alias', config.get('validate_by_alias')), + ('validate_by_name', config.get('validate_by_name')), + ('serialize_by_alias', config.get('serialize_by_alias')), + ('url_preserve_empty_path', config.get('url_preserve_empty_path')), + ('polymorphic_serialization', config.get('polymorphic_serialization')), + ) + if v is not None + } + ) + + def __repr__(self): + c = ', '.join(f'{k}={v!r}' for k, v in self.config_dict.items()) + return f'ConfigWrapper({c})' + + +class ConfigWrapperStack: + """A stack of `ConfigWrapper` instances.""" + + def __init__(self, config_wrapper: ConfigWrapper): + self._config_wrapper_stack: list[ConfigWrapper] = [config_wrapper] + + @property + def tail(self) -> ConfigWrapper: + return self._config_wrapper_stack[-1] + + @contextmanager + def push(self, config_wrapper: ConfigWrapper | ConfigDict | None): + if config_wrapper is None: + yield + return + + if not isinstance(config_wrapper, ConfigWrapper): + config_wrapper = ConfigWrapper(config_wrapper, check=False) + + self._config_wrapper_stack.append(config_wrapper) + try: + yield + finally: + self._config_wrapper_stack.pop() + + +config_defaults = ConfigDict( + title=None, + str_to_lower=False, + str_to_upper=False, + str_strip_whitespace=False, + str_min_length=0, + str_max_length=None, + # let the model / dataclass decide how to handle it + extra=None, + frozen=False, + populate_by_name=False, + use_enum_values=False, + validate_assignment=False, + arbitrary_types_allowed=False, + from_attributes=False, + loc_by_alias=True, + alias_generator=None, + model_title_generator=None, + field_title_generator=None, + ignored_types=(), + allow_inf_nan=True, + json_schema_extra=None, + strict=False, + revalidate_instances='never', + ser_json_timedelta='iso8601', + ser_json_temporal='iso8601', + val_temporal_unit='infer', + ser_json_bytes='utf8', + val_json_bytes='utf8', + ser_json_inf_nan='null', + validate_default=False, + validate_return=False, + protected_namespaces=('model_validate', 'model_dump'), + hide_input_in_errors=False, + json_encoders=None, + defer_build=False, + schema_generator=None, + plugin_settings=None, + json_schema_serialization_defaults_required=False, + json_schema_mode_override=None, + coerce_numbers_to_str=False, + regex_engine='rust-regex', + validation_error_cause=False, + use_attribute_docstrings=False, + cache_strings=True, + validate_by_alias=True, + validate_by_name=False, + serialize_by_alias=False, + url_preserve_empty_path=False, + polymorphic_serialization=False, +) + + +def prepare_config(config: ConfigDict | dict[str, Any] | type[Any] | None) -> ConfigDict: + """Create a `ConfigDict` instance from an existing dict, a class (e.g. old class-based config) or None. + + Args: + config: The input config. + + Returns: + A ConfigDict object created from config. + """ + if config is None: + return ConfigDict() + + if not isinstance(config, dict): + warnings.warn(DEPRECATION_MESSAGE, PydanticDeprecatedSince20, stacklevel=4) + config = {k: getattr(config, k) for k in dir(config) if not k.startswith('__')} + + config_dict = cast(ConfigDict, config) + check_deprecated(config_dict) + return config_dict + + +config_keys = set(ConfigDict.__annotations__.keys()) + + +V2_REMOVED_KEYS = { + 'allow_mutation', + 'error_msg_templates', + 'fields', + 'getter_dict', + 'smart_union', + 'underscore_attrs_are_private', + 'json_loads', + 'json_dumps', + 'copy_on_model_validation', + 'post_init_call', +} +V2_RENAMED_KEYS = { + 'allow_population_by_field_name': 'validate_by_name', + 'anystr_lower': 'str_to_lower', + 'anystr_strip_whitespace': 'str_strip_whitespace', + 'anystr_upper': 'str_to_upper', + 'keep_untouched': 'ignored_types', + 'max_anystr_length': 'str_max_length', + 'min_anystr_length': 'str_min_length', + 'orm_mode': 'from_attributes', + 'schema_extra': 'json_schema_extra', + 'validate_all': 'validate_default', +} + + +def check_deprecated(config_dict: ConfigDict) -> None: + """Check for deprecated config keys and warn the user. + + Args: + config_dict: The input config. + """ + deprecated_removed_keys = V2_REMOVED_KEYS & config_dict.keys() + deprecated_renamed_keys = V2_RENAMED_KEYS.keys() & config_dict.keys() + if deprecated_removed_keys or deprecated_renamed_keys: + renamings = {k: V2_RENAMED_KEYS[k] for k in sorted(deprecated_renamed_keys)} + renamed_bullets = [f'* {k!r} has been renamed to {v!r}' for k, v in renamings.items()] + removed_bullets = [f'* {k!r} has been removed' for k in sorted(deprecated_removed_keys)] + message = '\n'.join(['Valid config keys have changed in V2:'] + renamed_bullets + removed_bullets) + warnings.warn(message, UserWarning) diff --git a/source/target-vendors/cp312/pydantic/_internal/_core_metadata.py b/source/target-vendors/cp312/pydantic/_internal/_core_metadata.py new file mode 100644 index 0000000..9f2510c --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_core_metadata.py @@ -0,0 +1,97 @@ +from __future__ import annotations as _annotations + +from typing import TYPE_CHECKING, Any, TypedDict, cast +from warnings import warn + +if TYPE_CHECKING: + from ..config import JsonDict, JsonSchemaExtraCallable + from ._schema_generation_shared import ( + GetJsonSchemaFunction, + ) + + +class CoreMetadata(TypedDict, total=False): + """A `TypedDict` for holding the metadata dict of the schema. + + Attributes: + pydantic_js_functions: List of JSON schema functions that resolve refs during application. + pydantic_js_annotation_functions: List of JSON schema functions that don't resolve refs during application. + pydantic_js_prefer_positional_arguments: Whether JSON schema generator will + prefer positional over keyword arguments for an 'arguments' schema. + custom validation function. Only applies to before, plain, and wrap validators. + pydantic_js_updates: key / value pair updates to apply to the JSON schema for a type. + pydantic_js_extra: WIP, either key/value pair updates to apply to the JSON schema, or a custom callable. + pydantic_internal_union_tag_key: Used internally by the `Tag` metadata to specify the tag used for a discriminated union. + pydantic_internal_union_discriminator: Used internally to specify the discriminator value for a discriminated union + when the discriminator was applied to a `'definition-ref'` schema, and that reference was missing at the time + of the annotation application. + + TODO: Perhaps we should move this structure to pydantic-core. At the moment, though, + it's easier to iterate on if we leave it in pydantic until we feel there is a semi-stable API. + + TODO: It's unfortunate how functionally oriented JSON schema generation is, especially that which occurs during + the core schema generation process. It's inevitable that we need to store some json schema related information + on core schemas, given that we generate JSON schemas directly from core schemas. That being said, debugging related + issues is quite difficult when JSON schema information is disguised via dynamically defined functions. + """ + + pydantic_js_functions: list[GetJsonSchemaFunction] + pydantic_js_annotation_functions: list[GetJsonSchemaFunction] + pydantic_js_prefer_positional_arguments: bool + pydantic_js_updates: JsonDict + pydantic_js_extra: JsonDict | JsonSchemaExtraCallable + pydantic_internal_union_tag_key: str + pydantic_internal_union_discriminator: str + + +def update_core_metadata( + core_metadata: Any, + /, + *, + pydantic_js_functions: list[GetJsonSchemaFunction] | None = None, + pydantic_js_annotation_functions: list[GetJsonSchemaFunction] | None = None, + pydantic_js_updates: JsonDict | None = None, + pydantic_js_extra: JsonDict | JsonSchemaExtraCallable | None = None, +) -> None: + from ..json_schema import PydanticJsonSchemaWarning + + """Update CoreMetadata instance in place. When we make modifications in this function, they + take effect on the `core_metadata` reference passed in as the first (and only) positional argument. + + First, cast to `CoreMetadata`, then finish with a cast to `dict[str, Any]` for core schema compatibility. + We do this here, instead of before / after each call to this function so that this typing hack + can be easily removed if/when we move `CoreMetadata` to `pydantic-core`. + + For parameter descriptions, see `CoreMetadata` above. + """ + core_metadata = cast(CoreMetadata, core_metadata) + + if pydantic_js_functions: + core_metadata.setdefault('pydantic_js_functions', []).extend(pydantic_js_functions) + + if pydantic_js_annotation_functions: + core_metadata.setdefault('pydantic_js_annotation_functions', []).extend(pydantic_js_annotation_functions) + + if pydantic_js_updates: + if (existing_updates := core_metadata.get('pydantic_js_updates')) is not None: + core_metadata['pydantic_js_updates'] = {**existing_updates, **pydantic_js_updates} + else: + core_metadata['pydantic_js_updates'] = pydantic_js_updates + + if pydantic_js_extra is not None: + existing_pydantic_js_extra = core_metadata.get('pydantic_js_extra') + if existing_pydantic_js_extra is None: + core_metadata['pydantic_js_extra'] = pydantic_js_extra + if isinstance(existing_pydantic_js_extra, dict): + if isinstance(pydantic_js_extra, dict): + core_metadata['pydantic_js_extra'] = {**existing_pydantic_js_extra, **pydantic_js_extra} + if callable(pydantic_js_extra): + warn( + 'Composing `dict` and `callable` type `json_schema_extra` is not supported.' + 'The `callable` type is being ignored.' + "If you'd like support for this behavior, please open an issue on pydantic.", + PydanticJsonSchemaWarning, + ) + if callable(existing_pydantic_js_extra): + # if ever there's a case of a callable, we'll just keep the last json schema extra spec + core_metadata['pydantic_js_extra'] = pydantic_js_extra diff --git a/source/target-vendors/cp312/pydantic/_internal/_core_utils.py b/source/target-vendors/cp312/pydantic/_internal/_core_utils.py new file mode 100644 index 0000000..caa51e8 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_core_utils.py @@ -0,0 +1,174 @@ +from __future__ import annotations + +import inspect +from collections.abc import Mapping, Sequence +from typing import TYPE_CHECKING, Any, Union + +from pydantic_core import CoreSchema, core_schema +from typing_extensions import TypeGuard, get_args, get_origin +from typing_inspection import typing_objects + +from . import _repr +from ._typing_extra import is_generic_alias + +if TYPE_CHECKING: + from rich.console import Console + +AnyFunctionSchema = Union[ + core_schema.AfterValidatorFunctionSchema, + core_schema.BeforeValidatorFunctionSchema, + core_schema.WrapValidatorFunctionSchema, + core_schema.PlainValidatorFunctionSchema, +] + + +FunctionSchemaWithInnerSchema = Union[ + core_schema.AfterValidatorFunctionSchema, + core_schema.BeforeValidatorFunctionSchema, + core_schema.WrapValidatorFunctionSchema, +] + +CoreSchemaField = Union[ + core_schema.ModelField, core_schema.DataclassField, core_schema.TypedDictField, core_schema.ComputedField +] +CoreSchemaOrField = Union[core_schema.CoreSchema, CoreSchemaField] + +_CORE_SCHEMA_FIELD_TYPES = {'typed-dict-field', 'dataclass-field', 'model-field', 'computed-field'} +_FUNCTION_WITH_INNER_SCHEMA_TYPES = {'function-before', 'function-after', 'function-wrap'} +_LIST_LIKE_SCHEMA_WITH_ITEMS_TYPES = {'list', 'set', 'frozenset'} + + +def is_core_schema( + schema: CoreSchemaOrField, +) -> TypeGuard[CoreSchema]: + return schema['type'] not in _CORE_SCHEMA_FIELD_TYPES + + +def is_core_schema_field( + schema: CoreSchemaOrField, +) -> TypeGuard[CoreSchemaField]: + return schema['type'] in _CORE_SCHEMA_FIELD_TYPES + + +def is_function_with_inner_schema( + schema: CoreSchemaOrField, +) -> TypeGuard[FunctionSchemaWithInnerSchema]: + return schema['type'] in _FUNCTION_WITH_INNER_SCHEMA_TYPES + + +def is_list_like_schema_with_items_schema( + schema: CoreSchema, +) -> TypeGuard[core_schema.ListSchema | core_schema.SetSchema | core_schema.FrozenSetSchema]: + return schema['type'] in _LIST_LIKE_SCHEMA_WITH_ITEMS_TYPES + + +def get_type_ref(type_: Any, args_override: tuple[type[Any], ...] | None = None) -> str: + """Produces the ref to be used for this type by pydantic_core's core schemas. + + This `args_override` argument was added for the purpose of creating valid recursive references + when creating generic models without needing to create a concrete class. + """ + origin = get_origin(type_) or type_ + + args = get_args(type_) if is_generic_alias(type_) else (args_override or ()) + generic_metadata = getattr(type_, '__pydantic_generic_metadata__', None) + if generic_metadata: + origin = generic_metadata['origin'] or origin + args = generic_metadata['args'] or args + + module_name = getattr(origin, '__module__', '') + if typing_objects.is_typealiastype(origin): + type_ref = f'{module_name}.{origin.__name__}:{id(origin)}' + else: + try: + qualname = getattr(origin, '__qualname__', f'') + except Exception: + qualname = getattr(origin, '__qualname__', '') + type_ref = f'{module_name}.{qualname}:{id(origin)}' + + arg_refs: list[str] = [] + for arg in args: + if isinstance(arg, str): + # Handle string literals as a special case; we may be able to remove this special handling if we + # wrap them in a ForwardRef at some point. + arg_ref = f'{arg}:str-{id(arg)}' + else: + arg_ref = f'{_repr.display_as_type(arg)}:{id(arg)}' + arg_refs.append(arg_ref) + if arg_refs: + type_ref = f'{type_ref}[{",".join(arg_refs)}]' + return type_ref + + +def get_ref(s: core_schema.CoreSchema) -> None | str: + """Get the ref from the schema if it has one. + This exists just for type checking to work correctly. + """ + return s.get('ref', None) + + +def _clean_schema_for_pretty_print(obj: Any, strip_metadata: bool = True) -> Any: # pragma: no cover + """A utility function to remove irrelevant information from a core schema.""" + if isinstance(obj, Mapping): + new_dct = {} + for k, v in obj.items(): + if k == 'metadata' and strip_metadata: + new_metadata = {} + + for meta_k, meta_v in v.items(): + if meta_k in ('pydantic_js_functions', 'pydantic_js_annotation_functions'): + new_metadata['js_metadata'] = '' + else: + new_metadata[meta_k] = _clean_schema_for_pretty_print(meta_v, strip_metadata=strip_metadata) + + if list(new_metadata.keys()) == ['js_metadata']: + new_metadata = {''} + + new_dct[k] = new_metadata + # Remove some defaults: + elif k in ('custom_init', 'root_model') and not v: + continue + else: + new_dct[k] = _clean_schema_for_pretty_print(v, strip_metadata=strip_metadata) + + return new_dct + elif isinstance(obj, Sequence) and not isinstance(obj, str): + return [_clean_schema_for_pretty_print(v, strip_metadata=strip_metadata) for v in obj] + else: + return obj + + +def pretty_print_core_schema( + val: Any, + *, + console: Console | None = None, + max_depth: int | None = None, + strip_metadata: bool = True, +) -> None: # pragma: no cover + """Pretty-print a core schema using the `rich` library. + + Args: + val: The core schema to print, or a Pydantic model/dataclass/type adapter + (in which case the cached core schema is fetched and printed). + console: A rich console to use when printing. Defaults to the global rich console instance. + max_depth: The number of nesting levels which may be printed. + strip_metadata: Whether to strip metadata in the output. If `True` any known core metadata + attributes will be stripped (but custom attributes are kept). Defaults to `True`. + """ + # lazy import: + from rich.pretty import pprint + + # circ. imports: + from pydantic import BaseModel, TypeAdapter + from pydantic.dataclasses import is_pydantic_dataclass + + if (inspect.isclass(val) and issubclass(val, BaseModel)) or is_pydantic_dataclass(val): + val = val.__pydantic_core_schema__ + if isinstance(val, TypeAdapter): + val = val.core_schema + cleaned_schema = _clean_schema_for_pretty_print(val, strip_metadata=strip_metadata) + + pprint(cleaned_schema, console=console, max_depth=max_depth) + + +pps = pretty_print_core_schema diff --git a/source/target-vendors/cp312/pydantic/_internal/_dataclasses.py b/source/target-vendors/cp312/pydantic/_internal/_dataclasses.py new file mode 100644 index 0000000..f00c8b7 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_dataclasses.py @@ -0,0 +1,315 @@ +"""Private logic for creating pydantic dataclasses.""" + +from __future__ import annotations as _annotations + +import copy +import dataclasses +import sys +import warnings +from collections.abc import Generator +from contextlib import contextmanager +from functools import partial +from typing import TYPE_CHECKING, Any, ClassVar, Protocol, cast + +from pydantic_core import ( + ArgsKwargs, + SchemaSerializer, + SchemaValidator, + core_schema, +) +from typing_extensions import TypeAlias, TypeIs + +from ..errors import PydanticUndefinedAnnotation +from ..fields import FieldInfo +from ..plugin._schema_validator import PluggableSchemaValidator, create_schema_validator +from ..warnings import PydanticDeprecatedSince20 +from . import _config, _decorators +from ._fields import collect_dataclass_fields +from ._generate_schema import GenerateSchema, InvalidSchemaError +from ._generics import get_standard_typevars_map +from ._mock_val_ser import set_dataclass_mocks +from ._namespace_utils import NsResolver +from ._signature import generate_pydantic_signature +from ._utils import LazyClassAttribute + +if TYPE_CHECKING: + from _typeshed import DataclassInstance as StandardDataclass + + from ..config import ConfigDict + + class PydanticDataclass(StandardDataclass, Protocol): + """A protocol containing attributes only available once a class has been decorated as a Pydantic dataclass. + + Attributes: + __pydantic_config__: Pydantic-specific configuration settings for the dataclass. + __pydantic_complete__: Whether dataclass building is completed, or if there are still undefined fields. + __pydantic_core_schema__: The pydantic-core schema used to build the SchemaValidator and SchemaSerializer. + __pydantic_decorators__: Metadata containing the decorators defined on the dataclass. + __pydantic_fields__: Metadata about the fields defined on the dataclass. + __pydantic_serializer__: The pydantic-core SchemaSerializer used to dump instances of the dataclass. + __pydantic_validator__: The pydantic-core SchemaValidator used to validate instances of the dataclass. + """ + + __pydantic_config__: ClassVar[ConfigDict] + __pydantic_complete__: ClassVar[bool] + __pydantic_core_schema__: ClassVar[core_schema.CoreSchema] + __pydantic_decorators__: ClassVar[_decorators.DecoratorInfos] + __pydantic_fields__: ClassVar[dict[str, FieldInfo]] + __pydantic_serializer__: ClassVar[SchemaSerializer] + __pydantic_validator__: ClassVar[SchemaValidator | PluggableSchemaValidator] + + @classmethod + def __pydantic_fields_complete__(cls) -> bool: ... + + +def set_dataclass_fields( + cls: type[StandardDataclass], + config_wrapper: _config.ConfigWrapper, + ns_resolver: NsResolver | None = None, +) -> None: + """Collect and set `cls.__pydantic_fields__`. + + Args: + cls: The class. + config_wrapper: The config wrapper instance. + ns_resolver: Namespace resolver to use when getting dataclass annotations. + """ + typevars_map = get_standard_typevars_map(cls) + fields = collect_dataclass_fields( + cls, ns_resolver=ns_resolver, typevars_map=typevars_map, config_wrapper=config_wrapper + ) + + cls.__pydantic_fields__ = fields # type: ignore + + +def complete_dataclass( + cls: type[Any], + config_wrapper: _config.ConfigWrapper, + *, + raise_errors: bool = True, + ns_resolver: NsResolver | None = None, + _force_build: bool = False, +) -> bool: + """Finish building a pydantic dataclass. + + This logic is called on a class which has already been wrapped in `dataclasses.dataclass()`. + + This is somewhat analogous to `pydantic._internal._model_construction.complete_model_class`. + + Args: + cls: The class. + config_wrapper: The config wrapper instance. + raise_errors: Whether to raise errors, defaults to `True`. + ns_resolver: The namespace resolver instance to use when collecting dataclass fields + and during schema building. + _force_build: Whether to force building the dataclass, no matter if + [`defer_build`][pydantic.config.ConfigDict.defer_build] is set. + + Returns: + `True` if building a pydantic dataclass is successfully completed, `False` otherwise. + + Raises: + PydanticUndefinedAnnotation: If `raise_error` is `True` and there is an undefined annotations. + """ + original_init = cls.__init__ + + # dataclass.__init__ must be defined here so its `__qualname__` can be changed since functions can't be copied, + # and so that the mock validator is used if building was deferred: + def __init__(__dataclass_self__: PydanticDataclass, *args: Any, **kwargs: Any) -> None: + __tracebackhide__ = True + s = __dataclass_self__ + s.__pydantic_validator__.validate_python(ArgsKwargs(args, kwargs), self_instance=s) + + __init__.__qualname__ = f'{cls.__qualname__}.__init__' + + cls.__init__ = __init__ # type: ignore + cls.__pydantic_config__ = config_wrapper.config_dict # type: ignore + + set_dataclass_fields(cls, config_wrapper=config_wrapper, ns_resolver=ns_resolver) + + if not _force_build and config_wrapper.defer_build: + set_dataclass_mocks(cls) + return False + + if hasattr(cls, '__post_init_post_parse__'): + warnings.warn( + 'Support for `__post_init_post_parse__` has been dropped, the method will not be called', + PydanticDeprecatedSince20, + ) + + typevars_map = get_standard_typevars_map(cls) + gen_schema = GenerateSchema( + config_wrapper, + ns_resolver=ns_resolver, + typevars_map=typevars_map, + ) + + # set __signature__ attr only for the class, but not for its instances + # (because instances can define `__call__`, and `inspect.signature` shouldn't + # use the `__signature__` attribute and instead generate from `__call__`). + cls.__signature__ = LazyClassAttribute( + '__signature__', + partial( + generate_pydantic_signature, + # It's important that we reference the `original_init` here, + # as it is the one synthesized by the stdlib `dataclass` module: + init=original_init, + fields=cls.__pydantic_fields__, # type: ignore + validate_by_name=config_wrapper.validate_by_name, + extra=config_wrapper.extra, + is_dataclass=True, + ), + ) + + try: + schema = gen_schema.generate_schema(cls) + except PydanticUndefinedAnnotation as e: + if raise_errors: + raise + set_dataclass_mocks(cls, f'`{e.name}`') + return False + + core_config = config_wrapper.core_config(title=cls.__name__) + + try: + schema = gen_schema.clean_schema(schema) + except InvalidSchemaError: + set_dataclass_mocks(cls) + return False + + # We are about to set all the remaining required properties expected for this cast; + # __pydantic_decorators__ and __pydantic_fields__ should already be set + cls = cast('type[PydanticDataclass]', cls) + + cls.__pydantic_core_schema__ = schema + cls.__pydantic_validator__ = create_schema_validator( + schema, cls, cls.__module__, cls.__qualname__, 'dataclass', core_config, config_wrapper.plugin_settings + ) + cls.__pydantic_serializer__ = SchemaSerializer(schema, core_config) + cls.__pydantic_complete__ = True + return True + + +def is_stdlib_dataclass(cls: type[Any], /) -> TypeIs[type[StandardDataclass]]: + """Returns `True` if the class is a stdlib dataclass and *not* a Pydantic dataclass. + + Unlike the stdlib `dataclasses.is_dataclass()` function, this does *not* include subclasses + of a dataclass that are themselves not dataclasses. + + Args: + cls: The class. + + Returns: + `True` if the class is a stdlib dataclass, `False` otherwise. + """ + return '__dataclass_fields__' in cls.__dict__ and not hasattr(cls, '__pydantic_validator__') + + +def as_dataclass_field(pydantic_field: FieldInfo) -> dataclasses.Field[Any]: + field_args: dict[str, Any] = {'default': pydantic_field} + + # Needed because if `doc` is set, the dataclass slots will be a dict (field name -> doc) instead of a tuple: + if sys.version_info >= (3, 14) and pydantic_field.description is not None: + field_args['doc'] = pydantic_field.description + + # Needed as the stdlib dataclass module processes kw_only in a specific way during class construction: + if sys.version_info >= (3, 10) and pydantic_field.kw_only is not None: + field_args['kw_only'] = pydantic_field.kw_only + + # Needed as the stdlib dataclass modules generates `__repr__()` during class construction: + if pydantic_field.repr is not True: + field_args['repr'] = pydantic_field.repr + + return dataclasses.field(**field_args) + + +DcFields: TypeAlias = dict[str, dataclasses.Field[Any]] + + +@contextmanager +def patch_base_fields(cls: type[Any]) -> Generator[None]: + """Temporarily patch the stdlib dataclasses bases of `cls` if the Pydantic `Field()` function is used. + + When creating a Pydantic dataclass, it is possible to inherit from stdlib dataclasses, where + the Pydantic `Field()` function is used. To create this Pydantic dataclass, we first apply + the stdlib `@dataclass` decorator on it. During the construction of the stdlib dataclass, + the `kw_only` and `repr` field arguments need to be understood by the stdlib *during* the + dataclass construction. To do so, we temporarily patch the fields dictionary of the affected + bases. + + For instance, with the following example: + + ```python {test="skip" lint="skip"} + import dataclasses as stdlib_dc + + import pydantic + import pydantic.dataclasses as pydantic_dc + + @stdlib_dc.dataclass + class A: + a: int = pydantic.Field(repr=False) + + # Notice that the `repr` attribute of the dataclass field is `True`: + A.__dataclass_fields__['a'] + #> dataclass.Field(default=FieldInfo(repr=False), repr=True, ...) + + @pydantic_dc.dataclass + class B(A): + b: int = pydantic.Field(repr=False) + ``` + + When passing `B` to the stdlib `@dataclass` decorator, it will look for fields in the parent classes + and reuse them directly. When this context manager is active, `A` will be temporarily patched to be + equivalent to: + + ```python {test="skip" lint="skip"} + @stdlib_dc.dataclass + class A: + a: int = stdlib_dc.field(default=Field(repr=False), repr=False) + ``` + + !!! note + This is only applied to the bases of `cls`, and not `cls` itself. The reason is that the Pydantic + dataclass decorator "owns" `cls` (in the previous example, `B`). As such, we instead modify the fields + directly (in the previous example, we simply do `setattr(B, 'b', as_dataclass_field(pydantic_field))`). + + !!! note + This approach is far from ideal, and can probably be the source of unwanted side effects/race conditions. + The previous implemented approach was mutating the `__annotations__` dict of `cls`, which is no longer a + safe operation in Python 3.14+, and resulted in unexpected behavior with field ordering anyway. + """ + # A list of two-tuples, the first element being a reference to the + # dataclass fields dictionary, the second element being a mapping between + # the field names that were modified, and their original `Field`: + original_fields_list: list[tuple[DcFields, DcFields]] = [] + + for base in cls.__mro__[1:]: + dc_fields: dict[str, dataclasses.Field[Any]] = base.__dict__.get('__dataclass_fields__', {}) + dc_fields_with_pydantic_field_defaults = { + field_name: field + for field_name, field in dc_fields.items() + if isinstance(field.default, FieldInfo) + # Only do the patching if one of the affected attributes is set: + and (field.default.description is not None or field.default.kw_only or field.default.repr is not True) + } + if dc_fields_with_pydantic_field_defaults: + original_fields_list.append((dc_fields, dc_fields_with_pydantic_field_defaults)) + for field_name, field in dc_fields_with_pydantic_field_defaults.items(): + default = cast(FieldInfo, field.default) + # `dataclasses.Field` isn't documented as working with `copy.copy()`. + # It is a class with `__slots__`, so should work (and we hope for the best): + new_dc_field = copy.copy(field) + # For base fields, no need to set `doc` from `FieldInfo.description`, this is only relevant + # for the class under construction and handled in `as_dataclass_field()`. + if sys.version_info >= (3, 10) and default.kw_only: + new_dc_field.kw_only = True + if default.repr is not True: + new_dc_field.repr = default.repr + dc_fields[field_name] = new_dc_field + + try: + yield + finally: + for fields, original_fields in original_fields_list: + for field_name, original_field in original_fields.items(): + fields[field_name] = original_field diff --git a/source/target-vendors/cp312/pydantic/_internal/_decorators.py b/source/target-vendors/cp312/pydantic/_internal/_decorators.py new file mode 100644 index 0000000..e24ad2c --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_decorators.py @@ -0,0 +1,873 @@ +"""Logic related to validators applied to models etc. via the `@field_validator` and `@model_validator` decorators.""" + +from __future__ import annotations as _annotations + +import types +from collections import deque +from collections.abc import Iterable +from copy import copy +from dataclasses import dataclass, field +from functools import cached_property, partial, partialmethod +from inspect import Parameter, Signature, isdatadescriptor, ismethoddescriptor +from itertools import islice +from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generic, Literal, TypeVar, Union + +from pydantic_core import PydanticUndefined, PydanticUndefinedType, core_schema +from typing_extensions import Self, TypeAlias, is_typeddict + +from ..errors import PydanticUserError +from ._core_utils import get_type_ref +from ._internal_dataclass import slots_true +from ._namespace_utils import GlobalsNamespace, MappingNamespace +from ._typing_extra import get_function_type_hints, signature_no_eval +from ._utils import can_be_positional + +if TYPE_CHECKING: + from ..fields import ComputedFieldInfo + from ..functional_validators import FieldValidatorModes + from ._config import ConfigWrapper + + +@dataclass(**slots_true) +class ValidatorDecoratorInfo: + """A container for data from `@validator` so that we can access it + while building the pydantic-core schema. + + Attributes: + decorator_repr: A class variable representing the decorator string, '@validator'. + fields: A tuple of field names the validator should be called on. + mode: The proposed validator mode. + each_item: For complex objects (sets, lists etc.) whether to validate individual + elements rather than the whole object. + always: Whether this method and other validators should be called even if the value is missing. + check_fields: Whether to check that the fields actually exist on the model. + """ + + decorator_repr: ClassVar[str] = '@validator' + + fields: tuple[str, ...] + mode: Literal['before', 'after'] + each_item: bool + always: bool + check_fields: bool | None + + +@dataclass(**slots_true) +class FieldValidatorDecoratorInfo: + """A container for data from `@field_validator` so that we can access it + while building the pydantic-core schema. + + Attributes: + decorator_repr: A class variable representing the decorator string, '@field_validator'. + fields: A tuple of field names the validator should be called on. + mode: The proposed validator mode. + check_fields: Whether to check that the fields actually exist on the model. + json_schema_input_type: The input type of the function. This is only used to generate + the appropriate JSON Schema (in validation mode) and can only specified + when `mode` is either `'before'`, `'plain'` or `'wrap'`. + """ + + decorator_repr: ClassVar[str] = '@field_validator' + + fields: tuple[str, ...] + mode: FieldValidatorModes + check_fields: bool | None + json_schema_input_type: Any + + +@dataclass(**slots_true) +class RootValidatorDecoratorInfo: + """A container for data from `@root_validator` so that we can access it + while building the pydantic-core schema. + + Attributes: + decorator_repr: A class variable representing the decorator string, '@root_validator'. + mode: The proposed validator mode. + """ + + decorator_repr: ClassVar[str] = '@root_validator' + mode: Literal['before', 'after'] + + +@dataclass(**slots_true) +class FieldSerializerDecoratorInfo: + """A container for data from `@field_serializer` so that we can access it + while building the pydantic-core schema. + + Attributes: + decorator_repr: A class variable representing the decorator string, '@field_serializer'. + fields: A tuple of field names the serializer should be called on. + mode: The proposed serializer mode. + return_type: The type of the serializer's return value. + when_used: The serialization condition. Accepts a string with values `'always'`, `'unless-none'`, `'json'`, + and `'json-unless-none'`. + check_fields: Whether to check that the fields actually exist on the model. + """ + + decorator_repr: ClassVar[str] = '@field_serializer' + fields: tuple[str, ...] + mode: Literal['plain', 'wrap'] + return_type: Any + when_used: core_schema.WhenUsed + check_fields: bool | None + + +@dataclass(**slots_true) +class ModelSerializerDecoratorInfo: + """A container for data from `@model_serializer` so that we can access it + while building the pydantic-core schema. + + Attributes: + decorator_repr: A class variable representing the decorator string, '@model_serializer'. + mode: The proposed serializer mode. + return_type: The type of the serializer's return value. + when_used: The serialization condition. Accepts a string with values `'always'`, `'unless-none'`, `'json'`, + and `'json-unless-none'`. + """ + + decorator_repr: ClassVar[str] = '@model_serializer' + mode: Literal['plain', 'wrap'] + return_type: Any + when_used: core_schema.WhenUsed + + +@dataclass(**slots_true) +class ModelValidatorDecoratorInfo: + """A container for data from `@model_validator` so that we can access it + while building the pydantic-core schema. + + Attributes: + decorator_repr: A class variable representing the decorator string, '@model_validator'. + mode: The proposed serializer mode. + """ + + decorator_repr: ClassVar[str] = '@model_validator' + mode: Literal['wrap', 'before', 'after'] + + +DecoratorInfo: TypeAlias = """Union[ + ValidatorDecoratorInfo, + FieldValidatorDecoratorInfo, + RootValidatorDecoratorInfo, + FieldSerializerDecoratorInfo, + ModelSerializerDecoratorInfo, + ModelValidatorDecoratorInfo, + ComputedFieldInfo, +]""" + +ReturnType = TypeVar('ReturnType') +DecoratedType: TypeAlias = ( + 'Union[classmethod[Any, Any, ReturnType], staticmethod[Any, ReturnType], Callable[..., ReturnType], property]' +) + + +@dataclass # can't use slots here since we set attributes on `__post_init__` +class PydanticDescriptorProxy(Generic[ReturnType]): + """Wrap a classmethod, staticmethod, property or unbound function + and act as a descriptor that allows us to detect decorated items + from the class' attributes. + + This class' __get__ returns the wrapped item's __get__ result, + which makes it transparent for classmethods and staticmethods. + + Attributes: + wrapped: The decorator that has to be wrapped. + decorator_info: The decorator info. + shim: A wrapper function to wrap V1 style function. + """ + + wrapped: DecoratedType[ReturnType] + decorator_info: DecoratorInfo + shim: Callable[[Callable[..., Any]], Callable[..., Any]] | None = None + + def __post_init__(self): + for attr in 'setter', 'deleter': + if hasattr(self.wrapped, attr): + f = partial(self._call_wrapped_attr, name=attr) + setattr(self, attr, f) + + def _call_wrapped_attr(self, func: Callable[[Any], None], *, name: str) -> PydanticDescriptorProxy[ReturnType]: + self.wrapped = getattr(self.wrapped, name)(func) + if isinstance(self.wrapped, property): + # update ComputedFieldInfo.wrapped_property + from ..fields import ComputedFieldInfo + + if isinstance(self.decorator_info, ComputedFieldInfo): + self.decorator_info.wrapped_property = self.wrapped + return self + + def __get__(self, obj: object | None, obj_type: type[object] | None = None) -> PydanticDescriptorProxy[ReturnType]: + try: + return self.wrapped.__get__(obj, obj_type) # pyright: ignore[reportReturnType] + except AttributeError: + # not a descriptor, e.g. a partial object + return self.wrapped # type: ignore[return-value] + + def __set_name__(self, instance: Any, name: str) -> None: + if hasattr(self.wrapped, '__set_name__'): + self.wrapped.__set_name__(instance, name) # pyright: ignore[reportFunctionMemberAccess] + + def __getattr__(self, name: str, /) -> Any: + """Forward checks for __isabstractmethod__ and such.""" + return getattr(self.wrapped, name) + + +DecoratorInfoType = TypeVar('DecoratorInfoType', bound=DecoratorInfo) + + +@dataclass(**slots_true) +class Decorator(Generic[DecoratorInfoType]): + """A generic container class to join together the decorator metadata + (metadata from decorator itself, which we have when the + decorator is called but not when we are building the core-schema) + and the bound function (which we have after the class itself is created). + + Attributes: + cls_ref: The class ref. + cls_var_name: The decorated function name. + func: The decorated function. + shim: A wrapper function to wrap V1 style function. + info: The decorator info. + """ + + cls_ref: str + cls_var_name: str + func: Callable[..., Any] + shim: Callable[[Any], Any] | None + info: DecoratorInfoType + + @staticmethod + def build( + cls_: Any, + *, + cls_var_name: str, + shim: Callable[[Any], Any] | None, + info: DecoratorInfoType, + ) -> Decorator[DecoratorInfoType]: + """Build a new decorator. + + Args: + cls_: The class. + cls_var_name: The decorated function name. + shim: A wrapper function to wrap V1 style function. + info: The decorator info. + + Returns: + The new decorator instance. + """ + func = get_attribute_from_bases(cls_, cls_var_name) + if shim is not None: + func = shim(func) + func = unwrap_wrapped_function(func, unwrap_partial=False) + if not callable(func): + # TODO most likely this branch can be removed when we drop support for Python 3.12: + # This branch will get hit for classmethod properties + attribute = get_attribute_from_base_dicts(cls_, cls_var_name) # prevents the binding call to `__get__` + if isinstance(attribute, PydanticDescriptorProxy): + func = unwrap_wrapped_function(attribute.wrapped) + return Decorator( + cls_ref=get_type_ref(cls_), + cls_var_name=cls_var_name, + func=func, + shim=shim, + info=info, + ) + + def bind_to_cls(self, cls: Any) -> Decorator[DecoratorInfoType]: + """Bind the decorator to a class. + + Args: + cls: the class. + + Returns: + The new decorator instance. + """ + return self.build( + cls, + cls_var_name=self.cls_var_name, + shim=self.shim, + info=copy(self.info), + ) + + +def get_bases(tp: type[Any]) -> tuple[type[Any], ...]: + """Get the base classes of a class or typeddict. + + Args: + tp: The type or class to get the bases. + + Returns: + The base classes. + """ + if is_typeddict(tp): + return tp.__orig_bases__ # type: ignore + try: + return tp.__bases__ + except AttributeError: + return () + + +def mro(tp: type[Any]) -> tuple[type[Any], ...]: + """Calculate the Method Resolution Order of bases using the C3 algorithm. + + See https://www.python.org/download/releases/2.3/mro/ + """ + # try to use the existing mro, for performance mainly + # but also because it helps verify the implementation below + if not is_typeddict(tp): + try: + return tp.__mro__ + except AttributeError: + # GenericAlias and some other cases + pass + + bases = get_bases(tp) + return (tp,) + mro_for_bases(bases) + + +def mro_for_bases(bases: tuple[type[Any], ...]) -> tuple[type[Any], ...]: + def merge_seqs(seqs: list[deque[type[Any]]]) -> Iterable[type[Any]]: + while True: + non_empty = [seq for seq in seqs if seq] + if not non_empty: + # Nothing left to process, we're done. + return + candidate: type[Any] | None = None + for seq in non_empty: # Find merge candidates among seq heads. + candidate = seq[0] + not_head = [s for s in non_empty if candidate in islice(s, 1, None)] + if not_head: + # Reject the candidate. + candidate = None + else: + break + if not candidate: + raise TypeError('Inconsistent hierarchy, no C3 MRO is possible') + yield candidate + for seq in non_empty: + # Remove candidate. + if seq[0] == candidate: + seq.popleft() + + seqs = [deque(mro(base)) for base in bases] + [deque(bases)] + return tuple(merge_seqs(seqs)) + + +_sentinel = object() + + +def get_attribute_from_bases(tp: type[Any] | tuple[type[Any], ...], name: str) -> Any: + """Get the attribute from the next class in the MRO that has it, + aiming to simulate calling the method on the actual class. + + The reason for iterating over the mro instead of just getting + the attribute (which would do that for us) is to support TypedDict, + which lacks a real __mro__, but can have a virtual one constructed + from its bases (as done here). + + Args: + tp: The type or class to search for the attribute. If a tuple, this is treated as a set of base classes. + name: The name of the attribute to retrieve. + + Returns: + Any: The attribute value, if found. + + Raises: + AttributeError: If the attribute is not found in any class in the MRO. + """ + if isinstance(tp, tuple): + for base in mro_for_bases(tp): + attribute = base.__dict__.get(name, _sentinel) + if attribute is not _sentinel: + attribute_get = getattr(attribute, '__get__', None) + if attribute_get is not None: + return attribute_get(None, tp) + return attribute + raise AttributeError(f'{name} not found in {tp}') + else: + try: + return getattr(tp, name) + except AttributeError: + return get_attribute_from_bases(mro(tp), name) + + +def get_attribute_from_base_dicts(tp: type[Any], name: str) -> Any: + """Get an attribute out of the `__dict__` following the MRO. + This prevents the call to `__get__` on the descriptor, and allows + us to get the original function for classmethod properties. + + Args: + tp: The type or class to search for the attribute. + name: The name of the attribute to retrieve. + + Returns: + Any: The attribute value, if found. + + Raises: + KeyError: If the attribute is not found in any class's `__dict__` in the MRO. + """ + for base in reversed(mro(tp)): + if name in base.__dict__: + return base.__dict__[name] + return tp.__dict__[name] # raise the error + + +@dataclass(**slots_true) +class DecoratorInfos: + """Mapping of name in the class namespace to decorator info. + + note that the name in the class namespace is the function or attribute name + not the field name! + """ + + validators: dict[str, Decorator[ValidatorDecoratorInfo]] = field(default_factory=dict) + field_validators: dict[str, Decorator[FieldValidatorDecoratorInfo]] = field(default_factory=dict) + root_validators: dict[str, Decorator[RootValidatorDecoratorInfo]] = field(default_factory=dict) + field_serializers: dict[str, Decorator[FieldSerializerDecoratorInfo]] = field(default_factory=dict) + model_serializers: dict[str, Decorator[ModelSerializerDecoratorInfo]] = field(default_factory=dict) + model_validators: dict[str, Decorator[ModelValidatorDecoratorInfo]] = field(default_factory=dict) + computed_fields: dict[str, Decorator[ComputedFieldInfo]] = field(default_factory=dict) + + @classmethod + def build( + cls, + typ: type[Any], + # Default to `True` for backwards compatibility: + replace_wrapped_methods: bool = True, + ) -> Self: + """Build a `DecoratorInfos` instance for the given model, dataclass or `TypedDict` type. + + Decorators from parent classes are included, including "bare" classes (e.g. if `typ` + is a Pydantic model, non Pydantic parent model classes are also taken into account). + The collection of the decorators happens by respecting the MRO. + + If one of the bases has an `__pydantic_decorators__` attribute set, it is assumed to be + a `DecoratorInfos` instance and is used as-is. The `__pydantic_decorators__` attribute + is *not* being set on the provided `typ`. + + Args: + typ: The model, dataclass or `TypedDict` type to use when building the `DecoratorInfos` instance. + replace_wrapped_methods: Whether to replace the decorator's wrapped methods on `typ`. + This is useful e.g. for field validators which are initially class methods. This should + only be set to `True` if `typ` is a Pydantic model or dataclass (otherwise this results + in mutations of classes Pydantic doesn't "own"). + """ + # reminder: dicts are ordered and replacement does not alter the order + res = cls() + # Iterate over the bases, without the actual `typ`. + # `1:-1` because we don't need to include `object`/`TypedDict`: + for base in reversed(mro(typ)[1:-1]): + existing: DecoratorInfos | None = base.__dict__.get('__pydantic_decorators__') + if existing is None: + existing, _ = _decorator_infos_for_class(base, collect_to_replace=False) + res.validators.update({k: v.bind_to_cls(typ) for k, v in existing.validators.items()}) + res.field_validators.update({k: v.bind_to_cls(typ) for k, v in existing.field_validators.items()}) + res.root_validators.update({k: v.bind_to_cls(typ) for k, v in existing.root_validators.items()}) + res.field_serializers.update({k: v.bind_to_cls(typ) for k, v in existing.field_serializers.items()}) + res.model_serializers.update({k: v.bind_to_cls(typ) for k, v in existing.model_serializers.items()}) + res.model_validators.update({k: v.bind_to_cls(typ) for k, v in existing.model_validators.items()}) + res.computed_fields.update({k: v.bind_to_cls(typ) for k, v in existing.computed_fields.items()}) + + decorator_infos, to_replace = _decorator_infos_for_class(typ, collect_to_replace=True) + + res.validators.update(decorator_infos.validators) + res.field_validators.update(decorator_infos.field_validators) + res.root_validators.update(decorator_infos.root_validators) + res.field_serializers.update(decorator_infos.field_serializers) + res.model_serializers.update(decorator_infos.model_serializers) + res.model_validators.update(decorator_infos.model_validators) + res.computed_fields.update(decorator_infos.computed_fields) + + if replace_wrapped_methods and to_replace: + for name, value in to_replace: + setattr(typ, name, value) + + res._validate() + return res + + def _validate(self) -> None: + seen: set[str] = set() + for field_ser in self.field_serializers.values(): + for f_name in field_ser.info.fields: + if f_name in seen: + raise PydanticUserError( + f'Multiple field serializer functions were defined for field {f_name!r}, this is not allowed.', + code='multiple-field-serializers', + ) + seen.add(f_name) + + def update_from_config(self, config_wrapper: ConfigWrapper) -> None: + """Update the decorator infos from the configuration of the class they are attached to.""" + for name, computed_field_dec in self.computed_fields.items(): + computed_field_dec.info._update_from_config(config_wrapper, name) + + +def _decorator_infos_for_class( + typ: type[Any], + *, + collect_to_replace: bool, +) -> tuple[DecoratorInfos, list[tuple[str, Any]]]: + """Collect a `DecoratorInfos` for class, without looking into bases.""" + res = DecoratorInfos() + to_replace: list[tuple[str, Any]] = [] + + for var_name, var_value in vars(typ).items(): + if isinstance(var_value, PydanticDescriptorProxy): + info = var_value.decorator_info + if isinstance(info, ValidatorDecoratorInfo): + res.validators[var_name] = Decorator.build(typ, cls_var_name=var_name, shim=var_value.shim, info=info) + elif isinstance(info, FieldValidatorDecoratorInfo): + res.field_validators[var_name] = Decorator.build( + typ, cls_var_name=var_name, shim=var_value.shim, info=info + ) + elif isinstance(info, RootValidatorDecoratorInfo): + res.root_validators[var_name] = Decorator.build( + typ, cls_var_name=var_name, shim=var_value.shim, info=info + ) + elif isinstance(info, FieldSerializerDecoratorInfo): + res.field_serializers[var_name] = Decorator.build( + typ, cls_var_name=var_name, shim=var_value.shim, info=info + ) + elif isinstance(info, ModelValidatorDecoratorInfo): + res.model_validators[var_name] = Decorator.build( + typ, cls_var_name=var_name, shim=var_value.shim, info=info + ) + elif isinstance(info, ModelSerializerDecoratorInfo): + res.model_serializers[var_name] = Decorator.build( + typ, cls_var_name=var_name, shim=var_value.shim, info=info + ) + else: + from ..fields import ComputedFieldInfo + + isinstance(var_value, ComputedFieldInfo) + res.computed_fields[var_name] = Decorator.build(typ, cls_var_name=var_name, shim=None, info=info) + if collect_to_replace: + to_replace.append((var_name, var_value.wrapped)) + + return res, to_replace + + +def inspect_validator( + validator: Callable[..., Any], *, mode: FieldValidatorModes, type: Literal['field', 'model'] +) -> bool: + """Look at a field or model validator function and determine whether it takes an info argument. + + An error is raised if the function has an invalid signature. + + Args: + validator: The validator function to inspect. + mode: The proposed validator mode. + type: The type of validator, either 'field' or 'model'. + + Returns: + Whether the validator takes an info argument. + """ + try: + sig = signature_no_eval(validator) + except (ValueError, TypeError): + # `inspect.signature` might not be able to infer a signature, e.g. with C objects. + # In this case, we assume no info argument is present: + return False + n_positional = count_positional_required_params(sig) + if mode == 'wrap': + if n_positional == 3: + return True + elif n_positional == 2: + return False + else: + assert mode in {'before', 'after', 'plain'}, f"invalid mode: {mode!r}, expected 'before', 'after' or 'plain" + if n_positional == 2: + return True + elif n_positional == 1: + return False + + raise PydanticUserError( + f'Unrecognized {type} validator function signature for {validator} with `mode={mode}`: {sig}', + code='validator-signature', + ) + + +def inspect_field_serializer(serializer: Callable[..., Any], mode: Literal['plain', 'wrap']) -> tuple[bool, bool]: + """Look at a field serializer function and determine if it is a field serializer, + and whether it takes an info argument. + + An error is raised if the function has an invalid signature. + + Args: + serializer: The serializer function to inspect. + mode: The serializer mode, either 'plain' or 'wrap'. + + Returns: + Tuple of (is_field_serializer, info_arg). + """ + try: + sig = signature_no_eval(serializer) + except (ValueError, TypeError): + # `inspect.signature` might not be able to infer a signature, e.g. with C objects. + # In this case, we assume no info argument is present and this is not a method: + return (False, False) + + first = next(iter(sig.parameters.values()), None) + is_field_serializer = first is not None and first.name == 'self' + + n_positional = count_positional_required_params(sig) + if is_field_serializer: + # -1 to correct for self parameter + info_arg = _serializer_info_arg(mode, n_positional - 1) + else: + info_arg = _serializer_info_arg(mode, n_positional) + + if info_arg is None: + raise PydanticUserError( + f'Unrecognized field_serializer function signature for {serializer} with `mode={mode}`:{sig}', + code='field-serializer-signature', + ) + + return is_field_serializer, info_arg + + +def inspect_annotated_serializer(serializer: Callable[..., Any], mode: Literal['plain', 'wrap']) -> bool: + """Look at a serializer function used via `Annotated` and determine whether it takes an info argument. + + An error is raised if the function has an invalid signature. + + Args: + serializer: The serializer function to check. + mode: The serializer mode, either 'plain' or 'wrap'. + + Returns: + info_arg + """ + try: + sig = signature_no_eval(serializer) + except (ValueError, TypeError): + # `inspect.signature` might not be able to infer a signature, e.g. with C objects. + # In this case, we assume no info argument is present: + return False + info_arg = _serializer_info_arg(mode, count_positional_required_params(sig)) + if info_arg is None: + raise PydanticUserError( + f'Unrecognized field_serializer function signature for {serializer} with `mode={mode}`:{sig}', + code='field-serializer-signature', + ) + else: + return info_arg + + +def inspect_model_serializer(serializer: Callable[..., Any], mode: Literal['plain', 'wrap']) -> bool: + """Look at a model serializer function and determine whether it takes an info argument. + + An error is raised if the function has an invalid signature. + + Args: + serializer: The serializer function to check. + mode: The serializer mode, either 'plain' or 'wrap'. + + Returns: + `info_arg` - whether the function expects an info argument. + """ + if isinstance(serializer, (staticmethod, classmethod)) or not is_instance_method_from_sig(serializer): + raise PydanticUserError( + '`@model_serializer` must be applied to instance methods', code='model-serializer-instance-method' + ) + + sig = signature_no_eval(serializer) + info_arg = _serializer_info_arg(mode, count_positional_required_params(sig)) + if info_arg is None: + raise PydanticUserError( + f'Unrecognized model_serializer function signature for {serializer} with `mode={mode}`:{sig}', + code='model-serializer-signature', + ) + else: + return info_arg + + +def _serializer_info_arg(mode: Literal['plain', 'wrap'], n_positional: int) -> bool | None: + if mode == 'plain': + if n_positional == 1: + # (input_value: Any, /) -> Any + return False + elif n_positional == 2: + # (model: Any, input_value: Any, /) -> Any + return True + else: + assert mode == 'wrap', f"invalid mode: {mode!r}, expected 'plain' or 'wrap'" + if n_positional == 2: + # (input_value: Any, serializer: SerializerFunctionWrapHandler, /) -> Any + return False + elif n_positional == 3: + # (input_value: Any, serializer: SerializerFunctionWrapHandler, info: SerializationInfo, /) -> Any + return True + + return None + + +AnyDecoratorCallable: TypeAlias = ( + 'Union[classmethod[Any, Any, Any], staticmethod[Any, Any], partialmethod[Any], Callable[..., Any]]' +) + + +def is_instance_method_from_sig(function: AnyDecoratorCallable) -> bool: + """Whether the function is an instance method. + + It will consider a function as instance method if the first parameter of + function is `self`. + + Args: + function: The function to check. + + Returns: + `True` if the function is an instance method, `False` otherwise. + """ + sig = signature_no_eval(unwrap_wrapped_function(function)) + first = next(iter(sig.parameters.values()), None) + if first and first.name == 'self': + return True + return False + + +def ensure_classmethod_based_on_signature(function: AnyDecoratorCallable) -> Any: + """Apply the `@classmethod` decorator on the function. + + Args: + function: The function to apply the decorator on. + + Return: + The `@classmethod` decorator applied function. + """ + if not isinstance( + unwrap_wrapped_function(function, unwrap_class_static_method=False), classmethod + ) and _is_classmethod_from_sig(function): + return classmethod(function) # type: ignore[arg-type] + return function + + +def _is_classmethod_from_sig(function: AnyDecoratorCallable) -> bool: + sig = signature_no_eval(unwrap_wrapped_function(function)) + first = next(iter(sig.parameters.values()), None) + if first and first.name == 'cls': + return True + return False + + +def unwrap_wrapped_function( + func: Any, + *, + unwrap_partial: bool = True, + unwrap_class_static_method: bool = True, +) -> Any: + """Recursively unwraps a wrapped function until the underlying function is reached. + This handles property, functools.partial, functools.partialmethod, staticmethod, and classmethod. + + Args: + func: The function to unwrap. + unwrap_partial: If True (default), unwrap partial and partialmethod decorators. + unwrap_class_static_method: If True (default), also unwrap classmethod and staticmethod + decorators. If False, only unwrap partial and partialmethod decorators. + + Returns: + The underlying function of the wrapped function. + """ + # Define the types we want to check against as a single tuple. + unwrap_types = ( + (property, cached_property) + + ((partial, partialmethod) if unwrap_partial else ()) + + ((staticmethod, classmethod) if unwrap_class_static_method else ()) + ) + + while isinstance(func, unwrap_types): + if unwrap_class_static_method and isinstance(func, (classmethod, staticmethod)): + func = func.__func__ + elif isinstance(func, (partial, partialmethod)): + func = func.func + elif isinstance(func, property): + func = func.fget # arbitrary choice, convenient for computed fields + else: + # Make coverage happy as it can only get here in the last possible case + assert isinstance(func, cached_property) + func = func.func # type: ignore + + return func + + +_function_like = ( + partial, + partialmethod, + types.FunctionType, + types.BuiltinFunctionType, + types.MethodType, + types.WrapperDescriptorType, + types.MethodWrapperType, + types.MemberDescriptorType, +) + + +def get_callable_return_type( + callable_obj: Any, + globalns: GlobalsNamespace | None = None, + localns: MappingNamespace | None = None, +) -> Any | PydanticUndefinedType: + """Get the callable return type. + + Args: + callable_obj: The callable to analyze. + globalns: The globals namespace to use during type annotation evaluation. + localns: The locals namespace to use during type annotation evaluation. + + Returns: + The function return type. + """ + if isinstance(callable_obj, type): + # types are callables, and we assume the return type + # is the type itself (e.g. `int()` results in an instance of `int`). + return callable_obj + + if not isinstance(callable_obj, _function_like): + call_func = getattr(type(callable_obj), '__call__', None) # noqa: B004 + if call_func is not None: + callable_obj = call_func + + hints = get_function_type_hints( + unwrap_wrapped_function(callable_obj), + include_keys={'return'}, + globalns=globalns, + localns=localns, + ) + return hints.get('return', PydanticUndefined) + + +def count_positional_required_params(sig: Signature) -> int: + """Get the number of positional (required) arguments of a signature. + + This function should only be used to inspect signatures of validation and serialization functions. + The first argument (the value being serialized or validated) is counted as a required argument + even if a default value exists. + + Returns: + The number of positional arguments of a signature. + """ + parameters = list(sig.parameters.values()) + return sum( + 1 + for param in parameters + if can_be_positional(param) + # First argument is the value being validated/serialized, and can have a default value + # (e.g. `float`, which has signature `(x=0, /)`). We assume other parameters (the info arg + # for instance) should be required, and thus without any default value. + and (param.default is Parameter.empty or param is parameters[0]) + ) + + +def ensure_property(f: Any) -> Any: + """Ensure that a function is a `property` or `cached_property`, or is a valid descriptor. + + Args: + f: The function to check. + + Returns: + The function, or a `property` or `cached_property` instance wrapping the function. + """ + if ismethoddescriptor(f) or isdatadescriptor(f): + return f + else: + return property(f) diff --git a/source/target-vendors/cp312/pydantic/_internal/_decorators_v1.py b/source/target-vendors/cp312/pydantic/_internal/_decorators_v1.py new file mode 100644 index 0000000..3427377 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_decorators_v1.py @@ -0,0 +1,174 @@ +"""Logic for V1 validators, e.g. `@validator` and `@root_validator`.""" + +from __future__ import annotations as _annotations + +from inspect import Parameter, signature +from typing import Any, Union, cast + +from pydantic_core import core_schema +from typing_extensions import Protocol + +from ..errors import PydanticUserError +from ._utils import can_be_positional + + +class V1OnlyValueValidator(Protocol): + """A simple validator, supported for V1 validators and V2 validators.""" + + def __call__(self, __value: Any) -> Any: ... + + +class V1ValidatorWithValues(Protocol): + """A validator with `values` argument, supported for V1 validators and V2 validators.""" + + def __call__(self, __value: Any, values: dict[str, Any]) -> Any: ... + + +class V1ValidatorWithValuesKwOnly(Protocol): + """A validator with keyword only `values` argument, supported for V1 validators and V2 validators.""" + + def __call__(self, __value: Any, *, values: dict[str, Any]) -> Any: ... + + +class V1ValidatorWithKwargs(Protocol): + """A validator with `kwargs` argument, supported for V1 validators and V2 validators.""" + + def __call__(self, __value: Any, **kwargs: Any) -> Any: ... + + +class V1ValidatorWithValuesAndKwargs(Protocol): + """A validator with `values` and `kwargs` arguments, supported for V1 validators and V2 validators.""" + + def __call__(self, __value: Any, values: dict[str, Any], **kwargs: Any) -> Any: ... + + +V1Validator = Union[ + V1ValidatorWithValues, V1ValidatorWithValuesKwOnly, V1ValidatorWithKwargs, V1ValidatorWithValuesAndKwargs +] + + +def can_be_keyword(param: Parameter) -> bool: + return param.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY) + + +def make_generic_v1_field_validator(validator: V1Validator) -> core_schema.WithInfoValidatorFunction: + """Wrap a V1 style field validator for V2 compatibility. + + Args: + validator: The V1 style field validator. + + Returns: + A wrapped V2 style field validator. + + Raises: + PydanticUserError: If the signature is not supported or the parameters are + not available in Pydantic V2. + """ + sig = signature(validator) + + needs_values_kw = False + + for param_num, (param_name, parameter) in enumerate(sig.parameters.items()): + if can_be_keyword(parameter) and param_name in ('field', 'config'): + raise PydanticUserError( + 'The `field` and `config` parameters are not available in Pydantic V2, ' + 'please use the `info` parameter instead.', + code='validator-field-config-info', + ) + if parameter.kind is Parameter.VAR_KEYWORD: + needs_values_kw = True + elif can_be_keyword(parameter) and param_name == 'values': + needs_values_kw = True + elif can_be_positional(parameter) and param_num == 0: + # value + continue + elif parameter.default is Parameter.empty: # ignore params with defaults e.g. bound by functools.partial + raise PydanticUserError( + f'Unsupported signature for V1 style validator {validator}: {sig} is not supported.', + code='validator-v1-signature', + ) + + if needs_values_kw: + # (v, **kwargs), (v, values, **kwargs), (v, *, values, **kwargs) or (v, *, values) + val1 = cast(V1ValidatorWithValues, validator) + + def wrapper1(value: Any, info: core_schema.ValidationInfo) -> Any: + return val1(value, values=info.data) + + return wrapper1 + else: + val2 = cast(V1OnlyValueValidator, validator) + + def wrapper2(value: Any, _: core_schema.ValidationInfo) -> Any: + return val2(value) + + return wrapper2 + + +RootValidatorValues = dict[str, Any] +# technically tuple[model_dict, model_extra, fields_set] | tuple[dataclass_dict, init_vars] +RootValidatorFieldsTuple = tuple[Any, ...] + + +class V1RootValidatorFunction(Protocol): + """A simple root validator, supported for V1 validators and V2 validators.""" + + def __call__(self, __values: RootValidatorValues) -> RootValidatorValues: ... + + +class V2CoreBeforeRootValidator(Protocol): + """V2 validator with mode='before'.""" + + def __call__(self, __values: RootValidatorValues, __info: core_schema.ValidationInfo) -> RootValidatorValues: ... + + +class V2CoreAfterRootValidator(Protocol): + """V2 validator with mode='after'.""" + + def __call__( + self, __fields_tuple: RootValidatorFieldsTuple, __info: core_schema.ValidationInfo + ) -> RootValidatorFieldsTuple: ... + + +def make_v1_generic_root_validator( + validator: V1RootValidatorFunction, pre: bool +) -> V2CoreBeforeRootValidator | V2CoreAfterRootValidator: + """Wrap a V1 style root validator for V2 compatibility. + + Args: + validator: The V1 style field validator. + pre: Whether the validator is a pre validator. + + Returns: + A wrapped V2 style validator. + """ + if pre is True: + # mode='before' for pydantic-core + def _wrapper1(values: RootValidatorValues, _: core_schema.ValidationInfo) -> RootValidatorValues: + return validator(values) + + return _wrapper1 + + # mode='after' for pydantic-core + def _wrapper2(fields_tuple: RootValidatorFieldsTuple, _: core_schema.ValidationInfo) -> RootValidatorFieldsTuple: + if len(fields_tuple) == 2: + # dataclass, this is easy + values, init_vars = fields_tuple + values = validator(values) + return values, init_vars + else: + # ugly hack: to match v1 behaviour, we merge values and model_extra, then split them up based on fields + # afterwards + model_dict, model_extra, fields_set = fields_tuple + if model_extra: + fields = set(model_dict.keys()) + model_dict.update(model_extra) + model_dict_new = validator(model_dict) + for k in list(model_dict_new.keys()): + if k not in fields: + model_extra[k] = model_dict_new.pop(k) + else: + model_dict_new = validator(model_dict) + return model_dict_new, model_extra, fields_set + + return _wrapper2 diff --git a/source/target-vendors/cp312/pydantic/_internal/_discriminated_union.py b/source/target-vendors/cp312/pydantic/_internal/_discriminated_union.py new file mode 100644 index 0000000..fc52f30 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_discriminated_union.py @@ -0,0 +1,494 @@ +from __future__ import annotations as _annotations + +from collections.abc import Hashable, Sequence +from typing import TYPE_CHECKING, Any, cast + +from pydantic_core import CoreSchema, core_schema + +from ..errors import PydanticUserError +from . import _core_utils +from ._core_utils import ( + CoreSchemaField, +) + +if TYPE_CHECKING: + from ..types import Discriminator + from ._core_metadata import CoreMetadata + + +class MissingDefinitionForUnionRef(Exception): + """Raised when applying a discriminated union discriminator to a schema + requires a definition that is not yet defined + """ + + def __init__(self, ref: str) -> None: + self.ref = ref + super().__init__(f'Missing definition for ref {self.ref!r}') + + +def set_discriminator_in_metadata(schema: CoreSchema, discriminator: Any) -> None: + metadata = cast('CoreMetadata', schema.setdefault('metadata', {})) + metadata['pydantic_internal_union_discriminator'] = discriminator + + +def apply_discriminator( + schema: core_schema.CoreSchema, + discriminator: str | Discriminator, + definitions: dict[str, core_schema.CoreSchema] | None = None, +) -> core_schema.CoreSchema: + """Applies the discriminator and returns a new core schema. + + Args: + schema: The input schema. + discriminator: The name of the field which will serve as the discriminator. + definitions: A mapping of schema ref to schema. + + Returns: + The new core schema. + + Raises: + TypeError: + - If `discriminator` is used with invalid union variant. + - If `discriminator` is used with `Union` type with one variant. + - If `discriminator` value mapped to multiple choices. + MissingDefinitionForUnionRef: + If the definition for ref is missing. + PydanticUserError: + - If a model in union doesn't have a discriminator field. + - If discriminator field has a non-string alias. + - If discriminator fields have different aliases. + - If discriminator field not of type `Literal`. + """ + from ..types import Discriminator + + if isinstance(discriminator, Discriminator): + if isinstance(discriminator.discriminator, str): + discriminator = discriminator.discriminator + else: + return discriminator._convert_schema(schema) + + return _ApplyInferredDiscriminator(discriminator, definitions or {}).apply(schema) + + +class _ApplyInferredDiscriminator: + """This class is used to convert an input schema containing a union schema into one where that union is + replaced with a tagged-union, with all the associated debugging and performance benefits. + + This is done by: + * Validating that the input schema is compatible with the provided discriminator + * Introspecting the schema to determine which discriminator values should map to which union choices + * Handling various edge cases such as 'definitions', 'default', 'nullable' schemas, and more + + I have chosen to implement the conversion algorithm in this class, rather than a function, + to make it easier to maintain state while recursively walking the provided CoreSchema. + """ + + def __init__(self, discriminator: str, definitions: dict[str, core_schema.CoreSchema]): + # `discriminator` should be the name of the field which will serve as the discriminator. + # It must be the python name of the field, and *not* the field's alias. Note that as of now, + # all members of a discriminated union _must_ use a field with the same name as the discriminator. + # This may change if/when we expose a way to manually specify the TaggedUnionSchema's choices. + self.discriminator = discriminator + + # `definitions` should contain a mapping of schema ref to schema for all schemas which might + # be referenced by some choice + self.definitions = definitions + + # `_discriminator_alias` will hold the value, if present, of the alias for the discriminator + # + # Note: following the v1 implementation, we currently disallow the use of different aliases + # for different choices. This is not a limitation of pydantic_core, but if we try to handle + # this, the inference logic gets complicated very quickly, and could result in confusing + # debugging challenges for users making subtle mistakes. + # + # Rather than trying to do the most powerful inference possible, I think we should eventually + # expose a way to more-manually control the way the TaggedUnionSchema is constructed through + # the use of a new type which would be placed as an Annotation on the Union type. This would + # provide the full flexibility/power of pydantic_core's TaggedUnionSchema where necessary for + # more complex cases, without over-complicating the inference logic for the common cases. + self._discriminator_alias: str | None = None + + # `_should_be_nullable` indicates whether the converted union has `None` as an allowed value. + # If `None` is an acceptable value of the (possibly-wrapped) union, we ignore it while + # constructing the TaggedUnionSchema, but set the `_should_be_nullable` attribute to True. + # Once we have constructed the TaggedUnionSchema, if `_should_be_nullable` is True, we ensure + # that the final schema gets wrapped as a NullableSchema. This has the same semantics on the + # python side, but resolves the issue that `None` cannot correspond to any discriminator values. + self._should_be_nullable = False + + # `_is_nullable` is used to track if the final produced schema will definitely be nullable; + # we set it to True if the input schema is wrapped in a nullable schema that we know will be preserved + # as an indication that, even if None is discovered as one of the union choices, we will not need to wrap + # the final value in another nullable schema. + # + # This is more complicated than just checking for the final outermost schema having type 'nullable' thanks + # to the possible presence of other wrapper schemas such as DefinitionsSchema, WithDefaultSchema, etc. + self._is_nullable = False + + # `_choices_to_handle` serves as a stack of choices to add to the tagged union. Initially, choices + # from the union in the wrapped schema will be appended to this list, and the recursive choice-handling + # algorithm may add more choices to this stack as (nested) unions are encountered. + self._choices_to_handle: list[core_schema.CoreSchema] = [] + + # `_tagged_union_choices` is built during the call to `apply`, and will hold the choices to be included + # in the output TaggedUnionSchema that will replace the union from the input schema + self._tagged_union_choices: dict[Hashable, core_schema.CoreSchema] = {} + + # `_used` is changed to True after applying the discriminator to prevent accidental reuse + self._used = False + + def apply(self, schema: core_schema.CoreSchema) -> core_schema.CoreSchema: + """Return a new CoreSchema based on `schema` that uses a tagged-union with the discriminator provided + to this class. + + Args: + schema: The input schema. + + Returns: + The new core schema. + + Raises: + TypeError: + - If `discriminator` is used with invalid union variant. + - If `discriminator` is used with `Union` type with one variant. + - If `discriminator` value mapped to multiple choices. + ValueError: + If the definition for ref is missing. + PydanticUserError: + - If a model in union doesn't have a discriminator field. + - If discriminator field has a non-string alias. + - If discriminator fields have different aliases. + - If discriminator field not of type `Literal`. + """ + assert not self._used + schema = self._apply_to_root(schema) + if self._should_be_nullable and not self._is_nullable: + schema = core_schema.nullable_schema(schema) + self._used = True + return schema + + def _apply_to_root(self, schema: core_schema.CoreSchema) -> core_schema.CoreSchema: + """This method handles the outer-most stage of recursion over the input schema: + unwrapping nullable or definitions schemas, and calling the `_handle_choice` + method iteratively on the choices extracted (recursively) from the possibly-wrapped union. + """ + if schema['type'] == 'nullable': + self._is_nullable = True + wrapped = self._apply_to_root(schema['schema']) + nullable_wrapper = schema.copy() + nullable_wrapper['schema'] = wrapped + return nullable_wrapper + + if schema['type'] == 'definitions': + wrapped = self._apply_to_root(schema['schema']) + definitions_wrapper = schema.copy() + definitions_wrapper['schema'] = wrapped + return definitions_wrapper + + if schema['type'] == 'definition-ref': + schema_ref = schema['schema_ref'] + if schema_ref not in self.definitions: # pragma: no cover + raise MissingDefinitionForUnionRef(schema_ref) + + def_schema = self.definitions[schema_ref] + # If using a referenceable union as discriminated (e.g. `type Pet = Cat | Dog; field: Pet = Field(discriminator=...)`): + if def_schema['type'] == 'union': + schema = def_schema.copy() + schema.pop('ref') + + if schema['type'] != 'union': + # If the schema is not a union, it probably means it just had a single member and + # was flattened by pydantic_core. + # However, it still may make sense to apply the discriminator to this schema, + # as a way to get discriminated-union-style error messages, so we allow this here. + schema = core_schema.union_schema([schema]) + + # Reverse the choices list before extending the stack so that they get handled in the order they occur + choices_schemas = [v[0] if isinstance(v, tuple) else v for v in schema['choices'][::-1]] + self._choices_to_handle.extend(choices_schemas) + while self._choices_to_handle: + choice = self._choices_to_handle.pop() + self._handle_choice(choice) + + if self._discriminator_alias is not None and self._discriminator_alias != self.discriminator: + # * We need to annotate `discriminator` as a union here to handle both branches of this conditional + # * We need to annotate `discriminator` as list[list[str | int]] and not list[list[str]] due to the + # invariance of list, and because list[list[str | int]] is the type of the discriminator argument + # to tagged_union_schema below + # * See the docstring of pydantic_core.core_schema.tagged_union_schema for more details about how to + # interpret the value of the discriminator argument to tagged_union_schema. (The list[list[str]] here + # is the appropriate way to provide a list of fallback attributes to check for a discriminator value.) + discriminator: str | list[list[str | int]] = [[self.discriminator], [self._discriminator_alias]] + else: + discriminator = self.discriminator + return core_schema.tagged_union_schema( + choices=self._tagged_union_choices, + discriminator=discriminator, + custom_error_type=schema.get('custom_error_type'), + custom_error_message=schema.get('custom_error_message'), + custom_error_context=schema.get('custom_error_context'), + strict=False, + from_attributes=True, + ref=schema.get('ref'), + metadata=schema.get('metadata'), + serialization=schema.get('serialization'), + ) + + def _handle_choice(self, choice: core_schema.CoreSchema) -> None: + """This method handles the "middle" stage of recursion over the input schema. + Specifically, it is responsible for handling each choice of the outermost union + (and any "coalesced" choices obtained from inner unions). + + Here, "handling" entails: + * Coalescing nested unions and compatible tagged-unions + * Tracking the presence of 'none' and 'nullable' schemas occurring as choices + * Validating that each allowed discriminator value maps to a unique choice + * Updating the _tagged_union_choices mapping that will ultimately be used to build the TaggedUnionSchema. + """ + if choice['type'] == 'definition-ref': + if choice['schema_ref'] not in self.definitions: + raise MissingDefinitionForUnionRef(choice['schema_ref']) + + if choice['type'] == 'none': + self._should_be_nullable = True + elif choice['type'] == 'definitions': + self._handle_choice(choice['schema']) + elif choice['type'] == 'nullable': + self._should_be_nullable = True + self._handle_choice(choice['schema']) # unwrap the nullable schema + elif choice['type'] == 'union': + # Reverse the choices list before extending the stack so that they get handled in the order they occur + choices_schemas = [v[0] if isinstance(v, tuple) else v for v in choice['choices'][::-1]] + self._choices_to_handle.extend(choices_schemas) + elif choice['type'] not in { + 'model', + 'typed-dict', + 'tagged-union', + 'lax-or-strict', + 'dataclass', + 'dataclass-args', + 'definition-ref', + } and not _core_utils.is_function_with_inner_schema(choice): + # We should eventually handle 'definition-ref' as well + err_str = f'The core schema type {choice["type"]!r} is not a valid discriminated union variant.' + if choice['type'] == 'list': + err_str += ( + ' If you are making use of a list of union types, make sure the discriminator is applied to the ' + 'union type and not the list (e.g. `list[Annotated[ | , Field(discriminator=...)]]`).' + ) + raise TypeError(err_str) + else: + if choice['type'] == 'tagged-union' and self._is_discriminator_shared(choice): + # In this case, this inner tagged-union is compatible with the outer tagged-union, + # and its choices can be coalesced into the outer TaggedUnionSchema. + subchoices = [x for x in choice['choices'].values() if not isinstance(x, (str, int))] + # Reverse the choices list before extending the stack so that they get handled in the order they occur + self._choices_to_handle.extend(subchoices[::-1]) + return + + inferred_discriminator_values = self._infer_discriminator_values_for_choice(choice, source_name=None) + self._set_unique_choice_for_values(choice, inferred_discriminator_values) + + def _is_discriminator_shared(self, choice: core_schema.TaggedUnionSchema) -> bool: + """This method returns a boolean indicating whether the discriminator for the `choice` + is the same as that being used for the outermost tagged union. This is used to + determine whether this TaggedUnionSchema choice should be "coalesced" into the top level, + or whether it should be treated as a separate (nested) choice. + """ + inner_discriminator = choice['discriminator'] + return inner_discriminator == self.discriminator or ( + isinstance(inner_discriminator, list) + and (self.discriminator in inner_discriminator or [self.discriminator] in inner_discriminator) + ) + + def _infer_discriminator_values_for_choice( # noqa C901 + self, choice: core_schema.CoreSchema, source_name: str | None + ) -> list[str | int]: + """This function recurses over `choice`, extracting all discriminator values that should map to this choice. + + `model_name` is accepted for the purpose of producing useful error messages. + """ + if choice['type'] == 'definitions': + return self._infer_discriminator_values_for_choice(choice['schema'], source_name=source_name) + + elif _core_utils.is_function_with_inner_schema(choice): + return self._infer_discriminator_values_for_choice(choice['schema'], source_name=source_name) + + elif choice['type'] == 'lax-or-strict': + return sorted( + set( + self._infer_discriminator_values_for_choice(choice['lax_schema'], source_name=None) + + self._infer_discriminator_values_for_choice(choice['strict_schema'], source_name=None) + ) + ) + + elif choice['type'] == 'tagged-union': + values: list[str | int] = [] + # Ignore str/int "choices" since these are just references to other choices + subchoices = [x for x in choice['choices'].values() if not isinstance(x, (str, int))] + for subchoice in subchoices: + subchoice_values = self._infer_discriminator_values_for_choice(subchoice, source_name=None) + values.extend(subchoice_values) + return values + + elif choice['type'] == 'union': + values = [] + for subchoice in choice['choices']: + subchoice_schema = subchoice[0] if isinstance(subchoice, tuple) else subchoice + subchoice_values = self._infer_discriminator_values_for_choice(subchoice_schema, source_name=None) + values.extend(subchoice_values) + return values + + elif choice['type'] == 'nullable': + self._should_be_nullable = True + return self._infer_discriminator_values_for_choice(choice['schema'], source_name=None) + + elif choice['type'] == 'model': + return self._infer_discriminator_values_for_choice(choice['schema'], source_name=choice['cls'].__name__) + + elif choice['type'] == 'dataclass': + return self._infer_discriminator_values_for_choice(choice['schema'], source_name=choice['cls'].__name__) + + elif choice['type'] == 'model-fields': + return self._infer_discriminator_values_for_model_choice(choice, source_name=source_name) + + elif choice['type'] == 'dataclass-args': + return self._infer_discriminator_values_for_dataclass_choice(choice, source_name=source_name) + + elif choice['type'] == 'typed-dict': + return self._infer_discriminator_values_for_typed_dict_choice(choice, source_name=source_name) + + elif choice['type'] == 'definition-ref': + schema_ref = choice['schema_ref'] + if schema_ref not in self.definitions: + raise MissingDefinitionForUnionRef(schema_ref) + return self._infer_discriminator_values_for_choice(self.definitions[schema_ref], source_name=source_name) + else: + err_str = f'The core schema type {choice["type"]!r} is not a valid discriminated union variant.' + if choice['type'] == 'list': + err_str += ( + ' If you are making use of a list of union types, make sure the discriminator is applied to the ' + 'union type and not the list (e.g. `list[Annotated[ | , Field(discriminator=...)]]`).' + ) + raise TypeError(err_str) + + def _infer_discriminator_values_for_typed_dict_choice( + self, choice: core_schema.TypedDictSchema, source_name: str | None = None + ) -> list[str | int]: + """This method just extracts the _infer_discriminator_values_for_choice logic specific to TypedDictSchema + for the sake of readability. + """ + source = 'TypedDict' if source_name is None else f'TypedDict {source_name!r}' + field = choice['fields'].get(self.discriminator) + if field is None: + raise PydanticUserError( + f'{source} needs a discriminator field for key {self.discriminator!r}', code='discriminator-no-field' + ) + return self._infer_discriminator_values_for_field(field, source) + + def _infer_discriminator_values_for_model_choice( + self, choice: core_schema.ModelFieldsSchema, source_name: str | None = None + ) -> list[str | int]: + source = 'ModelFields' if source_name is None else f'Model {source_name!r}' + field = choice['fields'].get(self.discriminator) + if field is None: + raise PydanticUserError( + f'{source} needs a discriminator field for key {self.discriminator!r}', code='discriminator-no-field' + ) + return self._infer_discriminator_values_for_field(field, source) + + def _infer_discriminator_values_for_dataclass_choice( + self, choice: core_schema.DataclassArgsSchema, source_name: str | None = None + ) -> list[str | int]: + source = 'DataclassArgs' if source_name is None else f'Dataclass {source_name!r}' + for field in choice['fields']: + if field['name'] == self.discriminator: + break + else: + raise PydanticUserError( + f'{source} needs a discriminator field for key {self.discriminator!r}', code='discriminator-no-field' + ) + return self._infer_discriminator_values_for_field(field, source) + + def _infer_discriminator_values_for_field(self, field: CoreSchemaField, source: str) -> list[str | int]: + if field['type'] == 'computed-field': + # This should never occur as a discriminator, as it is only relevant to serialization + return [] + alias = field.get('validation_alias', self.discriminator) + if not isinstance(alias, str): + raise PydanticUserError( + f'Alias {alias!r} is not supported in a discriminated union', code='discriminator-alias-type' + ) + if self._discriminator_alias is None: + self._discriminator_alias = alias + elif self._discriminator_alias != alias: + raise PydanticUserError( + f'Aliases for discriminator {self.discriminator!r} must be the same ' + f'(got {alias}, {self._discriminator_alias})', + code='discriminator-alias', + ) + return self._infer_discriminator_values_for_inner_schema(field['schema'], source) + + def _infer_discriminator_values_for_inner_schema( + self, schema: core_schema.CoreSchema, source: str + ) -> list[str | int]: + """When inferring discriminator values for a field, we typically extract the expected values from a literal + schema. This function does that, but also handles nested unions and defaults. + """ + if schema['type'] == 'literal': + return schema['expected'] + + elif schema['type'] == 'union': + # Generally when multiple values are allowed they should be placed in a single `Literal`, but + # we add this case to handle the situation where a field is annotated as a `Union` of `Literal`s. + # For example, this lets us handle `Union[Literal['key'], Union[Literal['Key'], Literal['KEY']]]` + values: list[Any] = [] + for choice in schema['choices']: + choice_schema = choice[0] if isinstance(choice, tuple) else choice + choice_values = self._infer_discriminator_values_for_inner_schema(choice_schema, source) + values.extend(choice_values) + return values + + elif schema['type'] == 'default': + # This will happen if the field has a default value; we ignore it while extracting the discriminator values + return self._infer_discriminator_values_for_inner_schema(schema['schema'], source) + + elif schema['type'] == 'function-after': + # After validators don't affect the discriminator values + return self._infer_discriminator_values_for_inner_schema(schema['schema'], source) + + elif schema['type'] == 'model' and schema.get('root_model'): + # Support RootModel[Literal[...]] as discriminator field type + return self._infer_discriminator_values_for_inner_schema(schema['schema'], source) + + elif schema['type'] in {'function-before', 'function-wrap', 'function-plain'}: + validator_type = repr(schema['type'].split('-')[1]) + raise PydanticUserError( + f'Cannot use a mode={validator_type} validator in the' + f' discriminator field {self.discriminator!r} of {source}', + code='discriminator-validator', + ) + + else: + raise PydanticUserError( + f'{source} needs field {self.discriminator!r} to be of type `Literal`', + code='discriminator-needs-literal', + ) + + def _set_unique_choice_for_values(self, choice: core_schema.CoreSchema, values: Sequence[str | int]) -> None: + """This method updates `self.tagged_union_choices` so that all provided (discriminator) `values` map to the + provided `choice`, validating that none of these values already map to another (different) choice. + """ + for discriminator_value in values: + if discriminator_value in self._tagged_union_choices: + # It is okay if `value` is already in tagged_union_choices as long as it maps to the same value. + # Because tagged_union_choices may map values to other values, we need to walk the choices dict + # until we get to a "real" choice, and confirm that is equal to the one assigned. + existing_choice = self._tagged_union_choices[discriminator_value] + if existing_choice != choice: + raise TypeError( + f'Value {discriminator_value!r} for discriminator ' + f'{self.discriminator!r} mapped to multiple choices' + ) + else: + self._tagged_union_choices[discriminator_value] = choice diff --git a/source/target-vendors/cp312/pydantic/_internal/_docs_extraction.py b/source/target-vendors/cp312/pydantic/_internal/_docs_extraction.py new file mode 100644 index 0000000..6df77bf --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_docs_extraction.py @@ -0,0 +1,113 @@ +"""Utilities related to attribute docstring extraction.""" + +from __future__ import annotations + +import ast +import inspect +import sys +import textwrap +from typing import Any + + +class DocstringVisitor(ast.NodeVisitor): + def __init__(self) -> None: + super().__init__() + + self.target: str | None = None + self.attrs: dict[str, str] = {} + self.previous_node_type: type[ast.AST] | None = None + + def visit(self, node: ast.AST) -> Any: + node_result = super().visit(node) + self.previous_node_type = type(node) + return node_result + + def visit_AnnAssign(self, node: ast.AnnAssign) -> Any: + if isinstance(node.target, ast.Name): + self.target = node.target.id + + def visit_Expr(self, node: ast.Expr) -> Any: + if ( + isinstance(node.value, ast.Constant) + and isinstance(node.value.value, str) + and self.previous_node_type is ast.AnnAssign + ): + docstring = inspect.cleandoc(node.value.value) + if self.target: + self.attrs[self.target] = docstring + self.target = None + + +def _dedent_source_lines(source: list[str]) -> str: + # Required for nested class definitions, e.g. in a function block + dedent_source = textwrap.dedent(''.join(source)) + if dedent_source.startswith((' ', '\t')): + # We are in the case where there's a dedented (usually multiline) string + # at a lower indentation level than the class itself. We wrap our class + # in a function as a workaround. + dedent_source = f'def dedent_workaround():\n{dedent_source}' + return dedent_source + + +def _extract_source_from_frame(cls: type[Any]) -> list[str] | None: + frame = inspect.currentframe() + + while frame: + if inspect.getmodule(frame) is inspect.getmodule(cls): + lnum = frame.f_lineno + try: + lines, _ = inspect.findsource(frame) + except OSError: # pragma: no cover + # Source can't be retrieved (maybe because running in an interactive terminal), + # we don't want to error here. + pass + else: + block_lines = inspect.getblock(lines[lnum - 1 :]) + dedent_source = _dedent_source_lines(block_lines) + try: + block_tree = ast.parse(dedent_source) + except SyntaxError: + pass + else: + stmt = block_tree.body[0] + if isinstance(stmt, ast.FunctionDef) and stmt.name == 'dedent_workaround': + # `_dedent_source_lines` wrapped the class around the workaround function + stmt = stmt.body[0] + if isinstance(stmt, ast.ClassDef) and stmt.name == cls.__name__: + return block_lines + + frame = frame.f_back + + +def extract_docstrings_from_cls(cls: type[Any], use_inspect: bool = False) -> dict[str, str]: + """Map model attributes and their corresponding docstring. + + Args: + cls: The class of the Pydantic model to inspect. + use_inspect: Whether to skip usage of frames to find the object and use + the `inspect` module instead. + + Returns: + A mapping containing attribute names and their corresponding docstring. + """ + if use_inspect or sys.version_info >= (3, 13): + # On Python < 3.13, `inspect.getsourcelines()` might not work as expected + # if two classes have the same name in the same source file. + # On Python 3.13+, it will use the new `__firstlineno__` class attribute, + # making it way more robust. + try: + source, _ = inspect.getsourcelines(cls) + except OSError: # pragma: no cover + return {} + else: + # TODO remove this implementation when we drop support for Python 3.12: + source = _extract_source_from_frame(cls) + + if not source: + return {} + + dedent_source = _dedent_source_lines(source) + + visitor = DocstringVisitor() + visitor.visit(ast.parse(dedent_source)) + return visitor.attrs diff --git a/source/target-vendors/cp312/pydantic/_internal/_fields.py b/source/target-vendors/cp312/pydantic/_internal/_fields.py new file mode 100644 index 0000000..9a2a96b --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_fields.py @@ -0,0 +1,729 @@ +"""Private logic related to fields (the `Field()` function and `FieldInfo` class), and arguments to `Annotated`.""" + +from __future__ import annotations as _annotations + +import dataclasses +import warnings +from collections.abc import Mapping +from functools import cache +from inspect import Parameter, ismethoddescriptor +from re import Pattern +from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast + +from pydantic_core import PydanticUndefined +from typing_extensions import TypeIs +from typing_inspection.introspection import AnnotationSource + +from pydantic import PydanticDeprecatedSince211 +from pydantic.errors import PydanticUserError + +from ..aliases import AliasGenerator +from . import _generics, _typing_extra +from ._config import ConfigWrapper +from ._docs_extraction import extract_docstrings_from_cls +from ._import_utils import import_cached_base_model, import_cached_field_info +from ._internal_dataclass import slots_true +from ._namespace_utils import NsResolver +from ._repr import Representation +from ._utils import can_be_positional, get_first_not_none + +if TYPE_CHECKING: + from annotated_types import BaseMetadata + + from ..fields import FieldInfo + from ..main import BaseModel + from ._dataclasses import PydanticDataclass, StandardDataclass + from ._decorators import DecoratorInfos + + +class PydanticMetadata(Representation): + """Base class for annotation markers like `Strict`.""" + + __slots__ = () + + +@dataclasses.dataclass(**slots_true) # TODO: make kw_only when we drop support for 3.9. +class PydanticExtraInfo: + # TODO: make use of PEP 747: + annotation: Any + complete: bool + + +def pydantic_general_metadata(**metadata: Any) -> BaseMetadata: + """Create a new `_PydanticGeneralMetadata` class with the given metadata. + + Args: + **metadata: The metadata to add. + + Returns: + The new `_PydanticGeneralMetadata` class. + """ + return _general_metadata_cls()(metadata) # type: ignore + + +@cache +def _general_metadata_cls() -> type[BaseMetadata]: + """Do it this way to avoid importing `annotated_types` at import time.""" + from annotated_types import BaseMetadata + + class _PydanticGeneralMetadata(PydanticMetadata, BaseMetadata): + """Pydantic general metadata like `max_digits`.""" + + def __init__(self, metadata: Any): + self.__dict__ = metadata + + return _PydanticGeneralMetadata # type: ignore + + +def _check_protected_namespaces( + protected_namespaces: tuple[str | Pattern[str], ...], + ann_name: str, + bases: tuple[type[Any], ...], + cls_name: str, +) -> None: + BaseModel = import_cached_base_model() + + for protected_namespace in protected_namespaces: + ns_violation = False + if isinstance(protected_namespace, Pattern): + ns_violation = protected_namespace.match(ann_name) is not None + elif isinstance(protected_namespace, str): + ns_violation = ann_name.startswith(protected_namespace) + + if ns_violation: + for b in bases: + if hasattr(b, ann_name): + if not (issubclass(b, BaseModel) and ann_name in getattr(b, '__pydantic_fields__', {})): + raise ValueError( + f'Field {ann_name!r} conflicts with member {getattr(b, ann_name)}' + f' of protected namespace {protected_namespace!r}.' + ) + else: + valid_namespaces: list[str] = [] + for pn in protected_namespaces: + if isinstance(pn, Pattern): + if not pn.match(ann_name): + valid_namespaces.append(f're.compile({pn.pattern!r})') + else: + if not ann_name.startswith(pn): + valid_namespaces.append(f"'{pn}'") + + valid_namespaces_str = f'({", ".join(valid_namespaces)}{",)" if len(valid_namespaces) == 1 else ")"}' + + warnings.warn( + f'Field {ann_name!r} in {cls_name!r} conflicts with protected namespace {protected_namespace!r}.\n\n' + f"You may be able to solve this by setting the 'protected_namespaces' configuration to {valid_namespaces_str}.", + UserWarning, + stacklevel=5, + ) + + +def _update_fields_from_docstrings(cls: type[Any], fields: dict[str, FieldInfo], use_inspect: bool = False) -> None: + fields_docs = extract_docstrings_from_cls(cls, use_inspect=use_inspect) + for ann_name, field_info in fields.items(): + if field_info.description is None and ann_name in fields_docs: + field_info.description = fields_docs[ann_name] + + +def _apply_field_title_generator_to_field_info( + title_generator: Callable[[str, FieldInfo], str], + field_name: str, + field_info: FieldInfo, +): + if field_info.title is None: + title = title_generator(field_name, field_info) + if not isinstance(title, str): + raise TypeError(f'field_title_generator {title_generator} must return str, not {title.__class__}') + + field_info.title = title + + +def _apply_alias_generator_to_field_info( + alias_generator: Callable[[str], str] | AliasGenerator, field_name: str, field_info: FieldInfo +): + """Apply an alias generator to aliases on a `FieldInfo` instance if appropriate. + + Args: + alias_generator: A callable that takes a string and returns a string, or an `AliasGenerator` instance. + field_name: The name of the field from which to generate the alias. + field_info: The `FieldInfo` instance to which the alias generator is (maybe) applied. + """ + # Apply an alias_generator if + # 1. An alias is not specified + # 2. An alias is specified, but the priority is <= 1 + if ( + field_info.alias_priority is None + or field_info.alias_priority <= 1 + or field_info.alias is None + or field_info.validation_alias is None + or field_info.serialization_alias is None + ): + alias, validation_alias, serialization_alias = None, None, None + + if isinstance(alias_generator, AliasGenerator): + alias, validation_alias, serialization_alias = alias_generator.generate_aliases(field_name) + elif callable(alias_generator): + alias = alias_generator(field_name) + if not isinstance(alias, str): + raise TypeError(f'alias_generator {alias_generator} must return str, not {alias.__class__}') + + # if priority is not set, we set to 1 + # which supports the case where the alias_generator from a child class is used + # to generate an alias for a field in a parent class + if field_info.alias_priority is None or field_info.alias_priority <= 1: + field_info.alias_priority = 1 + + # if the priority is 1, then we set the aliases to the generated alias + if field_info.alias_priority == 1: + field_info.serialization_alias = get_first_not_none(serialization_alias, alias) + field_info.validation_alias = get_first_not_none(validation_alias, alias) + field_info.alias = alias + + # if any of the aliases are not set, then we set them to the corresponding generated alias + if field_info.alias is None: + field_info.alias = alias + if field_info.serialization_alias is None: + field_info.serialization_alias = get_first_not_none(serialization_alias, alias) + if field_info.validation_alias is None: + field_info.validation_alias = get_first_not_none(validation_alias, alias) + + +def update_field_from_config(config_wrapper: ConfigWrapper, field_name: str, field_info: FieldInfo) -> None: + """Update the `FieldInfo` instance from the configuration set on the model it belongs to. + + This will apply the title and alias generators from the configuration. + + Args: + config_wrapper: The configuration from the model. + field_name: The field name the `FieldInfo` instance is attached to. + field_info: The `FieldInfo` instance to update. + """ + field_title_generator = field_info.field_title_generator or config_wrapper.field_title_generator + if field_title_generator is not None: + _apply_field_title_generator_to_field_info(field_title_generator, field_name, field_info) + if config_wrapper.alias_generator is not None: + _apply_alias_generator_to_field_info(config_wrapper.alias_generator, field_name, field_info) + + +_deprecated_method_names = {'dict', 'json', 'copy', '_iter', '_copy_and_set_values', '_calculate_keys'} + +_deprecated_classmethod_names = { + 'parse_obj', + 'parse_raw', + 'parse_file', + 'from_orm', + 'construct', + 'schema', + 'schema_json', + 'validate', + 'update_forward_refs', + '_get_value', +} + + +def collect_model_fields( # noqa: C901 + cls: type[BaseModel], + config_wrapper: ConfigWrapper, + ns_resolver: NsResolver, + *, + typevars_map: Mapping[TypeVar, Any] | None = None, +) -> tuple[dict[str, FieldInfo], PydanticExtraInfo | None, set[str]]: + """Collect the fields and class variables names of a nascent Pydantic model. + + The fields collection process is *lenient*, meaning it won't error if string annotations + fail to evaluate. If this happens, the original annotation (and assigned value, if any) + is stored on the created `FieldInfo` instance. + + The `rebuild_model_fields()` should be called at a later point (e.g. when rebuilding the model), + and will make use of these stored attributes. + + Args: + cls: BaseModel or dataclass. + config_wrapper: The config wrapper instance. + ns_resolver: Namespace resolver to use when getting model annotations. + typevars_map: A dictionary mapping type variables to their concrete types. + + Returns: + A three-tuple containing the model fields, the `PydanticExtraInfo` instance if the `__pydantic_extra__` annotation is set, + and class variables names. + + Raises: + NameError: + - If there is a conflict between a field name and protected namespaces. + - If there is a field other than `root` in `RootModel`. + - If a field shadows an attribute in the parent model. + """ + FieldInfo_ = import_cached_field_info() + BaseModel_ = import_cached_base_model() + + bases = cls.__bases__ + parent_fields_lookup: dict[str, FieldInfo] = {} + for base in reversed(bases): + if model_fields := getattr(base, '__pydantic_fields__', None): + parent_fields_lookup.update(model_fields) + + type_hints = _typing_extra.get_model_type_hints(cls, ns_resolver=ns_resolver) + + # `cls_annotations` is only used to determine if an annotation comes from a parent class + cls_annotations = _typing_extra.safe_get_annotations(cls) + + fields: dict[str, FieldInfo] = {} + + class_vars: set[str] = set() + for ann_name, (ann_type, evaluated) in type_hints.items(): + if ann_name == 'model_config': + # We never want to treat `model_config` as a field + # Note: we may need to change this logic if/when we introduce a `BareModel` class with no + # protected namespaces (where `model_config` might be allowed as a field name) + continue + + _check_protected_namespaces( + protected_namespaces=config_wrapper.protected_namespaces, + ann_name=ann_name, + bases=bases, + cls_name=cls.__name__, + ) + + if _typing_extra.is_classvar_annotation(ann_type): + class_vars.add(ann_name) + continue + + assigned_value = getattr(cls, ann_name, PydanticUndefined) + if assigned_value is not PydanticUndefined and ( + # One of the deprecated instance methods was used as a field name (e.g. `dict()`): + any(getattr(BaseModel_, depr_name, None) is assigned_value for depr_name in _deprecated_method_names) + # One of the deprecated class methods was used as a field name (e.g. `schema()`): + or ( + hasattr(assigned_value, '__func__') + and any( + getattr(getattr(BaseModel_, depr_name, None), '__func__', None) is assigned_value.__func__ # pyright: ignore[reportAttributeAccessIssue] + for depr_name in _deprecated_classmethod_names + ) + ) + ): + # Then `assigned_value` would be the method, even though no default was specified: + assigned_value = PydanticUndefined + + if not is_valid_field_name(ann_name): + continue + if cls.__pydantic_root_model__ and ann_name != 'root': + raise NameError( + f"Unexpected field with name {ann_name!r}; only 'root' is allowed as a field of a `RootModel`" + ) + + for base in bases: + if hasattr(base, ann_name): + if ann_name not in cls_annotations: + # Don't warn when a field exists in a parent class but has not been defined in the current class + continue + + # when building a generic model with `MyModel[int]`, the generic_origin check makes sure we don't get + # "... shadows an attribute" warnings + generic_origin = getattr(cls, '__pydantic_generic_metadata__', {}).get('origin') + if base is generic_origin: + # Don't warn when "shadowing" of attributes in parametrized generics + continue + + dataclass_fields = { + field.name for field in (dataclasses.fields(base) if dataclasses.is_dataclass(base) else ()) + } + if ann_name in dataclass_fields: + # Don't warn when inheriting stdlib dataclasses whose fields are "shadowed" by defaults being set + # on the class instance. + continue + + warnings.warn( + f'Field name "{ann_name}" in "{cls.__qualname__}" shadows an attribute in parent ' + f'"{base.__qualname__}"', + UserWarning, + stacklevel=4, + ) + + if assigned_value is PydanticUndefined: # no assignment, just a plain annotation + if ann_name in cls_annotations or ann_name not in parent_fields_lookup: + # field is either: + # - present in the current model's annotations (and *not* from parent classes) + # - not found on any base classes; this seems to be caused by fields not getting + # generated due to models not being fully defined while initializing recursive models. + # Nothing stops us from just creating a `FieldInfo` for this type hint, so we do this. + field_info = FieldInfo_.from_annotation(ann_type, _source=AnnotationSource.CLASS) + field_info._original_annotation = ann_type + if not evaluated: + field_info._complete = False + # Store the original annotation that should be used to rebuild + # the field info later: + else: + # The field was present on one of the (possibly multiple) base classes, we make a copy directly from it. + parent_field_info = parent_fields_lookup[ann_name]._copy() + + # The only case where substituting the type variables is relevant (i.e. when `typevars_map` is not empty) + # is when a generic class is parameterized (e.g. `MyGenericModel[int, str]`), which creates a new class object + # (unlike the stdlib genercis that create a generic alias). In this case, we are guaranteed to only have to copy + # from the origin/parent model (e.g. `MyGenericModel`). + if typevars_map: + field_info = _recreate_field_info( + parent_field_info, ns_resolver=ns_resolver, typevars_map=typevars_map, lenient=True + ) + else: + field_info = parent_field_info + + else: # An assigned value is present (either the default value, or a `Field()` function) + if isinstance(assigned_value, FieldInfo_) and ismethoddescriptor(assigned_value.default): + # `assigned_value` was fetched using `getattr`, which triggers a call to `__get__` + # for descriptors, so we do the same if the `= field(default=...)` form is used. + # Note that we only do this for method descriptors for now, we might want to + # extend this to any descriptor in the future (by simply checking for + # `hasattr(assigned_value.default, '__get__')`). + default = assigned_value.default.__get__(None, cls) + assigned_value.default = default + assigned_value._attributes_set['default'] = default + + field_info = FieldInfo_.from_annotated_attribute(ann_type, assigned_value, _source=AnnotationSource.CLASS) + + # Store the original annotation and assignment value that could be used to rebuild the field info later. + field_info._original_assignment = assigned_value + field_info._original_annotation = ann_type + if not evaluated: + field_info._complete = False + elif 'final' in field_info._qualifiers and not field_info.is_required(): + warnings.warn( + f'Annotation {ann_name!r} is marked as final and has a default value. Pydantic treats {ann_name!r} as a ' + 'class variable, but it will be considered as a normal field in V3 to be aligned with dataclasses. If you ' + f'still want {ann_name!r} to be considered as a class variable, annotate it as: `ClassVar[] = .`', + category=PydanticDeprecatedSince211, + # Incorrect when `create_model` is used, but the chance that final with a default is used is low in that case: + stacklevel=4, + ) + class_vars.add(ann_name) + continue + + # attributes which are fields are removed from the class namespace: + # 1. To match the behaviour of annotation-only fields + # 2. To avoid false positives in the NameError check above + try: + delattr(cls, ann_name) + except AttributeError: + pass # indicates the attribute was on a parent class + + # Use cls.__dict__['__pydantic_decorators__'] instead of cls.__pydantic_decorators__ + # to make sure the decorators have already been built for this exact class + decorators: DecoratorInfos = cls.__dict__['__pydantic_decorators__'] + if ann_name in decorators.computed_fields: + raise TypeError( + f'Field {ann_name!r} of class {cls.__name__!r} overrides symbol of same name in a parent class. ' + 'This override with a computed_field is incompatible.' + ) + fields[ann_name] = field_info + + if field_info._complete: + # If not complete, this will be called in `rebuild_model_fields()`: + update_field_from_config(config_wrapper, ann_name, field_info) + + if config_wrapper.use_attribute_docstrings: + _update_fields_from_docstrings(cls, fields) + + pydantic_extra_info: PydanticExtraInfo | None = None + if '__pydantic_extra__' in type_hints: + ann, complete = type_hints['__pydantic_extra__'] + pydantic_extra_info = PydanticExtraInfo( + annotation=ann, + complete=complete, + ) + + return fields, pydantic_extra_info, class_vars + + +def rebuild_model_fields( + cls: type[BaseModel], + *, + config_wrapper: ConfigWrapper, + ns_resolver: NsResolver, + typevars_map: Mapping[TypeVar, Any], +) -> tuple[dict[str, FieldInfo], PydanticExtraInfo | None]: + """Rebuild the (already present) model fields by trying to reevaluate annotations. + + This function should be called whenever a model with incomplete fields is encountered. + + Returns: + A two-tuple, the first element being the rebuilt fields, the second element being + the rebuild `PydanticExtraInfo` instance, if available. + + Raises: + NameError: If one of the annotations failed to evaluate. + + Note: + This function *doesn't* mutate the model fields in place, as it can be called during + schema generation, where you don't want to mutate other model's fields. + """ + rebuilt_fields: dict[str, FieldInfo] = {} + with ns_resolver.push(cls): + for f_name, field_info in cls.__pydantic_fields__.items(): + if field_info._complete: + rebuilt_fields[f_name] = field_info + else: + new_field = _recreate_field_info( + field_info, ns_resolver=ns_resolver, typevars_map=typevars_map, lenient=False + ) + update_field_from_config(config_wrapper, f_name, new_field) + rebuilt_fields[f_name] = new_field + + if cls.__pydantic_extra_info__ is not None and not cls.__pydantic_extra_info__.complete: + rebuilt_extra_info = PydanticExtraInfo( + annotation=_typing_extra.eval_type( + cls.__pydantic_extra_info__.annotation, *ns_resolver.types_namespace + ), + complete=True, + ) + else: + rebuilt_extra_info = cls.__pydantic_extra_info__ + + return rebuilt_fields, rebuilt_extra_info + + +def _recreate_field_info( + field_info: FieldInfo, + ns_resolver: NsResolver, + typevars_map: Mapping[TypeVar, Any], + *, + lenient: bool, +) -> FieldInfo: + FieldInfo_ = import_cached_field_info() + + existing_desc = field_info.description + if lenient: + ann = _generics.replace_types(field_info._original_annotation, typevars_map) + ann, evaluated = _typing_extra.try_eval_type( + ann, + *ns_resolver.types_namespace, + ) + else: + # Not the best pattern, maybe we could ship our own `eval_type()`, + # that would replace the type variables on the fly during evaluation. + ann = _typing_extra.eval_type( + field_info._original_annotation, + *ns_resolver.types_namespace, + ) + ann = _generics.replace_types(ann, typevars_map) + ann = _typing_extra.eval_type( + ann, + *ns_resolver.types_namespace, + ) + evaluated = True + + if (assign := field_info._original_assignment) is PydanticUndefined: + new_field = FieldInfo_.from_annotation(ann, _source=AnnotationSource.CLASS) + else: + new_field = FieldInfo_.from_annotated_attribute(ann, assign, _source=AnnotationSource.CLASS) + new_field._original_assignment = assign + new_field._original_annotation = ann + # The description might come from the docstring if `use_attribute_docstrings` was `True`: + new_field.description = new_field.description if new_field.description is not None else existing_desc + if not evaluated: + new_field._complete = False + + return new_field + + +def collect_dataclass_fields( + cls: type[StandardDataclass], + *, + config_wrapper: ConfigWrapper, + ns_resolver: NsResolver | None = None, + typevars_map: dict[Any, Any] | None = None, +) -> dict[str, FieldInfo]: + """Collect the fields of a dataclass. + + Args: + cls: dataclass. + config_wrapper: The config wrapper instance. + ns_resolver: Namespace resolver to use when getting dataclass annotations. + Defaults to an empty instance. + typevars_map: A dictionary mapping type variables to their concrete types. + + Returns: + The dataclass fields. + """ + FieldInfo_ = import_cached_field_info() + + fields: dict[str, FieldInfo] = {} + ns_resolver = ns_resolver or NsResolver() + dataclass_fields = cls.__dataclass_fields__ + + # The logic here is similar to `_typing_extra.get_cls_type_hints`, + # although we do it manually as stdlib dataclasses already have annotations + # collected in each class: + for base in reversed(cls.__mro__): + if not dataclasses.is_dataclass(base): + continue + + with ns_resolver.push(base): + for ann_name, dataclass_field in dataclass_fields.items(): + base_anns = _typing_extra.safe_get_annotations(base) + + if ann_name not in base_anns: + # `__dataclass_fields__`contains every field, even the ones from base classes. + # Only collect the ones defined on `base`. + continue + + globalns, localns = ns_resolver.types_namespace + ann_type, evaluated = _typing_extra.try_eval_type(dataclass_field.type, globalns, localns) + + if _typing_extra.is_classvar_annotation(ann_type): + continue + + if ( + not dataclass_field.init + and dataclass_field.default is dataclasses.MISSING + and dataclass_field.default_factory is dataclasses.MISSING + ): + # TODO: We should probably do something with this so that validate_assignment behaves properly + # Issue: https://github.com/pydantic/pydantic/issues/5470 + continue + + if isinstance(dataclass_field.default, FieldInfo_): + if dataclass_field.default.init_var: + if dataclass_field.default.init is False: + raise PydanticUserError( + f'Dataclass field {ann_name} has init=False and init_var=True, but these are mutually exclusive.', + code='clashing-init-and-init-var', + ) + + # TODO: same note as above re validate_assignment + continue + field_info = FieldInfo_.from_annotated_attribute( + ann_type, dataclass_field.default, _source=AnnotationSource.DATACLASS + ) + field_info._original_assignment = dataclass_field.default + else: + field_info = FieldInfo_.from_annotated_attribute( + ann_type, dataclass_field, _source=AnnotationSource.DATACLASS + ) + field_info._original_assignment = dataclass_field + + if not evaluated: + field_info._complete = False + field_info._original_annotation = ann_type + + fields[ann_name] = field_info + update_field_from_config(config_wrapper, ann_name, field_info) + + if field_info.default is not PydanticUndefined and isinstance( + getattr(cls, ann_name, field_info), FieldInfo_ + ): + # We need this to fix the default when the "default" from __dataclass_fields__ is a pydantic.FieldInfo + setattr(cls, ann_name, field_info.default) + + if typevars_map: + for field in fields.values(): + # We don't pass any ns, as `field.annotation` + # was already evaluated. TODO: is this method relevant? + # Can't we juste use `_generics.replace_types`? + field.apply_typevars_map(typevars_map) + + if config_wrapper.use_attribute_docstrings: + _update_fields_from_docstrings( + cls, + fields, + # We can't rely on the (more reliable) frame inspection method + # for stdlib dataclasses: + use_inspect=not hasattr(cls, '__is_pydantic_dataclass__'), + ) + + return fields + + +def rebuild_dataclass_fields( + cls: type[PydanticDataclass], + *, + config_wrapper: ConfigWrapper, + ns_resolver: NsResolver, + typevars_map: Mapping[TypeVar, Any], +) -> dict[str, FieldInfo]: + """Rebuild the (already present) dataclass fields by trying to reevaluate annotations. + + This function should be called whenever a dataclass with incomplete fields is encountered. + + Raises: + NameError: If one of the annotations failed to evaluate. + + Note: + This function *doesn't* mutate the dataclass fields in place, as it can be called during + schema generation, where you don't want to mutate other dataclass's fields. + """ + FieldInfo_ = import_cached_field_info() + + rebuilt_fields: dict[str, FieldInfo] = {} + with ns_resolver.push(cls): + for f_name, field_info in cls.__pydantic_fields__.items(): + if field_info._complete: + rebuilt_fields[f_name] = field_info + else: + existing_desc = field_info.description + ann = _typing_extra.eval_type( + field_info._original_annotation, + *ns_resolver.types_namespace, + ) + ann = _generics.replace_types(ann, typevars_map) + new_field = FieldInfo_.from_annotated_attribute( + ann, + field_info._original_assignment, + _source=AnnotationSource.DATACLASS, + ) + + # The description might come from the docstring if `use_attribute_docstrings` was `True`: + new_field.description = new_field.description if new_field.description is not None else existing_desc + update_field_from_config(config_wrapper, f_name, new_field) + rebuilt_fields[f_name] = new_field + + return rebuilt_fields + + +def is_valid_field_name(name: str) -> bool: + return not name.startswith('_') + + +def is_valid_privateattr_name(name: str) -> bool: + return name.startswith('_') and not name.startswith('__') + + +def takes_validated_data_argument( + default_factory: Callable[[], Any] | Callable[[dict[str, Any]], Any], +) -> TypeIs[Callable[[dict[str, Any]], Any]]: + """Whether the provided default factory callable has a validated data parameter.""" + try: + sig = _typing_extra.signature_no_eval(default_factory) + except (ValueError, TypeError): + # `inspect.signature` might not be able to infer a signature, e.g. with C objects. + # In this case, we assume no data argument is present: + return False + + parameters = list(sig.parameters.values()) + + return len(parameters) == 1 and can_be_positional(parameters[0]) and parameters[0].default is Parameter.empty + + +def resolve_default_value( + default: Any, + default_factory: Callable[[], Any] | Callable[[dict[str, Any]], Any] | None, + *, + validated_data: dict[str, Any] | None = None, + call_default_factory: bool = False, +) -> Any: + """Resolve the default value using either a static default or a default_factory.""" + from ._utils import smart_deepcopy + + if default_factory is None: + return smart_deepcopy(default) + if call_default_factory: + if takes_validated_data_argument(default_factory=default_factory): + fac = cast('Callable[[dict[str, Any]], Any]', default_factory) + if validated_data is None: + raise ValueError( + "The default factory requires the 'validated_data' argument, which was not provided when calling 'get_default()'." + ) + return fac(validated_data) + else: + fac = cast('Callable[[], Any]', default_factory) + return fac() + + return PydanticUndefined diff --git a/source/target-vendors/cp312/pydantic/_internal/_forward_ref.py b/source/target-vendors/cp312/pydantic/_internal/_forward_ref.py new file mode 100644 index 0000000..231f81d --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_forward_ref.py @@ -0,0 +1,23 @@ +from __future__ import annotations as _annotations + +from dataclasses import dataclass +from typing import Union + + +@dataclass +class PydanticRecursiveRef: + type_ref: str + + __name__ = 'PydanticRecursiveRef' + __hash__ = object.__hash__ + + def __call__(self) -> None: + """Defining __call__ is necessary for the `typing` module to let you use an instance of + this class as the result of resolving a standard ForwardRef. + """ + + def __or__(self, other): + return Union[self, other] # type: ignore + + def __ror__(self, other): + return Union[other, self] # type: ignore diff --git a/source/target-vendors/cp312/pydantic/_internal/_generate_schema.py b/source/target-vendors/cp312/pydantic/_internal/_generate_schema.py new file mode 100644 index 0000000..98fae40 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_generate_schema.py @@ -0,0 +1,2934 @@ +"""Convert python types to pydantic-core schema.""" + +from __future__ import annotations as _annotations + +import collections.abc +import dataclasses +import datetime +import inspect +import os +import pathlib +import re +import sys +import typing +import warnings +from collections.abc import Generator, Iterable, Iterator, Mapping +from contextlib import contextmanager +from copy import copy +from decimal import Decimal +from enum import Enum +from fractions import Fraction +from functools import partial +from inspect import Parameter, _ParameterKind +from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network +from itertools import chain +from operator import attrgetter +from types import FunctionType, GenericAlias, LambdaType, MethodType +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Final, + ForwardRef, + Literal, + TypeVar, + Union, + cast, + overload, +) +from uuid import UUID +from zoneinfo import ZoneInfo + +import typing_extensions +from pydantic_core import ( + MISSING, + CoreSchema, + MultiHostUrl, + PydanticCustomError, + PydanticSerializationUnexpectedValue, + PydanticUndefined, + Url, + core_schema, + to_jsonable_python, +) +from typing_extensions import TypeAlias, TypeAliasType, get_args, get_origin, is_typeddict +from typing_inspection import typing_objects +from typing_inspection.introspection import AnnotationSource, get_literal_values, is_union_origin + +from ..aliases import AliasChoices, AliasPath +from ..annotated_handlers import GetCoreSchemaHandler, GetJsonSchemaHandler +from ..config import ConfigDict, JsonDict, JsonEncoder, JsonSchemaExtraCallable +from ..errors import ( + PydanticForbiddenQualifier, + PydanticInvalidForJsonSchema, + PydanticSchemaGenerationError, + PydanticUndefinedAnnotation, + PydanticUserError, +) +from ..functional_validators import AfterValidator, BeforeValidator, FieldValidatorModes, PlainValidator, WrapValidator +from ..json_schema import JsonSchemaValue +from ..version import version_short +from ..warnings import ( + ArbitraryTypeWarning, + PydanticDeprecatedSince20, + TypedDictExtraConfigWarning, + UnsupportedFieldAttributeWarning, +) +from . import _decorators, _discriminated_union, _known_annotated_metadata, _repr, _typing_extra +from ._config import ConfigWrapper, ConfigWrapperStack +from ._core_metadata import CoreMetadata, update_core_metadata +from ._core_utils import ( + get_ref, + get_type_ref, + is_list_like_schema_with_items_schema, +) +from ._decorators import ( + Decorator, + DecoratorInfos, + FieldSerializerDecoratorInfo, + FieldValidatorDecoratorInfo, + ModelSerializerDecoratorInfo, + ModelValidatorDecoratorInfo, + RootValidatorDecoratorInfo, + ValidatorDecoratorInfo, + get_attribute_from_bases, + inspect_field_serializer, + inspect_model_serializer, + inspect_validator, +) +from ._docs_extraction import extract_docstrings_from_cls +from ._fields import ( + collect_dataclass_fields, + rebuild_dataclass_fields, + rebuild_model_fields, + takes_validated_data_argument, + update_field_from_config, +) +from ._forward_ref import PydanticRecursiveRef +from ._generics import get_standard_typevars_map, replace_types +from ._import_utils import import_cached_base_model, import_cached_field_info +from ._mock_val_ser import MockCoreSchema +from ._namespace_utils import NamespacesTuple, NsResolver +from ._schema_gather import MissingDefinitionError, gather_schemas_for_cleaning +from ._schema_generation_shared import CallbackGetCoreSchemaHandler +from ._utils import lenient_issubclass, smart_deepcopy + +if TYPE_CHECKING: + from ..fields import ComputedFieldInfo, FieldInfo + from ..main import BaseModel + from ..types import Discriminator + from ._dataclasses import StandardDataclass + from ._schema_generation_shared import GetJsonSchemaFunction + +_SUPPORTS_TYPEDDICT = sys.version_info >= (3, 12) + +FieldDecoratorInfo = Union[ValidatorDecoratorInfo, FieldValidatorDecoratorInfo, FieldSerializerDecoratorInfo] +FieldDecoratorInfoType = TypeVar('FieldDecoratorInfoType', bound=FieldDecoratorInfo) +AnyFieldDecorator = Union[ + Decorator[ValidatorDecoratorInfo], + Decorator[FieldValidatorDecoratorInfo], + Decorator[FieldSerializerDecoratorInfo], +] + +ModifyCoreSchemaWrapHandler: TypeAlias = GetCoreSchemaHandler +GetCoreSchemaFunction: TypeAlias = Callable[[Any, ModifyCoreSchemaWrapHandler], core_schema.CoreSchema] +ParametersCallback: TypeAlias = "Callable[[int, str, Any], Literal['skip'] | None]" + +TUPLE_TYPES: list[type] = [typing.Tuple, tuple] # noqa: UP006 +LIST_TYPES: list[type] = [typing.List, list, collections.abc.MutableSequence] # noqa: UP006 +SET_TYPES: list[type] = [typing.Set, set, collections.abc.MutableSet] # noqa: UP006 +FROZEN_SET_TYPES: list[type] = [typing.FrozenSet, frozenset, collections.abc.Set] # noqa: UP006 +DICT_TYPES: list[type] = [typing.Dict, dict] # noqa: UP006 +IP_TYPES: list[type] = [IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network] +SEQUENCE_TYPES: list[type] = [typing.Sequence, collections.abc.Sequence] +ITERABLE_TYPES: list[type] = [typing.Iterable, collections.abc.Iterable, typing.Generator, collections.abc.Generator] +TYPE_TYPES: list[type] = [typing.Type, type] # noqa: UP006 +PATTERN_TYPES: list[type] = [typing.Pattern, re.Pattern] +PATH_TYPES: list[type] = [ + os.PathLike, + pathlib.Path, + pathlib.PurePath, + pathlib.PosixPath, + pathlib.PurePosixPath, + pathlib.PureWindowsPath, +] +MAPPING_TYPES = [ + typing.Mapping, + typing.MutableMapping, + collections.abc.Mapping, + collections.abc.MutableMapping, + collections.OrderedDict, + typing_extensions.OrderedDict, + typing.DefaultDict, # noqa: UP006 + collections.defaultdict, +] +COUNTER_TYPES = [collections.Counter, typing.Counter] +DEQUE_TYPES: list[type] = [collections.deque, typing.Deque] # noqa: UP006 + +# Note: This does not play very well with type checkers. For example, +# `a: LambdaType = lambda x: x` will raise a type error by Pyright. +ValidateCallSupportedTypes = Union[ + LambdaType, + FunctionType, + MethodType, + partial, +] + +VALIDATE_CALL_SUPPORTED_TYPES = get_args(ValidateCallSupportedTypes) +UNSUPPORTED_STANDALONE_FIELDINFO_ATTRIBUTES: list[tuple[str, Any]] = [ + ('alias', None), + ('validation_alias', None), + ('serialization_alias', None), + # will be set if any alias is set, so disable it to avoid double warnings: + # 'alias_priority', + ('default', PydanticUndefined), + ('default_factory', None), + ('exclude', None), + ('deprecated', None), + ('repr', True), + ('validate_default', None), + ('frozen', None), + ('init', None), + ('init_var', None), + ('kw_only', None), +] +"""`FieldInfo` attributes (and their default value) that can't be used outside of a model (e.g. in a type adapter or a PEP 695 type alias).""" + +_mode_to_validator: dict[ + FieldValidatorModes, type[BeforeValidator | AfterValidator | PlainValidator | WrapValidator] +] = {'before': BeforeValidator, 'after': AfterValidator, 'plain': PlainValidator, 'wrap': WrapValidator} + + +def check_validator_fields_against_field_name( + info: FieldDecoratorInfo, + field: str, +) -> bool: + """Check if field name is in validator fields. + + Args: + info: The field info. + field: The field name to check. + + Returns: + `True` if field name is in validator fields, `False` otherwise. + """ + fields = info.fields + return '*' in fields or field in fields + + +def check_decorator_fields_exist(decorators: Iterable[AnyFieldDecorator], fields: Iterable[str]) -> None: + """Check if the defined fields in decorators exist in `fields` param. + + It ignores the check for a decorator if the decorator has `*` as field or `check_fields=False`. + + Args: + decorators: An iterable of decorators. + fields: An iterable of fields name. + + Raises: + PydanticUserError: If one of the field names does not exist in `fields` param. + """ + fields = set(fields) + for dec in decorators: + if '*' in dec.info.fields: + continue + if dec.info.check_fields is False: + continue + for field in dec.info.fields: + if field not in fields: + raise PydanticUserError( + f'Decorators defined with incorrect fields: {dec.cls_ref}.{dec.cls_var_name}' + " (use check_fields=False if you're inheriting from the model and intended this)", + code='decorator-missing-field', + ) + + +def filter_field_decorator_info_by_field( + validator_functions: Iterable[Decorator[FieldDecoratorInfoType]], field: str +) -> list[Decorator[FieldDecoratorInfoType]]: + return [dec for dec in validator_functions if check_validator_fields_against_field_name(dec.info, field)] + + +def apply_each_item_validators( + schema: core_schema.CoreSchema, + each_item_validators: list[Decorator[ValidatorDecoratorInfo]], +) -> core_schema.CoreSchema: + # This V1 compatibility shim should eventually be removed + + # fail early if each_item_validators is empty + if not each_item_validators: + return schema + + # push down any `each_item=True` validators + # note that this won't work for any Annotated types that get wrapped by a function validator + # but that's okay because that didn't exist in V1 + if schema['type'] == 'nullable': + schema['schema'] = apply_each_item_validators(schema['schema'], each_item_validators) + return schema + elif schema['type'] == 'tuple': + if (variadic_item_index := schema.get('variadic_item_index')) is not None: + schema['items_schema'][variadic_item_index] = apply_validators( + schema['items_schema'][variadic_item_index], + each_item_validators, + ) + elif is_list_like_schema_with_items_schema(schema): + inner_schema = schema.get('items_schema', core_schema.any_schema()) + schema['items_schema'] = apply_validators(inner_schema, each_item_validators) + elif schema['type'] == 'dict': + inner_schema = schema.get('values_schema', core_schema.any_schema()) + schema['values_schema'] = apply_validators(inner_schema, each_item_validators) + else: + raise TypeError( + f'`@validator(..., each_item=True)` cannot be applied to fields with a schema of {schema["type"]}' + ) + return schema + + +def _extract_json_schema_info_from_field_info( + info: FieldInfo | ComputedFieldInfo, +) -> tuple[JsonDict | None, JsonDict | JsonSchemaExtraCallable | None]: + json_schema_updates = { + 'title': info.title, + 'description': info.description, + 'deprecated': bool(info.deprecated) or info.deprecated == '' or None, + 'examples': to_jsonable_python(info.examples), + } + json_schema_updates = {k: v for k, v in json_schema_updates.items() if v is not None} + return (json_schema_updates or None, info.json_schema_extra) + + +JsonEncoders = dict[type[Any], JsonEncoder] + + +def _add_custom_serialization_from_json_encoders( + json_encoders: JsonEncoders | None, tp: Any, schema: CoreSchema +) -> CoreSchema: + """Iterate over the json_encoders and add the first matching encoder to the schema. + + Args: + json_encoders: A dictionary of types and their encoder functions. + tp: The type to check for a matching encoder. + schema: The schema to add the encoder to. + """ + if not json_encoders: + return schema + if 'serialization' in schema: + return schema + # Check the class type and its superclasses for a matching encoder + # Decimal.__class__.__mro__ (and probably other cases) doesn't include Decimal itself + # if the type is a GenericAlias (e.g. from list[int]) we need to use __class__ instead of .__mro__ + for base in (tp, *getattr(tp, '__mro__', tp.__class__.__mro__)[:-1]): + encoder = json_encoders.get(base) + if encoder is None: + continue + + warnings.warn( + f'`json_encoders` is deprecated. See https://docs.pydantic.dev/{version_short()}/concepts/serialization/#custom-serializers for alternatives', + PydanticDeprecatedSince20, + ) + + # TODO: in theory we should check that the schema accepts a serialization key + schema['serialization'] = core_schema.plain_serializer_function_ser_schema(encoder, when_used='json') + return schema + + return schema + + +GENERATE_SCHEMA_ERRORS = ( + PydanticForbiddenQualifier, + PydanticInvalidForJsonSchema, + PydanticSchemaGenerationError, + PydanticUndefinedAnnotation, +) +"""Errors raised during core schema generation. This does *not* include `InvalidSchemaError`, which is raised during schema cleaning.""" + + +class InvalidSchemaError(Exception): + """The core schema is invalid.""" + + +class GenerateSchema: + """Generate core schema for a Pydantic model, dataclass and types like `str`, `datetime`, ... .""" + + __slots__ = ( + '_config_wrapper_stack', + '_ns_resolver', + '_typevars_map', + 'field_name_stack', + 'model_type_stack', + 'defs', + ) + + def __init__( + self, + config_wrapper: ConfigWrapper, + ns_resolver: NsResolver | None = None, + typevars_map: Mapping[TypeVar, Any] | None = None, + ) -> None: + # we need a stack for recursing into nested models + self._config_wrapper_stack = ConfigWrapperStack(config_wrapper) + self._ns_resolver = ns_resolver or NsResolver() + self._typevars_map = typevars_map + self.field_name_stack = _FieldNameStack() + self.model_type_stack = _ModelTypeStack() + self.defs = _Definitions() + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + warnings.warn( + 'Subclassing `GenerateSchema` is not supported. The API is highly subject to change in minor versions.', + UserWarning, + stacklevel=2, + ) + + @property + def _config_wrapper(self) -> ConfigWrapper: + return self._config_wrapper_stack.tail + + @property + def _types_namespace(self) -> NamespacesTuple: + return self._ns_resolver.types_namespace + + @property + def _arbitrary_types(self) -> bool: + return self._config_wrapper.arbitrary_types_allowed + + # the following methods can be overridden but should be considered + # unstable / private APIs + def _list_schema(self, items_type: Any) -> CoreSchema: + return core_schema.list_schema(self.generate_schema(items_type)) + + def _dict_schema(self, keys_type: Any, values_type: Any) -> CoreSchema: + return core_schema.dict_schema(self.generate_schema(keys_type), self.generate_schema(values_type)) + + def _set_schema(self, items_type: Any) -> CoreSchema: + return core_schema.set_schema(self.generate_schema(items_type)) + + def _frozenset_schema(self, items_type: Any) -> CoreSchema: + return core_schema.frozenset_schema(self.generate_schema(items_type)) + + def _enum_schema(self, enum_type: type[Enum]) -> CoreSchema: + cases: list[Any] = list(enum_type.__members__.values()) + + enum_ref = get_type_ref(enum_type) + description = None if not enum_type.__doc__ else inspect.cleandoc(enum_type.__doc__) + if ( + description == 'An enumeration.' + ): # This is the default value provided by enum.EnumMeta.__new__; don't use it + description = None + js_updates = {'title': enum_type.__name__, 'description': description} + js_updates = {k: v for k, v in js_updates.items() if v is not None} + + sub_type: Literal['str', 'int', 'float'] | None = None + if issubclass(enum_type, int): + sub_type = 'int' + value_ser_type: core_schema.SerSchema = core_schema.simple_ser_schema('int') + elif issubclass(enum_type, str): + # this handles `StrEnum` (3.11 only), and also `Foobar(str, Enum)` + sub_type = 'str' + value_ser_type = core_schema.simple_ser_schema('str') + elif issubclass(enum_type, float): + sub_type = 'float' + value_ser_type = core_schema.simple_ser_schema('float') + else: + # TODO this is an ugly hack, how do we trigger an Any schema for serialization? + value_ser_type = core_schema.plain_serializer_function_ser_schema(lambda x: x) + + if cases: + + def get_json_schema(schema: CoreSchema, handler: GetJsonSchemaHandler) -> JsonSchemaValue: + json_schema = handler(schema) + original_schema = handler.resolve_ref_schema(json_schema) + original_schema.update(js_updates) + return json_schema + + # we don't want to add the missing to the schema if it's the default one + default_missing = getattr(enum_type._missing_, '__func__', None) is Enum._missing_.__func__ # pyright: ignore[reportFunctionMemberAccess] + enum_schema = core_schema.enum_schema( + enum_type, + cases, + sub_type=sub_type, + missing=None if default_missing else enum_type._missing_, + ref=enum_ref, + metadata={'pydantic_js_functions': [get_json_schema]}, + ) + + if self._config_wrapper.use_enum_values: + enum_schema = core_schema.no_info_after_validator_function( + attrgetter('value'), enum_schema, serialization=value_ser_type + ) + + return enum_schema + + else: + + def get_json_schema_no_cases(_, handler: GetJsonSchemaHandler) -> JsonSchemaValue: + json_schema = handler(core_schema.enum_schema(enum_type, cases, sub_type=sub_type, ref=enum_ref)) + original_schema = handler.resolve_ref_schema(json_schema) + original_schema.update(js_updates) + return json_schema + + # Use an isinstance check for enums with no cases. + # The most important use case for this is creating TypeVar bounds for generics that should + # be restricted to enums. This is more consistent than it might seem at first, since you can only + # subclass enum.Enum (or subclasses of enum.Enum) if all parent classes have no cases. + # We use the get_json_schema function when an Enum subclass has been declared with no cases + # so that we can still generate a valid json schema. + return core_schema.is_instance_schema( + enum_type, + metadata={'pydantic_js_functions': [get_json_schema_no_cases]}, + ) + + def _ip_schema(self, tp: Any) -> CoreSchema: + from ._validators import IP_VALIDATOR_LOOKUP, IpType + + ip_type_json_schema_format: dict[type[IpType], str] = { + IPv4Address: 'ipv4', + IPv4Network: 'ipv4network', + IPv4Interface: 'ipv4interface', + IPv6Address: 'ipv6', + IPv6Network: 'ipv6network', + IPv6Interface: 'ipv6interface', + } + + def ser_ip(ip: Any, info: core_schema.SerializationInfo) -> str | IpType: + if not isinstance(ip, (tp, str)): + raise PydanticSerializationUnexpectedValue( + f"Expected `{tp}` but got `{type(ip)}` with value `'{ip}'` - serialized value may not be as expected." + ) + if info.mode == 'python': + return ip + return str(ip) + + return core_schema.lax_or_strict_schema( + lax_schema=core_schema.no_info_plain_validator_function(IP_VALIDATOR_LOOKUP[tp]), + strict_schema=core_schema.json_or_python_schema( + json_schema=core_schema.no_info_after_validator_function(tp, core_schema.str_schema()), + python_schema=core_schema.is_instance_schema(tp), + ), + serialization=core_schema.plain_serializer_function_ser_schema(ser_ip, info_arg=True, when_used='always'), + metadata={ + 'pydantic_js_functions': [lambda _1, _2: {'type': 'string', 'format': ip_type_json_schema_format[tp]}] + }, + ) + + def _path_schema(self, tp: Any, path_type: Any) -> CoreSchema: + if tp is os.PathLike and (path_type not in {str, bytes} and not typing_objects.is_any(path_type)): + raise PydanticUserError( + '`os.PathLike` can only be used with `str`, `bytes` or `Any`', code='schema-for-unknown-type' + ) + + path_constructor = pathlib.PurePath if tp is os.PathLike else tp + strict_inner_schema = ( + core_schema.bytes_schema(strict=True) if (path_type is bytes) else core_schema.str_schema(strict=True) + ) + lax_inner_schema = core_schema.bytes_schema() if (path_type is bytes) else core_schema.str_schema() + + def path_validator(input_value: str | bytes) -> os.PathLike[Any]: # type: ignore + try: + if path_type is bytes: + if isinstance(input_value, bytes): + try: + input_value = input_value.decode() + except UnicodeDecodeError as e: + raise PydanticCustomError('bytes_type', 'Input must be valid bytes') from e + else: + raise PydanticCustomError('bytes_type', 'Input must be bytes') + elif not isinstance(input_value, str): + raise PydanticCustomError('path_type', 'Input is not a valid path') + + return path_constructor(input_value) # type: ignore + except TypeError as e: + raise PydanticCustomError('path_type', 'Input is not a valid path') from e + + def ser_path(path: Any, info: core_schema.SerializationInfo) -> str | os.PathLike[Any]: + if not isinstance(path, (tp, str)): + raise PydanticSerializationUnexpectedValue( + f"Expected `{tp}` but got `{type(path)}` with value `'{path}'` - serialized value may not be as expected." + ) + if info.mode == 'python': + return path + return str(path) + + instance_schema = core_schema.json_or_python_schema( + json_schema=core_schema.no_info_after_validator_function(path_validator, lax_inner_schema), + python_schema=core_schema.is_instance_schema(tp), + ) + + schema = core_schema.lax_or_strict_schema( + lax_schema=core_schema.union_schema( + [ + instance_schema, + core_schema.no_info_after_validator_function(path_validator, strict_inner_schema), + ], + custom_error_type='path_type', + custom_error_message=f'Input is not a valid path for {tp}', + ), + strict_schema=instance_schema, + serialization=core_schema.plain_serializer_function_ser_schema(ser_path, info_arg=True, when_used='always'), + metadata={'pydantic_js_functions': [lambda source, handler: {**handler(source), 'format': 'path'}]}, + ) + return schema + + def _deque_schema(self, items_type: Any) -> CoreSchema: + from ._serializers import serialize_sequence_via_list + from ._validators import deque_validator + + item_type_schema = self.generate_schema(items_type) + + # we have to use a lax list schema here, because we need to validate the deque's + # items via a list schema, but it's ok if the deque itself is not a list + list_schema = core_schema.list_schema(item_type_schema, strict=False) + + check_instance = core_schema.json_or_python_schema( + json_schema=list_schema, + python_schema=core_schema.is_instance_schema(collections.deque, cls_repr='Deque'), + ) + + lax_schema = core_schema.no_info_wrap_validator_function(deque_validator, list_schema) + + return core_schema.lax_or_strict_schema( + lax_schema=lax_schema, + strict_schema=core_schema.chain_schema([check_instance, lax_schema]), + serialization=core_schema.wrap_serializer_function_ser_schema( + serialize_sequence_via_list, schema=item_type_schema, info_arg=True + ), + ) + + def _mapping_schema(self, tp: Any, keys_type: Any, values_type: Any) -> CoreSchema: + from ._validators import MAPPING_ORIGIN_MAP, defaultdict_validator, get_defaultdict_default_default_factory + + mapped_origin = MAPPING_ORIGIN_MAP[tp] + keys_schema = self.generate_schema(keys_type) + with warnings.catch_warnings(): + # We kind of abused `Field()` default factories to be able to specify + # the `defaultdict`'s `default_factory`. As a consequence, we get warnings + # as normally `FieldInfo.default_factory` is unsupported in the context where + # `Field()` is used and our only solution is to ignore them (note that this might + # wrongfully ignore valid warnings, e.g. if the `value_type` is a PEP 695 type alias + # with unsupported metadata). + warnings.simplefilter('ignore', category=UnsupportedFieldAttributeWarning) + values_schema = self.generate_schema(values_type) + dict_schema = core_schema.dict_schema(keys_schema, values_schema, strict=False) + + if mapped_origin is dict: + schema = dict_schema + else: + check_instance = core_schema.json_or_python_schema( + json_schema=dict_schema, + python_schema=core_schema.is_instance_schema(mapped_origin), + ) + + if tp is collections.defaultdict: + default_default_factory = get_defaultdict_default_default_factory(values_type) + coerce_instance_wrap = partial( + core_schema.no_info_wrap_validator_function, + partial(defaultdict_validator, default_default_factory=default_default_factory), + ) + else: + coerce_instance_wrap = partial(core_schema.no_info_after_validator_function, mapped_origin) + + lax_schema = coerce_instance_wrap(dict_schema) + strict_schema = core_schema.chain_schema([check_instance, lax_schema]) + + schema = core_schema.lax_or_strict_schema( + lax_schema=lax_schema, + strict_schema=strict_schema, + serialization=core_schema.wrap_serializer_function_ser_schema( + lambda v, h: h(v), schema=dict_schema, info_arg=False + ), + ) + + return schema + + def _fraction_schema(self) -> CoreSchema: + """Support for [`fractions.Fraction`][fractions.Fraction].""" + from ._validators import fraction_validator + + # TODO: note, this is a fairly common pattern, re lax / strict for attempted type coercion, + # can we use a helper function to reduce boilerplate? + return core_schema.lax_or_strict_schema( + lax_schema=core_schema.no_info_plain_validator_function(fraction_validator), + strict_schema=core_schema.json_or_python_schema( + json_schema=core_schema.no_info_plain_validator_function(fraction_validator), + python_schema=core_schema.is_instance_schema(Fraction), + ), + # use str serialization to guarantee round trip behavior + serialization=core_schema.to_string_ser_schema(when_used='always'), + metadata={'pydantic_js_functions': [lambda _1, _2: {'type': 'string', 'format': 'fraction'}]}, + ) + + def _arbitrary_type_schema(self, tp: Any) -> CoreSchema: + if not isinstance(tp, type): + warnings.warn( + f'{tp!r} is not a Python type (it may be an instance of an object),' + ' Pydantic will allow any object with no validation since we cannot even' + ' enforce that the input is an instance of the given type.' + ' To get rid of this error wrap the type with `pydantic.SkipValidation`.', + ArbitraryTypeWarning, + ) + return core_schema.any_schema() + return core_schema.is_instance_schema(tp) + + def _unknown_type_schema(self, obj: Any) -> CoreSchema: + raise PydanticSchemaGenerationError( + f'Unable to generate pydantic-core schema for {obj!r}. ' + 'Set `arbitrary_types_allowed=True` in the model_config to ignore this error' + ' or implement `__get_pydantic_core_schema__` on your type to fully support it.' + '\n\nIf you got this error by calling handler() within' + ' `__get_pydantic_core_schema__` then you likely need to call' + ' `handler.generate_schema()` since we do not call' + ' `__get_pydantic_core_schema__` on `` otherwise to avoid infinite recursion.' + ) + + def _apply_discriminator_to_union( + self, schema: CoreSchema, discriminator: str | Discriminator | None + ) -> CoreSchema: + if discriminator is None: + return schema + try: + return _discriminated_union.apply_discriminator( + schema, + discriminator, + self.defs._definitions, + ) + except _discriminated_union.MissingDefinitionForUnionRef: + # defer until defs are resolved + _discriminated_union.set_discriminator_in_metadata( + schema, + discriminator, + ) + return schema + + def clean_schema(self, schema: CoreSchema) -> CoreSchema: + return self.defs.finalize_schema(schema) + + def _add_js_function(self, metadata_schema: CoreSchema, js_function: Callable[..., Any]) -> None: + metadata = metadata_schema.get('metadata', {}) + pydantic_js_functions = metadata.setdefault('pydantic_js_functions', []) + # because of how we generate core schemas for nested generic models + # we can end up adding `BaseModel.__get_pydantic_json_schema__` multiple times + # this check may fail to catch duplicates if the function is a `functools.partial` + # or something like that, but if it does it'll fail by inserting the duplicate + if js_function not in pydantic_js_functions: + pydantic_js_functions.append(js_function) + metadata_schema['metadata'] = metadata + + def generate_schema( + self, + obj: Any, + ) -> core_schema.CoreSchema: + """Generate core schema. + + Args: + obj: The object to generate core schema for. + + Returns: + The generated core schema. + + Raises: + PydanticUndefinedAnnotation: + If it is not possible to evaluate forward reference. + PydanticSchemaGenerationError: + If it is not possible to generate pydantic-core schema. + TypeError: + - If `alias_generator` returns a disallowed type (must be str, AliasPath or AliasChoices). + - If V1 style validator with `each_item=True` applied on a wrong field. + PydanticUserError: + - If `typing.TypedDict` is used instead of `typing_extensions.TypedDict` on Python < 3.12. + - If `__modify_schema__` method is used instead of `__get_pydantic_json_schema__`. + """ + schema = self._generate_schema_from_get_schema_method(obj, obj) + + if schema is None: + schema = self._generate_schema_inner(obj) + + metadata_js_function = _extract_get_pydantic_json_schema(obj) + if metadata_js_function is not None: + metadata_schema = resolve_original_schema(schema, self.defs) + if metadata_schema: + self._add_js_function(metadata_schema, metadata_js_function) + + schema = _add_custom_serialization_from_json_encoders(self._config_wrapper.json_encoders, obj, schema) + + return schema + + def _model_schema(self, cls: type[BaseModel]) -> core_schema.CoreSchema: + """Generate schema for a Pydantic model.""" + BaseModel_ = import_cached_base_model() + + with self.defs.get_schema_or_ref(cls) as (model_ref, maybe_schema): + if maybe_schema is not None: + return maybe_schema + + schema = cls.__dict__.get('__pydantic_core_schema__') + if schema is not None and not isinstance(schema, MockCoreSchema): + if schema['type'] == 'definitions': + schema = self.defs.unpack_definitions(schema) + ref = get_ref(schema) + if ref: + return self.defs.create_definition_reference_schema(schema) + else: + return schema + + config_wrapper = ConfigWrapper(cls.model_config, check=False) + + with self._config_wrapper_stack.push(config_wrapper), self._ns_resolver.push(cls): + core_config = self._config_wrapper.core_config(title=cls.__name__) + + if cls.__pydantic_fields_complete__ or cls is BaseModel_: + fields = getattr(cls, '__pydantic_fields__', {}) + extra_info = getattr(cls, '__pydantic_extra_info__', None) + else: + if '__pydantic_fields__' not in cls.__dict__: + # This happens when we have a loop in the schema generation: + # class Base[T](BaseModel): + # t: T + # + # class Other(BaseModel): + # b: 'Base[Other]' + # When we build fields for `Other`, we evaluate the forward annotation. + # At this point, `Other` doesn't have the model fields set. We create + # `Base[Other]`; model fields are successfully built, and we try to generate + # a schema for `t: Other`. As `Other.__pydantic_fields__` aren't set, we abort. + raise PydanticUndefinedAnnotation( + name=cls.__name__, + message=f'Class {cls.__name__!r} is not defined', + ) + try: + fields, extra_info = rebuild_model_fields( + cls, + config_wrapper=self._config_wrapper, + ns_resolver=self._ns_resolver, + typevars_map=self._typevars_map or {}, + ) + except NameError as e: + raise PydanticUndefinedAnnotation.from_name_error(e) from e + + decorators = cls.__pydantic_decorators__ + computed_fields = decorators.computed_fields + check_decorator_fields_exist( + chain( + decorators.field_validators.values(), + decorators.field_serializers.values(), + decorators.validators.values(), + ), + {*fields.keys(), *computed_fields.keys()}, + ) + + model_validators = decorators.model_validators.values() + + extras_schema = None + extras_keys_schema = None + if core_config.get('extra_fields_behavior') == 'allow' and extra_info is not None: + tp = get_origin(extra_info.annotation) + if tp not in DICT_TYPES: + raise PydanticSchemaGenerationError( + 'The type annotation for `__pydantic_extra__` must be `dict[str, ...]`' + ) + # See the comments in `_get_args_resolving_forward_refs()` for why we need + # to re-evaluate the annotation: + extra_keys_type, extra_items_type = self._get_args_resolving_forward_refs( + extra_info.annotation, + required=True, + ) + if extra_keys_type is not str: + extras_keys_schema = self.generate_schema(extra_keys_type) + if not typing_objects.is_any(extra_items_type): + extras_schema = self.generate_schema(extra_items_type) + + generic_origin: type[BaseModel] | None = getattr(cls, '__pydantic_generic_metadata__', {}).get('origin') + + if cls.__pydantic_root_model__: + inner_schema, metadata = self._common_field_schema('root', fields['root'], decorators) + if cls.__doc__ and metadata.get('pydantic_js_updates', {}).get('description'): + # This is a bit of a leaky abstraction, but as the model docstring takes priority + # over the root field's description, we need to override it here. This can't be done + # in the JSON Schema generation logic because the metadata's `pydantic_js_updates` are + # applied last, and overrides any value previously set (so the description set from the + # docstring in `GenerateJsonSchema._update_class_schema()` is overridden): + update_core_metadata( + metadata, pydantic_js_updates={'description': inspect.cleandoc(cls.__doc__)} + ) + + inner_schema = apply_model_validators(inner_schema, model_validators, 'inner') + model_schema = core_schema.model_schema( + cls, + inner_schema, + generic_origin=generic_origin, + custom_init=getattr(cls, '__pydantic_custom_init__', None), + root_model=True, + post_init=getattr(cls, '__pydantic_post_init__', None), + config=core_config, + ref=model_ref, + metadata=metadata, + ) + else: + fields_schema: core_schema.CoreSchema = core_schema.model_fields_schema( + {k: self._generate_md_field_schema(k, v, decorators) for k, v in fields.items()}, + computed_fields=[ + self._computed_field_schema(d, decorators.field_serializers) + for d in computed_fields.values() + ], + extras_schema=extras_schema, + extras_keys_schema=extras_keys_schema, + model_name=cls.__name__, + ) + inner_schema = apply_validators(fields_schema, decorators.root_validators.values()) + inner_schema = apply_model_validators(inner_schema, model_validators, 'inner') + + model_schema = core_schema.model_schema( + cls, + inner_schema, + generic_origin=generic_origin, + custom_init=getattr(cls, '__pydantic_custom_init__', None), + root_model=False, + post_init=getattr(cls, '__pydantic_post_init__', None), + config=core_config, + ref=model_ref, + ) + + schema = self._apply_model_serializers(model_schema, decorators.model_serializers.values()) + schema = apply_model_validators(schema, model_validators, 'outer') + return self.defs.create_definition_reference_schema(schema) + + def _resolve_self_type(self, obj: Any) -> Any: + obj = self.model_type_stack.get() + if obj is None: + raise PydanticUserError('`typing.Self` is invalid in this context', code='invalid-self-type') + return obj + + def _generate_schema_from_get_schema_method(self, obj: Any, source: Any) -> core_schema.CoreSchema | None: + BaseModel_ = import_cached_base_model() + + get_schema = getattr(obj, '__get_pydantic_core_schema__', None) + is_base_model_get_schema = ( + getattr(get_schema, '__func__', None) is BaseModel_.__get_pydantic_core_schema__.__func__ # pyright: ignore[reportFunctionMemberAccess] + ) + + if ( + get_schema is not None + # BaseModel.__get_pydantic_core_schema__ is defined for backwards compatibility, + # to allow existing code to call `super().__get_pydantic_core_schema__` in Pydantic + # model that overrides `__get_pydantic_core_schema__`. However, it raises a deprecation + # warning stating that the method will be removed, and during the core schema gen we actually + # don't call the method: + and not is_base_model_get_schema + ): + # Some referenceable types might have a `__get_pydantic_core_schema__` method + # defined on it by users (e.g. on a dataclass). This generally doesn't play well + # as these types are already recognized by the `GenerateSchema` class and isn't ideal + # as we might end up calling `get_schema_or_ref` (expensive) on types that are actually + # not referenceable: + with self.defs.get_schema_or_ref(obj) as (_, maybe_schema): + if maybe_schema is not None: + return maybe_schema + + if obj is source: + ref_mode = 'unpack' + else: + ref_mode = 'to-def' + schema = get_schema( + source, CallbackGetCoreSchemaHandler(self._generate_schema_inner, self, ref_mode=ref_mode) + ) + if schema['type'] == 'definitions': + schema = self.defs.unpack_definitions(schema) + + ref = get_ref(schema) + if ref: + return self.defs.create_definition_reference_schema(schema) + + # Note: if schema is of type `'definition-ref'`, we might want to copy it as a + # safety measure (because these are inlined in place -- i.e. mutated directly) + return schema + + if get_schema is None and (validators := getattr(obj, '__get_validators__', None)) is not None: + from pydantic.v1 import BaseModel as BaseModelV1 + + if issubclass(obj, BaseModelV1): + warnings.warn( + f'Mixing V1 models and V2 models (or constructs, like `TypeAdapter`) is not supported. Please upgrade `{obj.__name__}` to V2.', + UserWarning, + ) + else: + warnings.warn( + '`__get_validators__` is deprecated and will be removed, use `__get_pydantic_core_schema__` instead.', + PydanticDeprecatedSince20, + ) + return core_schema.chain_schema([core_schema.with_info_plain_validator_function(v) for v in validators()]) + + def _resolve_forward_ref(self, obj: Any) -> Any: + # we assume that types_namespace has the target of forward references in its scope, + # but this could fail, for example, if calling Validator on an imported type which contains + # forward references to other types only defined in the module from which it was imported + # `Validator(SomeImportedTypeAliasWithAForwardReference)` + # or the equivalent for BaseModel + # class Model(BaseModel): + # x: SomeImportedTypeAliasWithAForwardReference + try: + obj = _typing_extra.eval_type_backport(obj, *self._types_namespace) + except NameError as e: + raise PydanticUndefinedAnnotation.from_name_error(e) from e + + # if obj is still a ForwardRef, it means we can't evaluate it, raise PydanticUndefinedAnnotation + if isinstance(obj, ForwardRef): + raise PydanticUndefinedAnnotation(obj.__forward_arg__, f'Unable to evaluate forward reference {obj}') + + if self._typevars_map: + obj = replace_types(obj, self._typevars_map) + + return obj + + @overload + def _get_args_resolving_forward_refs(self, obj: Any, required: Literal[True]) -> tuple[Any, ...]: ... + + @overload + def _get_args_resolving_forward_refs(self, obj: Any) -> tuple[Any, ...] | None: ... + + def _get_args_resolving_forward_refs(self, obj: Any, required: bool = False) -> tuple[Any, ...] | None: + args = get_args(obj) + if args: + if isinstance(obj, GenericAlias): + # PEP 585 generic aliases don't convert args to ForwardRefs, unlike `typing.List/Dict` etc. + # This was fixed in https://github.com/python/cpython/pull/30900 (Python 3.11). + # TODO: this shouldn't be necessary (probably even this `_get_args_resolving_forward_refs()` function) + # once we drop support for Python 3.10 *or* if we implement our own `typing._eval_type()` implementation. + args = (_typing_extra._make_forward_ref(a) if isinstance(a, str) else a for a in args) + args = tuple(self._resolve_forward_ref(a) if isinstance(a, ForwardRef) else a for a in args) + elif required: # pragma: no cover + raise TypeError(f'Expected {obj} to have generic parameters but it had none') + return args + + def _get_first_arg_or_any(self, obj: Any) -> Any: + args = self._get_args_resolving_forward_refs(obj) + if not args: + return Any + return args[0] + + def _get_first_two_args_or_any(self, obj: Any) -> tuple[Any, Any]: + args = self._get_args_resolving_forward_refs(obj) + if not args: + return (Any, Any) + if len(args) < 2: + origin = get_origin(obj) + raise TypeError(f'Expected two type arguments for {origin}, got 1') + return args[0], args[1] + + def _generate_schema_inner(self, obj: Any) -> core_schema.CoreSchema: + if typing_objects.is_self(obj): + obj = self._resolve_self_type(obj) + + if typing_objects.is_annotated(get_origin(obj)): + return self._annotated_schema(obj) + + if isinstance(obj, dict): + # we assume this is already a valid schema + return obj # type: ignore[return-value] + + if isinstance(obj, str): + obj = ForwardRef(obj) + + if isinstance(obj, ForwardRef): + return self.generate_schema(self._resolve_forward_ref(obj)) + + BaseModel = import_cached_base_model() + + if lenient_issubclass(obj, BaseModel): + with self.model_type_stack.push(obj): + return self._model_schema(obj) + + if isinstance(obj, PydanticRecursiveRef): + return core_schema.definition_reference_schema(schema_ref=obj.type_ref) + + return self.match_type(obj) + + def match_type(self, obj: Any) -> core_schema.CoreSchema: # noqa: C901 + """Main mapping of types to schemas. + + The general structure is a series of if statements starting with the simple cases + (non-generic primitive types) and then handling generics and other more complex cases. + + Each case either generates a schema directly, calls into a public user-overridable method + (like `GenerateSchema.tuple_variable_schema`) or calls into a private method that handles some + boilerplate before calling into the user-facing method (e.g. `GenerateSchema._tuple_schema`). + + The idea is that we'll evolve this into adding more and more user facing methods over time + as they get requested and we figure out what the right API for them is. + """ + if obj is str: + return core_schema.str_schema() + elif obj is bytes: + return core_schema.bytes_schema() + elif obj is int: + return core_schema.int_schema() + elif obj is float: + return core_schema.float_schema() + elif obj is bool: + return core_schema.bool_schema() + elif obj is complex: + return core_schema.complex_schema() + elif typing_objects.is_any(obj) or obj is object: + return core_schema.any_schema() + elif obj is datetime.date: + return core_schema.date_schema() + elif obj is datetime.datetime: + return core_schema.datetime_schema() + elif obj is datetime.time: + return core_schema.time_schema() + elif obj is datetime.timedelta: + return core_schema.timedelta_schema() + elif obj is Decimal: + return core_schema.decimal_schema() + elif obj is UUID: + return core_schema.uuid_schema() + elif obj is Url: + return core_schema.url_schema() + elif obj is Fraction: + return self._fraction_schema() + elif obj is MultiHostUrl: + return core_schema.multi_host_url_schema() + elif obj is None or obj is _typing_extra.NoneType: + return core_schema.none_schema() + if obj is MISSING: + return core_schema.missing_sentinel_schema() + elif obj in IP_TYPES: + return self._ip_schema(obj) + elif obj in TUPLE_TYPES: + return self._tuple_schema(obj) + elif obj in LIST_TYPES: + return self._list_schema(Any) + elif obj in SET_TYPES: + return self._set_schema(Any) + elif obj in FROZEN_SET_TYPES: + return self._frozenset_schema(Any) + elif obj in SEQUENCE_TYPES: + return self._sequence_schema(Any) + elif obj in ITERABLE_TYPES: + return self._iterable_schema(obj) + elif obj in DICT_TYPES: + return self._dict_schema(Any, Any) + elif obj in PATH_TYPES: + return self._path_schema(obj, Any) + elif obj in DEQUE_TYPES: + return self._deque_schema(Any) + elif obj in MAPPING_TYPES: + return self._mapping_schema(obj, Any, Any) + elif obj in COUNTER_TYPES: + return self._mapping_schema(obj, Any, int) + elif typing_objects.is_typealiastype(obj): + return self._type_alias_type_schema(obj) + elif obj is type: + return self._type_schema() + elif _typing_extra.is_callable(obj): + return core_schema.callable_schema() + elif typing_objects.is_literal(get_origin(obj)): + return self._literal_schema(obj) + elif is_typeddict(obj): + return self._typed_dict_schema(obj, None) + elif inspect.isclass(obj) and issubclass(obj, Enum): + # NOTE: this must come before the `is_namedtuple()` check as enums values + # can be namedtuples: + return self._enum_schema(obj) + elif _typing_extra.is_namedtuple(obj): + return self._namedtuple_schema(obj, None) + elif typing_objects.is_newtype(obj): + # NewType, can't use isinstance because it fails <3.10 + return self.generate_schema(obj.__supertype__) + elif obj in PATTERN_TYPES: + return self._pattern_schema(obj) + elif _typing_extra.is_hashable(obj): + return self._hashable_schema() + elif isinstance(obj, typing.TypeVar): + return self._unsubstituted_typevar_schema(obj) + elif _typing_extra.is_finalvar(obj): + if obj is Final: + return core_schema.any_schema() + return self.generate_schema( + self._get_first_arg_or_any(obj), + ) + elif isinstance(obj, VALIDATE_CALL_SUPPORTED_TYPES): + return self._call_schema(obj) # pyright: ignore[reportArgumentType] + elif obj is ZoneInfo: + return self._zoneinfo_schema() + + # dataclasses.is_dataclass coerces dc instances to types, but we only handle + # the case of a dc type here + if dataclasses.is_dataclass(obj): + return self._dataclass_schema(obj, None) # pyright: ignore[reportArgumentType] + + origin = get_origin(obj) + if origin is not None: + return self._match_generic_type(obj, origin) + + if self._arbitrary_types: + return self._arbitrary_type_schema(obj) + return self._unknown_type_schema(obj) + + def _match_generic_type(self, obj: Any, origin: Any) -> CoreSchema: # noqa: C901 + # Need to handle generic dataclasses before looking for the schema properties because attribute accesses + # on _GenericAlias delegate to the origin type, so lose the information about the concrete parametrization + # As a result, currently, there is no way to cache the schema for generic dataclasses. This may be possible + # to resolve by modifying the value returned by `Generic.__class_getitem__`, but that is a dangerous game. + if dataclasses.is_dataclass(origin): + return self._dataclass_schema(obj, origin) # pyright: ignore[reportArgumentType] + if _typing_extra.is_namedtuple(origin): + return self._namedtuple_schema(obj, origin) + + schema = self._generate_schema_from_get_schema_method(origin, obj) + if schema is not None: + return schema + + if typing_objects.is_typealiastype(origin): + return self._type_alias_type_schema(obj) + elif is_union_origin(origin): + return self._union_schema(obj) + elif origin in TUPLE_TYPES: + return self._tuple_schema(obj) + elif origin in LIST_TYPES: + return self._list_schema(self._get_first_arg_or_any(obj)) + elif origin in SET_TYPES: + return self._set_schema(self._get_first_arg_or_any(obj)) + elif origin in FROZEN_SET_TYPES: + return self._frozenset_schema(self._get_first_arg_or_any(obj)) + elif origin in DICT_TYPES: + return self._dict_schema(*self._get_first_two_args_or_any(obj)) + elif origin in PATH_TYPES: + return self._path_schema(origin, self._get_first_arg_or_any(obj)) + elif origin in DEQUE_TYPES: + return self._deque_schema(self._get_first_arg_or_any(obj)) + elif origin in MAPPING_TYPES: + return self._mapping_schema(origin, *self._get_first_two_args_or_any(obj)) + elif origin in COUNTER_TYPES: + return self._mapping_schema(origin, self._get_first_arg_or_any(obj), int) + elif is_typeddict(origin): + return self._typed_dict_schema(obj, origin) + elif origin in TYPE_TYPES: + return self._subclass_schema(obj) + elif origin in SEQUENCE_TYPES: + return self._sequence_schema(self._get_first_arg_or_any(obj)) + elif origin in ITERABLE_TYPES: + return self._iterable_schema(obj) + elif origin in PATTERN_TYPES: + return self._pattern_schema(obj) + + if self._arbitrary_types: + return self._arbitrary_type_schema(origin) + return self._unknown_type_schema(obj) + + def _generate_td_field_schema( + self, + name: str, + field_info: FieldInfo, + decorators: DecoratorInfos, + *, + required: bool = True, + ) -> core_schema.TypedDictField: + """Prepare a TypedDictField to represent a model or typeddict field.""" + schema, metadata = self._common_field_schema(name, field_info, decorators) + return core_schema.typed_dict_field( + schema, + required=False if not field_info.is_required() else required, + serialization_exclude=field_info.exclude, + validation_alias=_convert_to_aliases(field_info.validation_alias), + serialization_alias=field_info.serialization_alias, + serialization_exclude_if=field_info.exclude_if, + metadata=metadata, + ) + + def _generate_md_field_schema( + self, + name: str, + field_info: FieldInfo, + decorators: DecoratorInfos, + ) -> core_schema.ModelField: + """Prepare a ModelField to represent a model field.""" + schema, metadata = self._common_field_schema(name, field_info, decorators) + return core_schema.model_field( + schema, + serialization_exclude=field_info.exclude, + validation_alias=_convert_to_aliases(field_info.validation_alias), + serialization_alias=field_info.serialization_alias, + serialization_exclude_if=field_info.exclude_if, + frozen=field_info.frozen, + metadata=metadata, + ) + + def _generate_dc_field_schema( + self, + name: str, + field_info: FieldInfo, + decorators: DecoratorInfos, + ) -> core_schema.DataclassField: + """Prepare a DataclassField to represent the parameter/field, of a dataclass.""" + schema, metadata = self._common_field_schema(name, field_info, decorators) + return core_schema.dataclass_field( + name, + schema, + init=field_info.init, + init_only=field_info.init_var or None, + kw_only=None if field_info.kw_only else False, + serialization_exclude=field_info.exclude, + validation_alias=_convert_to_aliases(field_info.validation_alias), + serialization_alias=field_info.serialization_alias, + serialization_exclude_if=field_info.exclude_if, + frozen=field_info.frozen, + metadata=metadata, + ) + + def _common_field_schema( # C901 + self, name: str, field_info: FieldInfo, decorators: DecoratorInfos + ) -> tuple[CoreSchema, dict[str, Any]]: + source_type, annotations = field_info.annotation, field_info.metadata + + def set_discriminator(schema: CoreSchema) -> CoreSchema: + schema = self._apply_discriminator_to_union(schema, field_info.discriminator) + return schema + + # Convert `@field_validator` decorators to `Before/After/Plain/WrapValidator` instances: + validators_from_decorators = [ + _mode_to_validator[decorator.info.mode]._from_decorator(decorator) + for decorator in filter_field_decorator_info_by_field(decorators.field_validators.values(), name) + ] + + with self.field_name_stack.push(name): + if field_info.discriminator is not None: + schema = self._apply_annotations( + source_type, annotations + validators_from_decorators, transform_inner_schema=set_discriminator + ) + else: + schema = self._apply_annotations( + source_type, + annotations + validators_from_decorators, + ) + + # This V1 compatibility shim should eventually be removed + # push down any `each_item=True` validators + # note that this won't work for any Annotated types that get wrapped by a function validator + # but that's okay because that didn't exist in V1 + this_field_validators = filter_field_decorator_info_by_field(decorators.validators.values(), name) + if _validators_require_validate_default(this_field_validators): + field_info.validate_default = True + each_item_validators = [v for v in this_field_validators if v.info.each_item is True] + this_field_validators = [v for v in this_field_validators if v not in each_item_validators] + schema = apply_each_item_validators(schema, each_item_validators) + + schema = apply_validators(schema, this_field_validators) + + # the default validator needs to go outside of any other validators + # so that it is the topmost validator for the field validator + # which uses it to check if the field has a default value or not + if not field_info.is_required(): + schema = wrap_default(field_info, schema) + + schema = self._apply_field_serializers( + schema, filter_field_decorator_info_by_field(decorators.field_serializers.values(), name) + ) + + pydantic_js_updates, pydantic_js_extra = _extract_json_schema_info_from_field_info(field_info) + core_metadata: dict[str, Any] = {} + update_core_metadata( + core_metadata, pydantic_js_updates=pydantic_js_updates, pydantic_js_extra=pydantic_js_extra + ) + + return schema, core_metadata + + def _union_schema(self, union_type: Any) -> core_schema.CoreSchema: + """Generate schema for a Union.""" + args = self._get_args_resolving_forward_refs(union_type, required=True) + choices: list[CoreSchema] = [] + nullable = False + for arg in args: + if arg is None or arg is _typing_extra.NoneType: + nullable = True + else: + choices.append(self.generate_schema(arg)) + + if len(choices) == 1: + s = choices[0] + else: + choices_with_tags: list[CoreSchema | tuple[CoreSchema, str]] = [] + for choice in choices: + tag = cast(CoreMetadata, choice.get('metadata', {})).get('pydantic_internal_union_tag_key') + if tag is not None: + choices_with_tags.append((choice, tag)) + else: + choices_with_tags.append(choice) + s = core_schema.union_schema(choices_with_tags) + + if nullable: + s = core_schema.nullable_schema(s) + return s + + def _type_alias_type_schema(self, obj: TypeAliasType) -> CoreSchema: + with self.defs.get_schema_or_ref(obj) as (ref, maybe_schema): + if maybe_schema is not None: + return maybe_schema + + origin: TypeAliasType = get_origin(obj) or obj + typevars_map = get_standard_typevars_map(obj) + + with self._ns_resolver.push(origin): + try: + annotation = _typing_extra.eval_type(origin.__value__, *self._types_namespace) + except NameError as e: + raise PydanticUndefinedAnnotation.from_name_error(e) from e + annotation = replace_types(annotation, typevars_map) + schema = self.generate_schema(annotation) + assert schema['type'] != 'definitions' + schema['ref'] = ref # type: ignore + return self.defs.create_definition_reference_schema(schema) + + def _literal_schema(self, literal_type: Any) -> CoreSchema: + """Generate schema for a Literal.""" + expected = list(get_literal_values(literal_type, type_check=False, unpack_type_aliases='eager')) + assert expected, f'literal "expected" cannot be empty, obj={literal_type}' + schema = core_schema.literal_schema(expected) + + if self._config_wrapper.use_enum_values and any(isinstance(v, Enum) for v in expected): + schema = core_schema.no_info_after_validator_function( + lambda v: v.value if isinstance(v, Enum) else v, schema + ) + + return schema + + def _typed_dict_schema(self, typed_dict_cls: Any, origin: Any) -> core_schema.CoreSchema: + """Generate a core schema for a `TypedDict` class. + + To be able to build a `DecoratorInfos` instance for the `TypedDict` class (which will include + validators, serializers, etc.), we need to have access to the original bases of the class + (see https://docs.python.org/3/library/types.html#types.get_original_bases). + However, the `__orig_bases__` attribute was only added in 3.12 (https://github.com/python/cpython/pull/103698). + + For this reason, we require Python 3.12 (or using the `typing_extensions` backport). + """ + FieldInfo = import_cached_field_info() + + with ( + self.model_type_stack.push(typed_dict_cls), + self.defs.get_schema_or_ref(typed_dict_cls) as ( + typed_dict_ref, + maybe_schema, + ), + ): + if maybe_schema is not None: + return maybe_schema + + typevars_map = get_standard_typevars_map(typed_dict_cls) + if origin is not None: + typed_dict_cls = origin + + if not _SUPPORTS_TYPEDDICT and type(typed_dict_cls).__module__ == 'typing': + raise PydanticUserError( + 'Please use `typing_extensions.TypedDict` instead of `typing.TypedDict` on Python < 3.12.', + code='typed-dict-version', + ) + + try: + # if a typed dictionary class doesn't have config, we use the parent's config, hence a default of `None` + # see https://github.com/pydantic/pydantic/issues/10917 + config: ConfigDict | None = get_attribute_from_bases(typed_dict_cls, '__pydantic_config__') + except AttributeError: + config = None + + with self._config_wrapper_stack.push(config): + core_config = self._config_wrapper.core_config(title=typed_dict_cls.__name__) + + required_keys: frozenset[str] = typed_dict_cls.__required_keys__ + + fields: dict[str, core_schema.TypedDictField] = {} + + decorators = DecoratorInfos.build(typed_dict_cls, replace_wrapped_methods=False) + decorators.update_from_config(self._config_wrapper) + + if self._config_wrapper.use_attribute_docstrings: + field_docstrings = extract_docstrings_from_cls(typed_dict_cls, use_inspect=True) + else: + field_docstrings = None + + try: + annotations = _typing_extra.get_cls_type_hints(typed_dict_cls, ns_resolver=self._ns_resolver) + except NameError as e: + raise PydanticUndefinedAnnotation.from_name_error(e) from e + + readonly_fields: list[str] = [] + + for field_name, annotation in annotations.items(): + field_info = FieldInfo.from_annotation(annotation, _source=AnnotationSource.TYPED_DICT) + field_info.annotation = replace_types(field_info.annotation, typevars_map) + + required = ( + field_name in required_keys or 'required' in field_info._qualifiers + ) and 'not_required' not in field_info._qualifiers + if 'read_only' in field_info._qualifiers: + readonly_fields.append(field_name) + + if ( + field_docstrings is not None + and field_info.description is None + and field_name in field_docstrings + ): + field_info.description = field_docstrings[field_name] + update_field_from_config(self._config_wrapper, field_name, field_info) + + fields[field_name] = self._generate_td_field_schema( + field_name, field_info, decorators, required=required + ) + + if readonly_fields: + fields_repr = ', '.join(repr(f) for f in readonly_fields) + plural = len(readonly_fields) >= 2 + warnings.warn( + f'Item{"s" if plural else ""} {fields_repr} on TypedDict class {typed_dict_cls.__name__!r} ' + f'{"are" if plural else "is"} using the `ReadOnly` qualifier. Pydantic will not protect items ' + 'from any mutation on dictionary instances.', + UserWarning, + ) + + extra_behavior: core_schema.ExtraBehavior = 'ignore' + extras_schema: CoreSchema | None = None # For 'allow', equivalent to `Any` - no validation performed. + + # `__closed__` is `None` when not specified (equivalent to `False`): + is_closed = bool(getattr(typed_dict_cls, '__closed__', False)) + extra_items = getattr(typed_dict_cls, '__extra_items__', typing_extensions.NoExtraItems) + if is_closed: + extra_behavior = 'forbid' + extras_schema = None + elif not typing_objects.is_noextraitems(extra_items): + extra_behavior = 'allow' + extras_schema = self.generate_schema(replace_types(extra_items, typevars_map)) + + if (config_extra := self._config_wrapper.extra) in ('allow', 'forbid'): + if is_closed and config_extra == 'allow': + warnings.warn( + f"TypedDict class {typed_dict_cls.__qualname__!r} is closed, but 'extra' configuration " + "is set to `'allow'`. The 'extra' configuration value will be ignored.", + category=TypedDictExtraConfigWarning, + ) + elif not typing_objects.is_noextraitems(extra_items) and config_extra == 'forbid': + warnings.warn( + f"TypedDict class {typed_dict_cls.__qualname__!r} allows extra items, but 'extra' configuration " + "is set to `'forbid'`. The 'extra' configuration value will be ignored.", + category=TypedDictExtraConfigWarning, + ) + else: + extra_behavior = config_extra + + td_schema = core_schema.typed_dict_schema( + fields, + cls=typed_dict_cls, + computed_fields=[ + self._computed_field_schema(d, decorators.field_serializers) + for d in decorators.computed_fields.values() + ], + extra_behavior=extra_behavior, + extras_schema=extras_schema, + ref=typed_dict_ref, + config=core_config, + ) + + schema = self._apply_model_serializers(td_schema, decorators.model_serializers.values()) + schema = apply_model_validators(schema, decorators.model_validators.values(), 'all') + return self.defs.create_definition_reference_schema(schema) + + def _namedtuple_schema(self, namedtuple_cls: Any, origin: Any) -> core_schema.CoreSchema: + """Generate schema for a NamedTuple.""" + with ( + self.model_type_stack.push(namedtuple_cls), + self.defs.get_schema_or_ref(namedtuple_cls) as ( + namedtuple_ref, + maybe_schema, + ), + ): + if maybe_schema is not None: + return maybe_schema + typevars_map = get_standard_typevars_map(namedtuple_cls) + if origin is not None: + namedtuple_cls = origin + + try: + annotations = _typing_extra.get_cls_type_hints(namedtuple_cls, ns_resolver=self._ns_resolver) + except NameError as e: + raise PydanticUndefinedAnnotation.from_name_error(e) from e + + # Filter annotations to only include fields that are actually in the NamedTuple + # (as subclassing an existing NamedTuple is not supported yet - see https://github.com/python/typing/issues/427) + # and use `Any` if no annotation exist (i.e. when using `collections.namedtuple()`). + annotations = {field_name: annotations.get(field_name, Any) for field_name in namedtuple_cls._fields} + + if typevars_map: + annotations = { + field_name: replace_types(annotation, typevars_map) + for field_name, annotation in annotations.items() + } + + arguments_schema = core_schema.arguments_schema( + [ + self._generate_parameter_schema( + field_name, + annotation, + source=AnnotationSource.NAMED_TUPLE, + default=namedtuple_cls._field_defaults.get(field_name, Parameter.empty), + ) + for field_name, annotation in annotations.items() + ], + metadata={'pydantic_js_prefer_positional_arguments': True}, + ) + schema = core_schema.call_schema(arguments_schema, namedtuple_cls, ref=namedtuple_ref) + return self.defs.create_definition_reference_schema(schema) + + def _generate_parameter_schema( + self, + name: str, + annotation: type[Any], + source: AnnotationSource, + default: Any = Parameter.empty, + mode: Literal['positional_only', 'positional_or_keyword', 'keyword_only'] | None = None, + ) -> core_schema.ArgumentsParameter: + """Generate the definition of a field in a namedtuple or a parameter in a function signature. + + This definition is meant to be used for the `'arguments'` core schema, which will be replaced + in V3 by the `'arguments-v3`'. + """ + FieldInfo = import_cached_field_info() + + if default is Parameter.empty: + field = FieldInfo.from_annotation(annotation, _source=source) + else: + field = FieldInfo.from_annotated_attribute(annotation, default, _source=source) + + assert field.annotation is not None, 'field.annotation should not be None when generating a schema' + update_field_from_config(self._config_wrapper, name, field) + + with self.field_name_stack.push(name): + schema = self._apply_annotations( + field.annotation, + [field], + # Because we pass `field` as metadata above (required for attributes relevant for + # JSON Scheme generation), we need to ignore the potential warnings about `FieldInfo` + # attributes that will not be used: + check_unsupported_field_info_attributes=False, + ) + + if not field.is_required(): + schema = wrap_default(field, schema) + + parameter_schema = core_schema.arguments_parameter( + name, + schema, + mode=mode, + alias=_convert_to_aliases(field.validation_alias), + ) + + return parameter_schema + + def _generate_parameter_v3_schema( + self, + name: str, + annotation: Any, + source: AnnotationSource, + mode: Literal[ + 'positional_only', + 'positional_or_keyword', + 'keyword_only', + 'var_args', + 'var_kwargs_uniform', + 'var_kwargs_unpacked_typed_dict', + ], + default: Any = Parameter.empty, + ) -> core_schema.ArgumentsV3Parameter: + """Generate the definition of a parameter in a function signature. + + This definition is meant to be used for the `'arguments-v3'` core schema, which will replace + the `'arguments`' schema in V3. + """ + FieldInfo = import_cached_field_info() + + if default is Parameter.empty: + field = FieldInfo.from_annotation(annotation, _source=source) + else: + field = FieldInfo.from_annotated_attribute(annotation, default, _source=source) + update_field_from_config(self._config_wrapper, name, field) + + with self.field_name_stack.push(name): + schema = self._apply_annotations( + field.annotation, + [field], + # Because we pass `field` as metadata above (required for attributes relevant for + # JSON Scheme generation), we need to ignore the potential warnings about `FieldInfo` + # attributes that will not be used: + check_unsupported_field_info_attributes=False, + ) + + if not field.is_required(): + schema = wrap_default(field, schema) + + parameter_schema = core_schema.arguments_v3_parameter( + name=name, + schema=schema, + mode=mode, + alias=_convert_to_aliases(field.validation_alias), + ) + + return parameter_schema + + def _tuple_schema(self, tuple_type: Any) -> core_schema.CoreSchema: + """Generate schema for a Tuple, e.g. `tuple[int, str]` or `tuple[int, ...]`.""" + # TODO: do we really need to resolve type vars here? + typevars_map = get_standard_typevars_map(tuple_type) + params = self._get_args_resolving_forward_refs(tuple_type) + + if typevars_map and params: + params = tuple(replace_types(param, typevars_map) for param in params) + + # NOTE: subtle difference: `tuple[()]` gives `params=()`, whereas `typing.Tuple[()]` gives `params=((),)` + # This is only true for <3.11, on Python 3.11+ `typing.Tuple[()]` gives `params=()` + if not params: + if tuple_type in TUPLE_TYPES: + return core_schema.tuple_schema([core_schema.any_schema()], variadic_item_index=0) + else: + # special case for `tuple[()]` which means `tuple[]` - an empty tuple + return core_schema.tuple_schema([]) + elif params[-1] is Ellipsis: + if len(params) == 2: + return core_schema.tuple_schema([self.generate_schema(params[0])], variadic_item_index=0) + else: + # TODO: something like https://github.com/pydantic/pydantic/issues/5952 + raise ValueError('Variable tuples can only have one type') + elif len(params) == 1 and params[0] == (): + # special case for `tuple[()]` which means `tuple[]` - an empty tuple + # NOTE: This conditional can be removed when we drop support for Python 3.10. + return core_schema.tuple_schema([]) + else: + return core_schema.tuple_schema([self.generate_schema(param) for param in params]) + + def _type_schema(self) -> core_schema.CoreSchema: + return core_schema.custom_error_schema( + core_schema.is_instance_schema(type), + custom_error_type='is_type', + custom_error_message='Input should be a type', + ) + + def _zoneinfo_schema(self) -> core_schema.CoreSchema: + """Generate schema for a zone_info.ZoneInfo object""" + from ._validators import validate_str_is_valid_iana_tz + + metadata = {'pydantic_js_functions': [lambda _1, _2: {'type': 'string', 'format': 'zoneinfo'}]} + return core_schema.no_info_plain_validator_function( + validate_str_is_valid_iana_tz, + serialization=core_schema.to_string_ser_schema(), + metadata=metadata, + ) + + def _union_is_subclass_schema(self, union_type: Any) -> core_schema.CoreSchema: + """Generate schema for `type[Union[X, ...]]`.""" + args = self._get_args_resolving_forward_refs(union_type, required=True) + return core_schema.union_schema([self.generate_schema(type[args]) for args in args]) + + def _subclass_schema(self, type_: Any) -> core_schema.CoreSchema: + """Generate schema for a type, e.g. `type[int]`.""" + type_param = self._get_first_arg_or_any(type_) + + # Assume `type[Annotated[, ...]]` is equivalent to `type[]`: + type_param = _typing_extra.annotated_type(type_param) or type_param + + if typing_objects.is_any(type_param): + return self._type_schema() + elif typing_objects.is_typealiastype(type_param): + return self.generate_schema(type[type_param.__value__]) + elif typing_objects.is_typevar(type_param): + if type_param.__bound__: + if is_union_origin(get_origin(type_param.__bound__)): + return self._union_is_subclass_schema(type_param.__bound__) + return core_schema.is_subclass_schema(type_param.__bound__) + elif type_param.__constraints__: + return core_schema.union_schema([self.generate_schema(type[c]) for c in type_param.__constraints__]) + else: + return self._type_schema() + elif is_union_origin(get_origin(type_param)): + return self._union_is_subclass_schema(type_param) + else: + if typing_objects.is_self(type_param): + type_param = self._resolve_self_type(type_param) + if _typing_extra.is_generic_alias(type_param): + raise PydanticUserError( + 'Subscripting `type[]` with an already parametrized type is not supported. ' + f'Instead of using type[{type_param!r}], use type[{_repr.display_as_type(get_origin(type_param))}].', + code=None, + ) + if not inspect.isclass(type_param): + # when using type[None], this doesn't type convert to type[NoneType], and None isn't a class + # so we handle it manually here + if type_param is None: + return core_schema.is_subclass_schema(_typing_extra.NoneType) + raise TypeError(f'Expected a class, got {type_param!r}') + return core_schema.is_subclass_schema(type_param) + + def _sequence_schema(self, items_type: Any) -> core_schema.CoreSchema: + """Generate schema for a Sequence, e.g. `Sequence[int]`.""" + from ._serializers import serialize_sequence_via_list + + item_type_schema = self.generate_schema(items_type) + list_schema = core_schema.list_schema(item_type_schema) + + json_schema = smart_deepcopy(list_schema) + python_schema = core_schema.is_instance_schema(typing.Sequence, cls_repr='Sequence') + if not typing_objects.is_any(items_type): + from ._validators import sequence_validator + + python_schema = core_schema.chain_schema( + [python_schema, core_schema.no_info_wrap_validator_function(sequence_validator, list_schema)], + ) + + serialization = core_schema.wrap_serializer_function_ser_schema( + serialize_sequence_via_list, schema=item_type_schema, info_arg=True + ) + return core_schema.json_or_python_schema( + json_schema=json_schema, python_schema=python_schema, serialization=serialization + ) + + def _iterable_schema(self, type_: Any) -> core_schema.GeneratorSchema: + """Generate a schema for an `Iterable`.""" + item_type = self._get_first_arg_or_any(type_) + + return core_schema.generator_schema(self.generate_schema(item_type)) + + def _pattern_schema(self, pattern_type: Any) -> core_schema.CoreSchema: + from . import _validators + + metadata = {'pydantic_js_functions': [lambda _1, _2: {'type': 'string', 'format': 'regex'}]} + ser = core_schema.plain_serializer_function_ser_schema( + attrgetter('pattern'), when_used='json', return_schema=core_schema.str_schema() + ) + if pattern_type is typing.Pattern or pattern_type is re.Pattern: + # bare type + return core_schema.no_info_plain_validator_function( + _validators.pattern_either_validator, serialization=ser, metadata=metadata + ) + + param = self._get_args_resolving_forward_refs( + pattern_type, + required=True, + )[0] + if param is str: + return core_schema.no_info_plain_validator_function( + _validators.pattern_str_validator, serialization=ser, metadata=metadata + ) + elif param is bytes: + return core_schema.no_info_plain_validator_function( + _validators.pattern_bytes_validator, serialization=ser, metadata=metadata + ) + else: + raise PydanticSchemaGenerationError(f'Unable to generate pydantic-core schema for {pattern_type!r}.') + + def _hashable_schema(self) -> core_schema.CoreSchema: + return core_schema.custom_error_schema( + schema=core_schema.json_or_python_schema( + json_schema=core_schema.chain_schema( + [core_schema.any_schema(), core_schema.is_instance_schema(collections.abc.Hashable)] + ), + python_schema=core_schema.is_instance_schema(collections.abc.Hashable), + ), + custom_error_type='is_hashable', + custom_error_message='Input should be hashable', + ) + + def _dataclass_schema( + self, dataclass: type[StandardDataclass], origin: type[StandardDataclass] | None + ) -> core_schema.CoreSchema: + """Generate schema for a dataclass.""" + with ( + self.model_type_stack.push(dataclass), + self.defs.get_schema_or_ref(dataclass) as ( + dataclass_ref, + maybe_schema, + ), + ): + if maybe_schema is not None: + return maybe_schema + + schema = dataclass.__dict__.get('__pydantic_core_schema__') + if schema is not None and not isinstance(schema, MockCoreSchema): + if schema['type'] == 'definitions': + schema = self.defs.unpack_definitions(schema) + ref = get_ref(schema) + if ref: + return self.defs.create_definition_reference_schema(schema) + else: + return schema + + typevars_map = get_standard_typevars_map(dataclass) + if origin is not None: + dataclass = origin + + # if (plain) dataclass doesn't have config, we use the parent's config, hence a default of `None` + # (Pydantic dataclasses have an empty dict config by default). + # see https://github.com/pydantic/pydantic/issues/10917 + config = getattr(dataclass, '__pydantic_config__', None) + + from ..dataclasses import is_pydantic_dataclass + + with self._ns_resolver.push(dataclass), self._config_wrapper_stack.push(config): + if is_pydantic_dataclass(dataclass): + if dataclass.__pydantic_fields_complete__(): + # Copy the field info instances to avoid mutating the `FieldInfo` instances + # of the generic dataclass generic origin (e.g. `apply_typevars_map` below). + # Note that we don't apply `deepcopy` on `__pydantic_fields__` because we + # don't want to copy the `FieldInfo` attributes: + fields = { + f_name: copy(field_info) for f_name, field_info in dataclass.__pydantic_fields__.items() + } + if typevars_map: + for field in fields.values(): + field.apply_typevars_map(typevars_map, *self._types_namespace) + else: + try: + fields = rebuild_dataclass_fields( + dataclass, + config_wrapper=self._config_wrapper, + ns_resolver=self._ns_resolver, + typevars_map=typevars_map or {}, + ) + except NameError as e: + raise PydanticUndefinedAnnotation.from_name_error(e) from e + else: + fields = collect_dataclass_fields( + dataclass, + typevars_map=typevars_map, + config_wrapper=self._config_wrapper, + ) + + if self._config_wrapper.extra == 'allow': + # disallow combination of init=False on a dataclass field and extra='allow' on a dataclass + for field_name, field in fields.items(): + if field.init is False: + raise PydanticUserError( + f'Field {field_name} has `init=False` and dataclass has config setting `extra="allow"`. ' + f'This combination is not allowed.', + code='dataclass-init-false-extra-allow', + ) + + decorators = dataclass.__dict__.get('__pydantic_decorators__') + if decorators is None: + decorators = DecoratorInfos.build(dataclass, replace_wrapped_methods=False) + decorators.update_from_config(self._config_wrapper) + # Move kw_only=False args to the start of the list, as this is how vanilla dataclasses work. + # Note that when kw_only is missing or None, it is treated as equivalent to kw_only=True + args = sorted( + (self._generate_dc_field_schema(k, v, decorators) for k, v in fields.items()), + key=lambda a: a.get('kw_only') is not False, + ) + has_post_init = hasattr(dataclass, '__post_init__') + has_slots = hasattr(dataclass, '__slots__') + + args_schema = core_schema.dataclass_args_schema( + dataclass.__name__, + args, + computed_fields=[ + self._computed_field_schema(d, decorators.field_serializers) + for d in decorators.computed_fields.values() + ], + collect_init_only=has_post_init, + ) + + inner_schema = apply_validators(args_schema, decorators.root_validators.values()) + + model_validators = decorators.model_validators.values() + inner_schema = apply_model_validators(inner_schema, model_validators, 'inner') + + core_config = self._config_wrapper.core_config(title=dataclass.__name__) + + dc_schema = core_schema.dataclass_schema( + dataclass, + inner_schema, + generic_origin=origin, + post_init=has_post_init, + ref=dataclass_ref, + fields=[field.name for field in dataclasses.fields(dataclass)], + slots=has_slots, + config=core_config, + # we don't use a custom __setattr__ for dataclasses, so we must + # pass along the frozen config setting to the pydantic-core schema + frozen=self._config_wrapper_stack.tail.frozen, + ) + schema = self._apply_model_serializers(dc_schema, decorators.model_serializers.values()) + schema = apply_model_validators(schema, model_validators, 'outer') + return self.defs.create_definition_reference_schema(schema) + + def _call_schema(self, function: ValidateCallSupportedTypes) -> core_schema.CallSchema: + """Generate schema for a Callable. + + TODO support functional validators once we support them in Config + """ + arguments_schema = self._arguments_schema(function) + + return_schema: core_schema.CoreSchema | None = None + config_wrapper = self._config_wrapper + if config_wrapper.validate_return: + sig = _typing_extra.signature_no_eval(function) + return_hint = sig.return_annotation + if return_hint is not sig.empty: + globalns, localns = self._types_namespace + type_hints = _typing_extra.get_function_type_hints( + function, globalns=globalns, localns=localns, include_keys={'return'} + ) + return_schema = self.generate_schema(type_hints['return']) + + return core_schema.call_schema( + arguments_schema, + function, + return_schema=return_schema, + ) + + def _arguments_schema( + self, function: ValidateCallSupportedTypes, parameters_callback: ParametersCallback | None = None + ) -> core_schema.ArgumentsSchema: + """Generate schema for a Signature.""" + mode_lookup: dict[_ParameterKind, Literal['positional_only', 'positional_or_keyword', 'keyword_only']] = { + Parameter.POSITIONAL_ONLY: 'positional_only', + Parameter.POSITIONAL_OR_KEYWORD: 'positional_or_keyword', + Parameter.KEYWORD_ONLY: 'keyword_only', + } + + sig = _typing_extra.signature_no_eval(function) + globalns, localns = self._types_namespace + type_hints = _typing_extra.get_function_type_hints(function, globalns=globalns, localns=localns) + + arguments_list: list[core_schema.ArgumentsParameter] = [] + var_args_schema: core_schema.CoreSchema | None = None + var_kwargs_schema: core_schema.CoreSchema | None = None + var_kwargs_mode: core_schema.VarKwargsMode | None = None + + for i, (name, p) in enumerate(sig.parameters.items()): + if p.annotation is sig.empty: + annotation = typing.cast(Any, Any) + else: + annotation = type_hints[name] + + if parameters_callback is not None: + result = parameters_callback(i, name, annotation) + if result == 'skip': + continue + + parameter_mode = mode_lookup.get(p.kind) + if parameter_mode is not None: + arg_schema = self._generate_parameter_schema( + name, annotation, AnnotationSource.FUNCTION, p.default, parameter_mode + ) + arguments_list.append(arg_schema) + elif p.kind == Parameter.VAR_POSITIONAL: + var_args_schema = self.generate_schema(annotation) + else: + assert p.kind == Parameter.VAR_KEYWORD, p.kind + + unpack_type = _typing_extra.unpack_type(annotation) + if unpack_type is not None: + origin = get_origin(unpack_type) or unpack_type + if not is_typeddict(origin): + raise PydanticUserError( + f'Expected a `TypedDict` class inside `Unpack[...]`, got {unpack_type!r}', + code='unpack-typed-dict', + ) + non_pos_only_param_names = { + name for name, p in sig.parameters.items() if p.kind != Parameter.POSITIONAL_ONLY + } + overlapping_params = non_pos_only_param_names.intersection(origin.__annotations__) + if overlapping_params: + raise PydanticUserError( + f'Typed dictionary {origin.__name__!r} overlaps with parameter' + f'{"s" if len(overlapping_params) >= 2 else ""} ' + f'{", ".join(repr(p) for p in sorted(overlapping_params))}', + code='overlapping-unpack-typed-dict', + ) + + var_kwargs_mode = 'unpacked-typed-dict' + var_kwargs_schema = self._typed_dict_schema(unpack_type, get_origin(unpack_type)) + else: + var_kwargs_mode = 'uniform' + var_kwargs_schema = self.generate_schema(annotation) + + return core_schema.arguments_schema( + arguments_list, + var_args_schema=var_args_schema, + var_kwargs_mode=var_kwargs_mode, + var_kwargs_schema=var_kwargs_schema, + validate_by_name=self._config_wrapper.validate_by_name, + ) + + def _arguments_v3_schema( + self, function: ValidateCallSupportedTypes, parameters_callback: ParametersCallback | None = None + ) -> core_schema.ArgumentsV3Schema: + mode_lookup: dict[ + _ParameterKind, Literal['positional_only', 'positional_or_keyword', 'var_args', 'keyword_only'] + ] = { + Parameter.POSITIONAL_ONLY: 'positional_only', + Parameter.POSITIONAL_OR_KEYWORD: 'positional_or_keyword', + Parameter.VAR_POSITIONAL: 'var_args', + Parameter.KEYWORD_ONLY: 'keyword_only', + } + + sig = _typing_extra.signature_no_eval(function) + globalns, localns = self._types_namespace + type_hints = _typing_extra.get_function_type_hints(function, globalns=globalns, localns=localns) + + parameters_list: list[core_schema.ArgumentsV3Parameter] = [] + + for i, (name, p) in enumerate(sig.parameters.items()): + if parameters_callback is not None: + result = parameters_callback(i, name, p.annotation) + if result == 'skip': + continue + + if p.annotation is Parameter.empty: + annotation = typing.cast(Any, Any) + else: + annotation = type_hints[name] + + parameter_mode = mode_lookup.get(p.kind) + if parameter_mode is None: + assert p.kind == Parameter.VAR_KEYWORD, p.kind + + unpack_type = _typing_extra.unpack_type(annotation) + if unpack_type is not None: + origin = get_origin(unpack_type) or unpack_type + if not is_typeddict(origin): + raise PydanticUserError( + f'Expected a `TypedDict` class inside `Unpack[...]`, got {unpack_type!r}', + code='unpack-typed-dict', + ) + non_pos_only_param_names = { + name for name, p in sig.parameters.items() if p.kind != Parameter.POSITIONAL_ONLY + } + overlapping_params = non_pos_only_param_names.intersection(origin.__annotations__) + if overlapping_params: + raise PydanticUserError( + f'Typed dictionary {origin.__name__!r} overlaps with parameter' + f'{"s" if len(overlapping_params) >= 2 else ""} ' + f'{", ".join(repr(p) for p in sorted(overlapping_params))}', + code='overlapping-unpack-typed-dict', + ) + parameter_mode = 'var_kwargs_unpacked_typed_dict' + annotation = unpack_type + else: + parameter_mode = 'var_kwargs_uniform' + + parameters_list.append( + self._generate_parameter_v3_schema( + name, annotation, AnnotationSource.FUNCTION, parameter_mode, default=p.default + ) + ) + + return core_schema.arguments_v3_schema( + parameters_list, + validate_by_name=self._config_wrapper.validate_by_name, + ) + + def _unsubstituted_typevar_schema(self, typevar: typing.TypeVar) -> core_schema.CoreSchema: + try: + has_default = typevar.has_default() # pyright: ignore[reportAttributeAccessIssue] + except AttributeError: + # Happens if using `typing.TypeVar` (and not `typing_extensions`) on Python < 3.13 + pass + else: + if has_default: + return self.generate_schema(typevar.__default__) # pyright: ignore[reportAttributeAccessIssue] + + if constraints := typevar.__constraints__: + return self._union_schema(typing.Union[constraints]) + + if bound := typevar.__bound__: + schema = self.generate_schema(bound) + schema['serialization'] = core_schema.simple_ser_schema('any') + return schema + + return core_schema.any_schema() + + def _computed_field_schema( + self, + d: Decorator[ComputedFieldInfo], + field_serializers: dict[str, Decorator[FieldSerializerDecoratorInfo]], + ) -> core_schema.ComputedField: + if d.info.return_type is not PydanticUndefined: + return_type = d.info.return_type + else: + try: + # Do not pass in globals as the function could be defined in a different module. + # Instead, let `get_callable_return_type` infer the globals to use, but still pass + # in locals that may contain a parent/rebuild namespace: + return_type = _decorators.get_callable_return_type(d.func, localns=self._types_namespace.locals) + except NameError as e: + raise PydanticUndefinedAnnotation.from_name_error(e) from e + if return_type is PydanticUndefined: + raise PydanticUserError( + 'Computed field is missing return type annotation or specifying `return_type`' + ' to the `@computed_field` decorator (e.g. `@computed_field(return_type=int | str)`)', + code='model-field-missing-annotation', + ) + + return_type = replace_types(return_type, self._typevars_map) + # Create a new ComputedFieldInfo so that different type parametrizations of the same + # generic model's computed field can have different return types. + d.info = dataclasses.replace(d.info, return_type=return_type) + return_type_schema = self.generate_schema(return_type) + # Apply serializers to computed field if there exist + return_type_schema = self._apply_field_serializers( + return_type_schema, + filter_field_decorator_info_by_field(field_serializers.values(), d.cls_var_name), + ) + + pydantic_js_updates, pydantic_js_extra = _extract_json_schema_info_from_field_info(d.info) + core_metadata: dict[str, Any] = {} + update_core_metadata( + core_metadata, + pydantic_js_updates={'readOnly': True, **(pydantic_js_updates if pydantic_js_updates else {})}, + pydantic_js_extra=pydantic_js_extra, + ) + exclude_if = d.info.exclude_if + # TODO: Should we support exclude_if from annotations? + return core_schema.computed_field( + d.cls_var_name, + return_schema=return_type_schema, + alias=d.info.alias, + serialization_exclude_if=exclude_if, + metadata=core_metadata, + ) + + def _annotated_schema(self, annotated_type: Any) -> core_schema.CoreSchema: + """Generate schema for an Annotated type, e.g. `Annotated[int, Field(...)]` or `Annotated[int, Gt(0)]`.""" + FieldInfo = import_cached_field_info() + source_type, *annotations = self._get_args_resolving_forward_refs( + annotated_type, + required=True, + ) + schema = self._apply_annotations(source_type, annotations) + # put the default validator last so that TypeAdapter.get_default_value() works + # even if there are function validators involved + for annotation in annotations: + if isinstance(annotation, FieldInfo): + schema = wrap_default(annotation, schema) + return schema + + def _apply_annotations( + self, + source_type: Any, + annotations: list[Any], + transform_inner_schema: Callable[[CoreSchema], CoreSchema] = lambda x: x, + check_unsupported_field_info_attributes: bool = True, + ) -> CoreSchema: + """Apply arguments from `Annotated` or from `FieldInfo` to a schema. + + This gets called by `GenerateSchema._annotated_schema` but differs from it in that it does + not expect `source_type` to be an `Annotated` object, it expects it to be the first argument of that + (in other words, `GenerateSchema._annotated_schema` just unpacks `Annotated`, this process it). + """ + annotations = list(_known_annotated_metadata.expand_grouped_metadata(annotations)) + + pydantic_js_annotation_functions: list[GetJsonSchemaFunction] = [] + + def inner_handler(obj: Any) -> CoreSchema: + schema = self._generate_schema_from_get_schema_method(obj, source_type) + + if schema is None: + schema = self._generate_schema_inner(obj) + + metadata_js_function = _extract_get_pydantic_json_schema(obj) + if metadata_js_function is not None: + metadata_schema = resolve_original_schema(schema, self.defs) + if metadata_schema is not None: + self._add_js_function(metadata_schema, metadata_js_function) + return transform_inner_schema(schema) + + get_inner_schema = CallbackGetCoreSchemaHandler(inner_handler, self) + + for annotation in annotations: + if annotation is None: + continue + get_inner_schema = self._get_wrapped_inner_schema( + get_inner_schema, + annotation, + pydantic_js_annotation_functions, + check_unsupported_field_info_attributes=check_unsupported_field_info_attributes, + ) + + schema = get_inner_schema(source_type) + if pydantic_js_annotation_functions: + core_metadata = schema.setdefault('metadata', {}) + update_core_metadata(core_metadata, pydantic_js_annotation_functions=pydantic_js_annotation_functions) + return _add_custom_serialization_from_json_encoders(self._config_wrapper.json_encoders, source_type, schema) + + def _apply_single_annotation( + self, + schema: core_schema.CoreSchema, + metadata: Any, + check_unsupported_field_info_attributes: bool = True, + ) -> core_schema.CoreSchema: + FieldInfo = import_cached_field_info() + + if isinstance(metadata, FieldInfo): + if ( + check_unsupported_field_info_attributes + # HACK: we don't want to emit the warning for `FieldInfo` subclasses, because FastAPI does weird manipulations + # with its subclasses and their annotations: + and type(metadata) is FieldInfo + ): + for attr, value in (unsupported_attributes := self._get_unsupported_field_info_attributes(metadata)): + warnings.warn( + f'The {attr!r} attribute with value {value!r} was provided to the `Field()` function, ' + f'which has no effect in the context it was used. {attr!r} is field-specific metadata, ' + 'and can only be attached to a model field using `Annotated` metadata or by assignment. ' + 'This may have happened because an `Annotated` type alias using the `type` statement was ' + 'used, or if the `Field()` function was attached to a single member of a union type.', + category=UnsupportedFieldAttributeWarning, + ) + + if ( + metadata.default_factory_takes_validated_data + and self.model_type_stack.get() is None + and 'defaut_factory' not in unsupported_attributes + ): + warnings.warn( + "A 'default_factory' taking validated data as an argument was provided to the `Field()` function, " + 'but no validated data is available in the context it was used.', + category=UnsupportedFieldAttributeWarning, + ) + + for field_metadata in metadata.metadata: + schema = self._apply_single_annotation(schema, field_metadata) + + if metadata.discriminator is not None: + schema = self._apply_discriminator_to_union(schema, metadata.discriminator) + return schema + + if schema['type'] == 'nullable': + # for nullable schemas, metadata is automatically applied to the inner schema + inner = schema.get('schema', core_schema.any_schema()) + inner = self._apply_single_annotation(inner, metadata) + if inner: + schema['schema'] = inner + return schema + + if schema['type'] == 'union' and any( + choice['type'] == 'missing-sentinel' for choice in core_schema.iter_union_choices(schema) + ): + # Same behavior as for nullable schemas. This is a bit gross, but we have to support the same pattern + filtered_choices = [ + choice + for choice in schema['choices'] + if (choice[0] if isinstance(choice, tuple) else choice)['type'] != 'missing-sentinel' + ] + if len(filtered_choices) >= 2: + # e.g. `Annotated[int | str | MISSING, Constraint(...)]`. We apply `Constraint(...)` to `int | str`, + # and create a new union semantically equivalent to `Annotated[int | str, Constraint(...)] | MISSING`: + filtered_union = core_schema.union_schema(filtered_choices) + filtered_union = self._apply_single_annotation(filtered_union, metadata) + new_union = schema.copy() + new_union['choices'] = [ + filtered_union, + next( + choice + for choice in schema['choices'] + if (choice[0] if isinstance(choice, tuple) else choice)['type'] == 'missing-sentinel' + ), + ] + return new_union + elif len(filtered_choices) == 1: + # e.g. `Annotated[int | MISSING, Constraint(...)]`. We apply `Constraint(...)` to `int`, and reconstruct + # a new union preserving the order. + inner = filtered_choices[0][0] if isinstance(filtered_choices[0], tuple) else filtered_choices[0] + inner = self._apply_single_annotation(inner, metadata) + + # Create a new union schema, preserving the order of the union: + new_union = schema.copy() + new_union['choices'] = [ + (inner, choice[1]) + if isinstance(choice, tuple) and choice[0]['type'] != 'missing-sentinel' + else inner + if not isinstance(choice, tuple) and choice['type'] != 'missing-sentinel' + else choice + for choice in schema['choices'] + ] + return new_union + + original_schema = schema + ref = schema.get('ref') + if ref is not None: + schema = schema.copy() + new_ref = ref + f'_{repr(metadata)}' + if (existing := self.defs.get_schema_from_ref(new_ref)) is not None: + return existing + schema['ref'] = new_ref # pyright: ignore[reportGeneralTypeIssues] + elif schema['type'] == 'definition-ref': + ref = schema['schema_ref'] + if (referenced_schema := self.defs.get_schema_from_ref(ref)) is not None: + schema = referenced_schema.copy() + new_ref = ref + f'_{repr(metadata)}' + if (existing := self.defs.get_schema_from_ref(new_ref)) is not None: + return existing + schema['ref'] = new_ref # pyright: ignore[reportGeneralTypeIssues] + + maybe_updated_schema = _known_annotated_metadata.apply_known_metadata(metadata, schema) + + if maybe_updated_schema is not None: + return maybe_updated_schema + return original_schema + + def _apply_single_annotation_json_schema( + self, schema: core_schema.CoreSchema, metadata: Any + ) -> core_schema.CoreSchema: + FieldInfo = import_cached_field_info() + + if isinstance(metadata, FieldInfo): + for field_metadata in metadata.metadata: + schema = self._apply_single_annotation_json_schema(schema, field_metadata) + + pydantic_js_updates, pydantic_js_extra = _extract_json_schema_info_from_field_info(metadata) + core_metadata = schema.setdefault('metadata', {}) + update_core_metadata( + core_metadata, pydantic_js_updates=pydantic_js_updates, pydantic_js_extra=pydantic_js_extra + ) + return schema + + def _get_unsupported_field_info_attributes(self, field_info: FieldInfo) -> list[tuple[str, Any]]: + """Get the list of unsupported `FieldInfo` attributes when not directly used in `Annotated` for field annotations.""" + unused_metadata: list[tuple[str, Any]] = [] + for unused_metadata_name, unset_value in UNSUPPORTED_STANDALONE_FIELDINFO_ATTRIBUTES: + if ( + (unused_metadata_value := getattr(field_info, unused_metadata_name)) is not unset_value + # `default` and `default_factory` can still be used with a type adapter, so only include them + # if used with a model-like class: + and ( + unused_metadata_name not in ('default', 'default_factory') + or self.model_type_stack.get() is not None + ) + # Setting `alias` will set `validation/serialization_alias` as well, so we want to avoid duplicate warnings: + and ( + unused_metadata_name not in ('validation_alias', 'serialization_alias') + or 'alias' not in field_info._attributes_set + ) + ): + unused_metadata.append((unused_metadata_name, unused_metadata_value)) + + return unused_metadata + + def _get_wrapped_inner_schema( + self, + get_inner_schema: GetCoreSchemaHandler, + annotation: Any, + pydantic_js_annotation_functions: list[GetJsonSchemaFunction], + check_unsupported_field_info_attributes: bool = False, + ) -> CallbackGetCoreSchemaHandler: + annotation_get_schema: GetCoreSchemaFunction | None = getattr(annotation, '__get_pydantic_core_schema__', None) + + def new_handler(source: Any) -> core_schema.CoreSchema: + if annotation_get_schema is not None: + schema = annotation_get_schema(source, get_inner_schema) + else: + schema = get_inner_schema(source) + schema = self._apply_single_annotation( + schema, + annotation, + check_unsupported_field_info_attributes=check_unsupported_field_info_attributes, + ) + schema = self._apply_single_annotation_json_schema(schema, annotation) + + metadata_js_function = _extract_get_pydantic_json_schema(annotation) + if metadata_js_function is not None: + pydantic_js_annotation_functions.append(metadata_js_function) + return schema + + return CallbackGetCoreSchemaHandler(new_handler, self) + + def _apply_field_serializers( + self, + schema: core_schema.CoreSchema, + serializers: list[Decorator[FieldSerializerDecoratorInfo]], + ) -> core_schema.CoreSchema: + """Apply field serializers to a schema.""" + if serializers: + schema = copy(schema) + if schema['type'] == 'definitions': + inner_schema = schema['schema'] + schema['schema'] = self._apply_field_serializers(inner_schema, serializers) + return schema + elif 'ref' in schema: + schema = self.defs.create_definition_reference_schema(schema) + + # use the last serializer to make it easy to override a serializer set on a parent model + serializer = serializers[-1] + is_field_serializer, info_arg = inspect_field_serializer(serializer.func, serializer.info.mode) + + if serializer.info.return_type is not PydanticUndefined: + return_type = serializer.info.return_type + else: + try: + # Do not pass in globals as the function could be defined in a different module. + # Instead, let `get_callable_return_type` infer the globals to use, but still pass + # in locals that may contain a parent/rebuild namespace: + return_type = _decorators.get_callable_return_type( + serializer.func, localns=self._types_namespace.locals + ) + except NameError as e: + raise PydanticUndefinedAnnotation.from_name_error(e) from e + + if return_type is PydanticUndefined: + return_schema = None + else: + return_schema = self.generate_schema(return_type) + + if serializer.info.mode == 'wrap': + schema['serialization'] = core_schema.wrap_serializer_function_ser_schema( + serializer.func, + is_field_serializer=is_field_serializer, + info_arg=info_arg, + return_schema=return_schema, + when_used=serializer.info.when_used, + ) + else: + assert serializer.info.mode == 'plain' + schema['serialization'] = core_schema.plain_serializer_function_ser_schema( + serializer.func, + is_field_serializer=is_field_serializer, + info_arg=info_arg, + return_schema=return_schema, + when_used=serializer.info.when_used, + ) + return schema + + def _apply_model_serializers( + self, schema: core_schema.CoreSchema, serializers: Iterable[Decorator[ModelSerializerDecoratorInfo]] + ) -> core_schema.CoreSchema: + """Apply model serializers to a schema.""" + ref: str | None = schema.pop('ref', None) # type: ignore + if serializers: + serializer = list(serializers)[-1] + info_arg = inspect_model_serializer(serializer.func, serializer.info.mode) + + if serializer.info.return_type is not PydanticUndefined: + return_type = serializer.info.return_type + else: + try: + # Do not pass in globals as the function could be defined in a different module. + # Instead, let `get_callable_return_type` infer the globals to use, but still pass + # in locals that may contain a parent/rebuild namespace: + return_type = _decorators.get_callable_return_type( + serializer.func, localns=self._types_namespace.locals + ) + except NameError as e: + raise PydanticUndefinedAnnotation.from_name_error(e) from e + + if return_type is PydanticUndefined: + return_schema = None + else: + return_schema = self.generate_schema(return_type) + + if serializer.info.mode == 'wrap': + ser_schema: core_schema.SerSchema = core_schema.wrap_serializer_function_ser_schema( + serializer.func, + info_arg=info_arg, + return_schema=return_schema, + when_used=serializer.info.when_used, + ) + else: + # plain + ser_schema = core_schema.plain_serializer_function_ser_schema( + serializer.func, + info_arg=info_arg, + return_schema=return_schema, + when_used=serializer.info.when_used, + ) + schema['serialization'] = ser_schema + if ref: + schema['ref'] = ref # type: ignore + return schema + + +_VALIDATOR_F_MATCH: Mapping[ + tuple[FieldValidatorModes, Literal['no-info', 'with-info']], + Callable[[Callable[..., Any], core_schema.CoreSchema], core_schema.CoreSchema], +] = { + ('before', 'no-info'): lambda f, schema: core_schema.no_info_before_validator_function(f, schema), + ('after', 'no-info'): lambda f, schema: core_schema.no_info_after_validator_function(f, schema), + ('plain', 'no-info'): lambda f, _: core_schema.no_info_plain_validator_function(f), + ('wrap', 'no-info'): lambda f, schema: core_schema.no_info_wrap_validator_function(f, schema), + ('before', 'with-info'): lambda f, schema: core_schema.with_info_before_validator_function(f, schema), + ('after', 'with-info'): lambda f, schema: core_schema.with_info_after_validator_function(f, schema), + ('plain', 'with-info'): lambda f, _: core_schema.with_info_plain_validator_function(f), + ('wrap', 'with-info'): lambda f, schema: core_schema.with_info_wrap_validator_function(f, schema), +} + + +# TODO V3: this function is only used for deprecated decorators. It should +# be removed once we drop support for those. +def apply_validators( + schema: core_schema.CoreSchema, + validators: Iterable[Decorator[RootValidatorDecoratorInfo]] + | Iterable[Decorator[ValidatorDecoratorInfo]] + | Iterable[Decorator[FieldValidatorDecoratorInfo]], +) -> core_schema.CoreSchema: + """Apply validators to a schema. + + Args: + schema: The schema to apply validators on. + validators: An iterable of validators. + field_name: The name of the field if validators are being applied to a model field. + + Returns: + The updated schema. + """ + for validator in validators: + # Actually, type could be 'field' or 'model', but this is only used for deprecated + # decorators, so let's not worry about it. + info_arg = inspect_validator(validator.func, mode=validator.info.mode, type='field') + val_type = 'with-info' if info_arg else 'no-info' + + schema = _VALIDATOR_F_MATCH[(validator.info.mode, val_type)](validator.func, schema) + return schema + + +def _validators_require_validate_default(validators: Iterable[Decorator[ValidatorDecoratorInfo]]) -> bool: + """In v1, if any of the validators for a field had `always=True`, the default value would be validated. + + This serves as an auxiliary function for re-implementing that logic, by looping over a provided + collection of (v1-style) ValidatorDecoratorInfo's and checking if any of them have `always=True`. + + We should be able to drop this function and the associated logic calling it once we drop support + for v1-style validator decorators. (Or we can extend it and keep it if we add something equivalent + to the v1-validator `always` kwarg to `field_validator`.) + """ + for validator in validators: + if validator.info.always: + return True + return False + + +def _convert_to_aliases( + alias: str | AliasChoices | AliasPath | None, +) -> str | list[str | int] | list[list[str | int]] | None: + if isinstance(alias, (AliasChoices, AliasPath)): + return alias.convert_to_aliases() + else: + return alias + + +def apply_model_validators( + schema: core_schema.CoreSchema, + validators: Iterable[Decorator[ModelValidatorDecoratorInfo]], + mode: Literal['inner', 'outer', 'all'], +) -> core_schema.CoreSchema: + """Apply model validators to a schema. + + If mode == 'inner', only "before" validators are applied + If mode == 'outer', validators other than "before" are applied + If mode == 'all', all validators are applied + + Args: + schema: The schema to apply validators on. + validators: An iterable of validators. + mode: The validator mode. + + Returns: + The updated schema. + """ + ref: str | None = schema.pop('ref', None) # type: ignore + for validator in validators: + if mode == 'inner' and validator.info.mode != 'before': + continue + if mode == 'outer' and validator.info.mode == 'before': + continue + info_arg = inspect_validator(validator.func, mode=validator.info.mode, type='model') + if validator.info.mode == 'wrap': + if info_arg: + schema = core_schema.with_info_wrap_validator_function(function=validator.func, schema=schema) + else: + schema = core_schema.no_info_wrap_validator_function(function=validator.func, schema=schema) + elif validator.info.mode == 'before': + if info_arg: + schema = core_schema.with_info_before_validator_function(function=validator.func, schema=schema) + else: + schema = core_schema.no_info_before_validator_function(function=validator.func, schema=schema) + else: + assert validator.info.mode == 'after' + if info_arg: + schema = core_schema.with_info_after_validator_function(function=validator.func, schema=schema) + else: + schema = core_schema.no_info_after_validator_function(function=validator.func, schema=schema) + if ref: + schema['ref'] = ref # type: ignore + return schema + + +def wrap_default(field_info: FieldInfo, schema: core_schema.CoreSchema) -> core_schema.CoreSchema: + """Wrap schema with default schema if default value or `default_factory` are available. + + Args: + field_info: The field info object. + schema: The schema to apply default on. + + Returns: + Updated schema by default value or `default_factory`. + """ + if field_info.default_factory: + return core_schema.with_default_schema( + schema, + default_factory=field_info.default_factory, + default_factory_takes_data=takes_validated_data_argument(field_info.default_factory), + validate_default=field_info.validate_default, + ) + elif field_info.default is not PydanticUndefined: + return core_schema.with_default_schema( + schema, default=field_info.default, validate_default=field_info.validate_default + ) + else: + return schema + + +def _extract_get_pydantic_json_schema(tp: Any) -> GetJsonSchemaFunction | None: + """Extract `__get_pydantic_json_schema__` from a type, handling the deprecated `__modify_schema__`.""" + js_modify_function = getattr(tp, '__get_pydantic_json_schema__', None) + + if hasattr(tp, '__modify_schema__'): + BaseModel = import_cached_base_model() + + has_custom_v2_modify_js_func = ( + js_modify_function is not None + and BaseModel.__get_pydantic_json_schema__.__func__ # type: ignore + not in (js_modify_function, getattr(js_modify_function, '__func__', None)) + ) + + if not has_custom_v2_modify_js_func: + cls_name = getattr(tp, '__name__', None) + raise PydanticUserError( + f'The `__modify_schema__` method is not supported in Pydantic v2. ' + f'Use `__get_pydantic_json_schema__` instead{f" in class `{cls_name}`" if cls_name else ""}.', + code='custom-json-schema', + ) + + if (origin := get_origin(tp)) is not None: + # Generic aliases proxy attribute access to the origin, *except* dunder attributes, + # such as `__get_pydantic_json_schema__`, hence the explicit check. + return _extract_get_pydantic_json_schema(origin) + + if js_modify_function is None: + return None + + return js_modify_function + + +def resolve_original_schema(schema: CoreSchema, definitions: _Definitions) -> CoreSchema | None: + if schema['type'] == 'definition-ref': + return definitions.get_schema_from_ref(schema['schema_ref']) + elif schema['type'] == 'definitions': + return schema['schema'] + else: + return schema + + +def _inlining_behavior( + def_ref: core_schema.DefinitionReferenceSchema, +) -> Literal['inline', 'keep', 'preserve_metadata']: + """Determine the inlining behavior of the `'definition-ref'` schema. + + - If no `'serialization'` schema and no metadata is attached, the schema can safely be inlined. + - If it has metadata but only related to the deferred discriminator application, it can be inlined + provided that such metadata is kept. + - Otherwise, the schema should not be inlined. Doing so would remove the `'serialization'` schema or metadata. + """ + if 'serialization' in def_ref: + return 'keep' + metadata = def_ref.get('metadata') + if not metadata: + return 'inline' + if len(metadata) == 1 and 'pydantic_internal_union_discriminator' in metadata: + return 'preserve_metadata' + return 'keep' + + +class _Definitions: + """Keeps track of references and definitions.""" + + _recursively_seen: set[str] + """A set of recursively seen references. + + When a referenceable type is encountered, the `get_schema_or_ref` context manager is + entered to compute the reference. If the type references itself by some way (e.g. for + a dataclass a Pydantic model, the class can be referenced as a field annotation), + entering the context manager again will yield a `'definition-ref'` schema that should + short-circuit the normal generation process, as the reference was already in this set. + """ + + _definitions: dict[str, core_schema.CoreSchema] + """A mapping of references to their corresponding schema. + + When a schema for a referenceable type is generated, it is stored in this mapping. If the + same type is encountered again, the reference is yielded by the `get_schema_or_ref` context + manager. + """ + + def __init__(self) -> None: + self._recursively_seen = set() + self._definitions = {} + + @contextmanager + def get_schema_or_ref(self, tp: Any, /) -> Generator[tuple[str, core_schema.DefinitionReferenceSchema | None]]: + """Get a definition for `tp` if one exists. + + If a definition exists, a tuple of `(ref_string, CoreSchema)` is returned. + If no definition exists yet, a tuple of `(ref_string, None)` is returned. + + Note that the returned `CoreSchema` will always be a `DefinitionReferenceSchema`, + not the actual definition itself. + + This should be called for any type that can be identified by reference. + This includes any recursive types. + + At present the following types can be named/recursive: + + - Pydantic model + - Pydantic and stdlib dataclasses + - Typed dictionaries + - Named tuples + - `TypeAliasType` instances + - Enums + """ + ref = get_type_ref(tp) + # return the reference if we're either (1) in a cycle or (2) it the reference was already encountered: + if ref in self._recursively_seen or ref in self._definitions: + yield (ref, core_schema.definition_reference_schema(ref)) + else: + self._recursively_seen.add(ref) + try: + yield (ref, None) + finally: + self._recursively_seen.discard(ref) + + def get_schema_from_ref(self, ref: str) -> CoreSchema | None: + """Resolve the schema from the given reference.""" + return self._definitions.get(ref) + + def create_definition_reference_schema(self, schema: CoreSchema) -> core_schema.DefinitionReferenceSchema: + """Store the schema as a definition and return a `'definition-reference'` schema pointing to it. + + The schema must have a reference attached to it. + """ + ref = schema['ref'] # pyright: ignore + self._definitions[ref] = schema + return core_schema.definition_reference_schema(ref) + + def unpack_definitions(self, schema: core_schema.DefinitionsSchema) -> CoreSchema: + """Store the definitions of the `'definitions'` core schema and return the inner core schema.""" + for def_schema in schema['definitions']: + self._definitions[def_schema['ref']] = def_schema # pyright: ignore + return schema['schema'] + + def finalize_schema(self, schema: CoreSchema) -> CoreSchema: + """Finalize the core schema. + + This traverses the core schema and referenced definitions, replaces `'definition-ref'` schemas + by the referenced definition if possible, and applies deferred discriminators. + """ + definitions = self._definitions + try: + gather_result = gather_schemas_for_cleaning( + schema, + definitions=definitions, + ) + except MissingDefinitionError as e: + raise InvalidSchemaError from e + + remaining_defs: dict[str, CoreSchema] = {} + + # Note: this logic doesn't play well when core schemas with deferred discriminator metadata + # and references are encountered. See the `test_deferred_discriminated_union_and_references()` test. + for ref, inlinable_def_ref in gather_result['collected_references'].items(): + if inlinable_def_ref is not None and (inlining_behavior := _inlining_behavior(inlinable_def_ref)) != 'keep': + if inlining_behavior == 'inline': + # `ref` was encountered, and only once: + # - `inlinable_def_ref` is a `'definition-ref'` schema and is guaranteed to be + # the only one. Transform it into the definition it points to. + # - Do not store the definition in the `remaining_defs`. + inlinable_def_ref.clear() # pyright: ignore[reportAttributeAccessIssue] + inlinable_def_ref.update(self._resolve_definition(ref, definitions)) # pyright: ignore + elif inlining_behavior == 'preserve_metadata': + # `ref` was encountered, and only once, but contains discriminator metadata. + # We will do the same thing as if `inlining_behavior` was `'inline'`, but make + # sure to keep the metadata for the deferred discriminator application logic below. + meta = inlinable_def_ref.pop('metadata') + inlinable_def_ref.clear() # pyright: ignore[reportAttributeAccessIssue] + inlinable_def_ref.update(self._resolve_definition(ref, definitions)) # pyright: ignore + inlinable_def_ref['metadata'] = meta + else: + # `ref` was encountered, at least two times (or only once, but with metadata or a serialization schema): + # - Do not inline the `'definition-ref'` schemas (they are not provided in the gather result anyway). + # - Store the definition in the `remaining_defs` + remaining_defs[ref] = self._resolve_definition(ref, definitions) + + for cs in gather_result['deferred_discriminator_schemas']: + discriminator: str | None = cs['metadata'].pop('pydantic_internal_union_discriminator', None) # pyright: ignore[reportTypedDictNotRequiredAccess] + if discriminator is None: + # This can happen in rare scenarios, when a deferred schema is present multiple times in the + # gather result (e.g. when using the `Sequence` type -- see `test_sequence_discriminated_union()`). + # In this case, a previous loop iteration applied the discriminator and so we can just skip it here. + continue + applied = _discriminated_union.apply_discriminator(cs.copy(), discriminator, remaining_defs) + # Mutate the schema directly to have the discriminator applied + cs.clear() # pyright: ignore[reportAttributeAccessIssue] + cs.update(applied) # pyright: ignore + + if remaining_defs: + schema = core_schema.definitions_schema(schema=schema, definitions=[*remaining_defs.values()]) + return schema + + def _resolve_definition(self, ref: str, definitions: dict[str, CoreSchema]) -> CoreSchema: + definition = definitions[ref] + if definition['type'] != 'definition-ref': + return definition + + # Some `'definition-ref'` schemas might act as "intermediate" references (e.g. when using + # a PEP 695 type alias (which is referenceable) that references another PEP 695 type alias): + visited: set[str] = set() + while definition['type'] == 'definition-ref' and _inlining_behavior(definition) == 'inline': + schema_ref = definition['schema_ref'] + if schema_ref in visited: + raise PydanticUserError( + f'{ref} contains a circular reference to itself.', code='circular-reference-schema' + ) + visited.add(schema_ref) + definition = definitions[schema_ref] + return {**definition, 'ref': ref} # pyright: ignore[reportReturnType] + + +class _FieldNameStack: + __slots__ = ('_stack',) + + def __init__(self) -> None: + self._stack: list[str] = [] + + @contextmanager + def push(self, field_name: str) -> Iterator[None]: + self._stack.append(field_name) + yield + self._stack.pop() + + def get(self) -> str | None: + if self._stack: + return self._stack[-1] + else: + return None + + +class _ModelTypeStack: + __slots__ = ('_stack',) + + def __init__(self) -> None: + self._stack: list[type] = [] + + @contextmanager + def push(self, type_obj: type) -> Iterator[None]: + self._stack.append(type_obj) + yield + self._stack.pop() + + def get(self) -> type | None: + if self._stack: + return self._stack[-1] + else: + return None diff --git a/source/target-vendors/cp312/pydantic/_internal/_generics.py b/source/target-vendors/cp312/pydantic/_internal/_generics.py new file mode 100644 index 0000000..f3e927c --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_generics.py @@ -0,0 +1,530 @@ +from __future__ import annotations + +import operator +import sys +import types +import typing +from collections import ChainMap +from collections.abc import Iterator, Mapping +from contextlib import contextmanager +from contextvars import ContextVar +from functools import reduce +from itertools import zip_longest +from types import prepare_class +from typing import TYPE_CHECKING, Annotated, Any, TypedDict, TypeVar, cast +from weakref import WeakValueDictionary + +import typing_extensions +from typing_inspection import typing_objects +from typing_inspection.introspection import is_union_origin + +from . import _typing_extra +from ._core_utils import get_type_ref +from ._forward_ref import PydanticRecursiveRef +from ._utils import all_identical, is_model_class + +if TYPE_CHECKING: + from ..main import BaseModel + +GenericTypesCacheKey = tuple[Any, Any, tuple[Any, ...]] + +# Note: We want to remove LimitedDict, but to do this, we'd need to improve the handling of generics caching. +# Right now, to handle recursive generics, we some types must remain cached for brief periods without references. +# By chaining the WeakValuesDict with a LimitedDict, we have a way to retain caching for all types with references, +# while also retaining a limited number of types even without references. This is generally enough to build +# specific recursive generic models without losing required items out of the cache. + +KT = TypeVar('KT') +VT = TypeVar('VT') +_LIMITED_DICT_SIZE = 100 + + +class LimitedDict(dict[KT, VT]): + def __init__(self, size_limit: int = _LIMITED_DICT_SIZE) -> None: + self.size_limit = size_limit + super().__init__() + + def __setitem__(self, key: KT, value: VT, /) -> None: + super().__setitem__(key, value) + if len(self) > self.size_limit: + excess = len(self) - self.size_limit + self.size_limit // 10 + to_remove = list(self.keys())[:excess] + for k in to_remove: + del self[k] + + +# weak dictionaries allow the dynamically created parametrized versions of generic models to get collected +# once they are no longer referenced by the caller. +GenericTypesCache = WeakValueDictionary[GenericTypesCacheKey, 'type[BaseModel]'] + +if TYPE_CHECKING: + + class DeepChainMap(ChainMap[KT, VT]): # type: ignore + ... + +else: + + class DeepChainMap(ChainMap): + """Variant of ChainMap that allows direct updates to inner scopes. + + Taken from https://docs.python.org/3/library/collections.html#collections.ChainMap, + with some light modifications for this use case. + """ + + def clear(self) -> None: + for mapping in self.maps: + mapping.clear() + + def __setitem__(self, key: KT, value: VT) -> None: + for mapping in self.maps: + mapping[key] = value + + def __delitem__(self, key: KT) -> None: + hit = False + for mapping in self.maps: + if key in mapping: + del mapping[key] + hit = True + if not hit: + raise KeyError(key) + + +# Despite the fact that LimitedDict _seems_ no longer necessary, I'm very nervous to actually remove it +# and discover later on that we need to re-add all this infrastructure... +# _GENERIC_TYPES_CACHE = DeepChainMap(GenericTypesCache(), LimitedDict()) + +_GENERIC_TYPES_CACHE = GenericTypesCache() + + +class PydanticGenericMetadata(TypedDict): + origin: type[BaseModel] | None # analogous to typing._GenericAlias.__origin__ + args: tuple[Any, ...] # analogous to typing._GenericAlias.__args__ + parameters: tuple[TypeVar, ...] # analogous to typing.Generic.__parameters__ + + +def create_generic_submodel( + model_name: str, origin: type[BaseModel], args: tuple[Any, ...], params: tuple[Any, ...] +) -> type[BaseModel]: + """Dynamically create a submodel of a provided (generic) BaseModel. + + This is used when producing concrete parametrizations of generic models. This function + only *creates* the new subclass; the schema/validators/serialization must be updated to + reflect a concrete parametrization elsewhere. + + Args: + model_name: The name of the newly created model. + origin: The base class for the new model to inherit from. + args: A tuple of generic metadata arguments. + params: A tuple of generic metadata parameters. + + Returns: + The created submodel. + """ + namespace: dict[str, Any] = {'__module__': origin.__module__} + bases = (origin,) + meta, ns, kwds = prepare_class(model_name, bases) + namespace.update(ns) + created_model = meta( + model_name, + bases, + namespace, + __pydantic_generic_metadata__={ + 'origin': origin, + 'args': args, + 'parameters': params, + }, + __pydantic_reset_parent_namespace__=False, + **kwds, + ) + + model_module, called_globally = _get_caller_frame_info(depth=3) + if called_globally: # create global reference and therefore allow pickling + object_by_reference = None + reference_name = model_name + reference_module_globals = sys.modules[created_model.__module__].__dict__ + while object_by_reference is not created_model: + object_by_reference = reference_module_globals.setdefault(reference_name, created_model) + reference_name += '_' + + return created_model + + +def _get_caller_frame_info(depth: int = 2) -> tuple[str | None, bool]: + """Used inside a function to check whether it was called globally. + + Args: + depth: The depth to get the frame. + + Returns: + A tuple contains `module_name` and `called_globally`. + + Raises: + RuntimeError: If the function is not called inside a function. + """ + try: + previous_caller_frame = sys._getframe(depth) + except ValueError as e: + raise RuntimeError('This function must be used inside another function') from e + except AttributeError: # sys module does not have _getframe function, so there's nothing we can do about it + return None, False + frame_globals = previous_caller_frame.f_globals + return frame_globals.get('__name__'), previous_caller_frame.f_locals is frame_globals + + +DictValues: type[Any] = {}.values().__class__ + + +def iter_contained_typevars(v: Any) -> Iterator[TypeVar]: + """Recursively iterate through all subtypes and type args of `v` and yield any typevars that are found. + + This is inspired as an alternative to directly accessing the `__parameters__` attribute of a GenericAlias, + since __parameters__ of (nested) generic BaseModel subclasses won't show up in that list. + """ + if isinstance(v, TypeVar): + yield v + elif is_model_class(v): + yield from v.__pydantic_generic_metadata__['parameters'] + elif isinstance(v, (DictValues, list)): + for var in v: + yield from iter_contained_typevars(var) + else: + args = get_args(v) + for arg in args: + yield from iter_contained_typevars(arg) + + +def get_args(v: Any) -> Any: + pydantic_generic_metadata: PydanticGenericMetadata | None = getattr(v, '__pydantic_generic_metadata__', None) + if pydantic_generic_metadata: + return pydantic_generic_metadata.get('args') + return typing_extensions.get_args(v) + + +def get_origin(v: Any) -> Any: + pydantic_generic_metadata: PydanticGenericMetadata | None = getattr(v, '__pydantic_generic_metadata__', None) + if pydantic_generic_metadata: + return pydantic_generic_metadata.get('origin') + return typing_extensions.get_origin(v) + + +def get_standard_typevars_map(cls: Any) -> dict[TypeVar, Any] | None: + """Package a generic type's typevars and parametrization (if present) into a dictionary compatible with the + `replace_types` function. Specifically, this works with standard typing generics and typing._GenericAlias. + """ + origin = get_origin(cls) + if origin is None: + return None + if not hasattr(origin, '__parameters__'): + return None + + # In this case, we know that cls is a _GenericAlias, and origin is the generic type + # So it is safe to access cls.__args__ and origin.__parameters__ + args: tuple[Any, ...] = cls.__args__ # type: ignore + parameters: tuple[TypeVar, ...] = origin.__parameters__ + return dict(zip(parameters, args)) + + +def get_model_typevars_map(cls: type[BaseModel]) -> dict[TypeVar, Any]: + """Package a generic BaseModel's typevars and concrete parametrization (if present) into a dictionary compatible + with the `replace_types` function. + + Since BaseModel.__class_getitem__ does not produce a typing._GenericAlias, and the BaseModel generic info is + stored in the __pydantic_generic_metadata__ attribute, we need special handling here. + """ + # TODO: This could be unified with `get_standard_typevars_map` if we stored the generic metadata + # in the __origin__, __args__, and __parameters__ attributes of the model. + generic_metadata = cls.__pydantic_generic_metadata__ + origin = generic_metadata['origin'] + args = generic_metadata['args'] + if not args: + # No need to go into `iter_contained_typevars`: + return {} + return dict(zip(iter_contained_typevars(origin), args)) + + +def replace_types(type_: Any, type_map: Mapping[TypeVar, Any] | None) -> Any: + """Return type with all occurrences of `type_map` keys recursively replaced with their values. + + Args: + type_: The class or generic alias. + type_map: Mapping from `TypeVar` instance to concrete types. + + Returns: + A new type representing the basic structure of `type_` with all + `typevar_map` keys recursively replaced. + + Example: + ```python + from typing import Union + + from pydantic._internal._generics import replace_types + + replace_types(tuple[str, Union[list[str], float]], {str: int}) + #> tuple[int, Union[list[int], float]] + ``` + """ + if not type_map: + return type_ + + type_args = get_args(type_) + origin_type = get_origin(type_) + + if typing_objects.is_annotated(origin_type): + annotated_type, *annotations = type_args + annotated_type = replace_types(annotated_type, type_map) + # TODO remove parentheses when we drop support for Python 3.10: + return Annotated[(annotated_type, *annotations)] + + # Having type args is a good indicator that this is a typing special form + # instance or a generic alias of some sort. + if type_args: + resolved_type_args = tuple(replace_types(arg, type_map) for arg in type_args) + if all_identical(type_args, resolved_type_args): + # If all arguments are the same, there is no need to modify the + # type or create a new object at all + return type_ + + if ( + origin_type is not None + and isinstance(type_, _typing_extra.typing_base) + and not isinstance(origin_type, _typing_extra.typing_base) + and getattr(type_, '_name', None) is not None + ): + # In python < 3.9 generic aliases don't exist so any of these like `list`, + # `type` or `collections.abc.Callable` need to be translated. + # See: https://www.python.org/dev/peps/pep-0585 + origin_type = getattr(typing, type_._name) + assert origin_type is not None + + if is_union_origin(origin_type): + if any(typing_objects.is_any(arg) for arg in resolved_type_args): + # `Any | T` ~ `Any`: + resolved_type_args = (Any,) + # `Never | T` ~ `T`: + resolved_type_args = tuple( + arg + for arg in resolved_type_args + if not (typing_objects.is_noreturn(arg) or typing_objects.is_never(arg)) + ) + + # PEP-604 syntax (e.g. `list | str`) is represented with a types.UnionType object that does not + # implement `__getitem__()`. In Python 3.14+, `typing.Union` and `types.UnionType` are the same, + # and we instead rely on `typing.Union` as it implicitly converts string annotations to `ForwardRef` + # instances (this is to avoid type errors as per https://github.com/python/cpython/pull/105366). + # TODO remove type ignore comment when we drop support for Python 3.9 (https://github.com/microsoft/pyright/issues/11241): + if (3, 10) <= sys.version_info < (3, 14) and origin_type is types.UnionType: # pyright: ignore[reportAttributeAccessIssue] + return reduce(operator.or_, resolved_type_args) + # NotRequired[T] and Required[T] don't support tuple type resolved_type_args, hence the condition below + return origin_type[resolved_type_args[0] if len(resolved_type_args) == 1 else resolved_type_args] + + # We handle pydantic generic models separately as they don't have the same + # semantics as "typing" classes or generic aliases + + if not origin_type and is_model_class(type_): + parameters = type_.__pydantic_generic_metadata__['parameters'] + if not parameters: + return type_ + resolved_type_args = tuple(replace_types(t, type_map) for t in parameters) + if all_identical(parameters, resolved_type_args): + return type_ + return type_[resolved_type_args] + + # Handle special case for typehints that can have lists as arguments. + # `typing.Callable[[int, str], int]` is an example for this. + if isinstance(type_, list): + resolved_list = [replace_types(element, type_map) for element in type_] + if all_identical(type_, resolved_list): + return type_ + return resolved_list + + # If all else fails, we try to resolve the type directly and otherwise just + # return the input with no modifications. + return type_map.get(type_, type_) + + +def map_generic_model_arguments(cls: type[BaseModel], args: tuple[Any, ...]) -> dict[TypeVar, Any]: + """Return a mapping between the parameters of a generic model and the provided arguments during parameterization. + + Raises: + TypeError: If the number of arguments does not match the parameters (i.e. if providing too few or too many arguments). + + Example: + ```python {test="skip" lint="skip"} + class Model[T, U, V = int](BaseModel): ... + + map_generic_model_arguments(Model, (str, bytes)) + #> {T: str, U: bytes, V: int} + + map_generic_model_arguments(Model, (str,)) + #> TypeError: Too few arguments for ; actual 1, expected at least 2 + + map_generic_model_arguments(Model, (str, bytes, int, complex)) + #> TypeError: Too many arguments for ; actual 4, expected 3 + ``` + + Note: + This function is analogous to the private `typing._check_generic_specialization` function. + """ + parameters = cls.__pydantic_generic_metadata__['parameters'] + expected_len = len(parameters) + typevars_map: dict[TypeVar, Any] = {} + + _missing = object() + for parameter, argument in zip_longest(parameters, args, fillvalue=_missing): + if parameter is _missing: + raise TypeError(f'Too many arguments for {cls}; actual {len(args)}, expected {expected_len}') + + if argument is _missing: + param = cast(TypeVar, parameter) + try: + has_default = param.has_default() # pyright: ignore[reportAttributeAccessIssue] + except AttributeError: + # Happens if using `typing.TypeVar` (and not `typing_extensions`) on Python < 3.13. + has_default = False + if has_default: + # The default might refer to other type parameters. For an example, see: + # https://typing.python.org/en/latest/spec/generics.html#type-parameters-as-parameters-to-generics + typevars_map[param] = replace_types(param.__default__, typevars_map) # pyright: ignore[reportAttributeAccessIssue] + else: + expected_len -= sum(hasattr(p, 'has_default') and p.has_default() for p in parameters) # pyright: ignore[reportAttributeAccessIssue] + raise TypeError(f'Too few arguments for {cls}; actual {len(args)}, expected at least {expected_len}') + else: + param = cast(TypeVar, parameter) + typevars_map[param] = argument + + return typevars_map + + +_generic_recursion_cache: ContextVar[set[str] | None] = ContextVar('_generic_recursion_cache', default=None) + + +@contextmanager +def generic_recursion_self_type( + origin: type[BaseModel], args: tuple[Any, ...] +) -> Iterator[PydanticRecursiveRef | None]: + """This contextmanager should be placed around the recursive calls used to build a generic type, + and accept as arguments the generic origin type and the type arguments being passed to it. + + If the same origin and arguments are observed twice, it implies that a self-reference placeholder + can be used while building the core schema, and will produce a schema_ref that will be valid in the + final parent schema. + """ + previously_seen_type_refs = _generic_recursion_cache.get() + if previously_seen_type_refs is None: + previously_seen_type_refs = set() + token = _generic_recursion_cache.set(previously_seen_type_refs) + else: + token = None + + try: + type_ref = get_type_ref(origin, args_override=args) + if type_ref in previously_seen_type_refs: + self_type = PydanticRecursiveRef(type_ref=type_ref) + yield self_type + else: + previously_seen_type_refs.add(type_ref) + yield + previously_seen_type_refs.remove(type_ref) + finally: + if token: + _generic_recursion_cache.reset(token) + + +def recursively_defined_type_refs() -> set[str]: + visited = _generic_recursion_cache.get() + if not visited: + return set() # not in a generic recursion, so there are no types + + return visited.copy() # don't allow modifications + + +def get_cached_generic_type_early(parent: type[BaseModel], typevar_values: Any) -> type[BaseModel] | None: + """The use of a two-stage cache lookup approach was necessary to have the highest performance possible for + repeated calls to `__class_getitem__` on generic types (which may happen in tighter loops during runtime), + while still ensuring that certain alternative parametrizations ultimately resolve to the same type. + + As a concrete example, this approach was necessary to make Model[List[T]][int] equal to Model[List[int]]. + The approach could be modified to not use two different cache keys at different points, but the + _early_cache_key is optimized to be as quick to compute as possible (for repeated-access speed), and the + _late_cache_key is optimized to be as "correct" as possible, so that two types that will ultimately be the + same after resolving the type arguments will always produce cache hits. + + If we wanted to move to only using a single cache key per type, we would either need to always use the + slower/more computationally intensive logic associated with _late_cache_key, or would need to accept + that Model[List[T]][int] is a different type than Model[List[T]][int]. Because we rely on subclass relationships + during validation, I think it is worthwhile to ensure that types that are functionally equivalent are actually + equal. + """ + return _GENERIC_TYPES_CACHE.get(_early_cache_key(parent, typevar_values)) + + +def get_cached_generic_type_late( + parent: type[BaseModel], typevar_values: Any, origin: type[BaseModel], args: tuple[Any, ...] +) -> type[BaseModel] | None: + """See the docstring of `get_cached_generic_type_early` for more information about the two-stage cache lookup.""" + cached = _GENERIC_TYPES_CACHE.get(_late_cache_key(origin, args, typevar_values)) + if cached is not None: + set_cached_generic_type(parent, typevar_values, cached, origin, args) + return cached + + +def set_cached_generic_type( + parent: type[BaseModel], + typevar_values: tuple[Any, ...], + type_: type[BaseModel], + origin: type[BaseModel] | None = None, + args: tuple[Any, ...] | None = None, +) -> None: + """See the docstring of `get_cached_generic_type_early` for more information about why items are cached with + two different keys. + """ + _GENERIC_TYPES_CACHE[_early_cache_key(parent, typevar_values)] = type_ + if len(typevar_values) == 1: + _GENERIC_TYPES_CACHE[_early_cache_key(parent, typevar_values[0])] = type_ + if origin and args: + _GENERIC_TYPES_CACHE[_late_cache_key(origin, args, typevar_values)] = type_ + + +def _union_orderings_key(typevar_values: Any) -> Any: + """This is intended to help differentiate between Union types with the same arguments in different order. + + Thanks to caching internal to the `typing` module, it is not possible to distinguish between + List[Union[int, float]] and List[Union[float, int]] (and similarly for other "parent" origins besides List) + because `typing` considers Union[int, float] to be equal to Union[float, int]. + + However, you _can_ distinguish between (top-level) Union[int, float] vs. Union[float, int]. + Because we parse items as the first Union type that is successful, we get slightly more consistent behavior + if we make an effort to distinguish the ordering of items in a union. It would be best if we could _always_ + get the exact-correct order of items in the union, but that would require a change to the `typing` module itself. + (See https://github.com/python/cpython/issues/86483 for reference.) + """ + if isinstance(typevar_values, tuple): + return tuple(_union_orderings_key(value) for value in typevar_values) + elif typing_objects.is_union(typing_extensions.get_origin(typevar_values)): + return get_args(typevar_values) + else: + return () + + +def _early_cache_key(cls: type[BaseModel], typevar_values: Any) -> GenericTypesCacheKey: + """This is intended for minimal computational overhead during lookups of cached types. + + Note that this is overly simplistic, and it's possible that two different cls/typevar_values + inputs would ultimately result in the same type being created in BaseModel.__class_getitem__. + To handle this, we have a fallback _late_cache_key that is checked later if the _early_cache_key + lookup fails, and should result in a cache hit _precisely_ when the inputs to __class_getitem__ + would result in the same type. + """ + return cls, typevar_values, _union_orderings_key(typevar_values) + + +def _late_cache_key(origin: type[BaseModel], args: tuple[Any, ...], typevar_values: Any) -> GenericTypesCacheKey: + """This is intended for use later in the process of creating a new type, when we have more information + about the exact args that will be passed. If it turns out that a different set of inputs to + __class_getitem__ resulted in the same inputs to the generic type creation process, we can still + return the cached type, and update the cache with the _early_cache_key as well. + """ + # The _union_orderings_key is placed at the start here to ensure there cannot be a collision with an + # _early_cache_key, as that function will always produce a BaseModel subclass as the first item in the key, + # whereas this function will always produce a tuple as the first item in the key. + return _union_orderings_key(typevar_values), origin, args diff --git a/source/target-vendors/cp312/pydantic/_internal/_git.py b/source/target-vendors/cp312/pydantic/_internal/_git.py new file mode 100644 index 0000000..96dcda2 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_git.py @@ -0,0 +1,27 @@ +"""Git utilities, adopted from mypy's git utilities (https://github.com/python/mypy/blob/master/mypy/git.py).""" + +from __future__ import annotations + +import subprocess +from pathlib import Path + + +def is_git_repo(dir: Path) -> bool: + """Is the given directory version-controlled with git?""" + return dir.joinpath('.git').exists() + + +def have_git() -> bool: # pragma: no cover + """Can we run the git executable?""" + try: + subprocess.check_output(['git', '--help']) + return True + except subprocess.CalledProcessError: + return False + except OSError: + return False + + +def git_revision(dir: Path) -> str: + """Get the SHA-1 of the HEAD of a git repository.""" + return subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'], cwd=dir).decode('utf-8').strip() diff --git a/source/target-vendors/cp312/pydantic/_internal/_import_utils.py b/source/target-vendors/cp312/pydantic/_internal/_import_utils.py new file mode 100644 index 0000000..638102f --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_import_utils.py @@ -0,0 +1,20 @@ +from functools import cache +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pydantic import BaseModel + from pydantic.fields import FieldInfo + + +@cache +def import_cached_base_model() -> type['BaseModel']: + from pydantic import BaseModel + + return BaseModel + + +@cache +def import_cached_field_info() -> type['FieldInfo']: + from pydantic.fields import FieldInfo + + return FieldInfo diff --git a/source/target-vendors/cp312/pydantic/_internal/_internal_dataclass.py b/source/target-vendors/cp312/pydantic/_internal/_internal_dataclass.py new file mode 100644 index 0000000..33e152c --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_internal_dataclass.py @@ -0,0 +1,7 @@ +import sys + +# `slots` is available on Python >= 3.10 +if sys.version_info >= (3, 10): + slots_true = {'slots': True} +else: + slots_true = {} diff --git a/source/target-vendors/cp312/pydantic/_internal/_known_annotated_metadata.py b/source/target-vendors/cp312/pydantic/_internal/_known_annotated_metadata.py new file mode 100644 index 0000000..5954dba --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_known_annotated_metadata.py @@ -0,0 +1,403 @@ +from __future__ import annotations + +from collections import defaultdict +from collections.abc import Iterable +from copy import copy +from functools import lru_cache, partial +from typing import TYPE_CHECKING, Any + +from pydantic_core import CoreSchema, PydanticCustomError, ValidationError, to_jsonable_python +from pydantic_core import core_schema as cs + +from ._fields import PydanticMetadata +from ._import_utils import import_cached_field_info + +if TYPE_CHECKING: + pass + +STRICT = {'strict'} +FAIL_FAST = {'fail_fast'} +LENGTH_CONSTRAINTS = {'min_length', 'max_length'} +INEQUALITY = {'le', 'ge', 'lt', 'gt'} +NUMERIC_CONSTRAINTS = {'multiple_of', *INEQUALITY} +ALLOW_INF_NAN = {'allow_inf_nan'} + +STR_CONSTRAINTS = { + *LENGTH_CONSTRAINTS, + *STRICT, + 'strip_whitespace', + 'to_lower', + 'to_upper', + 'pattern', + 'coerce_numbers_to_str', + 'ascii_only', +} +BYTES_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT} + +LIST_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT, *FAIL_FAST} +TUPLE_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT, *FAIL_FAST} +SET_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT, *FAIL_FAST} +DICT_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT} +GENERATOR_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT} +SEQUENCE_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *FAIL_FAST} + +FLOAT_CONSTRAINTS = {*NUMERIC_CONSTRAINTS, *ALLOW_INF_NAN, *STRICT} +DECIMAL_CONSTRAINTS = {'max_digits', 'decimal_places', *FLOAT_CONSTRAINTS} +INT_CONSTRAINTS = {*NUMERIC_CONSTRAINTS, *ALLOW_INF_NAN, *STRICT} +BOOL_CONSTRAINTS = STRICT +UUID_CONSTRAINTS = STRICT + +DATE_TIME_CONSTRAINTS = {*NUMERIC_CONSTRAINTS, *STRICT} +TIMEDELTA_CONSTRAINTS = {*NUMERIC_CONSTRAINTS, *STRICT} +TIME_CONSTRAINTS = {*NUMERIC_CONSTRAINTS, *STRICT} +LAX_OR_STRICT_CONSTRAINTS = STRICT +ENUM_CONSTRAINTS = STRICT +COMPLEX_CONSTRAINTS = STRICT + +UNION_CONSTRAINTS = {'union_mode'} +URL_CONSTRAINTS = { + 'max_length', + 'allowed_schemes', + 'host_required', + 'default_host', + 'default_port', + 'default_path', +} + +TEXT_SCHEMA_TYPES = ('str', 'bytes', 'url', 'multi-host-url') +SEQUENCE_SCHEMA_TYPES = ('list', 'tuple', 'set', 'frozenset', 'generator', *TEXT_SCHEMA_TYPES) +NUMERIC_SCHEMA_TYPES = ('float', 'int', 'date', 'time', 'timedelta', 'datetime') + +CONSTRAINTS_TO_ALLOWED_SCHEMAS: dict[str, set[str]] = defaultdict(set) + +constraint_schema_pairings: list[tuple[set[str], tuple[str, ...]]] = [ + (STR_CONSTRAINTS, TEXT_SCHEMA_TYPES), + (BYTES_CONSTRAINTS, ('bytes',)), + (LIST_CONSTRAINTS, ('list',)), + (TUPLE_CONSTRAINTS, ('tuple',)), + (SET_CONSTRAINTS, ('set', 'frozenset')), + (DICT_CONSTRAINTS, ('dict',)), + (GENERATOR_CONSTRAINTS, ('generator',)), + (FLOAT_CONSTRAINTS, ('float',)), + (INT_CONSTRAINTS, ('int',)), + (DATE_TIME_CONSTRAINTS, ('date', 'time', 'datetime', 'timedelta')), + # TODO: this is a bit redundant, we could probably avoid some of these + (STRICT, (*TEXT_SCHEMA_TYPES, *SEQUENCE_SCHEMA_TYPES, *NUMERIC_SCHEMA_TYPES, 'typed-dict', 'model')), + (UNION_CONSTRAINTS, ('union',)), + (URL_CONSTRAINTS, ('url', 'multi-host-url')), + (BOOL_CONSTRAINTS, ('bool',)), + (UUID_CONSTRAINTS, ('uuid',)), + (LAX_OR_STRICT_CONSTRAINTS, ('lax-or-strict',)), + (ENUM_CONSTRAINTS, ('enum',)), + (DECIMAL_CONSTRAINTS, ('decimal',)), + (COMPLEX_CONSTRAINTS, ('complex',)), +] + +for constraints, schemas in constraint_schema_pairings: + for c in constraints: + CONSTRAINTS_TO_ALLOWED_SCHEMAS[c].update(schemas) + + +def as_jsonable_value(v: Any) -> Any: + if type(v) not in (int, str, float, bytes, bool, type(None)): + return to_jsonable_python(v) + return v + + +def expand_grouped_metadata(annotations: Iterable[Any]) -> Iterable[Any]: + """Expand the annotations. + + Args: + annotations: An iterable of annotations. + + Returns: + An iterable of expanded annotations. + + Example: + ```python + from annotated_types import Ge, Len + + from pydantic._internal._known_annotated_metadata import expand_grouped_metadata + + print(list(expand_grouped_metadata([Ge(4), Len(5)]))) + #> [Ge(ge=4), MinLen(min_length=5)] + ``` + """ + import annotated_types as at + + FieldInfo = import_cached_field_info() + + for annotation in annotations: + if isinstance(annotation, at.GroupedMetadata): + yield from annotation + elif isinstance(annotation, FieldInfo): + yield from annotation.metadata + # this is a bit problematic in that it results in duplicate metadata + # all of our "consumers" can handle it, but it is not ideal + # we probably should split up FieldInfo into: + # - annotated types metadata + # - individual metadata known only to Pydantic + annotation = copy(annotation) + annotation.metadata = [] + yield annotation + else: + yield annotation + + +@lru_cache +def _get_at_to_constraint_map() -> dict[type, str]: + """Return a mapping of annotated types to constraints. + + Normally, we would define a mapping like this in the module scope, but we can't do that + because we don't permit module level imports of `annotated_types`, in an attempt to speed up + the import time of `pydantic`. We still only want to have this dictionary defined in one place, + so we use this function to cache the result. + """ + import annotated_types as at + + return { + at.Gt: 'gt', + at.Ge: 'ge', + at.Lt: 'lt', + at.Le: 'le', + at.MultipleOf: 'multiple_of', + at.MinLen: 'min_length', + at.MaxLen: 'max_length', + } + + +def apply_known_metadata(annotation: Any, schema: CoreSchema) -> CoreSchema | None: # noqa: C901 + """Apply `annotation` to `schema` if it is an annotation we know about (Gt, Le, etc.). + Otherwise return `None`. + + This does not handle all known annotations. If / when it does, it can always + return a CoreSchema and return the unmodified schema if the annotation should be ignored. + + Assumes that GroupedMetadata has already been expanded via `expand_grouped_metadata`. + + Args: + annotation: The annotation. + schema: The schema. + + Returns: + An updated schema with annotation if it is an annotation we know about, `None` otherwise. + + Raises: + RuntimeError: If a constraint can't be applied to a specific schema type. + ValueError: If an unknown constraint is encountered. + """ + import annotated_types as at + + from ._validators import NUMERIC_VALIDATOR_LOOKUP, forbid_inf_nan_check + + schema = schema.copy() + schema_update, other_metadata = collect_known_metadata([annotation]) + schema_type = schema['type'] + + chain_schema_constraints: set[str] = { + 'pattern', + 'strip_whitespace', + 'to_lower', + 'to_upper', + 'coerce_numbers_to_str', + 'ascii_only', + } + chain_schema_steps: list[CoreSchema] = [] + + for constraint, value in schema_update.items(): + if constraint not in CONSTRAINTS_TO_ALLOWED_SCHEMAS: + raise ValueError(f'Unknown constraint {constraint}') + allowed_schemas = CONSTRAINTS_TO_ALLOWED_SCHEMAS[constraint] + + # if it becomes necessary to handle more than one constraint + # in this recursive case with function-after or function-wrap, we should refactor + # this is a bit challenging because we sometimes want to apply constraints to the inner schema, + # whereas other times we want to wrap the existing schema with a new one that enforces a new constraint. + if schema_type in {'function-before', 'function-wrap', 'function-after'} and constraint == 'strict': + schema['schema'] = apply_known_metadata(annotation, schema['schema']) # type: ignore # schema is function schema + return schema + + # if we're allowed to apply constraint directly to the schema, like le to int, do that + if schema_type in allowed_schemas: + if constraint == 'union_mode' and schema_type == 'union': + schema['mode'] = value # type: ignore # schema is UnionSchema + else: + schema[constraint] = value + continue + + # else, apply a function after validator to the schema to enforce the corresponding constraint + if constraint in chain_schema_constraints: + + def _apply_constraint_with_incompatibility_info( + value: Any, handler: cs.ValidatorFunctionWrapHandler + ) -> Any: + try: + x = handler(value) + except ValidationError as ve: + # if the error is about the type, it's likely that the constraint is incompatible the type of the field + # for example, the following invalid schema wouldn't be caught during schema build, but rather at this point + # with a cryptic 'string_type' error coming from the string validator, + # that we'd rather express as a constraint incompatibility error (TypeError) + # Annotated[list[int], Field(pattern='abc')] + if 'type' in ve.errors()[0]['type']: + raise TypeError( + f"Unable to apply constraint '{constraint}' to supplied value {value} for schema of type '{schema_type}'" # noqa: B023 + ) + raise ve + return x + + chain_schema_steps.append( + cs.no_info_wrap_validator_function( + _apply_constraint_with_incompatibility_info, cs.str_schema(**{constraint: value}) + ) + ) + elif constraint in NUMERIC_VALIDATOR_LOOKUP: + if constraint in LENGTH_CONSTRAINTS: + inner_schema = schema + while inner_schema['type'] in {'function-before', 'function-wrap', 'function-after'}: + inner_schema = inner_schema['schema'] # type: ignore + inner_schema_type = inner_schema['type'] + if inner_schema_type == 'list' or ( + inner_schema_type == 'json-or-python' and inner_schema['json_schema']['type'] == 'list' # type: ignore + ): + js_constraint_key = 'minItems' if constraint == 'min_length' else 'maxItems' + else: + js_constraint_key = 'minLength' if constraint == 'min_length' else 'maxLength' + else: + js_constraint_key = constraint + + schema = cs.no_info_after_validator_function( + partial(NUMERIC_VALIDATOR_LOOKUP[constraint], **{constraint: value}), schema + ) + metadata = schema.get('metadata', {}) + if (existing_json_schema_updates := metadata.get('pydantic_js_updates')) is not None: + metadata['pydantic_js_updates'] = { + **existing_json_schema_updates, + **{js_constraint_key: as_jsonable_value(value)}, + } + else: + metadata['pydantic_js_updates'] = {js_constraint_key: as_jsonable_value(value)} + schema['metadata'] = metadata + elif constraint == 'allow_inf_nan' and value is False: + schema = cs.no_info_after_validator_function( + forbid_inf_nan_check, + schema, + ) + else: + # It's rare that we'd get here, but it's possible if we add a new constraint and forget to handle it + # Most constraint errors are caught at runtime during attempted application + raise RuntimeError(f"Unable to apply constraint '{constraint}' to schema of type '{schema_type}'") + + for annotation in other_metadata: + if (annotation_type := type(annotation)) in (at_to_constraint_map := _get_at_to_constraint_map()): + constraint = at_to_constraint_map[annotation_type] + validator = NUMERIC_VALIDATOR_LOOKUP.get(constraint) + if validator is None: + raise ValueError(f'Unknown constraint {constraint}') + schema = cs.no_info_after_validator_function( + partial(validator, {constraint: getattr(annotation, constraint)}), schema + ) + continue + elif isinstance(annotation, (at.Predicate, at.Not)): + predicate_name = f'{annotation.func.__qualname__!r} ' if hasattr(annotation.func, '__qualname__') else '' + + # Note: B023 is ignored because even though we iterate over `other_metadata`, it is guaranteed + # to be of length 1. `apply_known_metadata()` is called from `GenerateSchema`, where annotations + # were already expanded via `expand_grouped_metadata()`. Confusing, but this falls into the annotations + # refactor. + if isinstance(annotation, at.Predicate): + + def val_func(v: Any) -> Any: + predicate_satisfied = annotation.func(v) # noqa: B023 + if not predicate_satisfied: + raise PydanticCustomError( + 'predicate_failed', + f'Predicate {predicate_name}failed', # pyright: ignore[reportArgumentType] # noqa: B023 + ) + return v + + else: + + def val_func(v: Any) -> Any: + predicate_satisfied = annotation.func(v) # noqa: B023 + if predicate_satisfied: + raise PydanticCustomError( + 'not_operation_failed', + f'Not of {predicate_name}failed', # pyright: ignore[reportArgumentType] # noqa: B023 + ) + return v + + schema = cs.no_info_after_validator_function(val_func, schema) + else: + # ignore any other unknown metadata + return None + + if chain_schema_steps: + chain_schema_steps = [schema] + chain_schema_steps + return cs.chain_schema(chain_schema_steps) + + return schema + + +def collect_known_metadata(annotations: Iterable[Any]) -> tuple[dict[str, Any], list[Any]]: + """Split `annotations` into known metadata and unknown annotations. + + Args: + annotations: An iterable of annotations. + + Returns: + A tuple contains a dict of known metadata and a list of unknown annotations. + + Example: + ```python + from annotated_types import Gt, Len + + from pydantic._internal._known_annotated_metadata import collect_known_metadata + + print(collect_known_metadata([Gt(1), Len(42), ...])) + #> ({'gt': 1, 'min_length': 42}, [Ellipsis]) + ``` + """ + annotations = expand_grouped_metadata(annotations) + + res: dict[str, Any] = {} + remaining: list[Any] = [] + + for annotation in annotations: + # isinstance(annotation, PydanticMetadata) also covers ._fields:_PydanticGeneralMetadata + if isinstance(annotation, PydanticMetadata): + res.update(annotation.__dict__) + # we don't use dataclasses.asdict because that recursively calls asdict on the field values + elif (annotation_type := type(annotation)) in (at_to_constraint_map := _get_at_to_constraint_map()): + constraint = at_to_constraint_map[annotation_type] + res[constraint] = getattr(annotation, constraint) + elif isinstance(annotation, type) and issubclass(annotation, PydanticMetadata): + # also support PydanticMetadata classes being used without initialisation, + # e.g. `Annotated[int, Strict]` as well as `Annotated[int, Strict()]` + res.update({k: v for k, v in vars(annotation).items() if not k.startswith('_')}) + else: + remaining.append(annotation) + # Nones can sneak in but pydantic-core will reject them + # it'd be nice to clean things up so we don't put in None (we probably don't _need_ to, it was just easier) + # but this is simple enough to kick that can down the road + res = {k: v for k, v in res.items() if v is not None} + return res, remaining + + +def check_metadata(metadata: dict[str, Any], allowed: Iterable[str], source_type: Any) -> None: + """A small utility function to validate that the given metadata can be applied to the target. + More than saving lines of code, this gives us a consistent error message for all of our internal implementations. + + Args: + metadata: A dict of metadata. + allowed: An iterable of allowed metadata. + source_type: The source type. + + Raises: + TypeError: If there is metadatas that can't be applied on source type. + """ + unknown = metadata.keys() - set(allowed) + if unknown: + raise TypeError( + f'The following constraints cannot be applied to {source_type!r}: {", ".join([f"{k!r}" for k in unknown])}' + ) diff --git a/source/target-vendors/cp312/pydantic/_internal/_mock_val_ser.py b/source/target-vendors/cp312/pydantic/_internal/_mock_val_ser.py new file mode 100644 index 0000000..9125ab3 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_mock_val_ser.py @@ -0,0 +1,228 @@ +from __future__ import annotations + +from collections.abc import Iterator, Mapping +from typing import TYPE_CHECKING, Any, Callable, Generic, Literal, TypeVar, Union + +from pydantic_core import CoreSchema, SchemaSerializer, SchemaValidator + +from ..errors import PydanticErrorCodes, PydanticUserError +from ..plugin._schema_validator import PluggableSchemaValidator + +if TYPE_CHECKING: + from ..dataclasses import PydanticDataclass + from ..main import BaseModel + from ..type_adapter import TypeAdapter + + +ValSer = TypeVar('ValSer', bound=Union[SchemaValidator, PluggableSchemaValidator, SchemaSerializer]) +T = TypeVar('T') + + +class MockCoreSchema(Mapping[str, Any]): + """Mocker for `pydantic_core.CoreSchema` which optionally attempts to + rebuild the thing it's mocking when one of its methods is accessed and raises an error if that fails. + """ + + __slots__ = '_error_message', '_code', '_attempt_rebuild', '_built_memo' + + def __init__( + self, + error_message: str, + *, + code: PydanticErrorCodes, + attempt_rebuild: Callable[[], CoreSchema | None] | None = None, + ) -> None: + self._error_message = error_message + self._code: PydanticErrorCodes = code + self._attempt_rebuild = attempt_rebuild + self._built_memo: CoreSchema | None = None + + def __getitem__(self, key: str) -> Any: + return self._get_built().__getitem__(key) + + def __len__(self) -> int: + return self._get_built().__len__() + + def __iter__(self) -> Iterator[str]: + return self._get_built().__iter__() + + def _get_built(self) -> CoreSchema: + if self._built_memo is not None: + return self._built_memo + + if self._attempt_rebuild: + schema = self._attempt_rebuild() + if schema is not None: + self._built_memo = schema + return schema + raise PydanticUserError(self._error_message, code=self._code) + + def rebuild(self) -> CoreSchema | None: + self._built_memo = None + if self._attempt_rebuild: + schema = self._attempt_rebuild() + if schema is not None: + return schema + else: + raise PydanticUserError(self._error_message, code=self._code) + return None + + +class MockValSer(Generic[ValSer]): + """Mocker for `pydantic_core.SchemaValidator` or `pydantic_core.SchemaSerializer` which optionally attempts to + rebuild the thing it's mocking when one of its methods is accessed and raises an error if that fails. + """ + + __slots__ = '_error_message', '_code', '_val_or_ser', '_attempt_rebuild' + + def __init__( + self, + error_message: str, + *, + code: PydanticErrorCodes, + val_or_ser: Literal['validator', 'serializer'], + attempt_rebuild: Callable[[], ValSer | None] | None = None, + ) -> None: + self._error_message = error_message + self._val_or_ser = SchemaValidator if val_or_ser == 'validator' else SchemaSerializer + self._code: PydanticErrorCodes = code + self._attempt_rebuild = attempt_rebuild + + def __getattr__(self, item: str) -> None: + __tracebackhide__ = True + if self._attempt_rebuild: + val_ser = self._attempt_rebuild() + if val_ser is not None: + return getattr(val_ser, item) + + # raise an AttributeError if `item` doesn't exist + getattr(self._val_or_ser, item) + raise PydanticUserError(self._error_message, code=self._code) + + def rebuild(self) -> ValSer | None: + if self._attempt_rebuild: + val_ser = self._attempt_rebuild() + if val_ser is not None: + return val_ser + else: + raise PydanticUserError(self._error_message, code=self._code) + return None + + +def set_type_adapter_mocks(adapter: TypeAdapter) -> None: + """Set `core_schema`, `validator` and `serializer` to mock core types on a type adapter instance. + + Args: + adapter: The type adapter instance to set the mocks on + """ + type_repr = str(adapter._type) + undefined_type_error_message = ( + f'`TypeAdapter[{type_repr}]` is not fully defined; you should define `{type_repr}` and all referenced types,' + f' then call `.rebuild()` on the instance.' + ) + + def attempt_rebuild_fn(attr_fn: Callable[[TypeAdapter], T]) -> Callable[[], T | None]: + def handler() -> T | None: + if adapter.rebuild(raise_errors=False, _parent_namespace_depth=5) is not False: + return attr_fn(adapter) + return None + + return handler + + adapter.core_schema = MockCoreSchema( # pyright: ignore[reportAttributeAccessIssue] + undefined_type_error_message, + code='class-not-fully-defined', + attempt_rebuild=attempt_rebuild_fn(lambda ta: ta.core_schema), + ) + adapter.validator = MockValSer( # pyright: ignore[reportAttributeAccessIssue] + undefined_type_error_message, + code='class-not-fully-defined', + val_or_ser='validator', + attempt_rebuild=attempt_rebuild_fn(lambda ta: ta.validator), + ) + adapter.serializer = MockValSer( # pyright: ignore[reportAttributeAccessIssue] + undefined_type_error_message, + code='class-not-fully-defined', + val_or_ser='serializer', + attempt_rebuild=attempt_rebuild_fn(lambda ta: ta.serializer), + ) + + +def set_model_mocks(cls: type[BaseModel], undefined_name: str = 'all referenced types') -> None: + """Set `__pydantic_core_schema__`, `__pydantic_validator__` and `__pydantic_serializer__` to mock core types on a model. + + Args: + cls: The model class to set the mocks on + undefined_name: Name of the undefined thing, used in error messages + """ + undefined_type_error_message = ( + f'`{cls.__name__}` is not fully defined; you should define {undefined_name},' + f' then call `{cls.__name__}.model_rebuild()`.' + ) + + def attempt_rebuild_fn(attr_fn: Callable[[type[BaseModel]], T]) -> Callable[[], T | None]: + def handler() -> T | None: + if cls.model_rebuild(raise_errors=False, _parent_namespace_depth=5) is not False: + return attr_fn(cls) + return None + + return handler + + cls.__pydantic_core_schema__ = MockCoreSchema( # pyright: ignore[reportAttributeAccessIssue] + undefined_type_error_message, + code='class-not-fully-defined', + attempt_rebuild=attempt_rebuild_fn(lambda c: c.__pydantic_core_schema__), + ) + cls.__pydantic_validator__ = MockValSer( # pyright: ignore[reportAttributeAccessIssue] + undefined_type_error_message, + code='class-not-fully-defined', + val_or_ser='validator', + attempt_rebuild=attempt_rebuild_fn(lambda c: c.__pydantic_validator__), + ) + cls.__pydantic_serializer__ = MockValSer( # pyright: ignore[reportAttributeAccessIssue] + undefined_type_error_message, + code='class-not-fully-defined', + val_or_ser='serializer', + attempt_rebuild=attempt_rebuild_fn(lambda c: c.__pydantic_serializer__), + ) + + +def set_dataclass_mocks(cls: type[PydanticDataclass], undefined_name: str = 'all referenced types') -> None: + """Set `__pydantic_validator__` and `__pydantic_serializer__` to `MockValSer`s on a dataclass. + + Args: + cls: The model class to set the mocks on + undefined_name: Name of the undefined thing, used in error messages + """ + from ..dataclasses import rebuild_dataclass + + undefined_type_error_message = ( + f'`{cls.__name__}` is not fully defined; you should define {undefined_name},' + f' then call `pydantic.dataclasses.rebuild_dataclass({cls.__name__})`.' + ) + + def attempt_rebuild_fn(attr_fn: Callable[[type[PydanticDataclass]], T]) -> Callable[[], T | None]: + def handler() -> T | None: + if rebuild_dataclass(cls, raise_errors=False, _parent_namespace_depth=5) is not False: + return attr_fn(cls) + return None + + return handler + + cls.__pydantic_core_schema__ = MockCoreSchema( # pyright: ignore[reportAttributeAccessIssue] + undefined_type_error_message, + code='class-not-fully-defined', + attempt_rebuild=attempt_rebuild_fn(lambda c: c.__pydantic_core_schema__), + ) + cls.__pydantic_validator__ = MockValSer( # pyright: ignore[reportAttributeAccessIssue] + undefined_type_error_message, + code='class-not-fully-defined', + val_or_ser='validator', + attempt_rebuild=attempt_rebuild_fn(lambda c: c.__pydantic_validator__), + ) + cls.__pydantic_serializer__ = MockValSer( # pyright: ignore[reportAttributeAccessIssue] + undefined_type_error_message, + code='class-not-fully-defined', + val_or_ser='serializer', + attempt_rebuild=attempt_rebuild_fn(lambda c: c.__pydantic_serializer__), + ) diff --git a/source/target-vendors/cp312/pydantic/_internal/_model_construction.py b/source/target-vendors/cp312/pydantic/_internal/_model_construction.py new file mode 100644 index 0000000..b794544 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_model_construction.py @@ -0,0 +1,868 @@ +"""Private logic for creating models.""" + +from __future__ import annotations as _annotations + +import operator +import sys +import typing +import warnings +import weakref +from abc import ABCMeta +from functools import cache, partial, wraps +from types import FunctionType +from typing import TYPE_CHECKING, Any, Callable, Generic, Literal, NoReturn, TypeVar, cast + +from pydantic_core import PydanticUndefined, SchemaSerializer +from typing_extensions import TypeAliasType, dataclass_transform, deprecated, get_args, get_origin +from typing_inspection import typing_objects + +from ..errors import PydanticUndefinedAnnotation, PydanticUserError +from ..plugin._schema_validator import create_schema_validator +from ..warnings import GenericBeforeBaseModelWarning, PydanticDeprecatedSince20 +from ._config import ConfigWrapper +from ._decorators import DecoratorInfos, PydanticDescriptorProxy, get_attribute_from_bases, unwrap_wrapped_function +from ._fields import collect_model_fields, is_valid_field_name, is_valid_privateattr_name, rebuild_model_fields +from ._generate_schema import GenerateSchema, InvalidSchemaError +from ._generics import PydanticGenericMetadata, get_model_typevars_map +from ._import_utils import import_cached_base_model, import_cached_field_info +from ._mock_val_ser import set_model_mocks +from ._namespace_utils import NsResolver +from ._signature import generate_pydantic_signature +from ._typing_extra import ( + _make_forward_ref, + eval_type_backport, + is_classvar_annotation, + parent_frame_namespace, +) +from ._utils import LazyClassAttribute, SafeGetItemProxy + +if TYPE_CHECKING: + from ..fields import Field as PydanticModelField + from ..fields import FieldInfo, ModelPrivateAttr + from ..fields import PrivateAttr as PydanticModelPrivateAttr + from ..main import BaseModel + from ._fields import PydanticExtraInfo +else: + PydanticModelField = object() + PydanticModelPrivateAttr = object() + +object_setattr = object.__setattr__ + + +class _ModelNamespaceDict(dict): + """A dictionary subclass that intercepts attribute setting on model classes and + warns about overriding of decorators. + """ + + def __setitem__(self, k: str, v: object) -> None: + existing: Any = self.get(k, None) + if existing and v is not existing and isinstance(existing, PydanticDescriptorProxy): + warnings.warn( + f'`{k}` overrides an existing Pydantic `{existing.decorator_info.decorator_repr}` decorator', + stacklevel=2, + ) + + return super().__setitem__(k, v) + + +def NoInitField( + *, + init: Literal[False] = False, +) -> Any: + """Only for typing purposes. Used as default value of `__pydantic_fields_set__`, + `__pydantic_extra__`, `__pydantic_private__`, so they could be ignored when + synthesizing the `__init__` signature. + """ + + +# For ModelMetaclass.register(): +_T = TypeVar('_T') + + +@dataclass_transform(kw_only_default=True, field_specifiers=(PydanticModelField, PydanticModelPrivateAttr, NoInitField)) +class ModelMetaclass(ABCMeta): + def __new__( + mcs, + cls_name: str, + bases: tuple[type[Any], ...], + namespace: dict[str, Any], + __pydantic_generic_metadata__: PydanticGenericMetadata | None = None, + __pydantic_reset_parent_namespace__: bool = True, + _create_model_module: str | None = None, + **kwargs: Any, + ) -> type: + """Metaclass for creating Pydantic models. + + Args: + cls_name: The name of the class to be created. + bases: The base classes of the class to be created. + namespace: The attribute dictionary of the class to be created. + __pydantic_generic_metadata__: Metadata for generic models. + __pydantic_reset_parent_namespace__: Reset parent namespace. + _create_model_module: The module of the class to be created, if created by `create_model`. + **kwargs: Catch-all for any other keyword arguments. + + Returns: + The new class created by the metaclass. + """ + # Note `ModelMetaclass` refers to `BaseModel`, but is also used to *create* `BaseModel`, so we rely on the fact + # that `BaseModel` itself won't have any bases, but any subclass of it will, to determine whether the `__new__` + # call we're in the middle of is for the `BaseModel` class. + if bases: + raw_annotations: dict[str, Any] + if sys.version_info >= (3, 14): + if ( + '__annotations__' in namespace + ): # `from __future__ import annotations` was used in the model's module + raw_annotations = namespace['__annotations__'] + else: + # See https://docs.python.org/3.14/library/annotationlib.html#using-annotations-in-a-metaclass: + from annotationlib import Format, call_annotate_function, get_annotate_from_class_namespace + + if annotate := get_annotate_from_class_namespace(namespace): + raw_annotations = call_annotate_function(annotate, format=Format.FORWARDREF) + else: + raw_annotations = {} + else: + raw_annotations = namespace.get('__annotations__', {}) + + base_field_names, class_vars, base_private_attributes = mcs._collect_bases_data(bases) + + config_wrapper = ConfigWrapper.for_model(bases, namespace, raw_annotations, kwargs) + namespace['model_config'] = config_wrapper.config_dict + private_attributes = inspect_namespace( + namespace, raw_annotations, config_wrapper.ignored_types, class_vars, base_field_names + ) + if private_attributes or base_private_attributes: + original_model_post_init = get_model_post_init(namespace, bases) + if original_model_post_init is not None: + # if there are private attributes and a model_post_init function, we handle both + + @wraps(original_model_post_init) + def wrapped_model_post_init(self: BaseModel, context: Any, /) -> None: + """We need to both initialize private attributes and call the user-defined model_post_init + method. + """ + init_private_attributes(self, context) + original_model_post_init(self, context) + + namespace['model_post_init'] = wrapped_model_post_init + else: + namespace['model_post_init'] = init_private_attributes + + namespace['__class_vars__'] = class_vars + namespace['__private_attributes__'] = {**base_private_attributes, **private_attributes} + + cls = cast('type[BaseModel]', super().__new__(mcs, cls_name, bases, namespace, **kwargs)) + BaseModel_ = import_cached_base_model() + + mro = cls.__mro__ + if Generic in mro and mro.index(Generic) < mro.index(BaseModel_): + warnings.warn( + GenericBeforeBaseModelWarning( + 'Classes should inherit from `BaseModel` before generic classes (e.g. `typing.Generic[T]`) ' + 'for pydantic generics to work properly.' + ), + stacklevel=2, + ) + + cls.__pydantic_custom_init__ = not getattr(cls.__init__, '__pydantic_base_init__', False) + cls.__pydantic_post_init__ = ( + None if cls.model_post_init is BaseModel_.model_post_init else 'model_post_init' + ) + + cls.__pydantic_setattr_handlers__ = {} + + cls.__pydantic_decorators__ = DecoratorInfos.build(cls, replace_wrapped_methods=True) + cls.__pydantic_decorators__.update_from_config(config_wrapper) + + # Use the getattr below to grab the __parameters__ from the `typing.Generic` parent class + if __pydantic_generic_metadata__: + cls.__pydantic_generic_metadata__ = __pydantic_generic_metadata__ + else: + parent_parameters = getattr(cls, '__pydantic_generic_metadata__', {}).get('parameters', ()) + parameters = getattr(cls, '__parameters__', None) or parent_parameters + if parameters and parent_parameters and not all(x in parameters for x in parent_parameters): + from ..root_model import RootModelRootType + + missing_parameters = tuple(x for x in parameters if x not in parent_parameters) + if RootModelRootType in parent_parameters and RootModelRootType not in parameters: + # This is a special case where the user has subclassed RootModel, but has not parameterized + # RootModel with the generic type identifiers being used. Ex: + # class MyModel(RootModel, Generic[T]): + # root: T + # Should instead just be: + # class MyModel(RootModel[T]): + # root: T + parameters_str = ', '.join([x.__name__ for x in missing_parameters]) + error_message = ( + f'{cls.__name__} is a subclass of `RootModel`, but does not include the generic type identifier(s) ' + f'{parameters_str} in its parameters. ' + f'You should parametrize RootModel directly, e.g., `class {cls.__name__}(RootModel[{parameters_str}]): ...`.' + ) + else: + combined_parameters = parent_parameters + missing_parameters + parameters_str = ', '.join([str(x) for x in combined_parameters]) + generic_type_label = f'typing.Generic[{parameters_str}]' + error_message = ( + f'All parameters must be present on typing.Generic;' + f' you should inherit from {generic_type_label}.' + ) + if Generic not in bases: # pragma: no cover + # We raise an error here not because it is desirable, but because some cases are mishandled. + # It would be nice to remove this error and still have things behave as expected, it's just + # challenging because we are using a custom `__class_getitem__` to parametrize generic models, + # and not returning a typing._GenericAlias from it. + bases_str = ', '.join([x.__name__ for x in bases] + [generic_type_label]) + error_message += ( + f' Note: `typing.Generic` must go last: `class {cls.__name__}({bases_str}): ...`)' + ) + raise TypeError(error_message) + + cls.__pydantic_generic_metadata__ = { + 'origin': None, + 'args': (), + 'parameters': parameters, + } + + cls.__pydantic_complete__ = False # Ensure this specific class gets completed + + # preserve `__set_name__` protocol defined in https://peps.python.org/pep-0487 + # for attributes not in `new_namespace` (e.g. private attributes) + for name, obj in private_attributes.items(): + obj.__set_name__(cls, name) + + if __pydantic_reset_parent_namespace__: + cls.__pydantic_parent_namespace__ = build_lenient_weakvaluedict(parent_frame_namespace()) + parent_namespace: dict[str, Any] | None = getattr(cls, '__pydantic_parent_namespace__', None) + if isinstance(parent_namespace, dict): + parent_namespace = unpack_lenient_weakvaluedict(parent_namespace) + + ns_resolver = NsResolver(parent_namespace=parent_namespace) + + set_model_fields(cls, config_wrapper=config_wrapper, ns_resolver=ns_resolver) + + # This is also set in `complete_model_class()`, after schema gen because they are recreated. + # We set them here as well for backwards compatibility: + cls.__pydantic_computed_fields__ = { + k: v.info for k, v in cls.__pydantic_decorators__.computed_fields.items() + } + + if config_wrapper.defer_build: + set_model_mocks(cls) + else: + # Any operation that requires accessing the field infos instances should be put inside + # `complete_model_class()`: + complete_model_class( + cls, + config_wrapper, + ns_resolver, + raise_errors=False, + create_model_module=_create_model_module, + ) + + if config_wrapper.frozen and '__hash__' not in namespace: + set_default_hash_func(cls, bases) + + # using super(cls, cls) on the next line ensures we only call the parent class's __pydantic_init_subclass__ + # I believe the `type: ignore` is only necessary because mypy doesn't realize that this code branch is + # only hit for _proper_ subclasses of BaseModel + super(cls, cls).__pydantic_init_subclass__(**kwargs) # type: ignore[misc] + return cls + else: + # These are instance variables, but have been assigned to `NoInitField` to trick the type checker. + for instance_slot in '__pydantic_fields_set__', '__pydantic_extra__', '__pydantic_private__': + namespace.pop( + instance_slot, + None, # In case the metaclass is used with a class other than `BaseModel`. + ) + namespace.get('__annotations__', {}).clear() + return super().__new__(mcs, cls_name, bases, namespace, **kwargs) + + if not TYPE_CHECKING: # pragma: no branch + # We put `__getattr__` in a non-TYPE_CHECKING block because otherwise, mypy allows arbitrary attribute access + + def __getattr__(self, item: str) -> Any: + """This is necessary to keep attribute access working for class attribute access.""" + private_attributes = self.__dict__.get('__private_attributes__') + if private_attributes and item in private_attributes: + return private_attributes[item] + raise AttributeError(item) + + @classmethod + def __prepare__(cls, *args: Any, **kwargs: Any) -> dict[str, object]: + return _ModelNamespaceDict() + + # Due to performance and memory issues, in the ABCMeta.__subclasscheck__ implementation, we don't support + # registered virtual subclasses. See https://github.com/python/cpython/issues/92810#issuecomment-2762454345. + # This may change once CPython is fixed (possibly in 3.15), in which case we should conditionally + # define `register()`. + def register(self, subclass: type[_T]) -> type[_T]: + warnings.warn( + f"For performance reasons, virtual subclasses registered using '{self.__qualname__}.register()' " + "are not supported in 'isinstance()' and 'issubclass()' checks.", + stacklevel=2, + ) + return super().register(subclass) + + __instancecheck__ = type.__instancecheck__ # pyright: ignore[reportAssignmentType] + __subclasscheck__ = type.__subclasscheck__ # pyright: ignore[reportAssignmentType] + + @staticmethod + def _collect_bases_data(bases: tuple[type[Any], ...]) -> tuple[set[str], set[str], dict[str, ModelPrivateAttr]]: + BaseModel = import_cached_base_model() + + field_names: set[str] = set() + class_vars: set[str] = set() + private_attributes: dict[str, ModelPrivateAttr] = {} + for base in bases: + if issubclass(base, BaseModel) and base is not BaseModel: + # model_fields might not be defined yet in the case of generics, so we use getattr here: + field_names.update(getattr(base, '__pydantic_fields__', {}).keys()) + class_vars.update(base.__class_vars__) + private_attributes.update(base.__private_attributes__) + return field_names, class_vars, private_attributes + + @property + @deprecated( + 'The `__fields__` attribute is deprecated, use the `model_fields` class property instead.', category=None + ) + def __fields__(self) -> dict[str, FieldInfo]: + warnings.warn( + 'The `__fields__` attribute is deprecated, use the `model_fields` class property instead.', + PydanticDeprecatedSince20, + stacklevel=2, + ) + return getattr(self, '__pydantic_fields__', {}) + + @property + def __pydantic_fields_complete__(self) -> bool: + """Whether the fields were successfully collected (i.e. type hints were successfully resolved). + + This is a private attribute, not meant to be used outside Pydantic. + """ + if '__pydantic_fields__' not in self.__dict__: + return False + + field_infos = cast('dict[str, FieldInfo]', self.__pydantic_fields__) # pyright: ignore[reportAttributeAccessIssue] + + pydantic_extra_info = cast('PydanticExtraInfo | None', self.__pydantic_extra_info__) # pyright: ignore[reportAttributeAccessIssue] + if pydantic_extra_info is not None: + extra_complete = pydantic_extra_info.complete + else: + extra_complete = True + + return all(field_info._complete for field_info in field_infos.values()) and extra_complete + + def __dir__(self) -> list[str]: + attributes = list(super().__dir__()) + if '__fields__' in attributes: + attributes.remove('__fields__') + return attributes + + +def init_private_attributes(self: BaseModel, context: Any, /) -> None: + """This function is meant to behave like a BaseModel method to initialize private attributes. + + It takes context as an argument since that's what pydantic-core passes when calling it. + + Args: + self: The BaseModel instance. + context: The context. + """ + if getattr(self, '__pydantic_private__', None) is None: + pydantic_private = {} + for name, private_attr in self.__private_attributes__.items(): + # Avoid needlessly creating a new dict for the validated data: + if private_attr.default_factory_takes_validated_data: + default = private_attr.get_default( + call_default_factory=True, validated_data={**self.__dict__, **pydantic_private} + ) + else: + default = private_attr.get_default(call_default_factory=True) + if default is not PydanticUndefined: + pydantic_private[name] = default + object_setattr(self, '__pydantic_private__', pydantic_private) + + +def get_model_post_init(namespace: dict[str, Any], bases: tuple[type[Any], ...]) -> Callable[..., Any] | None: + """Get the `model_post_init` method from the namespace or the class bases, or `None` if not defined.""" + if 'model_post_init' in namespace: + return namespace['model_post_init'] + + BaseModel = import_cached_base_model() + + model_post_init = get_attribute_from_bases(bases, 'model_post_init') + if model_post_init is not BaseModel.model_post_init: + return model_post_init + + +def inspect_namespace( # noqa C901 + namespace: dict[str, Any], + raw_annotations: dict[str, Any], + ignored_types: tuple[type[Any], ...], + base_class_vars: set[str], + base_class_fields: set[str], +) -> dict[str, ModelPrivateAttr]: + """Iterate over the namespace and: + * gather private attributes + * check for items which look like fields but are not (e.g. have no annotation) and warn. + + Args: + namespace: The attribute dictionary of the class to be created. + raw_annotations: The (non-evaluated) annotations of the model. + ignored_types: A tuple of ignore types. + base_class_vars: A set of base class class variables. + base_class_fields: A set of base class fields. + + Returns: + A dict containing private attributes info. + + Raises: + TypeError: If there is a `__root__` field in model. + NameError: If private attribute name is invalid. + PydanticUserError: + - If a field does not have a type annotation. + - If a field on base class was overridden by a non-annotated attribute. + """ + from ..fields import ModelPrivateAttr, PrivateAttr + + FieldInfo = import_cached_field_info() + + all_ignored_types = ignored_types + default_ignored_types() + + private_attributes: dict[str, ModelPrivateAttr] = {} + + if '__root__' in raw_annotations or '__root__' in namespace: + raise TypeError("To define root models, use `pydantic.RootModel` rather than a field called '__root__'") + + ignored_names: set[str] = set() + for var_name, value in list(namespace.items()): + if var_name == 'model_config' or var_name == '__pydantic_extra__': + continue + elif ( + isinstance(value, type) + and value.__module__ == namespace['__module__'] + and '__qualname__' in namespace + and value.__qualname__.startswith(f'{namespace["__qualname__"]}.') + ): + # `value` is a nested type defined in this namespace; don't error + continue + elif isinstance(value, all_ignored_types) or value.__class__.__module__ == 'functools': + ignored_names.add(var_name) + continue + elif isinstance(value, ModelPrivateAttr): + if var_name.startswith('__'): + raise NameError( + 'Private attributes must not use dunder names;' + f' use a single underscore prefix instead of {var_name!r}.' + ) + elif is_valid_field_name(var_name): + raise NameError( + 'Private attributes must not use valid field names;' + f' use sunder names, e.g. {"_" + var_name!r} instead of {var_name!r}.' + ) + private_attributes[var_name] = value + del namespace[var_name] + elif isinstance(value, FieldInfo) and not is_valid_field_name(var_name): + suggested_name = var_name.lstrip('_') or 'my_field' # don't suggest '' for all-underscore name + raise NameError( + f'Fields must not use names with leading underscores;' + f' e.g., use {suggested_name!r} instead of {var_name!r}.' + ) + + elif var_name.startswith('__'): + continue + elif is_valid_privateattr_name(var_name): + if var_name not in raw_annotations or not is_classvar_annotation(raw_annotations[var_name]): + private_attributes[var_name] = cast(ModelPrivateAttr, PrivateAttr(default=value)) + del namespace[var_name] + elif var_name in base_class_vars: + continue + elif var_name not in raw_annotations: + if var_name in base_class_fields: + raise PydanticUserError( + f'Field {var_name!r} defined on a base class was overridden by a non-annotated attribute. ' + f'All field definitions, including overrides, require a type annotation.', + code='model-field-overridden', + ) + elif isinstance(value, FieldInfo): + raise PydanticUserError( + f'Field {var_name!r} requires a type annotation', code='model-field-missing-annotation' + ) + else: + raise PydanticUserError( + f'A non-annotated attribute was detected: `{var_name} = {value!r}`. All model fields require a ' + f'type annotation; if `{var_name}` is not meant to be a field, you may be able to resolve this ' + f"error by annotating it as a `ClassVar` or updating `model_config['ignored_types']`.", + code='model-field-missing-annotation', + ) + + for ann_name, ann_type in raw_annotations.items(): + if ( + is_valid_privateattr_name(ann_name) + and ann_name not in private_attributes + and ann_name not in ignored_names + # This condition can be a false negative when `ann_type` is stringified, + # but it is handled in most cases in `set_model_fields`: + and not is_classvar_annotation(ann_type) + and ann_type not in all_ignored_types + and getattr(ann_type, '__module__', None) != 'functools' + ): + if isinstance(ann_type, str): + # Walking up the frames to get the module namespace where the model is defined + # (as the model class wasn't created yet, we unfortunately can't use `cls.__module__`): + frame = sys._getframe(2) + if frame is not None: + try: + ann_type = eval_type_backport( + _make_forward_ref(ann_type, is_argument=False, is_class=True), + globalns=frame.f_globals, + localns=frame.f_locals, + ) + except (NameError, TypeError): + pass + + if typing_objects.is_annotated(get_origin(ann_type)): + _, *metadata = get_args(ann_type) + private_attr = next((v for v in metadata if isinstance(v, ModelPrivateAttr)), None) + if private_attr is not None: + private_attributes[ann_name] = private_attr + continue + private_attributes[ann_name] = PrivateAttr() + + return private_attributes + + +def set_default_hash_func(cls: type[BaseModel], bases: tuple[type[Any], ...]) -> None: + base_hash_func = get_attribute_from_bases(bases, '__hash__') + new_hash_func = make_hash_func(cls) + if base_hash_func in {None, object.__hash__} or getattr(base_hash_func, '__code__', None) == new_hash_func.__code__: + # If `__hash__` is some default, we generate a hash function. + # It will be `None` if not overridden from BaseModel. + # It may be `object.__hash__` if there is another + # parent class earlier in the bases which doesn't override `__hash__` (e.g. `typing.Generic`). + # It may be a value set by `set_default_hash_func` if `cls` is a subclass of another frozen model. + # In the last case we still need a new hash function to account for new `model_fields`. + cls.__hash__ = new_hash_func + + +def make_hash_func(cls: type[BaseModel]) -> Any: + getter = operator.itemgetter(*cls.__pydantic_fields__.keys()) if cls.__pydantic_fields__ else lambda _: 0 + + def hash_func(self: Any) -> int: + try: + return hash(getter(self.__dict__)) + except KeyError: + # In rare cases (such as when using the deprecated copy method), the __dict__ may not contain + # all model fields, which is how we can get here. + # getter(self.__dict__) is much faster than any 'safe' method that accounts for missing keys, + # and wrapping it in a `try` doesn't slow things down much in the common case. + return hash(getter(SafeGetItemProxy(self.__dict__))) + + return hash_func + + +def set_model_fields( + cls: type[BaseModel], + config_wrapper: ConfigWrapper, + ns_resolver: NsResolver, +) -> None: + """Collect and set `cls.__pydantic_fields__` and `cls.__class_vars__`. + + Args: + cls: BaseModel or dataclass. + config_wrapper: The config wrapper instance. + ns_resolver: Namespace resolver to use when getting model annotations. + """ + typevars_map = get_model_typevars_map(cls) + fields, pydantic_extra_info, class_vars = collect_model_fields( + cls, config_wrapper, ns_resolver, typevars_map=typevars_map + ) + + cls.__pydantic_fields__ = fields + cls.__pydantic_extra_info__ = pydantic_extra_info + cls.__class_vars__.update(class_vars) + + for k in class_vars: + # Class vars should not be private attributes + # We remove them _here_ and not earlier because we rely on inspecting the class to determine its classvars, + # but private attributes are determined by inspecting the namespace _prior_ to class creation. + # In the case that a classvar with a leading-'_' is defined via a ForwardRef (e.g., when using + # `__future__.annotations`), we want to remove the private attribute which was detected _before_ we knew it + # evaluated to a classvar + + value = cls.__private_attributes__.pop(k, None) + if value is not None and value.default is not PydanticUndefined: + setattr(cls, k, value.default) + + +def complete_model_class( + cls: type[BaseModel], + config_wrapper: ConfigWrapper, + ns_resolver: NsResolver, + *, + raise_errors: bool = True, + call_on_complete_hook: bool = True, + create_model_module: str | None = None, + is_force_rebuild: bool = False, +) -> bool: + """Finish building a model class. + + This logic must be called after class has been created since validation functions must be bound + and `get_type_hints` requires a class object. + + Args: + cls: BaseModel or dataclass. + config_wrapper: The config wrapper instance. + ns_resolver: The namespace resolver instance to use during schema building. + raise_errors: Whether to raise errors. + call_on_complete_hook: Whether to call the `__pydantic_on_complete__` hook. + create_model_module: The module of the class to be created, if created by `create_model`. + is_force_rebuild: Whether the model is being force-rebuilt (if True, pre-built serializers and + validators are not used, to avoid stale references). + + Returns: + `True` if the model is successfully completed, else `False`. + + Raises: + PydanticUndefinedAnnotation: If PydanticUndefinedAnnotation occurs in __get_pydantic_core_schema__ + and `raise_errors=True`. + """ + typevars_map = get_model_typevars_map(cls) + + if not cls.__pydantic_fields_complete__: + # Note: when coming from `ModelMetaclass.__new__()`, this results in fields being built twice. + # We do so a second time here so that we can get the ``NameError`` for the specific undefined annotation. + # Alternatively, we could let `GenerateSchema()` raise the error, but there are cases where incomplete + # fields are inherited in `collect_model_fields()` and can actually have their annotation resolved in the + # generate schema process. As we want to avoid having `__pydantic_fields_complete__` set to `False` + # when `__pydantic_complete__` is `True`, we rebuild here: + try: + cls.__pydantic_fields__, cls.__pydantic_extra_info__ = rebuild_model_fields( + cls, + config_wrapper=config_wrapper, + ns_resolver=ns_resolver, + typevars_map=typevars_map, + ) + except NameError as e: + exc = PydanticUndefinedAnnotation.from_name_error(e) + set_model_mocks(cls, f'`{exc.name}`') + if raise_errors: + raise exc from e + + if not raise_errors and not cls.__pydantic_fields_complete__: + # No need to continue with schema gen, it is guaranteed to fail + return False + + assert cls.__pydantic_fields_complete__ + + gen_schema = GenerateSchema( + config_wrapper, + ns_resolver, + typevars_map, + ) + + try: + schema = gen_schema.generate_schema(cls) + except PydanticUndefinedAnnotation as e: + if raise_errors: + raise + set_model_mocks(cls, f'`{e.name}`') + return False + + core_config = config_wrapper.core_config(title=cls.__name__) + + try: + schema = gen_schema.clean_schema(schema) + except InvalidSchemaError: + set_model_mocks(cls) + return False + + # This needs to happen *after* model schema generation, as the return types + # of the properties are evaluated and the `ComputedFieldInfo` are recreated: + cls.__pydantic_computed_fields__ = {k: v.info for k, v in cls.__pydantic_decorators__.computed_fields.items()} + + set_deprecated_descriptors(cls) + + cls.__pydantic_core_schema__ = schema + + cls.__pydantic_validator__ = create_schema_validator( + schema, + cls, + create_model_module or cls.__module__, + cls.__qualname__, + 'create_model' if create_model_module else 'BaseModel', + core_config, + config_wrapper.plugin_settings, + _use_prebuilt=not is_force_rebuild, + ) + cls.__pydantic_serializer__ = SchemaSerializer(schema, core_config, _use_prebuilt=not is_force_rebuild) + + # set __signature__ attr only for model class, but not for its instances + # (because instances can define `__call__`, and `inspect.signature` shouldn't + # use the `__signature__` attribute and instead generate from `__call__`). + cls.__signature__ = LazyClassAttribute( + '__signature__', + partial( + generate_pydantic_signature, + init=cls.__init__, + fields=cls.__pydantic_fields__, + validate_by_name=config_wrapper.validate_by_name, + extra=config_wrapper.extra, + ), + ) + + cls.__pydantic_complete__ = True + + if call_on_complete_hook: + cls.__pydantic_on_complete__() + + return True + + +def set_deprecated_descriptors(cls: type[BaseModel]) -> None: + """Set data descriptors on the class for deprecated fields.""" + for field, field_info in cls.__pydantic_fields__.items(): + if (msg := field_info.deprecation_message) is not None: + desc = _DeprecatedFieldDescriptor(msg) + desc.__set_name__(cls, field) + setattr(cls, field, desc) + + for field, computed_field_info in cls.__pydantic_computed_fields__.items(): + if ( + (msg := computed_field_info.deprecation_message) is not None + # Avoid having two warnings emitted: + and not hasattr(unwrap_wrapped_function(computed_field_info.wrapped_property), '__deprecated__') + ): + desc = _DeprecatedFieldDescriptor(msg, computed_field_info.wrapped_property) + desc.__set_name__(cls, field) + setattr(cls, field, desc) + + +class _DeprecatedFieldDescriptor: + """Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field. + + Attributes: + msg: The deprecation message to be emitted. + wrapped_property: The property instance if the deprecated field is a computed field, or `None`. + field_name: The name of the field being deprecated. + """ + + field_name: str + + def __init__(self, msg: str, wrapped_property: property | None = None) -> None: + self.msg = msg + self.wrapped_property = wrapped_property + + def __set_name__(self, cls: type[BaseModel], name: str) -> None: + self.field_name = name + + def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any: + if obj is None: + if self.wrapped_property is not None: + return self.wrapped_property.__get__(None, obj_type) + raise AttributeError(self.field_name) + + warnings.warn(self.msg, DeprecationWarning, stacklevel=2) + + if self.wrapped_property is not None: + return self.wrapped_property.__get__(obj, obj_type) + return obj.__dict__[self.field_name] + + # Defined to make it a data descriptor and take precedence over the instance's dictionary. + # Note that it will not be called when setting a value on a model instance + # as `BaseModel.__setattr__` is defined and takes priority. + def __set__(self, obj: Any, value: Any) -> NoReturn: + raise AttributeError(self.field_name) + + +class _PydanticWeakRef: + """Wrapper for `weakref.ref` that enables `pickle` serialization. + + Cloudpickle fails to serialize weakref.ref objects due to an arcane error related to + to abstract base classes (`abc.ABC`). This class works around the issue by wrapping + `weakref.ref` instead of subclassing it. + + See https://github.com/pydantic/pydantic/issues/6763 for context. + + Semantics: + - If not pickled, behaves the same as a `weakref.ref`. + - If pickled along with the referenced object, the same `weakref.ref` behavior + will be maintained between them after unpickling. + - If pickled without the referenced object, after unpickling the underlying + reference will be cleared (`__call__` will always return `None`). + """ + + def __init__(self, obj: Any): + if obj is None: + # The object will be `None` upon deserialization if the serialized weakref + # had lost its underlying object. + self._wr = None + else: + self._wr = weakref.ref(obj) + + def __call__(self) -> Any: + if self._wr is None: + return None + else: + return self._wr() + + def __reduce__(self) -> tuple[Callable, tuple[weakref.ReferenceType | None]]: + return _PydanticWeakRef, (self(),) + + +def build_lenient_weakvaluedict(d: dict[str, Any] | None) -> dict[str, Any] | None: + """Takes an input dictionary, and produces a new value that (invertibly) replaces the values with weakrefs. + + We can't just use a WeakValueDictionary because many types (including int, str, etc.) can't be stored as values + in a WeakValueDictionary. + + The `unpack_lenient_weakvaluedict` function can be used to reverse this operation. + """ + if d is None: + return None + result = {} + for k, v in d.items(): + try: + proxy = _PydanticWeakRef(v) + except TypeError: + proxy = v + result[k] = proxy + return result + + +def unpack_lenient_weakvaluedict(d: dict[str, Any] | None) -> dict[str, Any] | None: + """Inverts the transform performed by `build_lenient_weakvaluedict`.""" + if d is None: + return None + + result = {} + for k, v in d.items(): + if isinstance(v, _PydanticWeakRef): + v = v() + if v is not None: + result[k] = v + else: + result[k] = v + return result + + +@cache +def default_ignored_types() -> tuple[type[Any], ...]: + from ..fields import ComputedFieldInfo + + ignored_types = [ + FunctionType, + property, + classmethod, + staticmethod, + PydanticDescriptorProxy, + ComputedFieldInfo, + TypeAliasType, # from `typing_extensions` + ] + + if sys.version_info >= (3, 12): + ignored_types.append(typing.TypeAliasType) + + return tuple(ignored_types) diff --git a/source/target-vendors/cp312/pydantic/_internal/_namespace_utils.py b/source/target-vendors/cp312/pydantic/_internal/_namespace_utils.py new file mode 100644 index 0000000..af0cddb --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_namespace_utils.py @@ -0,0 +1,293 @@ +from __future__ import annotations + +import sys +from collections.abc import Generator, Iterator, Mapping +from contextlib import contextmanager +from functools import cached_property +from typing import Any, Callable, NamedTuple, TypeVar + +from typing_extensions import ParamSpec, TypeAlias, TypeAliasType, TypeVarTuple + +GlobalsNamespace: TypeAlias = 'dict[str, Any]' +"""A global namespace. + +In most cases, this is a reference to the `__dict__` attribute of a module. +This namespace type is expected as the `globals` argument during annotations evaluation. +""" + +MappingNamespace: TypeAlias = Mapping[str, Any] +"""Any kind of namespace. + +In most cases, this is a local namespace (e.g. the `__dict__` attribute of a class, +the [`f_locals`][frame.f_locals] attribute of a frame object, when dealing with types +defined inside functions). +This namespace type is expected as the `locals` argument during annotations evaluation. +""" + +_TypeVarLike: TypeAlias = 'TypeVar | ParamSpec | TypeVarTuple' + + +class NamespacesTuple(NamedTuple): + """A tuple of globals and locals to be used during annotations evaluation. + + This datastructure is defined as a named tuple so that it can easily be unpacked: + + ```python {lint="skip" test="skip"} + def eval_type(typ: type[Any], ns: NamespacesTuple) -> None: + return eval(typ, *ns) + ``` + """ + + globals: GlobalsNamespace + """The namespace to be used as the `globals` argument during annotations evaluation.""" + + locals: MappingNamespace + """The namespace to be used as the `locals` argument during annotations evaluation.""" + + +def get_module_ns_of(obj: Any) -> dict[str, Any]: + """Get the namespace of the module where the object is defined. + + Caution: this function does not return a copy of the module namespace, so the result + should not be mutated. The burden of enforcing this is on the caller. + """ + module_name = getattr(obj, '__module__', None) + if module_name: + try: + return sys.modules[module_name].__dict__ + except KeyError: + # happens occasionally, see https://github.com/pydantic/pydantic/issues/2363 + return {} + return {} + + +# Note that this class is almost identical to `collections.ChainMap`, but need to enforce +# immutable mappings here: +class LazyLocalNamespace(Mapping[str, Any]): + """A lazily evaluated mapping, to be used as the `locals` argument during annotations evaluation. + + While the [`eval`][eval] function expects a mapping as the `locals` argument, it only + performs `__getitem__` calls. The [`Mapping`][collections.abc.Mapping] abstract base class + is fully implemented only for type checking purposes. + + Args: + *namespaces: The namespaces to consider, in ascending order of priority. + + Example: + ```python {lint="skip" test="skip"} + ns = LazyLocalNamespace({'a': 1, 'b': 2}, {'a': 3}) + ns['a'] + #> 3 + ns['b'] + #> 2 + ``` + """ + + def __init__(self, *namespaces: MappingNamespace) -> None: + self._namespaces = namespaces + + @cached_property + def data(self) -> dict[str, Any]: + return {k: v for ns in self._namespaces for k, v in ns.items()} + + def __len__(self) -> int: + return len(self.data) + + def __getitem__(self, key: str) -> Any: + return self.data[key] + + def __contains__(self, key: object) -> bool: + return key in self.data + + def __iter__(self) -> Iterator[str]: + return iter(self.data) + + +def ns_for_function(obj: Callable[..., Any], parent_namespace: MappingNamespace | None = None) -> NamespacesTuple: + """Return the global and local namespaces to be used when evaluating annotations for the provided function. + + The global namespace will be the `__dict__` attribute of the module the function was defined in. + The local namespace will contain the `__type_params__` introduced by PEP 695. + + Args: + obj: The object to use when building namespaces. + parent_namespace: Optional namespace to be added with the lowest priority in the local namespace. + If the passed function is a method, the `parent_namespace` will be the namespace of the class + the method is defined in. Thus, we also fetch type `__type_params__` from there (i.e. the + class-scoped type variables). + """ + locals_list: list[MappingNamespace] = [] + if parent_namespace is not None: + locals_list.append(parent_namespace) + + # Get the `__type_params__` attribute introduced by PEP 695. + # Note that the `typing._eval_type` function expects type params to be + # passed as a separate argument. However, internally, `_eval_type` calls + # `ForwardRef._evaluate` which will merge type params with the localns, + # essentially mimicking what we do here. + type_params: tuple[_TypeVarLike, ...] = getattr(obj, '__type_params__', ()) + if parent_namespace is not None: + # We also fetch type params from the parent namespace. If present, it probably + # means the function was defined in a class. This is to support the following: + # https://github.com/python/cpython/issues/124089. + type_params += parent_namespace.get('__type_params__', ()) + + locals_list.append({t.__name__: t for t in type_params}) + + # What about short-circuiting to `obj.__globals__`? + globalns = get_module_ns_of(obj) + + return NamespacesTuple(globalns, LazyLocalNamespace(*locals_list)) + + +class NsResolver: + """A class responsible for the namespaces resolving logic for annotations evaluation. + + This class handles the namespace logic when evaluating annotations mainly for class objects. + + It holds a stack of classes that are being inspected during the core schema building, + and the `types_namespace` property exposes the globals and locals to be used for + type annotation evaluation. Additionally -- if no class is present in the stack -- a + fallback globals and locals can be provided using the `namespaces_tuple` argument + (this is useful when generating a schema for a simple annotation, e.g. when using + `TypeAdapter`). + + The namespace creation logic is unfortunately flawed in some cases, for backwards + compatibility reasons and to better support valid edge cases. See the description + for the `parent_namespace` argument and the example for more details. + + Args: + namespaces_tuple: The default globals and locals to use if no class is present + on the stack. This can be useful when using the `GenerateSchema` class + with `TypeAdapter`, where the "type" being analyzed is a simple annotation. + parent_namespace: An optional parent namespace that will be added to the locals + with the lowest priority. For a given class defined in a function, the locals + of this function are usually used as the parent namespace: + + ```python {lint="skip" test="skip"} + from pydantic import BaseModel + + def func() -> None: + SomeType = int + + class Model(BaseModel): + f: 'SomeType' + + # when collecting fields, an namespace resolver instance will be created + # this way: + # ns_resolver = NsResolver(parent_namespace={'SomeType': SomeType}) + ``` + + For backwards compatibility reasons and to support valid edge cases, this parent + namespace will be used for *every* type being pushed to the stack. In the future, + we might want to be smarter by only doing so when the type being pushed is defined + in the same module as the parent namespace. + + Example: + ```python {lint="skip" test="skip"} + ns_resolver = NsResolver( + parent_namespace={'fallback': 1}, + ) + + class Sub: + m: 'Model' + + class Model: + some_local = 1 + sub: Sub + + ns_resolver = NsResolver() + + # This is roughly what happens when we build a core schema for `Model`: + with ns_resolver.push(Model): + ns_resolver.types_namespace + #> NamespacesTuple({'Sub': Sub}, {'Model': Model, 'some_local': 1}) + # First thing to notice here, the model being pushed is added to the locals. + # Because `NsResolver` is being used during the model definition, it is not + # yet added to the globals. This is useful when resolving self-referencing annotations. + + with ns_resolver.push(Sub): + ns_resolver.types_namespace + #> NamespacesTuple({'Sub': Sub}, {'Sub': Sub, 'Model': Model}) + # Second thing to notice: `Sub` is present in both the globals and locals. + # This is not an issue, just that as described above, the model being pushed + # is added to the locals, but it happens to be present in the globals as well + # because it is already defined. + # Third thing to notice: `Model` is also added in locals. This is a backwards + # compatibility workaround that allows for `Sub` to be able to resolve `'Model'` + # correctly (as otherwise models would have to be rebuilt even though this + # doesn't look necessary). + ``` + """ + + def __init__( + self, + namespaces_tuple: NamespacesTuple | None = None, + parent_namespace: MappingNamespace | None = None, + ) -> None: + self._base_ns_tuple = namespaces_tuple or NamespacesTuple({}, {}) + self._parent_ns = parent_namespace + self._types_stack: list[type[Any] | TypeAliasType] = [] + + @cached_property + def types_namespace(self) -> NamespacesTuple: + """The current global and local namespaces to be used for annotations evaluation.""" + if not self._types_stack: + # TODO: should we merge the parent namespace here? + # This is relevant for TypeAdapter, where there are no types on the stack, and we might + # need access to the parent_ns. Right now, we sidestep this in `type_adapter.py` by passing + # locals to both parent_ns and the base_ns_tuple, but this is a bit hacky. + # we might consider something like: + # if self._parent_ns is not None: + # # Hacky workarounds, see class docstring: + # # An optional parent namespace that will be added to the locals with the lowest priority + # locals_list: list[MappingNamespace] = [self._parent_ns, self._base_ns_tuple.locals] + # return NamespacesTuple(self._base_ns_tuple.globals, LazyLocalNamespace(*locals_list)) + return self._base_ns_tuple + + typ = self._types_stack[-1] + + globalns = get_module_ns_of(typ) + + locals_list: list[MappingNamespace] = [] + # Hacky workarounds, see class docstring: + # An optional parent namespace that will be added to the locals with the lowest priority + if self._parent_ns is not None: + locals_list.append(self._parent_ns) + if len(self._types_stack) > 1: + first_type = self._types_stack[0] + locals_list.append({first_type.__name__: first_type}) + + # Adding `__type_params__` *before* `vars(typ)`, as the latter takes priority + # (see https://github.com/python/cpython/pull/120272). + # TODO `typ.__type_params__` when we drop support for Python 3.11: + type_params: tuple[_TypeVarLike, ...] = getattr(typ, '__type_params__', ()) + if type_params: + # Adding `__type_params__` is mostly useful for generic classes defined using + # PEP 695 syntax *and* using forward annotations (see the example in + # https://github.com/python/cpython/issues/114053). For TypeAliasType instances, + # it is way less common, but still required if using a string annotation in the alias + # value, e.g. `type A[T] = 'T'` (which is not necessary in most cases). + locals_list.append({t.__name__: t for t in type_params}) + + # TypeAliasType instances don't have a `__dict__` attribute, so the check + # is necessary: + if hasattr(typ, '__dict__'): + locals_list.append(vars(typ)) + + # The `len(self._types_stack) > 1` check above prevents this from being added twice: + locals_list.append({typ.__name__: typ}) + + return NamespacesTuple(globalns, LazyLocalNamespace(*locals_list)) + + @contextmanager + def push(self, typ: type[Any] | TypeAliasType, /) -> Generator[None]: + """Push a type to the stack.""" + self._types_stack.append(typ) + # Reset the cached property: + self.__dict__.pop('types_namespace', None) + try: + yield + finally: + self._types_stack.pop() + self.__dict__.pop('types_namespace', None) diff --git a/source/target-vendors/cp312/pydantic/_internal/_repr.py b/source/target-vendors/cp312/pydantic/_internal/_repr.py new file mode 100644 index 0000000..7e80a9c --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_repr.py @@ -0,0 +1,124 @@ +"""Tools to provide pretty/human-readable display of objects.""" + +from __future__ import annotations as _annotations + +import types +from collections.abc import Callable, Collection, Generator, Iterable +from typing import TYPE_CHECKING, Any, ForwardRef, cast + +import typing_extensions +from typing_extensions import TypeAlias +from typing_inspection import typing_objects +from typing_inspection.introspection import is_union_origin + +from . import _typing_extra + +if TYPE_CHECKING: + # TODO remove type error comments when we drop support for Python 3.9 + ReprArgs: TypeAlias = Iterable[tuple[str | None, Any]] # pyright: ignore[reportGeneralTypeIssues] + RichReprResult: TypeAlias = Iterable[Any | tuple[Any] | tuple[str, Any] | tuple[str, Any, Any]] # pyright: ignore[reportGeneralTypeIssues] + + +class PlainRepr(str): + """String class where repr doesn't include quotes. Useful with Representation when you want to return a string + representation of something that is valid (or pseudo-valid) python. + """ + + def __repr__(self) -> str: + return str(self) + + +class Representation: + # Mixin to provide `__str__`, `__repr__`, and `__pretty__` and `__rich_repr__` methods. + # `__pretty__` is used by [devtools](https://python-devtools.helpmanual.io/). + # `__rich_repr__` is used by [rich](https://rich.readthedocs.io/en/stable/pretty.html). + # (this is not a docstring to avoid adding a docstring to classes which inherit from Representation) + + __slots__ = () + + def __repr_args__(self) -> ReprArgs: + """Returns the attributes to show in __str__, __repr__, and __pretty__ this is generally overridden. + + Can either return: + * name - value pairs, e.g.: `[('foo_name', 'foo'), ('bar_name', ['b', 'a', 'r'])]` + * or, just values, e.g.: `[(None, 'foo'), (None, ['b', 'a', 'r'])]` + """ + attrs_names = cast(Collection[str], self.__slots__) + if not attrs_names and hasattr(self, '__dict__'): + attrs_names = self.__dict__.keys() + attrs = ((s, getattr(self, s)) for s in attrs_names) + return [(a, v if v is not self else self.__repr_recursion__(v)) for a, v in attrs if v is not None] + + def __repr_name__(self) -> str: + """Name of the instance's class, used in __repr__.""" + return self.__class__.__name__ + + def __repr_recursion__(self, object: Any) -> str: + """Returns the string representation of a recursive object.""" + # This is copied over from the stdlib `pprint` module: + return f'' + + def __repr_str__(self, join_str: str) -> str: + return join_str.join(repr(v) if a is None else f'{a}={v!r}' for a, v in self.__repr_args__()) + + def __pretty__(self, fmt: Callable[[Any], Any], **kwargs: Any) -> Generator[Any]: + """Used by devtools (https://python-devtools.helpmanual.io/) to pretty print objects.""" + yield self.__repr_name__() + '(' + yield 1 + for name, value in self.__repr_args__(): + if name is not None: + yield name + '=' + yield fmt(value) + yield ',' + yield 0 + yield -1 + yield ')' + + def __rich_repr__(self) -> RichReprResult: + """Used by Rich (https://rich.readthedocs.io/en/stable/pretty.html) to pretty print objects.""" + for name, field_repr in self.__repr_args__(): + if name is None: + yield field_repr + else: + yield name, field_repr + + def __str__(self) -> str: + return self.__repr_str__(' ') + + def __repr__(self) -> str: + return f'{self.__repr_name__()}({self.__repr_str__(", ")})' + + +def display_as_type(obj: Any) -> str: + """Pretty representation of a type, should be as close as possible to the original type definition string. + + Takes some logic from `typing._type_repr`. + """ + if isinstance(obj, (types.FunctionType, types.BuiltinFunctionType)): + return obj.__name__ + elif obj is ...: + return '...' + elif isinstance(obj, Representation): + return repr(obj) + elif isinstance(obj, ForwardRef) or typing_objects.is_typealiastype(obj): + return str(obj) + + if not isinstance(obj, (_typing_extra.typing_base, _typing_extra.WithArgsTypes, type)): + obj = obj.__class__ + + if is_union_origin(typing_extensions.get_origin(obj)): + args = ', '.join(map(display_as_type, typing_extensions.get_args(obj))) + return f'Union[{args}]' + elif isinstance(obj, _typing_extra.WithArgsTypes): + if typing_objects.is_literal(typing_extensions.get_origin(obj)): + args = ', '.join(map(repr, typing_extensions.get_args(obj))) + else: + args = ', '.join(map(display_as_type, typing_extensions.get_args(obj))) + try: + return f'{obj.__qualname__}[{args}]' + except AttributeError: + return str(obj).replace('typing.', '').replace('typing_extensions.', '') # handles TypeAliasType in 3.12 + elif isinstance(obj, type): + return obj.__qualname__ + else: + return repr(obj).replace('typing.', '').replace('typing_extensions.', '') diff --git a/source/target-vendors/cp312/pydantic/_internal/_schema_gather.py b/source/target-vendors/cp312/pydantic/_internal/_schema_gather.py new file mode 100644 index 0000000..8e07b20 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_schema_gather.py @@ -0,0 +1,212 @@ +# pyright: reportTypedDictNotRequiredAccess=false, reportGeneralTypeIssues=false, reportArgumentType=false, reportAttributeAccessIssue=false +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TypedDict + +from pydantic_core.core_schema import ( + ComputedField, + CoreSchema, + DefinitionReferenceSchema, + SerSchema, + iter_union_choices, +) +from typing_extensions import TypeAlias + +AllSchemas: TypeAlias = 'CoreSchema | SerSchema | ComputedField' + + +class GatherResult(TypedDict): + """Schema traversing result.""" + + collected_references: dict[str, DefinitionReferenceSchema | None] + """The collected definition references. + + If a definition reference schema can be inlined, it means that there is + only one in the whole core schema. As such, it is stored as the value. + Otherwise, the value is set to `None`. + """ + + deferred_discriminator_schemas: list[CoreSchema] + """The list of core schemas having the discriminator application deferred.""" + + +class MissingDefinitionError(LookupError): + """A reference was pointing to a non-existing core schema.""" + + def __init__(self, schema_reference: str, /) -> None: + self.schema_reference = schema_reference + + +@dataclass +class GatherContext: + """The current context used during core schema traversing. + + Context instances should only be used during schema traversing. + """ + + definitions: dict[str, CoreSchema] + """The available definitions.""" + + deferred_discriminator_schemas: list[CoreSchema] = field(init=False, default_factory=list) + """The list of core schemas having the discriminator application deferred. + + Internally, these core schemas have a specific key set in the core metadata dict. + """ + + collected_references: dict[str, DefinitionReferenceSchema | None] = field(init=False, default_factory=dict) + """The collected definition references. + + If a definition reference schema can be inlined, it means that there is + only one in the whole core schema. As such, it is stored as the value. + Otherwise, the value is set to `None`. + + During schema traversing, definition reference schemas can be added as candidates, or removed + (by setting the value to `None`). + """ + + +def traverse_metadata(schema: AllSchemas, ctx: GatherContext) -> None: + meta = schema.get('metadata') + if meta is not None and 'pydantic_internal_union_discriminator' in meta: + ctx.deferred_discriminator_schemas.append(schema) # pyright: ignore[reportArgumentType] + + +def traverse_definition_ref(def_ref_schema: DefinitionReferenceSchema, ctx: GatherContext) -> None: + schema_ref = def_ref_schema['schema_ref'] + + if schema_ref not in ctx.collected_references: + definition = ctx.definitions.get(schema_ref) + if definition is None: + raise MissingDefinitionError(schema_ref) + + # The `'definition-ref'` schema was only encountered once, make it + # a candidate to be inlined: + ctx.collected_references[schema_ref] = def_ref_schema + traverse_schema(definition, ctx) + if 'serialization' in def_ref_schema: + traverse_schema(def_ref_schema['serialization'], ctx) + traverse_metadata(def_ref_schema, ctx) + else: + # The `'definition-ref'` schema was already encountered, meaning + # the previously encountered schema (and this one) can't be inlined: + ctx.collected_references[schema_ref] = None + + +def traverse_schema(schema: AllSchemas, context: GatherContext) -> None: + # TODO When we drop 3.9, use a match statement to get better type checking and remove + # file-level type ignore. + # (the `'type'` could also be fetched in every `if/elif` statement, but this alters performance). + schema_type = schema['type'] + + if schema_type == 'definition-ref': + traverse_definition_ref(schema, context) + # `traverse_definition_ref` handles the possible serialization and metadata schemas: + return + elif schema_type == 'definitions': + traverse_schema(schema['schema'], context) + for definition in schema['definitions']: + traverse_schema(definition, context) + elif schema_type in {'list', 'set', 'frozenset', 'generator'}: + if 'items_schema' in schema: + traverse_schema(schema['items_schema'], context) + elif schema_type == 'tuple': + if 'items_schema' in schema: + for s in schema['items_schema']: + traverse_schema(s, context) + elif schema_type == 'dict': + if 'keys_schema' in schema: + traverse_schema(schema['keys_schema'], context) + if 'values_schema' in schema: + traverse_schema(schema['values_schema'], context) + elif schema_type == 'union': + for choice in iter_union_choices(schema): + traverse_schema(choice, context) + elif schema_type == 'tagged-union': + for v in schema['choices'].values(): + traverse_schema(v, context) + elif schema_type == 'chain': + for step in schema['steps']: + traverse_schema(step, context) + elif schema_type == 'lax-or-strict': + traverse_schema(schema['lax_schema'], context) + traverse_schema(schema['strict_schema'], context) + elif schema_type == 'json-or-python': + traverse_schema(schema['json_schema'], context) + traverse_schema(schema['python_schema'], context) + elif schema_type in {'model-fields', 'typed-dict'}: + if 'extras_schema' in schema: + traverse_schema(schema['extras_schema'], context) + if 'computed_fields' in schema: + for s in schema['computed_fields']: + traverse_schema(s, context) + for s in schema['fields'].values(): + traverse_schema(s, context) + elif schema_type == 'dataclass-args': + if 'computed_fields' in schema: + for s in schema['computed_fields']: + traverse_schema(s, context) + for s in schema['fields']: + traverse_schema(s, context) + elif schema_type == 'arguments': + for s in schema['arguments_schema']: + traverse_schema(s['schema'], context) + if 'var_args_schema' in schema: + traverse_schema(schema['var_args_schema'], context) + if 'var_kwargs_schema' in schema: + traverse_schema(schema['var_kwargs_schema'], context) + elif schema_type == 'arguments-v3': + for s in schema['arguments_schema']: + traverse_schema(s['schema'], context) + elif schema_type == 'call': + traverse_schema(schema['arguments_schema'], context) + if 'return_schema' in schema: + traverse_schema(schema['return_schema'], context) + elif schema_type == 'computed-field': + traverse_schema(schema['return_schema'], context) + elif schema_type == 'function-before': + if 'schema' in schema: + traverse_schema(schema['schema'], context) + if 'json_schema_input_schema' in schema: + traverse_schema(schema['json_schema_input_schema'], context) + elif schema_type == 'function-plain': + # TODO duplicate schema types for serializers and validators, needs to be deduplicated. + if 'return_schema' in schema: + traverse_schema(schema['return_schema'], context) + if 'json_schema_input_schema' in schema: + traverse_schema(schema['json_schema_input_schema'], context) + elif schema_type == 'function-wrap': + # TODO duplicate schema types for serializers and validators, needs to be deduplicated. + if 'return_schema' in schema: + traverse_schema(schema['return_schema'], context) + if 'schema' in schema: + traverse_schema(schema['schema'], context) + if 'json_schema_input_schema' in schema: + traverse_schema(schema['json_schema_input_schema'], context) + else: + if 'schema' in schema: + traverse_schema(schema['schema'], context) + + if 'serialization' in schema: + traverse_schema(schema['serialization'], context) + traverse_metadata(schema, context) + + +def gather_schemas_for_cleaning(schema: CoreSchema, definitions: dict[str, CoreSchema]) -> GatherResult: + """Traverse the core schema and definitions and return the necessary information for schema cleaning. + + During the core schema traversing, any `'definition-ref'` schema is: + + - Validated: the reference must point to an existing definition. If this is not the case, a + `MissingDefinitionError` exception is raised. + - Stored in the context: the actual reference is stored in the context. Depending on whether + the `'definition-ref'` schema is encountered more that once, the schema itself is also + saved in the context to be inlined (i.e. replaced by the definition it points to). + """ + context = GatherContext(definitions) + traverse_schema(schema, context) + + return { + 'collected_references': context.collected_references, + 'deferred_discriminator_schemas': context.deferred_discriminator_schemas, + } diff --git a/source/target-vendors/cp312/pydantic/_internal/_schema_generation_shared.py b/source/target-vendors/cp312/pydantic/_internal/_schema_generation_shared.py new file mode 100644 index 0000000..b231a82 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_schema_generation_shared.py @@ -0,0 +1,125 @@ +"""Types and utility functions used by various other internal tools.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Callable, Literal + +from pydantic_core import core_schema + +from ..annotated_handlers import GetCoreSchemaHandler, GetJsonSchemaHandler + +if TYPE_CHECKING: + from ..json_schema import GenerateJsonSchema, JsonSchemaValue + from ._core_utils import CoreSchemaOrField + from ._generate_schema import GenerateSchema + from ._namespace_utils import NamespacesTuple + + GetJsonSchemaFunction = Callable[[CoreSchemaOrField, GetJsonSchemaHandler], JsonSchemaValue] + HandlerOverride = Callable[[CoreSchemaOrField], JsonSchemaValue] + + +class GenerateJsonSchemaHandler(GetJsonSchemaHandler): + """JsonSchemaHandler implementation that doesn't do ref unwrapping by default. + + This is used for any Annotated metadata so that we don't end up with conflicting + modifications to the definition schema. + + Used internally by Pydantic, please do not rely on this implementation. + See `GetJsonSchemaHandler` for the handler API. + """ + + def __init__(self, generate_json_schema: GenerateJsonSchema, handler_override: HandlerOverride | None) -> None: + self.generate_json_schema = generate_json_schema + self.handler = handler_override or generate_json_schema.generate_inner + self.mode = generate_json_schema.mode + + def __call__(self, core_schema: CoreSchemaOrField, /) -> JsonSchemaValue: + return self.handler(core_schema) + + def resolve_ref_schema(self, maybe_ref_json_schema: JsonSchemaValue) -> JsonSchemaValue: + """Resolves `$ref` in the json schema. + + This returns the input json schema if there is no `$ref` in json schema. + + Args: + maybe_ref_json_schema: The input json schema that may contains `$ref`. + + Returns: + Resolved json schema. + + Raises: + LookupError: If it can't find the definition for `$ref`. + """ + if '$ref' not in maybe_ref_json_schema: + return maybe_ref_json_schema + ref = maybe_ref_json_schema['$ref'] + json_schema = self.generate_json_schema.get_schema_from_definitions(ref) + if json_schema is None: + raise LookupError( + f'Could not find a ref for {ref}.' + ' Maybe you tried to call resolve_ref_schema from within a recursive model?' + ) + return json_schema + + +class CallbackGetCoreSchemaHandler(GetCoreSchemaHandler): + """Wrapper to use an arbitrary function as a `GetCoreSchemaHandler`. + + Used internally by Pydantic, please do not rely on this implementation. + See `GetCoreSchemaHandler` for the handler API. + """ + + def __init__( + self, + handler: Callable[[Any], core_schema.CoreSchema], + generate_schema: GenerateSchema, + ref_mode: Literal['to-def', 'unpack'] = 'to-def', + ) -> None: + self._handler = handler + self._generate_schema = generate_schema + self._ref_mode = ref_mode + + def __call__(self, source_type: Any, /) -> core_schema.CoreSchema: + schema = self._handler(source_type) + if self._ref_mode == 'to-def': + ref = schema.get('ref') + if ref is not None: + return self._generate_schema.defs.create_definition_reference_schema(schema) + return schema + else: # ref_mode = 'unpack' + return self.resolve_ref_schema(schema) + + def _get_types_namespace(self) -> NamespacesTuple: + return self._generate_schema._types_namespace + + def generate_schema(self, source_type: Any, /) -> core_schema.CoreSchema: + return self._generate_schema.generate_schema(source_type) + + @property + def field_name(self) -> str | None: + return self._generate_schema.field_name_stack.get() + + def resolve_ref_schema(self, maybe_ref_schema: core_schema.CoreSchema) -> core_schema.CoreSchema: + """Resolves reference in the core schema. + + Args: + maybe_ref_schema: The input core schema that may contains reference. + + Returns: + Resolved core schema. + + Raises: + LookupError: If it can't find the definition for reference. + """ + if maybe_ref_schema['type'] == 'definition-ref': + ref = maybe_ref_schema['schema_ref'] + definition = self._generate_schema.defs.get_schema_from_ref(ref) + if definition is None: + raise LookupError( + f'Could not find a ref for {ref}.' + ' Maybe you tried to call resolve_ref_schema from within a recursive model?' + ) + return definition + elif maybe_ref_schema['type'] == 'definitions': + return self.resolve_ref_schema(maybe_ref_schema['schema']) + return maybe_ref_schema diff --git a/source/target-vendors/cp312/pydantic/_internal/_serializers.py b/source/target-vendors/cp312/pydantic/_internal/_serializers.py new file mode 100644 index 0000000..a4058e0 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_serializers.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import collections +import collections.abc +import typing +from typing import Any + +from pydantic_core import PydanticOmit, core_schema + +SEQUENCE_ORIGIN_MAP: dict[Any, Any] = { + typing.Deque: collections.deque, # noqa: UP006 + collections.deque: collections.deque, + list: list, + typing.List: list, # noqa: UP006 + tuple: tuple, + typing.Tuple: tuple, # noqa: UP006 + set: set, + typing.AbstractSet: set, + typing.Set: set, # noqa: UP006 + frozenset: frozenset, + typing.FrozenSet: frozenset, # noqa: UP006 + typing.Sequence: list, + typing.MutableSequence: list, + typing.MutableSet: set, + # this doesn't handle subclasses of these + # parametrized typing.Set creates one of these + collections.abc.MutableSet: set, + collections.abc.Set: frozenset, +} + + +def serialize_sequence_via_list( + v: Any, handler: core_schema.SerializerFunctionWrapHandler, info: core_schema.SerializationInfo +) -> Any: + items: list[Any] = [] + + mapped_origin = SEQUENCE_ORIGIN_MAP.get(type(v), None) + if mapped_origin is None: + # we shouldn't hit this branch, should probably add a serialization error or something + return v + + for index, item in enumerate(v): + try: + v = handler(item, index) + except PydanticOmit: # noqa: PERF203 + pass + else: + items.append(v) + + if info.mode_is_json(): + return items + else: + return mapped_origin(items) diff --git a/source/target-vendors/cp312/pydantic/_internal/_signature.py b/source/target-vendors/cp312/pydantic/_internal/_signature.py new file mode 100644 index 0000000..3b0c5ae --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_signature.py @@ -0,0 +1,189 @@ +from __future__ import annotations + +import dataclasses +from inspect import Parameter, Signature +from typing import TYPE_CHECKING, Any, Callable + +from pydantic_core import PydanticUndefined + +from ._typing_extra import signature_no_eval +from ._utils import is_valid_identifier + +if TYPE_CHECKING: + from ..config import ExtraValues + from ..fields import FieldInfo + + +# Copied over from stdlib dataclasses +class _HAS_DEFAULT_FACTORY_CLASS: + def __repr__(self): + return '' + + +_HAS_DEFAULT_FACTORY = _HAS_DEFAULT_FACTORY_CLASS() + + +def _field_name_for_signature(field_name: str, field_info: FieldInfo) -> str: + """Extract the correct name to use for the field when generating a signature. + + Assuming the field has a valid alias, this will return the alias. Otherwise, it will return the field name. + First priority is given to the alias, then the validation_alias, then the field name. + + Args: + field_name: The name of the field + field_info: The corresponding FieldInfo object. + + Returns: + The correct name to use when generating a signature. + """ + if isinstance(field_info.alias, str) and is_valid_identifier(field_info.alias): + return field_info.alias + if isinstance(field_info.validation_alias, str) and is_valid_identifier(field_info.validation_alias): + return field_info.validation_alias + + return field_name + + +def _process_param_defaults(param: Parameter) -> Parameter: + """Modify the signature for a parameter in a dataclass where the default value is a FieldInfo instance. + + Args: + param (Parameter): The parameter + + Returns: + Parameter: The custom processed parameter + """ + from ..fields import FieldInfo + + param_default = param.default + if isinstance(param_default, FieldInfo): + annotation = param.annotation + # Replace the annotation if appropriate + # inspect does "clever" things to show annotations as strings because we have + # `from __future__ import annotations` in main, we don't want that + if annotation == 'Any': + annotation = Any + + # Replace the field default + default = param_default.default + if default is PydanticUndefined: + if param_default.default_factory is None: + default = Signature.empty + else: + # this is used by dataclasses to indicate a factory exists: + default = dataclasses._HAS_DEFAULT_FACTORY # type: ignore + return param.replace( + annotation=annotation, name=_field_name_for_signature(param.name, param_default), default=default + ) + return param + + +def _generate_signature_parameters( # noqa: C901 (ignore complexity, could use a refactor) + init: Callable[..., None], + fields: dict[str, FieldInfo], + validate_by_name: bool, + extra: ExtraValues | None, +) -> dict[str, Parameter]: + """Generate a mapping of parameter names to Parameter objects for a pydantic BaseModel or dataclass.""" + from itertools import islice + + present_params = signature_no_eval(init).parameters.values() + merged_params: dict[str, Parameter] = {} + var_kw = None + use_var_kw = False + + for param in islice(present_params, 1, None): # skip self arg + # inspect does "clever" things to show annotations as strings because we have + # `from __future__ import annotations` in main, we don't want that + if fields.get(param.name): + # exclude params with init=False + if getattr(fields[param.name], 'init', True) is False: + continue + param = param.replace(name=_field_name_for_signature(param.name, fields[param.name])) + if param.annotation == 'Any': + param = param.replace(annotation=Any) + if param.kind is param.VAR_KEYWORD: + var_kw = param + continue + merged_params[param.name] = param + + if var_kw: # if custom init has no var_kw, fields which are not declared in it cannot be passed through + allow_names = validate_by_name + for field_name, field in fields.items(): + # when alias is a str it should be used for signature generation + param_name = _field_name_for_signature(field_name, field) + + if field_name in merged_params or param_name in merged_params: + continue + + if not is_valid_identifier(param_name): + if allow_names: + param_name = field_name + else: + use_var_kw = True + continue + + if field.is_required(): + default = Parameter.empty + elif field.default_factory is not None: + # Mimics stdlib dataclasses: + default = _HAS_DEFAULT_FACTORY + else: + default = field.default + merged_params[param_name] = Parameter( + param_name, + Parameter.KEYWORD_ONLY, + annotation=field.rebuild_annotation(), + default=default, + ) + + if extra == 'allow': + use_var_kw = True + + if var_kw and use_var_kw: + # Make sure the parameter for extra kwargs + # does not have the same name as a field + default_model_signature = [ + ('self', Parameter.POSITIONAL_ONLY), + ('data', Parameter.VAR_KEYWORD), + ] + if [(p.name, p.kind) for p in present_params] == default_model_signature: + # if this is the standard model signature, use extra_data as the extra args name + var_kw_name = 'extra_data' + else: + # else start from var_kw + var_kw_name = var_kw.name + + # generate a name that's definitely unique + while var_kw_name in fields: + var_kw_name += '_' + merged_params[var_kw_name] = var_kw.replace(name=var_kw_name) + + return merged_params + + +def generate_pydantic_signature( + init: Callable[..., None], + fields: dict[str, FieldInfo], + validate_by_name: bool, + extra: ExtraValues | None, + is_dataclass: bool = False, +) -> Signature: + """Generate signature for a pydantic BaseModel or dataclass. + + Args: + init: The class init. + fields: The model fields. + validate_by_name: The `validate_by_name` value of the config. + extra: The `extra` value of the config. + is_dataclass: Whether the model is a dataclass. + + Returns: + The dataclass/BaseModel subclass signature. + """ + merged_params = _generate_signature_parameters(init, fields, validate_by_name, extra) + + if is_dataclass: + merged_params = {k: _process_param_defaults(v) for k, v in merged_params.items()} + + return Signature(parameters=list(merged_params.values()), return_annotation=None) diff --git a/source/target-vendors/cp312/pydantic/_internal/_typing_extra.py b/source/target-vendors/cp312/pydantic/_internal/_typing_extra.py new file mode 100644 index 0000000..c217f11 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_typing_extra.py @@ -0,0 +1,785 @@ +"""Logic for interacting with type annotations, mostly extensions, shims and hacks to wrap Python's typing module.""" + +from __future__ import annotations + +import collections.abc +import re +import sys +import types +import typing +from functools import partial +from inspect import Signature, signature +from typing import TYPE_CHECKING, Any, Callable, cast + +import typing_extensions +from typing_extensions import deprecated, get_args, get_origin +from typing_inspection import typing_objects +from typing_inspection.introspection import is_union_origin + +from pydantic.version import version_short + +from ._namespace_utils import GlobalsNamespace, MappingNamespace, NsResolver, get_module_ns_of + +if sys.version_info < (3, 10): + NoneType = type(None) + EllipsisType = type(Ellipsis) +else: + from types import EllipsisType as EllipsisType + from types import NoneType as NoneType + +if sys.version_info >= (3, 14): + import annotationlib + +if TYPE_CHECKING: + from pydantic import BaseModel + from pydantic.fields import FieldInfo + +# As per https://typing-extensions.readthedocs.io/en/latest/#runtime-use-of-types, +# always check for both `typing` and `typing_extensions` variants of a typing construct. +# (this is implemented differently than the suggested approach in the `typing_extensions` +# docs for performance). + + +_t_annotated = typing.Annotated +_te_annotated = typing_extensions.Annotated + + +def is_annotated(tp: Any, /) -> bool: + """Return whether the provided argument is a `Annotated` special form. + + ```python {test="skip" lint="skip"} + is_annotated(Annotated[int, ...]) + #> True + ``` + """ + origin = get_origin(tp) + return origin is _t_annotated or origin is _te_annotated + + +def annotated_type(tp: Any, /) -> Any | None: + """Return the type of the `Annotated` special form, or `None`.""" + return tp.__origin__ if typing_objects.is_annotated(get_origin(tp)) else None + + +def unpack_type(tp: Any, /) -> Any | None: + """Return the type wrapped by the `Unpack` special form, or `None`.""" + return get_args(tp)[0] if typing_objects.is_unpack(get_origin(tp)) else None + + +def is_hashable(tp: Any, /) -> bool: + """Return whether the provided argument is the `Hashable` class. + + ```python {test="skip" lint="skip"} + is_hashable(Hashable) + #> True + ``` + """ + # `get_origin` is documented as normalizing any typing-module aliases to `collections` classes, + # hence the second check: + return tp is collections.abc.Hashable or get_origin(tp) is collections.abc.Hashable + + +def is_callable(tp: Any, /) -> bool: + """Return whether the provided argument is a `Callable`, parametrized or not. + + ```python {test="skip" lint="skip"} + is_callable(Callable[[int], str]) + #> True + is_callable(typing.Callable) + #> True + is_callable(collections.abc.Callable) + #> True + ``` + """ + # `get_origin` is documented as normalizing any typing-module aliases to `collections` classes, + # hence the second check: + return tp is collections.abc.Callable or get_origin(tp) is collections.abc.Callable + + +_classvar_re = re.compile(r'((\w+\.)?Annotated\[)?(\w+\.)?ClassVar\[') + + +def is_classvar_annotation(tp: Any, /) -> bool: + """Return whether the provided argument represents a class variable annotation. + + Although not explicitly stated by the typing specification, `ClassVar` can be used + inside `Annotated` and as such, this function checks for this specific scenario. + + Because this function is used to detect class variables before evaluating forward references + (or because evaluation failed), we also implement a naive regex match implementation. This is + required because class variables are inspected before fields are collected, so we try to be + as accurate as possible. + """ + if typing_objects.is_classvar(tp): + return True + + origin = get_origin(tp) + + if typing_objects.is_classvar(origin): + return True + + if typing_objects.is_annotated(origin): + annotated_type = tp.__origin__ + if typing_objects.is_classvar(annotated_type) or typing_objects.is_classvar(get_origin(annotated_type)): + return True + + str_ann: str | None = None + if isinstance(tp, typing.ForwardRef): + str_ann = tp.__forward_arg__ + if isinstance(tp, str): + str_ann = tp + + if str_ann is not None and _classvar_re.match(str_ann): + # stdlib dataclasses do something similar, although a bit more advanced + # (see `dataclass._is_type`). + return True + + return False + + +_t_final = typing.Final +_te_final = typing_extensions.Final + + +# TODO implement `is_finalvar_annotation` as Final can be wrapped with other special forms: +def is_finalvar(tp: Any, /) -> bool: + """Return whether the provided argument is a `Final` special form, parametrized or not. + + ```python {test="skip" lint="skip"} + is_finalvar(Final[int]) + #> True + is_finalvar(Final) + #> True + """ + # Final is not necessarily parametrized: + if tp is _t_final or tp is _te_final: + return True + origin = get_origin(tp) + return origin is _t_final or origin is _te_final + + +_NONE_TYPES: tuple[Any, ...] = (None, NoneType, typing.Literal[None], typing_extensions.Literal[None]) + + +def is_none_type(tp: Any, /) -> bool: + """Return whether the argument represents the `None` type as part of an annotation. + + ```python {test="skip" lint="skip"} + is_none_type(None) + #> True + is_none_type(NoneType) + #> True + is_none_type(Literal[None]) + #> True + is_none_type(type[None]) + #> False + """ + return tp in _NONE_TYPES + + +def is_namedtuple(tp: Any, /) -> bool: + """Return whether the provided argument is a named tuple class. + + The class can be created using `typing.NamedTuple` or `collections.namedtuple`. + Parametrized generic classes are *not* assumed to be named tuples. + """ + from ._utils import lenient_issubclass # circ. import + + return lenient_issubclass(tp, tuple) and hasattr(tp, '_fields') + + +# TODO In 2.12, delete this export. It is currently defined only to not break +# pydantic-settings which relies on it: +origin_is_union = is_union_origin + + +def is_generic_alias(tp: Any, /) -> bool: + return isinstance(tp, (types.GenericAlias, typing._GenericAlias)) # pyright: ignore[reportAttributeAccessIssue] + + +# TODO: Ideally, we should avoid relying on the private `typing` constructs: + +if sys.version_info < (3, 10): + WithArgsTypes: tuple[Any, ...] = (typing._GenericAlias, types.GenericAlias) # pyright: ignore[reportAttributeAccessIssue] +else: + WithArgsTypes: tuple[Any, ...] = (typing._GenericAlias, types.GenericAlias, types.UnionType) # pyright: ignore[reportAttributeAccessIssue] + + +# Similarly, we shouldn't rely on this `_Final` class, which is even more private than `_GenericAlias`: +typing_base: Any = typing._Final # pyright: ignore[reportAttributeAccessIssue] + + +### Annotation evaluations functions: + + +def parent_frame_namespace(*, parent_depth: int = 2, force: bool = False) -> dict[str, Any] | None: + """Fetch the local namespace of the parent frame where this function is called. + + Using this function is mostly useful to resolve forward annotations pointing to members defined in a local namespace, + such as assignments inside a function. Using the standard library tools, it is currently not possible to resolve + such annotations: + + ```python {lint="skip" test="skip"} + from typing import get_type_hints + + def func() -> None: + Alias = int + + class C: + a: 'Alias' + + # Raises a `NameError: 'Alias' is not defined` + get_type_hints(C) + ``` + + Pydantic uses this function when a Pydantic model is being defined to fetch the parent frame locals. However, + this only allows us to fetch the parent frame namespace and not other parents (e.g. a model defined in a function, + itself defined in another function). Inspecting the next outer frames (using `f_back`) is not reliable enough + (see https://discuss.python.org/t/20659). + + Because this function is mostly used to better resolve forward annotations, nothing is returned if the parent frame's + code object is defined at the module level. In this case, the locals of the frame will be the same as the module + globals where the class is defined (see `_namespace_utils.get_module_ns_of`). However, if you still want to fetch + the module globals (e.g. when rebuilding a model, where the frame where the rebuild call is performed might contain + members that you want to use for forward annotations evaluation), you can use the `force` parameter. + + Args: + parent_depth: The depth at which to get the frame. Defaults to 2, meaning the parent frame where this function + is called will be used. + force: Whether to always return the frame locals, even if the frame's code object is defined at the module level. + + Returns: + The locals of the namespace, or `None` if it was skipped as per the described logic. + """ + frame = sys._getframe(parent_depth) + + if frame.f_code.co_name.startswith('`, + # and we need to skip this frame as it is irrelevant. + frame = cast(types.FrameType, frame.f_back) # guaranteed to not be `None` + + # note, we don't copy frame.f_locals here (or during the last return call), because we don't expect the namespace to be + # modified down the line if this becomes a problem, we could implement some sort of frozen mapping structure to enforce this. + if force: + return frame.f_locals + + # If either of the following conditions are true, the class is defined at the top module level. + # To better understand why we need both of these checks, see + # https://github.com/pydantic/pydantic/pull/10113#discussion_r1714981531. + if frame.f_back is None or frame.f_code.co_name == '': + return None + + return frame.f_locals + + +def _type_convert(arg: Any) -> Any: + """Convert `None` to `NoneType` and strings to `ForwardRef` instances. + + This is a backport of the private `typing._type_convert` function. When + evaluating a type, `ForwardRef._evaluate` ends up being called, and is + responsible for making this conversion. However, we still have to apply + it for the first argument passed to our type evaluation functions, similarly + to the `typing.get_type_hints` function. + """ + if arg is None: + return NoneType + if isinstance(arg, str): + # Like `typing.get_type_hints`, assume the arg can be in any context, + # hence the proper `is_argument` and `is_class` args: + return _make_forward_ref(arg, is_argument=False, is_class=True) + return arg + + +def safe_get_annotations(obj: Any) -> dict[str, Any]: + """Get the annotations for the provided object, accounting for potential deferred forward references. + + Starting with Python 3.14, accessing the `__annotations__` attribute might raise a `NameError` if + a referenced symbol isn't defined yet. In this case, we return the annotation in the *forward ref* + format. + """ + if sys.version_info >= (3, 14): + return annotationlib.get_annotations(obj, format=annotationlib.Format.FORWARDREF) + else: + # TODO just do getattr(obj, '__annotations__', {}) when dropping support for Python 3.9: + if isinstance(obj, type): + return obj.__dict__.get('__annotations__', {}) + else: + return getattr(obj, '__annotations__', {}) + + +def get_model_type_hints( + model_class: type[BaseModel], + *, + ns_resolver: NsResolver | None = None, +) -> dict[str, tuple[Any, bool]]: + """Collect annotations from a Pydantic model class, including those from parent classes. + + Args: + model_class: The Pydantic model class to inspect. + ns_resolver: A namespace resolver instance to use. Defaults to an empty instance. + + Returns: + A dictionary mapping annotation names to a two-tuple: the first element is the evaluated + type or the original annotation if a `NameError` occurred, the second element is a boolean + indicating if whether the evaluation succeeded. + """ + hints: dict[str, Any] | dict[str, tuple[Any, bool]] = {} + ns_resolver = ns_resolver or NsResolver() + + for base in reversed(model_class.__mro__[:-1]): + # For Python 3.14, we could also use `Format.VALUE` and pass the globals/locals + # from the ns_resolver, but we want to be able to know which specific field failed + # to evaluate: + ann = safe_get_annotations(base) + + if not ann: + continue + + with ns_resolver.push(base): + base_model_fields: dict[str, FieldInfo] | None = base.__dict__.get('__pydantic_fields__') + + for name, value in ann.items(): + if name.startswith('_'): + globalns, localns = ns_resolver.types_namespace + + # For private attributes, we only need the annotation to detect the `ClassVar` special form. + # For this reason, we still try to evaluate it, but we also catch any possible exception (on + # top of the `NameError`s caught in `try_eval_type`) that could happen so that users are free + # to use any kind of forward annotation for private fields (e.g. circular imports, new typing + # syntax, etc). + try: + hints[name] = try_eval_type(value, globalns, localns) + except Exception: + hints[name] = (value, False) + else: + if base_model_fields is not None and name in base_model_fields: + # Avoid unnecessarily evaluating annotations from parent models, as we'll end up + # copying the `FieldInfo` instance from it anyway if we need to. + # We use the `annotation` attribute here, but in reality could put anything here, + # As we are guaranteed to not make use of it: + hints[name] = (base_model_fields[name].annotation, True) + else: + globalns, localns = ns_resolver.types_namespace + + hints[name] = try_eval_type(value, globalns, localns) + + return hints + + +def get_cls_type_hints( + obj: type[Any], + *, + ns_resolver: NsResolver | None = None, +) -> dict[str, Any]: + """Collect annotations from a class, including those from parent classes. + + Args: + obj: The class to inspect. + ns_resolver: A namespace resolver instance to use. Defaults to an empty instance. + """ + hints: dict[str, Any] = {} + ns_resolver = ns_resolver or NsResolver() + + for base in reversed(obj.__mro__): + # For Python 3.14, we could also use `Format.VALUE` and pass the globals/locals + # from the ns_resolver, but we want to be able to know which specific field failed + # to evaluate: + ann = safe_get_annotations(base) + + if not ann: + continue + + with ns_resolver.push(base): + globalns, localns = ns_resolver.types_namespace + for name, value in ann.items(): + hints[name] = eval_type(value, globalns, localns) + return hints + + +def try_eval_type( + value: Any, + globalns: GlobalsNamespace | None = None, + localns: MappingNamespace | None = None, +) -> tuple[Any, bool]: + """Try evaluating the annotation using the provided namespaces. + + Args: + value: The value to evaluate. If `None`, it will be replaced by `type[None]`. If an instance + of `str`, it will be converted to a `ForwardRef`. + localns: The global namespace to use during annotation evaluation. + globalns: The local namespace to use during annotation evaluation. + + Returns: + A two-tuple containing the possibly evaluated type and a boolean indicating + whether the evaluation succeeded or not. + """ + value = _type_convert(value) + + try: + return eval_type_backport(value, globalns, localns), True + except NameError: + return value, False + + +def eval_type( + value: Any, + globalns: GlobalsNamespace | None = None, + localns: MappingNamespace | None = None, +) -> Any: + """Evaluate the annotation using the provided namespaces. + + Args: + value: The value to evaluate. If `None`, it will be replaced by `type[None]`. If an instance + of `str`, it will be converted to a `ForwardRef`. + localns: The global namespace to use during annotation evaluation. + globalns: The local namespace to use during annotation evaluation. + """ + value = _type_convert(value) + return eval_type_backport(value, globalns, localns) + + +@deprecated( + '`eval_type_lenient` is deprecated, use `try_eval_type` instead.', + category=None, +) +def eval_type_lenient( + value: Any, + globalns: GlobalsNamespace | None = None, + localns: MappingNamespace | None = None, +) -> Any: + ev, _ = try_eval_type(value, globalns, localns) + return ev + + +def eval_type_backport( + value: Any, + globalns: GlobalsNamespace | None = None, + localns: MappingNamespace | None = None, + type_params: tuple[Any, ...] | None = None, +) -> Any: + """An enhanced version of `typing._eval_type` which will fall back to using the `eval_type_backport` + package if it's installed to let older Python versions use newer typing constructs. + + Specifically, this transforms `X | Y` into `typing.Union[X, Y]` and `list[X]` into `typing.List[X]` + (as well as all the types made generic in PEP 585) if the original syntax is not supported in the + current Python version. + + This function will also display a helpful error if the value passed fails to evaluate. + """ + try: + return _eval_type_backport(value, globalns, localns, type_params) + except TypeError as e: + if 'Unable to evaluate type annotation' in str(e): + raise + + # If it is a `TypeError` and value isn't a `ForwardRef`, it would have failed during annotation definition. + # Thus we assert here for type checking purposes: + assert isinstance(value, typing.ForwardRef) + + message = f'Unable to evaluate type annotation {value.__forward_arg__!r}.' + if sys.version_info >= (3, 11): + e.add_note(message) + raise + else: + raise TypeError(message) from e + except RecursionError as e: + # TODO ideally recursion errors should be checked in `eval_type` above, but `eval_type_backport` + # is used directly in some places. + message = ( + "If you made use of an implicit recursive type alias (e.g. `MyType = list['MyType']), " + 'consider using PEP 695 type aliases instead. For more details, refer to the documentation: ' + f'https://docs.pydantic.dev/{version_short()}/concepts/types/#named-recursive-types' + ) + if sys.version_info >= (3, 11): + e.add_note(message) + raise + else: + raise RecursionError(f'{e.args[0]}\n{message}') + + +def _eval_type_backport( + value: Any, + globalns: GlobalsNamespace | None = None, + localns: MappingNamespace | None = None, + type_params: tuple[Any, ...] | None = None, +) -> Any: + try: + return _eval_type(value, globalns, localns, type_params) + except TypeError as e: + if not (isinstance(value, typing.ForwardRef) and is_backport_fixable_error(e)): + raise + + try: + from eval_type_backport import eval_type_backport + except ImportError: + raise TypeError( + f'Unable to evaluate type annotation {value.__forward_arg__!r}. If you are making use ' + 'of the new typing syntax (unions using `|` since Python 3.10 or builtins subscripting ' + 'since Python 3.9), you should either replace the use of new syntax with the existing ' + '`typing` constructs or install the `eval_type_backport` package.' + ) from e + + return eval_type_backport( + value, + globalns, + localns, # pyright: ignore[reportArgumentType], waiting on a new `eval_type_backport` release. + try_default=False, + ) + + +def _eval_type( + value: Any, + globalns: GlobalsNamespace | None = None, + localns: MappingNamespace | None = None, + type_params: tuple[Any, ...] | None = None, +) -> Any: + if sys.version_info >= (3, 14): + # Starting in 3.14, `_eval_type()` does *not* apply `_type_convert()` + # anymore. This means the `None` -> `type(None)` conversion does not apply: + evaluated = typing._eval_type( # type: ignore + value, + globalns, + localns, + type_params=type_params, + # This is relevant when evaluating types from `TypedDict` classes, where string annotations + # are automatically converted to `ForwardRef` instances with a module set. In this case, + # Our `globalns` is irrelevant and we need to indicate `typing._eval_type()` that it should + # infer it from the `ForwardRef.__forward_module__` attribute instead (`typing.get_type_hints()` + # does the same). Note that this would probably be unnecessary if we properly iterated over the + # `__orig_bases__` for TypedDicts in `get_cls_type_hints()`: + prefer_fwd_module=True, + ) + if evaluated is None: + evaluated = type(None) + return evaluated + elif sys.version_info >= (3, 13): + return typing._eval_type( # type: ignore + value, globalns, localns, type_params=type_params + ) + else: + return typing._eval_type( # type: ignore + value, globalns, localns + ) + + +def is_backport_fixable_error(e: TypeError) -> bool: + msg = str(e) + + return sys.version_info < (3, 10) and msg.startswith('unsupported operand type(s) for |: ') + + +def signature_no_eval(f: Callable[..., Any]) -> Signature: + """Get the signature of a callable without evaluating any annotations.""" + if sys.version_info >= (3, 14): + from annotationlib import Format + + return signature(f, annotation_format=Format.FORWARDREF) + else: + return signature(f) + + +def get_function_type_hints( + function: Callable[..., Any], + *, + include_keys: set[str] | None = None, + globalns: GlobalsNamespace | None = None, + localns: MappingNamespace | None = None, +) -> dict[str, Any]: + """Return type hints for a function. + + This is similar to the `typing.get_type_hints` function, with a few differences: + - Support `functools.partial` by using the underlying `func` attribute. + - Do not wrap type annotation of a parameter with `Optional` if it has a default value of `None` + (related bug: https://github.com/python/cpython/issues/90353, only fixed in 3.11+). + """ + if isinstance(function, partial): + annotations = safe_get_annotations(function.func) + else: + annotations = safe_get_annotations(function) + + if globalns is None: + globalns = get_module_ns_of(function) + type_params: tuple[Any, ...] | None = None + if localns is None: + # If localns was specified, it is assumed to already contain type params. This is because + # Pydantic has more advanced logic to do so (see `_namespace_utils.ns_for_function`). + type_params = getattr(function, '__type_params__', ()) + + type_hints = {} + for name, value in annotations.items(): + if include_keys is not None and name not in include_keys: + continue + if value is None: + value = NoneType + elif isinstance(value, str): + value = _make_forward_ref(value) + + type_hints[name] = eval_type_backport(value, globalns, localns, type_params) + + return type_hints + + +# TODO use typing.ForwardRef directly when we stop supporting 3.9: +if sys.version_info < (3, 9, 8) or (3, 10) <= sys.version_info < (3, 10, 1): + + def _make_forward_ref( + arg: Any, + is_argument: bool = True, + *, + is_class: bool = False, + ) -> typing.ForwardRef: + """Wrapper for ForwardRef that accounts for the `is_class` argument missing in older versions. + The `module` argument is omitted as it breaks <3.9.8, =3.10.0 and isn't used in the calls below. + + See https://github.com/python/cpython/pull/28560 for some background. + The backport happened on 3.9.8, see: + https://github.com/pydantic/pydantic/discussions/6244#discussioncomment-6275458, + and on 3.10.1 for the 3.10 branch, see: + https://github.com/pydantic/pydantic/issues/6912 + + Implemented as EAFP with memory. + """ + return typing.ForwardRef(arg, is_argument) # pyright: ignore[reportCallIssue] + +else: + _make_forward_ref = typing.ForwardRef # pyright: ignore[reportAssignmentType] + + +if sys.version_info >= (3, 10): + get_type_hints = typing.get_type_hints + +else: + """ + For older versions of python, we have a custom implementation of `get_type_hints` which is a close as possible to + the implementation in CPython 3.10.8. + """ + + @typing.no_type_check + def get_type_hints( # noqa: C901 + obj: Any, + globalns: dict[str, Any] | None = None, + localns: dict[str, Any] | None = None, + include_extras: bool = False, + ) -> dict[str, Any]: # pragma: no cover + """Taken verbatim from python 3.10.8 unchanged, except: + * type annotations of the function definition above. + * prefixing `typing.` where appropriate + * Use `_make_forward_ref` instead of `typing.ForwardRef` to handle the `is_class` argument. + + https://github.com/python/cpython/blob/aaaf5174241496afca7ce4d4584570190ff972fe/Lib/typing.py#L1773-L1875 + + DO NOT CHANGE THIS METHOD UNLESS ABSOLUTELY NECESSARY. + ====================================================== + + Return type hints for an object. + + This is often the same as obj.__annotations__, but it handles + forward references encoded as string literals, adds Optional[t] if a + default value equal to None is set and recursively replaces all + 'Annotated[T, ...]' with 'T' (unless 'include_extras=True'). + + The argument may be a module, class, method, or function. The annotations + are returned as a dictionary. For classes, annotations include also + inherited members. + + TypeError is raised if the argument is not of a type that can contain + annotations, and an empty dictionary is returned if no annotations are + present. + + BEWARE -- the behavior of globalns and localns is counterintuitive + (unless you are familiar with how eval() and exec() work). The + search order is locals first, then globals. + + - If no dict arguments are passed, an attempt is made to use the + globals from obj (or the respective module's globals for classes), + and these are also used as the locals. If the object does not appear + to have globals, an empty dictionary is used. For classes, the search + order is globals first then locals. + + - If one dict argument is passed, it is used for both globals and + locals. + + - If two dict arguments are passed, they specify globals and + locals, respectively. + """ + if getattr(obj, '__no_type_check__', None): + return {} + # Classes require a special treatment. + if isinstance(obj, type): + hints = {} + for base in reversed(obj.__mro__): + if globalns is None: + base_globals = getattr(sys.modules.get(base.__module__, None), '__dict__', {}) + else: + base_globals = globalns + ann = base.__dict__.get('__annotations__', {}) + if isinstance(ann, types.GetSetDescriptorType): + ann = {} + base_locals = dict(vars(base)) if localns is None else localns + if localns is None and globalns is None: + # This is surprising, but required. Before Python 3.10, + # get_type_hints only evaluated the globalns of + # a class. To maintain backwards compatibility, we reverse + # the globalns and localns order so that eval() looks into + # *base_globals* first rather than *base_locals*. + # This only affects ForwardRefs. + base_globals, base_locals = base_locals, base_globals + for name, value in ann.items(): + if value is None: + value = type(None) + if isinstance(value, str): + value = _make_forward_ref(value, is_argument=False, is_class=True) + + value = eval_type_backport(value, base_globals, base_locals) + hints[name] = value + if not include_extras and hasattr(typing, '_strip_annotations'): + return { + k: typing._strip_annotations(t) # type: ignore + for k, t in hints.items() + } + else: + return hints + + if globalns is None: + if isinstance(obj, types.ModuleType): + globalns = obj.__dict__ + else: + nsobj = obj + # Find globalns for the unwrapped object. + while hasattr(nsobj, '__wrapped__'): + nsobj = nsobj.__wrapped__ + globalns = getattr(nsobj, '__globals__', {}) + if localns is None: + localns = globalns + elif localns is None: + localns = globalns + hints = getattr(obj, '__annotations__', None) + if hints is None: + # Return empty annotations for something that _could_ have them. + if isinstance(obj, typing._allowed_types): # type: ignore + return {} + else: + raise TypeError(f'{obj!r} is not a module, class, method, or function.') + defaults = typing._get_defaults(obj) # type: ignore + hints = dict(hints) + for name, value in hints.items(): + if value is None: + value = type(None) + if isinstance(value, str): + # class-level forward refs were handled above, this must be either + # a module-level annotation or a function argument annotation + + value = _make_forward_ref( + value, + is_argument=not isinstance(obj, types.ModuleType), + is_class=False, + ) + value = eval_type_backport(value, globalns, localns) + if name in defaults and defaults[name] is None: + value = typing.Optional[value] + hints[name] = value + return hints if include_extras else {k: typing._strip_annotations(t) for k, t in hints.items()} # type: ignore diff --git a/source/target-vendors/cp312/pydantic/_internal/_utils.py b/source/target-vendors/cp312/pydantic/_internal/_utils.py new file mode 100644 index 0000000..7a0346d --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_utils.py @@ -0,0 +1,446 @@ +"""Bucket of reusable internal utilities. + +This should be reduced as much as possible with functions only used in one place, moved to that place. +""" + +from __future__ import annotations as _annotations + +import dataclasses +import keyword +import sys +import warnings +import weakref +from collections import OrderedDict, defaultdict, deque +from collections.abc import Callable, Iterable, Mapping +from collections.abc import Set as AbstractSet +from copy import deepcopy +from functools import cached_property +from inspect import Parameter +from itertools import zip_longest +from types import BuiltinFunctionType, CodeType, FunctionType, GeneratorType, LambdaType, ModuleType +from typing import TYPE_CHECKING, Any, Generic, TypeVar, overload + +from pydantic_core import MISSING, PydanticUndefined +from typing_extensions import TypeAlias, TypeGuard, deprecated + +from pydantic import PydanticDeprecatedSince211 + +from . import _repr, _typing_extra +from ._import_utils import import_cached_base_model + +if TYPE_CHECKING: + # TODO remove type error comments when we drop support for Python 3.9 + MappingIntStrAny: TypeAlias = Mapping[int, Any] | Mapping[str, Any] # pyright: ignore[reportGeneralTypeIssues] + AbstractSetIntStr: TypeAlias = AbstractSet[int] | AbstractSet[str] # pyright: ignore[reportGeneralTypeIssues] + from ..main import BaseModel + + +# these are types that are returned unchanged by deepcopy +IMMUTABLE_NON_COLLECTIONS_TYPES: set[type[Any]] = { + int, + float, + complex, + str, + bool, + bytes, + type, + _typing_extra.NoneType, + FunctionType, + BuiltinFunctionType, + LambdaType, + weakref.ref, + CodeType, + # note: including ModuleType will differ from behaviour of deepcopy by not producing error. + # It might be not a good idea in general, but considering that this function used only internally + # against default values of fields, this will allow to actually have a field with module as default value + ModuleType, + NotImplemented.__class__, + Ellipsis.__class__, +} + +# these are types that if empty, might be copied with simple copy() instead of deepcopy() +BUILTIN_COLLECTIONS: set[type[Any]] = { + list, + set, + tuple, + frozenset, + dict, + OrderedDict, + defaultdict, + deque, +} + + +def can_be_positional(param: Parameter) -> bool: + """Return whether the parameter accepts a positional argument. + + ```python {test="skip" lint="skip"} + def func(a, /, b, *, c): + pass + + params = inspect.signature(func).parameters + can_be_positional(params['a']) + #> True + can_be_positional(params['b']) + #> True + can_be_positional(params['c']) + #> False + ``` + """ + return param.kind in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD) + + +def sequence_like(v: Any) -> bool: + return isinstance(v, (list, tuple, set, frozenset, GeneratorType, deque)) + + +def lenient_isinstance(o: Any, class_or_tuple: type[Any] | tuple[type[Any], ...] | None) -> bool: # pragma: no cover + try: + return isinstance(o, class_or_tuple) # type: ignore[arg-type] + except TypeError: + return False + + +def lenient_issubclass(cls: Any, class_or_tuple: Any) -> bool: # pragma: no cover + try: + return isinstance(cls, type) and issubclass(cls, class_or_tuple) + except TypeError: + if isinstance(cls, _typing_extra.WithArgsTypes): + return False + raise # pragma: no cover + + +def is_model_class(cls: Any) -> TypeGuard[type[BaseModel]]: + """Returns true if cls is a _proper_ subclass of BaseModel, and provides proper type-checking, + unlike raw calls to lenient_issubclass. + """ + BaseModel = import_cached_base_model() + + return lenient_issubclass(cls, BaseModel) and cls is not BaseModel + + +def is_valid_identifier(identifier: str) -> bool: + """Checks that a string is a valid identifier and not a Python keyword. + :param identifier: The identifier to test. + :return: True if the identifier is valid. + """ + return identifier.isidentifier() and not keyword.iskeyword(identifier) + + +KeyType = TypeVar('KeyType') + + +def deep_update(mapping: dict[KeyType, Any], *updating_mappings: dict[KeyType, Any]) -> dict[KeyType, Any]: + updated_mapping = mapping.copy() + for updating_mapping in updating_mappings: + for k, v in updating_mapping.items(): + if k in updated_mapping and isinstance(updated_mapping[k], dict) and isinstance(v, dict): + updated_mapping[k] = deep_update(updated_mapping[k], v) + else: + updated_mapping[k] = v + return updated_mapping + + +def update_not_none(mapping: dict[Any, Any], **update: Any) -> None: + mapping.update({k: v for k, v in update.items() if v is not None}) + + +T = TypeVar('T') + + +def unique_list( + input_list: list[T] | tuple[T, ...], + *, + name_factory: Callable[[T], str] = str, +) -> list[T]: + """Make a list unique while maintaining order. + We update the list if another one with the same name is set + (e.g. model validator overridden in subclass). + """ + result: list[T] = [] + result_names: list[str] = [] + for v in input_list: + v_name = name_factory(v) + if v_name not in result_names: + result_names.append(v_name) + result.append(v) + else: + result[result_names.index(v_name)] = v + + return result + + +class ValueItems(_repr.Representation): + """Class for more convenient calculation of excluded or included fields on values.""" + + __slots__ = ('_items', '_type') + + def __init__(self, value: Any, items: AbstractSetIntStr | MappingIntStrAny) -> None: + items = self._coerce_items(items) + + if isinstance(value, (list, tuple)): + items = self._normalize_indexes(items, len(value)) # type: ignore + + self._items: MappingIntStrAny = items # type: ignore + + def is_excluded(self, item: Any) -> bool: + """Check if item is fully excluded. + + :param item: key or index of a value + """ + return self.is_true(self._items.get(item)) + + def is_included(self, item: Any) -> bool: + """Check if value is contained in self._items. + + :param item: key or index of value + """ + return item in self._items + + def for_element(self, e: int | str) -> AbstractSetIntStr | MappingIntStrAny | None: + """:param e: key or index of element on value + :return: raw values for element if self._items is dict and contain needed element + """ + item = self._items.get(e) # type: ignore + return item if not self.is_true(item) else None + + def _normalize_indexes(self, items: MappingIntStrAny, v_length: int) -> dict[int | str, Any]: + """:param items: dict or set of indexes which will be normalized + :param v_length: length of sequence indexes of which will be + + >>> self._normalize_indexes({0: True, -2: True, -1: True}, 4) + {0: True, 2: True, 3: True} + >>> self._normalize_indexes({'__all__': True}, 4) + {0: True, 1: True, 2: True, 3: True} + """ + normalized_items: dict[int | str, Any] = {} + all_items = None + for i, v in items.items(): + if not (isinstance(v, Mapping) or isinstance(v, AbstractSet) or self.is_true(v)): + raise TypeError(f'Unexpected type of exclude value for index "{i}" {v.__class__}') + if i == '__all__': + all_items = self._coerce_value(v) + continue + if not isinstance(i, int): + raise TypeError( + 'Excluding fields from a sequence of sub-models or dicts must be performed index-wise: ' + 'expected integer keys or keyword "__all__"' + ) + normalized_i = v_length + i if i < 0 else i + normalized_items[normalized_i] = self.merge(v, normalized_items.get(normalized_i)) + + if not all_items: + return normalized_items + if self.is_true(all_items): + for i in range(v_length): + normalized_items.setdefault(i, ...) + return normalized_items + for i in range(v_length): + normalized_item = normalized_items.setdefault(i, {}) + if not self.is_true(normalized_item): + normalized_items[i] = self.merge(all_items, normalized_item) + return normalized_items + + @classmethod + def merge(cls, base: Any, override: Any, intersect: bool = False) -> Any: + """Merge a `base` item with an `override` item. + + Both `base` and `override` are converted to dictionaries if possible. + Sets are converted to dictionaries with the sets entries as keys and + Ellipsis as values. + + Each key-value pair existing in `base` is merged with `override`, + while the rest of the key-value pairs are updated recursively with this function. + + Merging takes place based on the "union" of keys if `intersect` is + set to `False` (default) and on the intersection of keys if + `intersect` is set to `True`. + """ + override = cls._coerce_value(override) + base = cls._coerce_value(base) + if override is None: + return base + if cls.is_true(base) or base is None: + return override + if cls.is_true(override): + return base if intersect else override + + # intersection or union of keys while preserving ordering: + if intersect: + merge_keys = [k for k in base if k in override] + [k for k in override if k in base] + else: + merge_keys = list(base) + [k for k in override if k not in base] + + merged: dict[int | str, Any] = {} + for k in merge_keys: + merged_item = cls.merge(base.get(k), override.get(k), intersect=intersect) + if merged_item is not None: + merged[k] = merged_item + + return merged + + @staticmethod + def _coerce_items(items: AbstractSetIntStr | MappingIntStrAny) -> MappingIntStrAny: + if isinstance(items, Mapping): + pass + elif isinstance(items, AbstractSet): + items = dict.fromkeys(items, ...) # type: ignore + else: + class_name = getattr(items, '__class__', '???') + raise TypeError(f'Unexpected type of exclude value {class_name}') + return items # type: ignore + + @classmethod + def _coerce_value(cls, value: Any) -> Any: + if value is None or cls.is_true(value): + return value + return cls._coerce_items(value) + + @staticmethod + def is_true(v: Any) -> bool: + return v is True or v is ... + + def __repr_args__(self) -> _repr.ReprArgs: + return [(None, self._items)] + + +if TYPE_CHECKING: + + def LazyClassAttribute(name: str, get_value: Callable[[], T]) -> T: ... + +else: + + class LazyClassAttribute: + """A descriptor exposing an attribute only accessible on a class (hidden from instances). + + The attribute is lazily computed and cached during the first access. + """ + + def __init__(self, name: str, get_value: Callable[[], Any]) -> None: + self.name = name + self.get_value = get_value + + @cached_property + def value(self) -> Any: + return self.get_value() + + def __get__(self, instance: Any, owner: type[Any]) -> None: + if instance is None: + return self.value + raise AttributeError(f'{self.name!r} attribute of {owner.__name__!r} is class-only') + + +Obj = TypeVar('Obj') + + +def smart_deepcopy(obj: Obj) -> Obj: + """Return type as is for immutable built-in types + Use obj.copy() for built-in empty collections + Use copy.deepcopy() for non-empty collections and unknown objects. + """ + if obj is MISSING or obj is PydanticUndefined: + return obj # pyright: ignore[reportReturnType] + obj_type = obj.__class__ + if obj_type in IMMUTABLE_NON_COLLECTIONS_TYPES: + return obj # fastest case: obj is immutable and not collection therefore will not be copied anyway + try: + if not obj and obj_type in BUILTIN_COLLECTIONS: + # faster way for empty collections, no need to copy its members + return obj if obj_type is tuple else obj.copy() # tuple doesn't have copy method # type: ignore + except (TypeError, ValueError, RuntimeError): + # do we really dare to catch ALL errors? Seems a bit risky + pass + + return deepcopy(obj) # slowest way when we actually might need a deepcopy + + +_SENTINEL = object() + + +def all_identical(left: Iterable[Any], right: Iterable[Any]) -> bool: + """Check that the items of `left` are the same objects as those in `right`. + + >>> a, b = object(), object() + >>> all_identical([a, b, a], [a, b, a]) + True + >>> all_identical([a, b, [a]], [a, b, [a]]) # new list object, while "equal" is not "identical" + False + """ + for left_item, right_item in zip_longest(left, right, fillvalue=_SENTINEL): + if left_item is not right_item: + return False + return True + + +def get_first_not_none(a: Any, b: Any) -> Any: + """Return the first argument if it is not `None`, otherwise return the second argument.""" + return a if a is not None else b + + +@dataclasses.dataclass(frozen=True) +class SafeGetItemProxy: + """Wrapper redirecting `__getitem__` to `get` with a sentinel value as default + + This makes is safe to use in `operator.itemgetter` when some keys may be missing + """ + + # Define __slots__manually for performances + # @dataclasses.dataclass() only support slots=True in python>=3.10 + __slots__ = ('wrapped',) + + wrapped: Mapping[str, Any] + + def __getitem__(self, key: str, /) -> Any: + return self.wrapped.get(key, _SENTINEL) + + # required to pass the object to operator.itemgetter() instances due to a quirk of typeshed + # https://github.com/python/mypy/issues/13713 + # https://github.com/python/typeshed/pull/8785 + # Since this is typing-only, hide it in a typing.TYPE_CHECKING block + if TYPE_CHECKING: + + def __contains__(self, key: str, /) -> bool: + return self.wrapped.__contains__(key) + + +_ModelT = TypeVar('_ModelT', bound='BaseModel') +_RT = TypeVar('_RT') + + +class deprecated_instance_property(Generic[_ModelT, _RT]): + """A decorator exposing the decorated class method as a property, with a warning on instance access. + + This decorator takes a class method defined on the `BaseModel` class and transforms it into + an attribute. The attribute can be accessed on both the class and instances of the class. If accessed + via an instance, a deprecation warning is emitted stating that instance access will be removed in V3. + """ + + def __init__(self, fget: Callable[[type[_ModelT]], _RT], /) -> None: + # Note: fget should be a classmethod: + self.fget = fget + + @overload + def __get__(self, instance: None, objtype: type[_ModelT]) -> _RT: ... + @overload + @deprecated( + 'Accessing this attribute on the instance is deprecated, and will be removed in Pydantic V3. ' + 'Instead, you should access this attribute from the model class.', + category=None, + ) + def __get__(self, instance: _ModelT, objtype: type[_ModelT]) -> _RT: ... + def __get__(self, instance: _ModelT | None, objtype: type[_ModelT]) -> _RT: + if instance is not None: + # fmt: off + attr_name = ( + self.fget.__name__ + if sys.version_info >= (3, 10) + else self.fget.__func__.__name__ # pyright: ignore[reportFunctionMemberAccess] + ) + # fmt: on + warnings.warn( + f'Accessing the {attr_name!r} attribute on the instance is deprecated. ' + 'Instead, you should access this attribute from the model class.', + category=PydanticDeprecatedSince211, + stacklevel=2, + ) + return self.fget.__get__(instance, objtype)() diff --git a/source/target-vendors/cp312/pydantic/_internal/_validate_call.py b/source/target-vendors/cp312/pydantic/_internal/_validate_call.py new file mode 100644 index 0000000..49ccd4f --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_validate_call.py @@ -0,0 +1,141 @@ +from __future__ import annotations as _annotations + +import functools +import inspect +from collections.abc import Awaitable +from functools import partial +from typing import Any, Callable + +import pydantic_core + +from ..config import ConfigDict +from ..plugin._schema_validator import create_schema_validator +from ._config import ConfigWrapper +from ._generate_schema import GenerateSchema, ValidateCallSupportedTypes +from ._namespace_utils import MappingNamespace, NsResolver, ns_for_function +from ._typing_extra import signature_no_eval + + +def extract_function_name(func: ValidateCallSupportedTypes) -> str: + """Extract the name of a `ValidateCallSupportedTypes` object.""" + return f'partial({func.func.__name__})' if isinstance(func, functools.partial) else func.__name__ + + +def extract_function_qualname(func: ValidateCallSupportedTypes) -> str: + """Extract the qualname of a `ValidateCallSupportedTypes` object.""" + return f'partial({func.func.__qualname__})' if isinstance(func, functools.partial) else func.__qualname__ + + +def update_wrapper_attributes(wrapped: ValidateCallSupportedTypes, wrapper: Callable[..., Any]): + """Update the `wrapper` function with the attributes of the `wrapped` function. Return the updated function.""" + if inspect.iscoroutinefunction(wrapped): + + @functools.wraps(wrapped) + async def wrapper_function(*args, **kwargs): # type: ignore + return await wrapper(*args, **kwargs) + else: + + @functools.wraps(wrapped) + def wrapper_function(*args, **kwargs): + return wrapper(*args, **kwargs) + + # We need to manually update this because `partial` object has no `__name__` and `__qualname__`. + wrapper_function.__name__ = extract_function_name(wrapped) + wrapper_function.__qualname__ = extract_function_qualname(wrapped) + wrapper_function.raw_function = wrapped # type: ignore + + return wrapper_function + + +class ValidateCallWrapper: + """This is a wrapper around a function that validates the arguments passed to it, and optionally the return value.""" + + __slots__ = ( + 'function', + 'validate_return', + 'schema_type', + 'module', + 'qualname', + 'ns_resolver', + 'config_wrapper', + '__pydantic_complete__', + '__pydantic_validator__', + '__return_pydantic_validator__', + ) + + def __init__( + self, + function: ValidateCallSupportedTypes, + config: ConfigDict | None, + validate_return: bool, + parent_namespace: MappingNamespace | None, + ) -> None: + self.function = function + self.validate_return = validate_return + if isinstance(function, partial): + self.schema_type = function.func + self.module = function.func.__module__ + else: + self.schema_type = function + self.module = function.__module__ + self.qualname = extract_function_qualname(function) + + self.ns_resolver = NsResolver( + namespaces_tuple=ns_for_function(self.schema_type, parent_namespace=parent_namespace) + ) + self.config_wrapper = ConfigWrapper(config) + if not self.config_wrapper.defer_build: + self._create_validators() + else: + self.__pydantic_complete__ = False + + def _create_validators(self) -> None: + gen_schema = GenerateSchema(self.config_wrapper, self.ns_resolver) + schema = gen_schema.clean_schema(gen_schema.generate_schema(self.function)) + core_config = self.config_wrapper.core_config(title=self.qualname) + + self.__pydantic_validator__ = create_schema_validator( + schema, + self.schema_type, + self.module, + self.qualname, + 'validate_call', + core_config, + self.config_wrapper.plugin_settings, + ) + if self.validate_return: + signature = signature_no_eval(self.function) + return_type = signature.return_annotation if signature.return_annotation is not signature.empty else Any + gen_schema = GenerateSchema(self.config_wrapper, self.ns_resolver) + schema = gen_schema.clean_schema(gen_schema.generate_schema(return_type)) + validator = create_schema_validator( + schema, + self.schema_type, + self.module, + self.qualname, + 'validate_call', + core_config, + self.config_wrapper.plugin_settings, + ) + if inspect.iscoroutinefunction(self.function): + + async def return_val_wrapper(aw: Awaitable[Any]) -> None: + return validator.validate_python(await aw) + + self.__return_pydantic_validator__ = return_val_wrapper + else: + self.__return_pydantic_validator__ = validator.validate_python + else: + self.__return_pydantic_validator__ = None + + self.__pydantic_complete__ = True + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + if not self.__pydantic_complete__: + self._create_validators() + + res = self.__pydantic_validator__.validate_python(pydantic_core.ArgsKwargs(args, kwargs)) + if self.__return_pydantic_validator__: + return self.__return_pydantic_validator__(res) + else: + return res diff --git a/source/target-vendors/cp312/pydantic/_internal/_validators.py b/source/target-vendors/cp312/pydantic/_internal/_validators.py new file mode 100644 index 0000000..3ad6c92 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_internal/_validators.py @@ -0,0 +1,534 @@ +"""Validator functions for standard library types. + +Import of this module is deferred since it contains imports of many standard library modules. +""" + +from __future__ import annotations as _annotations + +import collections.abc +import math +import re +import typing +from collections.abc import Sequence +from decimal import Decimal +from fractions import Fraction +from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network +from typing import Any, Callable, TypeVar, Union, cast +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError + +import typing_extensions +from pydantic_core import PydanticCustomError, PydanticKnownError, core_schema +from typing_extensions import get_args, get_origin +from typing_inspection import typing_objects + +from pydantic._internal._import_utils import import_cached_field_info +from pydantic.errors import PydanticSchemaGenerationError + + +def sequence_validator( + input_value: Sequence[Any], + /, + validator: core_schema.ValidatorFunctionWrapHandler, +) -> Sequence[Any]: + """Validator for `Sequence` types, isinstance(v, Sequence) has already been called.""" + value_type = type(input_value) + + # We don't accept any plain string as a sequence + # Relevant issue: https://github.com/pydantic/pydantic/issues/5595 + if issubclass(value_type, (str, bytes)): + raise PydanticCustomError( + 'sequence_str', + "'{type_name}' instances are not allowed as a Sequence value", + {'type_name': value_type.__name__}, + ) + + # TODO: refactor sequence validation to validate with either a list or a tuple + # schema, depending on the type of the value. + # Additionally, we should be able to remove one of either this validator or the + # SequenceValidator in _std_types_schema.py (preferably this one, while porting over some logic). + # Effectively, a refactor for sequence validation is needed. + if value_type is tuple: + input_value = list(input_value) + + v_list = validator(input_value) + + # the rest of the logic is just re-creating the original type from `v_list` + if value_type is list: + return v_list + elif issubclass(value_type, range): + # return the list as we probably can't re-create the range + return v_list + elif value_type is tuple: + return tuple(v_list) + else: + # best guess at how to re-create the original type, more custom construction logic might be required + return value_type(v_list) # type: ignore[call-arg] + + +def import_string(value: Any) -> Any: + if isinstance(value, str): + try: + return _import_string_logic(value) + except ImportError as e: + raise PydanticCustomError('import_error', 'Invalid python path: {error}', {'error': str(e)}) from e + else: + # otherwise we just return the value and let the next validator do the rest of the work + return value + + +def _import_string_logic(dotted_path: str) -> Any: + """Inspired by uvicorn — dotted paths should include a colon before the final item if that item is not a module. + (This is necessary to distinguish between a submodule and an attribute when there is a conflict.). + + If the dotted path does not include a colon and the final item is not a valid module, importing as an attribute + rather than a submodule will be attempted automatically. + + So, for example, the following values of `dotted_path` result in the following returned values: + * 'collections': + * 'collections.abc': + * 'collections.abc:Mapping': + * `collections.abc.Mapping`: (though this is a bit slower than the previous line) + + An error will be raised under any of the following scenarios: + * `dotted_path` contains more than one colon (e.g., 'collections:abc:Mapping') + * the substring of `dotted_path` before the colon is not a valid module in the environment (e.g., '123:Mapping') + * the substring of `dotted_path` after the colon is not an attribute of the module (e.g., 'collections:abc123') + """ + from importlib import import_module + + components = dotted_path.strip().split(':') + if len(components) > 2: + raise ImportError(f"Import strings should have at most one ':'; received {dotted_path!r}") + attribute = None + if len(components) == 2: + attribute = components[1] + module_path = components[0] + if not module_path: + raise ImportError(f'Import strings should have a nonempty module name; received {dotted_path!r}') + + try: + module = import_module(module_path) + except ModuleNotFoundError: + if attribute is None and '.' in module_path: + # Try interpreting the final dotted segment as an attribute, not a submodule + maybe_module_path, maybe_attribute = module_path.rsplit('.', 1) + + try: + return _import_string_logic(f'{maybe_module_path}:{maybe_attribute}') + except ImportError: + pass + raise + + if attribute is not None: + try: + return getattr(module, attribute) + except AttributeError as e: + raise ImportError(f'cannot import name {attribute!r} from {module_path!r}') from e + else: + return module + + +def pattern_either_validator(input_value: Any, /) -> re.Pattern[Any]: + if isinstance(input_value, re.Pattern): + return input_value + elif isinstance(input_value, (str, bytes)): + # todo strict mode + return compile_pattern(input_value) # type: ignore + else: + raise PydanticCustomError('pattern_type', 'Input should be a valid pattern') + + +def pattern_str_validator(input_value: Any, /) -> re.Pattern[str]: + if isinstance(input_value, re.Pattern): + if isinstance(input_value.pattern, str): + return input_value + else: + raise PydanticCustomError('pattern_str_type', 'Input should be a string pattern') + elif isinstance(input_value, str): + return compile_pattern(input_value) + elif isinstance(input_value, bytes): + raise PydanticCustomError('pattern_str_type', 'Input should be a string pattern') + else: + raise PydanticCustomError('pattern_type', 'Input should be a valid pattern') + + +def pattern_bytes_validator(input_value: Any, /) -> re.Pattern[bytes]: + if isinstance(input_value, re.Pattern): + if isinstance(input_value.pattern, bytes): + return input_value + else: + raise PydanticCustomError('pattern_bytes_type', 'Input should be a bytes pattern') + elif isinstance(input_value, bytes): + return compile_pattern(input_value) + elif isinstance(input_value, str): + raise PydanticCustomError('pattern_bytes_type', 'Input should be a bytes pattern') + else: + raise PydanticCustomError('pattern_type', 'Input should be a valid pattern') + + +PatternType = TypeVar('PatternType', str, bytes) + + +def compile_pattern(pattern: PatternType) -> re.Pattern[PatternType]: + try: + return re.compile(pattern) + except re.error: + raise PydanticCustomError('pattern_regex', 'Input should be a valid regular expression') + + +def ip_v4_address_validator(input_value: Any, /) -> IPv4Address: + if isinstance(input_value, IPv4Address): + return input_value + + try: + return IPv4Address(input_value) + except ValueError: + raise PydanticCustomError('ip_v4_address', 'Input is not a valid IPv4 address') + + +def ip_v6_address_validator(input_value: Any, /) -> IPv6Address: + if isinstance(input_value, IPv6Address): + return input_value + + try: + return IPv6Address(input_value) + except ValueError: + raise PydanticCustomError('ip_v6_address', 'Input is not a valid IPv6 address') + + +def ip_v4_network_validator(input_value: Any, /) -> IPv4Network: + """Assume IPv4Network initialised with a default `strict` argument. + + See more: + https://docs.python.org/library/ipaddress.html#ipaddress.IPv4Network + """ + if isinstance(input_value, IPv4Network): + return input_value + + try: + return IPv4Network(input_value) + except ValueError: + raise PydanticCustomError('ip_v4_network', 'Input is not a valid IPv4 network') + + +def ip_v6_network_validator(input_value: Any, /) -> IPv6Network: + """Assume IPv6Network initialised with a default `strict` argument. + + See more: + https://docs.python.org/library/ipaddress.html#ipaddress.IPv6Network + """ + if isinstance(input_value, IPv6Network): + return input_value + + try: + return IPv6Network(input_value) + except ValueError: + raise PydanticCustomError('ip_v6_network', 'Input is not a valid IPv6 network') + + +def ip_v4_interface_validator(input_value: Any, /) -> IPv4Interface: + if isinstance(input_value, IPv4Interface): + return input_value + + try: + return IPv4Interface(input_value) + except ValueError: + raise PydanticCustomError('ip_v4_interface', 'Input is not a valid IPv4 interface') + + +def ip_v6_interface_validator(input_value: Any, /) -> IPv6Interface: + if isinstance(input_value, IPv6Interface): + return input_value + + try: + return IPv6Interface(input_value) + except ValueError: + raise PydanticCustomError('ip_v6_interface', 'Input is not a valid IPv6 interface') + + +def fraction_validator(input_value: Any, /) -> Fraction: + if isinstance(input_value, Fraction): + return input_value + + try: + return Fraction(input_value) + except ValueError: + raise PydanticCustomError('fraction_parsing', 'Input is not a valid fraction') + + +def forbid_inf_nan_check(x: Any) -> Any: + if not math.isfinite(x): + raise PydanticKnownError('finite_number') + return x + + +def _safe_repr(v: Any) -> int | float | str: + """The context argument for `PydanticKnownError` requires a number or str type, so we do a simple repr() coercion for types like timedelta. + + See tests/test_types.py::test_annotated_metadata_any_order for some context. + """ + if isinstance(v, (int, float, str)): + return v + return repr(v) + + +def greater_than_validator(x: Any, gt: Any) -> Any: + try: + if not (x > gt): + raise PydanticKnownError('greater_than', {'gt': _safe_repr(gt)}) + return x + except TypeError: + raise TypeError(f"Unable to apply constraint 'gt' to supplied value {x}") + + +def greater_than_or_equal_validator(x: Any, ge: Any) -> Any: + try: + if not (x >= ge): + raise PydanticKnownError('greater_than_equal', {'ge': _safe_repr(ge)}) + return x + except TypeError: + raise TypeError(f"Unable to apply constraint 'ge' to supplied value {x}") + + +def less_than_validator(x: Any, lt: Any) -> Any: + try: + if not (x < lt): + raise PydanticKnownError('less_than', {'lt': _safe_repr(lt)}) + return x + except TypeError: + raise TypeError(f"Unable to apply constraint 'lt' to supplied value {x}") + + +def less_than_or_equal_validator(x: Any, le: Any) -> Any: + try: + if not (x <= le): + raise PydanticKnownError('less_than_equal', {'le': _safe_repr(le)}) + return x + except TypeError: + raise TypeError(f"Unable to apply constraint 'le' to supplied value {x}") + + +def multiple_of_validator(x: Any, multiple_of: Any) -> Any: + try: + if x % multiple_of: + raise PydanticKnownError('multiple_of', {'multiple_of': _safe_repr(multiple_of)}) + return x + except TypeError: + raise TypeError(f"Unable to apply constraint 'multiple_of' to supplied value {x}") + + +def min_length_validator(x: Any, min_length: Any) -> Any: + try: + if not (len(x) >= min_length): + raise PydanticKnownError( + 'too_short', {'field_type': 'Value', 'min_length': min_length, 'actual_length': len(x)} + ) + return x + except TypeError: + raise TypeError(f"Unable to apply constraint 'min_length' to supplied value {x}") + + +def max_length_validator(x: Any, max_length: Any) -> Any: + try: + if len(x) > max_length: + raise PydanticKnownError( + 'too_long', + {'field_type': 'Value', 'max_length': max_length, 'actual_length': len(x)}, + ) + return x + except TypeError: + raise TypeError(f"Unable to apply constraint 'max_length' to supplied value {x}") + + +def _extract_decimal_digits_info(decimal: Decimal) -> tuple[int, int]: + """Compute the total number of digits and decimal places for a given [`Decimal`][decimal.Decimal] instance. + + This function handles both normalized and non-normalized Decimal instances. + Example: Decimal('1.230') -> 4 digits, 3 decimal places + + Args: + decimal (Decimal): The decimal number to analyze. + + Returns: + tuple[int, int]: A tuple containing the number of decimal places and total digits. + + Though this could be divided into two separate functions, the logic is easier to follow if we couple the computation + of the number of decimals and digits together. + """ + try: + decimal_tuple = decimal.as_tuple() + + assert isinstance(decimal_tuple.exponent, int) + + exponent = decimal_tuple.exponent + num_digits = len(decimal_tuple.digits) + + if exponent >= 0: + # A positive exponent adds that many trailing zeros + # Ex: digit_tuple=(1, 2, 3), exponent=2 -> 12300 -> 0 decimal places, 5 digits + num_digits += exponent + decimal_places = 0 + else: + # If the absolute value of the negative exponent is larger than the + # number of digits, then it's the same as the number of digits, + # because it'll consume all the digits in digit_tuple and then + # add abs(exponent) - len(digit_tuple) leading zeros after the decimal point. + # Ex: digit_tuple=(1, 2, 3), exponent=-2 -> 1.23 -> 2 decimal places, 3 digits + # Ex: digit_tuple=(1, 2, 3), exponent=-4 -> 0.0123 -> 4 decimal places, 4 digits + decimal_places = abs(exponent) + num_digits = max(num_digits, decimal_places) + + return decimal_places, num_digits + except (AssertionError, AttributeError): + raise TypeError(f'Unable to extract decimal digits info from supplied value {decimal}') + + +def max_digits_validator(x: Any, max_digits: Any) -> Any: + try: + _, num_digits = _extract_decimal_digits_info(x) + _, normalized_num_digits = _extract_decimal_digits_info(x.normalize()) + if (num_digits > max_digits) and (normalized_num_digits > max_digits): + raise PydanticKnownError( + 'decimal_max_digits', + {'max_digits': max_digits}, + ) + return x + except TypeError: + raise TypeError(f"Unable to apply constraint 'max_digits' to supplied value {x}") + + +def decimal_places_validator(x: Any, decimal_places: Any) -> Any: + try: + decimal_places_, _ = _extract_decimal_digits_info(x) + if decimal_places_ > decimal_places: + normalized_decimal_places, _ = _extract_decimal_digits_info(x.normalize()) + if normalized_decimal_places > decimal_places: + raise PydanticKnownError( + 'decimal_max_places', + {'decimal_places': decimal_places}, + ) + return x + except TypeError: + raise TypeError(f"Unable to apply constraint 'decimal_places' to supplied value {x}") + + +def deque_validator(input_value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> collections.deque[Any]: + return collections.deque(handler(input_value), maxlen=getattr(input_value, 'maxlen', None)) + + +def defaultdict_validator( + input_value: Any, handler: core_schema.ValidatorFunctionWrapHandler, default_default_factory: Callable[[], Any] +) -> collections.defaultdict[Any, Any]: + if isinstance(input_value, collections.defaultdict): + default_factory = input_value.default_factory + return collections.defaultdict(default_factory, handler(input_value)) + else: + return collections.defaultdict(default_default_factory, handler(input_value)) + + +def get_defaultdict_default_default_factory(values_source_type: Any) -> Callable[[], Any]: + FieldInfo = import_cached_field_info() + + values_type_origin = get_origin(values_source_type) + + def infer_default() -> Callable[[], Any]: + allowed_default_types: dict[Any, Any] = { + tuple: tuple, + collections.abc.Sequence: tuple, + collections.abc.MutableSequence: list, + list: list, + typing.Sequence: list, + set: set, + typing.MutableSet: set, + collections.abc.MutableSet: set, + collections.abc.Set: frozenset, + typing.MutableMapping: dict, + typing.Mapping: dict, + collections.abc.Mapping: dict, + collections.abc.MutableMapping: dict, + float: float, + int: int, + str: str, + bool: bool, + } + values_type = values_type_origin or values_source_type + instructions = 'set using `DefaultDict[..., Annotated[..., Field(default_factory=...)]]`' + if typing_objects.is_typevar(values_type): + + def type_var_default_factory() -> None: + raise RuntimeError( + 'Generic defaultdict cannot be used without a concrete value type or an' + ' explicit default factory, ' + instructions + ) + + return type_var_default_factory + elif values_type not in allowed_default_types: + # a somewhat subjective set of types that have reasonable default values + allowed_msg = ', '.join([t.__name__ for t in set(allowed_default_types.values())]) + raise PydanticSchemaGenerationError( + f'Unable to infer a default factory for keys of type {values_source_type}.' + f' Only {allowed_msg} are supported, other types require an explicit default factory' + ' ' + instructions + ) + return allowed_default_types[values_type] + + # Assume Annotated[..., Field(...)] + if typing_objects.is_annotated(values_type_origin): + field_info = next((v for v in get_args(values_source_type) if isinstance(v, FieldInfo)), None) + else: + field_info = None + if field_info and field_info.default_factory: + # Assume the default factory does not take any argument: + default_default_factory = cast(Callable[[], Any], field_info.default_factory) + else: + default_default_factory = infer_default() + return default_default_factory + + +def validate_str_is_valid_iana_tz(value: Any, /) -> ZoneInfo: + if isinstance(value, ZoneInfo): + return value + try: + return ZoneInfo(value) + except (ZoneInfoNotFoundError, ValueError, TypeError): + raise PydanticCustomError('zoneinfo_str', 'invalid timezone: {value}', {'value': value}) + + +NUMERIC_VALIDATOR_LOOKUP: dict[str, Callable] = { + 'gt': greater_than_validator, + 'ge': greater_than_or_equal_validator, + 'lt': less_than_validator, + 'le': less_than_or_equal_validator, + 'multiple_of': multiple_of_validator, + 'min_length': min_length_validator, + 'max_length': max_length_validator, + 'max_digits': max_digits_validator, + 'decimal_places': decimal_places_validator, +} + +IpType = Union[IPv4Address, IPv6Address, IPv4Network, IPv6Network, IPv4Interface, IPv6Interface] + +IP_VALIDATOR_LOOKUP: dict[type[IpType], Callable] = { + IPv4Address: ip_v4_address_validator, + IPv6Address: ip_v6_address_validator, + IPv4Network: ip_v4_network_validator, + IPv6Network: ip_v6_network_validator, + IPv4Interface: ip_v4_interface_validator, + IPv6Interface: ip_v6_interface_validator, +} + +MAPPING_ORIGIN_MAP: dict[Any, Any] = { + typing.DefaultDict: collections.defaultdict, # noqa: UP006 + collections.defaultdict: collections.defaultdict, + typing.OrderedDict: collections.OrderedDict, # noqa: UP006 + collections.OrderedDict: collections.OrderedDict, + typing_extensions.OrderedDict: collections.OrderedDict, + typing.Counter: collections.Counter, + collections.Counter: collections.Counter, + # this doesn't handle subclasses of these + typing.Mapping: dict, + typing.MutableMapping: dict, + # parametrized typing.{Mutable}Mapping creates one of these + collections.abc.Mapping: dict, + collections.abc.MutableMapping: dict, +} diff --git a/source/target-vendors/cp312/pydantic/_migration.py b/source/target-vendors/cp312/pydantic/_migration.py new file mode 100644 index 0000000..b4ecd28 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/_migration.py @@ -0,0 +1,316 @@ +import sys +from typing import Any, Callable + +from pydantic.warnings import PydanticDeprecatedSince20 + +from .version import version_short + +MOVED_IN_V2 = { + 'pydantic.utils:version_info': 'pydantic.version:version_info', + 'pydantic.error_wrappers:ValidationError': 'pydantic:ValidationError', + 'pydantic.utils:to_camel': 'pydantic.alias_generators:to_pascal', + 'pydantic.utils:to_lower_camel': 'pydantic.alias_generators:to_camel', + 'pydantic:PyObject': 'pydantic.types:ImportString', + 'pydantic.types:PyObject': 'pydantic.types:ImportString', + 'pydantic.generics:GenericModel': 'pydantic.BaseModel', +} + +DEPRECATED_MOVED_IN_V2 = { + 'pydantic.tools:schema_of': 'pydantic.deprecated.tools:schema_of', + 'pydantic.tools:parse_obj_as': 'pydantic.deprecated.tools:parse_obj_as', + 'pydantic.tools:schema_json_of': 'pydantic.deprecated.tools:schema_json_of', + 'pydantic.json:pydantic_encoder': 'pydantic.deprecated.json:pydantic_encoder', + 'pydantic:validate_arguments': 'pydantic.deprecated.decorator:validate_arguments', + 'pydantic.json:custom_pydantic_encoder': 'pydantic.deprecated.json:custom_pydantic_encoder', + 'pydantic.json:timedelta_isoformat': 'pydantic.deprecated.json:timedelta_isoformat', + 'pydantic.decorator:validate_arguments': 'pydantic.deprecated.decorator:validate_arguments', + 'pydantic.class_validators:validator': 'pydantic.deprecated.class_validators:validator', + 'pydantic.class_validators:root_validator': 'pydantic.deprecated.class_validators:root_validator', + 'pydantic.config:BaseConfig': 'pydantic.deprecated.config:BaseConfig', + 'pydantic.config:Extra': 'pydantic.deprecated.config:Extra', +} + +REDIRECT_TO_V1 = { + f'pydantic.utils:{obj}': f'pydantic.v1.utils:{obj}' + for obj in ( + 'deep_update', + 'GetterDict', + 'lenient_issubclass', + 'lenient_isinstance', + 'is_valid_field', + 'update_not_none', + 'import_string', + 'Representation', + 'ROOT_KEY', + 'smart_deepcopy', + 'sequence_like', + ) +} + + +REMOVED_IN_V2 = { + 'pydantic:ConstrainedBytes', + 'pydantic:ConstrainedDate', + 'pydantic:ConstrainedDecimal', + 'pydantic:ConstrainedFloat', + 'pydantic:ConstrainedFrozenSet', + 'pydantic:ConstrainedInt', + 'pydantic:ConstrainedList', + 'pydantic:ConstrainedSet', + 'pydantic:ConstrainedStr', + 'pydantic:JsonWrapper', + 'pydantic:NoneBytes', + 'pydantic:NoneStr', + 'pydantic:NoneStrBytes', + 'pydantic:Protocol', + 'pydantic:Required', + 'pydantic:StrBytes', + 'pydantic:compiled', + 'pydantic.config:get_config', + 'pydantic.config:inherit_config', + 'pydantic.config:prepare_config', + 'pydantic:create_model_from_namedtuple', + 'pydantic:create_model_from_typeddict', + 'pydantic.dataclasses:create_pydantic_model_from_dataclass', + 'pydantic.dataclasses:make_dataclass_validator', + 'pydantic.dataclasses:set_validation', + 'pydantic.datetime_parse:parse_date', + 'pydantic.datetime_parse:parse_time', + 'pydantic.datetime_parse:parse_datetime', + 'pydantic.datetime_parse:parse_duration', + 'pydantic.error_wrappers:ErrorWrapper', + 'pydantic.errors:AnyStrMaxLengthError', + 'pydantic.errors:AnyStrMinLengthError', + 'pydantic.errors:ArbitraryTypeError', + 'pydantic.errors:BoolError', + 'pydantic.errors:BytesError', + 'pydantic.errors:CallableError', + 'pydantic.errors:ClassError', + 'pydantic.errors:ColorError', + 'pydantic.errors:ConfigError', + 'pydantic.errors:DataclassTypeError', + 'pydantic.errors:DateError', + 'pydantic.errors:DateNotInTheFutureError', + 'pydantic.errors:DateNotInThePastError', + 'pydantic.errors:DateTimeError', + 'pydantic.errors:DecimalError', + 'pydantic.errors:DecimalIsNotFiniteError', + 'pydantic.errors:DecimalMaxDigitsError', + 'pydantic.errors:DecimalMaxPlacesError', + 'pydantic.errors:DecimalWholeDigitsError', + 'pydantic.errors:DictError', + 'pydantic.errors:DurationError', + 'pydantic.errors:EmailError', + 'pydantic.errors:EnumError', + 'pydantic.errors:EnumMemberError', + 'pydantic.errors:ExtraError', + 'pydantic.errors:FloatError', + 'pydantic.errors:FrozenSetError', + 'pydantic.errors:FrozenSetMaxLengthError', + 'pydantic.errors:FrozenSetMinLengthError', + 'pydantic.errors:HashableError', + 'pydantic.errors:IPv4AddressError', + 'pydantic.errors:IPv4InterfaceError', + 'pydantic.errors:IPv4NetworkError', + 'pydantic.errors:IPv6AddressError', + 'pydantic.errors:IPv6InterfaceError', + 'pydantic.errors:IPv6NetworkError', + 'pydantic.errors:IPvAnyAddressError', + 'pydantic.errors:IPvAnyInterfaceError', + 'pydantic.errors:IPvAnyNetworkError', + 'pydantic.errors:IntEnumError', + 'pydantic.errors:IntegerError', + 'pydantic.errors:InvalidByteSize', + 'pydantic.errors:InvalidByteSizeUnit', + 'pydantic.errors:InvalidDiscriminator', + 'pydantic.errors:InvalidLengthForBrand', + 'pydantic.errors:JsonError', + 'pydantic.errors:JsonTypeError', + 'pydantic.errors:ListError', + 'pydantic.errors:ListMaxLengthError', + 'pydantic.errors:ListMinLengthError', + 'pydantic.errors:ListUniqueItemsError', + 'pydantic.errors:LuhnValidationError', + 'pydantic.errors:MissingDiscriminator', + 'pydantic.errors:MissingError', + 'pydantic.errors:NoneIsAllowedError', + 'pydantic.errors:NoneIsNotAllowedError', + 'pydantic.errors:NotDigitError', + 'pydantic.errors:NotNoneError', + 'pydantic.errors:NumberNotGeError', + 'pydantic.errors:NumberNotGtError', + 'pydantic.errors:NumberNotLeError', + 'pydantic.errors:NumberNotLtError', + 'pydantic.errors:NumberNotMultipleError', + 'pydantic.errors:PathError', + 'pydantic.errors:PathNotADirectoryError', + 'pydantic.errors:PathNotAFileError', + 'pydantic.errors:PathNotExistsError', + 'pydantic.errors:PatternError', + 'pydantic.errors:PyObjectError', + 'pydantic.errors:PydanticTypeError', + 'pydantic.errors:PydanticValueError', + 'pydantic.errors:SequenceError', + 'pydantic.errors:SetError', + 'pydantic.errors:SetMaxLengthError', + 'pydantic.errors:SetMinLengthError', + 'pydantic.errors:StrError', + 'pydantic.errors:StrRegexError', + 'pydantic.errors:StrictBoolError', + 'pydantic.errors:SubclassError', + 'pydantic.errors:TimeError', + 'pydantic.errors:TupleError', + 'pydantic.errors:TupleLengthError', + 'pydantic.errors:UUIDError', + 'pydantic.errors:UUIDVersionError', + 'pydantic.errors:UrlError', + 'pydantic.errors:UrlExtraError', + 'pydantic.errors:UrlHostError', + 'pydantic.errors:UrlHostTldError', + 'pydantic.errors:UrlPortError', + 'pydantic.errors:UrlSchemeError', + 'pydantic.errors:UrlSchemePermittedError', + 'pydantic.errors:UrlUserInfoError', + 'pydantic.errors:WrongConstantError', + 'pydantic.main:validate_model', + 'pydantic.networks:stricturl', + 'pydantic:parse_file_as', + 'pydantic:parse_raw_as', + 'pydantic:stricturl', + 'pydantic.tools:parse_file_as', + 'pydantic.tools:parse_raw_as', + 'pydantic.types:ConstrainedBytes', + 'pydantic.types:ConstrainedDate', + 'pydantic.types:ConstrainedDecimal', + 'pydantic.types:ConstrainedFloat', + 'pydantic.types:ConstrainedFrozenSet', + 'pydantic.types:ConstrainedInt', + 'pydantic.types:ConstrainedList', + 'pydantic.types:ConstrainedSet', + 'pydantic.types:ConstrainedStr', + 'pydantic.types:JsonWrapper', + 'pydantic.types:NoneBytes', + 'pydantic.types:NoneStr', + 'pydantic.types:NoneStrBytes', + 'pydantic.types:StrBytes', + 'pydantic.typing:evaluate_forwardref', + 'pydantic.typing:AbstractSetIntStr', + 'pydantic.typing:AnyCallable', + 'pydantic.typing:AnyClassMethod', + 'pydantic.typing:CallableGenerator', + 'pydantic.typing:DictAny', + 'pydantic.typing:DictIntStrAny', + 'pydantic.typing:DictStrAny', + 'pydantic.typing:IntStr', + 'pydantic.typing:ListStr', + 'pydantic.typing:MappingIntStrAny', + 'pydantic.typing:NoArgAnyCallable', + 'pydantic.typing:NoneType', + 'pydantic.typing:ReprArgs', + 'pydantic.typing:SetStr', + 'pydantic.typing:StrPath', + 'pydantic.typing:TupleGenerator', + 'pydantic.typing:WithArgsTypes', + 'pydantic.typing:all_literal_values', + 'pydantic.typing:display_as_type', + 'pydantic.typing:get_all_type_hints', + 'pydantic.typing:get_args', + 'pydantic.typing:get_origin', + 'pydantic.typing:get_sub_types', + 'pydantic.typing:is_callable_type', + 'pydantic.typing:is_classvar', + 'pydantic.typing:is_finalvar', + 'pydantic.typing:is_literal_type', + 'pydantic.typing:is_namedtuple', + 'pydantic.typing:is_new_type', + 'pydantic.typing:is_none_type', + 'pydantic.typing:is_typeddict', + 'pydantic.typing:is_typeddict_special', + 'pydantic.typing:is_union', + 'pydantic.typing:new_type_supertype', + 'pydantic.typing:resolve_annotations', + 'pydantic.typing:typing_base', + 'pydantic.typing:update_field_forward_refs', + 'pydantic.typing:update_model_forward_refs', + 'pydantic.utils:ClassAttribute', + 'pydantic.utils:DUNDER_ATTRIBUTES', + 'pydantic.utils:PyObjectStr', + 'pydantic.utils:ValueItems', + 'pydantic.utils:almost_equal_floats', + 'pydantic.utils:get_discriminator_alias_and_values', + 'pydantic.utils:get_model', + 'pydantic.utils:get_unique_discriminator_alias', + 'pydantic.utils:in_ipython', + 'pydantic.utils:is_valid_identifier', + 'pydantic.utils:path_type', + 'pydantic.utils:validate_field_name', + 'pydantic:validate_model', +} + + +def getattr_migration(module: str) -> Callable[[str], Any]: + """Implement PEP 562 for objects that were either moved or removed on the migration + to V2. + + Args: + module: The module name. + + Returns: + A callable that will raise an error if the object is not found. + """ + # This avoids circular import with errors.py. + from .errors import PydanticImportError + + def wrapper(name: str) -> object: + """Raise an error if the object is not found, or warn if it was moved. + + In case it was moved, it still returns the object. + + Args: + name: The object name. + + Returns: + The object. + """ + if name == '__path__': + raise AttributeError(f'module {module!r} has no attribute {name!r}') + + import warnings + + from ._internal._validators import import_string + + import_path = f'{module}:{name}' + if import_path in MOVED_IN_V2.keys(): + new_location = MOVED_IN_V2[import_path] + warnings.warn( + f'`{import_path}` has been moved to `{new_location}`.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + return import_string(MOVED_IN_V2[import_path]) + if import_path in DEPRECATED_MOVED_IN_V2: + # skip the warning here because a deprecation warning will be raised elsewhere + return import_string(DEPRECATED_MOVED_IN_V2[import_path]) + if import_path in REDIRECT_TO_V1: + new_location = REDIRECT_TO_V1[import_path] + warnings.warn( + f'`{import_path}` has been removed. We are importing from `{new_location}` instead.' + 'See the migration guide for more details: https://docs.pydantic.dev/latest/migration/', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + return import_string(REDIRECT_TO_V1[import_path]) + if import_path == 'pydantic:BaseSettings': + raise PydanticImportError( + '`BaseSettings` has been moved to the `pydantic-settings` package. ' + f'See https://docs.pydantic.dev/{version_short()}/migration/#basesettings-has-moved-to-pydantic-settings ' + 'for more details.' + ) + if import_path in REMOVED_IN_V2: + raise PydanticImportError(f'`{import_path}` has been removed in V2.') + globals: dict[str, Any] = sys.modules[module].__dict__ + if name in globals: + return globals[name] + raise AttributeError(f'module {module!r} has no attribute {name!r}') + + return wrapper diff --git a/source/target-vendors/cp312/pydantic/alias_generators.py b/source/target-vendors/cp312/pydantic/alias_generators.py new file mode 100644 index 0000000..0b7653f --- /dev/null +++ b/source/target-vendors/cp312/pydantic/alias_generators.py @@ -0,0 +1,62 @@ +"""Alias generators for converting between different capitalization conventions.""" + +import re + +__all__ = ('to_pascal', 'to_camel', 'to_snake') + +# TODO: in V3, change the argument names to be more descriptive +# Generally, don't only convert from snake_case, or name the functions +# more specifically like snake_to_camel. + + +def to_pascal(snake: str) -> str: + """Convert a snake_case string to PascalCase. + + Args: + snake: The string to convert. + + Returns: + The PascalCase string. + """ + camel = snake.title() + return re.sub('([0-9A-Za-z])_(?=[0-9A-Z])', lambda m: m.group(1), camel) + + +def to_camel(snake: str) -> str: + """Convert a snake_case string to camelCase. + + Args: + snake: The string to convert. + + Returns: + The converted camelCase string. + """ + # If the string is already in camelCase and does not contain a digit followed + # by a lowercase letter, return it as it is + if re.match('^[a-z]+[A-Za-z0-9]*$', snake) and not re.search(r'\d[a-z]', snake): + return snake + + camel = to_pascal(snake) + return re.sub('(^_*[A-Z])', lambda m: m.group(1).lower(), camel) + + +def to_snake(camel: str) -> str: + """Convert a PascalCase, camelCase, or kebab-case string to snake_case. + + Args: + camel: The string to convert. + + Returns: + The converted string in snake_case. + """ + # Handle the sequence of uppercase letters followed by a lowercase letter + snake = re.sub(r'([A-Z]+)([A-Z][a-z])', lambda m: f'{m.group(1)}_{m.group(2)}', camel) + # Insert an underscore between a lowercase letter and an uppercase letter + snake = re.sub(r'([a-z])([A-Z])', lambda m: f'{m.group(1)}_{m.group(2)}', snake) + # Insert an underscore between a digit and an uppercase letter + snake = re.sub(r'([0-9])([A-Z])', lambda m: f'{m.group(1)}_{m.group(2)}', snake) + # Insert an underscore between a lowercase letter and a digit + snake = re.sub(r'([a-z])([0-9])', lambda m: f'{m.group(1)}_{m.group(2)}', snake) + # Replace hyphens with underscores to handle kebab-case + snake = snake.replace('-', '_') + return snake.lower() diff --git a/source/target-vendors/cp312/pydantic/aliases.py b/source/target-vendors/cp312/pydantic/aliases.py new file mode 100644 index 0000000..ac22737 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/aliases.py @@ -0,0 +1,135 @@ +"""Support for alias configurations.""" + +from __future__ import annotations + +import dataclasses +from typing import Any, Callable, Literal + +from pydantic_core import PydanticUndefined + +from ._internal import _internal_dataclass + +__all__ = ('AliasGenerator', 'AliasPath', 'AliasChoices') + + +@dataclasses.dataclass(**_internal_dataclass.slots_true) +class AliasPath: + """!!! abstract "Usage Documentation" + [`AliasPath` and `AliasChoices`](../concepts/alias.md#aliaspath-and-aliaschoices) + + A data class used by `validation_alias` as a convenience to create aliases. + + Attributes: + path: A list of string or integer aliases. + """ + + path: list[int | str] + + def __init__(self, first_arg: str, *args: str | int) -> None: + self.path = [first_arg] + list(args) + + def convert_to_aliases(self) -> list[str | int]: + """Converts arguments to a list of string or integer aliases. + + Returns: + The list of aliases. + """ + return self.path + + def search_dict_for_path(self, d: dict) -> Any: + """Searches a dictionary for the path specified by the alias. + + Returns: + The value at the specified path, or `PydanticUndefined` if the path is not found. + """ + v = d + for k in self.path: + if isinstance(v, str): + # disallow indexing into a str, like for AliasPath('x', 0) and x='abc' + return PydanticUndefined + try: + v = v[k] + except (KeyError, IndexError, TypeError): + return PydanticUndefined + return v + + +@dataclasses.dataclass(**_internal_dataclass.slots_true) +class AliasChoices: + """!!! abstract "Usage Documentation" + [`AliasPath` and `AliasChoices`](../concepts/alias.md#aliaspath-and-aliaschoices) + + A data class used by `validation_alias` as a convenience to create aliases. + + Attributes: + choices: A list containing a string or `AliasPath`. + """ + + choices: list[str | AliasPath] + + def __init__(self, first_choice: str | AliasPath, *choices: str | AliasPath) -> None: + self.choices = [first_choice] + list(choices) + + def convert_to_aliases(self) -> list[list[str | int]]: + """Converts arguments to a list of lists containing string or integer aliases. + + Returns: + The list of aliases. + """ + aliases: list[list[str | int]] = [] + for c in self.choices: + if isinstance(c, AliasPath): + aliases.append(c.convert_to_aliases()) + else: + aliases.append([c]) + return aliases + + +@dataclasses.dataclass(**_internal_dataclass.slots_true) +class AliasGenerator: + """!!! abstract "Usage Documentation" + [Using an `AliasGenerator`](../concepts/alias.md#using-an-aliasgenerator) + + A data class used by `alias_generator` as a convenience to create various aliases. + + Attributes: + alias: A callable that takes a field name and returns an alias for it. + validation_alias: A callable that takes a field name and returns a validation alias for it. + serialization_alias: A callable that takes a field name and returns a serialization alias for it. + """ + + alias: Callable[[str], str] | None = None + validation_alias: Callable[[str], str | AliasPath | AliasChoices] | None = None + serialization_alias: Callable[[str], str] | None = None + + def _generate_alias( + self, + alias_kind: Literal['alias', 'validation_alias', 'serialization_alias'], + allowed_types: tuple[type[str] | type[AliasPath] | type[AliasChoices], ...], + field_name: str, + ) -> str | AliasPath | AliasChoices | None: + """Generate an alias of the specified kind. Returns None if the alias generator is None. + + Raises: + TypeError: If the alias generator produces an invalid type. + """ + alias = None + if alias_generator := getattr(self, alias_kind): + alias = alias_generator(field_name) + if alias and not isinstance(alias, allowed_types): + raise TypeError( + f'Invalid `{alias_kind}` type. `{alias_kind}` generator must produce one of `{allowed_types}`' + ) + return alias + + def generate_aliases(self, field_name: str) -> tuple[str | None, str | AliasPath | AliasChoices | None, str | None]: + """Generate `alias`, `validation_alias`, and `serialization_alias` for a field. + + Returns: + A tuple of three aliases - validation, alias, and serialization. + """ + alias = self._generate_alias('alias', (str,), field_name) + validation_alias = self._generate_alias('validation_alias', (str, AliasChoices, AliasPath), field_name) + serialization_alias = self._generate_alias('serialization_alias', (str,), field_name) + + return alias, validation_alias, serialization_alias # type: ignore diff --git a/source/target-vendors/cp312/pydantic/annotated_handlers.py b/source/target-vendors/cp312/pydantic/annotated_handlers.py new file mode 100644 index 0000000..d0cb5d3 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/annotated_handlers.py @@ -0,0 +1,122 @@ +"""Type annotations to use with `__get_pydantic_core_schema__` and `__get_pydantic_json_schema__`.""" + +from __future__ import annotations as _annotations + +from typing import TYPE_CHECKING, Any, Union + +from pydantic_core import core_schema + +if TYPE_CHECKING: + from ._internal._namespace_utils import NamespacesTuple + from .json_schema import JsonSchemaMode, JsonSchemaValue + + CoreSchemaOrField = Union[ + core_schema.CoreSchema, + core_schema.ModelField, + core_schema.DataclassField, + core_schema.TypedDictField, + core_schema.ComputedField, + ] + +__all__ = 'GetJsonSchemaHandler', 'GetCoreSchemaHandler' + + +class GetJsonSchemaHandler: + """Handler to call into the next JSON schema generation function. + + Attributes: + mode: Json schema mode, can be `validation` or `serialization`. + """ + + mode: JsonSchemaMode + + def __call__(self, core_schema: CoreSchemaOrField, /) -> JsonSchemaValue: + """Call the inner handler and get the JsonSchemaValue it returns. + This will call the next JSON schema modifying function up until it calls + into `pydantic.json_schema.GenerateJsonSchema`, which will raise a + `pydantic.errors.PydanticInvalidForJsonSchema` error if it cannot generate + a JSON schema. + + Args: + core_schema: A `pydantic_core.core_schema.CoreSchema`. + + Returns: + JsonSchemaValue: The JSON schema generated by the inner JSON schema modify + functions. + """ + raise NotImplementedError + + def resolve_ref_schema(self, maybe_ref_json_schema: JsonSchemaValue, /) -> JsonSchemaValue: + """Get the real schema for a `{"$ref": ...}` schema. + If the schema given is not a `$ref` schema, it will be returned as is. + This means you don't have to check before calling this function. + + Args: + maybe_ref_json_schema: A JsonSchemaValue which may be a `$ref` schema. + + Raises: + LookupError: If the ref is not found. + + Returns: + JsonSchemaValue: A JsonSchemaValue that has no `$ref`. + """ + raise NotImplementedError + + +class GetCoreSchemaHandler: + """Handler to call into the next CoreSchema schema generation function.""" + + def __call__(self, source_type: Any, /) -> core_schema.CoreSchema: + """Call the inner handler and get the CoreSchema it returns. + This will call the next CoreSchema modifying function up until it calls + into Pydantic's internal schema generation machinery, which will raise a + `pydantic.errors.PydanticSchemaGenerationError` error if it cannot generate + a CoreSchema for the given source type. + + Args: + source_type: The input type. + + Returns: + CoreSchema: The `pydantic-core` CoreSchema generated. + """ + raise NotImplementedError + + def generate_schema(self, source_type: Any, /) -> core_schema.CoreSchema: + """Generate a schema unrelated to the current context. + Use this function if e.g. you are handling schema generation for a sequence + and want to generate a schema for its items. + Otherwise, you may end up doing something like applying a `min_length` constraint + that was intended for the sequence itself to its items! + + Args: + source_type: The input type. + + Returns: + CoreSchema: The `pydantic-core` CoreSchema generated. + """ + raise NotImplementedError + + def resolve_ref_schema(self, maybe_ref_schema: core_schema.CoreSchema, /) -> core_schema.CoreSchema: + """Get the real schema for a `definition-ref` schema. + If the schema given is not a `definition-ref` schema, it will be returned as is. + This means you don't have to check before calling this function. + + Args: + maybe_ref_schema: A `CoreSchema`, `ref`-based or not. + + Raises: + LookupError: If the `ref` is not found. + + Returns: + A concrete `CoreSchema`. + """ + raise NotImplementedError + + @property + def field_name(self) -> str | None: + """Get the name of the closest field to this validator.""" + raise NotImplementedError + + def _get_types_namespace(self) -> NamespacesTuple: + """Internal method used during type resolution for serializer annotations.""" + raise NotImplementedError diff --git a/source/target-vendors/cp312/pydantic/class_validators.py b/source/target-vendors/cp312/pydantic/class_validators.py new file mode 100644 index 0000000..9977150 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/class_validators.py @@ -0,0 +1,5 @@ +"""`class_validators` module is a backport module from V1.""" + +from ._migration import getattr_migration + +__getattr__ = getattr_migration(__name__) diff --git a/source/target-vendors/cp312/pydantic/color.py b/source/target-vendors/cp312/pydantic/color.py new file mode 100644 index 0000000..9a42d58 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/color.py @@ -0,0 +1,604 @@ +"""Color definitions are used as per the CSS3 +[CSS Color Module Level 3](http://www.w3.org/TR/css3-color/#svg-color) specification. + +A few colors have multiple names referring to the sames colors, eg. `grey` and `gray` or `aqua` and `cyan`. + +In these cases the _last_ color when sorted alphabetically takes preferences, +eg. `Color((0, 255, 255)).as_named() == 'cyan'` because "cyan" comes after "aqua". + +Warning: Deprecated + The `Color` class is deprecated, use `pydantic_extra_types` instead. + See [`pydantic-extra-types.Color`](../usage/types/extra_types/color_types.md) + for more information. +""" + +import math +import re +from colorsys import hls_to_rgb, rgb_to_hls +from typing import Any, Callable, Optional, Union, cast + +from pydantic_core import CoreSchema, PydanticCustomError, core_schema +from typing_extensions import deprecated + +from ._internal import _repr +from ._internal._schema_generation_shared import GetJsonSchemaHandler as _GetJsonSchemaHandler +from .json_schema import JsonSchemaValue +from .warnings import PydanticDeprecatedSince20 + +ColorTuple = Union[tuple[int, int, int], tuple[int, int, int, float]] +ColorType = Union[ColorTuple, str] +HslColorTuple = Union[tuple[float, float, float], tuple[float, float, float, float]] + + +class RGBA: + """Internal use only as a representation of a color.""" + + __slots__ = 'r', 'g', 'b', 'alpha', '_tuple' + + def __init__(self, r: float, g: float, b: float, alpha: Optional[float]): + self.r = r + self.g = g + self.b = b + self.alpha = alpha + + self._tuple: tuple[float, float, float, Optional[float]] = (r, g, b, alpha) + + def __getitem__(self, item: Any) -> Any: + return self._tuple[item] + + +# these are not compiled here to avoid import slowdown, they'll be compiled the first time they're used, then cached +_r_255 = r'(\d{1,3}(?:\.\d+)?)' +_r_comma = r'\s*,\s*' +_r_alpha = r'(\d(?:\.\d+)?|\.\d+|\d{1,2}%)' +_r_h = r'(-?\d+(?:\.\d+)?|-?\.\d+)(deg|rad|turn)?' +_r_sl = r'(\d{1,3}(?:\.\d+)?)%' +r_hex_short = r'\s*(?:#|0x)?([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])?\s*' +r_hex_long = r'\s*(?:#|0x)?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?\s*' +# CSS3 RGB examples: rgb(0, 0, 0), rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 50%) +r_rgb = rf'\s*rgba?\(\s*{_r_255}{_r_comma}{_r_255}{_r_comma}{_r_255}(?:{_r_comma}{_r_alpha})?\s*\)\s*' +# CSS3 HSL examples: hsl(270, 60%, 50%), hsla(270, 60%, 50%, 0.5), hsla(270, 60%, 50%, 50%) +r_hsl = rf'\s*hsla?\(\s*{_r_h}{_r_comma}{_r_sl}{_r_comma}{_r_sl}(?:{_r_comma}{_r_alpha})?\s*\)\s*' +# CSS4 RGB examples: rgb(0 0 0), rgb(0 0 0 / 0.5), rgb(0 0 0 / 50%), rgba(0 0 0 / 50%) +r_rgb_v4_style = rf'\s*rgba?\(\s*{_r_255}\s+{_r_255}\s+{_r_255}(?:\s*/\s*{_r_alpha})?\s*\)\s*' +# CSS4 HSL examples: hsl(270 60% 50%), hsl(270 60% 50% / 0.5), hsl(270 60% 50% / 50%), hsla(270 60% 50% / 50%) +r_hsl_v4_style = rf'\s*hsla?\(\s*{_r_h}\s+{_r_sl}\s+{_r_sl}(?:\s*/\s*{_r_alpha})?\s*\)\s*' + +# colors where the two hex characters are the same, if all colors match this the short version of hex colors can be used +repeat_colors = {int(c * 2, 16) for c in '0123456789abcdef'} +rads = 2 * math.pi + + +@deprecated( + 'The `Color` class is deprecated, use `pydantic_extra_types` instead. ' + 'See https://docs.pydantic.dev/latest/api/pydantic_extra_types_color/.', + category=PydanticDeprecatedSince20, +) +class Color(_repr.Representation): + """Represents a color.""" + + __slots__ = '_original', '_rgba' + + def __init__(self, value: ColorType) -> None: + self._rgba: RGBA + self._original: ColorType + if isinstance(value, (tuple, list)): + self._rgba = parse_tuple(value) + elif isinstance(value, str): + self._rgba = parse_str(value) + elif isinstance(value, Color): + self._rgba = value._rgba + value = value._original + else: + raise PydanticCustomError( + 'color_error', 'value is not a valid color: value must be a tuple, list or string' + ) + + # if we've got here value must be a valid color + self._original = value + + @classmethod + def __get_pydantic_json_schema__( + cls, core_schema: core_schema.CoreSchema, handler: _GetJsonSchemaHandler + ) -> JsonSchemaValue: + field_schema = {} + field_schema.update(type='string', format='color') + return field_schema + + def original(self) -> ColorType: + """Original value passed to `Color`.""" + return self._original + + def as_named(self, *, fallback: bool = False) -> str: + """Returns the name of the color if it can be found in `COLORS_BY_VALUE` dictionary, + otherwise returns the hexadecimal representation of the color or raises `ValueError`. + + Args: + fallback: If True, falls back to returning the hexadecimal representation of + the color instead of raising a ValueError when no named color is found. + + Returns: + The name of the color, or the hexadecimal representation of the color. + + Raises: + ValueError: When no named color is found and fallback is `False`. + """ + if self._rgba.alpha is None: + rgb = cast(tuple[int, int, int], self.as_rgb_tuple()) + try: + return COLORS_BY_VALUE[rgb] + except KeyError as e: + if fallback: + return self.as_hex() + else: + raise ValueError('no named color found, use fallback=True, as_hex() or as_rgb()') from e + else: + return self.as_hex() + + def as_hex(self) -> str: + """Returns the hexadecimal representation of the color. + + Hex string representing the color can be 3, 4, 6, or 8 characters depending on whether the string + a "short" representation of the color is possible and whether there's an alpha channel. + + Returns: + The hexadecimal representation of the color. + """ + values = [float_to_255(c) for c in self._rgba[:3]] + if self._rgba.alpha is not None: + values.append(float_to_255(self._rgba.alpha)) + + as_hex = ''.join(f'{v:02x}' for v in values) + if all(c in repeat_colors for c in values): + as_hex = ''.join(as_hex[c] for c in range(0, len(as_hex), 2)) + return '#' + as_hex + + def as_rgb(self) -> str: + """Color as an `rgb(, , )` or `rgba(, , , )` string.""" + if self._rgba.alpha is None: + return f'rgb({float_to_255(self._rgba.r)}, {float_to_255(self._rgba.g)}, {float_to_255(self._rgba.b)})' + else: + return ( + f'rgba({float_to_255(self._rgba.r)}, {float_to_255(self._rgba.g)}, {float_to_255(self._rgba.b)}, ' + f'{round(self._alpha_float(), 2)})' + ) + + def as_rgb_tuple(self, *, alpha: Optional[bool] = None) -> ColorTuple: + """Returns the color as an RGB or RGBA tuple. + + Args: + alpha: Whether to include the alpha channel. There are three options for this input: + + - `None` (default): Include alpha only if it's set. (e.g. not `None`) + - `True`: Always include alpha. + - `False`: Always omit alpha. + + Returns: + A tuple that contains the values of the red, green, and blue channels in the range 0 to 255. + If alpha is included, it is in the range 0 to 1. + """ + r, g, b = (float_to_255(c) for c in self._rgba[:3]) + if alpha is None: + if self._rgba.alpha is None: + return r, g, b + else: + return r, g, b, self._alpha_float() + elif alpha: + return r, g, b, self._alpha_float() + else: + # alpha is False + return r, g, b + + def as_hsl(self) -> str: + """Color as an `hsl(, , )` or `hsl(, , , )` string.""" + if self._rgba.alpha is None: + h, s, li = self.as_hsl_tuple(alpha=False) # type: ignore + return f'hsl({h * 360:0.0f}, {s:0.0%}, {li:0.0%})' + else: + h, s, li, a = self.as_hsl_tuple(alpha=True) # type: ignore + return f'hsl({h * 360:0.0f}, {s:0.0%}, {li:0.0%}, {round(a, 2)})' + + def as_hsl_tuple(self, *, alpha: Optional[bool] = None) -> HslColorTuple: + """Returns the color as an HSL or HSLA tuple. + + Args: + alpha: Whether to include the alpha channel. + + - `None` (default): Include the alpha channel only if it's set (e.g. not `None`). + - `True`: Always include alpha. + - `False`: Always omit alpha. + + Returns: + The color as a tuple of hue, saturation, lightness, and alpha (if included). + All elements are in the range 0 to 1. + + Note: + This is HSL as used in HTML and most other places, not HLS as used in Python's `colorsys`. + """ + h, l, s = rgb_to_hls(self._rgba.r, self._rgba.g, self._rgba.b) # noqa: E741 + if alpha is None: + if self._rgba.alpha is None: + return h, s, l + else: + return h, s, l, self._alpha_float() + if alpha: + return h, s, l, self._alpha_float() + else: + # alpha is False + return h, s, l + + def _alpha_float(self) -> float: + return 1 if self._rgba.alpha is None else self._rgba.alpha + + @classmethod + def __get_pydantic_core_schema__( + cls, source: type[Any], handler: Callable[[Any], CoreSchema] + ) -> core_schema.CoreSchema: + return core_schema.with_info_plain_validator_function( + cls._validate, serialization=core_schema.to_string_ser_schema() + ) + + @classmethod + def _validate(cls, __input_value: Any, _: Any) -> 'Color': + return cls(__input_value) + + def __str__(self) -> str: + return self.as_named(fallback=True) + + def __repr_args__(self) -> '_repr.ReprArgs': + return [(None, self.as_named(fallback=True))] + [('rgb', self.as_rgb_tuple())] + + def __eq__(self, other: Any) -> bool: + return isinstance(other, Color) and self.as_rgb_tuple() == other.as_rgb_tuple() + + def __hash__(self) -> int: + return hash(self.as_rgb_tuple()) + + +def parse_tuple(value: tuple[Any, ...]) -> RGBA: + """Parse a tuple or list to get RGBA values. + + Args: + value: A tuple or list. + + Returns: + An `RGBA` tuple parsed from the input tuple. + + Raises: + PydanticCustomError: If tuple is not valid. + """ + if len(value) == 3: + r, g, b = (parse_color_value(v) for v in value) + return RGBA(r, g, b, None) + elif len(value) == 4: + r, g, b = (parse_color_value(v) for v in value[:3]) + return RGBA(r, g, b, parse_float_alpha(value[3])) + else: + raise PydanticCustomError('color_error', 'value is not a valid color: tuples must have length 3 or 4') + + +def parse_str(value: str) -> RGBA: + """Parse a string representing a color to an RGBA tuple. + + Possible formats for the input string include: + + * named color, see `COLORS_BY_NAME` + * hex short eg. `fff` (prefix can be `#`, `0x` or nothing) + * hex long eg. `ffffff` (prefix can be `#`, `0x` or nothing) + * `rgb(, , )` + * `rgba(, , , )` + + Args: + value: A string representing a color. + + Returns: + An `RGBA` tuple parsed from the input string. + + Raises: + ValueError: If the input string cannot be parsed to an RGBA tuple. + """ + value_lower = value.lower() + try: + r, g, b = COLORS_BY_NAME[value_lower] + except KeyError: + pass + else: + return ints_to_rgba(r, g, b, None) + + m = re.fullmatch(r_hex_short, value_lower) + if m: + *rgb, a = m.groups() + r, g, b = (int(v * 2, 16) for v in rgb) + if a: + alpha: Optional[float] = int(a * 2, 16) / 255 + else: + alpha = None + return ints_to_rgba(r, g, b, alpha) + + m = re.fullmatch(r_hex_long, value_lower) + if m: + *rgb, a = m.groups() + r, g, b = (int(v, 16) for v in rgb) + if a: + alpha = int(a, 16) / 255 + else: + alpha = None + return ints_to_rgba(r, g, b, alpha) + + m = re.fullmatch(r_rgb, value_lower) or re.fullmatch(r_rgb_v4_style, value_lower) + if m: + return ints_to_rgba(*m.groups()) # type: ignore + + m = re.fullmatch(r_hsl, value_lower) or re.fullmatch(r_hsl_v4_style, value_lower) + if m: + return parse_hsl(*m.groups()) # type: ignore + + raise PydanticCustomError('color_error', 'value is not a valid color: string not recognised as a valid color') + + +def ints_to_rgba(r: Union[int, str], g: Union[int, str], b: Union[int, str], alpha: Optional[float] = None) -> RGBA: + """Converts integer or string values for RGB color and an optional alpha value to an `RGBA` object. + + Args: + r: An integer or string representing the red color value. + g: An integer or string representing the green color value. + b: An integer or string representing the blue color value. + alpha: A float representing the alpha value. Defaults to None. + + Returns: + An instance of the `RGBA` class with the corresponding color and alpha values. + """ + return RGBA(parse_color_value(r), parse_color_value(g), parse_color_value(b), parse_float_alpha(alpha)) + + +def parse_color_value(value: Union[int, str], max_val: int = 255) -> float: + """Parse the color value provided and return a number between 0 and 1. + + Args: + value: An integer or string color value. + max_val: Maximum range value. Defaults to 255. + + Raises: + PydanticCustomError: If the value is not a valid color. + + Returns: + A number between 0 and 1. + """ + try: + color = float(value) + except ValueError: + raise PydanticCustomError('color_error', 'value is not a valid color: color values must be a valid number') + if 0 <= color <= max_val: + return color / max_val + else: + raise PydanticCustomError( + 'color_error', + 'value is not a valid color: color values must be in the range 0 to {max_val}', + {'max_val': max_val}, + ) + + +def parse_float_alpha(value: Union[None, str, float, int]) -> Optional[float]: + """Parse an alpha value checking it's a valid float in the range 0 to 1. + + Args: + value: The input value to parse. + + Returns: + The parsed value as a float, or `None` if the value was None or equal 1. + + Raises: + PydanticCustomError: If the input value cannot be successfully parsed as a float in the expected range. + """ + if value is None: + return None + try: + if isinstance(value, str) and value.endswith('%'): + alpha = float(value[:-1]) / 100 + else: + alpha = float(value) + except ValueError: + raise PydanticCustomError('color_error', 'value is not a valid color: alpha values must be a valid float') + + if math.isclose(alpha, 1): + return None + elif 0 <= alpha <= 1: + return alpha + else: + raise PydanticCustomError('color_error', 'value is not a valid color: alpha values must be in the range 0 to 1') + + +def parse_hsl(h: str, h_units: str, sat: str, light: str, alpha: Optional[float] = None) -> RGBA: + """Parse raw hue, saturation, lightness, and alpha values and convert to RGBA. + + Args: + h: The hue value. + h_units: The unit for hue value. + sat: The saturation value. + light: The lightness value. + alpha: Alpha value. + + Returns: + An instance of `RGBA`. + """ + s_value, l_value = parse_color_value(sat, 100), parse_color_value(light, 100) + + h_value = float(h) + if h_units in {None, 'deg'}: + h_value = h_value % 360 / 360 + elif h_units == 'rad': + h_value = h_value % rads / rads + else: + # turns + h_value = h_value % 1 + + r, g, b = hls_to_rgb(h_value, l_value, s_value) + return RGBA(r, g, b, parse_float_alpha(alpha)) + + +def float_to_255(c: float) -> int: + """Converts a float value between 0 and 1 (inclusive) to an integer between 0 and 255 (inclusive). + + Args: + c: The float value to be converted. Must be between 0 and 1 (inclusive). + + Returns: + The integer equivalent of the given float value rounded to the nearest whole number. + + Raises: + ValueError: If the given float value is outside the acceptable range of 0 to 1 (inclusive). + """ + return int(round(c * 255)) + + +COLORS_BY_NAME = { + 'aliceblue': (240, 248, 255), + 'antiquewhite': (250, 235, 215), + 'aqua': (0, 255, 255), + 'aquamarine': (127, 255, 212), + 'azure': (240, 255, 255), + 'beige': (245, 245, 220), + 'bisque': (255, 228, 196), + 'black': (0, 0, 0), + 'blanchedalmond': (255, 235, 205), + 'blue': (0, 0, 255), + 'blueviolet': (138, 43, 226), + 'brown': (165, 42, 42), + 'burlywood': (222, 184, 135), + 'cadetblue': (95, 158, 160), + 'chartreuse': (127, 255, 0), + 'chocolate': (210, 105, 30), + 'coral': (255, 127, 80), + 'cornflowerblue': (100, 149, 237), + 'cornsilk': (255, 248, 220), + 'crimson': (220, 20, 60), + 'cyan': (0, 255, 255), + 'darkblue': (0, 0, 139), + 'darkcyan': (0, 139, 139), + 'darkgoldenrod': (184, 134, 11), + 'darkgray': (169, 169, 169), + 'darkgreen': (0, 100, 0), + 'darkgrey': (169, 169, 169), + 'darkkhaki': (189, 183, 107), + 'darkmagenta': (139, 0, 139), + 'darkolivegreen': (85, 107, 47), + 'darkorange': (255, 140, 0), + 'darkorchid': (153, 50, 204), + 'darkred': (139, 0, 0), + 'darksalmon': (233, 150, 122), + 'darkseagreen': (143, 188, 143), + 'darkslateblue': (72, 61, 139), + 'darkslategray': (47, 79, 79), + 'darkslategrey': (47, 79, 79), + 'darkturquoise': (0, 206, 209), + 'darkviolet': (148, 0, 211), + 'deeppink': (255, 20, 147), + 'deepskyblue': (0, 191, 255), + 'dimgray': (105, 105, 105), + 'dimgrey': (105, 105, 105), + 'dodgerblue': (30, 144, 255), + 'firebrick': (178, 34, 34), + 'floralwhite': (255, 250, 240), + 'forestgreen': (34, 139, 34), + 'fuchsia': (255, 0, 255), + 'gainsboro': (220, 220, 220), + 'ghostwhite': (248, 248, 255), + 'gold': (255, 215, 0), + 'goldenrod': (218, 165, 32), + 'gray': (128, 128, 128), + 'green': (0, 128, 0), + 'greenyellow': (173, 255, 47), + 'grey': (128, 128, 128), + 'honeydew': (240, 255, 240), + 'hotpink': (255, 105, 180), + 'indianred': (205, 92, 92), + 'indigo': (75, 0, 130), + 'ivory': (255, 255, 240), + 'khaki': (240, 230, 140), + 'lavender': (230, 230, 250), + 'lavenderblush': (255, 240, 245), + 'lawngreen': (124, 252, 0), + 'lemonchiffon': (255, 250, 205), + 'lightblue': (173, 216, 230), + 'lightcoral': (240, 128, 128), + 'lightcyan': (224, 255, 255), + 'lightgoldenrodyellow': (250, 250, 210), + 'lightgray': (211, 211, 211), + 'lightgreen': (144, 238, 144), + 'lightgrey': (211, 211, 211), + 'lightpink': (255, 182, 193), + 'lightsalmon': (255, 160, 122), + 'lightseagreen': (32, 178, 170), + 'lightskyblue': (135, 206, 250), + 'lightslategray': (119, 136, 153), + 'lightslategrey': (119, 136, 153), + 'lightsteelblue': (176, 196, 222), + 'lightyellow': (255, 255, 224), + 'lime': (0, 255, 0), + 'limegreen': (50, 205, 50), + 'linen': (250, 240, 230), + 'magenta': (255, 0, 255), + 'maroon': (128, 0, 0), + 'mediumaquamarine': (102, 205, 170), + 'mediumblue': (0, 0, 205), + 'mediumorchid': (186, 85, 211), + 'mediumpurple': (147, 112, 219), + 'mediumseagreen': (60, 179, 113), + 'mediumslateblue': (123, 104, 238), + 'mediumspringgreen': (0, 250, 154), + 'mediumturquoise': (72, 209, 204), + 'mediumvioletred': (199, 21, 133), + 'midnightblue': (25, 25, 112), + 'mintcream': (245, 255, 250), + 'mistyrose': (255, 228, 225), + 'moccasin': (255, 228, 181), + 'navajowhite': (255, 222, 173), + 'navy': (0, 0, 128), + 'oldlace': (253, 245, 230), + 'olive': (128, 128, 0), + 'olivedrab': (107, 142, 35), + 'orange': (255, 165, 0), + 'orangered': (255, 69, 0), + 'orchid': (218, 112, 214), + 'palegoldenrod': (238, 232, 170), + 'palegreen': (152, 251, 152), + 'paleturquoise': (175, 238, 238), + 'palevioletred': (219, 112, 147), + 'papayawhip': (255, 239, 213), + 'peachpuff': (255, 218, 185), + 'peru': (205, 133, 63), + 'pink': (255, 192, 203), + 'plum': (221, 160, 221), + 'powderblue': (176, 224, 230), + 'purple': (128, 0, 128), + 'red': (255, 0, 0), + 'rosybrown': (188, 143, 143), + 'royalblue': (65, 105, 225), + 'saddlebrown': (139, 69, 19), + 'salmon': (250, 128, 114), + 'sandybrown': (244, 164, 96), + 'seagreen': (46, 139, 87), + 'seashell': (255, 245, 238), + 'sienna': (160, 82, 45), + 'silver': (192, 192, 192), + 'skyblue': (135, 206, 235), + 'slateblue': (106, 90, 205), + 'slategray': (112, 128, 144), + 'slategrey': (112, 128, 144), + 'snow': (255, 250, 250), + 'springgreen': (0, 255, 127), + 'steelblue': (70, 130, 180), + 'tan': (210, 180, 140), + 'teal': (0, 128, 128), + 'thistle': (216, 191, 216), + 'tomato': (255, 99, 71), + 'turquoise': (64, 224, 208), + 'violet': (238, 130, 238), + 'wheat': (245, 222, 179), + 'white': (255, 255, 255), + 'whitesmoke': (245, 245, 245), + 'yellow': (255, 255, 0), + 'yellowgreen': (154, 205, 50), +} + +COLORS_BY_VALUE = {v: k for k, v in COLORS_BY_NAME.items()} diff --git a/source/target-vendors/cp312/pydantic/config.py b/source/target-vendors/cp312/pydantic/config.py new file mode 100644 index 0000000..6d89525 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/config.py @@ -0,0 +1,1296 @@ +"""Configuration for Pydantic models.""" + +from __future__ import annotations as _annotations + +import warnings +from re import Pattern +from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, Union, cast, overload + +from typing_extensions import TypeAlias, TypedDict, Unpack, deprecated + +from ._migration import getattr_migration +from .aliases import AliasGenerator +from .errors import PydanticUserError +from .warnings import PydanticDeprecatedSince211 + +if TYPE_CHECKING: + from ._internal._generate_schema import GenerateSchema as _GenerateSchema + from .fields import ComputedFieldInfo, FieldInfo + +__all__ = ('ConfigDict', 'with_config') + + +JsonValue: TypeAlias = Union[int, float, str, bool, None, list['JsonValue'], 'JsonDict'] +JsonDict: TypeAlias = dict[str, JsonValue] + +JsonEncoder = Callable[[Any], Any] + +JsonSchemaExtraCallable: TypeAlias = Union[ + Callable[[JsonDict], None], + Callable[[JsonDict, type[Any]], None], +] + +ExtraValues = Literal['allow', 'ignore', 'forbid'] + + +class ConfigDict(TypedDict, total=False): + """A TypedDict for configuring Pydantic behaviour.""" + + title: str | None + """The title for the generated JSON schema, defaults to the model's name""" + + model_title_generator: Callable[[type], str] | None + """A callable that takes a model class and returns the title for it. Defaults to `None`.""" + + field_title_generator: Callable[[str, FieldInfo | ComputedFieldInfo], str] | None + """A callable that takes a field's name and info and returns title for it. Defaults to `None`.""" + + str_to_lower: bool + """Whether to convert all characters to lowercase for str types. Defaults to `False`.""" + + str_to_upper: bool + """Whether to convert all characters to uppercase for str types. Defaults to `False`.""" + + str_strip_whitespace: bool + """Whether to strip leading and trailing whitespace for str types.""" + + str_min_length: int + """The minimum length for str types. Defaults to `None`.""" + + str_max_length: int | None + """The maximum length for str types. Defaults to `None`.""" + + extra: ExtraValues | None + ''' + Whether to ignore, allow, or forbid extra data during model initialization. Defaults to `'ignore'`. + + Three configuration values are available: + + - `'ignore'`: Providing extra data is ignored (the default): + ```python + from pydantic import BaseModel, ConfigDict + + class User(BaseModel): + model_config = ConfigDict(extra='ignore') # (1)! + + name: str + + user = User(name='John Doe', age=20) # (2)! + print(user) + #> name='John Doe' + ``` + + 1. This is the default behaviour. + 2. The `age` argument is ignored. + + - `'forbid'`: Providing extra data is not permitted, and a [`ValidationError`][pydantic_core.ValidationError] + will be raised if this is the case: + ```python + from pydantic import BaseModel, ConfigDict, ValidationError + + + class Model(BaseModel): + x: int + + model_config = ConfigDict(extra='forbid') + + + try: + Model(x=1, y='a') + except ValidationError as exc: + print(exc) + """ + 1 validation error for Model + y + Extra inputs are not permitted [type=extra_forbidden, input_value='a', input_type=str] + """ + ``` + + - `'allow'`: Providing extra data is allowed and stored in the `__pydantic_extra__` dictionary attribute: + ```python + from pydantic import BaseModel, ConfigDict + + + class Model(BaseModel): + x: int + + model_config = ConfigDict(extra='allow') + + + m = Model(x=1, y='a') + assert m.__pydantic_extra__ == {'y': 'a'} + ``` + By default, no validation will be applied to these extra items, but you can set a type for the values by overriding + the type annotation for `__pydantic_extra__`: + ```python + from pydantic import BaseModel, ConfigDict, Field, ValidationError + + + class Model(BaseModel): + __pydantic_extra__: dict[str, int] = Field(init=False) # (1)! + + x: int + + model_config = ConfigDict(extra='allow') + + + try: + Model(x=1, y='a') + except ValidationError as exc: + print(exc) + """ + 1 validation error for Model + y + Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='a', input_type=str] + """ + + m = Model(x=1, y='2') + assert m.x == 1 + assert m.y == 2 + assert m.model_dump() == {'x': 1, 'y': 2} + assert m.__pydantic_extra__ == {'y': 2} + ``` + + 1. The `= Field(init=False)` does not have any effect at runtime, but prevents the `__pydantic_extra__` field from + being included as a parameter to the model's `__init__` method by type checkers. + + As well as specifying an `extra` configuration value on the model, you can also provide it as an argument to the validation methods. + This will override any `extra` configuration value set on the model: + ```python + from pydantic import BaseModel, ConfigDict, ValidationError + + class Model(BaseModel): + x: int + model_config = ConfigDict(extra="allow") + + try: + # Override model config and forbid extra fields just this time + Model.model_validate({"x": 1, "y": 2}, extra="forbid") + except ValidationError as exc: + print(exc) + """ + 1 validation error for Model + y + Extra inputs are not permitted [type=extra_forbidden, input_value=2, input_type=int] + """ + ``` + ''' + + frozen: bool + """ + Whether models are faux-immutable, i.e. whether `__setattr__` is allowed, and also generates + a `__hash__()` method for the model. This makes instances of the model potentially hashable if all the + attributes are hashable. Defaults to `False`. + + Note: + On V1, the inverse of this setting was called `allow_mutation`, and was `True` by default. + """ + + populate_by_name: bool + """ + Whether an aliased field may be populated by its name as given by the model + attribute, as well as the alias. Defaults to `False`. + + !!! warning + `populate_by_name` usage is not recommended in v2.11+ and will be deprecated in v3. + Instead, you should use the [`validate_by_name`][pydantic.config.ConfigDict.validate_by_name] configuration setting. + + When `validate_by_name=True` and `validate_by_alias=True`, this is strictly equivalent to the + previous behavior of `populate_by_name=True`. + + In v2.11, we also introduced a [`validate_by_alias`][pydantic.config.ConfigDict.validate_by_alias] setting that introduces more fine grained + control for validation behavior. + + Here's how you might go about using the new settings to achieve the same behavior: + + ```python + from pydantic import BaseModel, ConfigDict, Field + + class Model(BaseModel): + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + my_field: str = Field(alias='my_alias') # (1)! + + m = Model(my_alias='foo') # (2)! + print(m) + #> my_field='foo' + + m = Model(my_field='foo') # (3)! + print(m) + #> my_field='foo' + ``` + + 1. The field `'my_field'` has an alias `'my_alias'`. + 2. The model is populated by the alias `'my_alias'`. + 3. The model is populated by the attribute name `'my_field'`. + """ + + use_enum_values: bool + """ + Whether to populate models with the `value` property of enums, rather than the raw enum. + This may be useful if you want to serialize `model.model_dump()` later. Defaults to `False`. + + !!! note + If you have an `Optional[Enum]` value that you set a default for, you need to use `validate_default=True` + for said Field to ensure that the `use_enum_values` flag takes effect on the default, as extracting an + enum's value occurs during validation, not serialization. + + ```python + from enum import Enum + from typing import Optional + + from pydantic import BaseModel, ConfigDict, Field + + class SomeEnum(Enum): + FOO = 'foo' + BAR = 'bar' + BAZ = 'baz' + + class SomeModel(BaseModel): + model_config = ConfigDict(use_enum_values=True) + + some_enum: SomeEnum + another_enum: Optional[SomeEnum] = Field( + default=SomeEnum.FOO, validate_default=True + ) + + model1 = SomeModel(some_enum=SomeEnum.BAR) + print(model1.model_dump()) + #> {'some_enum': 'bar', 'another_enum': 'foo'} + + model2 = SomeModel(some_enum=SomeEnum.BAR, another_enum=SomeEnum.BAZ) + print(model2.model_dump()) + #> {'some_enum': 'bar', 'another_enum': 'baz'} + ``` + """ + + validate_assignment: bool + """ + Whether to validate the data when the model is changed. Defaults to `False`. + + The default behavior of Pydantic is to validate the data when the model is created. + + In case the user changes the data after the model is created, the model is _not_ revalidated. + + ```python + from pydantic import BaseModel + + class User(BaseModel): + name: str + + user = User(name='John Doe') # (1)! + print(user) + #> name='John Doe' + user.name = 123 # (1)! + print(user) + #> name=123 + ``` + + 1. The validation happens only when the model is created. + 2. The validation does not happen when the data is changed. + + In case you want to revalidate the model when the data is changed, you can use `validate_assignment=True`: + + ```python + from pydantic import BaseModel, ValidationError + + class User(BaseModel, validate_assignment=True): # (1)! + name: str + + user = User(name='John Doe') # (2)! + print(user) + #> name='John Doe' + try: + user.name = 123 # (3)! + except ValidationError as e: + print(e) + ''' + 1 validation error for User + name + Input should be a valid string [type=string_type, input_value=123, input_type=int] + ''' + ``` + + 1. You can either use class keyword arguments, or `model_config` to set `validate_assignment=True`. + 2. The validation happens when the model is created. + 3. The validation _also_ happens when the data is changed. + """ + + arbitrary_types_allowed: bool + """ + Whether arbitrary types are allowed for field types. Defaults to `False`. + + ```python + from pydantic import BaseModel, ConfigDict, ValidationError + + # This is not a pydantic model, it's an arbitrary class + class Pet: + def __init__(self, name: str): + self.name = name + + class Model(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + pet: Pet + owner: str + + pet = Pet(name='Hedwig') + # A simple check of instance type is used to validate the data + model = Model(owner='Harry', pet=pet) + print(model) + #> pet=<__main__.Pet object at 0x0123456789ab> owner='Harry' + print(model.pet) + #> <__main__.Pet object at 0x0123456789ab> + print(model.pet.name) + #> Hedwig + print(type(model.pet)) + #> + try: + # If the value is not an instance of the type, it's invalid + Model(owner='Harry', pet='Hedwig') + except ValidationError as e: + print(e) + ''' + 1 validation error for Model + pet + Input should be an instance of Pet [type=is_instance_of, input_value='Hedwig', input_type=str] + ''' + + # Nothing in the instance of the arbitrary type is checked + # Here name probably should have been a str, but it's not validated + pet2 = Pet(name=42) + model2 = Model(owner='Harry', pet=pet2) + print(model2) + #> pet=<__main__.Pet object at 0x0123456789ab> owner='Harry' + print(model2.pet) + #> <__main__.Pet object at 0x0123456789ab> + print(model2.pet.name) + #> 42 + print(type(model2.pet)) + #> + ``` + """ + + from_attributes: bool + """ + Whether to build models and look up discriminators of tagged unions using python object attributes. + """ + + loc_by_alias: bool + """Whether to use the actual key provided in the data (e.g. alias) for error `loc`s rather than the field's name. Defaults to `True`.""" + + alias_generator: Callable[[str], str] | AliasGenerator | None + """ + A callable that takes a field name and returns an alias for it + or an instance of [`AliasGenerator`][pydantic.aliases.AliasGenerator]. Defaults to `None`. + + When using a callable, the alias generator is used for both validation and serialization. + If you want to use different alias generators for validation and serialization, you can use + [`AliasGenerator`][pydantic.aliases.AliasGenerator] instead. + + If data source field names do not match your code style (e.g. CamelCase fields), + you can automatically generate aliases using `alias_generator`. Here's an example with + a basic callable: + + ```python + from pydantic import BaseModel, ConfigDict + from pydantic.alias_generators import to_pascal + + class Voice(BaseModel): + model_config = ConfigDict(alias_generator=to_pascal) + + name: str + language_code: str + + voice = Voice(Name='Filiz', LanguageCode='tr-TR') + print(voice.language_code) + #> tr-TR + print(voice.model_dump(by_alias=True)) + #> {'Name': 'Filiz', 'LanguageCode': 'tr-TR'} + ``` + + If you want to use different alias generators for validation and serialization, you can use + [`AliasGenerator`][pydantic.aliases.AliasGenerator]. + + ```python + from pydantic import AliasGenerator, BaseModel, ConfigDict + from pydantic.alias_generators import to_camel, to_pascal + + class Athlete(BaseModel): + first_name: str + last_name: str + sport: str + + model_config = ConfigDict( + alias_generator=AliasGenerator( + validation_alias=to_camel, + serialization_alias=to_pascal, + ) + ) + + athlete = Athlete(firstName='John', lastName='Doe', sport='track') + print(athlete.model_dump(by_alias=True)) + #> {'FirstName': 'John', 'LastName': 'Doe', 'Sport': 'track'} + ``` + + Note: + Pydantic offers three built-in alias generators: [`to_pascal`][pydantic.alias_generators.to_pascal], + [`to_camel`][pydantic.alias_generators.to_camel], and [`to_snake`][pydantic.alias_generators.to_snake]. + """ + + ignored_types: tuple[type, ...] + """A tuple of types that may occur as values of class attributes without annotations. This is + typically used for custom descriptors (classes that behave like `property`). If an attribute is set on a + class without an annotation and has a type that is not in this tuple (or otherwise recognized by + _pydantic_), an error will be raised. Defaults to `()`. + """ + + allow_inf_nan: bool + """Whether to allow infinity (`+inf` an `-inf`) and NaN values to float and decimal fields. Defaults to `True`.""" + + json_schema_extra: JsonDict | JsonSchemaExtraCallable | None + """A dict or callable to provide extra JSON schema properties. Defaults to `None`.""" + + json_encoders: dict[type[object], JsonEncoder] | None + """ + A `dict` of custom JSON encoders for specific types. Defaults to `None`. + + /// version-deprecated | v2 + This configuration option is a carryover from v1. We originally planned to remove it in v2 but didn't have a 1:1 replacement + so we are keeping it for now. It is still deprecated and will likely be removed in the future. + /// + """ + + # new in V2 + strict: bool + """ + Whether strict validation is applied to all fields on the model. + + By default, Pydantic attempts to coerce values to the correct type, when possible. + + There are situations in which you may want to disable this behavior, and instead raise an error if a value's type + does not match the field's type annotation. + + To configure strict mode for all fields on a model, you can set `strict=True` on the model. + + ```python + from pydantic import BaseModel, ConfigDict + + class Model(BaseModel): + model_config = ConfigDict(strict=True) + + name: str + age: int + ``` + + See [Strict Mode](../concepts/strict_mode.md) for more details. + + See the [Conversion Table](../concepts/conversion_table.md) for more details on how Pydantic converts data in both + strict and lax modes. + + /// version-added | v2 + /// + """ + # whether instances of models and dataclasses (including subclass instances) should re-validate, default 'never' + revalidate_instances: Literal['always', 'never', 'subclass-instances'] + """ + When and how to revalidate models and dataclasses during validation. Can be one of: + + - `'never'`: will *not* revalidate models and dataclasses during validation + - `'always'`: will revalidate models and dataclasses during validation + - `'subclass-instances'`: will revalidate models and dataclasses during validation if the instance is a + subclass of the model or dataclass + + The default is `'never'` (no revalidation). + + This configuration only affects *the current model* it is applied on, and does *not* propagate to the models + referenced in fields. + + ```python + from pydantic import BaseModel + + class User(BaseModel, revalidate_instances='never'): # (1)! + name: str + + class Transaction(BaseModel): + user: User + + my_user = User(name='John') + t = Transaction(user=my_user) + + my_user.name = 1 # (2)! + t = Transaction(user=my_user) # (3)! + print(t) + #> user=User(name=1) + ``` + + 1. This is the default behavior. + 2. The assignment is *not* validated, unless you set [`validate_assignment`][pydantic.ConfigDict.validate_assignment] in the configuration. + 3. Since `revalidate_instances` is set to `'never'`, the user instance is not revalidated. + + Here is an example demonstrating the behavior of `'subclass-instances'`: + + ```python + from pydantic import BaseModel + + class User(BaseModel, revalidate_instances='subclass-instances'): + name: str + + class SubUser(User): + age: int + + class Transaction(BaseModel): + user: User + + my_user = User(name='John') + my_user.name = 1 # (1)! + t = Transaction(user=my_user) # (2)! + print(t) + #> user=User(name=1) + + my_sub_user = SubUser(name='John', age=20) + t = Transaction(user=my_sub_user) + print(t) # (3)! + #> user=User(name='John') + ``` + + 1. The assignment is *not* validated, unless you set [`validate_assignment`][pydantic.ConfigDict.validate_assignment] in the configuration. + 2. Because `my_user` is a "direct" instance of `User`, it is *not* being revalidated. It would have been the case if + `revalidate_instances` was set to `'always'`. + 3. Because `my_sub_user` is an instance of a `User` subclass, it is being revalidated. In this case, Pydantic coerces `my_sub_user` to the defined + `User` class defined on `Transaction`. If one of its fields had an invalid value, a validation error would have been raised. + + /// version-added | v2 + /// + """ + + ser_json_timedelta: Literal['iso8601', 'float'] + """ + The format of JSON serialized timedeltas. Accepts the string values of `'iso8601'` and + `'float'`. Defaults to `'iso8601'`. + + - `'iso8601'` will serialize timedeltas to [ISO 8601 text format](https://en.wikipedia.org/wiki/ISO_8601#Durations). + - `'float'` will serialize timedeltas to the total number of seconds. + + /// version-changed | v2.12 + It is now recommended to use the [`ser_json_temporal`][pydantic.config.ConfigDict.ser_json_temporal] + setting. `ser_json_timedelta` will be deprecated in v3. + /// + """ + + ser_json_temporal: Literal['iso8601', 'seconds', 'milliseconds'] + """ + The format of JSON serialized temporal types from the [`datetime`][] module. This includes: + + - [`datetime.datetime`][] + - [`datetime.date`][] + - [`datetime.time`][] + - [`datetime.timedelta`][] + + Can be one of: + + - `'iso8601'` will serialize date-like types to [ISO 8601 text format](https://en.wikipedia.org/wiki/ISO_8601#Durations). + - `'milliseconds'` will serialize date-like types to a floating point number of milliseconds since the epoch. + - `'seconds'` will serialize date-like types to a floating point number of seconds since the epoch. + + Defaults to `'iso8601'`. + + /// version-added | v2.12 + This setting replaces [`ser_json_timedelta`][pydantic.config.ConfigDict.ser_json_timedelta], + which will be deprecated in v3. `ser_json_temporal` adds more configurability for the other temporal types. + /// + """ + + val_temporal_unit: Literal['seconds', 'milliseconds', 'infer'] + """ + The unit to assume for validating numeric input for datetime-like types ([`datetime.datetime`][] and [`datetime.date`][]). Can be one of: + + - `'seconds'` will validate date or time numeric inputs as seconds since the [epoch]. + - `'milliseconds'` will validate date or time numeric inputs as milliseconds since the [epoch]. + - `'infer'` will infer the unit from the string numeric input on unix time as: + + * seconds since the [epoch] if $-2^{10} <= v <= 2^{10}$ + * milliseconds since the [epoch] (if $v < -2^{10}$ or $v > 2^{10}$). + + Defaults to `'infer'`. + + /// version-added | v2.12 + /// + + [epoch]: https://en.wikipedia.org/wiki/Unix_time + """ + + ser_json_bytes: Literal['utf8', 'base64', 'hex'] + """ + The encoding of JSON serialized bytes. Defaults to `'utf8'`. + Set equal to `val_json_bytes` to get back an equal value after serialization round trip. + + - `'utf8'` will serialize bytes to UTF-8 strings. + - `'base64'` will serialize bytes to URL safe base64 strings. + - `'hex'` will serialize bytes to hexadecimal strings. + """ + + val_json_bytes: Literal['utf8', 'base64', 'hex'] + """ + /// version-added | v2.9 + /// + + The encoding of JSON serialized bytes to decode. Defaults to `'utf8'`. + Set equal to `ser_json_bytes` to get back an equal value after serialization round trip. + + - `'utf8'` will deserialize UTF-8 strings to bytes. + - `'base64'` will deserialize URL safe base64 strings to bytes. + - `'hex'` will deserialize hexadecimal strings to bytes. + """ + + ser_json_inf_nan: Literal['null', 'constants', 'strings'] + """ + The encoding of JSON serialized infinity and NaN float values. Defaults to `'null'`. + + - `'null'` will serialize infinity and NaN values as `null`. + - `'constants'` will serialize infinity and NaN values as `Infinity` and `NaN`. + - `'strings'` will serialize infinity as string `"Infinity"` and NaN as string `"NaN"`. + """ + + # whether to validate default values during validation, default False + validate_default: bool + """Whether to validate default values during validation. Defaults to `False`.""" + + validate_return: bool + """Whether to validate the return value from call validators. Defaults to `False`.""" + + protected_namespaces: tuple[str | Pattern[str], ...] + """ + A tuple of strings and/or regex patterns that prevent models from having fields with names that conflict with its existing members/methods. + + Strings are matched on a prefix basis. For instance, with `'dog'`, having a field named `'dog_name'` will be disallowed. + + Regex patterns are matched on the entire field name. For instance, with the pattern `'^dog$'`, having a field named `'dog'` will be disallowed, + but `'dog_name'` will be accepted. + + Defaults to `('model_validate', 'model_dump')`. This default is used to prevent collisions with the existing (and possibly future) + [validation](../concepts/models.md#validating-data) and [serialization](../concepts/serialization.md#serializing-data) methods. + + ```python + import warnings + + from pydantic import BaseModel + + warnings.filterwarnings('error') # Raise warnings as errors + + try: + + class Model(BaseModel): + model_dump_something: str + + except UserWarning as e: + print(e) + ''' + Field 'model_dump_something' in 'Model' conflicts with protected namespace 'model_dump'. + + You may be able to solve this by setting the 'protected_namespaces' configuration to ('model_validate',). + ''' + ``` + + You can customize this behavior using the `protected_namespaces` setting: + + ```python {test="skip"} + import re + import warnings + + from pydantic import BaseModel, ConfigDict + + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.simplefilter('always') # Catch all warnings + + class Model(BaseModel): + safe_field: str + also_protect_field: str + protect_this: str + + model_config = ConfigDict( + protected_namespaces=( + 'protect_me_', + 'also_protect_', + re.compile('^protect_this$'), + ) + ) + + for warning in caught_warnings: + print(f'{warning.message}') + ''' + Field 'also_protect_field' in 'Model' conflicts with protected namespace 'also_protect_'. + You may be able to solve this by setting the 'protected_namespaces' configuration to ('protect_me_', re.compile('^protect_this$'))`. + + Field 'protect_this' in 'Model' conflicts with protected namespace 're.compile('^protect_this$')'. + You may be able to solve this by setting the 'protected_namespaces' configuration to ('protect_me_', 'also_protect_')`. + ''' + ``` + + While Pydantic will only emit a warning when an item is in a protected namespace but does not actually have a collision, + an error _is_ raised if there is an actual collision with an existing attribute: + + ```python + from pydantic import BaseModel, ConfigDict + + try: + + class Model(BaseModel): + model_validate: str + + model_config = ConfigDict(protected_namespaces=('model_',)) + + except ValueError as e: + print(e) + ''' + Field 'model_validate' conflicts with member > of protected namespace 'model_'. + ''' + ``` + + /// version-changed | v2.10 + The default protected namespaces was changed from `('model_',)` to `('model_validate', 'model_dump')`, to allow + for fields like `model_id`, `model_name` to be used. + /// + """ + + hide_input_in_errors: bool + """ + Whether to hide inputs when printing errors. Defaults to `False`. + + Pydantic shows the input value and type when it raises `ValidationError` during the validation. + + ```python + from pydantic import BaseModel, ValidationError + + class Model(BaseModel): + a: str + + try: + Model(a=123) + except ValidationError as e: + print(e) + ''' + 1 validation error for Model + a + Input should be a valid string [type=string_type, input_value=123, input_type=int] + ''' + ``` + + You can hide the input value and type by setting the `hide_input_in_errors` config to `True`. + + ```python + from pydantic import BaseModel, ConfigDict, ValidationError + + class Model(BaseModel): + a: str + model_config = ConfigDict(hide_input_in_errors=True) + + try: + Model(a=123) + except ValidationError as e: + print(e) + ''' + 1 validation error for Model + a + Input should be a valid string [type=string_type] + ''' + ``` + """ + + defer_build: bool + """ + Whether to defer model validator and serializer construction until the first model validation. Defaults to False. + + This can be useful to avoid the overhead of building models which are only + used nested within other models, or when you want to manually define type namespace via + [`Model.model_rebuild(_types_namespace=...)`][pydantic.BaseModel.model_rebuild]. + + /// version-changed | v2.10 + The setting also applies to [Pydantic dataclasses](../concepts/dataclasses.md) and [type adapters](../concepts/type_adapter.md). + /// + """ + + plugin_settings: dict[str, object] | None + """A `dict` of settings for plugins. Defaults to `None`.""" + + schema_generator: type[_GenerateSchema] | None + """ + The `GenerateSchema` class to use during core schema generation. + + /// version-deprecated | v2.10 + The `GenerateSchema` class is private and highly subject to change. + /// + """ + + json_schema_serialization_defaults_required: bool + """ + Whether fields with default values should be marked as required in the serialization schema. Defaults to `False`. + + This ensures that the serialization schema will reflect the fact a field with a default will always be present + when serializing the model, even though it is not required for validation. + + However, there are scenarios where this may be undesirable — in particular, if you want to share the schema + between validation and serialization, and don't mind fields with defaults being marked as not required during + serialization. See [#7209](https://github.com/pydantic/pydantic/issues/7209) for more details. + + ```python + from pydantic import BaseModel, ConfigDict + + class Model(BaseModel): + a: str = 'a' + + model_config = ConfigDict(json_schema_serialization_defaults_required=True) + + print(Model.model_json_schema(mode='validation')) + ''' + { + 'properties': {'a': {'default': 'a', 'title': 'A', 'type': 'string'}}, + 'title': 'Model', + 'type': 'object', + } + ''' + print(Model.model_json_schema(mode='serialization')) + ''' + { + 'properties': {'a': {'default': 'a', 'title': 'A', 'type': 'string'}}, + 'required': ['a'], + 'title': 'Model', + 'type': 'object', + } + ''' + ``` + + /// version-added | v2.4 + /// + """ + + json_schema_mode_override: Literal['validation', 'serialization', None] + """ + If not `None`, the specified mode will be used to generate the JSON schema regardless of what `mode` was passed to + the function call. Defaults to `None`. + + This provides a way to force the JSON schema generation to reflect a specific mode, e.g., to always use the + validation schema. + + It can be useful when using frameworks (such as FastAPI) that may generate different schemas for validation + and serialization that must both be referenced from the same schema; when this happens, we automatically append + `-Input` to the definition reference for the validation schema and `-Output` to the definition reference for the + serialization schema. By specifying a `json_schema_mode_override` though, this prevents the conflict between + the validation and serialization schemas (since both will use the specified schema), and so prevents the suffixes + from being added to the definition references. + + ```python + from pydantic import BaseModel, ConfigDict, Json + + class Model(BaseModel): + a: Json[int] # requires a string to validate, but will dump an int + + print(Model.model_json_schema(mode='serialization')) + ''' + { + 'properties': {'a': {'title': 'A', 'type': 'integer'}}, + 'required': ['a'], + 'title': 'Model', + 'type': 'object', + } + ''' + + class ForceInputModel(Model): + # the following ensures that even with mode='serialization', we + # will get the schema that would be generated for validation. + model_config = ConfigDict(json_schema_mode_override='validation') + + print(ForceInputModel.model_json_schema(mode='serialization')) + ''' + { + 'properties': { + 'a': { + 'contentMediaType': 'application/json', + 'contentSchema': {'type': 'integer'}, + 'title': 'A', + 'type': 'string', + } + }, + 'required': ['a'], + 'title': 'ForceInputModel', + 'type': 'object', + } + ''' + ``` + + /// version-added | v2.4 + /// + """ + + coerce_numbers_to_str: bool + """ + If `True`, enables automatic coercion of any `Number` type to `str` in "lax" (non-strict) mode. Defaults to `False`. + + Pydantic doesn't allow number types (`int`, `float`, `Decimal`) to be coerced as type `str` by default. + + ```python + from decimal import Decimal + + from pydantic import BaseModel, ConfigDict, ValidationError + + class Model(BaseModel): + value: str + + try: + print(Model(value=42)) + except ValidationError as e: + print(e) + ''' + 1 validation error for Model + value + Input should be a valid string [type=string_type, input_value=42, input_type=int] + ''' + + class Model(BaseModel): + model_config = ConfigDict(coerce_numbers_to_str=True) + + value: str + + repr(Model(value=42).value) + #> "42" + repr(Model(value=42.13).value) + #> "42.13" + repr(Model(value=Decimal('42.13')).value) + #> "42.13" + ``` + """ + + regex_engine: Literal['rust-regex', 'python-re'] + """ + The regex engine to be used for pattern validation. + Defaults to `'rust-regex'`. + + - `'rust-regex'` uses the [`regex`](https://docs.rs/regex) Rust crate, + which is non-backtracking and therefore more DDoS resistant, but does not support all regex features. + - `'python-re'` use the [`re`][] module, which supports all regex features, but may be slower. + + !!! note + If you use a compiled regex pattern, the `'python-re'` engine will be used regardless of this setting. + This is so that flags such as [`re.IGNORECASE`][] are respected. + + ```python + from pydantic import BaseModel, ConfigDict, Field, ValidationError + + class Model(BaseModel): + model_config = ConfigDict(regex_engine='python-re') + + value: str = Field(pattern=r'^abc(?=def)') + + print(Model(value='abcdef').value) + #> abcdef + + try: + print(Model(value='abxyzcdef')) + except ValidationError as e: + print(e) + ''' + 1 validation error for Model + value + String should match pattern '^abc(?=def)' [type=string_pattern_mismatch, input_value='abxyzcdef', input_type=str] + ''' + ``` + + /// version-added | v2.5 + /// + """ + + validation_error_cause: bool + """ + If `True`, Python exceptions that were part of a validation failure will be shown as an exception group as a cause. Can be useful for debugging. Defaults to `False`. + + Note: + Python 3.10 and older don't support exception groups natively. <=3.10, backport must be installed: `pip install exceptiongroup`. + + Note: + The structure of validation errors are likely to change in future Pydantic versions. Pydantic offers no guarantees about their structure. Should be used for visual traceback debugging only. + + /// version-added | v2.5 + /// + """ + + use_attribute_docstrings: bool + ''' + Whether docstrings of attributes (bare string literals immediately following the attribute declaration) + should be used for field descriptions. Defaults to `False`. + + ```python + from pydantic import BaseModel, ConfigDict, Field + + + class Model(BaseModel): + model_config = ConfigDict(use_attribute_docstrings=True) + + x: str + """ + Example of an attribute docstring + """ + + y: int = Field(description="Description in Field") + """ + Description in Field overrides attribute docstring + """ + + + print(Model.model_fields["x"].description) + # > Example of an attribute docstring + print(Model.model_fields["y"].description) + # > Description in Field + ``` + This requires the source code of the class to be available at runtime (and so won't work in the interactive interpreter shell). + + !!! warning "Usage with `TypedDict` and stdlib dataclasses" + Due to current limitations, attribute docstrings detection may not work as expected when using + [`TypedDict`][typing.TypedDict] and stdlib dataclasses, in particular when: + + - inheritance is being used. + - multiple classes have the same name in the same source file (unless Python 3.13 or greater is used). + + /// version-added | v2.7 + /// + ''' + + cache_strings: bool | Literal['all', 'keys', 'none'] + """ + Whether to cache strings to avoid constructing new Python objects. Defaults to True. + + Enabling this setting should significantly improve validation performance while increasing memory usage slightly. + + - `True` or `'all'` (the default): cache all strings + - `'keys'`: cache only dictionary keys + - `False` or `'none'`: no caching + + !!! note + `True` or `'all'` is required to cache strings during general validation because + validators don't know if they're in a key or a value. + + !!! tip + If repeated strings are rare, it's recommended to use `'keys'` or `'none'` to reduce memory usage, + as the performance difference is minimal if repeated strings are rare. + + /// version-added | v2.7 + /// + """ + + validate_by_alias: bool + """ + Whether an aliased field may be populated by its alias. Defaults to `True`. + + Here's an example of disabling validation by alias: + + ```py + from pydantic import BaseModel, ConfigDict, Field + + class Model(BaseModel): + model_config = ConfigDict(validate_by_name=True, validate_by_alias=False) + + my_field: str = Field(validation_alias='my_alias') # (1)! + + m = Model(my_field='foo') # (2)! + print(m) + #> my_field='foo' + ``` + + 1. The field `'my_field'` has an alias `'my_alias'`. + 2. The model can only be populated by the attribute name `'my_field'`. + + !!! warning + You cannot set both `validate_by_alias` and `validate_by_name` to `False`. + This would make it impossible to populate an attribute. + + See [usage errors](../errors/usage_errors.md#validate-by-alias-and-name-false) for an example. + + If you set `validate_by_alias` to `False`, under the hood, Pydantic dynamically sets + `validate_by_name` to `True` to ensure that validation can still occur. + + /// version-added | v2.11 + This setting was introduced in conjunction with [`validate_by_name`][pydantic.ConfigDict.validate_by_name] + to empower users with more fine grained validation control. + /// + """ + + validate_by_name: bool + """ + Whether an aliased field may be populated by its name as given by the model + attribute. Defaults to `False`. + + ```python + from pydantic import BaseModel, ConfigDict, Field + + class Model(BaseModel): + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + my_field: str = Field(validation_alias='my_alias') # (1)! + + m = Model(my_alias='foo') # (2)! + print(m) + #> my_field='foo' + + m = Model(my_field='foo') # (3)! + print(m) + #> my_field='foo' + ``` + + 1. The field `'my_field'` has an alias `'my_alias'`. + 2. The model is populated by the alias `'my_alias'`. + 3. The model is populated by the attribute name `'my_field'`. + + !!! warning + You cannot set both `validate_by_alias` and `validate_by_name` to `False`. + This would make it impossible to populate an attribute. + + See [usage errors](../errors/usage_errors.md#validate-by-alias-and-name-false) for an example. + + /// version-added | v2.11 + This setting was introduced in conjunction with [`validate_by_alias`][pydantic.ConfigDict.validate_by_alias] + to empower users with more fine grained validation control. It is an alternative to [`populate_by_name`][pydantic.ConfigDict.populate_by_name], + that enables validation by name **and** by alias. + /// + """ + + serialize_by_alias: bool + """ + Whether an aliased field should be serialized by its alias. Defaults to `False`. + + Note: In v2.11, `serialize_by_alias` was introduced to address the + [popular request](https://github.com/pydantic/pydantic/issues/8379) + for consistency with alias behavior for validation and serialization settings. + In v3, the default value is expected to change to `True` for consistency with the validation default. + + ```python + from pydantic import BaseModel, ConfigDict, Field + + class Model(BaseModel): + model_config = ConfigDict(serialize_by_alias=True) + + my_field: str = Field(serialization_alias='my_alias') # (1)! + + m = Model(my_field='foo') + print(m.model_dump()) # (2)! + #> {'my_alias': 'foo'} + ``` + + 1. The field `'my_field'` has an alias `'my_alias'`. + 2. The model is serialized using the alias `'my_alias'` for the `'my_field'` attribute. + + + /// version-added | v2.11 + This setting was introduced to address the [popular request](https://github.com/pydantic/pydantic/issues/8379) + for consistency with alias behavior for validation and serialization. + + In v3, the default value is expected to change to `True` for consistency with the validation default. + /// + """ + + url_preserve_empty_path: bool + """ + Whether to preserve empty URL paths when validating values for a URL type. Defaults to `False`. + + ```python + from pydantic import AnyUrl, BaseModel, ConfigDict + + class Model(BaseModel): + model_config = ConfigDict(url_preserve_empty_path=True) + + url: AnyUrl + + m = Model(url='http://example.com') + print(m.url) + #> http://example.com + ``` + + /// version-added | v2.12 + /// + """ + + polymorphic_serialization: bool + """ + Whether to use polymorphic serialization for subclasses of the model or Pydantic dataclass. Defaults to `False`. + """ + + +_TypeT = TypeVar('_TypeT', bound=type) + + +@overload +@deprecated('Passing `config` as a keyword argument is deprecated. Pass `config` as a positional argument instead.') +def with_config(*, config: ConfigDict) -> Callable[[_TypeT], _TypeT]: ... + + +@overload +def with_config(config: ConfigDict, /) -> Callable[[_TypeT], _TypeT]: ... + + +@overload +def with_config(**config: Unpack[ConfigDict]) -> Callable[[_TypeT], _TypeT]: ... + + +def with_config(config: ConfigDict | None = None, /, **kwargs: Any) -> Callable[[_TypeT], _TypeT]: + """!!! abstract "Usage Documentation" + [Configuration with other types](../concepts/config.md#configuration-on-other-supported-types) + + A convenience decorator to set a [Pydantic configuration](config.md) on a `TypedDict` or a `dataclass` from the standard library. + + Although the configuration can be set using the `__pydantic_config__` attribute, it does not play well with type checkers, + especially with `TypedDict`. + + !!! example "Usage" + + ```python + from typing_extensions import TypedDict + + from pydantic import ConfigDict, TypeAdapter, with_config + + @with_config(ConfigDict(str_to_lower=True)) + class TD(TypedDict): + x: str + + ta = TypeAdapter(TD) + + print(ta.validate_python({'x': 'ABC'})) + #> {'x': 'abc'} + ``` + + /// deprecated-removed | v2.11 v3 + Passing `config` as a keyword argument. + /// + + /// version-changed | v2.11 + Keyword arguments can be provided directly instead of a config dictionary. + /// + """ + if config is not None and kwargs: + raise ValueError('Cannot specify both `config` and keyword arguments') + + if len(kwargs) == 1 and (kwargs_conf := kwargs.get('config')) is not None: + warnings.warn( + 'Passing `config` as a keyword argument is deprecated. Pass `config` as a positional argument instead', + category=PydanticDeprecatedSince211, + stacklevel=2, + ) + final_config = cast(ConfigDict, kwargs_conf) + else: + final_config = config if config is not None else cast(ConfigDict, kwargs) + + def inner(class_: _TypeT, /) -> _TypeT: + # Ideally, we would check for `class_` to either be a `TypedDict` or a stdlib dataclass. + # However, the `@with_config` decorator can be applied *after* `@dataclass`. To avoid + # common mistakes, we at least check for `class_` to not be a Pydantic model. + from ._internal._utils import is_model_class + + if is_model_class(class_): + raise PydanticUserError( + f'Cannot use `with_config` on {class_.__name__} as it is a Pydantic model', + code='with-config-on-model', + ) + class_.__pydantic_config__ = final_config + return class_ + + return inner + + +__getattr__ = getattr_migration(__name__) diff --git a/source/target-vendors/cp312/pydantic/dataclasses.py b/source/target-vendors/cp312/pydantic/dataclasses.py new file mode 100644 index 0000000..46401c3 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/dataclasses.py @@ -0,0 +1,413 @@ +"""Provide an enhanced dataclass that performs validation.""" + +from __future__ import annotations as _annotations + +import dataclasses +import functools +import sys +import types +from typing import TYPE_CHECKING, Any, Callable, Generic, Literal, NoReturn, TypeVar, overload +from warnings import warn + +from typing_extensions import TypeGuard, dataclass_transform + +from ._internal import _config, _decorators, _mock_val_ser, _namespace_utils, _typing_extra +from ._internal import _dataclasses as _pydantic_dataclasses +from ._migration import getattr_migration +from .config import ConfigDict +from .errors import PydanticUserError +from .fields import Field, FieldInfo, PrivateAttr + +if TYPE_CHECKING: + from ._internal._dataclasses import PydanticDataclass + from ._internal._namespace_utils import MappingNamespace + +__all__ = 'dataclass', 'rebuild_dataclass' + +_T = TypeVar('_T') + +if sys.version_info >= (3, 10): + + @dataclass_transform(field_specifiers=(dataclasses.field, Field, PrivateAttr)) + @overload + def dataclass( + *, + init: Literal[False] = False, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + config: ConfigDict | type[object] | None = None, + validate_on_init: bool | None = None, + kw_only: bool = ..., + slots: bool = ..., + ) -> Callable[[type[_T]], type[PydanticDataclass]]: # type: ignore + ... + + @dataclass_transform(field_specifiers=(dataclasses.field, Field, PrivateAttr)) + @overload + def dataclass( + _cls: type[_T], # type: ignore + *, + init: Literal[False] = False, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool | None = None, + config: ConfigDict | type[object] | None = None, + validate_on_init: bool | None = None, + kw_only: bool = ..., + slots: bool = ..., + ) -> type[PydanticDataclass]: ... + +else: + + @dataclass_transform(field_specifiers=(dataclasses.field, Field, PrivateAttr)) + @overload + def dataclass( + *, + init: Literal[False] = False, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool | None = None, + config: ConfigDict | type[object] | None = None, + validate_on_init: bool | None = None, + ) -> Callable[[type[_T]], type[PydanticDataclass]]: # type: ignore + ... + + @dataclass_transform(field_specifiers=(dataclasses.field, Field, PrivateAttr)) + @overload + def dataclass( + _cls: type[_T], # type: ignore + *, + init: Literal[False] = False, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool | None = None, + config: ConfigDict | type[object] | None = None, + validate_on_init: bool | None = None, + ) -> type[PydanticDataclass]: ... + + +@dataclass_transform(field_specifiers=(dataclasses.field, Field, PrivateAttr)) +def dataclass( + _cls: type[_T] | None = None, + *, + init: Literal[False] = False, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool | None = None, + config: ConfigDict | type[object] | None = None, + validate_on_init: bool | None = None, + kw_only: bool = False, + slots: bool = False, +) -> Callable[[type[_T]], type[PydanticDataclass]] | type[PydanticDataclass]: + """!!! abstract "Usage Documentation" + [`dataclasses`](../concepts/dataclasses.md) + + A decorator used to create a Pydantic-enhanced dataclass, similar to the standard Python `dataclass`, + but with added validation. + + This function should be used similarly to `dataclasses.dataclass`. + + Args: + _cls: The target `dataclass`. + init: Included for signature compatibility with `dataclasses.dataclass`, and is passed through to + `dataclasses.dataclass` when appropriate. If specified, must be set to `False`, as pydantic inserts its + own `__init__` function. + repr: A boolean indicating whether to include the field in the `__repr__` output. + eq: Determines if a `__eq__` method should be generated for the class. + order: Determines if comparison magic methods should be generated, such as `__lt__`, but not `__eq__`. + unsafe_hash: Determines if a `__hash__` method should be included in the class, as in `dataclasses.dataclass`. + frozen: Determines if the generated class should be a 'frozen' `dataclass`, which does not allow its + attributes to be modified after it has been initialized. If not set, the value from the provided `config` argument will be used (and will default to `False` otherwise). + config: The Pydantic config to use for the `dataclass`. + validate_on_init: A deprecated parameter included for backwards compatibility; in V2, all Pydantic dataclasses + are validated on init. + kw_only: Determines if `__init__` method parameters must be specified by keyword only. Defaults to `False`. + slots: Determines if the generated class should be a 'slots' `dataclass`, which does not allow the addition of + new attributes after instantiation. + + Returns: + A decorator that accepts a class as its argument and returns a Pydantic `dataclass`. + + Raises: + AssertionError: Raised if `init` is not `False` or `validate_on_init` is `False`. + """ + assert init is False, 'pydantic.dataclasses.dataclass only supports init=False' + assert validate_on_init is not False, 'validate_on_init=False is no longer supported' + + if sys.version_info >= (3, 10): + kwargs = {'kw_only': kw_only, 'slots': slots} + else: + kwargs = {} + + def create_dataclass(cls: type[Any]) -> type[PydanticDataclass]: + """Create a Pydantic dataclass from a regular dataclass. + + Args: + cls: The class to create the Pydantic dataclass from. + + Returns: + A Pydantic dataclass. + """ + from ._internal._utils import is_model_class + + if is_model_class(cls): + raise PydanticUserError( + f'Cannot create a Pydantic dataclass from {cls.__name__} as it is already a Pydantic model', + code='dataclass-on-model', + ) + + original_cls = cls + + # we warn on conflicting config specifications, but only if the class doesn't have a dataclass base + # because a dataclass base might provide a __pydantic_config__ attribute that we don't want to warn about + has_dataclass_base = any(dataclasses.is_dataclass(base) for base in cls.__bases__) + if not has_dataclass_base and config is not None and hasattr(cls, '__pydantic_config__'): + warn( + f'`config` is set via both the `dataclass` decorator and `__pydantic_config__` for dataclass {cls.__name__}. ' + f'The `config` specification from `dataclass` decorator will take priority.', + category=UserWarning, + stacklevel=2, + ) + + # if config is not explicitly provided, try to read it from the type + config_dict = config if config is not None else getattr(cls, '__pydantic_config__', None) + config_wrapper = _config.ConfigWrapper(config_dict) + decorators = _decorators.DecoratorInfos.build(cls, replace_wrapped_methods=True) + decorators.update_from_config(config_wrapper) + + # Keep track of the original __doc__ so that we can restore it after applying the dataclasses decorator + # Otherwise, classes with no __doc__ will have their signature added into the JSON schema description, + # since dataclasses.dataclass will set this as the __doc__ + original_doc = cls.__doc__ + + if _pydantic_dataclasses.is_stdlib_dataclass(cls): + # Vanilla dataclasses include a default docstring (representing the class signature), + # which we don't want to preserve. + original_doc = None + + # We don't want to add validation to the existing std lib dataclass, so we will subclass it + # If the class is generic, we need to make sure the subclass also inherits from Generic + # with all the same parameters. + bases = (cls,) + if issubclass(cls, Generic): + generic_base = Generic[cls.__parameters__] # type: ignore + bases = bases + (generic_base,) + cls = types.new_class(cls.__name__, bases) + + # Respect frozen setting from dataclass constructor and fallback to config setting if not provided + if frozen is not None: + frozen_ = frozen + if config_wrapper.frozen: + # It's not recommended to define both, as the setting from the dataclass decorator will take priority. + warn( + f'`frozen` is set via both the `dataclass` decorator and `config` for dataclass {cls.__name__!r}.' + 'This is not recommended. The `frozen` specification on `dataclass` will take priority.', + category=UserWarning, + stacklevel=2, + ) + else: + frozen_ = config_wrapper.frozen or False + + # Make Pydantic's `Field()` function compatible with stdlib dataclasses. As we'll decorate + # `cls` with the stdlib `@dataclass` decorator first, there are two attributes, `kw_only` and + # `repr` that need to be understood *during* the stdlib creation. We do so in two steps: + + # 1. On the decorated class, wrap `Field()` assignment with `dataclass.field()`, with the + # two attributes set (done in `as_dataclass_field()`) + cls_anns = _typing_extra.safe_get_annotations(cls) + for field_name in cls_anns: + # We should look for assignments in `__dict__` instead, but for now we follow + # the same behavior as stdlib dataclasses (see https://github.com/python/cpython/issues/88609) + field_value = getattr(cls, field_name, None) + if isinstance(field_value, FieldInfo): + setattr(cls, field_name, _pydantic_dataclasses.as_dataclass_field(field_value)) + + # 2. For bases of `cls` that are stdlib dataclasses, we temporarily patch their fields + # (see the docstring of the context manager): + with _pydantic_dataclasses.patch_base_fields(cls): + cls = dataclasses.dataclass( # pyright: ignore[reportCallIssue] + cls, + # the value of init here doesn't affect anything except that it makes it easier to generate a signature + init=True, + repr=repr, + eq=eq, + order=order, + unsafe_hash=unsafe_hash, + frozen=frozen_, + **kwargs, + ) + + if config_wrapper.validate_assignment: + original_setattr = cls.__setattr__ + + @functools.wraps(cls.__setattr__) + def validated_setattr(instance: PydanticDataclass, name: str, value: Any, /) -> None: + if frozen_: + return original_setattr(instance, name, value) # pyright: ignore[reportCallIssue] + inst_cls = type(instance) + attr = getattr(inst_cls, name, None) + + if isinstance(attr, property): + attr.__set__(instance, value) + elif isinstance(attr, functools.cached_property): + instance.__dict__.__setitem__(name, value) + else: + inst_cls.__pydantic_validator__.validate_assignment(instance, name, value) + + cls.__setattr__ = validated_setattr.__get__(None, cls) # type: ignore + + if slots and not hasattr(cls, '__setstate__'): + # If slots is set, `pickle` (relied on by `copy.copy()`) will use + # `__setattr__()` to reconstruct the dataclass. However, the custom + # `__setattr__()` set above relies on `validate_assignment()`, which + # in turn expects all the field values to be already present on the + # instance, resulting in attribute errors. + # As such, we make use of `object.__setattr__()` instead. + # Note that we do so only if `__setstate__()` isn't already set (this is the + # case if on top of `slots`, `frozen` is used). + + # Taken from `dataclasses._dataclass_get/setstate()`: + def _dataclass_getstate(self: Any) -> list[Any]: + return [getattr(self, f.name) for f in dataclasses.fields(self)] + + def _dataclass_setstate(self: Any, state: list[Any]) -> None: + for field, value in zip(dataclasses.fields(self), state): + object.__setattr__(self, field.name, value) + + cls.__getstate__ = _dataclass_getstate # pyright: ignore[reportAttributeAccessIssue] + cls.__setstate__ = _dataclass_setstate # pyright: ignore[reportAttributeAccessIssue] + + # This is an undocumented attribute to distinguish stdlib/Pydantic dataclasses. + # It should be set as early as possible: + cls.__is_pydantic_dataclass__ = True + cls.__pydantic_decorators__ = decorators # type: ignore + cls.__doc__ = original_doc + # Can be non-existent for dynamically created classes: + firstlineno = getattr(original_cls, '__firstlineno__', None) + cls.__module__ = original_cls.__module__ + if sys.version_info >= (3, 13) and firstlineno is not None: + # As per https://docs.python.org/3/reference/datamodel.html#type.__firstlineno__: + # Setting the `__module__` attribute removes the `__firstlineno__` item from the type’s dictionary. + original_cls.__firstlineno__ = firstlineno + cls.__firstlineno__ = firstlineno + cls.__qualname__ = original_cls.__qualname__ + cls.__pydantic_fields_complete__ = classmethod(_pydantic_fields_complete) + cls.__pydantic_complete__ = False # `complete_dataclass` will set it to `True` if successful. + # TODO `parent_namespace` is currently None, but we could do the same thing as Pydantic models: + # fetch the parent ns using `parent_frame_namespace` (if the dataclass was defined in a function), + # and possibly cache it (see the `__pydantic_parent_namespace__` logic for models). + _pydantic_dataclasses.complete_dataclass(cls, config_wrapper, raise_errors=False) + return cls + + return create_dataclass if _cls is None else create_dataclass(_cls) + + +def _pydantic_fields_complete(cls: type[PydanticDataclass]) -> bool: + """Return whether the fields were successfully collected (i.e. type hints were successfully resolved). + + This is a private helper, not meant to be used outside Pydantic. + """ + return all(field_info._complete for field_info in cls.__pydantic_fields__.values()) + + +__getattr__ = getattr_migration(__name__) + +if sys.version_info < (3, 11): + # Monkeypatch dataclasses.InitVar so that typing doesn't error if it occurs as a type when evaluating type hints + # Starting in 3.11, typing.get_type_hints will not raise an error if the retrieved type hints are not callable. + + def _call_initvar(*args: Any, **kwargs: Any) -> NoReturn: + """This function does nothing but raise an error that is as similar as possible to what you'd get + if you were to try calling `InitVar[int]()` without this monkeypatch. The whole purpose is just + to ensure typing._type_check does not error if the type hint evaluates to `InitVar[]`. + """ + raise TypeError("'InitVar' object is not callable") + + dataclasses.InitVar.__call__ = _call_initvar + + +def rebuild_dataclass( + cls: type[PydanticDataclass], + *, + force: bool = False, + raise_errors: bool = True, + _parent_namespace_depth: int = 2, + _types_namespace: MappingNamespace | None = None, +) -> bool | None: + """Try to rebuild the pydantic-core schema for the dataclass. + + This may be necessary when one of the annotations is a ForwardRef which could not be resolved during + the initial attempt to build the schema, and automatic rebuilding fails. + + This is analogous to `BaseModel.model_rebuild`. + + Args: + cls: The class to rebuild the pydantic-core schema for. + force: Whether to force the rebuilding of the schema, defaults to `False`. + raise_errors: Whether to raise errors, defaults to `True`. + _parent_namespace_depth: The depth level of the parent namespace, defaults to 2. + _types_namespace: The types namespace, defaults to `None`. + + Returns: + Returns `None` if the schema is already "complete" and rebuilding was not required. + If rebuilding _was_ required, returns `True` if rebuilding was successful, otherwise `False`. + """ + if not force and cls.__pydantic_complete__: + return None + + for attr in ('__pydantic_core_schema__', '__pydantic_validator__', '__pydantic_serializer__'): + if attr in cls.__dict__ and not isinstance(getattr(cls, attr), _mock_val_ser.MockValSer): + # Deleting the validator/serializer is necessary as otherwise they can get reused in + # pydantic-core. Same applies for the core schema that can be reused in schema generation. + delattr(cls, attr) + + cls.__pydantic_complete__ = False + + if _types_namespace is not None: + rebuild_ns = _types_namespace + elif _parent_namespace_depth > 0: + rebuild_ns = _typing_extra.parent_frame_namespace(parent_depth=_parent_namespace_depth, force=True) or {} + else: + rebuild_ns = {} + + ns_resolver = _namespace_utils.NsResolver( + parent_namespace=rebuild_ns, + ) + + return _pydantic_dataclasses.complete_dataclass( + cls, + _config.ConfigWrapper(cls.__pydantic_config__, check=False), + raise_errors=raise_errors, + ns_resolver=ns_resolver, + # We could provide a different config instead (with `'defer_build'` set to `True`) + # of this explicit `_force_build` argument, but because config can come from the + # decorator parameter or the `__pydantic_config__` attribute, `complete_dataclass` + # will overwrite `__pydantic_config__` with the provided config above: + _force_build=True, + ) + + +def is_pydantic_dataclass(class_: type[Any], /) -> TypeGuard[type[PydanticDataclass]]: + """Whether a class is a pydantic dataclass. + + Args: + class_: The class. + + Returns: + `True` if the class is a pydantic dataclass, `False` otherwise. + """ + try: + return '__is_pydantic_dataclass__' in class_.__dict__ and dataclasses.is_dataclass(class_) + except AttributeError: + return False diff --git a/source/target-vendors/cp312/pydantic/datetime_parse.py b/source/target-vendors/cp312/pydantic/datetime_parse.py new file mode 100644 index 0000000..53d5264 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/datetime_parse.py @@ -0,0 +1,5 @@ +"""The `datetime_parse` module is a backport module from V1.""" + +from ._migration import getattr_migration + +__getattr__ = getattr_migration(__name__) diff --git a/source/target-vendors/cp312/pydantic/decorator.py b/source/target-vendors/cp312/pydantic/decorator.py new file mode 100644 index 0000000..0d97560 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/decorator.py @@ -0,0 +1,5 @@ +"""The `decorator` module is a backport module from V1.""" + +from ._migration import getattr_migration + +__getattr__ = getattr_migration(__name__) diff --git a/source/target-vendors/cp312/pydantic/deprecated/__init__.py b/source/target-vendors/cp312/pydantic/deprecated/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/pydantic/deprecated/class_validators.py b/source/target-vendors/cp312/pydantic/deprecated/class_validators.py new file mode 100644 index 0000000..e6ba195 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/deprecated/class_validators.py @@ -0,0 +1,256 @@ +"""Old `@validator` and `@root_validator` function validators from V1.""" + +from __future__ import annotations as _annotations + +from functools import partial, partialmethod +from types import FunctionType +from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, Union, overload +from warnings import warn + +from typing_extensions import Protocol, TypeAlias, deprecated + +from .._internal import _decorators, _decorators_v1 +from ..errors import PydanticUserError +from ..warnings import PydanticDeprecatedSince20 + +_ALLOW_REUSE_WARNING_MESSAGE = '`allow_reuse` is deprecated and will be ignored; it should no longer be necessary' + + +if TYPE_CHECKING: + + class _OnlyValueValidatorClsMethod(Protocol): + def __call__(self, __cls: Any, __value: Any) -> Any: ... + + class _V1ValidatorWithValuesClsMethod(Protocol): + def __call__(self, __cls: Any, __value: Any, values: dict[str, Any]) -> Any: ... + + class _V1ValidatorWithValuesKwOnlyClsMethod(Protocol): + def __call__(self, __cls: Any, __value: Any, *, values: dict[str, Any]) -> Any: ... + + class _V1ValidatorWithKwargsClsMethod(Protocol): + def __call__(self, __cls: Any, **kwargs: Any) -> Any: ... + + class _V1ValidatorWithValuesAndKwargsClsMethod(Protocol): + def __call__(self, __cls: Any, values: dict[str, Any], **kwargs: Any) -> Any: ... + + class _V1RootValidatorClsMethod(Protocol): + def __call__( + self, __cls: Any, __values: _decorators_v1.RootValidatorValues + ) -> _decorators_v1.RootValidatorValues: ... + + V1Validator = Union[ + _OnlyValueValidatorClsMethod, + _V1ValidatorWithValuesClsMethod, + _V1ValidatorWithValuesKwOnlyClsMethod, + _V1ValidatorWithKwargsClsMethod, + _V1ValidatorWithValuesAndKwargsClsMethod, + _decorators_v1.V1ValidatorWithValues, + _decorators_v1.V1ValidatorWithValuesKwOnly, + _decorators_v1.V1ValidatorWithKwargs, + _decorators_v1.V1ValidatorWithValuesAndKwargs, + ] + + V1RootValidator = Union[ + _V1RootValidatorClsMethod, + _decorators_v1.V1RootValidatorFunction, + ] + + _PartialClsOrStaticMethod: TypeAlias = Union[classmethod[Any, Any, Any], staticmethod[Any, Any], partialmethod[Any]] + + # Allow both a V1 (assumed pre=False) or V2 (assumed mode='after') validator + # We lie to type checkers and say we return the same thing we get + # but in reality we return a proxy object that _mostly_ behaves like the wrapped thing + _V1ValidatorType = TypeVar('_V1ValidatorType', V1Validator, _PartialClsOrStaticMethod) + _V1RootValidatorFunctionType = TypeVar( + '_V1RootValidatorFunctionType', + _decorators_v1.V1RootValidatorFunction, + _V1RootValidatorClsMethod, + _PartialClsOrStaticMethod, + ) +else: + # See PyCharm issues https://youtrack.jetbrains.com/issue/PY-21915 + # and https://youtrack.jetbrains.com/issue/PY-51428 + DeprecationWarning = PydanticDeprecatedSince20 + + +@deprecated( + 'Pydantic V1 style `@validator` validators are deprecated.' + ' You should migrate to Pydantic V2 style `@field_validator` validators,' + ' see the migration guide for more details', + category=None, +) +def validator( + __field: str, + *fields: str, + pre: bool = False, + each_item: bool = False, + always: bool = False, + check_fields: bool | None = None, + allow_reuse: bool = False, +) -> Callable[[_V1ValidatorType], _V1ValidatorType]: + """Decorate methods on the class indicating that they should be used to validate fields. + + Args: + __field (str): The first field the validator should be called on; this is separate + from `fields` to ensure an error is raised if you don't pass at least one. + *fields (str): Additional field(s) the validator should be called on. + pre (bool, optional): Whether this validator should be called before the standard + validators (else after). Defaults to False. + each_item (bool, optional): For complex objects (sets, lists etc.) whether to validate + individual elements rather than the whole object. Defaults to False. + always (bool, optional): Whether this method and other validators should be called even if + the value is missing. Defaults to False. + check_fields (bool | None, optional): Whether to check that the fields actually exist on the model. + Defaults to None. + allow_reuse (bool, optional): Whether to track and raise an error if another validator refers to + the decorated function. Defaults to False. + + Returns: + Callable: A decorator that can be used to decorate a + function to be used as a validator. + """ + warn( + 'Pydantic V1 style `@validator` validators are deprecated.' + ' You should migrate to Pydantic V2 style `@field_validator` validators,' + ' see the migration guide for more details', + DeprecationWarning, + stacklevel=2, + ) + + if allow_reuse is True: # pragma: no cover + warn(_ALLOW_REUSE_WARNING_MESSAGE, DeprecationWarning, stacklevel=2) + fields = __field, *fields + if isinstance(fields[0], FunctionType): + raise PydanticUserError( + '`@validator` should be used with fields and keyword arguments, not bare. ' + "E.g. usage should be `@validator('', ...)`", + code='decorator-missing-arguments', + ) + elif not all(isinstance(field, str) for field in fields): + raise PydanticUserError( + '`@validator` fields should be passed as separate string args. ' + "E.g. usage should be `@validator('', '', ...)`", + code='decorator-invalid-fields', + ) + + mode: Literal['before', 'after'] = 'before' if pre is True else 'after' + + def dec(f: Any) -> _decorators.PydanticDescriptorProxy[Any]: + if _decorators.is_instance_method_from_sig(f): + raise PydanticUserError( + '`@validator` cannot be applied to instance methods', code='validator-instance-method' + ) + # auto apply the @classmethod decorator + f = _decorators.ensure_classmethod_based_on_signature(f) + wrap = _decorators_v1.make_generic_v1_field_validator + validator_wrapper_info = _decorators.ValidatorDecoratorInfo( + fields=fields, + mode=mode, + each_item=each_item, + always=always, + check_fields=check_fields, + ) + return _decorators.PydanticDescriptorProxy(f, validator_wrapper_info, shim=wrap) + + return dec # type: ignore[return-value] + + +@overload +def root_validator( + *, + # if you don't specify `pre` the default is `pre=False` + # which means you need to specify `skip_on_failure=True` + skip_on_failure: Literal[True], + allow_reuse: bool = ..., +) -> Callable[ + [_V1RootValidatorFunctionType], + _V1RootValidatorFunctionType, +]: ... + + +@overload +def root_validator( + *, + # if you specify `pre=True` then you don't need to specify + # `skip_on_failure`, in fact it is not allowed as an argument! + pre: Literal[True], + allow_reuse: bool = ..., +) -> Callable[ + [_V1RootValidatorFunctionType], + _V1RootValidatorFunctionType, +]: ... + + +@overload +def root_validator( + *, + # if you explicitly specify `pre=False` then you + # MUST specify `skip_on_failure=True` + pre: Literal[False], + skip_on_failure: Literal[True], + allow_reuse: bool = ..., +) -> Callable[ + [_V1RootValidatorFunctionType], + _V1RootValidatorFunctionType, +]: ... + + +@deprecated( + 'Pydantic V1 style `@root_validator` validators are deprecated.' + ' You should migrate to Pydantic V2 style `@model_validator` validators,' + ' see the migration guide for more details', + category=None, +) +def root_validator( + *__args, + pre: bool = False, + skip_on_failure: bool = False, + allow_reuse: bool = False, +) -> Any: + """Decorate methods on a model indicating that they should be used to validate (and perhaps + modify) data either before or after standard model parsing/validation is performed. + + Args: + pre (bool, optional): Whether this validator should be called before the standard + validators (else after). Defaults to False. + skip_on_failure (bool, optional): Whether to stop validation and return as soon as a + failure is encountered. Defaults to False. + allow_reuse (bool, optional): Whether to track and raise an error if another validator + refers to the decorated function. Defaults to False. + + Returns: + Any: A decorator that can be used to decorate a function to be used as a root_validator. + """ + warn( + 'Pydantic V1 style `@root_validator` validators are deprecated.' + ' You should migrate to Pydantic V2 style `@model_validator` validators,' + ' see the migration guide for more details', + DeprecationWarning, + stacklevel=2, + ) + + if __args: + # Ensure a nice error is raised if someone attempts to use the bare decorator + return root_validator()(*__args) # type: ignore + + if allow_reuse is True: # pragma: no cover + warn(_ALLOW_REUSE_WARNING_MESSAGE, DeprecationWarning, stacklevel=2) + mode: Literal['before', 'after'] = 'before' if pre is True else 'after' + if pre is False and skip_on_failure is not True: + raise PydanticUserError( + 'If you use `@root_validator` with pre=False (the default) you MUST specify `skip_on_failure=True`.' + ' Note that `@root_validator` is deprecated and should be replaced with `@model_validator`.', + code='root-validator-pre-skip', + ) + + wrap = partial(_decorators_v1.make_v1_generic_root_validator, pre=pre) + + def dec(f: Callable[..., Any] | classmethod[Any, Any, Any] | staticmethod[Any, Any]) -> Any: + if _decorators.is_instance_method_from_sig(f): + raise TypeError('`@root_validator` cannot be applied to instance methods') + # auto apply the @classmethod decorator + res = _decorators.ensure_classmethod_based_on_signature(f) + dec_info = _decorators.RootValidatorDecoratorInfo(mode=mode) + return _decorators.PydanticDescriptorProxy(res, dec_info, shim=wrap) + + return dec diff --git a/source/target-vendors/cp312/pydantic/deprecated/config.py b/source/target-vendors/cp312/pydantic/deprecated/config.py new file mode 100644 index 0000000..bd4692a --- /dev/null +++ b/source/target-vendors/cp312/pydantic/deprecated/config.py @@ -0,0 +1,72 @@ +from __future__ import annotations as _annotations + +import warnings +from typing import TYPE_CHECKING, Any, Literal + +from typing_extensions import deprecated + +from .._internal import _config +from ..warnings import PydanticDeprecatedSince20 + +if not TYPE_CHECKING: + # See PyCharm issues https://youtrack.jetbrains.com/issue/PY-21915 + # and https://youtrack.jetbrains.com/issue/PY-51428 + DeprecationWarning = PydanticDeprecatedSince20 + +__all__ = 'BaseConfig', 'Extra' + + +class _ConfigMetaclass(type): + def __getattr__(self, item: str) -> Any: + try: + obj = _config.config_defaults[item] + warnings.warn(_config.DEPRECATION_MESSAGE, DeprecationWarning) + return obj + except KeyError as exc: + raise AttributeError(f"type object '{self.__name__}' has no attribute {exc}") from exc + + +@deprecated('BaseConfig is deprecated. Use the `pydantic.ConfigDict` instead.', category=PydanticDeprecatedSince20) +class BaseConfig(metaclass=_ConfigMetaclass): + """This class is only retained for backwards compatibility. + + !!! Warning "Deprecated" + BaseConfig is deprecated. Use the [`pydantic.ConfigDict`][pydantic.ConfigDict] instead. + """ + + def __getattr__(self, item: str) -> Any: + try: + obj = super().__getattribute__(item) + warnings.warn(_config.DEPRECATION_MESSAGE, DeprecationWarning) + return obj + except AttributeError as exc: + try: + return getattr(type(self), item) + except AttributeError: + # re-raising changes the displayed text to reflect that `self` is not a type + raise AttributeError(str(exc)) from exc + + def __init_subclass__(cls, **kwargs: Any) -> None: + warnings.warn(_config.DEPRECATION_MESSAGE, DeprecationWarning) + return super().__init_subclass__(**kwargs) + + +class _ExtraMeta(type): + def __getattribute__(self, __name: str) -> Any: + # The @deprecated decorator accesses other attributes, so we only emit a warning for the expected ones + if __name in {'allow', 'ignore', 'forbid'}: + warnings.warn( + "`pydantic.config.Extra` is deprecated, use literal values instead (e.g. `extra='allow'`)", + DeprecationWarning, + stacklevel=2, + ) + return super().__getattribute__(__name) + + +@deprecated( + "Extra is deprecated. Use literal values instead (e.g. `extra='allow'`)", category=PydanticDeprecatedSince20 +) +class Extra(metaclass=_ExtraMeta): + allow: Literal['allow'] = 'allow' + ignore: Literal['ignore'] = 'ignore' + forbid: Literal['forbid'] = 'forbid' diff --git a/source/target-vendors/cp312/pydantic/deprecated/copy_internals.py b/source/target-vendors/cp312/pydantic/deprecated/copy_internals.py new file mode 100644 index 0000000..0170dc0 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/deprecated/copy_internals.py @@ -0,0 +1,224 @@ +from __future__ import annotations as _annotations + +import typing +from copy import deepcopy +from enum import Enum +from typing import Any + +import typing_extensions + +from .._internal import ( + _model_construction, + _typing_extra, + _utils, +) + +if typing.TYPE_CHECKING: + from .. import BaseModel + from .._internal._utils import AbstractSetIntStr, MappingIntStrAny + + AnyClassMethod = classmethod[Any, Any, Any] + TupleGenerator = typing.Generator[tuple[str, Any], None, None] + Model = typing.TypeVar('Model', bound='BaseModel') + # should be `set[int] | set[str] | dict[int, IncEx] | dict[str, IncEx] | None`, but mypy can't cope + IncEx: typing_extensions.TypeAlias = 'set[int] | set[str] | dict[int, Any] | dict[str, Any] | None' + +_object_setattr = _model_construction.object_setattr + + +def _iter( + self: BaseModel, + to_dict: bool = False, + by_alias: bool = False, + include: AbstractSetIntStr | MappingIntStrAny | None = None, + exclude: AbstractSetIntStr | MappingIntStrAny | None = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, +) -> TupleGenerator: + # Merge field set excludes with explicit exclude parameter with explicit overriding field set options. + # The extra "is not None" guards are not logically necessary but optimizes performance for the simple case. + if exclude is not None: + exclude = _utils.ValueItems.merge( + {k: v.exclude for k, v in self.__pydantic_fields__.items() if v.exclude is not None}, exclude + ) + + if include is not None: + include = _utils.ValueItems.merge(dict.fromkeys(self.__pydantic_fields__, True), include, intersect=True) + + allowed_keys = _calculate_keys(self, include=include, exclude=exclude, exclude_unset=exclude_unset) # type: ignore + if allowed_keys is None and not (to_dict or by_alias or exclude_unset or exclude_defaults or exclude_none): + # huge boost for plain _iter() + yield from self.__dict__.items() + if self.__pydantic_extra__: + yield from self.__pydantic_extra__.items() + return + + value_exclude = _utils.ValueItems(self, exclude) if exclude is not None else None + value_include = _utils.ValueItems(self, include) if include is not None else None + + if self.__pydantic_extra__ is None: + items = self.__dict__.items() + else: + items = list(self.__dict__.items()) + list(self.__pydantic_extra__.items()) + + for field_key, v in items: + if (allowed_keys is not None and field_key not in allowed_keys) or (exclude_none and v is None): + continue + + if exclude_defaults: + try: + field = self.__pydantic_fields__[field_key] + except KeyError: + pass + else: + if not field.is_required() and field.default == v: + continue + + if by_alias and field_key in self.__pydantic_fields__: + dict_key = self.__pydantic_fields__[field_key].alias or field_key + else: + dict_key = field_key + + if to_dict or value_include or value_exclude: + v = _get_value( + type(self), + v, + to_dict=to_dict, + by_alias=by_alias, + include=value_include and value_include.for_element(field_key), + exclude=value_exclude and value_exclude.for_element(field_key), + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + yield dict_key, v + + +def _copy_and_set_values( + self: Model, + values: dict[str, Any], + fields_set: set[str], + extra: dict[str, Any] | None = None, + private: dict[str, Any] | None = None, + *, + deep: bool, # UP006 +) -> Model: + if deep: + # chances of having empty dict here are quite low for using smart_deepcopy + values = deepcopy(values) + extra = deepcopy(extra) + private = deepcopy(private) + + cls = self.__class__ + m = cls.__new__(cls) + _object_setattr(m, '__dict__', values) + _object_setattr(m, '__pydantic_extra__', extra) + _object_setattr(m, '__pydantic_fields_set__', fields_set) + _object_setattr(m, '__pydantic_private__', private) + + return m + + +@typing.no_type_check +def _get_value( + cls: type[BaseModel], + v: Any, + to_dict: bool, + by_alias: bool, + include: AbstractSetIntStr | MappingIntStrAny | None, + exclude: AbstractSetIntStr | MappingIntStrAny | None, + exclude_unset: bool, + exclude_defaults: bool, + exclude_none: bool, +) -> Any: + from .. import BaseModel + + if isinstance(v, BaseModel): + if to_dict: + return v.model_dump( + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + include=include, # type: ignore + exclude=exclude, # type: ignore + exclude_none=exclude_none, + ) + else: + return v.copy(include=include, exclude=exclude) + + value_exclude = _utils.ValueItems(v, exclude) if exclude else None + value_include = _utils.ValueItems(v, include) if include else None + + if isinstance(v, dict): + return { + k_: _get_value( + cls, + v_, + to_dict=to_dict, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + include=value_include and value_include.for_element(k_), + exclude=value_exclude and value_exclude.for_element(k_), + exclude_none=exclude_none, + ) + for k_, v_ in v.items() + if (not value_exclude or not value_exclude.is_excluded(k_)) + and (not value_include or value_include.is_included(k_)) + } + + elif _utils.sequence_like(v): + seq_args = ( + _get_value( + cls, + v_, + to_dict=to_dict, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + include=value_include and value_include.for_element(i), + exclude=value_exclude and value_exclude.for_element(i), + exclude_none=exclude_none, + ) + for i, v_ in enumerate(v) + if (not value_exclude or not value_exclude.is_excluded(i)) + and (not value_include or value_include.is_included(i)) + ) + + return v.__class__(*seq_args) if _typing_extra.is_namedtuple(v.__class__) else v.__class__(seq_args) + + elif isinstance(v, Enum) and getattr(cls.model_config, 'use_enum_values', False): + return v.value + + else: + return v + + +def _calculate_keys( + self: BaseModel, + include: MappingIntStrAny | None, + exclude: MappingIntStrAny | None, + exclude_unset: bool, + update: dict[str, Any] | None = None, # noqa UP006 +) -> typing.AbstractSet[str] | None: + if include is None and exclude is None and exclude_unset is False: + return None + + keys: typing.AbstractSet[str] + if exclude_unset: + keys = self.__pydantic_fields_set__.copy() + else: + keys = set(self.__dict__.keys()) + keys = keys | (self.__pydantic_extra__ or {}).keys() + + if include is not None: + keys &= include.keys() + + if update: + keys -= update.keys() + + if exclude: + keys -= {k for k, v in exclude.items() if _utils.ValueItems.is_true(v)} + + return keys diff --git a/source/target-vendors/cp312/pydantic/deprecated/decorator.py b/source/target-vendors/cp312/pydantic/deprecated/decorator.py new file mode 100644 index 0000000..e73ad20 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/deprecated/decorator.py @@ -0,0 +1,284 @@ +import warnings +from collections.abc import Mapping +from functools import wraps +from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union, overload + +from typing_extensions import deprecated + +from .._internal import _config, _typing_extra +from ..alias_generators import to_pascal +from ..errors import PydanticUserError +from ..functional_validators import field_validator +from ..main import BaseModel, create_model +from ..warnings import PydanticDeprecatedSince20 + +if not TYPE_CHECKING: + # See PyCharm issues https://youtrack.jetbrains.com/issue/PY-21915 + # and https://youtrack.jetbrains.com/issue/PY-51428 + DeprecationWarning = PydanticDeprecatedSince20 + +__all__ = ('validate_arguments',) + +if TYPE_CHECKING: + AnyCallable = Callable[..., Any] + + AnyCallableT = TypeVar('AnyCallableT', bound=AnyCallable) + ConfigType = Union[None, type[Any], dict[str, Any]] + + +@overload +def validate_arguments( + func: None = None, *, config: 'ConfigType' = None +) -> Callable[['AnyCallableT'], 'AnyCallableT']: ... + + +@overload +def validate_arguments(func: 'AnyCallableT') -> 'AnyCallableT': ... + + +@deprecated( + 'The `validate_arguments` method is deprecated; use `validate_call` instead.', + category=None, +) +def validate_arguments(func: Optional['AnyCallableT'] = None, *, config: 'ConfigType' = None) -> Any: + """Decorator to validate the arguments passed to a function.""" + warnings.warn( + 'The `validate_arguments` method is deprecated; use `validate_call` instead.', + PydanticDeprecatedSince20, + stacklevel=2, + ) + + def validate(_func: 'AnyCallable') -> 'AnyCallable': + vd = ValidatedFunction(_func, config) + + @wraps(_func) + def wrapper_function(*args: Any, **kwargs: Any) -> Any: + return vd.call(*args, **kwargs) + + wrapper_function.vd = vd # type: ignore + wrapper_function.validate = vd.init_model_instance # type: ignore + wrapper_function.raw_function = vd.raw_function # type: ignore + wrapper_function.model = vd.model # type: ignore + return wrapper_function + + if func: + return validate(func) + else: + return validate + + +ALT_V_ARGS = 'v__args' +ALT_V_KWARGS = 'v__kwargs' +V_POSITIONAL_ONLY_NAME = 'v__positional_only' +V_DUPLICATE_KWARGS = 'v__duplicate_kwargs' + + +class ValidatedFunction: + def __init__(self, function: 'AnyCallable', config: 'ConfigType'): + from inspect import Parameter, signature + + parameters: Mapping[str, Parameter] = signature(function).parameters + + if parameters.keys() & {ALT_V_ARGS, ALT_V_KWARGS, V_POSITIONAL_ONLY_NAME, V_DUPLICATE_KWARGS}: + raise PydanticUserError( + f'"{ALT_V_ARGS}", "{ALT_V_KWARGS}", "{V_POSITIONAL_ONLY_NAME}" and "{V_DUPLICATE_KWARGS}" ' + f'are not permitted as argument names when using the "{validate_arguments.__name__}" decorator', + code=None, + ) + + self.raw_function = function + self.arg_mapping: dict[int, str] = {} + self.positional_only_args: set[str] = set() + self.v_args_name = 'args' + self.v_kwargs_name = 'kwargs' + + type_hints = _typing_extra.get_type_hints(function, include_extras=True) + takes_args = False + takes_kwargs = False + fields: dict[str, tuple[Any, Any]] = {} + for i, (name, p) in enumerate(parameters.items()): + if p.annotation is p.empty: + annotation = Any + else: + annotation = type_hints[name] + + default = ... if p.default is p.empty else p.default + if p.kind == Parameter.POSITIONAL_ONLY: + self.arg_mapping[i] = name + fields[name] = annotation, default + fields[V_POSITIONAL_ONLY_NAME] = list[str], None + self.positional_only_args.add(name) + elif p.kind == Parameter.POSITIONAL_OR_KEYWORD: + self.arg_mapping[i] = name + fields[name] = annotation, default + fields[V_DUPLICATE_KWARGS] = list[str], None + elif p.kind == Parameter.KEYWORD_ONLY: + fields[name] = annotation, default + elif p.kind == Parameter.VAR_POSITIONAL: + self.v_args_name = name + fields[name] = tuple[annotation, ...], None + takes_args = True + else: + assert p.kind == Parameter.VAR_KEYWORD, p.kind + self.v_kwargs_name = name + fields[name] = dict[str, annotation], None + takes_kwargs = True + + # these checks avoid a clash between "args" and a field with that name + if not takes_args and self.v_args_name in fields: + self.v_args_name = ALT_V_ARGS + + # same with "kwargs" + if not takes_kwargs and self.v_kwargs_name in fields: + self.v_kwargs_name = ALT_V_KWARGS + + if not takes_args: + # we add the field so validation below can raise the correct exception + fields[self.v_args_name] = list[Any], None + + if not takes_kwargs: + # same with kwargs + fields[self.v_kwargs_name] = dict[Any, Any], None + + self.create_model(fields, takes_args, takes_kwargs, config) + + def init_model_instance(self, *args: Any, **kwargs: Any) -> BaseModel: + values = self.build_values(args, kwargs) + return self.model(**values) + + def call(self, *args: Any, **kwargs: Any) -> Any: + m = self.init_model_instance(*args, **kwargs) + return self.execute(m) + + def build_values(self, args: tuple[Any, ...], kwargs: dict[str, Any]) -> dict[str, Any]: + values: dict[str, Any] = {} + if args: + arg_iter = enumerate(args) + while True: + try: + i, a = next(arg_iter) + except StopIteration: + break + arg_name = self.arg_mapping.get(i) + if arg_name is not None: + values[arg_name] = a + else: + values[self.v_args_name] = [a] + [a for _, a in arg_iter] + break + + var_kwargs: dict[str, Any] = {} + wrong_positional_args = [] + duplicate_kwargs = [] + fields_alias = [ + field.alias + for name, field in self.model.__pydantic_fields__.items() + if name not in (self.v_args_name, self.v_kwargs_name) + ] + non_var_fields = set(self.model.__pydantic_fields__) - {self.v_args_name, self.v_kwargs_name} + for k, v in kwargs.items(): + if k in non_var_fields or k in fields_alias: + if k in self.positional_only_args: + wrong_positional_args.append(k) + if k in values: + duplicate_kwargs.append(k) + values[k] = v + else: + var_kwargs[k] = v + + if var_kwargs: + values[self.v_kwargs_name] = var_kwargs + if wrong_positional_args: + values[V_POSITIONAL_ONLY_NAME] = wrong_positional_args + if duplicate_kwargs: + values[V_DUPLICATE_KWARGS] = duplicate_kwargs + return values + + def execute(self, m: BaseModel) -> Any: + d = { + k: v + for k, v in m.__dict__.items() + if k in m.__pydantic_fields_set__ or m.__pydantic_fields__[k].default_factory + } + var_kwargs = d.pop(self.v_kwargs_name, {}) + + if self.v_args_name in d: + args_: list[Any] = [] + in_kwargs = False + kwargs = {} + for name, value in d.items(): + if in_kwargs: + kwargs[name] = value + elif name == self.v_args_name: + args_ += value + in_kwargs = True + else: + args_.append(value) + return self.raw_function(*args_, **kwargs, **var_kwargs) + elif self.positional_only_args: + args_ = [] + kwargs = {} + for name, value in d.items(): + if name in self.positional_only_args: + args_.append(value) + else: + kwargs[name] = value + return self.raw_function(*args_, **kwargs, **var_kwargs) + else: + return self.raw_function(**d, **var_kwargs) + + def create_model(self, fields: dict[str, Any], takes_args: bool, takes_kwargs: bool, config: 'ConfigType') -> None: + pos_args = len(self.arg_mapping) + + config_wrapper = _config.ConfigWrapper(config) + + if config_wrapper.alias_generator: + raise PydanticUserError( + 'Setting the "alias_generator" property on custom Config for ' + '@validate_arguments is not yet supported, please remove.', + code=None, + ) + if config_wrapper.extra is None: + config_wrapper.config_dict['extra'] = 'forbid' + + class DecoratorBaseModel(BaseModel): + @field_validator(self.v_args_name, check_fields=False) + @classmethod + def check_args(cls, v: Optional[list[Any]]) -> Optional[list[Any]]: + if takes_args or v is None: + return v + + raise TypeError(f'{pos_args} positional arguments expected but {pos_args + len(v)} given') + + @field_validator(self.v_kwargs_name, check_fields=False) + @classmethod + def check_kwargs(cls, v: Optional[dict[str, Any]]) -> Optional[dict[str, Any]]: + if takes_kwargs or v is None: + return v + + plural = '' if len(v) == 1 else 's' + keys = ', '.join(map(repr, v.keys())) + raise TypeError(f'unexpected keyword argument{plural}: {keys}') + + @field_validator(V_POSITIONAL_ONLY_NAME, check_fields=False) + @classmethod + def check_positional_only(cls, v: Optional[list[str]]) -> None: + if v is None: + return + + plural = '' if len(v) == 1 else 's' + keys = ', '.join(map(repr, v)) + raise TypeError(f'positional-only argument{plural} passed as keyword argument{plural}: {keys}') + + @field_validator(V_DUPLICATE_KWARGS, check_fields=False) + @classmethod + def check_duplicate_kwargs(cls, v: Optional[list[str]]) -> None: + if v is None: + return + + plural = '' if len(v) == 1 else 's' + keys = ', '.join(map(repr, v)) + raise TypeError(f'multiple values for argument{plural}: {keys}') + + model_config = config_wrapper.config_dict + + self.model = create_model(to_pascal(self.raw_function.__name__), __base__=DecoratorBaseModel, **fields) diff --git a/source/target-vendors/cp312/pydantic/deprecated/json.py b/source/target-vendors/cp312/pydantic/deprecated/json.py new file mode 100644 index 0000000..1e216a7 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/deprecated/json.py @@ -0,0 +1,141 @@ +import datetime +import warnings +from collections import deque +from decimal import Decimal +from enum import Enum +from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network +from pathlib import Path +from re import Pattern +from types import GeneratorType +from typing import TYPE_CHECKING, Any, Callable, Union +from uuid import UUID + +from typing_extensions import deprecated + +from .._internal._import_utils import import_cached_base_model +from ..color import Color +from ..networks import NameEmail +from ..types import SecretBytes, SecretStr +from ..warnings import PydanticDeprecatedSince20 + +if not TYPE_CHECKING: + # See PyCharm issues https://youtrack.jetbrains.com/issue/PY-21915 + # and https://youtrack.jetbrains.com/issue/PY-51428 + DeprecationWarning = PydanticDeprecatedSince20 + +__all__ = 'pydantic_encoder', 'custom_pydantic_encoder', 'timedelta_isoformat' + + +def isoformat(o: Union[datetime.date, datetime.time]) -> str: + return o.isoformat() + + +def decimal_encoder(dec_value: Decimal) -> Union[int, float]: + """Encodes a Decimal as int of there's no exponent, otherwise float. + + This is useful when we use ConstrainedDecimal to represent Numeric(x,0) + where a integer (but not int typed) is used. Encoding this as a float + results in failed round-tripping between encode and parse. + Our Id type is a prime example of this. + + >>> decimal_encoder(Decimal("1.0")) + 1.0 + + >>> decimal_encoder(Decimal("1")) + 1 + """ + exponent = dec_value.as_tuple().exponent + if isinstance(exponent, int) and exponent >= 0: + return int(dec_value) + else: + return float(dec_value) + + +ENCODERS_BY_TYPE: dict[type[Any], Callable[[Any], Any]] = { + bytes: lambda o: o.decode(), + Color: str, + datetime.date: isoformat, + datetime.datetime: isoformat, + datetime.time: isoformat, + datetime.timedelta: lambda td: td.total_seconds(), + Decimal: decimal_encoder, + Enum: lambda o: o.value, + frozenset: list, + deque: list, + GeneratorType: list, + IPv4Address: str, + IPv4Interface: str, + IPv4Network: str, + IPv6Address: str, + IPv6Interface: str, + IPv6Network: str, + NameEmail: str, + Path: str, + Pattern: lambda o: o.pattern, + SecretBytes: str, + SecretStr: str, + set: list, + UUID: str, +} + + +@deprecated( + '`pydantic_encoder` is deprecated, use `pydantic_core.to_jsonable_python` instead.', + category=None, +) +def pydantic_encoder(obj: Any) -> Any: + warnings.warn( + '`pydantic_encoder` is deprecated, use `pydantic_core.to_jsonable_python` instead.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + from dataclasses import asdict, is_dataclass + + BaseModel = import_cached_base_model() + + if isinstance(obj, BaseModel): + return obj.model_dump() + elif is_dataclass(obj): + return asdict(obj) # type: ignore + + # Check the class type and its superclasses for a matching encoder + for base in obj.__class__.__mro__[:-1]: + try: + encoder = ENCODERS_BY_TYPE[base] + except KeyError: + continue + return encoder(obj) + else: # We have exited the for loop without finding a suitable encoder + raise TypeError(f"Object of type '{obj.__class__.__name__}' is not JSON serializable") + + +# TODO: Add a suggested migration path once there is a way to use custom encoders +@deprecated( + '`custom_pydantic_encoder` is deprecated, use `BaseModel.model_dump` instead.', + category=None, +) +def custom_pydantic_encoder(type_encoders: dict[Any, Callable[[type[Any]], Any]], obj: Any) -> Any: + warnings.warn( + '`custom_pydantic_encoder` is deprecated, use `BaseModel.model_dump` instead.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + # Check the class type and its superclasses for a matching encoder + for base in obj.__class__.__mro__[:-1]: + try: + encoder = type_encoders[base] + except KeyError: + continue + + return encoder(obj) + else: # We have exited the for loop without finding a suitable encoder + return pydantic_encoder(obj) + + +@deprecated('`timedelta_isoformat` is deprecated.', category=None) +def timedelta_isoformat(td: datetime.timedelta) -> str: + """ISO 8601 encoding for Python timedelta object.""" + warnings.warn('`timedelta_isoformat` is deprecated.', category=PydanticDeprecatedSince20, stacklevel=2) + minutes, seconds = divmod(td.seconds, 60) + hours, minutes = divmod(minutes, 60) + return f'{"-" if td.days < 0 else ""}P{abs(td.days)}DT{hours:d}H{minutes:d}M{seconds:d}.{td.microseconds:06d}S' diff --git a/source/target-vendors/cp312/pydantic/deprecated/parse.py b/source/target-vendors/cp312/pydantic/deprecated/parse.py new file mode 100644 index 0000000..2a92e62 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/deprecated/parse.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import json +import pickle +import warnings +from enum import Enum +from pathlib import Path +from typing import TYPE_CHECKING, Any, Callable + +from typing_extensions import deprecated + +from ..warnings import PydanticDeprecatedSince20 + +if not TYPE_CHECKING: + # See PyCharm issues https://youtrack.jetbrains.com/issue/PY-21915 + # and https://youtrack.jetbrains.com/issue/PY-51428 + DeprecationWarning = PydanticDeprecatedSince20 + + +class Protocol(str, Enum): + json = 'json' + pickle = 'pickle' + + +@deprecated('`load_str_bytes` is deprecated.', category=None) +def load_str_bytes( + b: str | bytes, + *, + content_type: str | None = None, + encoding: str = 'utf8', + proto: Protocol | None = None, + allow_pickle: bool = False, + json_loads: Callable[[str], Any] = json.loads, +) -> Any: + warnings.warn('`load_str_bytes` is deprecated.', category=PydanticDeprecatedSince20, stacklevel=2) + if proto is None and content_type: + if content_type.endswith(('json', 'javascript')): + pass + elif allow_pickle and content_type.endswith('pickle'): + proto = Protocol.pickle + else: + raise TypeError(f'Unknown content-type: {content_type}') + + proto = proto or Protocol.json + + if proto == Protocol.json: + if isinstance(b, bytes): + b = b.decode(encoding) + return json_loads(b) # type: ignore + elif proto == Protocol.pickle: + if not allow_pickle: + raise RuntimeError('Trying to decode with pickle with allow_pickle=False') + bb = b if isinstance(b, bytes) else b.encode() # type: ignore + return pickle.loads(bb) + else: + raise TypeError(f'Unknown protocol: {proto}') + + +@deprecated('`load_file` is deprecated.', category=None) +def load_file( + path: str | Path, + *, + content_type: str | None = None, + encoding: str = 'utf8', + proto: Protocol | None = None, + allow_pickle: bool = False, + json_loads: Callable[[str], Any] = json.loads, +) -> Any: + warnings.warn('`load_file` is deprecated.', category=PydanticDeprecatedSince20, stacklevel=2) + path = Path(path) + b = path.read_bytes() + if content_type is None: + if path.suffix in ('.js', '.json'): + proto = Protocol.json + elif path.suffix == '.pkl': + proto = Protocol.pickle + + return load_str_bytes( + b, proto=proto, content_type=content_type, encoding=encoding, allow_pickle=allow_pickle, json_loads=json_loads + ) diff --git a/source/target-vendors/cp312/pydantic/deprecated/tools.py b/source/target-vendors/cp312/pydantic/deprecated/tools.py new file mode 100644 index 0000000..5ad7fae --- /dev/null +++ b/source/target-vendors/cp312/pydantic/deprecated/tools.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +import json +import warnings +from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union + +from typing_extensions import deprecated + +from ..json_schema import DEFAULT_REF_TEMPLATE, GenerateJsonSchema +from ..type_adapter import TypeAdapter +from ..warnings import PydanticDeprecatedSince20 + +if not TYPE_CHECKING: + # See PyCharm issues https://youtrack.jetbrains.com/issue/PY-21915 + # and https://youtrack.jetbrains.com/issue/PY-51428 + DeprecationWarning = PydanticDeprecatedSince20 + +__all__ = 'parse_obj_as', 'schema_of', 'schema_json_of' + +NameFactory = Union[str, Callable[[type[Any]], str]] + + +T = TypeVar('T') + + +@deprecated( + '`parse_obj_as` is deprecated. Use `pydantic.TypeAdapter.validate_python` instead.', + category=None, +) +def parse_obj_as(type_: type[T], obj: Any, type_name: NameFactory | None = None) -> T: + warnings.warn( + '`parse_obj_as` is deprecated. Use `pydantic.TypeAdapter.validate_python` instead.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + if type_name is not None: # pragma: no cover + warnings.warn( + 'The type_name parameter is deprecated. parse_obj_as no longer creates temporary models', + DeprecationWarning, + stacklevel=2, + ) + return TypeAdapter(type_).validate_python(obj) + + +@deprecated( + '`schema_of` is deprecated. Use `pydantic.TypeAdapter.json_schema` instead.', + category=None, +) +def schema_of( + type_: Any, + *, + title: NameFactory | None = None, + by_alias: bool = True, + ref_template: str = DEFAULT_REF_TEMPLATE, + schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema, +) -> dict[str, Any]: + """Generate a JSON schema (as dict) for the passed model or dynamically generated one.""" + warnings.warn( + '`schema_of` is deprecated. Use `pydantic.TypeAdapter.json_schema` instead.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + res = TypeAdapter(type_).json_schema( + by_alias=by_alias, + schema_generator=schema_generator, + ref_template=ref_template, + ) + if title is not None: + if isinstance(title, str): + res['title'] = title + else: + warnings.warn( + 'Passing a callable for the `title` parameter is deprecated and no longer supported', + DeprecationWarning, + stacklevel=2, + ) + res['title'] = title(type_) + return res + + +@deprecated( + '`schema_json_of` is deprecated. Use `pydantic.TypeAdapter.json_schema` instead.', + category=None, +) +def schema_json_of( + type_: Any, + *, + title: NameFactory | None = None, + by_alias: bool = True, + ref_template: str = DEFAULT_REF_TEMPLATE, + schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema, + **dumps_kwargs: Any, +) -> str: + """Generate a JSON schema (as JSON) for the passed model or dynamically generated one.""" + warnings.warn( + '`schema_json_of` is deprecated. Use `pydantic.TypeAdapter.json_schema` instead.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + return json.dumps( + schema_of(type_, title=title, by_alias=by_alias, ref_template=ref_template, schema_generator=schema_generator), + **dumps_kwargs, + ) diff --git a/source/target-vendors/cp312/pydantic/env_settings.py b/source/target-vendors/cp312/pydantic/env_settings.py new file mode 100644 index 0000000..cd0b04e --- /dev/null +++ b/source/target-vendors/cp312/pydantic/env_settings.py @@ -0,0 +1,5 @@ +"""The `env_settings` module is a backport module from V1.""" + +from ._migration import getattr_migration + +__getattr__ = getattr_migration(__name__) diff --git a/source/target-vendors/cp312/pydantic/error_wrappers.py b/source/target-vendors/cp312/pydantic/error_wrappers.py new file mode 100644 index 0000000..2985419 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/error_wrappers.py @@ -0,0 +1,5 @@ +"""The `error_wrappers` module is a backport module from V1.""" + +from ._migration import getattr_migration + +__getattr__ = getattr_migration(__name__) diff --git a/source/target-vendors/cp312/pydantic/errors.py b/source/target-vendors/cp312/pydantic/errors.py new file mode 100644 index 0000000..33851bd --- /dev/null +++ b/source/target-vendors/cp312/pydantic/errors.py @@ -0,0 +1,189 @@ +"""Pydantic-specific errors.""" + +from __future__ import annotations as _annotations + +import re +from typing import Any, ClassVar, Literal + +from typing_extensions import Self +from typing_inspection.introspection import Qualifier + +from pydantic._internal import _repr + +from ._migration import getattr_migration +from .version import version_short + +__all__ = ( + 'PydanticUserError', + 'PydanticUndefinedAnnotation', + 'PydanticImportError', + 'PydanticSchemaGenerationError', + 'PydanticInvalidForJsonSchema', + 'PydanticForbiddenQualifier', + 'PydanticErrorCodes', +) + +# We use this URL to allow for future flexibility about how we host the docs, while allowing for Pydantic +# code in the while with "old" URLs to still work. +# 'u' refers to "user errors" - e.g. errors caused by developers using pydantic, as opposed to validation errors. +DEV_ERROR_DOCS_URL = f'https://errors.pydantic.dev/{version_short()}/u/' +PydanticErrorCodes = Literal[ + 'class-not-fully-defined', + 'custom-json-schema', + 'decorator-invalid-fields', + 'decorator-missing-arguments', + 'decorator-missing-field', + 'discriminator-no-field', + 'discriminator-alias-type', + 'discriminator-needs-literal', + 'discriminator-alias', + 'discriminator-validator', + 'callable-discriminator-no-tag', + 'typed-dict-version', + 'model-field-overridden', + 'model-field-missing-annotation', + 'config-both', + 'removed-kwargs', + 'circular-reference-schema', + 'invalid-for-json-schema', + 'json-schema-already-used', + 'base-model-instantiated', + 'undefined-annotation', + 'schema-for-unknown-type', + 'import-error', + 'create-model-field-definitions', + 'validator-instance-method', + 'validator-input-type', + 'root-validator-pre-skip', + 'model-serializer-instance-method', + 'validator-field-config-info', + 'validator-v1-signature', + 'validator-signature', + 'field-serializer-signature', + 'model-serializer-signature', + 'multiple-field-serializers', + 'invalid-annotated-type', + 'type-adapter-config-unused', + 'root-model-extra', + 'unevaluable-type-annotation', + 'dataclass-init-false-extra-allow', + 'clashing-init-and-init-var', + 'model-config-invalid-field-name', + 'with-config-on-model', + 'dataclass-on-model', + 'validate-call-type', + 'unpack-typed-dict', + 'overlapping-unpack-typed-dict', + 'invalid-self-type', + 'validate-by-alias-and-name-false', +] + + +class PydanticErrorMixin: + """A mixin class for common functionality shared by all Pydantic-specific errors. + + Attributes: + message: A message describing the error. + code: An optional error code from PydanticErrorCodes enum. + """ + + def __init__(self, message: str, *, code: PydanticErrorCodes | None) -> None: + self.message = message + self.code = code + + def __str__(self) -> str: + if self.code is None: + return self.message + else: + return f'{self.message}\n\nFor further information visit {DEV_ERROR_DOCS_URL}{self.code}' + + +class PydanticUserError(PydanticErrorMixin, RuntimeError): + """An error raised due to incorrect use of Pydantic.""" + + +class PydanticUndefinedAnnotation(PydanticErrorMixin, NameError): + """A subclass of `NameError` raised when handling undefined annotations during `CoreSchema` generation. + + Attributes: + name: Name of the error. + message: Description of the error. + """ + + def __init__(self, name: str, message: str) -> None: + self.name = name + super().__init__(message=message, code='undefined-annotation') + + @classmethod + def from_name_error(cls, name_error: NameError) -> Self: + """Convert a `NameError` to a `PydanticUndefinedAnnotation` error. + + Args: + name_error: `NameError` to be converted. + + Returns: + Converted `PydanticUndefinedAnnotation` error. + """ + try: + name = name_error.name # type: ignore # python > 3.10 + except AttributeError: + name = re.search(r".*'(.+?)'", str(name_error)).group(1) # type: ignore[union-attr] + return cls(name=name, message=str(name_error)) + + +class PydanticImportError(PydanticErrorMixin, ImportError): + """An error raised when an import fails due to module changes between V1 and V2. + + Attributes: + message: Description of the error. + """ + + def __init__(self, message: str) -> None: + super().__init__(message, code='import-error') + + +class PydanticSchemaGenerationError(PydanticUserError): + """An error raised during failures to generate a `CoreSchema` for some type. + + Attributes: + message: Description of the error. + """ + + def __init__(self, message: str) -> None: + super().__init__(message, code='schema-for-unknown-type') + + +class PydanticInvalidForJsonSchema(PydanticUserError): + """An error raised during failures to generate a JSON schema for some `CoreSchema`. + + Attributes: + message: Description of the error. + """ + + def __init__(self, message: str) -> None: + super().__init__(message, code='invalid-for-json-schema') + + +class PydanticForbiddenQualifier(PydanticUserError): + """An error raised if a forbidden type qualifier is found in a type annotation.""" + + _qualifier_repr_map: ClassVar[dict[Qualifier, str]] = { + 'required': 'typing.Required', + 'not_required': 'typing.NotRequired', + 'read_only': 'typing.ReadOnly', + 'class_var': 'typing.ClassVar', + 'init_var': 'dataclasses.InitVar', + 'final': 'typing.Final', + } + + def __init__(self, qualifier: Qualifier, annotation: Any) -> None: + super().__init__( + message=( + f'The annotation {_repr.display_as_type(annotation)!r} contains the {self._qualifier_repr_map[qualifier]!r} ' + f'type qualifier, which is invalid in the context it is defined.' + ), + code=None, + ) + + +__getattr__ = getattr_migration(__name__) diff --git a/source/target-vendors/cp312/pydantic/experimental/__init__.py b/source/target-vendors/cp312/pydantic/experimental/__init__.py new file mode 100644 index 0000000..5b5add1 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/experimental/__init__.py @@ -0,0 +1 @@ +"""The "experimental" module of pydantic contains potential new features that are subject to change.""" diff --git a/source/target-vendors/cp312/pydantic/experimental/arguments_schema.py b/source/target-vendors/cp312/pydantic/experimental/arguments_schema.py new file mode 100644 index 0000000..af4a8f3 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/experimental/arguments_schema.py @@ -0,0 +1,44 @@ +"""Experimental module exposing a function to generate a core schema that validates callable arguments.""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Any, Literal + +from pydantic_core import CoreSchema + +from pydantic import ConfigDict +from pydantic._internal import _config, _generate_schema, _namespace_utils + + +def generate_arguments_schema( + func: Callable[..., Any], + schema_type: Literal['arguments', 'arguments-v3'] = 'arguments-v3', + parameters_callback: Callable[[int, str, Any], Literal['skip'] | None] | None = None, + config: ConfigDict | None = None, +) -> CoreSchema: + """Generate the schema for the arguments of a function. + + Args: + func: The function to generate the schema for. + schema_type: The type of schema to generate. + parameters_callback: A callable that will be invoked for each parameter. The callback + should take three required arguments: the index, the name and the type annotation + (or [`Parameter.empty`][inspect.Parameter.empty] if not annotated) of the parameter. + The callback can optionally return `'skip'`, so that the parameter gets excluded + from the resulting schema. + config: The configuration to use. + + Returns: + The generated schema. + """ + generate_schema = _generate_schema.GenerateSchema( + _config.ConfigWrapper(config), + ns_resolver=_namespace_utils.NsResolver(namespaces_tuple=_namespace_utils.ns_for_function(func)), + ) + + if schema_type == 'arguments': + schema = generate_schema._arguments_schema(func, parameters_callback) # pyright: ignore[reportArgumentType] + else: + schema = generate_schema._arguments_v3_schema(func, parameters_callback) # pyright: ignore[reportArgumentType] + return generate_schema.clean_schema(schema) diff --git a/source/target-vendors/cp312/pydantic/experimental/missing_sentinel.py b/source/target-vendors/cp312/pydantic/experimental/missing_sentinel.py new file mode 100644 index 0000000..3e7f820 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/experimental/missing_sentinel.py @@ -0,0 +1,5 @@ +"""Experimental module exposing a function a `MISSING` sentinel.""" + +from pydantic_core import MISSING + +__all__ = ('MISSING',) diff --git a/source/target-vendors/cp312/pydantic/experimental/pipeline.py b/source/target-vendors/cp312/pydantic/experimental/pipeline.py new file mode 100644 index 0000000..9ead658 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/experimental/pipeline.py @@ -0,0 +1,663 @@ +"""Experimental pipeline API functionality. Be careful with this API, it's subject to change.""" + +from __future__ import annotations + +import datetime +import operator +import re +import sys +from collections import deque +from collections.abc import Container +from dataclasses import dataclass +from decimal import Decimal +from functools import cached_property, partial +from re import Pattern +from typing import TYPE_CHECKING, Annotated, Any, Callable, Generic, Protocol, TypeVar, Union, overload + +import annotated_types + +if TYPE_CHECKING: + from pydantic import GetCoreSchemaHandler + +from pydantic_core import PydanticCustomError +from pydantic_core import core_schema as cs + +from pydantic import Strict +from pydantic._internal._internal_dataclass import slots_true as _slots_true + +if sys.version_info < (3, 10): + EllipsisType = type(Ellipsis) +else: + from types import EllipsisType + +__all__ = ['validate_as', 'validate_as_deferred', 'transform'] + +_slots_frozen = {**_slots_true, 'frozen': True} + + +@dataclass(**_slots_frozen) +class _ValidateAs: + tp: type[Any] + strict: bool = False + + +@dataclass +class _ValidateAsDefer: + func: Callable[[], type[Any]] + + @cached_property + def tp(self) -> type[Any]: + return self.func() + + +@dataclass(**_slots_frozen) +class _Transform: + func: Callable[[Any], Any] + + +@dataclass(**_slots_frozen) +class _PipelineOr: + left: _Pipeline[Any, Any] + right: _Pipeline[Any, Any] + + +@dataclass(**_slots_frozen) +class _PipelineAnd: + left: _Pipeline[Any, Any] + right: _Pipeline[Any, Any] + + +@dataclass(**_slots_frozen) +class _Eq: + value: Any + + +@dataclass(**_slots_frozen) +class _NotEq: + value: Any + + +@dataclass(**_slots_frozen) +class _In: + values: Container[Any] + + +@dataclass(**_slots_frozen) +class _NotIn: + values: Container[Any] + + +_ConstraintAnnotation = Union[ + annotated_types.Le, + annotated_types.Ge, + annotated_types.Lt, + annotated_types.Gt, + annotated_types.Len, + annotated_types.MultipleOf, + annotated_types.Timezone, + annotated_types.Interval, + annotated_types.Predicate, + # common predicates not included in annotated_types + _Eq, + _NotEq, + _In, + _NotIn, + # regular expressions + Pattern[str], +] + + +@dataclass(**_slots_frozen) +class _Constraint: + constraint: _ConstraintAnnotation + + +_Step = Union[_ValidateAs, _ValidateAsDefer, _Transform, _PipelineOr, _PipelineAnd, _Constraint] + +_InT = TypeVar('_InT') +_OutT = TypeVar('_OutT') +_NewOutT = TypeVar('_NewOutT') + + +class _FieldTypeMarker: + pass + + +# TODO: ultimately, make this public, see https://github.com/pydantic/pydantic/pull/9459#discussion_r1628197626 +# Also, make this frozen eventually, but that doesn't work right now because of the generic base +# Which attempts to modify __orig_base__ and such. +# We could go with a manual freeze, but that seems overkill for now. +@dataclass(**_slots_true) +class _Pipeline(Generic[_InT, _OutT]): + """Abstract representation of a chain of validation, transformation, and parsing steps.""" + + _steps: tuple[_Step, ...] + + def transform( + self, + func: Callable[[_OutT], _NewOutT], + ) -> _Pipeline[_InT, _NewOutT]: + """Transform the output of the previous step. + + If used as the first step in a pipeline, the type of the field is used. + That is, the transformation is applied to after the value is parsed to the field's type. + """ + return _Pipeline[_InT, _NewOutT](self._steps + (_Transform(func),)) + + @overload + def validate_as(self, tp: type[_NewOutT], *, strict: bool = False) -> _Pipeline[_InT, _NewOutT]: ... + + @overload + def validate_as( + self, + tp: ellipsis, # noqa: F821 # TODO: use `_typing_extra.EllipsisType` when we drop Py3.9 + *, + strict: bool = False, + ) -> _Pipeline[_InT, Any]: ... + + # TODO PEP 747: use TypeForm to properly type Annotated aliases (e.g. NewPath, FilePath). + # This fallback accepts any type expression but loses generic type inference. + @overload + def validate_as(self, tp: Any, *, strict: bool = ...) -> _Pipeline[_InT, Any]: ... + + def validate_as(self, tp: type[_NewOutT] | EllipsisType | Any, *, strict: bool = False) -> _Pipeline[_InT, Any]: # type: ignore + """Validate / parse the input into a new type. + + If no type is provided, the type of the field is used. + + Types are parsed in Pydantic's `lax` mode by default, + but you can enable `strict` mode by passing `strict=True`. + """ + if isinstance(tp, EllipsisType): + return _Pipeline[_InT, Any](self._steps + (_ValidateAs(_FieldTypeMarker, strict=strict),)) + return _Pipeline[_InT, _NewOutT](self._steps + (_ValidateAs(tp, strict=strict),)) + + def validate_as_deferred(self, func: Callable[[], type[_NewOutT]]) -> _Pipeline[_InT, _NewOutT]: + """Parse the input into a new type, deferring resolution of the type until the current class + is fully defined. + + This is useful when you need to reference the class in it's own type annotations. + """ + return _Pipeline[_InT, _NewOutT](self._steps + (_ValidateAsDefer(func),)) + + # constraints + @overload + def constrain(self: _Pipeline[_InT, _NewOutGe], constraint: annotated_types.Ge) -> _Pipeline[_InT, _NewOutGe]: ... + + @overload + def constrain(self: _Pipeline[_InT, _NewOutGt], constraint: annotated_types.Gt) -> _Pipeline[_InT, _NewOutGt]: ... + + @overload + def constrain(self: _Pipeline[_InT, _NewOutLe], constraint: annotated_types.Le) -> _Pipeline[_InT, _NewOutLe]: ... + + @overload + def constrain(self: _Pipeline[_InT, _NewOutLt], constraint: annotated_types.Lt) -> _Pipeline[_InT, _NewOutLt]: ... + + @overload + def constrain( + self: _Pipeline[_InT, _NewOutLen], constraint: annotated_types.Len + ) -> _Pipeline[_InT, _NewOutLen]: ... + + @overload + def constrain( + self: _Pipeline[_InT, _NewOutT], constraint: annotated_types.MultipleOf + ) -> _Pipeline[_InT, _NewOutT]: ... + + @overload + def constrain( + self: _Pipeline[_InT, _NewOutDatetime], constraint: annotated_types.Timezone + ) -> _Pipeline[_InT, _NewOutDatetime]: ... + + @overload + def constrain(self: _Pipeline[_InT, _OutT], constraint: annotated_types.Predicate) -> _Pipeline[_InT, _OutT]: ... + + @overload + def constrain( + self: _Pipeline[_InT, _NewOutInterval], constraint: annotated_types.Interval + ) -> _Pipeline[_InT, _NewOutInterval]: ... + + @overload + def constrain(self: _Pipeline[_InT, _OutT], constraint: _Eq) -> _Pipeline[_InT, _OutT]: ... + + @overload + def constrain(self: _Pipeline[_InT, _OutT], constraint: _NotEq) -> _Pipeline[_InT, _OutT]: ... + + @overload + def constrain(self: _Pipeline[_InT, _OutT], constraint: _In) -> _Pipeline[_InT, _OutT]: ... + + @overload + def constrain(self: _Pipeline[_InT, _OutT], constraint: _NotIn) -> _Pipeline[_InT, _OutT]: ... + + @overload + def constrain(self: _Pipeline[_InT, _NewOutT], constraint: Pattern[str]) -> _Pipeline[_InT, _NewOutT]: ... + + def constrain(self, constraint: _ConstraintAnnotation) -> Any: + """Constrain a value to meet a certain condition. + + We support most conditions from `annotated_types`, as well as regular expressions. + + Most of the time you'll be calling a shortcut method like `gt`, `lt`, `len`, etc + so you don't need to call this directly. + """ + return _Pipeline[_InT, _OutT](self._steps + (_Constraint(constraint),)) + + def predicate(self: _Pipeline[_InT, _NewOutT], func: Callable[[_NewOutT], bool]) -> _Pipeline[_InT, _NewOutT]: + """Constrain a value to meet a certain predicate.""" + return self.constrain(annotated_types.Predicate(func)) + + def gt(self: _Pipeline[_InT, _NewOutGt], gt: _NewOutGt) -> _Pipeline[_InT, _NewOutGt]: + """Constrain a value to be greater than a certain value.""" + return self.constrain(annotated_types.Gt(gt)) + + def lt(self: _Pipeline[_InT, _NewOutLt], lt: _NewOutLt) -> _Pipeline[_InT, _NewOutLt]: + """Constrain a value to be less than a certain value.""" + return self.constrain(annotated_types.Lt(lt)) + + def ge(self: _Pipeline[_InT, _NewOutGe], ge: _NewOutGe) -> _Pipeline[_InT, _NewOutGe]: + """Constrain a value to be greater than or equal to a certain value.""" + return self.constrain(annotated_types.Ge(ge)) + + def le(self: _Pipeline[_InT, _NewOutLe], le: _NewOutLe) -> _Pipeline[_InT, _NewOutLe]: + """Constrain a value to be less than or equal to a certain value.""" + return self.constrain(annotated_types.Le(le)) + + def len(self: _Pipeline[_InT, _NewOutLen], min_len: int, max_len: int | None = None) -> _Pipeline[_InT, _NewOutLen]: + """Constrain a value to have a certain length.""" + return self.constrain(annotated_types.Len(min_len, max_len)) + + @overload + def multiple_of(self: _Pipeline[_InT, _NewOutDiv], multiple_of: _NewOutDiv) -> _Pipeline[_InT, _NewOutDiv]: ... + + @overload + def multiple_of(self: _Pipeline[_InT, _NewOutMod], multiple_of: _NewOutMod) -> _Pipeline[_InT, _NewOutMod]: ... + + def multiple_of(self: _Pipeline[_InT, Any], multiple_of: Any) -> _Pipeline[_InT, Any]: + """Constrain a value to be a multiple of a certain number.""" + return self.constrain(annotated_types.MultipleOf(multiple_of)) + + def eq(self: _Pipeline[_InT, _OutT], value: _OutT) -> _Pipeline[_InT, _OutT]: + """Constrain a value to be equal to a certain value.""" + return self.constrain(_Eq(value)) + + def not_eq(self: _Pipeline[_InT, _OutT], value: _OutT) -> _Pipeline[_InT, _OutT]: + """Constrain a value to not be equal to a certain value.""" + return self.constrain(_NotEq(value)) + + def in_(self: _Pipeline[_InT, _OutT], values: Container[_OutT]) -> _Pipeline[_InT, _OutT]: + """Constrain a value to be in a certain set.""" + return self.constrain(_In(values)) + + def not_in(self: _Pipeline[_InT, _OutT], values: Container[_OutT]) -> _Pipeline[_InT, _OutT]: + """Constrain a value to not be in a certain set.""" + return self.constrain(_NotIn(values)) + + # timezone methods + def datetime_tz_naive(self: _Pipeline[_InT, datetime.datetime]) -> _Pipeline[_InT, datetime.datetime]: + return self.constrain(annotated_types.Timezone(None)) + + def datetime_tz_aware(self: _Pipeline[_InT, datetime.datetime]) -> _Pipeline[_InT, datetime.datetime]: + return self.constrain(annotated_types.Timezone(...)) + + def datetime_tz( + self: _Pipeline[_InT, datetime.datetime], tz: datetime.tzinfo + ) -> _Pipeline[_InT, datetime.datetime]: + return self.constrain(annotated_types.Timezone(tz)) # type: ignore + + def datetime_with_tz( + self: _Pipeline[_InT, datetime.datetime], tz: datetime.tzinfo | None + ) -> _Pipeline[_InT, datetime.datetime]: + return self.transform(partial(datetime.datetime.replace, tzinfo=tz)) + + # string methods + def str_lower(self: _Pipeline[_InT, str]) -> _Pipeline[_InT, str]: + return self.transform(str.lower) + + def str_upper(self: _Pipeline[_InT, str]) -> _Pipeline[_InT, str]: + return self.transform(str.upper) + + def str_title(self: _Pipeline[_InT, str]) -> _Pipeline[_InT, str]: + return self.transform(str.title) + + def str_strip(self: _Pipeline[_InT, str]) -> _Pipeline[_InT, str]: + return self.transform(str.strip) + + def str_pattern(self: _Pipeline[_InT, str], pattern: str) -> _Pipeline[_InT, str]: + return self.constrain(re.compile(pattern)) + + def str_contains(self: _Pipeline[_InT, str], substring: str) -> _Pipeline[_InT, str]: + return self.predicate(lambda v: substring in v) + + def str_starts_with(self: _Pipeline[_InT, str], prefix: str) -> _Pipeline[_InT, str]: + return self.predicate(lambda v: v.startswith(prefix)) + + def str_ends_with(self: _Pipeline[_InT, str], suffix: str) -> _Pipeline[_InT, str]: + return self.predicate(lambda v: v.endswith(suffix)) + + # operators + def otherwise(self, other: _Pipeline[_OtherIn, _OtherOut]) -> _Pipeline[_InT | _OtherIn, _OutT | _OtherOut]: + """Combine two validation chains, returning the result of the first chain if it succeeds, and the second chain if it fails.""" + return _Pipeline((_PipelineOr(self, other),)) + + __or__ = otherwise + + def then(self, other: _Pipeline[_OutT, _OtherOut]) -> _Pipeline[_InT, _OtherOut]: + """Pipe the result of one validation chain into another.""" + return _Pipeline((_PipelineAnd(self, other),)) + + __and__ = then + + def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaHandler) -> cs.CoreSchema: + queue = deque(self._steps) + + s = None + + while queue: + step = queue.popleft() + s = _apply_step(step, s, handler, source_type) + + s = s or cs.any_schema() + return s + + def __supports_type__(self, _: _OutT) -> bool: + raise NotImplementedError + + +validate_as = _Pipeline[Any, Any](()).validate_as +validate_as_deferred = _Pipeline[Any, Any](()).validate_as_deferred +transform = _Pipeline[Any, Any]((_ValidateAs(_FieldTypeMarker),)).transform + + +def _check_func( + func: Callable[[Any], bool], predicate_err: str | Callable[[], str], s: cs.CoreSchema | None +) -> cs.CoreSchema: + def handler(v: Any) -> Any: + if func(v): + return v + raise ValueError(f'Expected {predicate_err if isinstance(predicate_err, str) else predicate_err()}') + + if s is None: + return cs.no_info_plain_validator_function(handler) + else: + return cs.no_info_after_validator_function(handler, s) + + +def _apply_step(step: _Step, s: cs.CoreSchema | None, handler: GetCoreSchemaHandler, source_type: Any) -> cs.CoreSchema: + if isinstance(step, _ValidateAs): + s = _apply_parse(s, step.tp, step.strict, handler, source_type) + elif isinstance(step, _ValidateAsDefer): + s = _apply_parse(s, step.tp, False, handler, source_type) + elif isinstance(step, _Transform): + s = _apply_transform(s, step.func, handler) + elif isinstance(step, _Constraint): + s = _apply_constraint(s, step.constraint) + elif isinstance(step, _PipelineOr): + s = cs.union_schema([handler(step.left), handler(step.right)]) + else: + assert isinstance(step, _PipelineAnd) + s = cs.chain_schema([handler(step.left), handler(step.right)]) + return s + + +def _apply_parse( + s: cs.CoreSchema | None, + tp: type[Any], + strict: bool, + handler: GetCoreSchemaHandler, + source_type: Any, +) -> cs.CoreSchema: + if tp is _FieldTypeMarker: + return cs.chain_schema([s, handler(source_type)]) if s else handler(source_type) + + if strict: + tp = Annotated[tp, Strict()] # type: ignore + + if s and s['type'] == 'any': + return handler(tp) + else: + return cs.chain_schema([s, handler(tp)]) if s else handler(tp) + + +def _apply_transform( + s: cs.CoreSchema | None, func: Callable[[Any], Any], handler: GetCoreSchemaHandler +) -> cs.CoreSchema: + if s is None: + return cs.no_info_plain_validator_function(func) + + if s['type'] == 'str': + if func is str.strip: + s = s.copy() + s['strip_whitespace'] = True + return s + elif func is str.lower: + s = s.copy() + s['to_lower'] = True + return s + elif func is str.upper: + s = s.copy() + s['to_upper'] = True + return s + + return cs.no_info_after_validator_function(func, s) + + +def _apply_constraint( # noqa: C901 + s: cs.CoreSchema | None, constraint: _ConstraintAnnotation +) -> cs.CoreSchema: + """Apply a single constraint to a schema.""" + if isinstance(constraint, annotated_types.Gt): + gt = constraint.gt + if s and s['type'] in {'int', 'float', 'decimal'}: + s = s.copy() + if s['type'] == 'int' and isinstance(gt, int): + s['gt'] = gt + elif s['type'] == 'float' and isinstance(gt, float): + s['gt'] = gt + elif s['type'] == 'decimal' and isinstance(gt, Decimal): + s['gt'] = gt + else: + + def check_gt(v: Any) -> bool: + return v > gt + + s = _check_func(check_gt, f'> {gt}', s) + elif isinstance(constraint, annotated_types.Ge): + ge = constraint.ge + if s and s['type'] in {'int', 'float', 'decimal'}: + s = s.copy() + if s['type'] == 'int' and isinstance(ge, int): + s['ge'] = ge + elif s['type'] == 'float' and isinstance(ge, float): + s['ge'] = ge + elif s['type'] == 'decimal' and isinstance(ge, Decimal): + s['ge'] = ge + + def check_ge(v: Any) -> bool: + return v >= ge + + s = _check_func(check_ge, f'>= {ge}', s) + elif isinstance(constraint, annotated_types.Lt): + lt = constraint.lt + if s and s['type'] in {'int', 'float', 'decimal'}: + s = s.copy() + if s['type'] == 'int' and isinstance(lt, int): + s['lt'] = lt + elif s['type'] == 'float' and isinstance(lt, float): + s['lt'] = lt + elif s['type'] == 'decimal' and isinstance(lt, Decimal): + s['lt'] = lt + + def check_lt(v: Any) -> bool: + return v < lt + + s = _check_func(check_lt, f'< {lt}', s) + elif isinstance(constraint, annotated_types.Le): + le = constraint.le + if s and s['type'] in {'int', 'float', 'decimal'}: + s = s.copy() + if s['type'] == 'int' and isinstance(le, int): + s['le'] = le + elif s['type'] == 'float' and isinstance(le, float): + s['le'] = le + elif s['type'] == 'decimal' and isinstance(le, Decimal): + s['le'] = le + + def check_le(v: Any) -> bool: + return v <= le + + s = _check_func(check_le, f'<= {le}', s) + elif isinstance(constraint, annotated_types.Len): + min_len = constraint.min_length + max_len = constraint.max_length + + if s and s['type'] in {'str', 'list', 'tuple', 'set', 'frozenset', 'dict'}: + assert ( + s['type'] == 'str' + or s['type'] == 'list' + or s['type'] == 'tuple' + or s['type'] == 'set' + or s['type'] == 'dict' + or s['type'] == 'frozenset' + ) + s = s.copy() + if min_len != 0: + s['min_length'] = min_len + if max_len is not None: + s['max_length'] = max_len + + def check_len(v: Any) -> bool: + if max_len is not None: + return (min_len <= len(v)) and (len(v) <= max_len) + return min_len <= len(v) + + s = _check_func(check_len, f'length >= {min_len} and length <= {max_len}', s) + elif isinstance(constraint, annotated_types.MultipleOf): + multiple_of = constraint.multiple_of + if s and s['type'] in {'int', 'float', 'decimal'}: + s = s.copy() + if s['type'] == 'int' and isinstance(multiple_of, int): + s['multiple_of'] = multiple_of + elif s['type'] == 'float' and isinstance(multiple_of, float): + s['multiple_of'] = multiple_of + elif s['type'] == 'decimal' and isinstance(multiple_of, Decimal): + s['multiple_of'] = multiple_of + + def check_multiple_of(v: Any) -> bool: + return v % multiple_of == 0 + + s = _check_func(check_multiple_of, f'% {multiple_of} == 0', s) + elif isinstance(constraint, annotated_types.Timezone): + tz = constraint.tz + + if tz is ...: + if s and s['type'] == 'datetime': + s = s.copy() + s['tz_constraint'] = 'aware' + else: + + def check_tz_aware(v: object) -> bool: + assert isinstance(v, datetime.datetime) + return v.tzinfo is not None + + s = _check_func(check_tz_aware, 'timezone aware', s) + elif tz is None: + if s and s['type'] == 'datetime': + s = s.copy() + s['tz_constraint'] = 'naive' + else: + + def check_tz_naive(v: object) -> bool: + assert isinstance(v, datetime.datetime) + return v.tzinfo is None + + s = _check_func(check_tz_naive, 'timezone naive', s) + else: + raise NotImplementedError('Constraining to a specific timezone is not yet supported') + elif isinstance(constraint, annotated_types.Interval): + if constraint.ge: + s = _apply_constraint(s, annotated_types.Ge(constraint.ge)) + if constraint.gt: + s = _apply_constraint(s, annotated_types.Gt(constraint.gt)) + if constraint.le: + s = _apply_constraint(s, annotated_types.Le(constraint.le)) + if constraint.lt: + s = _apply_constraint(s, annotated_types.Lt(constraint.lt)) + assert s is not None + elif isinstance(constraint, annotated_types.Predicate): + func = constraint.func + # Same logic as in `_known_annotated_metadata.apply_known_metadata()`: + predicate_name = f'{func.__qualname__!r} ' if hasattr(func, '__qualname__') else '' + + def predicate_func(v: Any) -> Any: + if not func(v): + raise PydanticCustomError( + 'predicate_failed', + f'Predicate {predicate_name}failed', # pyright: ignore[reportArgumentType] + ) + return v + + if s is None: + s = cs.no_info_plain_validator_function(predicate_func) + else: + s = cs.no_info_after_validator_function(predicate_func, s) + elif isinstance(constraint, _NotEq): + value = constraint.value + + def check_not_eq(v: Any) -> bool: + return operator.__ne__(v, value) + + s = _check_func(check_not_eq, f'!= {value}', s) + elif isinstance(constraint, _Eq): + value = constraint.value + + def check_eq(v: Any) -> bool: + return operator.__eq__(v, value) + + s = _check_func(check_eq, f'== {value}', s) + elif isinstance(constraint, _In): + values = constraint.values + + def check_in(v: Any) -> bool: + return operator.__contains__(values, v) + + s = _check_func(check_in, f'in {values}', s) + elif isinstance(constraint, _NotIn): + values = constraint.values + + def check_not_in(v: Any) -> bool: + return operator.__not__(operator.__contains__(values, v)) + + s = _check_func(check_not_in, f'not in {values}', s) + else: + assert isinstance(constraint, Pattern) + if s and s['type'] == 'str': + s = s.copy() + s['pattern'] = constraint.pattern + else: + + def check_pattern(v: object) -> bool: + assert isinstance(v, str) + return constraint.match(v) is not None + + s = _check_func(check_pattern, f'~ {constraint.pattern}', s) + return s + + +class _SupportsRange(annotated_types.SupportsLe, annotated_types.SupportsGe, Protocol): + pass + + +class _SupportsLen(Protocol): + def __len__(self) -> int: ... + + +_NewOutGt = TypeVar('_NewOutGt', bound=annotated_types.SupportsGt) +_NewOutGe = TypeVar('_NewOutGe', bound=annotated_types.SupportsGe) +_NewOutLt = TypeVar('_NewOutLt', bound=annotated_types.SupportsLt) +_NewOutLe = TypeVar('_NewOutLe', bound=annotated_types.SupportsLe) +_NewOutLen = TypeVar('_NewOutLen', bound=_SupportsLen) +_NewOutDiv = TypeVar('_NewOutDiv', bound=annotated_types.SupportsDiv) +_NewOutMod = TypeVar('_NewOutMod', bound=annotated_types.SupportsMod) +_NewOutDatetime = TypeVar('_NewOutDatetime', bound=datetime.datetime) +_NewOutInterval = TypeVar('_NewOutInterval', bound=_SupportsRange) +_OtherIn = TypeVar('_OtherIn') +_OtherOut = TypeVar('_OtherOut') diff --git a/source/target-vendors/cp312/pydantic/fields.py b/source/target-vendors/cp312/pydantic/fields.py new file mode 100644 index 0000000..ede966f --- /dev/null +++ b/source/target-vendors/cp312/pydantic/fields.py @@ -0,0 +1,1892 @@ +"""Defining fields on models.""" + +from __future__ import annotations as _annotations + +import dataclasses +import inspect +import re +import sys +from collections.abc import Callable, Mapping +from copy import copy +from dataclasses import Field as DataclassField +from functools import cached_property +from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Literal, TypeVar, final, overload +from warnings import warn + +import annotated_types +import typing_extensions +from pydantic_core import MISSING, PydanticUndefined +from typing_extensions import Self, TypeAlias, TypedDict, Unpack, deprecated +from typing_inspection import typing_objects +from typing_inspection.introspection import UNKNOWN, AnnotationSource, ForbiddenQualifier, Qualifier, inspect_annotation + +from . import types +from ._internal import _decorators, _fields, _generics, _internal_dataclass, _repr, _typing_extra, _utils +from ._internal._namespace_utils import GlobalsNamespace, MappingNamespace +from .aliases import AliasChoices, AliasGenerator, AliasPath +from .config import JsonDict +from .errors import PydanticForbiddenQualifier, PydanticUserError +from .json_schema import PydanticJsonSchemaWarning +from .warnings import PydanticDeprecatedSince20 + +if TYPE_CHECKING: + from ._internal._config import ConfigWrapper + from ._internal._repr import ReprArgs + + +__all__ = 'Field', 'FieldInfo', 'PrivateAttr', 'computed_field' + + +_Unset: Any = PydanticUndefined + +if sys.version_info >= (3, 13): + import warnings + + Deprecated: TypeAlias = warnings.deprecated | deprecated +else: + Deprecated: TypeAlias = deprecated + + +class _FromFieldInfoInputs(TypedDict, total=False): + """This class exists solely to add type checking for the `**kwargs` in `FieldInfo.from_field`.""" + + # TODO PEP 747: use TypeForm: + annotation: type[Any] | None + default_factory: Callable[[], Any] | Callable[[dict[str, Any]], Any] | None + alias: str | None + alias_priority: int | None + validation_alias: str | AliasPath | AliasChoices | None + serialization_alias: str | None + title: str | None + field_title_generator: Callable[[str, FieldInfo], str] | None + description: str | None + examples: list[Any] | None + exclude: bool | None + exclude_if: Callable[[Any], bool] | None + gt: annotated_types.SupportsGt | None + ge: annotated_types.SupportsGe | None + lt: annotated_types.SupportsLt | None + le: annotated_types.SupportsLe | None + multiple_of: float | None + strict: bool | None + min_length: int | None + max_length: int | None + pattern: str | re.Pattern[str] | None + allow_inf_nan: bool | None + max_digits: int | None + decimal_places: int | None + union_mode: Literal['smart', 'left_to_right'] | None + discriminator: str | types.Discriminator | None + deprecated: Deprecated | str | bool | None + json_schema_extra: JsonDict | Callable[[JsonDict], None] | None + frozen: bool | None + validate_default: bool | None + repr: bool + init: bool | None + init_var: bool | None + kw_only: bool | None + coerce_numbers_to_str: bool | None + fail_fast: bool | None + + +class _FieldInfoInputs(_FromFieldInfoInputs, total=False): + """This class exists solely to add type checking for the `**kwargs` in `FieldInfo.__init__`.""" + + default: Any + + +class _FieldInfoAsDict(TypedDict, closed=True): + # TODO PEP 747: use TypeForm: + annotation: Any + metadata: list[Any] + attributes: dict[str, Any] + + +@final +class FieldInfo(_repr.Representation): + """This class holds information about a field. + + `FieldInfo` is used for any field definition regardless of whether the [`Field()`][pydantic.fields.Field] + function is explicitly used. + + !!! warning + The `FieldInfo` class is meant to expose information about a field in a Pydantic model or dataclass. + `FieldInfo` instances shouldn't be instantiated directly, nor mutated. + + If you need to derive a new model from another one and are willing to alter `FieldInfo` instances, + refer to this [dynamic model example](../examples/dynamic_models.md). + + Attributes: + annotation: The type annotation of the field. + default: The default value of the field. + default_factory: A callable to generate the default value. The callable can either take 0 arguments + (in which case it is called as is) or a single argument containing the already validated data. + alias: The alias name of the field. + alias_priority: The priority of the field's alias. + validation_alias: The validation alias of the field. + serialization_alias: The serialization alias of the field. + title: The title of the field. + field_title_generator: A callable that takes a field name and returns title for it. + description: The description of the field. + examples: List of examples of the field. + exclude: Whether to exclude the field from the model serialization. + exclude_if: A callable that determines whether to exclude a field during serialization based on its value. + discriminator: Field name or Discriminator for discriminating the type in a tagged union. + deprecated: A deprecation message, an instance of `warnings.deprecated` or the `typing_extensions.deprecated` backport, + or a boolean. If `True`, a default deprecation message will be emitted when accessing the field. + json_schema_extra: A dict or callable to provide extra JSON schema properties. + frozen: Whether the field is frozen. + validate_default: Whether to validate the default value of the field. + repr: Whether to include the field in representation of the model. + init: Whether the field should be included in the constructor of the dataclass. + init_var: Whether the field should _only_ be included in the constructor of the dataclass, and not stored. + kw_only: Whether the field should be a keyword-only argument in the constructor of the dataclass. + metadata: The metadata list. Contains all the data that isn't expressed as direct `FieldInfo` attributes, including: + + * Type-specific constraints, such as `gt` or `min_length` (these are converted to metadata classes such as `annotated_types.Gt`). + * Any other arbitrary object used within [`Annotated`][typing.Annotated] metadata + (e.g. [custom types handlers](../concepts/types.md#as-an-annotation) or any object not recognized by Pydantic). + """ + + # TODO PEP 747: use TypeForm: + annotation: type[Any] | None + default: Any + default_factory: Callable[[], Any] | Callable[[dict[str, Any]], Any] | None + alias: str | None + alias_priority: int | None + validation_alias: str | AliasPath | AliasChoices | None + serialization_alias: str | None + title: str | None + field_title_generator: Callable[[str, FieldInfo], str] | None + description: str | None + examples: list[Any] | None + exclude: bool | None + exclude_if: Callable[[Any], bool] | None + discriminator: str | types.Discriminator | None + deprecated: Deprecated | str | bool | None + json_schema_extra: JsonDict | Callable[[JsonDict], None] | None + frozen: bool | None + validate_default: bool | None + repr: bool + init: bool | None + init_var: bool | None + kw_only: bool | None + metadata: list[Any] + + __slots__ = ( + 'annotation', + 'default', + 'default_factory', + 'alias', + 'alias_priority', + 'validation_alias', + 'serialization_alias', + 'title', + 'field_title_generator', + 'description', + 'examples', + 'exclude', + 'exclude_if', + 'discriminator', + 'deprecated', + 'json_schema_extra', + 'frozen', + 'validate_default', + 'repr', + 'init', + 'init_var', + 'kw_only', + 'metadata', + '_attributes_set', + '_qualifiers', + '_complete', + '_original_assignment', + '_original_annotation', + '_final', + ) + + # used to convert kwargs to metadata/constraints, + # None has a special meaning - these items are collected into a `PydanticGeneralMetadata` + metadata_lookup: ClassVar[dict[str, Callable[[Any], Any] | None]] = { + 'strict': types.Strict, + 'gt': annotated_types.Gt, + 'ge': annotated_types.Ge, + 'lt': annotated_types.Lt, + 'le': annotated_types.Le, + 'multiple_of': annotated_types.MultipleOf, + 'min_length': annotated_types.MinLen, + 'max_length': annotated_types.MaxLen, + 'pattern': None, + 'allow_inf_nan': None, + 'max_digits': None, + 'decimal_places': None, + 'union_mode': None, + 'coerce_numbers_to_str': None, + 'fail_fast': types.FailFast, + } + + def __init__(self, **kwargs: Unpack[_FieldInfoInputs]) -> None: + """This class should generally not be initialized directly; instead, use the `pydantic.fields.Field` function + or one of the constructor classmethods. + + See the signature of `pydantic.fields.Field` for more details about the expected arguments. + """ + # Tracking the explicitly set attributes is necessary to correctly merge `Field()` functions + # (e.g. with `Annotated[int, Field(alias='a'), Field(alias=None)]`, even though `None` is the default value, + # we need to track that `alias=None` was explicitly set): + self._attributes_set = {k: v for k, v in kwargs.items() if v is not _Unset and k not in self.metadata_lookup} + kwargs = {k: _DefaultValues.get(k) if v is _Unset else v for k, v in kwargs.items()} # type: ignore + self.annotation = kwargs.get('annotation') + + # Note: in theory, the second `pop()` arguments are not required below, as defaults are already set from `_DefaultsValues`. + default = kwargs.pop('default', PydanticUndefined) + if default is Ellipsis: + self.default = PydanticUndefined + self._attributes_set.pop('default', None) + else: + self.default = default + + self.default_factory = kwargs.pop('default_factory', None) + + if self.default is not PydanticUndefined and self.default_factory is not None: + raise TypeError('cannot specify both default and default_factory') + + self.alias = kwargs.pop('alias', None) + self.validation_alias = kwargs.pop('validation_alias', None) + self.serialization_alias = kwargs.pop('serialization_alias', None) + alias_is_set = any(alias is not None for alias in (self.alias, self.validation_alias, self.serialization_alias)) + self.alias_priority = kwargs.pop('alias_priority', None) or 2 if alias_is_set else None + self.title = kwargs.pop('title', None) + self.field_title_generator = kwargs.pop('field_title_generator', None) + self.description = kwargs.pop('description', None) + self.examples = kwargs.pop('examples', None) + self.exclude = kwargs.pop('exclude', None) + self.exclude_if = kwargs.pop('exclude_if', None) + self.discriminator = kwargs.pop('discriminator', None) + # For compatibility with FastAPI<=0.110.0, we preserve the existing value if it is not overridden + self.deprecated = kwargs.pop('deprecated', getattr(self, 'deprecated', None)) + self.repr = kwargs.pop('repr', True) + self.json_schema_extra = kwargs.pop('json_schema_extra', None) + self.validate_default = kwargs.pop('validate_default', None) + self.frozen = kwargs.pop('frozen', None) + # currently only used on dataclasses + self.init = kwargs.pop('init', None) + self.init_var = kwargs.pop('init_var', None) + self.kw_only = kwargs.pop('kw_only', None) + + self.metadata = self._collect_metadata(kwargs) # type: ignore + + # Private attributes: + self._qualifiers: set[Qualifier] = set() + # Used to rebuild FieldInfo instances: + self._complete = True + self._original_annotation: Any = PydanticUndefined + self._original_assignment: Any = PydanticUndefined + # Used to track whether the `FieldInfo` instance represents the data about a field (and is exposed in `model_fields`/`__pydantic_fields__`), + # or if it is the result of the `Field()` function being used as metadata in an `Annotated` type/as an assignment + # (not an ideal pattern, see https://github.com/pydantic/pydantic/issues/11122): + self._final = False + + @staticmethod + def from_field(default: Any = PydanticUndefined, **kwargs: Unpack[_FromFieldInfoInputs]) -> FieldInfo: + """Create a new `FieldInfo` object with the `Field` function. + + Args: + default: The default value for the field. Defaults to Undefined. + **kwargs: Additional arguments dictionary. + + Raises: + TypeError: If 'annotation' is passed as a keyword argument. + + Returns: + A new FieldInfo object with the given parameters. + + Example: + This is how you can create a field with default value like this: + + ```python + import pydantic + + class MyModel(pydantic.BaseModel): + foo: int = pydantic.Field(4) + ``` + """ + if 'annotation' in kwargs: + raise TypeError('"annotation" is not permitted as a Field keyword argument') + return FieldInfo(default=default, **kwargs) + + @staticmethod + def from_annotation(annotation: type[Any], *, _source: AnnotationSource = AnnotationSource.ANY) -> FieldInfo: + """Creates a `FieldInfo` instance from a bare annotation. + + This function is used internally to create a `FieldInfo` from a bare annotation like this: + + ```python + import pydantic + + class MyModel(pydantic.BaseModel): + foo: int # <-- like this + ``` + + We also account for the case where the annotation can be an instance of `Annotated` and where + one of the (not first) arguments in `Annotated` is an instance of `FieldInfo`, e.g.: + + ```python + from typing import Annotated + + import annotated_types + + import pydantic + + class MyModel(pydantic.BaseModel): + foo: Annotated[int, annotated_types.Gt(42)] + bar: Annotated[int, pydantic.Field(gt=42)] + ``` + + Args: + annotation: An annotation object. + + Returns: + An instance of the field metadata. + """ + try: + inspected_ann = inspect_annotation( + annotation, + annotation_source=_source, + unpack_type_aliases='skip', + ) + except ForbiddenQualifier as e: + raise PydanticForbiddenQualifier(e.qualifier, annotation) + + # TODO check for classvar and error? + + # No assigned value, this happens when using a bare `Final` qualifier (also for other + # qualifiers, but they shouldn't appear here). In this case we infer the type as `Any` + # because we don't have any assigned value. + type_expr: Any = Any if inspected_ann.type is UNKNOWN else inspected_ann.type + final = 'final' in inspected_ann.qualifiers + metadata = inspected_ann.metadata + + attr_overrides = {'annotation': type_expr} + if final: + attr_overrides['frozen'] = True + field_info = FieldInfo._construct(metadata, **attr_overrides) + field_info._qualifiers = inspected_ann.qualifiers + field_info._final = True + return field_info + + @staticmethod + def from_annotated_attribute( + annotation: type[Any], default: Any, *, _source: AnnotationSource = AnnotationSource.ANY + ) -> FieldInfo: + """Create `FieldInfo` from an annotation with a default value. + + This is used in cases like the following: + + ```python + from typing import Annotated + + import annotated_types + + import pydantic + + class MyModel(pydantic.BaseModel): + foo: int = 4 # <-- like this + bar: Annotated[int, annotated_types.Gt(4)] = 4 # <-- or this + spam: Annotated[int, pydantic.Field(gt=4)] = 4 # <-- or this + ``` + + Args: + annotation: The type annotation of the field. + default: The default value of the field. + + Returns: + A field object with the passed values. + """ + if annotation is not MISSING and annotation is default: + raise PydanticUserError( + 'Error when building FieldInfo from annotated attribute. ' + "Make sure you don't have any field name clashing with a type annotation.", + code='unevaluable-type-annotation', + ) + + try: + inspected_ann = inspect_annotation( + annotation, + annotation_source=_source, + unpack_type_aliases='skip', + ) + except ForbiddenQualifier as e: + raise PydanticForbiddenQualifier(e.qualifier, annotation) + + # TODO check for classvar and error? + + # TODO infer from the default, this can be done in v3 once we treat final fields with + # a default as proper fields and not class variables: + type_expr: Any = Any if inspected_ann.type is UNKNOWN else inspected_ann.type + final = 'final' in inspected_ann.qualifiers + metadata = inspected_ann.metadata + + # HACK 1: the order in which the metadata is merged is inconsistent; we need to prepend + # metadata from the assignment at the beginning of the metadata. Changing this is only + # possible in v3 (at least). See https://github.com/pydantic/pydantic/issues/10507 + prepend_metadata: list[Any] | None = None + attr_overrides = {'annotation': type_expr} + if final: + attr_overrides['frozen'] = True + + # HACK 2: FastAPI is subclassing `FieldInfo` and historically expected the actual + # instance's type to be preserved when constructing new models with its subclasses as assignments. + # This code is never reached by Pydantic itself, and in an ideal world this shouldn't be necessary. + if not metadata and isinstance(default, FieldInfo) and type(default) is not FieldInfo: + field_info = default._copy() + field_info._attributes_set.update(attr_overrides) + for k, v in attr_overrides.items(): + setattr(field_info, k, v) + return field_info + + if isinstance(default, FieldInfo): + default_copy = default._copy() # Copy unnecessary when we remove HACK 1. + prepend_metadata = default_copy.metadata + default_copy.metadata = [] + metadata = metadata + [default_copy] + if 'init_var' in inspected_ann.qualifiers: + # Only relevant for dataclasses, when `f: InitVar[] = Field(...)` + # is used: + attr_overrides['init_var'] = True + elif isinstance(default, dataclasses.Field): + from_field = FieldInfo._from_dataclass_field(default) + prepend_metadata = from_field.metadata # Unnecessary when we remove HACK 1. + from_field.metadata = [] + metadata = metadata + [from_field] + if 'init_var' in inspected_ann.qualifiers: + attr_overrides['init_var'] = True + if (init := getattr(default, 'init', None)) is not None: + attr_overrides['init'] = init + if (kw_only := getattr(default, 'kw_only', None)) is not None: + attr_overrides['kw_only'] = kw_only + else: + # `default` is the actual default value + attr_overrides['default'] = default + + field_info = FieldInfo._construct( + prepend_metadata + metadata if prepend_metadata is not None else metadata, **attr_overrides + ) + field_info._qualifiers = inspected_ann.qualifiers + field_info._final = True + return field_info + + @classmethod + def _construct(cls, metadata: list[Any], **attr_overrides: Any) -> Self: + """Construct the final `FieldInfo` instance, by merging the possibly existing `FieldInfo` instances from the metadata. + + With the following example: + + ```python {test="skip" lint="skip"} + class Model(BaseModel): + f: Annotated[int, Gt(1), Field(description='desc', lt=2)] + ``` + + `metadata` refers to the metadata elements of the `Annotated` form. This metadata is iterated over from left to right: + + - If the element is a `Field()` function (which is itself a `FieldInfo` instance), the field attributes (such as + `description`) are saved to be set on the final `FieldInfo` instance. + On the other hand, some kwargs (such as `lt`) are stored as `metadata` (see `FieldInfo.__init__()`, calling + `FieldInfo._collect_metadata()`). In this case, the final metadata list is extended with the one from this instance. + - Else, the element is considered as a single metadata object, and is appended to the final metadata list. + + Args: + metadata: The list of metadata elements to merge together. If the `FieldInfo` instance to be constructed is for + a field with an assigned `Field()`, this `Field()` assignment should be added as the last element of the + provided metadata. + **attr_overrides: Extra attributes that should be set on the final merged `FieldInfo` instance. + + Returns: + The final merged `FieldInfo` instance. + """ + merged_metadata: list[Any] = [] + merged_kwargs: dict[str, Any] = {} + + for meta in metadata: + if isinstance(meta, FieldInfo): + merged_metadata.extend(meta.metadata) + + new_js_extra: JsonDict | None = None + current_js_extra = meta.json_schema_extra + if current_js_extra is not None and 'json_schema_extra' in merged_kwargs: + # We need to merge `json_schema_extra`'s: + existing_js_extra = merged_kwargs['json_schema_extra'] + if isinstance(existing_js_extra, dict): + if isinstance(current_js_extra, dict): + new_js_extra = { + **existing_js_extra, + **current_js_extra, + } + elif callable(current_js_extra): + warn( + 'Composing `dict` and `callable` type `json_schema_extra` is not supported. ' + 'The `callable` type is being ignored. ' + "If you'd like support for this behavior, please open an issue on pydantic.", + UserWarning, + ) + elif callable(existing_js_extra) and isinstance(current_js_extra, dict): + warn( + 'Composing `dict` and `callable` type `json_schema_extra` is not supported. ' + 'The `callable` type is being ignored. ' + "If you'd like support for this behavior, please open an issue on pydantic.", + UserWarning, + ) + + # HACK: It is common for users to define "make model partial" (or similar) utilities, that + # convert all model fields to be optional (i.e. have a default value). To do so, they mutate + # each `FieldInfo` instance from `model_fields` to set a `default`, and use `create_model()` + # with `Annotated[ | None, mutated_field_info]`` as an annotation. However, such + # mutations (by doing simple assignments) are only accidentally working, because we also + # need to track attributes explicitly set in `_attributes_set` (relying on default values for + # each attribute is *not* enough, for instance with `Annotated[int, Field(alias='a'), Field(alias=None)]` + # the resulting `FieldInfo` should have `alias=None`). + # To mitigate this, we add a special case when a "final" `FieldInfo` instance (that is an instance coming + # from `model_fields`) is used in annotated metadata (or assignment). In this case, we assume *all* attributes + # were explicitly set, and as such we use all of them (and this will correctly pick up the mutations). + # In theory, this shouldn't really be supported, you are only supposed to use the `Field()` function, not + # a `FieldInfo` instance directly (granted, `Field()` returns a `FieldInfo`, see + # https://github.com/pydantic/pydantic/issues/11122): + if meta._final: + merged_kwargs.update({attr: getattr(meta, attr) for attr in _Attrs}) + else: + merged_kwargs.update(meta._attributes_set) + + if new_js_extra is not None: + merged_kwargs['json_schema_extra'] = new_js_extra + elif typing_objects.is_deprecated(meta): + merged_kwargs['deprecated'] = meta + else: + merged_metadata.append(meta) + + merged_kwargs.update(attr_overrides) + merged_field_info = cls(**merged_kwargs) + merged_field_info.metadata = merged_metadata + return merged_field_info + + @staticmethod + @typing_extensions.deprecated( + "The 'merge_field_infos()' method is deprecated and will be removed in a future version. " + 'If you relied on this method, please open an issue in the Pydantic issue tracker.', + category=None, + ) + def merge_field_infos(*field_infos: FieldInfo, **overrides: Any) -> FieldInfo: + """Merge `FieldInfo` instances keeping only explicitly set attributes. + + Later `FieldInfo` instances override earlier ones. + + Returns: + FieldInfo: A merged FieldInfo instance. + """ + if len(field_infos) == 1: + # No merging necessary, but we still need to make a copy and apply the overrides + field_info = field_infos[0]._copy() + field_info._attributes_set.update(overrides) + + default_override = overrides.pop('default', PydanticUndefined) + if default_override is Ellipsis: + default_override = PydanticUndefined + if default_override is not PydanticUndefined: + field_info.default = default_override + + for k, v in overrides.items(): + setattr(field_info, k, v) + return field_info # type: ignore + + merged_field_info_kwargs: dict[str, Any] = {} + metadata = {} + for field_info in field_infos: + attributes_set = field_info._attributes_set.copy() + + try: + json_schema_extra = attributes_set.pop('json_schema_extra') + existing_json_schema_extra = merged_field_info_kwargs.get('json_schema_extra') + + if existing_json_schema_extra is None: + merged_field_info_kwargs['json_schema_extra'] = json_schema_extra + if isinstance(existing_json_schema_extra, dict): + if isinstance(json_schema_extra, dict): + merged_field_info_kwargs['json_schema_extra'] = { + **existing_json_schema_extra, + **json_schema_extra, + } + if callable(json_schema_extra): + warn( + 'Composing `dict` and `callable` type `json_schema_extra` is not supported.' + 'The `callable` type is being ignored.' + "If you'd like support for this behavior, please open an issue on pydantic.", + PydanticJsonSchemaWarning, + ) + elif callable(json_schema_extra): + # if ever there's a case of a callable, we'll just keep the last json schema extra spec + merged_field_info_kwargs['json_schema_extra'] = json_schema_extra + except KeyError: + pass + + # later FieldInfo instances override everything except json_schema_extra from earlier FieldInfo instances + merged_field_info_kwargs.update(attributes_set) + + for x in field_info.metadata: + if not isinstance(x, FieldInfo): + metadata[type(x)] = x + + merged_field_info_kwargs.update(overrides) + field_info = FieldInfo(**merged_field_info_kwargs) + field_info.metadata = list(metadata.values()) + return field_info + + @staticmethod + def _from_dataclass_field(dc_field: DataclassField[Any]) -> FieldInfo: + """Return a new `FieldInfo` instance from a `dataclasses.Field` instance. + + Args: + dc_field: The `dataclasses.Field` instance to convert. + + Returns: + The corresponding `FieldInfo` instance. + + Raises: + TypeError: If any of the `FieldInfo` kwargs does not match the `dataclass.Field` kwargs. + """ + default = dc_field.default + if default is dataclasses.MISSING: + default = _Unset + + if dc_field.default_factory is dataclasses.MISSING: + default_factory = _Unset + else: + default_factory = dc_field.default_factory + + # use the `Field` function so in correct kwargs raise the correct `TypeError` + dc_field_metadata = {k: v for k, v in dc_field.metadata.items() if k in _FIELD_ARG_NAMES} + if sys.version_info >= (3, 14) and dc_field.doc is not None: + dc_field_metadata['description'] = dc_field.doc + return Field(default=default, default_factory=default_factory, repr=dc_field.repr, **dc_field_metadata) # pyright: ignore[reportCallIssue] + + @staticmethod + def _collect_metadata(kwargs: dict[str, Any]) -> list[Any]: + """Collect annotations from kwargs. + + Args: + kwargs: Keyword arguments passed to the function. + + Returns: + A list of metadata objects - a combination of `annotated_types.BaseMetadata` and + `PydanticMetadata`. + """ + metadata: list[Any] = [] + general_metadata = {} + for key, value in list(kwargs.items()): + try: + marker = FieldInfo.metadata_lookup[key] + except KeyError: + continue + + del kwargs[key] + if value is not None: + if marker is None: + general_metadata[key] = value + else: + metadata.append(marker(value)) + if general_metadata: + metadata.append(_fields.pydantic_general_metadata(**general_metadata)) + return metadata + + @property + def deprecation_message(self) -> str | None: + """The deprecation message to be emitted, or `None` if not set.""" + if self.deprecated is None: + return None + if isinstance(self.deprecated, bool): + return 'deprecated' if self.deprecated else None + return self.deprecated if isinstance(self.deprecated, str) else self.deprecated.message + + @property + def default_factory_takes_validated_data(self) -> bool | None: + """Whether the provided default factory callable has a validated data parameter. + + Returns `None` if no default factory is set. + """ + if self.default_factory is not None: + return _fields.takes_validated_data_argument(self.default_factory) + + @overload + def get_default( + self, *, call_default_factory: Literal[True], validated_data: dict[str, Any] | None = None + ) -> Any: ... + + @overload + def get_default(self, *, call_default_factory: Literal[False] = ...) -> Any: ... + + def get_default(self, *, call_default_factory: bool = False, validated_data: dict[str, Any] | None = None) -> Any: + """Get the default value. + + We expose an option for whether to call the default_factory (if present), as calling it may + result in side effects that we want to avoid. However, there are times when it really should + be called (namely, when instantiating a model via `model_construct`). + + Args: + call_default_factory: Whether to call the default factory or not. + validated_data: The already validated data to be passed to the default factory. + + Returns: + The default value, calling the default factory if requested or `PydanticUndefined` if not set. + """ + return _fields.resolve_default_value( + default=self.default, + default_factory=self.default_factory, + validated_data=validated_data, + call_default_factory=call_default_factory, + ) + + def is_required(self) -> bool: + """Check if the field is required (i.e., does not have a default value or factory). + + Returns: + `True` if the field is required, `False` otherwise. + """ + return self.default is PydanticUndefined and self.default_factory is None + + def rebuild_annotation(self) -> Any: + """Attempts to rebuild the original annotation for use in function signatures. + + If metadata is present, it adds it to the original annotation using + `Annotated`. Otherwise, it returns the original annotation as-is. + + Note that because the metadata has been flattened, the original annotation + may not be reconstructed exactly as originally provided, e.g. if the original + type had unrecognized annotations, or was annotated with a call to `pydantic.Field`. + + Returns: + The rebuilt annotation. + """ + if not self.metadata: + return self.annotation + else: + # Annotated arguments must be a tuple + return Annotated[(self.annotation, *self.metadata)] # type: ignore + + def apply_typevars_map( + self, + typevars_map: Mapping[TypeVar, Any] | None, + globalns: GlobalsNamespace | None = None, + localns: MappingNamespace | None = None, + ) -> None: + """Apply a `typevars_map` to the annotation. + + This method is used when analyzing parametrized generic types to replace typevars with their concrete types. + + This method applies the `typevars_map` to the annotation in place. + + Args: + typevars_map: A dictionary mapping type variables to their concrete types. + globalns: The globals namespace to use during type annotation evaluation. + localns: The locals namespace to use during type annotation evaluation. + + See Also: + pydantic._internal._generics.replace_types is used for replacing the typevars with + their concrete types. + """ + annotation = _generics.replace_types(self.annotation, typevars_map) + annotation, evaluated = _typing_extra.try_eval_type(annotation, globalns, localns) + self.annotation = annotation + if not evaluated: + self._complete = False + self._original_annotation = self.annotation + + def asdict(self) -> _FieldInfoAsDict: + """Return a dictionary representation of the `FieldInfo` instance. + + The returned value is a dictionary with three items: + + * `annotation`: The type annotation of the field. + * `metadata`: The metadata list. + * `attributes`: A mapping of the remaining `FieldInfo` attributes to their values (e.g. `alias`, `title`). + """ + return { + 'annotation': self.annotation, + 'metadata': self.metadata, + 'attributes': {attr: getattr(self, attr) for attr in _Attrs}, + } + + def _copy(self) -> Self: + """Return a copy of the `FieldInfo` instance.""" + # Note: we can't define a custom `__copy__()`, as `FieldInfo` is being subclassed + # by some third-party libraries with extra attributes defined (and as `FieldInfo` + # is slotted, we can't make a copy of the `__dict__`). + if type(self) is FieldInfo: + # Fast-path if the instance isn't a subclass (`copy.copy()` relies on pickling which is slower): + copied = FieldInfo.__new__(FieldInfo) + for attr_name in FieldInfo.__slots__: + setattr(copied, attr_name, getattr(self, attr_name)) + else: + copied = copy(self) + + for attr_name in ('metadata', '_attributes_set', '_qualifiers'): + # Apply "deep-copy" behavior on collections attributes: + setattr(copied, attr_name, getattr(copied, attr_name).copy()) + + return copied # pyright: ignore[reportReturnType] + + def __repr_args__(self) -> ReprArgs: + yield 'annotation', _repr.PlainRepr(_repr.display_as_type(self.annotation)) + yield 'required', self.is_required() + + for s in self.__slots__: + # TODO: properly make use of the protocol (https://rich.readthedocs.io/en/stable/pretty.html#rich-repr-protocol) + # By yielding a three-tuple: + if s in ( + 'annotation', + '_attributes_set', + '_qualifiers', + '_complete', + '_original_assignment', + '_original_annotation', + '_final', + ): + continue + elif s == 'metadata' and not self.metadata: + continue + elif s == 'repr' and self.repr is True: + continue + if s == 'frozen' and self.frozen is False: + continue + if s == 'validation_alias' and self.validation_alias == self.alias: + continue + if s == 'serialization_alias' and self.serialization_alias == self.alias: + continue + if s == 'default' and self.default is not PydanticUndefined: + yield 'default', self.default + elif s == 'default_factory' and self.default_factory is not None: + yield 'default_factory', _repr.PlainRepr(_repr.display_as_type(self.default_factory)) + else: + value = getattr(self, s) + if value is not None and value is not PydanticUndefined: + yield s, value + + +class _EmptyKwargs(TypedDict): + """This class exists solely to ensure that type checking warns about passing `**extra` in `Field`.""" + + +_Attrs = { + 'default': ..., + 'default_factory': None, + 'alias': None, + 'alias_priority': None, + 'validation_alias': None, + 'serialization_alias': None, + 'title': None, + 'field_title_generator': None, + 'description': None, + 'examples': None, + 'exclude': None, + 'exclude_if': None, + 'discriminator': None, + 'deprecated': None, + 'json_schema_extra': None, + 'frozen': None, + 'validate_default': None, + 'repr': True, + 'init': None, + 'init_var': None, + 'kw_only': None, +} + +_DefaultValues = { + **_Attrs, + 'kw_only': None, + 'pattern': None, + 'strict': None, + 'gt': None, + 'ge': None, + 'lt': None, + 'le': None, + 'multiple_of': None, + 'allow_inf_nan': None, + 'max_digits': None, + 'decimal_places': None, + 'min_length': None, + 'max_length': None, + 'coerce_numbers_to_str': None, +} + + +_T = TypeVar('_T') + + +# NOTE: Actual return type is 'FieldInfo', but we want to help type checkers +# to understand the magic that happens at runtime with the following overloads: +@overload # type hint the return value as `Any` to avoid type checking regressions when using `...`. +def Field( + default: ellipsis, # noqa: F821 # TODO: use `_typing_extra.EllipsisType` when we drop Py3.9 + *, + alias: str | None = _Unset, + alias_priority: int | None = _Unset, + validation_alias: str | AliasPath | AliasChoices | None = _Unset, + serialization_alias: str | None = _Unset, + title: str | None = _Unset, + field_title_generator: Callable[[str, FieldInfo], str] | None = _Unset, + description: str | None = _Unset, + examples: list[Any] | None = _Unset, + exclude: bool | None = _Unset, + exclude_if: Callable[[Any], bool] | None = _Unset, + discriminator: str | types.Discriminator | None = _Unset, + deprecated: Deprecated | str | bool | None = _Unset, + json_schema_extra: JsonDict | Callable[[JsonDict], None] | None = _Unset, + frozen: bool | None = _Unset, + validate_default: bool | None = _Unset, + repr: bool = _Unset, + init: bool | None = _Unset, + init_var: bool | None = _Unset, + kw_only: bool | None = _Unset, + pattern: str | re.Pattern[str] | None = _Unset, + strict: bool | None = _Unset, + coerce_numbers_to_str: bool | None = _Unset, + gt: annotated_types.SupportsGt | None = _Unset, + ge: annotated_types.SupportsGe | None = _Unset, + lt: annotated_types.SupportsLt | None = _Unset, + le: annotated_types.SupportsLe | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + min_length: int | None = _Unset, + max_length: int | None = _Unset, + union_mode: Literal['smart', 'left_to_right'] = _Unset, + fail_fast: bool | None = _Unset, + **extra: Unpack[_EmptyKwargs], +) -> Any: ... +@overload # `default` argument set, validate_default=True (no type checking on the default value) +def Field( + default: Any, + *, + alias: str | None = _Unset, + alias_priority: int | None = _Unset, + validation_alias: str | AliasPath | AliasChoices | None = _Unset, + serialization_alias: str | None = _Unset, + title: str | None = _Unset, + field_title_generator: Callable[[str, FieldInfo], str] | None = _Unset, + description: str | None = _Unset, + examples: list[Any] | None = _Unset, + exclude: bool | None = _Unset, + exclude_if: Callable[[Any], bool] | None = _Unset, + discriminator: str | types.Discriminator | None = _Unset, + deprecated: Deprecated | str | bool | None = _Unset, + json_schema_extra: JsonDict | Callable[[JsonDict], None] | None = _Unset, + frozen: bool | None = _Unset, + validate_default: Literal[True], + repr: bool = _Unset, + init: bool | None = _Unset, + init_var: bool | None = _Unset, + kw_only: bool | None = _Unset, + pattern: str | re.Pattern[str] | None = _Unset, + strict: bool | None = _Unset, + coerce_numbers_to_str: bool | None = _Unset, + gt: annotated_types.SupportsGt | None = _Unset, + ge: annotated_types.SupportsGe | None = _Unset, + lt: annotated_types.SupportsLt | None = _Unset, + le: annotated_types.SupportsLe | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + min_length: int | None = _Unset, + max_length: int | None = _Unset, + union_mode: Literal['smart', 'left_to_right'] = _Unset, + fail_fast: bool | None = _Unset, + **extra: Unpack[_EmptyKwargs], +) -> Any: ... +@overload # `default` argument set, validate_default=False or unset +def Field( + default: _T, + *, + alias: str | None = _Unset, + alias_priority: int | None = _Unset, + validation_alias: str | AliasPath | AliasChoices | None = _Unset, + serialization_alias: str | None = _Unset, + title: str | None = _Unset, + field_title_generator: Callable[[str, FieldInfo], str] | None = _Unset, + description: str | None = _Unset, + examples: list[Any] | None = _Unset, + exclude: bool | None = _Unset, + # NOTE: to get proper type checking on `exclude_if`'s argument, we could use `_T` instead of `Any`. However, + # this requires (at least for pyright) adding an additional overload where `exclude_if` is required (otherwise + # `a: int = Field(default_factory=str)` results in a false negative). + exclude_if: Callable[[Any], bool] | None = _Unset, + discriminator: str | types.Discriminator | None = _Unset, + deprecated: Deprecated | str | bool | None = _Unset, + json_schema_extra: JsonDict | Callable[[JsonDict], None] | None = _Unset, + frozen: bool | None = _Unset, + validate_default: Literal[False] = ..., + repr: bool = _Unset, + init: bool | None = _Unset, + init_var: bool | None = _Unset, + kw_only: bool | None = _Unset, + pattern: str | re.Pattern[str] | None = _Unset, + strict: bool | None = _Unset, + coerce_numbers_to_str: bool | None = _Unset, + gt: annotated_types.SupportsGt | None = _Unset, + ge: annotated_types.SupportsGe | None = _Unset, + lt: annotated_types.SupportsLt | None = _Unset, + le: annotated_types.SupportsLe | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + min_length: int | None = _Unset, + max_length: int | None = _Unset, + union_mode: Literal['smart', 'left_to_right'] = _Unset, + fail_fast: bool | None = _Unset, + **extra: Unpack[_EmptyKwargs], +) -> _T: ... +@overload # `default_factory` argument set, validate_default=True (no type checking on the default value) +def Field( # pyright: ignore[reportOverlappingOverload] + *, + default_factory: Callable[[], Any] | Callable[[dict[str, Any]], Any], + alias: str | None = _Unset, + alias_priority: int | None = _Unset, + validation_alias: str | AliasPath | AliasChoices | None = _Unset, + serialization_alias: str | None = _Unset, + title: str | None = _Unset, + field_title_generator: Callable[[str, FieldInfo], str] | None = _Unset, + description: str | None = _Unset, + examples: list[Any] | None = _Unset, + exclude: bool | None = _Unset, + exclude_if: Callable[[Any], bool] | None = _Unset, + discriminator: str | types.Discriminator | None = _Unset, + deprecated: Deprecated | str | bool | None = _Unset, + json_schema_extra: JsonDict | Callable[[JsonDict], None] | None = _Unset, + frozen: bool | None = _Unset, + validate_default: Literal[True], + repr: bool = _Unset, + init: bool | None = _Unset, + init_var: bool | None = _Unset, + kw_only: bool | None = _Unset, + pattern: str | re.Pattern[str] | None = _Unset, + strict: bool | None = _Unset, + coerce_numbers_to_str: bool | None = _Unset, + gt: annotated_types.SupportsGt | None = _Unset, + ge: annotated_types.SupportsGe | None = _Unset, + lt: annotated_types.SupportsLt | None = _Unset, + le: annotated_types.SupportsLe | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + min_length: int | None = _Unset, + max_length: int | None = _Unset, + union_mode: Literal['smart', 'left_to_right'] = _Unset, + fail_fast: bool | None = _Unset, + **extra: Unpack[_EmptyKwargs], +) -> Any: ... +@overload # `default_factory` argument set, validate_default=False or unset +def Field( + *, + default_factory: Callable[[], _T] | Callable[[dict[str, Any]], _T], + alias: str | None = _Unset, + alias_priority: int | None = _Unset, + validation_alias: str | AliasPath | AliasChoices | None = _Unset, + serialization_alias: str | None = _Unset, + title: str | None = _Unset, + field_title_generator: Callable[[str, FieldInfo], str] | None = _Unset, + description: str | None = _Unset, + examples: list[Any] | None = _Unset, + exclude: bool | None = _Unset, + # NOTE: to get proper type checking on `exclude_if`'s argument, we could use `_T` instead of `Any`. However, + # this requires (at least for pyright) adding an additional overload where `exclude_if` is required (otherwise + # `a: int = Field(default_factory=str)` results in a false negative). + exclude_if: Callable[[Any], bool] | None = _Unset, + discriminator: str | types.Discriminator | None = _Unset, + deprecated: Deprecated | str | bool | None = _Unset, + json_schema_extra: JsonDict | Callable[[JsonDict], None] | None = _Unset, + frozen: bool | None = _Unset, + validate_default: Literal[False] | None = _Unset, + repr: bool = _Unset, + init: bool | None = _Unset, + init_var: bool | None = _Unset, + kw_only: bool | None = _Unset, + pattern: str | re.Pattern[str] | None = _Unset, + strict: bool | None = _Unset, + coerce_numbers_to_str: bool | None = _Unset, + gt: annotated_types.SupportsGt | None = _Unset, + ge: annotated_types.SupportsGe | None = _Unset, + lt: annotated_types.SupportsLt | None = _Unset, + le: annotated_types.SupportsLe | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + min_length: int | None = _Unset, + max_length: int | None = _Unset, + union_mode: Literal['smart', 'left_to_right'] = _Unset, + fail_fast: bool | None = _Unset, + **extra: Unpack[_EmptyKwargs], +) -> _T: ... +@overload +def Field( # No default set + *, + alias: str | None = _Unset, + alias_priority: int | None = _Unset, + validation_alias: str | AliasPath | AliasChoices | None = _Unset, + serialization_alias: str | None = _Unset, + title: str | None = _Unset, + field_title_generator: Callable[[str, FieldInfo], str] | None = _Unset, + description: str | None = _Unset, + examples: list[Any] | None = _Unset, + exclude: bool | None = _Unset, + exclude_if: Callable[[Any], bool] | None = _Unset, + discriminator: str | types.Discriminator | None = _Unset, + deprecated: Deprecated | str | bool | None = _Unset, + json_schema_extra: JsonDict | Callable[[JsonDict], None] | None = _Unset, + frozen: bool | None = _Unset, + validate_default: bool | None = _Unset, + repr: bool = _Unset, + init: bool | None = _Unset, + init_var: bool | None = _Unset, + kw_only: bool | None = _Unset, + pattern: str | re.Pattern[str] | None = _Unset, + strict: bool | None = _Unset, + coerce_numbers_to_str: bool | None = _Unset, + gt: annotated_types.SupportsGt | None = _Unset, + ge: annotated_types.SupportsGe | None = _Unset, + lt: annotated_types.SupportsLt | None = _Unset, + le: annotated_types.SupportsLe | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + min_length: int | None = _Unset, + max_length: int | None = _Unset, + union_mode: Literal['smart', 'left_to_right'] = _Unset, + fail_fast: bool | None = _Unset, + **extra: Unpack[_EmptyKwargs], +) -> Any: ... +def Field( # noqa: C901 + default: Any = PydanticUndefined, + *, + default_factory: Callable[[], Any] | Callable[[dict[str, Any]], Any] | None = _Unset, + alias: str | None = _Unset, + alias_priority: int | None = _Unset, + validation_alias: str | AliasPath | AliasChoices | None = _Unset, + serialization_alias: str | None = _Unset, + title: str | None = _Unset, + field_title_generator: Callable[[str, FieldInfo], str] | None = _Unset, + description: str | None = _Unset, + examples: list[Any] | None = _Unset, + exclude: bool | None = _Unset, + exclude_if: Callable[[Any], bool] | None = _Unset, + discriminator: str | types.Discriminator | None = _Unset, + deprecated: Deprecated | str | bool | None = _Unset, + json_schema_extra: JsonDict | Callable[[JsonDict], None] | None = _Unset, + frozen: bool | None = _Unset, + validate_default: bool | None = _Unset, + repr: bool = _Unset, + init: bool | None = _Unset, + init_var: bool | None = _Unset, + kw_only: bool | None = _Unset, + pattern: str | re.Pattern[str] | None = _Unset, + strict: bool | None = _Unset, + coerce_numbers_to_str: bool | None = _Unset, + gt: annotated_types.SupportsGt | None = _Unset, + ge: annotated_types.SupportsGe | None = _Unset, + lt: annotated_types.SupportsLt | None = _Unset, + le: annotated_types.SupportsLe | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + min_length: int | None = _Unset, + max_length: int | None = _Unset, + union_mode: Literal['smart', 'left_to_right'] = _Unset, + fail_fast: bool | None = _Unset, + **extra: Unpack[_EmptyKwargs], +) -> Any: + """!!! abstract "Usage Documentation" + [Fields](../concepts/fields.md) + + Create a field for objects that can be configured. + + Used to provide extra information about a field, either for the model schema or complex validation. Some arguments + apply only to number fields (`int`, `float`, `Decimal`) and some apply only to `str`. + + Note: + - Any `_Unset` objects will be replaced by the corresponding value defined in the `_DefaultValues` dictionary. If a key for the `_Unset` object is not found in the `_DefaultValues` dictionary, it will default to `None` + + Args: + default: Default value if the field is not set. + default_factory: A callable to generate the default value. The callable can either take 0 arguments + (in which case it is called as is) or a single argument containing the already validated data. + alias: The name to use for the attribute when validating or serializing by alias. + This is often used for things like converting between snake and camel case. + alias_priority: Priority of the alias. This affects whether an alias generator is used. + validation_alias: Like `alias`, but only affects validation, not serialization. + serialization_alias: Like `alias`, but only affects serialization, not validation. + title: Human-readable title. + field_title_generator: A callable that takes a field name and returns title for it. + description: Human-readable description. + examples: Example values for this field. + exclude: Whether to exclude the field from the model serialization. + exclude_if: A callable that determines whether to exclude a field during serialization based on its value. + discriminator: Field name or Discriminator for discriminating the type in a tagged union. + deprecated: A deprecation message, an instance of `warnings.deprecated` or the `typing_extensions.deprecated` backport, + or a boolean. If `True`, a default deprecation message will be emitted when accessing the field. + json_schema_extra: A dict or callable to provide extra JSON schema properties. + frozen: Whether the field is frozen. If true, attempts to change the value on an instance will raise an error. + validate_default: If `True`, apply validation to the default value every time you create an instance. + Otherwise, for performance reasons, the default value of the field is trusted and not validated. + repr: A boolean indicating whether to include the field in the `__repr__` output. + init: Whether the field should be included in the constructor of the dataclass. + (Only applies to dataclasses.) + init_var: Whether the field should _only_ be included in the constructor of the dataclass. + (Only applies to dataclasses.) + kw_only: Whether the field should be a keyword-only argument in the constructor of the dataclass. + (Only applies to dataclasses.) + coerce_numbers_to_str: Whether to enable coercion of any `Number` type to `str` (not applicable in `strict` mode). + strict: If `True`, strict validation is applied to the field. + See [Strict Mode](../concepts/strict_mode.md) for details. + gt: Greater than. If set, value must be greater than this. Only applicable to numbers. + ge: Greater than or equal. If set, value must be greater than or equal to this. Only applicable to numbers. + lt: Less than. If set, value must be less than this. Only applicable to numbers. + le: Less than or equal. If set, value must be less than or equal to this. Only applicable to numbers. + multiple_of: Value must be a multiple of this. Only applicable to numbers. + min_length: Minimum length for iterables. + max_length: Maximum length for iterables. + pattern: Pattern for strings (a regular expression). + allow_inf_nan: Allow `inf`, `-inf`, `nan`. Only applicable to float and [`Decimal`][decimal.Decimal] numbers. + max_digits: Maximum number of allow digits for strings. + decimal_places: Maximum number of decimal places allowed for numbers. + union_mode: The strategy to apply when validating a union. Can be `smart` (the default), or `left_to_right`. + See [Union Mode](../concepts/unions.md#union-modes) for details. + fail_fast: If `True`, validation will stop on the first error. If `False`, all validation errors will be collected. + This option can be applied only to iterable types (list, tuple, set, and frozenset). + extra: (Deprecated) Extra fields that will be included in the JSON schema. + + !!! warning Deprecated + The `extra` kwargs is deprecated. Use `json_schema_extra` instead. + + Returns: + A new [`FieldInfo`][pydantic.fields.FieldInfo]. The return annotation is `Any` so `Field` can be used on + type-annotated fields without causing a type error. + """ + # Check deprecated and removed params from V1. This logic should eventually be removed. + const = extra.pop('const', None) # type: ignore + if const is not None: + raise PydanticUserError('`const` is removed, use `Literal` instead', code='removed-kwargs') + + min_items = extra.pop('min_items', None) # type: ignore + if min_items is not None: + warn( + '`min_items` is deprecated and will be removed, use `min_length` instead', + PydanticDeprecatedSince20, + stacklevel=2, + ) + if min_length in (None, _Unset): + min_length = min_items # type: ignore + + max_items = extra.pop('max_items', None) # type: ignore + if max_items is not None: + warn( + '`max_items` is deprecated and will be removed, use `max_length` instead', + PydanticDeprecatedSince20, + stacklevel=2, + ) + if max_length in (None, _Unset): + max_length = max_items # type: ignore + + unique_items = extra.pop('unique_items', None) # type: ignore + if unique_items is not None: + raise PydanticUserError( + ( + '`unique_items` is removed, use `Set` instead' + '(this feature is discussed in https://github.com/pydantic/pydantic-core/issues/296)' + ), + code='removed-kwargs', + ) + + allow_mutation = extra.pop('allow_mutation', None) # type: ignore + if allow_mutation is not None: + warn( + '`allow_mutation` is deprecated and will be removed. use `frozen` instead', + PydanticDeprecatedSince20, + stacklevel=2, + ) + if allow_mutation is False: + frozen = True + + regex = extra.pop('regex', None) # type: ignore + if regex is not None: + raise PydanticUserError('`regex` is removed. use `pattern` instead', code='removed-kwargs') + + if extra: + warn( + 'Using extra keyword arguments on `Field` is deprecated and will be removed.' + ' Use `json_schema_extra` instead.' + f' (Extra keys: {", ".join(k.__repr__() for k in extra.keys())})', + PydanticDeprecatedSince20, + stacklevel=2, + ) + if not json_schema_extra or json_schema_extra is _Unset: + json_schema_extra = extra # type: ignore + + if ( + validation_alias + and validation_alias is not _Unset + and not isinstance(validation_alias, (str, AliasChoices, AliasPath)) + ): + raise TypeError('Invalid `validation_alias` type. it should be `str`, `AliasChoices`, or `AliasPath`') + + if serialization_alias in (_Unset, None) and isinstance(alias, str): + serialization_alias = alias + + if validation_alias in (_Unset, None): + validation_alias = alias + + include = extra.pop('include', None) # type: ignore + if include is not None: + warn( + '`include` is deprecated and does nothing. It will be removed, use `exclude` instead', + PydanticDeprecatedSince20, + stacklevel=2, + ) + + return FieldInfo.from_field( + default, + default_factory=default_factory, + alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, + title=title, + field_title_generator=field_title_generator, + description=description, + examples=examples, + exclude=exclude, + exclude_if=exclude_if, + discriminator=discriminator, + deprecated=deprecated, + json_schema_extra=json_schema_extra, + frozen=frozen, + pattern=pattern, + validate_default=validate_default, + repr=repr, + init=init, + init_var=init_var, + kw_only=kw_only, + coerce_numbers_to_str=coerce_numbers_to_str, + strict=strict, + gt=gt, + ge=ge, + lt=lt, + le=le, + multiple_of=multiple_of, + min_length=min_length, + max_length=max_length, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + union_mode=union_mode, + fail_fast=fail_fast, + ) + + +_FIELD_ARG_NAMES = set(inspect.signature(Field).parameters) +_FIELD_ARG_NAMES.remove('extra') # do not include the varkwargs parameter + + +class ModelPrivateAttr(_repr.Representation): + """A descriptor for private attributes in class models. + + !!! warning + You generally shouldn't be creating `ModelPrivateAttr` instances directly, instead use + `pydantic.fields.PrivateAttr`. (This is similar to `FieldInfo` vs. `Field`.) + + Attributes: + default: The default value of the attribute if not provided. + default_factory: A callable to generate the default value. The callable can either take 0 arguments + (in which case it is called as is) or a single argument containing the validated data (the model's + [`__dict__`][object.__dict__]) and the already initialized private attributes. + """ + + __slots__ = ('default', 'default_factory') + + def __init__( + self, + default: Any = PydanticUndefined, + *, + default_factory: Callable[[], Any] | Callable[[dict[str, Any]], Any] | None = None, + ) -> None: + if default is Ellipsis: + self.default = PydanticUndefined + else: + self.default = default + self.default_factory = default_factory + + if not TYPE_CHECKING: + # We put `__getattr__` in a non-TYPE_CHECKING block because otherwise, mypy allows arbitrary attribute access + + def __getattr__(self, item: str) -> Any: + """This function improves compatibility with custom descriptors by ensuring delegation happens + as expected when the default value of a private attribute is a descriptor. + """ + if item in {'__get__', '__set__', '__delete__'}: + if hasattr(self.default, item): + return getattr(self.default, item) + raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}') + + def __set_name__(self, cls: type[Any], name: str) -> None: + """Preserve `__set_name__` protocol defined in https://peps.python.org/pep-0487.""" + default = self.default + if default is PydanticUndefined: + return + set_name = getattr(default, '__set_name__', None) + if callable(set_name): + set_name(cls, name) + + @property + def default_factory_takes_validated_data(self) -> bool | None: + """Whether the provided default factory callable has a validated data parameter. + + Returns `None` if no default factory is set. + """ + if self.default_factory is not None: + return _fields.takes_validated_data_argument(self.default_factory) + + @overload + def get_default( + self, *, call_default_factory: Literal[True], validated_data: dict[str, Any] | None = None + ) -> Any: ... + + @overload + def get_default(self, *, call_default_factory: Literal[False] = ...) -> Any: ... + + def get_default(self, *, call_default_factory: bool = False, validated_data: dict[str, Any] | None = None) -> Any: + """Get the default value. + + We expose an option for whether to call the default_factory (if present), as calling it may + result in side effects that we want to avoid. However, there are times when it really should + be called (namely, when instantiating a model via `model_construct`). + + Args: + call_default_factory: Whether to call the default factory or not. + validated_data: The already validated data to be passed to the default factory. + + Returns: + The default value, calling the default factory if requested or `None` if not set. + """ + return _fields.resolve_default_value( + default=self.default, + default_factory=self.default_factory, + validated_data=validated_data, + call_default_factory=call_default_factory, + ) + + def __eq__(self, other: Any) -> bool: + return isinstance(other, self.__class__) and (self.default, self.default_factory) == ( + other.default, + other.default_factory, + ) + + +# NOTE: Actual return type is 'ModelPrivateAttr', but we want to help type checkers +# to understand the magic that happens at runtime. +@overload # `default` argument set +def PrivateAttr( + default: _T, + *, + init: Literal[False] = False, +) -> _T: ... +@overload # `default_factory` argument set +def PrivateAttr( + *, + default_factory: Callable[[], _T] | Callable[[dict[str, Any]], _T], + init: Literal[False] = False, +) -> _T: ... +@overload # No default set +def PrivateAttr( + *, + init: Literal[False] = False, +) -> Any: ... +def PrivateAttr( + default: Any = PydanticUndefined, + *, + default_factory: Callable[[], Any] | Callable[[dict[str, Any]], Any] | None = None, + init: Literal[False] = False, +) -> Any: + """!!! abstract "Usage Documentation" + [Private Model Attributes](../concepts/models.md#private-model-attributes) + + Indicates that an attribute is intended for private use and not handled during normal validation/serialization. + + Private attributes are not validated by Pydantic, so it's up to you to ensure they are used in a type-safe manner. + + Private attributes are stored in `__private_attributes__` on the model. + + Args: + default: The attribute's default value. Defaults to Undefined. + default_factory: A callable to generate the default value. The callable can either take 0 arguments + (in which case it is called as is) or a single argument containing the validated data (the model's + [`__dict__`][object.__dict__]) and the already initialized private attributes. + If both `default` and `default_factory` are set, an error will be raised. + init: Whether the attribute should be included in the constructor of the dataclass. Always `False`. + + Returns: + An instance of [`ModelPrivateAttr`][pydantic.fields.ModelPrivateAttr] class. + + Raises: + ValueError: If both `default` and `default_factory` are set. + """ + if default is not PydanticUndefined and default_factory is not None: + raise TypeError('cannot specify both default and default_factory') + + return ModelPrivateAttr( + default, + default_factory=default_factory, + ) + + +@dataclasses.dataclass(**_internal_dataclass.slots_true) +class ComputedFieldInfo: + """A container for data from `@computed_field` so that we can access it while building the pydantic-core schema. + + Attributes: + decorator_repr: A class variable representing the decorator string, '@computed_field'. + wrapped_property: The wrapped computed field property. + return_type: The type of the computed field property's return value. + alias: The alias of the property to be used during serialization. + alias_priority: The priority of the alias. This affects whether an alias generator is used. + title: Title of the computed field to include in the serialization JSON schema. + field_title_generator: A callable that takes a field name and returns title for it. + description: Description of the computed field to include in the serialization JSON schema. + deprecated: A deprecation message, an instance of `warnings.deprecated` or the `typing_extensions.deprecated` backport, + or a boolean. If `True`, a default deprecation message will be emitted when accessing the field. + examples: Example values of the computed field to include in the serialization JSON schema. + json_schema_extra: A dict or callable to provide extra JSON schema properties. + repr: A boolean indicating whether to include the field in the __repr__ output. + """ + + decorator_repr: ClassVar[str] = '@computed_field' + wrapped_property: property + return_type: Any + alias: str | None + alias_priority: int | None + exclude_if: Callable[[Any], bool] | None + title: str | None + field_title_generator: Callable[[str, ComputedFieldInfo], str] | None + description: str | None + deprecated: Deprecated | str | bool | None + examples: list[Any] | None + json_schema_extra: JsonDict | Callable[[JsonDict], None] | None + repr: bool + # NOTE: if you add a new field, add it to the `__copy__()` implementation. + + def __copy__(self) -> Self: + return type(self)( + wrapped_property=self.wrapped_property, + return_type=self.return_type, + alias=self.alias, + alias_priority=self.alias_priority, + exclude_if=self.exclude_if, + title=self.title, + field_title_generator=self.field_title_generator, + description=self.description, + deprecated=self.deprecated, + examples=self.examples.copy() if isinstance(self.examples, list) else self.examples, + json_schema_extra=self.json_schema_extra.copy() + if isinstance(self.json_schema_extra, dict) + else self.json_schema_extra, + repr=self.repr, + ) + + @property + def deprecation_message(self) -> str | None: + """The deprecation message to be emitted, or `None` if not set.""" + if self.deprecated is None: + return None + if isinstance(self.deprecated, bool): + return 'deprecated' if self.deprecated else None + return self.deprecated if isinstance(self.deprecated, str) else self.deprecated.message + + def _update_from_config(self, config_wrapper: ConfigWrapper, name: str) -> None: + """Update the instance from the configuration set on the class this computed field belongs to.""" + title_generator = self.field_title_generator or config_wrapper.field_title_generator + if title_generator is not None and self.title is None: + self.title = title_generator(name, self) + if config_wrapper.alias_generator is not None: + self._apply_alias_generator(config_wrapper.alias_generator, name) + + def _apply_alias_generator(self, alias_generator: Callable[[str], str] | AliasGenerator, name: str) -> None: + """Apply an alias generator to aliases if appropriate. + + Args: + alias_generator: A callable that takes a string and returns a string, or an `AliasGenerator` instance. + name: The name of the computed field from which to generate the alias. + """ + # Apply an alias_generator if + # 1. An alias is not specified + # 2. An alias is specified, but the priority is <= 1 + + if self.alias_priority is None or self.alias_priority <= 1 or self.alias is None: + alias, _, serialization_alias = None, None, None + + if isinstance(alias_generator, AliasGenerator): + alias, _, serialization_alias = alias_generator.generate_aliases(name) + elif callable(alias_generator): + alias = alias_generator(name) + + # if priority is not set, we set to 1 + # which supports the case where the alias_generator from a child class is used + # to generate an alias for a field in a parent class + if self.alias_priority is None or self.alias_priority <= 1: + self.alias_priority = 1 + + # if the priority is 1, then we set the aliases to the generated alias + # note that we use the serialization_alias with priority over alias, as computed_field + # aliases are used for serialization only (not validation) + if self.alias_priority == 1: + self.alias = _utils.get_first_not_none(serialization_alias, alias) + + +def _wrapped_property_is_private(property_: cached_property | property) -> bool: # type: ignore + """Returns true if provided property is private, False otherwise.""" + wrapped_name: str = '' + + if isinstance(property_, property): + wrapped_name = getattr(property_.fget, '__name__', '') + elif isinstance(property_, cached_property): # type: ignore + wrapped_name = getattr(property_.func, '__name__', '') # type: ignore + + return wrapped_name.startswith('_') and not wrapped_name.startswith('__') + + +# this should really be `property[T], cached_property[T]` but property is not generic unlike cached_property +# See https://github.com/python/typing/issues/985 and linked issues +PropertyT = TypeVar('PropertyT') + + +@overload +def computed_field(func: PropertyT, /) -> PropertyT: ... + + +@overload +def computed_field( + *, + alias: str | None = None, + alias_priority: int | None = None, + exclude_if: Callable[[Any], bool] | None = None, + title: str | None = None, + field_title_generator: Callable[[str, ComputedFieldInfo], str] | None = None, + description: str | None = None, + deprecated: Deprecated | str | bool | None = None, + examples: list[Any] | None = None, + json_schema_extra: JsonDict | Callable[[JsonDict], None] | None = None, + repr: bool = True, + return_type: Any = PydanticUndefined, +) -> Callable[[PropertyT], PropertyT]: ... + + +def computed_field( + func: PropertyT | None = None, + /, + *, + alias: str | None = None, + alias_priority: int | None = None, + exclude_if: Callable[[Any], bool] | None = None, + title: str | None = None, + field_title_generator: Callable[[str, ComputedFieldInfo], str] | None = None, + description: str | None = None, + deprecated: Deprecated | str | bool | None = None, + examples: list[Any] | None = None, + json_schema_extra: JsonDict | Callable[[JsonDict], None] | None = None, + repr: bool | None = None, + return_type: Any = PydanticUndefined, +) -> PropertyT | Callable[[PropertyT], PropertyT]: + """!!! abstract "Usage Documentation" + [The `computed_field` decorator](../concepts/fields.md#the-computed_field-decorator) + + Decorator to include `property` and `cached_property` when serializing models or dataclasses. + + This is useful for fields that are computed from other fields, or for fields that are expensive to compute and should be cached. + + ```python + from pydantic import BaseModel, computed_field + + class Rectangle(BaseModel): + width: int + length: int + + @computed_field + @property + def area(self) -> int: + return self.width * self.length + + print(Rectangle(width=3, length=2).model_dump()) + #> {'width': 3, 'length': 2, 'area': 6} + ``` + + If applied to functions not yet decorated with `@property` or `@cached_property`, the function is + automatically wrapped with `property`. Although this is more concise, you will lose IntelliSense in your IDE, + and confuse static type checkers, thus explicit use of `@property` is recommended. + + !!! warning "Mypy Warning" + Even with the `@property` or `@cached_property` applied to your function before `@computed_field`, + mypy may throw a `Decorated property not supported` error. + See [mypy issue #1362](https://github.com/python/mypy/issues/1362), for more information. + To avoid this error message, add `# type: ignore[prop-decorator]` to the `@computed_field` line. + + [pyright](https://github.com/microsoft/pyright) supports `@computed_field` without error. + + ```python + import random + + from pydantic import BaseModel, computed_field + + class Square(BaseModel): + width: float + + @computed_field + def area(self) -> float: # converted to a `property` by `computed_field` + return round(self.width**2, 2) + + @area.setter + def area(self, new_area: float) -> None: + self.width = new_area**0.5 + + @computed_field(alias='the magic number', repr=False) + def random_number(self) -> int: + return random.randint(0, 1_000) + + square = Square(width=1.3) + + # `random_number` does not appear in representation + print(repr(square)) + #> Square(width=1.3, area=1.69) + + print(square.random_number) + #> 3 + + square.area = 4 + + print(square.model_dump_json(by_alias=True)) + #> {"width":2.0,"area":4.0,"the magic number":3} + ``` + + !!! warning "Overriding with `computed_field`" + You can't override a field from a parent class with a `computed_field` in the child class. + `mypy` complains about this behavior if allowed, and `dataclasses` doesn't allow this pattern either. + See the example below: + + ```python + from pydantic import BaseModel, computed_field + + class Parent(BaseModel): + a: str + + try: + + class Child(Parent): + @computed_field + @property + def a(self) -> str: + return 'new a' + + except TypeError as e: + print(e) + ''' + Field 'a' of class 'Child' overrides symbol of same name in a parent class. This override with a computed_field is incompatible. + ''' + ``` + + Private properties decorated with `@computed_field` have `repr=False` by default. + + ```python + from functools import cached_property + + from pydantic import BaseModel, computed_field + + class Model(BaseModel): + foo: int + + @computed_field + @cached_property + def _private_cached_property(self) -> int: + return -self.foo + + @computed_field + @property + def _private_property(self) -> int: + return -self.foo + + m = Model(foo=1) + print(repr(m)) + #> Model(foo=1) + ``` + + Args: + func: the function to wrap. + alias: alias to use when serializing this computed field, only used when `by_alias=True` + alias_priority: priority of the alias. This affects whether an alias generator is used + exclude_if: A callable that determines whether to exclude this computed field during serialization based on its value. + title: Title to use when including this computed field in JSON Schema + field_title_generator: A callable that takes a field name and returns title for it. + description: Description to use when including this computed field in JSON Schema, defaults to the function's + docstring + deprecated: A deprecation message (or an instance of `warnings.deprecated` or the `typing_extensions.deprecated` backport). + to be emitted when accessing the field. Or a boolean. This will automatically be set if the property is decorated with the + `deprecated` decorator. + examples: Example values to use when including this computed field in JSON Schema + json_schema_extra: A dict or callable to provide extra JSON schema properties. + repr: whether to include this computed field in model repr. + Default is `False` for private properties and `True` for public properties. + return_type: optional return for serialization logic to expect when serializing to JSON, if included + this must be correct, otherwise a `TypeError` is raised. + If you don't include a return type Any is used, which does runtime introspection to handle arbitrary + objects. + + Returns: + A proxy wrapper for the property. + """ + + def dec(f: Any) -> Any: + nonlocal description, deprecated, return_type, alias_priority + unwrapped = _decorators.unwrap_wrapped_function(f) + + if description is None and unwrapped.__doc__: + description = inspect.cleandoc(unwrapped.__doc__) + + if deprecated is None and hasattr(unwrapped, '__deprecated__'): + deprecated = unwrapped.__deprecated__ + + # if the function isn't already decorated with `@property` (or another descriptor), then we wrap it now + f = _decorators.ensure_property(f) + alias_priority = (alias_priority or 2) if alias is not None else None + + if repr is None: + repr_: bool = not _wrapped_property_is_private(property_=f) + else: + repr_ = repr + + dec_info = ComputedFieldInfo( + f, + return_type, + alias, + alias_priority, + exclude_if, + title, + field_title_generator, + description, + deprecated, + examples, + json_schema_extra, + repr_, + ) + return _decorators.PydanticDescriptorProxy(f, dec_info) + + if func is None: + return dec + else: + return dec(func) diff --git a/source/target-vendors/cp312/pydantic/functional_serializers.py b/source/target-vendors/cp312/pydantic/functional_serializers.py new file mode 100644 index 0000000..ed63336 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/functional_serializers.py @@ -0,0 +1,470 @@ +"""This module contains related classes and functions for serialization.""" + +from __future__ import annotations + +import dataclasses +from functools import partial, partialmethod +from typing import TYPE_CHECKING, Annotated, Any, Callable, Literal, TypeVar, overload + +from pydantic_core import PydanticUndefined, core_schema +from pydantic_core.core_schema import SerializationInfo, SerializerFunctionWrapHandler, WhenUsed +from typing_extensions import TypeAlias + +from . import PydanticUndefinedAnnotation +from ._internal import _decorators, _internal_dataclass +from .annotated_handlers import GetCoreSchemaHandler +from .errors import PydanticUserError + + +@dataclasses.dataclass(**_internal_dataclass.slots_true, frozen=True) +class PlainSerializer: + """Plain serializers use a function to modify the output of serialization. + + This is particularly helpful when you want to customize the serialization for annotated types. + Consider an input of `list`, which will be serialized into a space-delimited string. + + ```python + from typing import Annotated + + from pydantic import BaseModel, PlainSerializer + + CustomStr = Annotated[ + list, PlainSerializer(lambda x: ' '.join(x), return_type=str) + ] + + class StudentModel(BaseModel): + courses: CustomStr + + student = StudentModel(courses=['Math', 'Chemistry', 'English']) + print(student.model_dump()) + #> {'courses': 'Math Chemistry English'} + ``` + + Attributes: + func: The serializer function. + return_type: The return type for the function. If omitted it will be inferred from the type annotation. + when_used: Determines when this serializer should be used. Accepts a string with values `'always'`, + `'unless-none'`, `'json'`, and `'json-unless-none'`. Defaults to 'always'. + """ + + func: core_schema.SerializerFunction + return_type: Any = PydanticUndefined + when_used: WhenUsed = 'always' + + def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + """Gets the Pydantic core schema. + + Args: + source_type: The source type. + handler: The `GetCoreSchemaHandler` instance. + + Returns: + The Pydantic core schema. + """ + schema = handler(source_type) + if self.return_type is not PydanticUndefined: + return_type = self.return_type + else: + try: + # Do not pass in globals as the function could be defined in a different module. + # Instead, let `get_callable_return_type` infer the globals to use, but still pass + # in locals that may contain a parent/rebuild namespace: + return_type = _decorators.get_callable_return_type( + self.func, + localns=handler._get_types_namespace().locals, + ) + except NameError as e: + raise PydanticUndefinedAnnotation.from_name_error(e) from e + + return_schema = None if return_type is PydanticUndefined else handler.generate_schema(return_type) + schema['serialization'] = core_schema.plain_serializer_function_ser_schema( + function=self.func, + info_arg=_decorators.inspect_annotated_serializer(self.func, 'plain'), + return_schema=return_schema, + when_used=self.when_used, + ) + return schema + + +@dataclasses.dataclass(**_internal_dataclass.slots_true, frozen=True) +class WrapSerializer: + """Wrap serializers receive the raw inputs along with a handler function that applies the standard serialization + logic, and can modify the resulting value before returning it as the final output of serialization. + + For example, here's a scenario in which a wrap serializer transforms timezones to UTC **and** utilizes the existing `datetime` serialization logic. + + ```python + from datetime import datetime, timezone + from typing import Annotated, Any + + from pydantic import BaseModel, WrapSerializer + + class EventDatetime(BaseModel): + start: datetime + end: datetime + + def convert_to_utc(value: Any, handler, info) -> dict[str, datetime]: + # Note that `handler` can actually help serialize the `value` for + # further custom serialization in case it's a subclass. + partial_result = handler(value, info) + if info.mode == 'json': + return { + k: datetime.fromisoformat(v).astimezone(timezone.utc) + for k, v in partial_result.items() + } + return {k: v.astimezone(timezone.utc) for k, v in partial_result.items()} + + UTCEventDatetime = Annotated[EventDatetime, WrapSerializer(convert_to_utc)] + + class EventModel(BaseModel): + event_datetime: UTCEventDatetime + + dt = EventDatetime( + start='2024-01-01T07:00:00-08:00', end='2024-01-03T20:00:00+06:00' + ) + event = EventModel(event_datetime=dt) + print(event.model_dump()) + ''' + { + 'event_datetime': { + 'start': datetime.datetime( + 2024, 1, 1, 15, 0, tzinfo=datetime.timezone.utc + ), + 'end': datetime.datetime( + 2024, 1, 3, 14, 0, tzinfo=datetime.timezone.utc + ), + } + } + ''' + + print(event.model_dump_json()) + ''' + {"event_datetime":{"start":"2024-01-01T15:00:00Z","end":"2024-01-03T14:00:00Z"}} + ''' + ``` + + Attributes: + func: The serializer function to be wrapped. + return_type: The return type for the function. If omitted it will be inferred from the type annotation. + when_used: Determines when this serializer should be used. Accepts a string with values `'always'`, + `'unless-none'`, `'json'`, and `'json-unless-none'`. Defaults to 'always'. + """ + + func: core_schema.WrapSerializerFunction + return_type: Any = PydanticUndefined + when_used: WhenUsed = 'always' + + def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + """This method is used to get the Pydantic core schema of the class. + + Args: + source_type: Source type. + handler: Core schema handler. + + Returns: + The generated core schema of the class. + """ + schema = handler(source_type) + if self.return_type is not PydanticUndefined: + return_type = self.return_type + else: + try: + # Do not pass in globals as the function could be defined in a different module. + # Instead, let `get_callable_return_type` infer the globals to use, but still pass + # in locals that may contain a parent/rebuild namespace: + return_type = _decorators.get_callable_return_type( + self.func, + localns=handler._get_types_namespace().locals, + ) + except NameError as e: + raise PydanticUndefinedAnnotation.from_name_error(e) from e + + return_schema = None if return_type is PydanticUndefined else handler.generate_schema(return_type) + schema['serialization'] = core_schema.wrap_serializer_function_ser_schema( + function=self.func, + info_arg=_decorators.inspect_annotated_serializer(self.func, 'wrap'), + return_schema=return_schema, + when_used=self.when_used, + ) + return schema + + +if TYPE_CHECKING: + _Partial: TypeAlias = 'partial[Any] | partialmethod[Any]' + + FieldPlainSerializer: TypeAlias = 'core_schema.SerializerFunction | _Partial' + """A field serializer method or function in `plain` mode.""" + + FieldWrapSerializer: TypeAlias = 'core_schema.WrapSerializerFunction | _Partial' + """A field serializer method or function in `wrap` mode.""" + + FieldSerializer: TypeAlias = 'FieldPlainSerializer | FieldWrapSerializer' + """A field serializer method or function.""" + + _FieldPlainSerializerT = TypeVar('_FieldPlainSerializerT', bound=FieldPlainSerializer) + _FieldWrapSerializerT = TypeVar('_FieldWrapSerializerT', bound=FieldWrapSerializer) + + +@overload +def field_serializer( + field: str, + /, + *fields: str, + mode: Literal['wrap'], + return_type: Any = ..., + when_used: WhenUsed = ..., + check_fields: bool | None = ..., +) -> Callable[[_FieldWrapSerializerT], _FieldWrapSerializerT]: ... + + +@overload +def field_serializer( + field: str, + /, + *fields: str, + mode: Literal['plain'] = ..., + return_type: Any = ..., + when_used: WhenUsed = ..., + check_fields: bool | None = ..., +) -> Callable[[_FieldPlainSerializerT], _FieldPlainSerializerT]: ... + + +def field_serializer( # noqa: D417 + field: str, + /, + *fields: str, + mode: Literal['plain', 'wrap'] = 'plain', + # TODO PEP 747 (grep for 'return_type' on the whole code base): + return_type: Any = PydanticUndefined, + when_used: WhenUsed = 'always', + check_fields: bool | None = None, +) -> ( + Callable[[_FieldWrapSerializerT], _FieldWrapSerializerT] + | Callable[[_FieldPlainSerializerT], _FieldPlainSerializerT] +): + """Decorator that enables custom field serialization. + + In the below example, a field of type `set` is used to mitigate duplication. A `field_serializer` is used to serialize the data as a sorted list. + + ```python + from pydantic import BaseModel, field_serializer + + class StudentModel(BaseModel): + name: str = 'Jane' + courses: set[str] + + @field_serializer('courses', when_used='json') + def serialize_courses_in_order(self, courses: set[str]): + return sorted(courses) + + student = StudentModel(courses={'Math', 'Chemistry', 'English'}) + print(student.model_dump_json()) + #> {"name":"Jane","courses":["Chemistry","English","Math"]} + ``` + + See [the usage documentation](../concepts/serialization.md#serializers) for more information. + + Four signatures are supported for the decorated serializer: + + - `(self, value: Any, info: FieldSerializationInfo)` + - `(self, value: Any, nxt: SerializerFunctionWrapHandler, info: FieldSerializationInfo)` + - `(value: Any, info: SerializationInfo)` + - `(value: Any, nxt: SerializerFunctionWrapHandler, info: SerializationInfo)` + + Args: + *fields: The field names the serializer should apply to. + mode: The serialization mode. + + - `plain` means the function will be called instead of the default serialization logic, + - `wrap` means the function will be called with an argument to optionally call the + default serialization logic. + return_type: Optional return type for the function, if omitted it will be inferred from the type annotation. + when_used: Determines the serializer will be used for serialization. + check_fields: Whether to check that the fields actually exist on the model. + + Raises: + PydanticUserError: + - If the decorator is used without any arguments (at least one field name must be provided). + - If the provided field names are not strings. + """ + if callable(field) or isinstance(field, classmethod): + raise PydanticUserError( + 'The `@field_serializer` decorator cannot be used without arguments, at least one field must be provided. ' + "For example: `@field_serializer('', ...)`.", + code='decorator-missing-arguments', + ) + + fields = field, *fields + if not all(isinstance(field, str) for field in fields): + raise PydanticUserError( + 'The provided field names to the `@field_serializer` decorator should be strings. ' + "For example: `@field_serializer('', '', ...).`", + code='decorator-invalid-fields', + ) + + def dec(f: FieldSerializer) -> _decorators.PydanticDescriptorProxy[Any]: + dec_info = _decorators.FieldSerializerDecoratorInfo( + fields=fields, + mode=mode, + return_type=return_type, + when_used=when_used, + check_fields=check_fields, + ) + return _decorators.PydanticDescriptorProxy(f, dec_info) # pyright: ignore[reportArgumentType] + + return dec # pyright: ignore[reportReturnType] + + +if TYPE_CHECKING: + # The first argument in the following callables represent the `self` type: + + ModelPlainSerializerWithInfo: TypeAlias = Callable[[Any, SerializationInfo[Any]], Any] + """A model serializer method with the `info` argument, in `plain` mode.""" + + ModelPlainSerializerWithoutInfo: TypeAlias = Callable[[Any], Any] + """A model serializer method without the `info` argument, in `plain` mode.""" + + ModelPlainSerializer: TypeAlias = 'ModelPlainSerializerWithInfo | ModelPlainSerializerWithoutInfo' + """A model serializer method in `plain` mode.""" + + ModelWrapSerializerWithInfo: TypeAlias = Callable[[Any, SerializerFunctionWrapHandler, SerializationInfo[Any]], Any] + """A model serializer method with the `info` argument, in `wrap` mode.""" + + ModelWrapSerializerWithoutInfo: TypeAlias = Callable[[Any, SerializerFunctionWrapHandler], Any] + """A model serializer method without the `info` argument, in `wrap` mode.""" + + ModelWrapSerializer: TypeAlias = 'ModelWrapSerializerWithInfo | ModelWrapSerializerWithoutInfo' + """A model serializer method in `wrap` mode.""" + + ModelSerializer: TypeAlias = 'ModelPlainSerializer | ModelWrapSerializer' + + _ModelPlainSerializerT = TypeVar('_ModelPlainSerializerT', bound=ModelPlainSerializer) + _ModelWrapSerializerT = TypeVar('_ModelWrapSerializerT', bound=ModelWrapSerializer) + + +@overload +def model_serializer(f: _ModelPlainSerializerT, /) -> _ModelPlainSerializerT: ... + + +@overload +def model_serializer( + *, mode: Literal['wrap'], when_used: WhenUsed = 'always', return_type: Any = ... +) -> Callable[[_ModelWrapSerializerT], _ModelWrapSerializerT]: ... + + +@overload +def model_serializer( + *, + mode: Literal['plain'] = ..., + when_used: WhenUsed = 'always', + return_type: Any = ..., +) -> Callable[[_ModelPlainSerializerT], _ModelPlainSerializerT]: ... + + +def model_serializer( + f: _ModelPlainSerializerT | _ModelWrapSerializerT | None = None, + /, + *, + mode: Literal['plain', 'wrap'] = 'plain', + when_used: WhenUsed = 'always', + return_type: Any = PydanticUndefined, +) -> ( + _ModelPlainSerializerT + | Callable[[_ModelWrapSerializerT], _ModelWrapSerializerT] + | Callable[[_ModelPlainSerializerT], _ModelPlainSerializerT] +): + """Decorator that enables custom model serialization. + + This is useful when a model need to be serialized in a customized manner, allowing for flexibility beyond just specific fields. + + An example would be to serialize temperature to the same temperature scale, such as degrees Celsius. + + ```python + from typing import Literal + + from pydantic import BaseModel, model_serializer + + class TemperatureModel(BaseModel): + unit: Literal['C', 'F'] + value: int + + @model_serializer() + def serialize_model(self): + if self.unit == 'F': + return {'unit': 'C', 'value': int((self.value - 32) / 1.8)} + return {'unit': self.unit, 'value': self.value} + + temperature = TemperatureModel(unit='F', value=212) + print(temperature.model_dump()) + #> {'unit': 'C', 'value': 100} + ``` + + Two signatures are supported for `mode='plain'`, which is the default: + + - `(self)` + - `(self, info: SerializationInfo)` + + And two other signatures for `mode='wrap'`: + + - `(self, nxt: SerializerFunctionWrapHandler)` + - `(self, nxt: SerializerFunctionWrapHandler, info: SerializationInfo)` + + See [the usage documentation](../concepts/serialization.md#serializers) for more information. + + Args: + f: The function to be decorated. + mode: The serialization mode. + + - `'plain'` means the function will be called instead of the default serialization logic + - `'wrap'` means the function will be called with an argument to optionally call the default + serialization logic. + when_used: Determines when this serializer should be used. + return_type: The return type for the function. If omitted it will be inferred from the type annotation. + + Returns: + The decorator function. + """ + + def dec(f: ModelSerializer) -> _decorators.PydanticDescriptorProxy[Any]: + dec_info = _decorators.ModelSerializerDecoratorInfo(mode=mode, return_type=return_type, when_used=when_used) + return _decorators.PydanticDescriptorProxy(f, dec_info) + + if f is None: + return dec # pyright: ignore[reportReturnType] + else: + return dec(f) # pyright: ignore[reportReturnType] + + +AnyType = TypeVar('AnyType') + + +if TYPE_CHECKING: + SerializeAsAny = Annotated[AnyType, ...] # SerializeAsAny[list[str]] will be treated by type checkers as list[str] + """Annotation used to mark a type as having duck-typing serialization behavior. + + See [usage documentation](../concepts/serialization.md#serializing-with-duck-typing) for more details. + """ +else: + + @dataclasses.dataclass(**_internal_dataclass.slots_true) + class SerializeAsAny: + """Annotation used to mark a type as having duck-typing serialization behavior. + + See [usage documentation](../concepts/serialization.md#serializing-with-duck-typing) for more details. + """ + + def __class_getitem__(cls, item: Any) -> Any: + return Annotated[item, SerializeAsAny()] + + def __get_pydantic_core_schema__( + self, source_type: Any, handler: GetCoreSchemaHandler + ) -> core_schema.CoreSchema: + schema = handler(source_type) + schema_to_update = schema + while schema_to_update['type'] == 'definitions': + schema_to_update = schema_to_update.copy() + schema_to_update = schema_to_update['schema'] + schema_to_update['serialization'] = core_schema.simple_ser_schema('any') + return schema + + __hash__ = object.__hash__ diff --git a/source/target-vendors/cp312/pydantic/functional_validators.py b/source/target-vendors/cp312/pydantic/functional_validators.py new file mode 100644 index 0000000..558e99c --- /dev/null +++ b/source/target-vendors/cp312/pydantic/functional_validators.py @@ -0,0 +1,889 @@ +"""This module contains related classes and functions for validation.""" + +from __future__ import annotations as _annotations + +import dataclasses +import sys +import warnings +from functools import partialmethod +from typing import TYPE_CHECKING, Annotated, Any, Callable, Literal, TypeVar, Union, cast, overload + +from pydantic_core import PydanticUndefined, core_schema +from typing_extensions import Self, TypeAlias + +from ._internal import _decorators, _generics, _internal_dataclass +from .annotated_handlers import GetCoreSchemaHandler +from .errors import PydanticUserError +from .version import version_short +from .warnings import ArbitraryTypeWarning, PydanticDeprecatedSince212 + +if sys.version_info < (3, 11): + from typing_extensions import Protocol +else: + from typing import Protocol + +_inspect_validator = _decorators.inspect_validator + + +@dataclasses.dataclass(frozen=True, **_internal_dataclass.slots_true) +class AfterValidator: + """!!! abstract "Usage Documentation" + [field *after* validators](../concepts/validators.md#field-after-validator) + + A metadata class that indicates that a validation should be applied **after** the inner validation logic. + + Attributes: + func: The validator function. + + Example: + ```python + from typing import Annotated + + from pydantic import AfterValidator, BaseModel, ValidationError + + MyInt = Annotated[int, AfterValidator(lambda v: v + 1)] + + class Model(BaseModel): + a: MyInt + + print(Model(a=1).a) + #> 2 + + try: + Model(a='a') + except ValidationError as e: + print(e.json(indent=2)) + ''' + [ + { + "type": "int_parsing", + "loc": [ + "a" + ], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "a", + "url": "https://errors.pydantic.dev/2/v/int_parsing" + } + ] + ''' + ``` + """ + + func: core_schema.NoInfoValidatorFunction | core_schema.WithInfoValidatorFunction + + def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + schema = handler(source_type) + info_arg = _inspect_validator(self.func, mode='after', type='field') + if info_arg: + func = cast(core_schema.WithInfoValidatorFunction, self.func) + return core_schema.with_info_after_validator_function(func, schema=schema) + else: + func = cast(core_schema.NoInfoValidatorFunction, self.func) + return core_schema.no_info_after_validator_function(func, schema=schema) + + @classmethod + def _from_decorator(cls, decorator: _decorators.Decorator[_decorators.FieldValidatorDecoratorInfo]) -> Self: + return cls(func=decorator.func) + + +@dataclasses.dataclass(frozen=True, **_internal_dataclass.slots_true) +class BeforeValidator: + """!!! abstract "Usage Documentation" + [field *before* validators](../concepts/validators.md#field-before-validator) + + A metadata class that indicates that a validation should be applied **before** the inner validation logic. + + Attributes: + func: The validator function. + json_schema_input_type: The input type used to generate the appropriate + JSON Schema (in validation mode). The actual input type is `Any`. + + Example: + ```python + from typing import Annotated + + from pydantic import BaseModel, BeforeValidator + + MyInt = Annotated[int, BeforeValidator(lambda v: v + 1)] + + class Model(BaseModel): + a: MyInt + + print(Model(a=1).a) + #> 2 + + try: + Model(a='a') + except TypeError as e: + print(e) + #> can only concatenate str (not "int") to str + ``` + """ + + func: core_schema.NoInfoValidatorFunction | core_schema.WithInfoValidatorFunction + json_schema_input_type: Any = PydanticUndefined + + def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + schema = handler(source_type) + input_schema = ( + None + if self.json_schema_input_type is PydanticUndefined + else handler.generate_schema(self.json_schema_input_type) + ) + + info_arg = _inspect_validator(self.func, mode='before', type='field') + if info_arg: + func = cast(core_schema.WithInfoValidatorFunction, self.func) + return core_schema.with_info_before_validator_function( + func, + schema=schema, + json_schema_input_schema=input_schema, + ) + else: + func = cast(core_schema.NoInfoValidatorFunction, self.func) + return core_schema.no_info_before_validator_function( + func, schema=schema, json_schema_input_schema=input_schema + ) + + @classmethod + def _from_decorator(cls, decorator: _decorators.Decorator[_decorators.FieldValidatorDecoratorInfo]) -> Self: + return cls( + func=decorator.func, + json_schema_input_type=decorator.info.json_schema_input_type, + ) + + +@dataclasses.dataclass(frozen=True, **_internal_dataclass.slots_true) +class PlainValidator: + """!!! abstract "Usage Documentation" + [field *plain* validators](../concepts/validators.md#field-plain-validator) + + A metadata class that indicates that a validation should be applied **instead** of the inner validation logic. + + !!! note + Before v2.9, `PlainValidator` wasn't always compatible with JSON Schema generation for `mode='validation'`. + You can now use the `json_schema_input_type` argument to specify the input type of the function + to be used in the JSON schema when `mode='validation'` (the default). See the example below for more details. + + Attributes: + func: The validator function. + json_schema_input_type: The input type used to generate the appropriate + JSON Schema (in validation mode). The actual input type is `Any`. + + Example: + ```python + from typing import Annotated, Union + + from pydantic import BaseModel, PlainValidator + + def validate(v: object) -> int: + if not isinstance(v, (int, str)): + raise ValueError(f'Expected int or str, got {type(v)}') + + return int(v) + 1 + + MyInt = Annotated[ + int, + PlainValidator(validate, json_schema_input_type=Union[str, int]), # (1)! + ] + + class Model(BaseModel): + a: MyInt + + print(Model(a='1').a) + #> 2 + + print(Model(a=1).a) + #> 2 + ``` + + 1. In this example, we've specified the `json_schema_input_type` as `Union[str, int]` which indicates to the JSON schema + generator that in validation mode, the input type for the `a` field can be either a [`str`][] or an [`int`][]. + """ + + func: core_schema.NoInfoValidatorFunction | core_schema.WithInfoValidatorFunction + json_schema_input_type: Any = Any + + def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + # Note that for some valid uses of PlainValidator, it is not possible to generate a core schema for the + # source_type, so calling `handler(source_type)` will error, which prevents us from generating a proper + # serialization schema. To work around this for use cases that will not involve serialization, we simply + # catch any PydanticSchemaGenerationError that may be raised while attempting to build the serialization schema + # and abort any attempts to handle special serialization. + from pydantic import PydanticSchemaGenerationError + + try: + schema = handler(source_type) + # TODO if `schema['serialization']` is one of `'include-exclude-dict/sequence', + # schema validation will fail. That's why we use 'type ignore' comments below. + serialization = schema.get( + 'serialization', + core_schema.wrap_serializer_function_ser_schema( + function=lambda v, h: h(v), + schema=schema, + return_schema=handler.generate_schema(source_type), + ), + ) + except PydanticSchemaGenerationError: + serialization = None + + input_schema = handler.generate_schema(self.json_schema_input_type) + + info_arg = _inspect_validator(self.func, mode='plain', type='field') + if info_arg: + func = cast(core_schema.WithInfoValidatorFunction, self.func) + return core_schema.with_info_plain_validator_function( + func, + serialization=serialization, # pyright: ignore[reportArgumentType] + json_schema_input_schema=input_schema, + ) + else: + func = cast(core_schema.NoInfoValidatorFunction, self.func) + return core_schema.no_info_plain_validator_function( + func, + serialization=serialization, # pyright: ignore[reportArgumentType] + json_schema_input_schema=input_schema, + ) + + @classmethod + def _from_decorator(cls, decorator: _decorators.Decorator[_decorators.FieldValidatorDecoratorInfo]) -> Self: + return cls( + func=decorator.func, + json_schema_input_type=decorator.info.json_schema_input_type, + ) + + +@dataclasses.dataclass(frozen=True, **_internal_dataclass.slots_true) +class WrapValidator: + """!!! abstract "Usage Documentation" + [field *wrap* validators](../concepts/validators.md#field-wrap-validator) + + A metadata class that indicates that a validation should be applied **around** the inner validation logic. + + Attributes: + func: The validator function. + json_schema_input_type: The input type used to generate the appropriate + JSON Schema (in validation mode). The actual input type is `Any`. + + ```python + from datetime import datetime + from typing import Annotated + + from pydantic import BaseModel, ValidationError, WrapValidator + + def validate_timestamp(v, handler): + if v == 'now': + # we don't want to bother with further validation, just return the new value + return datetime.now() + try: + return handler(v) + except ValidationError: + # validation failed, in this case we want to return a default value + return datetime(2000, 1, 1) + + MyTimestamp = Annotated[datetime, WrapValidator(validate_timestamp)] + + class Model(BaseModel): + a: MyTimestamp + + print(Model(a='now').a) + #> 2032-01-02 03:04:05.000006 + print(Model(a='invalid').a) + #> 2000-01-01 00:00:00 + ``` + """ + + func: core_schema.NoInfoWrapValidatorFunction | core_schema.WithInfoWrapValidatorFunction + json_schema_input_type: Any = PydanticUndefined + + def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + schema = handler(source_type) + input_schema = ( + None + if self.json_schema_input_type is PydanticUndefined + else handler.generate_schema(self.json_schema_input_type) + ) + + info_arg = _inspect_validator(self.func, mode='wrap', type='field') + if info_arg: + func = cast(core_schema.WithInfoWrapValidatorFunction, self.func) + return core_schema.with_info_wrap_validator_function( + func, + schema=schema, + json_schema_input_schema=input_schema, + ) + else: + func = cast(core_schema.NoInfoWrapValidatorFunction, self.func) + return core_schema.no_info_wrap_validator_function( + func, + schema=schema, + json_schema_input_schema=input_schema, + ) + + @classmethod + def _from_decorator(cls, decorator: _decorators.Decorator[_decorators.FieldValidatorDecoratorInfo]) -> Self: + return cls( + func=decorator.func, + json_schema_input_type=decorator.info.json_schema_input_type, + ) + + +if TYPE_CHECKING: + + class _OnlyValueValidatorClsMethod(Protocol): + def __call__(self, cls: Any, value: Any, /) -> Any: ... + + class _V2ValidatorClsMethod(Protocol): + def __call__(self, cls: Any, value: Any, info: core_schema.ValidationInfo[Any], /) -> Any: ... + + class _OnlyValueWrapValidatorClsMethod(Protocol): + def __call__(self, cls: Any, value: Any, handler: core_schema.ValidatorFunctionWrapHandler, /) -> Any: ... + + class _V2WrapValidatorClsMethod(Protocol): + def __call__( + self, + cls: Any, + value: Any, + handler: core_schema.ValidatorFunctionWrapHandler, + info: core_schema.ValidationInfo[Any], + /, + ) -> Any: ... + + _V2Validator = Union[ + _V2ValidatorClsMethod, + core_schema.WithInfoValidatorFunction, + _OnlyValueValidatorClsMethod, + core_schema.NoInfoValidatorFunction, + ] + + _V2WrapValidator = Union[ + _V2WrapValidatorClsMethod, + core_schema.WithInfoWrapValidatorFunction, + _OnlyValueWrapValidatorClsMethod, + core_schema.NoInfoWrapValidatorFunction, + ] + + _PartialClsOrStaticMethod: TypeAlias = Union[classmethod[Any, Any, Any], staticmethod[Any, Any], partialmethod[Any]] + + _V2BeforeAfterOrPlainValidatorType = TypeVar( + '_V2BeforeAfterOrPlainValidatorType', + bound=Union[_V2Validator, _PartialClsOrStaticMethod], + ) + _V2WrapValidatorType = TypeVar('_V2WrapValidatorType', bound=Union[_V2WrapValidator, _PartialClsOrStaticMethod]) + +FieldValidatorModes: TypeAlias = Literal['before', 'after', 'wrap', 'plain'] + + +@overload +def field_validator( + field: str, + /, + *fields: str, + mode: Literal['wrap'], + check_fields: bool | None = ..., + json_schema_input_type: Any = ..., +) -> Callable[[_V2WrapValidatorType], _V2WrapValidatorType]: ... + + +@overload +def field_validator( + field: str, + /, + *fields: str, + mode: Literal['before', 'plain'], + check_fields: bool | None = ..., + json_schema_input_type: Any = ..., +) -> Callable[[_V2BeforeAfterOrPlainValidatorType], _V2BeforeAfterOrPlainValidatorType]: ... + + +@overload +def field_validator( + field: str, + /, + *fields: str, + mode: Literal['after'] = ..., + check_fields: bool | None = ..., +) -> Callable[[_V2BeforeAfterOrPlainValidatorType], _V2BeforeAfterOrPlainValidatorType]: ... + + +def field_validator( # noqa: D417 + field: str, + /, + *fields: str, + mode: FieldValidatorModes = 'after', + check_fields: bool | None = None, + json_schema_input_type: Any = PydanticUndefined, +) -> Callable[[Any], Any]: + """!!! abstract "Usage Documentation" + [field validators](../concepts/validators.md#field-validators) + + Decorate methods on the class indicating that they should be used to validate fields. + + Example usage: + ```python + from typing import Any + + from pydantic import ( + BaseModel, + ValidationError, + field_validator, + ) + + class Model(BaseModel): + a: str + + @field_validator('a') + @classmethod + def ensure_foobar(cls, v: Any): + if 'foobar' not in v: + raise ValueError('"foobar" not found in a') + return v + + print(repr(Model(a='this is foobar good'))) + #> Model(a='this is foobar good') + + try: + Model(a='snap') + except ValidationError as exc_info: + print(exc_info) + ''' + 1 validation error for Model + a + Value error, "foobar" not found in a [type=value_error, input_value='snap', input_type=str] + ''' + ``` + + For more in depth examples, see [Field Validators](../concepts/validators.md#field-validators). + + Args: + *fields: The field names the validator should apply to. + mode: Specifies whether to validate the fields before or after validation. + check_fields: Whether to check that the fields actually exist on the model. + json_schema_input_type: The input type of the function. This is only used to generate + the appropriate JSON Schema (in validation mode) and can only specified + when `mode` is either `'before'`, `'plain'` or `'wrap'`. + + Raises: + PydanticUserError: + - If the decorator is used without any arguments (at least one field name must be provided). + - If the provided field names are not strings. + - If `json_schema_input_type` is provided with an unsupported `mode`. + - If the decorator is applied to an instance method. + """ + if callable(field) or isinstance(field, classmethod): + raise PydanticUserError( + 'The `@field_validator` decorator cannot be used without arguments, at least one field must be provided. ' + "For example: `@field_validator('', ...)`.", + code='decorator-missing-arguments', + ) + + if mode not in ('before', 'plain', 'wrap') and json_schema_input_type is not PydanticUndefined: + raise PydanticUserError( + f"`json_schema_input_type` can't be used when mode is set to {mode!r}", + code='validator-input-type', + ) + + if json_schema_input_type is PydanticUndefined and mode == 'plain': + json_schema_input_type = Any + + fields = field, *fields + if not all(isinstance(field, str) for field in fields): + raise PydanticUserError( + 'The provided field names to the `@field_validator` decorator should be strings. ' + "For example: `@field_validator('', '', ...).`", + code='decorator-invalid-fields', + ) + + def dec( + f: Callable[..., Any] | staticmethod[Any, Any] | classmethod[Any, Any, Any], + ) -> _decorators.PydanticDescriptorProxy[Any]: + if _decorators.is_instance_method_from_sig(f): + raise PydanticUserError( + 'The `@field_validator` decorator cannot be applied to instance methods', + code='validator-instance-method', + ) + + # auto apply the @classmethod decorator + f = _decorators.ensure_classmethod_based_on_signature(f) + + dec_info = _decorators.FieldValidatorDecoratorInfo( + fields=fields, mode=mode, check_fields=check_fields, json_schema_input_type=json_schema_input_type + ) + return _decorators.PydanticDescriptorProxy(f, dec_info) + + return dec + + +_ModelType = TypeVar('_ModelType') +_ModelTypeCo = TypeVar('_ModelTypeCo', covariant=True) + + +class ModelWrapValidatorHandler(core_schema.ValidatorFunctionWrapHandler, Protocol[_ModelTypeCo]): + """`@model_validator` decorated function handler argument type. This is used when `mode='wrap'`.""" + + def __call__( # noqa: D102 + self, + value: Any, + outer_location: str | int | None = None, + /, + ) -> _ModelTypeCo: # pragma: no cover + ... + + +class ModelWrapValidatorWithoutInfo(Protocol[_ModelType]): + """A `@model_validator` decorated function signature. + This is used when `mode='wrap'` and the function does not have info argument. + """ + + def __call__( # noqa: D102 + self, + cls: type[_ModelType], + # this can be a dict, a model instance + # or anything else that gets passed to validate_python + # thus validators _must_ handle all cases + value: Any, + handler: ModelWrapValidatorHandler[_ModelType], + /, + ) -> _ModelType: ... + + +class ModelWrapValidator(Protocol[_ModelType]): + """A `@model_validator` decorated function signature. This is used when `mode='wrap'`.""" + + def __call__( # noqa: D102 + self, + cls: type[_ModelType], + # this can be a dict, a model instance + # or anything else that gets passed to validate_python + # thus validators _must_ handle all cases + value: Any, + handler: ModelWrapValidatorHandler[_ModelType], + info: core_schema.ValidationInfo, + /, + ) -> _ModelType: ... + + +class FreeModelBeforeValidatorWithoutInfo(Protocol): + """A `@model_validator` decorated function signature. + This is used when `mode='before'` and the function does not have info argument. + """ + + def __call__( # noqa: D102 + self, + # this can be a dict, a model instance + # or anything else that gets passed to validate_python + # thus validators _must_ handle all cases + value: Any, + /, + ) -> Any: ... + + +class ModelBeforeValidatorWithoutInfo(Protocol): + """A `@model_validator` decorated function signature. + This is used when `mode='before'` and the function does not have info argument. + """ + + def __call__( # noqa: D102 + self, + cls: Any, + # this can be a dict, a model instance + # or anything else that gets passed to validate_python + # thus validators _must_ handle all cases + value: Any, + /, + ) -> Any: ... + + +class FreeModelBeforeValidator(Protocol): + """A `@model_validator` decorated function signature. This is used when `mode='before'`.""" + + def __call__( # noqa: D102 + self, + # this can be a dict, a model instance + # or anything else that gets passed to validate_python + # thus validators _must_ handle all cases + value: Any, + info: core_schema.ValidationInfo[Any], + /, + ) -> Any: ... + + +class ModelBeforeValidator(Protocol): + """A `@model_validator` decorated function signature. This is used when `mode='before'`.""" + + def __call__( # noqa: D102 + self, + cls: Any, + # this can be a dict, a model instance + # or anything else that gets passed to validate_python + # thus validators _must_ handle all cases + value: Any, + info: core_schema.ValidationInfo[Any], + /, + ) -> Any: ... + + +ModelAfterValidatorWithoutInfo = Callable[[_ModelType], _ModelType] +"""A `@model_validator` decorated function signature. This is used when `mode='after'` and the function does not +have info argument. +""" + +ModelAfterValidator = Callable[[_ModelType, core_schema.ValidationInfo[Any]], _ModelType] +"""A `@model_validator` decorated function signature. This is used when `mode='after'`.""" + +_AnyModelWrapValidator = Union[ModelWrapValidator[_ModelType], ModelWrapValidatorWithoutInfo[_ModelType]] +_AnyModelBeforeValidator = Union[ + FreeModelBeforeValidator, ModelBeforeValidator, FreeModelBeforeValidatorWithoutInfo, ModelBeforeValidatorWithoutInfo +] +_AnyModelAfterValidator = Union[ModelAfterValidator[_ModelType], ModelAfterValidatorWithoutInfo[_ModelType]] + + +@overload +def model_validator( + *, + mode: Literal['wrap'], +) -> Callable[ + [_AnyModelWrapValidator[_ModelType]], _decorators.PydanticDescriptorProxy[_decorators.ModelValidatorDecoratorInfo] +]: ... + + +@overload +def model_validator( + *, + mode: Literal['before'], +) -> Callable[ + [_AnyModelBeforeValidator], _decorators.PydanticDescriptorProxy[_decorators.ModelValidatorDecoratorInfo] +]: ... + + +@overload +def model_validator( + *, + mode: Literal['after'], +) -> Callable[ + [_AnyModelAfterValidator[_ModelType]], _decorators.PydanticDescriptorProxy[_decorators.ModelValidatorDecoratorInfo] +]: ... + + +def model_validator( + *, + mode: Literal['wrap', 'before', 'after'], +) -> Any: + """!!! abstract "Usage Documentation" + [Model Validators](../concepts/validators.md#model-validators) + + Decorate model methods for validation purposes. + + Example usage: + ```python + from typing_extensions import Self + + from pydantic import BaseModel, ValidationError, model_validator + + class Square(BaseModel): + width: float + height: float + + @model_validator(mode='after') + def verify_square(self) -> Self: + if self.width != self.height: + raise ValueError('width and height do not match') + return self + + s = Square(width=1, height=1) + print(repr(s)) + #> Square(width=1.0, height=1.0) + + try: + Square(width=1, height=2) + except ValidationError as e: + print(e) + ''' + 1 validation error for Square + Value error, width and height do not match [type=value_error, input_value={'width': 1, 'height': 2}, input_type=dict] + ''' + ``` + + For more in depth examples, see [Model Validators](../concepts/validators.md#model-validators). + + Args: + mode: A required string literal that specifies the validation mode. + It can be one of the following: 'wrap', 'before', or 'after'. + + Returns: + A decorator that can be used to decorate a function to be used as a model validator. + """ + + def dec(f: Any) -> _decorators.PydanticDescriptorProxy[Any]: + # auto apply the @classmethod decorator. NOTE: in V3, do not apply the conversion for 'after' validators: + f = _decorators.ensure_classmethod_based_on_signature(f) + if mode == 'after' and isinstance(f, classmethod): + warnings.warn( + category=PydanticDeprecatedSince212, + message=( + "Using `@model_validator` with mode='after' on a classmethod is deprecated. Instead, use an instance method. " + f'See the documentation at https://docs.pydantic.dev/{version_short()}/concepts/validators/#model-after-validator.' + ), + stacklevel=2, + ) + + dec_info = _decorators.ModelValidatorDecoratorInfo(mode=mode) + return _decorators.PydanticDescriptorProxy(f, dec_info) + + return dec + + +AnyType = TypeVar('AnyType') + + +if TYPE_CHECKING: + # If we add configurable attributes to IsInstance, we'd probably need to stop hiding it from type checkers like this + InstanceOf = Annotated[AnyType, ...] # `IsInstance[Sequence]` will be recognized by type checkers as `Sequence` + +else: + + @dataclasses.dataclass(**_internal_dataclass.slots_true) + class InstanceOf: + '''Generic type for annotating a type that is an instance of a given class. + + Example: + ```python + from pydantic import BaseModel, InstanceOf + + class Foo: + ... + + class Bar(BaseModel): + foo: InstanceOf[Foo] + + Bar(foo=Foo()) + try: + Bar(foo=42) + except ValidationError as e: + print(e) + """ + [ + │ { + │ │ 'type': 'is_instance_of', + │ │ 'loc': ('foo',), + │ │ 'msg': 'Input should be an instance of Foo', + │ │ 'input': 42, + │ │ 'ctx': {'class': 'Foo'}, + │ │ 'url': 'https://errors.pydantic.dev/0.38.0/v/is_instance_of' + │ } + ] + """ + ``` + ''' + + @classmethod + def __class_getitem__(cls, item: AnyType) -> AnyType: + return Annotated[item, cls()] + + @classmethod + def __get_pydantic_core_schema__(cls, source: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + from pydantic._internal._generate_schema import GENERATE_SCHEMA_ERRORS + + # use the generic _origin_ as the second argument to isinstance when appropriate + instance_of_schema = core_schema.is_instance_schema(_generics.get_origin(source) or source) + + try: + # Try to generate the "standard" schema, which will be used when loading from JSON + original_schema = handler(source) + except GENERATE_SCHEMA_ERRORS: + # If that fails, just produce a schema that can validate from python + return instance_of_schema + else: + # Use the "original" approach to serialization + instance_of_schema['serialization'] = core_schema.wrap_serializer_function_ser_schema( + function=lambda v, h: h(v), schema=original_schema + ) + return core_schema.json_or_python_schema(python_schema=instance_of_schema, json_schema=original_schema) + + __hash__ = object.__hash__ + + +if TYPE_CHECKING: + SkipValidation = Annotated[AnyType, ...] # SkipValidation[list[str]] will be treated by type checkers as list[str] +else: + + @dataclasses.dataclass(**_internal_dataclass.slots_true) + class SkipValidation: + """If this is applied as an annotation (e.g., via `x: Annotated[int, SkipValidation]`), validation will be + skipped. You can also use `SkipValidation[int]` as a shorthand for `Annotated[int, SkipValidation]`. + + This can be useful if you want to use a type annotation for documentation/IDE/type-checking purposes, + and know that it is safe to skip validation for one or more of the fields. + + Because this converts the validation schema to `any_schema`, subsequent annotation-applied transformations + may not have the expected effects. Therefore, when used, this annotation should generally be the final + annotation applied to a type. + """ + + def __class_getitem__(cls, item: Any) -> Any: + return Annotated[item, SkipValidation()] + + @classmethod + def __get_pydantic_core_schema__(cls, source: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + with warnings.catch_warnings(): + warnings.simplefilter('ignore', ArbitraryTypeWarning) + original_schema = handler(source) + metadata = {'pydantic_js_annotation_functions': [lambda _c, h: h(original_schema)]} + return core_schema.any_schema( + metadata=metadata, + serialization=core_schema.wrap_serializer_function_ser_schema( + function=lambda v, h: h(v), schema=original_schema + ), + ) + + __hash__ = object.__hash__ + + +_FromTypeT = TypeVar('_FromTypeT') + + +class ValidateAs: + """A helper class to validate a custom type from a type that is natively supported by Pydantic. + + Args: + from_type: The type natively supported by Pydantic to use to perform validation. + instantiation_hook: A callable taking the validated type as an argument, and returning + the populated custom type. + + Example: + ```python {lint="skip"} + from typing import Annotated + + from pydantic import BaseModel, TypeAdapter, ValidateAs + + class MyCls: + def __init__(self, a: int) -> None: + self.a = a + + def __repr__(self) -> str: + return f"MyCls(a={self.a})" + + class Model(BaseModel): + a: int + + + ta = TypeAdapter( + Annotated[MyCls, ValidateAs(Model, lambda v: MyCls(a=v.a))] + ) + + print(ta.validate_python({'a': 1})) + #> MyCls(a=1) + ``` + """ + + # TODO: make use of PEP 747 + def __init__(self, from_type: type[_FromTypeT], /, instantiation_hook: Callable[[_FromTypeT], Any]) -> None: + self.from_type = from_type + self.instantiation_hook = instantiation_hook + + def __get_pydantic_core_schema__(self, source: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + schema = handler(self.from_type) + return core_schema.no_info_after_validator_function( + self.instantiation_hook, + schema=schema, + ) diff --git a/source/target-vendors/cp312/pydantic/generics.py b/source/target-vendors/cp312/pydantic/generics.py new file mode 100644 index 0000000..3f1070d --- /dev/null +++ b/source/target-vendors/cp312/pydantic/generics.py @@ -0,0 +1,5 @@ +"""The `generics` module is a backport module from V1.""" + +from ._migration import getattr_migration + +__getattr__ = getattr_migration(__name__) diff --git a/source/target-vendors/cp312/pydantic/json.py b/source/target-vendors/cp312/pydantic/json.py new file mode 100644 index 0000000..bcaff9f --- /dev/null +++ b/source/target-vendors/cp312/pydantic/json.py @@ -0,0 +1,5 @@ +"""The `json` module is a backport module from V1.""" + +from ._migration import getattr_migration + +__getattr__ = getattr_migration(__name__) diff --git a/source/target-vendors/cp312/pydantic/json_schema.py b/source/target-vendors/cp312/pydantic/json_schema.py new file mode 100644 index 0000000..b9b21fd --- /dev/null +++ b/source/target-vendors/cp312/pydantic/json_schema.py @@ -0,0 +1,2911 @@ +"""!!! abstract "Usage Documentation" + [JSON Schema](../concepts/json_schema.md) + +The `json_schema` module contains classes and functions to allow the way [JSON Schema](https://json-schema.org/) +is generated to be customized. + +In general you shouldn't need to use this module directly; instead, you can use +[`BaseModel.model_json_schema`][pydantic.BaseModel.model_json_schema] and +[`TypeAdapter.json_schema`][pydantic.TypeAdapter.json_schema]. +""" + +from __future__ import annotations as _annotations + +import collections.abc +import dataclasses +import inspect +import math +import os +import re +import warnings +from collections import Counter, defaultdict +from collections.abc import Hashable, Iterable, Sequence +from copy import deepcopy +from enum import Enum +from re import Pattern +from typing import ( + TYPE_CHECKING, + Annotated, + Any, + Callable, + Literal, + NewType, + TypeVar, + Union, + cast, + overload, +) + +import pydantic_core +from pydantic_core import MISSING, CoreSchema, PydanticOmit, core_schema, to_jsonable_python +from pydantic_core.core_schema import ComputedField +from typing_extensions import TypeAlias, assert_never, deprecated, final +from typing_inspection.introspection import get_literal_values + +from pydantic.warnings import PydanticDeprecatedSince26, PydanticDeprecatedSince29 + +from ._internal import ( + _config, + _core_metadata, + _core_utils, + _decorators, + _internal_dataclass, + _mock_val_ser, + _schema_generation_shared, + _typing_extra, +) +from .annotated_handlers import GetJsonSchemaHandler +from .config import JsonDict, JsonValue +from .errors import PydanticInvalidForJsonSchema, PydanticSchemaGenerationError, PydanticUserError + +if TYPE_CHECKING: + from . import ConfigDict + from ._internal._core_utils import CoreSchemaField, CoreSchemaOrField + from ._internal._dataclasses import PydanticDataclass + from ._internal._schema_generation_shared import GetJsonSchemaFunction + from .main import BaseModel + + +CoreSchemaOrFieldType = Literal[core_schema.CoreSchemaType, core_schema.CoreSchemaFieldType] +""" +A type alias for defined schema types that represents a union of +`core_schema.CoreSchemaType` and +`core_schema.CoreSchemaFieldType`. +""" + +JsonSchemaValue = dict[str, Any] +""" +A type alias for a JSON schema value. This is a dictionary of string keys to arbitrary JSON values. +""" + +JsonSchemaMode = Literal['validation', 'serialization'] +""" +A type alias that represents the mode of a JSON schema; either 'validation' or 'serialization'. + +For some types, the inputs to validation differ from the outputs of serialization. For example, +computed fields will only be present when serializing, and should not be provided when +validating. This flag provides a way to indicate whether you want the JSON schema required +for validation inputs, or that will be matched by serialization outputs. +""" + +_MODE_TITLE_MAPPING: dict[JsonSchemaMode, str] = {'validation': 'Input', 'serialization': 'Output'} + + +JsonSchemaWarningKind = Literal['skipped-choice', 'non-serializable-default', 'skipped-discriminator'] +""" +A type alias representing the kinds of warnings that can be emitted during JSON schema generation. + +See [`GenerateJsonSchema.render_warning_message`][pydantic.json_schema.GenerateJsonSchema.render_warning_message] +for more details. +""" + + +class PydanticJsonSchemaWarning(UserWarning): + """This class is used to emit warnings produced during JSON schema generation. + See the [`GenerateJsonSchema.emit_warning`][pydantic.json_schema.GenerateJsonSchema.emit_warning] and + [`GenerateJsonSchema.render_warning_message`][pydantic.json_schema.GenerateJsonSchema.render_warning_message] + methods for more details; these can be overridden to control warning behavior. + """ + + +NoDefault = object() +"""A sentinel value used to indicate that no default value should be used when generating a JSON Schema +for a core schema with a default value. +""" + + +# ##### JSON Schema Generation ##### +DEFAULT_REF_TEMPLATE = '#/$defs/{model}' +"""The default format string used to generate reference names.""" + +# There are three types of references relevant to building JSON schemas: +# 1. core_schema "ref" values; these are not exposed as part of the JSON schema +# * these might look like the fully qualified path of a model, its id, or something similar +CoreRef = NewType('CoreRef', str) +# 2. keys of the "definitions" object that will eventually go into the JSON schema +# * by default, these look like "MyModel", though may change in the presence of collisions +# * eventually, we may want to make it easier to modify the way these names are generated +DefsRef = NewType('DefsRef', str) +# 3. the values corresponding to the "$ref" key in the schema +# * By default, these look like "#/$defs/MyModel", as in {"$ref": "#/$defs/MyModel"} +JsonRef = NewType('JsonRef', str) + +CoreModeRef = tuple[CoreRef, JsonSchemaMode] +JsonSchemaKeyT = TypeVar('JsonSchemaKeyT', bound=Hashable) + +_PRIMITIVE_JSON_SCHEMA_TYPES = ('string', 'boolean', 'null', 'integer', 'number') + + +@dataclasses.dataclass(**_internal_dataclass.slots_true) +class _DefinitionsRemapping: + defs_remapping: dict[DefsRef, DefsRef] + json_remapping: dict[JsonRef, JsonRef] + + @staticmethod + def from_prioritized_choices( + prioritized_choices: dict[DefsRef, list[DefsRef]], + defs_to_json: dict[DefsRef, JsonRef], + definitions: dict[DefsRef, JsonSchemaValue], + ) -> _DefinitionsRemapping: + """ + This function should produce a remapping that replaces complex DefsRef with the simpler ones from the + prioritized_choices such that applying the name remapping would result in an equivalent JSON schema. + """ + # We need to iteratively simplify the definitions until we reach a fixed point. + # The reason for this is that outer definitions may reference inner definitions that get simplified + # into an equivalent reference, and the outer definitions won't be equivalent until we've simplified + # the inner definitions. + copied_definitions = deepcopy(definitions) + definitions_schema = {'$defs': copied_definitions} + for _iter in range(100): # prevent an infinite loop in the case of a bug, 100 iterations should be enough + # For every possible remapped DefsRef, collect all schemas that DefsRef might be used for: + schemas_for_alternatives: dict[DefsRef, list[JsonSchemaValue]] = defaultdict(list) + for defs_ref in copied_definitions: + alternatives = prioritized_choices[defs_ref] + for alternative in alternatives: + schemas_for_alternatives[alternative].append(copied_definitions[defs_ref]) + + # Deduplicate the schemas for each alternative; the idea is that we only want to remap to a new DefsRef + # if it introduces no ambiguity, i.e., there is only one distinct schema for that DefsRef. + for defs_ref in schemas_for_alternatives: + schemas_for_alternatives[defs_ref] = _deduplicate_schemas(schemas_for_alternatives[defs_ref]) + + # Build the remapping + defs_remapping: dict[DefsRef, DefsRef] = {} + json_remapping: dict[JsonRef, JsonRef] = {} + for original_defs_ref in definitions: + alternatives = prioritized_choices[original_defs_ref] + # Pick the first alternative that has only one schema, since that means there is no collision + remapped_defs_ref = next(x for x in alternatives if len(schemas_for_alternatives[x]) == 1) + defs_remapping[original_defs_ref] = remapped_defs_ref + + # Map all alternatives after the remapped one to the remapped one + # This ensures that intermediate simplifications are also remapped + remapped_index = alternatives.index(remapped_defs_ref) + for alt in alternatives[remapped_index:]: + json_remapping[defs_to_json[alt]] = defs_to_json[remapped_defs_ref] + remapping = _DefinitionsRemapping(defs_remapping, json_remapping) + new_definitions_schema = remapping.remap_json_schema({'$defs': copied_definitions}) + if definitions_schema == new_definitions_schema: + # We've reached the fixed point + return remapping + definitions_schema = new_definitions_schema + + raise PydanticInvalidForJsonSchema('Failed to simplify the JSON schema definitions') + + def remap_defs_ref(self, ref: DefsRef) -> DefsRef: + return self.defs_remapping.get(ref, ref) + + def remap_json_ref(self, ref: JsonRef) -> JsonRef: + return self.json_remapping.get(ref, ref) + + def remap_json_schema(self, schema: Any) -> Any: + """ + Recursively update the JSON schema replacing all $refs + """ + if isinstance(schema, str): + # Note: this may not really be a JsonRef; we rely on having no collisions between JsonRefs and other strings + return self.remap_json_ref(JsonRef(schema)) + elif isinstance(schema, list): + return [self.remap_json_schema(item) for item in schema] + elif isinstance(schema, dict): + for key, value in schema.items(): + if key == '$ref' and isinstance(value, str): + schema['$ref'] = self.remap_json_ref(JsonRef(value)) + elif key == '$defs': + schema['$defs'] = { + self.remap_defs_ref(DefsRef(key)): self.remap_json_schema(value) + for key, value in schema['$defs'].items() + } + else: + schema[key] = self.remap_json_schema(value) + return schema + + +class GenerateJsonSchema: + """!!! abstract "Usage Documentation" + [Customizing the JSON Schema Generation Process](../concepts/json_schema.md#customizing-the-json-schema-generation-process) + + A class for generating JSON schemas. + + This class generates JSON schemas based on configured parameters. The default schema dialect + is [https://json-schema.org/draft/2020-12/schema](https://json-schema.org/draft/2020-12/schema). + The class uses `by_alias` to configure how fields with + multiple names are handled and `ref_template` to format reference names. + + Attributes: + schema_dialect: The JSON schema dialect used to generate the schema. See + [Declaring a Dialect](https://json-schema.org/understanding-json-schema/reference/schema.html#id4) + in the JSON Schema documentation for more information about dialects. + ignored_warning_kinds: Warnings to ignore when generating the schema. `self.render_warning_message` will + do nothing if its argument `kind` is in `ignored_warning_kinds`; + this value can be modified on subclasses to easily control which warnings are emitted. + by_alias: Whether to use field aliases when generating the schema. + ref_template: The format string used when generating reference names. + core_to_json_refs: A mapping of core refs to JSON refs. + core_to_defs_refs: A mapping of core refs to definition refs. + defs_to_core_refs: A mapping of definition refs to core refs. + json_to_defs_refs: A mapping of JSON refs to definition refs. + definitions: Definitions in the schema. + + Args: + by_alias: Whether to use field aliases in the generated schemas. + ref_template: The format string to use when generating reference names. + union_format: The format to use when combining schemas from unions together. Can be one of: + + - `'any_of'`: Use the [`anyOf`](https://json-schema.org/understanding-json-schema/reference/combining#anyOf) + keyword to combine schemas (the default). + - `'primitive_type_array'`: Use the [`type`](https://json-schema.org/understanding-json-schema/reference/type) + keyword as an array of strings, containing each type of the combination. If any of the schemas is not a primitive + type (`string`, `boolean`, `null`, `integer` or `number`) or contains constraints/metadata, falls back to + `any_of`. + + Raises: + JsonSchemaError: If the instance of the class is inadvertently reused after generating a schema. + """ + + schema_dialect = 'https://json-schema.org/draft/2020-12/schema' + + # `self.render_warning_message` will do nothing if its argument `kind` is in `ignored_warning_kinds`; + # this value can be modified on subclasses to easily control which warnings are emitted + ignored_warning_kinds: set[JsonSchemaWarningKind] = {'skipped-choice'} + + def __init__( + self, + by_alias: bool = True, + ref_template: str = DEFAULT_REF_TEMPLATE, + union_format: Literal['any_of', 'primitive_type_array'] = 'any_of', + ) -> None: + self.by_alias = by_alias + self.ref_template = ref_template + self.union_format: Literal['any_of', 'primitive_type_array'] = union_format + + self.core_to_json_refs: dict[CoreModeRef, JsonRef] = {} + self.core_to_defs_refs: dict[CoreModeRef, DefsRef] = {} + self.defs_to_core_refs: dict[DefsRef, CoreModeRef] = {} + self.json_to_defs_refs: dict[JsonRef, DefsRef] = {} + + self.definitions: dict[DefsRef, JsonSchemaValue] = {} + self._config_wrapper_stack = _config.ConfigWrapperStack(_config.ConfigWrapper({})) + + self._mode: JsonSchemaMode = 'validation' + + # The following includes a mapping of a fully-unique defs ref choice to a list of preferred + # alternatives, which are generally simpler, such as only including the class name. + # At the end of schema generation, we use these to produce a JSON schema with more human-readable + # definitions, which would also work better in a generated OpenAPI client, etc. + self._prioritized_defsref_choices: dict[DefsRef, list[DefsRef]] = {} + self._collision_counter: dict[str, int] = defaultdict(int) + self._collision_index: dict[str, int] = {} + + self._schema_type_to_method = self.build_schema_type_to_method() + + # When we encounter definitions we need to try to build them immediately + # so that they are available schemas that reference them + # But it's possible that CoreSchema was never going to be used + # (e.g. because the CoreSchema that references short circuits is JSON schema generation without needing + # the reference) so instead of failing altogether if we can't build a definition we + # store the error raised and re-throw it if we end up needing that def + self._core_defs_invalid_for_json_schema: dict[DefsRef, PydanticInvalidForJsonSchema] = {} + + # This changes to True after generating a schema, to prevent issues caused by accidental reuse + # of a single instance of a schema generator + self._used = False + + @property + def _config(self) -> _config.ConfigWrapper: + return self._config_wrapper_stack.tail + + @property + def mode(self) -> JsonSchemaMode: + if self._config.json_schema_mode_override is not None: + return self._config.json_schema_mode_override + else: + return self._mode + + def build_schema_type_to_method( + self, + ) -> dict[CoreSchemaOrFieldType, Callable[[CoreSchemaOrField], JsonSchemaValue]]: + """Builds a dictionary mapping fields to methods for generating JSON schemas. + + Returns: + A dictionary containing the mapping of `CoreSchemaOrFieldType` to a handler method. + + Raises: + TypeError: If no method has been defined for generating a JSON schema for a given pydantic core schema type. + """ + mapping: dict[CoreSchemaOrFieldType, Callable[[CoreSchemaOrField], JsonSchemaValue]] = {} + core_schema_types: list[CoreSchemaOrFieldType] = list(get_literal_values(CoreSchemaOrFieldType)) + for key in core_schema_types: + method_name = f'{key.replace("-", "_")}_schema' + try: + mapping[key] = getattr(self, method_name) + except AttributeError as e: # pragma: no cover + if os.getenv('PYDANTIC_PRIVATE_ALLOW_UNHANDLED_SCHEMA_TYPES'): + continue + raise TypeError( + f'No method for generating JsonSchema for core_schema.type={key!r} ' + f'(expected: {type(self).__name__}.{method_name})' + ) from e + return mapping + + def generate_definitions( + self, inputs: Sequence[tuple[JsonSchemaKeyT, JsonSchemaMode, core_schema.CoreSchema]] + ) -> tuple[dict[tuple[JsonSchemaKeyT, JsonSchemaMode], JsonSchemaValue], dict[DefsRef, JsonSchemaValue]]: + """Generates JSON schema definitions from a list of core schemas, pairing the generated definitions with a + mapping that links the input keys to the definition references. + + Args: + inputs: A sequence of tuples, where: + + - The first element is a JSON schema key type. + - The second element is the JSON mode: either 'validation' or 'serialization'. + - The third element is a core schema. + + Returns: + A tuple where: + + - The first element is a dictionary whose keys are tuples of JSON schema key type and JSON mode, and + whose values are the JSON schema corresponding to that pair of inputs. (These schemas may have + JsonRef references to definitions that are defined in the second returned element.) + - The second element is a dictionary whose keys are definition references for the JSON schemas + from the first returned element, and whose values are the actual JSON schema definitions. + + Raises: + PydanticUserError: Raised if the JSON schema generator has already been used to generate a JSON schema. + """ + if self._used: + raise PydanticUserError( + 'This JSON schema generator has already been used to generate a JSON schema. ' + f'You must create a new instance of {type(self).__name__} to generate a new JSON schema.', + code='json-schema-already-used', + ) + + for _, mode, schema in inputs: + self._mode = mode + self.generate_inner(schema) + + definitions_remapping = self._build_definitions_remapping() + + json_schemas_map: dict[tuple[JsonSchemaKeyT, JsonSchemaMode], DefsRef] = {} + for key, mode, schema in inputs: + self._mode = mode + json_schema = self.generate_inner(schema) + json_schemas_map[(key, mode)] = definitions_remapping.remap_json_schema(json_schema) + + json_schema = {'$defs': self.definitions} + json_schema = definitions_remapping.remap_json_schema(json_schema) + self._used = True + return json_schemas_map, self.sort(json_schema['$defs']) # type: ignore + + def generate(self, schema: CoreSchema, mode: JsonSchemaMode = 'validation') -> JsonSchemaValue: + """Generates a JSON schema for a specified schema in a specified mode. + + Args: + schema: A Pydantic model. + mode: The mode in which to generate the schema. Defaults to 'validation'. + + Returns: + A JSON schema representing the specified schema. + + Raises: + PydanticUserError: If the JSON schema generator has already been used to generate a JSON schema. + """ + self._mode = mode + if self._used: + raise PydanticUserError( + 'This JSON schema generator has already been used to generate a JSON schema. ' + f'You must create a new instance of {type(self).__name__} to generate a new JSON schema.', + code='json-schema-already-used', + ) + + json_schema: JsonSchemaValue = self.generate_inner(schema) + json_ref_counts = self.get_json_ref_counts(json_schema) + + ref = cast(JsonRef, json_schema.get('$ref')) + while ref is not None: # may need to unpack multiple levels + ref_json_schema = self.get_schema_from_definitions(ref) + if json_ref_counts[ref] == 1 and ref_json_schema is not None and len(json_schema) == 1: + # "Unpack" the ref since this is the only reference and there are no sibling keys + json_schema = ref_json_schema.copy() # copy to prevent recursive dict reference + json_ref_counts[ref] -= 1 + ref = cast(JsonRef, json_schema.get('$ref')) + ref = None + + self._garbage_collect_definitions(json_schema) + definitions_remapping = self._build_definitions_remapping() + + if self.definitions: + json_schema['$defs'] = self.definitions + + json_schema = definitions_remapping.remap_json_schema(json_schema) + + # For now, we will not set the $schema key. However, if desired, this can be easily added by overriding + # this method and adding the following line after a call to super().generate(schema): + # json_schema['$schema'] = self.schema_dialect + + self._used = True + return self.sort(json_schema) + + def generate_inner(self, schema: CoreSchemaOrField) -> JsonSchemaValue: # noqa: C901 + """Generates a JSON schema for a given core schema. + + Args: + schema: The given core schema. + + Returns: + The generated JSON schema. + + TODO: the nested function definitions here seem like bad practice, I'd like to unpack these + in a future PR. It'd be great if we could shorten the call stack a bit for JSON schema generation, + and I think there's potential for that here. + """ + # If a schema with the same CoreRef has been handled, just return a reference to it + # Note that this assumes that it will _never_ be the case that the same CoreRef is used + # on types that should have different JSON schemas + if 'ref' in schema: + core_ref = CoreRef(schema['ref']) # type: ignore[typeddict-item] + core_mode_ref = (core_ref, self.mode) + if core_mode_ref in self.core_to_defs_refs and self.core_to_defs_refs[core_mode_ref] in self.definitions: + return {'$ref': self.core_to_json_refs[core_mode_ref]} + + def populate_defs(core_schema: CoreSchema, json_schema: JsonSchemaValue) -> JsonSchemaValue: + if 'ref' in core_schema: + core_ref = CoreRef(core_schema['ref']) # type: ignore[typeddict-item] + defs_ref, ref_json_schema = self.get_cache_defs_ref_schema(core_ref) + json_ref = JsonRef(ref_json_schema['$ref']) + # Replace the schema if it's not a reference to itself + # What we want to avoid is having the def be just a ref to itself + # which is what would happen if we blindly assigned any + if json_schema.get('$ref', None) != json_ref: + self.definitions[defs_ref] = json_schema + self._core_defs_invalid_for_json_schema.pop(defs_ref, None) + json_schema = ref_json_schema + return json_schema + + def handler_func(schema_or_field: CoreSchemaOrField) -> JsonSchemaValue: + """Generate a JSON schema based on the input schema. + + Args: + schema_or_field: The core schema to generate a JSON schema from. + + Returns: + The generated JSON schema. + + Raises: + TypeError: If an unexpected schema type is encountered. + """ + # Generate the core-schema-type-specific bits of the schema generation: + json_schema: JsonSchemaValue | None = None + if self.mode == 'serialization' and 'serialization' in schema_or_field: + # In this case, we skip the JSON Schema generation of the schema + # and use the `'serialization'` schema instead (canonical example: + # `Annotated[int, PlainSerializer(str)]`). + ser_schema = schema_or_field['serialization'] # type: ignore + json_schema = self.ser_schema(ser_schema) + + # It might be that the 'serialization'` is skipped depending on `when_used`. + # This is only relevant for `nullable` schemas though, so we special case here. + if ( + json_schema is not None + and ser_schema.get('when_used') in ('unless-none', 'json-unless-none') + and schema_or_field['type'] == 'nullable' + ): + json_schema = self.get_union_of_schemas([{'type': 'null'}, json_schema]) + if json_schema is None: + if _core_utils.is_core_schema(schema_or_field) or _core_utils.is_core_schema_field(schema_or_field): + generate_for_schema_type = self._schema_type_to_method[schema_or_field['type']] + json_schema = generate_for_schema_type(schema_or_field) + else: + raise TypeError(f'Unexpected schema type: schema={schema_or_field}') + return json_schema + + current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, handler_func) + + metadata = cast(_core_metadata.CoreMetadata, schema.get('metadata', {})) + + # TODO: I dislike that we have to wrap these basic dict updates in callables, is there any way around this? + + if js_updates := metadata.get('pydantic_js_updates'): + + def js_updates_handler_func( + schema_or_field: CoreSchemaOrField, + current_handler: GetJsonSchemaHandler = current_handler, + ) -> JsonSchemaValue: + json_schema = {**current_handler(schema_or_field), **js_updates} + return json_schema + + current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, js_updates_handler_func) + + if js_extra := metadata.get('pydantic_js_extra'): + + def js_extra_handler_func( + schema_or_field: CoreSchemaOrField, + current_handler: GetJsonSchemaHandler = current_handler, + ) -> JsonSchemaValue: + json_schema = current_handler(schema_or_field) + if isinstance(js_extra, dict): + json_schema.update(to_jsonable_python(js_extra)) + elif callable(js_extra): + # similar to typing issue in _update_class_schema when we're working with callable js extra + js_extra(json_schema) # type: ignore + return json_schema + + current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, js_extra_handler_func) + + for js_modify_function in metadata.get('pydantic_js_functions', ()): + + def new_handler_func( + schema_or_field: CoreSchemaOrField, + current_handler: GetJsonSchemaHandler = current_handler, + js_modify_function: GetJsonSchemaFunction = js_modify_function, + ) -> JsonSchemaValue: + json_schema = js_modify_function(schema_or_field, current_handler) + if _core_utils.is_core_schema(schema_or_field): + json_schema = populate_defs(schema_or_field, json_schema) + original_schema = current_handler.resolve_ref_schema(json_schema) + ref = json_schema.pop('$ref', None) + if ref and json_schema: + original_schema.update(json_schema) + return original_schema + + current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, new_handler_func) + + for js_modify_function in metadata.get('pydantic_js_annotation_functions', ()): + + def new_handler_func( + schema_or_field: CoreSchemaOrField, + current_handler: GetJsonSchemaHandler = current_handler, + js_modify_function: GetJsonSchemaFunction = js_modify_function, + ) -> JsonSchemaValue: + return js_modify_function(schema_or_field, current_handler) + + current_handler = _schema_generation_shared.GenerateJsonSchemaHandler(self, new_handler_func) + + json_schema = current_handler(schema) + if _core_utils.is_core_schema(schema): + json_schema = populate_defs(schema, json_schema) + return json_schema + + def sort(self, value: JsonSchemaValue, parent_key: str | None = None) -> JsonSchemaValue: + """Override this method to customize the sorting of the JSON schema (e.g., don't sort at all, sort all keys unconditionally, etc.) + + By default, alphabetically sort the keys in the JSON schema, skipping the 'properties' and 'default' keys to preserve field definition order. + This sort is recursive, so it will sort all nested dictionaries as well. + """ + sorted_dict: dict[str, JsonSchemaValue] = {} + keys = value.keys() + if parent_key not in ('properties', 'default'): + keys = sorted(keys) + for key in keys: + sorted_dict[key] = self._sort_recursive(value[key], parent_key=key) + return sorted_dict + + def _sort_recursive(self, value: Any, parent_key: str | None = None) -> Any: + """Recursively sort a JSON schema value.""" + if isinstance(value, dict): + sorted_dict: dict[str, JsonSchemaValue] = {} + keys = value.keys() + if parent_key not in ('properties', 'default'): + keys = sorted(keys) + for key in keys: + sorted_dict[key] = self._sort_recursive(value[key], parent_key=key) + return sorted_dict + elif isinstance(value, list): + sorted_list: list[JsonSchemaValue] = [self._sort_recursive(item, parent_key) for item in value] + return sorted_list + else: + return value + + # ### Schema generation methods + + def invalid_schema(self, schema: core_schema.InvalidSchema) -> JsonSchemaValue: + """Placeholder - should never be called.""" + + raise RuntimeError('Cannot generate schema for invalid_schema. This is a bug! Please report it.') + + def any_schema(self, schema: core_schema.AnySchema) -> JsonSchemaValue: + """Generates a JSON schema that matches any value. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + return {} + + def none_schema(self, schema: core_schema.NoneSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches `None`. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + return {'type': 'null'} + + def bool_schema(self, schema: core_schema.BoolSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a bool value. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + return {'type': 'boolean'} + + def int_schema(self, schema: core_schema.IntSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches an int value. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + json_schema: dict[str, Any] = {'type': 'integer'} + self.update_with_validations(json_schema, schema, self.ValidationsMapping.numeric) + json_schema = {k: v for k, v in json_schema.items() if v not in {math.inf, -math.inf}} + return json_schema + + def float_schema(self, schema: core_schema.FloatSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a float value. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + json_schema: dict[str, Any] = {'type': 'number'} + self.update_with_validations(json_schema, schema, self.ValidationsMapping.numeric) + json_schema = {k: v for k, v in json_schema.items() if v not in {math.inf, -math.inf}} + return json_schema + + def decimal_schema(self, schema: core_schema.DecimalSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a decimal value. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + + def get_decimal_pattern(schema: core_schema.DecimalSchema) -> str: + max_digits = schema.get('max_digits') + decimal_places = schema.get('decimal_places') + + pattern = ( + r'^(?!^[-+.]*$)[+-]?0*' # check it is not empty string and not one or sequence of ".+-" characters. + ) + + # Case 1: Both max_digits and decimal_places are set + if max_digits is not None and decimal_places is not None: + integer_places = max(0, max_digits - decimal_places) + pattern += ( + rf'(?:' + rf'\d{{0,{integer_places}}}' + rf'|' + rf'(?=[\d.]{{1,{max_digits + 1}}}0*$)' + rf'\d{{0,{integer_places}}}\.\d{{0,{decimal_places}}}0*$' + rf')' + ) + + # Case 2: Only max_digits is set + elif max_digits is not None and decimal_places is None: + pattern += ( + rf'(?:' + rf'\d{{0,{max_digits}}}' + rf'|' + rf'(?=[\d.]{{1,{max_digits + 1}}}0*$)' + rf'\d*\.\d*0*$' + rf')' + ) + + # Case 3: Only decimal_places is set + elif max_digits is None and decimal_places is not None: + pattern += rf'\d*\.?\d{{0,{decimal_places}}}0*$' + + # Case 4: Both are None (no restrictions) + else: + pattern += r'\d*\.?\d*$' # look for arbitrary integer or decimal + + return pattern + + json_schema = self.str_schema(core_schema.str_schema(pattern=get_decimal_pattern(schema))) + if self.mode == 'validation': + multiple_of = schema.get('multiple_of') + le = schema.get('le') + ge = schema.get('ge') + lt = schema.get('lt') + gt = schema.get('gt') + json_schema = { + 'anyOf': [ + self.float_schema( + core_schema.float_schema( + allow_inf_nan=schema.get('allow_inf_nan'), + multiple_of=None if multiple_of is None else float(multiple_of), + le=None if le is None else float(le), + ge=None if ge is None else float(ge), + lt=None if lt is None else float(lt), + gt=None if gt is None else float(gt), + ) + ), + json_schema, + ], + } + return json_schema + + def str_schema(self, schema: core_schema.StringSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a string value. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + json_schema = {'type': 'string'} + self.update_with_validations(json_schema, schema, self.ValidationsMapping.string) + if isinstance(json_schema.get('pattern'), Pattern): + # TODO: should we add regex flags to the pattern? + json_schema['pattern'] = json_schema.get('pattern').pattern # type: ignore + return json_schema + + def bytes_schema(self, schema: core_schema.BytesSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a bytes value. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + json_schema = {'type': 'string', 'format': 'base64url' if self._config.ser_json_bytes == 'base64' else 'binary'} + self.update_with_validations(json_schema, schema, self.ValidationsMapping.bytes) + return json_schema + + def date_schema(self, schema: core_schema.DateSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a date value. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + return {'type': 'string', 'format': 'date'} + + def time_schema(self, schema: core_schema.TimeSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a time value. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + return {'type': 'string', 'format': 'time'} + + def datetime_schema(self, schema: core_schema.DatetimeSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a datetime value. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + return {'type': 'string', 'format': 'date-time'} + + def timedelta_schema(self, schema: core_schema.TimedeltaSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a timedelta value. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + if self._config.ser_json_timedelta == 'float': + return {'type': 'number'} + return {'type': 'string', 'format': 'duration'} + + def literal_schema(self, schema: core_schema.LiteralSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a literal value. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + expected = [to_jsonable_python(v.value if isinstance(v, Enum) else v) for v in schema['expected']] + + result: dict[str, Any] = {} + if len(expected) == 1: + result['const'] = expected[0] + else: + result['enum'] = expected + + types = {type(e) for e in expected} + if types == {str}: + result['type'] = 'string' + elif types == {int}: + result['type'] = 'integer' + elif types == {float}: + result['type'] = 'number' + elif types == {bool}: + result['type'] = 'boolean' + elif types == {list}: + result['type'] = 'array' + elif types == {type(None)}: + result['type'] = 'null' + return result + + def missing_sentinel_schema(self, schema: core_schema.MissingSentinelSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches the `MISSING` sentinel value. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + raise PydanticOmit + + def enum_schema(self, schema: core_schema.EnumSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches an Enum value. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + enum_type = schema['cls'] + description = None if not enum_type.__doc__ else inspect.cleandoc(enum_type.__doc__) + if ( + description == 'An enumeration.' + ): # This is the default value provided by enum.EnumMeta.__new__; don't use it + description = None + result: dict[str, Any] = {'title': enum_type.__name__, 'description': description} + result = {k: v for k, v in result.items() if v is not None} + + expected = [to_jsonable_python(v.value) for v in schema['members']] + + result['enum'] = expected + + types = {type(e) for e in expected} + if isinstance(enum_type, str) or types == {str}: + result['type'] = 'string' + elif isinstance(enum_type, int) or types == {int}: + result['type'] = 'integer' + elif isinstance(enum_type, float) or types == {float}: + result['type'] = 'number' + elif types == {bool}: + result['type'] = 'boolean' + elif types == {list}: + result['type'] = 'array' + + return result + + def is_instance_schema(self, schema: core_schema.IsInstanceSchema) -> JsonSchemaValue: + """Handles JSON schema generation for a core schema that checks if a value is an instance of a class. + + Unless overridden in a subclass, this raises an error. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + return self.handle_invalid_for_json_schema(schema, f'core_schema.IsInstanceSchema ({schema["cls"]})') + + def is_subclass_schema(self, schema: core_schema.IsSubclassSchema) -> JsonSchemaValue: + """Handles JSON schema generation for a core schema that checks if a value is a subclass of a class. + + For backwards compatibility with v1, this does not raise an error, but can be overridden to change this. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + # Note: This is for compatibility with V1; you can override if you want different behavior. + return {} + + def callable_schema(self, schema: core_schema.CallableSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a callable value. + + Unless overridden in a subclass, this raises an error. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + return self.handle_invalid_for_json_schema(schema, 'core_schema.CallableSchema') + + def list_schema(self, schema: core_schema.ListSchema) -> JsonSchemaValue: + """Returns a schema that matches a list schema. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + items_schema = {} if 'items_schema' not in schema else self.generate_inner(schema['items_schema']) + json_schema = {'type': 'array', 'items': items_schema} + self.update_with_validations(json_schema, schema, self.ValidationsMapping.array) + return json_schema + + @deprecated('`tuple_positional_schema` is deprecated. Use `tuple_schema` instead.', category=None) + @final + def tuple_positional_schema(self, schema: core_schema.TupleSchema) -> JsonSchemaValue: + """Replaced by `tuple_schema`.""" + warnings.warn( + '`tuple_positional_schema` is deprecated. Use `tuple_schema` instead.', + PydanticDeprecatedSince26, + stacklevel=2, + ) + return self.tuple_schema(schema) + + @deprecated('`tuple_variable_schema` is deprecated. Use `tuple_schema` instead.', category=None) + @final + def tuple_variable_schema(self, schema: core_schema.TupleSchema) -> JsonSchemaValue: + """Replaced by `tuple_schema`.""" + warnings.warn( + '`tuple_variable_schema` is deprecated. Use `tuple_schema` instead.', + PydanticDeprecatedSince26, + stacklevel=2, + ) + return self.tuple_schema(schema) + + def tuple_schema(self, schema: core_schema.TupleSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a tuple schema e.g. `tuple[int, + str, bool]` or `tuple[int, ...]`. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + json_schema: JsonSchemaValue = {'type': 'array'} + if 'variadic_item_index' in schema: + variadic_item_index = schema['variadic_item_index'] + if variadic_item_index > 0: + json_schema['minItems'] = variadic_item_index + json_schema['prefixItems'] = [ + self.generate_inner(item) for item in schema['items_schema'][:variadic_item_index] + ] + if variadic_item_index + 1 == len(schema['items_schema']): + # if the variadic item is the last item, then represent it faithfully + json_schema['items'] = self.generate_inner(schema['items_schema'][variadic_item_index]) + else: + # otherwise, 'items' represents the schema for the variadic + # item plus the suffix, so just allow anything for simplicity + # for now + json_schema['items'] = True + else: + prefixItems = [self.generate_inner(item) for item in schema['items_schema']] + if prefixItems: + json_schema['prefixItems'] = prefixItems + json_schema['minItems'] = len(prefixItems) + json_schema['maxItems'] = len(prefixItems) + self.update_with_validations(json_schema, schema, self.ValidationsMapping.array) + return json_schema + + def set_schema(self, schema: core_schema.SetSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a set schema. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + return self._common_set_schema(schema) + + def frozenset_schema(self, schema: core_schema.FrozenSetSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a frozenset schema. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + return self._common_set_schema(schema) + + def _common_set_schema(self, schema: core_schema.SetSchema | core_schema.FrozenSetSchema) -> JsonSchemaValue: + items_schema = {} if 'items_schema' not in schema else self.generate_inner(schema['items_schema']) + json_schema = {'type': 'array', 'uniqueItems': True, 'items': items_schema} + self.update_with_validations(json_schema, schema, self.ValidationsMapping.array) + return json_schema + + def generator_schema(self, schema: core_schema.GeneratorSchema) -> JsonSchemaValue: + """Returns a JSON schema that represents the provided GeneratorSchema. + + Args: + schema: The schema. + + Returns: + The generated JSON schema. + """ + items_schema = {} if 'items_schema' not in schema else self.generate_inner(schema['items_schema']) + json_schema = {'type': 'array', 'items': items_schema} + self.update_with_validations(json_schema, schema, self.ValidationsMapping.array) + return json_schema + + def dict_schema(self, schema: core_schema.DictSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a dict schema. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + json_schema: JsonSchemaValue = {'type': 'object'} + + keys_schema = self.generate_inner(schema['keys_schema']).copy() if 'keys_schema' in schema else {} + if '$ref' not in keys_schema: + keys_pattern = keys_schema.pop('pattern', None) + # Don't give a title to patternProperties/propertyNames: + keys_schema.pop('title', None) + else: + # Here, we assume that if the keys schema is a definition reference, + # it can't be a simple string core schema (and thus no pattern can exist). + # However, this is only in practice (in theory, a definition reference core + # schema could be generated for a simple string schema). + # Note that we avoid calling `self.resolve_ref_schema`, as it might not exist yet. + keys_pattern = None + + values_schema = self.generate_inner(schema['values_schema']).copy() if 'values_schema' in schema else {} + # don't give a title to additionalProperties: + values_schema.pop('title', None) + + if values_schema or keys_pattern is not None: + if keys_pattern is None: + json_schema['additionalProperties'] = values_schema + else: + json_schema['patternProperties'] = {keys_pattern: values_schema} + else: # for `dict[str, Any]`, we allow any key and any value, since `str` is the default key type + json_schema['additionalProperties'] = True + + if ( + # The len check indicates that constraints are probably present: + (keys_schema.get('type') == 'string' and len(keys_schema) > 1) + # If this is a definition reference schema, it most likely has constraints: + or '$ref' in keys_schema + ): + keys_schema.pop('type', None) + json_schema['propertyNames'] = keys_schema + + self.update_with_validations(json_schema, schema, self.ValidationsMapping.object) + return json_schema + + def function_before_schema(self, schema: core_schema.BeforeValidatorFunctionSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a function-before schema. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + if self.mode == 'validation' and (input_schema := schema.get('json_schema_input_schema')): + return self.generate_inner(input_schema) + + return self.generate_inner(schema['schema']) + + def function_after_schema(self, schema: core_schema.AfterValidatorFunctionSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a function-after schema. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + return self.generate_inner(schema['schema']) + + def function_plain_schema(self, schema: core_schema.PlainValidatorFunctionSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a function-plain schema. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + if self.mode == 'validation' and (input_schema := schema.get('json_schema_input_schema')): + return self.generate_inner(input_schema) + + return self.handle_invalid_for_json_schema( + schema, f'core_schema.PlainValidatorFunctionSchema ({schema["function"]})' + ) + + def function_wrap_schema(self, schema: core_schema.WrapValidatorFunctionSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a function-wrap schema. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + if self.mode == 'validation' and (input_schema := schema.get('json_schema_input_schema')): + return self.generate_inner(input_schema) + + return self.generate_inner(schema['schema']) + + def default_schema(self, schema: core_schema.WithDefaultSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema with a default value. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + json_schema = self.generate_inner(schema['schema']) + + default = self.get_default_value(schema) + if default is NoDefault or default is MISSING: + return json_schema + + # we reflect the application of custom plain, no-info serializers to defaults for + # JSON Schemas viewed in serialization mode: + # TODO: improvements along with https://github.com/pydantic/pydantic/issues/8208 + if self.mode == 'serialization': + # `_get_ser_schema_for_default_value()` is used to unpack potentially nested validator schemas: + ser_schema = _get_ser_schema_for_default_value(schema['schema']) + if ( + ser_schema is not None + and (ser_func := ser_schema.get('function')) + and not (default is None and ser_schema.get('when_used') in ('unless-none', 'json-unless-none')) + ): + try: + default = ser_func(default) # type: ignore + except Exception: + # It might be that the provided default needs to be validated (read: parsed) first + # (assuming `validate_default` is enabled). However, we can't perform + # such validation during JSON Schema generation so we don't support + # this pattern for now. + # (One example is when using `foo: ByteSize = '1MB'`, which validates and + # serializes as an int. In this case, `ser_func` is `int` and `int('1MB')` fails). + self.emit_warning( + 'non-serializable-default', + f'Unable to serialize value {default!r} with the plain serializer; excluding default from JSON schema', + ) + return json_schema + + # Sort set/frozenset defaults to ensure deterministic JSON schema generation + # We only sort if len > 1 because sets of size 0 or 1 are already deterministic + if isinstance(default, collections.abc.Set) and len(default) > 1: + try: + default = sorted(default) + except TypeError: # pragma: no cover + # If items aren't comparable (e.g. mixed types), we can't sort them. + pass + + try: + encoded_default = self.encode_default(default) + except pydantic_core.PydanticSerializationError: + self.emit_warning( + 'non-serializable-default', + f'Default value {default} is not JSON serializable; excluding default from JSON schema', + ) + # Return the inner schema, as though there was no default + return json_schema + + json_schema['default'] = encoded_default + return json_schema + + def get_default_value(self, schema: core_schema.WithDefaultSchema) -> Any: + """Get the default value to be used when generating a JSON Schema for a core schema with a default. + + The default implementation is to use the statically defined default value. This method can be overridden + if you want to make use of the default factory. + + Args: + schema: The `'with-default'` core schema. + + Returns: + The default value to use, or [`NoDefault`][pydantic.json_schema.NoDefault] if no default + value is available. + """ + return schema.get('default', NoDefault) + + def nullable_schema(self, schema: core_schema.NullableSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that allows null values. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + null_schema = {'type': 'null'} + inner_json_schema = self.generate_inner(schema['schema']) + + if inner_json_schema == null_schema: + return null_schema + else: + return self.get_union_of_schemas([inner_json_schema, null_schema]) + + def union_schema(self, schema: core_schema.UnionSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that allows values matching any of the given schemas. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + generated: list[JsonSchemaValue] = [] + + for choice in core_schema.iter_union_choices(schema): + try: + generated.append(self.generate_inner(choice)) + except PydanticOmit: # noqa: PERF203 + continue + except PydanticInvalidForJsonSchema as exc: + self.emit_warning('skipped-choice', exc.message) + if len(generated) == 1: + return generated[0] + return self.get_union_of_schemas(generated) + + def get_union_of_schemas(self, schemas: list[JsonSchemaValue]) -> JsonSchemaValue: + """Returns the JSON Schema representation for the union of the provided JSON Schemas. + + The result depends on the configured `'union_format'`. + + Args: + schemas: The list of JSON Schemas to be included in the union. + + Returns: + The JSON Schema representing the union of schemas. + """ + if self.union_format == 'primitive_type_array': + types: list[str] = [] + for schema in schemas: + schema_types: list[str] | str | None = schema.get('type') + if schema_types is None: + # No type, meaning it can be a ref or an empty schema. + break + if not isinstance(schema_types, list): + schema_types = [schema_types] + if not all(t in _PRIMITIVE_JSON_SCHEMA_TYPES for t in schema_types): + break + if len(schema) != 1: + # We only want to include types that don't have any constraints. For instance, + # if `schemas = [{'type': 'string', 'maxLength': 3}, {'type': 'string', 'minLength': 5}]`, + # we don't want to produce `{'type': 'string', 'maxLength': 3, 'minLength': 5}`. + # Same if we have some metadata (e.g. `title`) on a specific union member, we want to preserve it. + break + + types.extend(schema_types) + else: + # If we got there, all the schemas where valid to be used with the `'primitive_type_array` format + return {'type': list(dict.fromkeys(types))} + + return self.get_flattened_anyof(schemas) + + def tagged_union_schema(self, schema: core_schema.TaggedUnionSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that allows values matching any of the given schemas, where + the schemas are tagged with a discriminator field that indicates which schema should be used to validate + the value. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + generated: dict[str, JsonSchemaValue] = {} + for k, v in schema['choices'].items(): + if isinstance(k, Enum): + k = k.value + try: + # Use str(k) since keys must be strings for json; while not technically correct, + # it's the closest that can be represented in valid JSON + generated[str(k)] = self.generate_inner(v).copy() + except PydanticOmit: + continue + except PydanticInvalidForJsonSchema as exc: + self.emit_warning('skipped-choice', exc.message) + + one_of_choices = _deduplicate_schemas(generated.values()) + json_schema: JsonSchemaValue = {'oneOf': one_of_choices} + + # This reflects the v1 behavior; TODO: we should make it possible to exclude OpenAPI stuff from the JSON schema + openapi_discriminator = self._extract_discriminator(schema, one_of_choices) + if openapi_discriminator is not None: + json_schema['discriminator'] = { + 'propertyName': openapi_discriminator, + 'mapping': {k: v.get('$ref', v) for k, v in generated.items()}, + } + + return json_schema + + def _extract_discriminator( + self, schema: core_schema.TaggedUnionSchema, one_of_choices: list[JsonDict] + ) -> str | None: + """Extract a compatible OpenAPI discriminator from the schema and one_of choices that end up in the final + schema.""" + openapi_discriminator: str | None = None + + if isinstance(schema['discriminator'], str): + return schema['discriminator'] + + if isinstance(schema['discriminator'], list): + # If the discriminator is a single item list containing a string, that is equivalent to the string case + if len(schema['discriminator']) == 1 and isinstance(schema['discriminator'][0], str): + return schema['discriminator'][0] + # When an alias is used that is different from the field name, the discriminator will be a list of single + # str lists, one for the attribute and one for the actual alias. The logic here will work even if there is + # more than one possible attribute, and looks for whether a single alias choice is present as a documented + # property on all choices. If so, that property will be used as the OpenAPI discriminator. + for alias_path in schema['discriminator']: + if not isinstance(alias_path, list): + break # this means that the discriminator is not a list of alias paths + if len(alias_path) != 1: + continue # this means that the "alias" does not represent a single field + alias = alias_path[0] + if not isinstance(alias, str): + continue # this means that the "alias" does not represent a field + alias_is_present_on_all_choices = True + for choice in one_of_choices: + try: + choice = self.resolve_ref_schema(choice) + except RuntimeError as exc: + # TODO: fixme - this is a workaround for the fact that we can't always resolve refs + # for tagged union choices at this point in the schema gen process, we might need to do + # another pass at the end like we do for core schemas + self.emit_warning('skipped-discriminator', str(exc)) + choice = {} + properties = choice.get('properties', {}) + if not isinstance(properties, dict) or alias not in properties: + alias_is_present_on_all_choices = False + break + if alias_is_present_on_all_choices: + openapi_discriminator = alias + break + return openapi_discriminator + + def chain_schema(self, schema: core_schema.ChainSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a core_schema.ChainSchema. + + When generating a schema for validation, we return the validation JSON schema for the first step in the chain. + For serialization, we return the serialization JSON schema for the last step in the chain. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + step_index = 0 if self.mode == 'validation' else -1 # use first step for validation, last for serialization + return self.generate_inner(schema['steps'][step_index]) + + def lax_or_strict_schema(self, schema: core_schema.LaxOrStrictSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that allows values matching either the lax schema or the + strict schema. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + # TODO: Need to read the default value off of model config or whatever + use_strict = schema.get('strict', False) # TODO: replace this default False + # If your JSON schema fails to generate it is probably + # because one of the following two branches failed. + if use_strict: + return self.generate_inner(schema['strict_schema']) + else: + return self.generate_inner(schema['lax_schema']) + + def json_or_python_schema(self, schema: core_schema.JsonOrPythonSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that allows values matching either the JSON schema or the + Python schema. + + The JSON schema is used instead of the Python schema. If you want to use the Python schema, you should override + this method. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + return self.generate_inner(schema['json_schema']) + + def typed_dict_schema(self, schema: core_schema.TypedDictSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that defines a typed dict. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + total = schema.get('total', True) + named_required_fields: list[tuple[str, bool, CoreSchemaField]] = [ + (name, self.field_is_required(field, total), field) + for name, field in schema['fields'].items() + if self.field_is_present(field) + ] + if self.mode == 'serialization': + named_required_fields.extend(self._name_required_computed_fields(schema.get('computed_fields', []))) + cls = schema.get('cls') + config = _get_typed_dict_config(cls) + with self._config_wrapper_stack.push(config): + json_schema = self._named_required_fields_schema(named_required_fields) + + # There's some duplication between `extra_behavior` and + # the config's `extra`/core config's `extra_fields_behavior`. + # However, it is common to manually create TypedDictSchemas, + # where you don't necessarily have a class. + # At runtime, `extra_behavior` takes priority over the config + # for validation, so follow the same for the JSON Schema: + if 'extras_schema' in schema and schema['extras_schema'] != core_schema.any_schema(): + allow_additional_props = self.generate_inner(schema['extras_schema']) + else: + allow_additional_props = True + + if schema.get('extra_behavior') == 'forbid': + json_schema['additionalProperties'] = False + elif schema.get('extra_behavior') == 'allow': + json_schema['additionalProperties'] = allow_additional_props + + if cls is not None: + # `_update_class_schema()` will not override + # `additionalProperties` if already present: + self._update_class_schema(json_schema, cls, config) + elif 'additionalProperties' not in json_schema: + extra = schema.get('config', {}).get('extra_fields_behavior') + if extra == 'forbid': + json_schema['additionalProperties'] = False + elif extra == 'allow': + json_schema['additionalProperties'] = allow_additional_props + + return json_schema + + @staticmethod + def _name_required_computed_fields( + computed_fields: list[ComputedField], + ) -> list[tuple[str, bool, core_schema.ComputedField]]: + return [(field['property_name'], True, field) for field in computed_fields] + + def _named_required_fields_schema( + self, named_required_fields: Sequence[tuple[str, bool, CoreSchemaField]] + ) -> JsonSchemaValue: + properties: dict[str, JsonSchemaValue] = {} + required_fields: list[str] = [] + for name, required, field in named_required_fields: + if self.by_alias: + name = self._get_alias_name(field, name) + try: + field_json_schema = self.generate_inner(field).copy() + except PydanticOmit: + continue + if 'title' not in field_json_schema and self.field_title_should_be_set(field): + title = self.get_title_from_name(name) + field_json_schema['title'] = title + field_json_schema = self.handle_ref_overrides(field_json_schema) + properties[name] = field_json_schema + if required: + required_fields.append(name) + + json_schema = {'type': 'object', 'properties': properties} + if required_fields: + json_schema['required'] = required_fields + return json_schema + + def _get_alias_name(self, field: CoreSchemaField, name: str) -> str: + if field['type'] == 'computed-field': + alias: Any = field.get('alias', name) + elif self.mode == 'validation': + alias = field.get('validation_alias', name) + else: + alias = field.get('serialization_alias', name) + if isinstance(alias, str): + name = alias + elif isinstance(alias, list): + alias = cast('list[str] | str', alias) + for path in alias: + if isinstance(path, list) and len(path) == 1 and isinstance(path[0], str): + # Use the first valid single-item string path; the code that constructs the alias array + # should ensure the first such item is what belongs in the JSON schema + name = path[0] + break + else: + assert_never(alias) + return name + + def typed_dict_field_schema(self, schema: core_schema.TypedDictField) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that defines a typed dict field. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + return self.generate_inner(schema['schema']) + + def dataclass_field_schema(self, schema: core_schema.DataclassField) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that defines a dataclass field. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + return self.generate_inner(schema['schema']) + + def model_field_schema(self, schema: core_schema.ModelField) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that defines a model field. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + return self.generate_inner(schema['schema']) + + def computed_field_schema(self, schema: core_schema.ComputedField) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that defines a computed field. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + return self.generate_inner(schema['return_schema']) + + def model_schema(self, schema: core_schema.ModelSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that defines a model. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + # We do not use schema['model'].model_json_schema() here + # because it could lead to inconsistent refs handling, etc. + cls = cast('type[BaseModel]', schema['cls']) + config = cls.model_config + + with self._config_wrapper_stack.push(config): + json_schema = self.generate_inner(schema['schema']) + + self._update_class_schema(json_schema, cls, config) + + return json_schema + + def _update_class_schema(self, json_schema: JsonSchemaValue, cls: type[Any], config: ConfigDict) -> None: + """Update json_schema with the following, extracted from `config` and `cls`: + + * title + * description + * additional properties + * json_schema_extra + * deprecated + + Done in place, hence there's no return value as the original json_schema is mutated. + No ref resolving is involved here, as that's not appropriate for simple updates. + """ + from ._internal._dataclasses import is_stdlib_dataclass + from .main import BaseModel + + if (config_title := config.get('title')) is not None: + json_schema.setdefault('title', config_title) + elif model_title_generator := config.get('model_title_generator'): + title = model_title_generator(cls) + if not isinstance(title, str): + raise TypeError(f'model_title_generator {model_title_generator} must return str, not {title.__class__}') + json_schema.setdefault('title', title) + if 'title' not in json_schema: + json_schema['title'] = cls.__name__ + + # BaseModel and dataclasses; don't use cls.__doc__ as it will contain the verbose class signature by default + if cls is BaseModel: + docstring = None + elif is_stdlib_dataclass(cls): # For Pydantic dataclasses, we already handle this at class creation + # The `dataclass` module generates a `__doc__` based on the `inspect.signature()` + # result, which we don't want to use as a description. Such `__doc__` startswith + # `cls.__name__(`, which could lead to mistakenly discarding it if for some reason + # an explicitly set class docstring follows the same pattern, but this is unlikely + # to happen. + doc = cls.__doc__ + docstring = None if doc is None or doc.startswith(f'{cls.__name__}(') else doc + else: + docstring = cls.__doc__ + + if docstring: + json_schema.setdefault('description', inspect.cleandoc(docstring)) + + extra = config.get('extra') + if 'additionalProperties' not in json_schema: # This check is particularly important for `typed_dict_schema()` + if extra == 'allow': + json_schema['additionalProperties'] = True + elif extra == 'forbid': + json_schema['additionalProperties'] = False + + json_schema_extra = config.get('json_schema_extra') + if issubclass(cls, BaseModel) and cls.__pydantic_root_model__: + root_json_schema_extra = cls.model_fields['root'].json_schema_extra + if json_schema_extra and root_json_schema_extra: + raise ValueError( + '"model_config[\'json_schema_extra\']" and "Field.json_schema_extra" on "RootModel.root"' + ' field must not be set simultaneously' + ) + if root_json_schema_extra: + json_schema_extra = root_json_schema_extra + + if isinstance(json_schema_extra, (staticmethod, classmethod)): + # In older versions of python, this is necessary to ensure staticmethod/classmethods are callable + json_schema_extra = json_schema_extra.__get__(cls) + + if isinstance(json_schema_extra, dict): + json_schema.update(json_schema_extra) + elif callable(json_schema_extra): + if len(_typing_extra.signature_no_eval(json_schema_extra).parameters) > 1: + json_schema_extra = cast(Callable[[JsonDict, type[Any]], None], json_schema_extra) + json_schema_extra(json_schema, cls) + else: + json_schema_extra = cast(Callable[[JsonDict], None], json_schema_extra) + json_schema_extra(json_schema) + elif json_schema_extra is not None: + raise ValueError( + f"model_config['json_schema_extra']={json_schema_extra} should be a dict, callable, or None" + ) + + if hasattr(cls, '__deprecated__'): + json_schema['deprecated'] = True + + def resolve_ref_schema(self, json_schema: JsonSchemaValue) -> JsonSchemaValue: + """Resolve a JsonSchemaValue to the non-ref schema if it is a $ref schema. + + Args: + json_schema: The schema to resolve. + + Returns: + The resolved schema. + + Raises: + RuntimeError: If the schema reference can't be found in definitions. + """ + while '$ref' in json_schema: + ref = json_schema['$ref'] + schema_to_update = self.get_schema_from_definitions(JsonRef(ref)) + if schema_to_update is None: + raise RuntimeError(f'Cannot update undefined schema for $ref={ref}') + json_schema = schema_to_update + return json_schema + + def model_fields_schema(self, schema: core_schema.ModelFieldsSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that defines a model's fields. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + named_required_fields: list[tuple[str, bool, CoreSchemaField]] = [ + (name, self.field_is_required(field, total=True), field) + for name, field in schema['fields'].items() + if self.field_is_present(field) + ] + if self.mode == 'serialization': + named_required_fields.extend(self._name_required_computed_fields(schema.get('computed_fields', []))) + json_schema = self._named_required_fields_schema(named_required_fields) + extras_schema = schema.get('extras_schema', None) + if extras_schema is not None: + schema_to_update = self.resolve_ref_schema(json_schema) + schema_to_update['additionalProperties'] = self.generate_inner(extras_schema) + return json_schema + + def field_is_present(self, field: CoreSchemaField) -> bool: + """Whether the field should be included in the generated JSON schema. + + Args: + field: The schema for the field itself. + + Returns: + `True` if the field should be included in the generated JSON schema, `False` otherwise. + """ + if self.mode == 'serialization': + # If you still want to include the field in the generated JSON schema, + # override this method and return True + return not field.get('serialization_exclude') + elif self.mode == 'validation': + return True + else: + assert_never(self.mode) + + def field_is_required( + self, + field: core_schema.ModelField | core_schema.DataclassField | core_schema.TypedDictField, + total: bool, + ) -> bool: + """Whether the field should be marked as required in the generated JSON schema. + (Note that this is irrelevant if the field is not present in the JSON schema.). + + Args: + field: The schema for the field itself. + total: Only applies to `TypedDictField`s. + Indicates if the `TypedDict` this field belongs to is total, in which case any fields that don't + explicitly specify `required=False` are required. + + Returns: + `True` if the field should be marked as required in the generated JSON schema, `False` otherwise. + """ + if field['type'] == 'typed-dict-field': + required = field.get('required', total) + else: + required = field['schema']['type'] != 'default' + + if self.mode == 'serialization': + has_exclude_if = field.get('serialization_exclude_if') is not None + if self._config.json_schema_serialization_defaults_required: + return not has_exclude_if + else: + return required and not has_exclude_if + else: + return required + + def dataclass_args_schema(self, schema: core_schema.DataclassArgsSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that defines a dataclass's constructor arguments. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + named_required_fields: list[tuple[str, bool, CoreSchemaField]] = [ + (field['name'], self.field_is_required(field, total=True), field) + for field in schema['fields'] + if self.field_is_present(field) + ] + if self.mode == 'serialization': + named_required_fields.extend(self._name_required_computed_fields(schema.get('computed_fields', []))) + return self._named_required_fields_schema(named_required_fields) + + def dataclass_schema(self, schema: core_schema.DataclassSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that defines a dataclass. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + + cls = schema['cls'] + config = cast('ConfigDict', getattr(cls, '__pydantic_config__', {})) + + with self._config_wrapper_stack.push(config): + json_schema = self.generate_inner(schema['schema']).copy() + + self._update_class_schema(json_schema, cls, config) + + return json_schema + + def arguments_schema(self, schema: core_schema.ArgumentsSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that defines a function's arguments. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + prefer_positional = schema.get('metadata', {}).get('pydantic_js_prefer_positional_arguments') + + arguments = schema['arguments_schema'] + kw_only_arguments = [a for a in arguments if a.get('mode') == 'keyword_only'] + kw_or_p_arguments = [a for a in arguments if a.get('mode') in {'positional_or_keyword', None}] + p_only_arguments = [a for a in arguments if a.get('mode') == 'positional_only'] + var_args_schema = schema.get('var_args_schema') + var_kwargs_schema = schema.get('var_kwargs_schema') + + if prefer_positional: + positional_possible = not kw_only_arguments and not var_kwargs_schema + if positional_possible: + return self.p_arguments_schema(p_only_arguments + kw_or_p_arguments, var_args_schema) + + keyword_possible = not p_only_arguments and not var_args_schema + if keyword_possible: + return self.kw_arguments_schema(kw_or_p_arguments + kw_only_arguments, var_kwargs_schema) + + if not prefer_positional: + positional_possible = not kw_only_arguments and not var_kwargs_schema + if positional_possible: + return self.p_arguments_schema(p_only_arguments + kw_or_p_arguments, var_args_schema) + + raise PydanticInvalidForJsonSchema( + 'Unable to generate JSON schema for arguments validator with positional-only and keyword-only arguments' + ) + + def kw_arguments_schema( + self, arguments: list[core_schema.ArgumentsParameter], var_kwargs_schema: CoreSchema | None + ) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that defines a function's keyword arguments. + + Args: + arguments: The core schema. + + Returns: + The generated JSON schema. + """ + properties: dict[str, JsonSchemaValue] = {} + required: list[str] = [] + for argument in arguments: + name = self.get_argument_name(argument) + argument_schema = self.generate_inner(argument['schema']).copy() + if 'title' not in argument_schema and self.field_title_should_be_set(argument['schema']): + argument_schema['title'] = self.get_title_from_name(name) + properties[name] = argument_schema + + if argument['schema']['type'] != 'default': + # This assumes that if the argument has a default value, + # the inner schema must be of type WithDefaultSchema. + # I believe this is true, but I am not 100% sure + required.append(name) + + json_schema: JsonSchemaValue = {'type': 'object', 'properties': properties} + if required: + json_schema['required'] = required + + if var_kwargs_schema: + additional_properties_schema = self.generate_inner(var_kwargs_schema) + if additional_properties_schema: + json_schema['additionalProperties'] = additional_properties_schema + else: + json_schema['additionalProperties'] = False + return json_schema + + def p_arguments_schema( + self, arguments: list[core_schema.ArgumentsParameter], var_args_schema: CoreSchema | None + ) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that defines a function's positional arguments. + + Args: + arguments: The core schema. + + Returns: + The generated JSON schema. + """ + prefix_items: list[JsonSchemaValue] = [] + min_items = 0 + + for argument in arguments: + name = self.get_argument_name(argument) + + argument_schema = self.generate_inner(argument['schema']).copy() + if 'title' not in argument_schema and self.field_title_should_be_set(argument['schema']): + argument_schema['title'] = self.get_title_from_name(name) + prefix_items.append(argument_schema) + + if argument['schema']['type'] != 'default': + # This assumes that if the argument has a default value, + # the inner schema must be of type WithDefaultSchema. + # I believe this is true, but I am not 100% sure + min_items += 1 + + json_schema: JsonSchemaValue = {'type': 'array'} + if prefix_items: + json_schema['prefixItems'] = prefix_items + if min_items: + json_schema['minItems'] = min_items + + if var_args_schema: + items_schema = self.generate_inner(var_args_schema) + if items_schema: + json_schema['items'] = items_schema + else: + json_schema['maxItems'] = len(prefix_items) + + return json_schema + + def get_argument_name(self, argument: core_schema.ArgumentsParameter | core_schema.ArgumentsV3Parameter) -> str: + """Retrieves the name of an argument. + + Args: + argument: The core schema. + + Returns: + The name of the argument. + """ + name = argument['name'] + if self.by_alias: + alias = argument.get('alias') + if isinstance(alias, str): + name = alias + else: + pass # might want to do something else? + return name + + def arguments_v3_schema(self, schema: core_schema.ArgumentsV3Schema) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that defines a function's arguments. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + arguments = schema['arguments_schema'] + properties: dict[str, JsonSchemaValue] = {} + required: list[str] = [] + for argument in arguments: + mode = argument.get('mode', 'positional_or_keyword') + name = self.get_argument_name(argument) + argument_schema = self.generate_inner(argument['schema']).copy() + if mode == 'var_args': + argument_schema = {'type': 'array', 'items': argument_schema} + elif mode == 'var_kwargs_uniform': + argument_schema = {'type': 'object', 'additionalProperties': argument_schema} + + argument_schema.setdefault('title', self.get_title_from_name(name)) + properties[name] = argument_schema + + if ( + (mode == 'var_kwargs_unpacked_typed_dict' and 'required' in argument_schema) + or mode not in {'var_args', 'var_kwargs_uniform', 'var_kwargs_unpacked_typed_dict'} + and argument['schema']['type'] != 'default' + ): + # This assumes that if the argument has a default value, + # the inner schema must be of type WithDefaultSchema. + # I believe this is true, but I am not 100% sure + required.append(name) + + json_schema: JsonSchemaValue = {'type': 'object', 'properties': properties} + if required: + json_schema['required'] = required + return json_schema + + def call_schema(self, schema: core_schema.CallSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that defines a function call. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + return self.generate_inner(schema['arguments_schema']) + + def custom_error_schema(self, schema: core_schema.CustomErrorSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that defines a custom error. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + return self.generate_inner(schema['schema']) + + def json_schema(self, schema: core_schema.JsonSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that defines a JSON object. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + content_core_schema = schema.get('schema') or core_schema.any_schema() + content_json_schema = self.generate_inner(content_core_schema) + if self.mode == 'validation': + return {'type': 'string', 'contentMediaType': 'application/json', 'contentSchema': content_json_schema} + else: + # self.mode == 'serialization' + return content_json_schema + + def url_schema(self, schema: core_schema.UrlSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that defines a URL. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + json_schema = {'type': 'string', 'format': 'uri', 'minLength': 1} + self.update_with_validations(json_schema, schema, self.ValidationsMapping.string) + return json_schema + + def multi_host_url_schema(self, schema: core_schema.MultiHostUrlSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that defines a URL that can be used with multiple hosts. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + # Note: 'multi-host-uri' is a custom/pydantic-specific format, not part of the JSON Schema spec + json_schema = {'type': 'string', 'format': 'multi-host-uri', 'minLength': 1} + self.update_with_validations(json_schema, schema, self.ValidationsMapping.string) + return json_schema + + def uuid_schema(self, schema: core_schema.UuidSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a UUID. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + return {'type': 'string', 'format': 'uuid'} + + def definitions_schema(self, schema: core_schema.DefinitionsSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that defines a JSON object with definitions. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + for definition in schema['definitions']: + try: + self.generate_inner(definition) + except PydanticInvalidForJsonSchema as e: # noqa: PERF203 + core_ref: CoreRef = CoreRef(definition['ref']) # type: ignore + self._core_defs_invalid_for_json_schema[self.get_defs_ref((core_ref, self.mode))] = e + continue + return self.generate_inner(schema['schema']) + + def definition_ref_schema(self, schema: core_schema.DefinitionReferenceSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that references a definition. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + core_ref = CoreRef(schema['schema_ref']) + _, ref_json_schema = self.get_cache_defs_ref_schema(core_ref) + return ref_json_schema + + def ser_schema( + self, schema: core_schema.SerSchema | core_schema.IncExSeqSerSchema | core_schema.IncExDictSerSchema + ) -> JsonSchemaValue | None: + """Generates a JSON schema that matches a schema that defines a serialized object. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + schema_type = schema['type'] + if schema_type == 'function-plain' or schema_type == 'function-wrap': + # PlainSerializerFunctionSerSchema or WrapSerializerFunctionSerSchema + return_schema = schema.get('return_schema') + if return_schema is not None: + return self.generate_inner(return_schema) + elif schema_type == 'format' or schema_type == 'to-string': + # FormatSerSchema or ToStringSerSchema + return self.str_schema(core_schema.str_schema()) + elif schema['type'] == 'model': + # ModelSerSchema + return self.generate_inner(schema['schema']) + return None + + def complex_schema(self, schema: core_schema.ComplexSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a complex number. + + JSON has no standard way to represent complex numbers. Complex number is not a numeric + type. Here we represent complex number as strings following the rule defined by Python. + For instance, '1+2j' is an accepted complex string. Details can be found in + [Python's `complex` documentation][complex]. + + Args: + schema: The core schema. + + Returns: + The generated JSON schema. + """ + return {'type': 'string'} + + # ### Utility methods + + def get_title_from_name(self, name: str) -> str: + """Retrieves a title from a name. + + Args: + name: The name to retrieve a title from. + + Returns: + The title. + """ + return name.title().replace('_', ' ').strip() + + def field_title_should_be_set(self, schema: CoreSchemaOrField) -> bool: + """Returns true if a field with the given schema should have a title set based on the field name. + + Intuitively, we want this to return true for schemas that wouldn't otherwise provide their own title + (e.g., int, float, str), and false for those that would (e.g., BaseModel subclasses). + + Args: + schema: The schema to check. + + Returns: + `True` if the field should have a title set, `False` otherwise. + """ + if _core_utils.is_core_schema_field(schema): + if schema['type'] == 'computed-field': + field_schema = schema['return_schema'] + else: + field_schema = schema['schema'] + return self.field_title_should_be_set(field_schema) + + elif _core_utils.is_core_schema(schema): + if schema.get('ref'): # things with refs, such as models and enums, should not have titles set + return False + if schema['type'] in {'default', 'nullable', 'definitions'}: + return self.field_title_should_be_set(schema['schema']) # type: ignore[typeddict-item] + if _core_utils.is_function_with_inner_schema(schema): + return self.field_title_should_be_set(schema['schema']) + if schema['type'] == 'definition-ref': + # Referenced schemas should not have titles set for the same reason + # schemas with refs should not + return False + return True # anything else should have title set + + else: + raise PydanticInvalidForJsonSchema(f'Unexpected schema type: schema={schema}') # pragma: no cover + + def normalize_name(self, name: str) -> str: + """Normalizes a name to be used as a key in a dictionary. + + Args: + name: The name to normalize. + + Returns: + The normalized name. + """ + return re.sub(r'[^a-zA-Z0-9.\-_]', '_', name).replace('.', '__') + + def get_defs_ref(self, core_mode_ref: CoreModeRef) -> DefsRef: + """Override this method to change the way that definitions keys are generated from a core reference. + + Args: + core_mode_ref: The core reference. + + Returns: + The definitions key. + """ + # Split the core ref into "components"; generic origins and arguments are each separate components + core_ref, mode = core_mode_ref + components = re.split(r'([\][,])', core_ref) + # Remove IDs from each component + components = [x.rsplit(':', 1)[0] for x in components] + core_ref_no_id = ''.join(components) + # Remove everything before the last period from each "component" + components = [re.sub(r'(?:[^.[\]]+\.)+((?:[^.[\]]+))', r'\1', x) for x in components] + short_ref = ''.join(components) + + mode_title = _MODE_TITLE_MAPPING[mode] + + # It is important that the generated defs_ref values be such that at least one choice will not + # be generated for any other core_ref. Currently, this should be the case because we include + # the id of the source type in the core_ref + name = DefsRef(self.normalize_name(short_ref)) + name_mode = DefsRef(self.normalize_name(short_ref) + f'-{mode_title}') + module_qualname = DefsRef(self.normalize_name(core_ref_no_id)) + module_qualname_mode = DefsRef(f'{module_qualname}-{mode_title}') + module_qualname_id = DefsRef(self.normalize_name(core_ref)) + occurrence_index = self._collision_index.get(module_qualname_id) + if occurrence_index is None: + self._collision_counter[module_qualname] += 1 + occurrence_index = self._collision_index[module_qualname_id] = self._collision_counter[module_qualname] + + module_qualname_occurrence = DefsRef(f'{module_qualname}__{occurrence_index}') + module_qualname_occurrence_mode = DefsRef(f'{module_qualname_mode}__{occurrence_index}') + + self._prioritized_defsref_choices[module_qualname_occurrence_mode] = [ + name, + name_mode, + module_qualname, + module_qualname_mode, + module_qualname_occurrence, + module_qualname_occurrence_mode, + ] + + return module_qualname_occurrence_mode + + def get_cache_defs_ref_schema(self, core_ref: CoreRef) -> tuple[DefsRef, JsonSchemaValue]: + """This method wraps the get_defs_ref method with some cache-lookup/population logic, + and returns both the produced defs_ref and the JSON schema that will refer to the right definition. + + Args: + core_ref: The core reference to get the definitions reference for. + + Returns: + A tuple of the definitions reference and the JSON schema that will refer to it. + """ + core_mode_ref = (core_ref, self.mode) + maybe_defs_ref = self.core_to_defs_refs.get(core_mode_ref) + if maybe_defs_ref is not None: + json_ref = self.core_to_json_refs[core_mode_ref] + return maybe_defs_ref, {'$ref': json_ref} + + defs_ref = self.get_defs_ref(core_mode_ref) + + # populate the ref translation mappings + self.core_to_defs_refs[core_mode_ref] = defs_ref + self.defs_to_core_refs[defs_ref] = core_mode_ref + + json_ref = JsonRef(self.ref_template.format(model=defs_ref)) + self.core_to_json_refs[core_mode_ref] = json_ref + self.json_to_defs_refs[json_ref] = defs_ref + ref_json_schema = {'$ref': json_ref} + return defs_ref, ref_json_schema + + def handle_ref_overrides(self, json_schema: JsonSchemaValue) -> JsonSchemaValue: + """Remove any sibling keys that are redundant with the referenced schema. + + Args: + json_schema: The schema to remove redundant sibling keys from. + + Returns: + The schema with redundant sibling keys removed. + """ + if '$ref' in json_schema: + # prevent modifications to the input; this copy may be safe to drop if there is significant overhead + json_schema = json_schema.copy() + + referenced_json_schema = self.get_schema_from_definitions(JsonRef(json_schema['$ref'])) + if referenced_json_schema is None: + # This can happen when building schemas for models with not-yet-defined references. + # It may be a good idea to do a recursive pass at the end of the generation to remove + # any redundant override keys. + return json_schema + for k, v in list(json_schema.items()): + if k == '$ref': + continue + if k in referenced_json_schema and referenced_json_schema[k] == v: + del json_schema[k] # redundant key + + return json_schema + + def get_schema_from_definitions(self, json_ref: JsonRef) -> JsonSchemaValue | None: + try: + def_ref = self.json_to_defs_refs[json_ref] + if def_ref in self._core_defs_invalid_for_json_schema: + raise self._core_defs_invalid_for_json_schema[def_ref] + return self.definitions.get(def_ref, None) + except KeyError: + if json_ref.startswith(('http://', 'https://')): + return None + raise + + def encode_default(self, dft: Any) -> Any: + """Encode a default value to a JSON-serializable value. + + This is used to encode default values for fields in the generated JSON schema. + + Args: + dft: The default value to encode. + + Returns: + The encoded default value. + """ + from .type_adapter import TypeAdapter, _type_has_config + + config = self._config + try: + default = ( + dft + if _type_has_config(type(dft)) + else TypeAdapter(type(dft), config=config.config_dict).dump_python( + dft, by_alias=self.by_alias, mode='json' + ) + ) + except PydanticSchemaGenerationError: + raise pydantic_core.PydanticSerializationError(f'Unable to encode default value {dft}') + + return pydantic_core.to_jsonable_python( + default, timedelta_mode=config.ser_json_timedelta, bytes_mode=config.ser_json_bytes, by_alias=self.by_alias + ) + + def update_with_validations( + self, json_schema: JsonSchemaValue, core_schema: CoreSchema, mapping: dict[str, str] + ) -> None: + """Update the json_schema with the corresponding validations specified in the core_schema, + using the provided mapping to translate keys in core_schema to the appropriate keys for a JSON schema. + + Args: + json_schema: The JSON schema to update. + core_schema: The core schema to get the validations from. + mapping: A mapping from core_schema attribute names to the corresponding JSON schema attribute names. + """ + for core_key, json_schema_key in mapping.items(): + if core_key in core_schema: + json_schema[json_schema_key] = core_schema[core_key] + + class ValidationsMapping: + """This class just contains mappings from core_schema attribute names to the corresponding + JSON schema attribute names. While I suspect it is unlikely to be necessary, you can in + principle override this class in a subclass of GenerateJsonSchema (by inheriting from + GenerateJsonSchema.ValidationsMapping) to change these mappings. + """ + + numeric = { + 'multiple_of': 'multipleOf', + 'le': 'maximum', + 'ge': 'minimum', + 'lt': 'exclusiveMaximum', + 'gt': 'exclusiveMinimum', + } + bytes = { + 'min_length': 'minLength', + 'max_length': 'maxLength', + } + string = { + 'min_length': 'minLength', + 'max_length': 'maxLength', + 'pattern': 'pattern', + } + array = { + 'min_length': 'minItems', + 'max_length': 'maxItems', + } + object = { + 'min_length': 'minProperties', + 'max_length': 'maxProperties', + } + + def get_flattened_anyof(self, schemas: list[JsonSchemaValue]) -> JsonSchemaValue: + members = [] + for schema in schemas: + if len(schema) == 1 and 'anyOf' in schema: + members.extend(schema['anyOf']) + else: + members.append(schema) + members = _deduplicate_schemas(members) + if len(members) == 1: + return members[0] + return {'anyOf': members} + + def get_json_ref_counts(self, json_schema: JsonSchemaValue) -> dict[JsonRef, int]: + """Get all values corresponding to the key '$ref' anywhere in the json_schema.""" + json_refs: dict[JsonRef, int] = Counter() + + def _add_json_refs(schema: Any) -> None: + if isinstance(schema, dict): + if '$ref' in schema: + json_ref = JsonRef(schema['$ref']) + if not isinstance(json_ref, str): + return # in this case, '$ref' might have been the name of a property + already_visited = json_ref in json_refs + json_refs[json_ref] += 1 + if already_visited: + return # prevent recursion on a definition that was already visited + try: + defs_ref = self.json_to_defs_refs[json_ref] + if defs_ref in self._core_defs_invalid_for_json_schema: + raise self._core_defs_invalid_for_json_schema[defs_ref] + _add_json_refs(self.definitions[defs_ref]) + except KeyError: + if not json_ref.startswith(('http://', 'https://')): + raise + + for k, v in schema.items(): + if k == 'examples' and isinstance(v, list): + # Skip examples that may contain arbitrary values and references + # (see the comment in `_get_all_json_refs` for more details). + continue + _add_json_refs(v) + elif isinstance(schema, list): + for v in schema: + _add_json_refs(v) + + _add_json_refs(json_schema) + return json_refs + + def handle_invalid_for_json_schema(self, schema: CoreSchemaOrField, error_info: str) -> JsonSchemaValue: + raise PydanticInvalidForJsonSchema(f'Cannot generate a JsonSchema for {error_info}') + + def emit_warning(self, kind: JsonSchemaWarningKind, detail: str) -> None: + """This method simply emits PydanticJsonSchemaWarnings based on handling in the `warning_message` method.""" + message = self.render_warning_message(kind, detail) + if message is not None: + warnings.warn(message, PydanticJsonSchemaWarning) + + def render_warning_message(self, kind: JsonSchemaWarningKind, detail: str) -> str | None: + """This method is responsible for ignoring warnings as desired, and for formatting the warning messages. + + You can override the value of `ignored_warning_kinds` in a subclass of GenerateJsonSchema + to modify what warnings are generated. If you want more control, you can override this method; + just return None in situations where you don't want warnings to be emitted. + + Args: + kind: The kind of warning to render. It can be one of the following: + + - 'skipped-choice': A choice field was skipped because it had no valid choices. + - 'non-serializable-default': A default value was skipped because it was not JSON-serializable. + detail: A string with additional details about the warning. + + Returns: + The formatted warning message, or `None` if no warning should be emitted. + """ + if kind in self.ignored_warning_kinds: + return None + return f'{detail} [{kind}]' + + def _build_definitions_remapping(self) -> _DefinitionsRemapping: + defs_to_json: dict[DefsRef, JsonRef] = {} + for defs_refs in self._prioritized_defsref_choices.values(): + for defs_ref in defs_refs: + json_ref = JsonRef(self.ref_template.format(model=defs_ref)) + defs_to_json[defs_ref] = json_ref + + return _DefinitionsRemapping.from_prioritized_choices( + self._prioritized_defsref_choices, defs_to_json, self.definitions + ) + + def _garbage_collect_definitions(self, schema: JsonSchemaValue) -> None: + visited_defs_refs: set[DefsRef] = set() + unvisited_json_refs = _get_all_json_refs(schema) + while unvisited_json_refs: + next_json_ref = unvisited_json_refs.pop() + try: + next_defs_ref = self.json_to_defs_refs[next_json_ref] + if next_defs_ref in visited_defs_refs: + continue + visited_defs_refs.add(next_defs_ref) + unvisited_json_refs.update(_get_all_json_refs(self.definitions[next_defs_ref])) + except KeyError: + if not next_json_ref.startswith(('http://', 'https://')): + raise + + self.definitions = {k: v for k, v in self.definitions.items() if k in visited_defs_refs} + + +# ##### Start JSON Schema Generation Functions ##### + + +def model_json_schema( + cls: type[BaseModel] | type[PydanticDataclass], + by_alias: bool = True, + ref_template: str = DEFAULT_REF_TEMPLATE, + union_format: Literal['any_of', 'primitive_type_array'] = 'any_of', + schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema, + mode: JsonSchemaMode = 'validation', +) -> dict[str, Any]: + """Utility function to generate a JSON Schema for a model. + + Args: + cls: The model class to generate a JSON Schema for. + by_alias: If `True` (the default), fields will be serialized according to their alias. + If `False`, fields will be serialized according to their attribute name. + ref_template: The template to use for generating JSON Schema references. + union_format: The format to use when combining schemas from unions together. Can be one of: + + - `'any_of'`: Use the [`anyOf`](https://json-schema.org/understanding-json-schema/reference/combining#anyOf) + keyword to combine schemas (the default). + - `'primitive_type_array'`: Use the [`type`](https://json-schema.org/understanding-json-schema/reference/type) + keyword as an array of strings, containing each type of the combination. If any of the schemas is not a primitive + type (`string`, `boolean`, `null`, `integer` or `number`) or contains constraints/metadata, falls back to + `any_of`. + schema_generator: The class to use for generating the JSON Schema. + mode: The mode to use for generating the JSON Schema. It can be one of the following: + + - 'validation': Generate a JSON Schema for validating data. + - 'serialization': Generate a JSON Schema for serializing data. + + Returns: + The generated JSON Schema. + """ + from .main import BaseModel + + schema_generator_instance = schema_generator( + by_alias=by_alias, ref_template=ref_template, union_format=union_format + ) + + if isinstance(cls.__pydantic_core_schema__, _mock_val_ser.MockCoreSchema): + cls.__pydantic_core_schema__.rebuild() + + if cls is BaseModel: + raise AttributeError('model_json_schema() must be called on a subclass of BaseModel, not BaseModel itself.') + + assert not isinstance(cls.__pydantic_core_schema__, _mock_val_ser.MockCoreSchema), 'this is a bug! please report it' + return schema_generator_instance.generate(cls.__pydantic_core_schema__, mode=mode) + + +def models_json_schema( + models: Sequence[tuple[type[BaseModel] | type[PydanticDataclass], JsonSchemaMode]], + *, + by_alias: bool = True, + title: str | None = None, + description: str | None = None, + ref_template: str = DEFAULT_REF_TEMPLATE, + union_format: Literal['any_of', 'primitive_type_array'] = 'any_of', + schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema, +) -> tuple[dict[tuple[type[BaseModel] | type[PydanticDataclass], JsonSchemaMode], JsonSchemaValue], JsonSchemaValue]: + """Utility function to generate a JSON Schema for multiple models. + + Args: + models: A sequence of tuples of the form (model, mode). + by_alias: Whether field aliases should be used as keys in the generated JSON Schema. + title: The title of the generated JSON Schema. + description: The description of the generated JSON Schema. + ref_template: The reference template to use for generating JSON Schema references. + union_format: The format to use when combining schemas from unions together. Can be one of: + + - `'any_of'`: Use the [`anyOf`](https://json-schema.org/understanding-json-schema/reference/combining#anyOf) + keyword to combine schemas (the default). + - `'primitive_type_array'`: Use the [`type`](https://json-schema.org/understanding-json-schema/reference/type) + keyword as an array of strings, containing each type of the combination. If any of the schemas is not a primitive + type (`string`, `boolean`, `null`, `integer` or `number`) or contains constraints/metadata, falls back to + `any_of`. + schema_generator: The schema generator to use for generating the JSON Schema. + + Returns: + A tuple where: + - The first element is a dictionary whose keys are tuples of JSON schema key type and JSON mode, and + whose values are the JSON schema corresponding to that pair of inputs. (These schemas may have + JsonRef references to definitions that are defined in the second returned element.) + - The second element is a JSON schema containing all definitions referenced in the first returned + element, along with the optional title and description keys. + """ + for cls, _ in models: + if isinstance(cls.__pydantic_core_schema__, _mock_val_ser.MockCoreSchema): + cls.__pydantic_core_schema__.rebuild() + + instance = schema_generator(by_alias=by_alias, ref_template=ref_template, union_format=union_format) + inputs: list[tuple[type[BaseModel] | type[PydanticDataclass], JsonSchemaMode, CoreSchema]] = [ + (m, mode, m.__pydantic_core_schema__) for m, mode in models + ] + json_schemas_map, definitions = instance.generate_definitions(inputs) + + json_schema: dict[str, Any] = {} + if definitions: + json_schema['$defs'] = definitions + if title: + json_schema['title'] = title + if description: + json_schema['description'] = description + + return json_schemas_map, json_schema + + +# ##### End JSON Schema Generation Functions ##### + + +_HashableJsonValue: TypeAlias = Union[ + int, float, str, bool, None, tuple['_HashableJsonValue', ...], tuple[tuple[str, '_HashableJsonValue'], ...] +] + + +def _deduplicate_schemas(schemas: Iterable[JsonDict]) -> list[JsonDict]: + return list({_make_json_hashable(schema): schema for schema in schemas}.values()) + + +def _make_json_hashable(value: JsonValue) -> _HashableJsonValue: + if isinstance(value, dict): + return tuple(sorted((k, _make_json_hashable(v)) for k, v in value.items())) + elif isinstance(value, list): + return tuple(_make_json_hashable(v) for v in value) + else: + return value + + +@dataclasses.dataclass(**_internal_dataclass.slots_true) +class WithJsonSchema: + """!!! abstract "Usage Documentation" + [`WithJsonSchema` Annotation](../concepts/json_schema.md#withjsonschema-annotation) + + An annotation used to override the JSON Schema for a type. + + This is useful when you want to set a JSON Schema for a type that don't produce any JSON Schemas by default + (e.g. [`Callable`][collections.abc.Callable]). + + If `mode` is set this will only apply to that schema generation mode, allowing you to set different JSON Schemas for validation and serialization. + + !!! note + If the `WithJsonSchema` annotation is coupled with the [`Field()`][pydantic.Field] function, the behavior overriding will vary depending on the location: + + * If the [`Annotated`][typing.Annotated] metadata is specified at the "top-level" field, `Field()` metadata arguments + (excluding [constraints](../concepts/fields.md#field-constraints)) such as `title` and `description` will be applied on + top of the `WithJsonSchema`, no matter the order: + + ```python + from typing import Annotated + + from pydantic import BaseModel, Field, WithJsonSchema + + class Model(BaseModel): + field: Annotated[ + int, + Field(title='My Field'), + WithJsonSchema({'type': 'integer', 'extra': 'data'}), + ] + + Model.model_json_schema()['properties']['field'] + #> {'type': 'integer', 'extra': 'data', 'title': 'My Field'} + ``` + + * If the [`Annotated`][typing.Annotated] metadata is specified on a specific inner type, `WithJsonSchema` will unconditionally + override the JSON Schema: + + ```python + from typing import Annotated + + from pydantic import BaseModel, Field, WithJsonSchema + + class Model(BaseModel): + field: list[ + Annotated[ + int, + Field(title='My Field'), + WithJsonSchema({'type': 'integer', 'extra': 'data'}), + ] + ] + + Model.model_json_schema()['properties']['field'] + #> {'items': {'extra': 'data', 'type': 'integer'}, 'title': 'Field', 'type': 'array'} + ``` + + See also the documentation about [the annotated pattern](../concepts/fields.md#the-annotated-pattern). + """ + + json_schema: JsonSchemaValue | None + mode: Literal['validation', 'serialization'] | None = None + + def __get_pydantic_json_schema__( + self, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler + ) -> JsonSchemaValue: + if self.mode is not None and self.mode != handler.mode: + return handler(core_schema) + if self.json_schema is None: + # This exception is handled in pydantic.json_schema.GenerateJsonSchema._named_required_fields_schema + raise PydanticOmit + else: + return self.json_schema.copy() + + def __hash__(self) -> int: + return hash(type(self.mode)) + + +class Examples: + """Add examples to a JSON schema. + + If the JSON Schema already contains examples, the provided examples + will be appended. + + If `mode` is set this will only apply to that schema generation mode, + allowing you to add different examples for validation and serialization. + """ + + @overload + @deprecated('Using a dict for `examples` is deprecated since v2.9 and will be removed in v3.0. Use a list instead.') + def __init__( + self, examples: dict[str, Any], mode: Literal['validation', 'serialization'] | None = None + ) -> None: ... + + @overload + def __init__(self, examples: list[Any], mode: Literal['validation', 'serialization'] | None = None) -> None: ... + + def __init__( + self, examples: dict[str, Any] | list[Any], mode: Literal['validation', 'serialization'] | None = None + ) -> None: + if isinstance(examples, dict): + warnings.warn( + 'Using a dict for `examples` is deprecated, use a list instead.', + PydanticDeprecatedSince29, + stacklevel=2, + ) + self.examples = examples + self.mode = mode + + def __get_pydantic_json_schema__( + self, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler + ) -> JsonSchemaValue: + mode = self.mode or handler.mode + json_schema = handler(core_schema) + if mode != handler.mode: + return json_schema + examples = json_schema.get('examples') + if examples is None: + json_schema['examples'] = to_jsonable_python(self.examples) + if isinstance(examples, dict): + if isinstance(self.examples, list): + warnings.warn( + 'Updating existing JSON Schema examples of type dict with examples of type list. ' + 'Only the existing examples values will be retained. Note that dict support for ' + 'examples is deprecated and will be removed in v3.0.', + UserWarning, + ) + json_schema['examples'] = to_jsonable_python( + [ex for value in examples.values() for ex in value] + self.examples + ) + else: + json_schema['examples'] = to_jsonable_python({**examples, **self.examples}) + if isinstance(examples, list): + if isinstance(self.examples, list): + json_schema['examples'] = to_jsonable_python(examples + self.examples) + elif isinstance(self.examples, dict): + warnings.warn( + 'Updating existing JSON Schema examples of type list with examples of type dict. ' + 'Only the examples values will be retained. Note that dict support for ' + 'examples is deprecated and will be removed in v3.0.', + UserWarning, + ) + json_schema['examples'] = to_jsonable_python( + examples + [ex for value in self.examples.values() for ex in value] + ) + + return json_schema + + def __hash__(self) -> int: + return hash(type(self.mode)) + + +def _get_all_json_refs(item: Any) -> set[JsonRef]: + """Get all the definitions references from a JSON schema.""" + refs: set[JsonRef] = set() + stack = [item] + + while stack: + current = stack.pop() + if isinstance(current, dict): + for key, value in current.items(): + if key == 'examples' and isinstance(value, list): + # Skip examples that may contain arbitrary values and references + # (e.g. `{"examples": [{"$ref": "..."}]}`). Note: checking for value + # of type list is necessary to avoid skipping valid portions of the schema, + # for instance when "examples" is used as a property key. A more robust solution + # could be found, but would require more advanced JSON Schema parsing logic. + continue + if key == '$ref' and isinstance(value, str): + refs.add(JsonRef(value)) + elif isinstance(value, dict): + stack.append(value) + elif isinstance(value, list): + stack.extend(value) + elif isinstance(current, list): + stack.extend(current) + + return refs + + +AnyType = TypeVar('AnyType') + +if TYPE_CHECKING: + SkipJsonSchema = Annotated[AnyType, ...] +else: + + @dataclasses.dataclass(**_internal_dataclass.slots_true) + class SkipJsonSchema: + """!!! abstract "Usage Documentation" + [`SkipJsonSchema` Annotation](../concepts/json_schema.md#skipjsonschema-annotation) + + Add this as an annotation on a field to skip generating a JSON schema for that field. + + Example: + ```python + from pprint import pprint + from typing import Union + + from pydantic import BaseModel + from pydantic.json_schema import SkipJsonSchema + + class Model(BaseModel): + a: Union[int, None] = None # (1)! + b: Union[int, SkipJsonSchema[None]] = None # (2)! + c: SkipJsonSchema[Union[int, None]] = None # (3)! + + pprint(Model.model_json_schema()) + ''' + { + 'properties': { + 'a': { + 'anyOf': [ + {'type': 'integer'}, + {'type': 'null'} + ], + 'default': None, + 'title': 'A' + }, + 'b': { + 'default': None, + 'title': 'B', + 'type': 'integer' + } + }, + 'title': 'Model', + 'type': 'object' + } + ''' + ``` + + 1. The integer and null types are both included in the schema for `a`. + 2. The integer type is the only type included in the schema for `b`. + 3. The entirety of the `c` field is omitted from the schema. + """ + + def __class_getitem__(cls, item: AnyType) -> AnyType: + return Annotated[item, cls()] + + def __get_pydantic_json_schema__( + self, core_schema: CoreSchema, handler: GetJsonSchemaHandler + ) -> JsonSchemaValue: + raise PydanticOmit + + def __hash__(self) -> int: + return hash(type(self)) + + +def _get_typed_dict_config(cls: type[Any] | None) -> ConfigDict: + if cls is not None: + try: + return _decorators.get_attribute_from_bases(cls, '__pydantic_config__') + except AttributeError: + pass + return {} + + +def _get_ser_schema_for_default_value(schema: CoreSchema) -> core_schema.PlainSerializerFunctionSerSchema | None: + """Get a `'function-plain'` serialization schema that can be used to serialize a default value. + + This takes into account having the serialization schema nested under validation schema(s). + """ + if ( + (ser_schema := schema.get('serialization')) + and ser_schema['type'] == 'function-plain' + and not ser_schema.get('info_arg') + ): + return ser_schema + if _core_utils.is_function_with_inner_schema(schema): + return _get_ser_schema_for_default_value(schema['schema']) diff --git a/source/target-vendors/cp312/pydantic/main.py b/source/target-vendors/cp312/pydantic/main.py new file mode 100644 index 0000000..2ed62e5 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/main.py @@ -0,0 +1,1838 @@ +"""Logic for creating models.""" + +# Because `dict` is in the local namespace of the `BaseModel` class, we use `Dict` for annotations. +# TODO v3 fallback to `dict` when the deprecated `dict` method gets removed. +# ruff: noqa: UP035 + +from __future__ import annotations as _annotations + +import operator +import sys +import types +import warnings +from collections.abc import Generator, Mapping +from copy import copy, deepcopy +from functools import cached_property +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Dict, + Generic, + Literal, + TypeVar, + Union, + cast, + overload, +) + +import pydantic_core +import typing_extensions +from pydantic_core import PydanticUndefined, ValidationError +from typing_extensions import Self, TypeAlias, Unpack + +from . import PydanticDeprecatedSince20, PydanticDeprecatedSince211 +from ._internal import ( + _config, + _decorators, + _fields, + _forward_ref, + _generics, + _mock_val_ser, + _model_construction, + _namespace_utils, + _repr, + _typing_extra, + _utils, +) +from ._migration import getattr_migration +from .aliases import AliasChoices, AliasPath +from .annotated_handlers import GetCoreSchemaHandler, GetJsonSchemaHandler +from .config import ConfigDict, ExtraValues +from .errors import PydanticUndefinedAnnotation, PydanticUserError +from .json_schema import DEFAULT_REF_TEMPLATE, GenerateJsonSchema, JsonSchemaMode, JsonSchemaValue, model_json_schema +from .plugin._schema_validator import PluggableSchemaValidator + +if TYPE_CHECKING: + from inspect import Signature + from pathlib import Path + + from pydantic_core import CoreSchema, SchemaSerializer, SchemaValidator + + from ._internal._fields import PydanticExtraInfo + from ._internal._namespace_utils import MappingNamespace + from ._internal._utils import AbstractSetIntStr, MappingIntStrAny + from .deprecated.parse import Protocol as DeprecatedParseProtocol + from .fields import ComputedFieldInfo, FieldInfo, ModelPrivateAttr + + +__all__ = 'BaseModel', 'create_model' + +# Keep these type aliases available at runtime: +TupleGenerator: TypeAlias = Generator[tuple[str, Any], None, None] +# NOTE: In reality, `bool` should be replaced by `Literal[True]` but mypy fails to correctly apply bidirectional +# type inference (e.g. when using `{'a': {'b': True}}`): +# NOTE: Keep this type alias in sync with the stub definition in `pydantic-core`: +IncEx: TypeAlias = Union[set[int], set[str], Mapping[int, Union['IncEx', bool]], Mapping[str, Union['IncEx', bool]]] + +_object_setattr = _model_construction.object_setattr + + +def _check_frozen(model_cls: type[BaseModel], name: str, value: Any) -> None: + if model_cls.model_config.get('frozen'): + error_type = 'frozen_instance' + elif getattr(model_cls.__pydantic_fields__.get(name), 'frozen', False): + error_type = 'frozen_field' + else: + return + + raise ValidationError.from_exception_data( + model_cls.__name__, [{'type': error_type, 'loc': (name,), 'input': value}] + ) + + +def _model_field_setattr_handler(model: BaseModel, name: str, val: Any) -> None: + model.__dict__[name] = val + model.__pydantic_fields_set__.add(name) + + +def _private_setattr_handler(model: BaseModel, name: str, val: Any) -> None: + if getattr(model, '__pydantic_private__', None) is None: + # While the attribute should be present at this point, this may not be the case if + # users do unusual stuff with `model_post_init()` (which is where the `__pydantic_private__` + # is initialized, by wrapping the user-defined `model_post_init()`), e.g. if they mock + # the `model_post_init()` call. Ideally we should find a better way to init private attrs. + object.__setattr__(model, '__pydantic_private__', {}) + model.__pydantic_private__[name] = val # pyright: ignore[reportOptionalSubscript] + + +_SIMPLE_SETATTR_HANDLERS: Mapping[str, Callable[[BaseModel, str, Any], None]] = { + 'model_field': _model_field_setattr_handler, + 'validate_assignment': lambda model, name, val: model.__pydantic_validator__.validate_assignment(model, name, val), # pyright: ignore[reportAssignmentType] + 'private': _private_setattr_handler, + 'cached_property': lambda model, name, val: model.__dict__.__setitem__(name, val), + 'extra_known': lambda model, name, val: _object_setattr(model, name, val), +} + + +class BaseModel(metaclass=_model_construction.ModelMetaclass): + """!!! abstract "Usage Documentation" + [Models](../concepts/models.md) + + A base class for creating Pydantic models. + + Attributes: + __class_vars__: The names of the class variables defined on the model. + __private_attributes__: Metadata about the private attributes of the model. + __signature__: The synthesized `__init__` [`Signature`][inspect.Signature] of the model. + + __pydantic_complete__: Whether model building is completed, or if there are still undefined fields. + __pydantic_core_schema__: The core schema of the model. + __pydantic_custom_init__: Whether the model has a custom `__init__` function. + __pydantic_decorators__: Metadata containing the decorators defined on the model. + This replaces `Model.__validators__` and `Model.__root_validators__` from Pydantic V1. + __pydantic_generic_metadata__: A dictionary containing metadata about generic Pydantic models. + The `origin` and `args` items map to the [`__origin__`][genericalias.__origin__] + and [`__args__`][genericalias.__args__] attributes of [generic aliases][types-genericalias], + and the `parameter` item maps to the `__parameter__` attribute of generic classes. + __pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models. + __pydantic_post_init__: The name of the post-init method for the model, if defined. + __pydantic_root_model__: Whether the model is a [`RootModel`][pydantic.root_model.RootModel]. + __pydantic_serializer__: The `pydantic-core` `SchemaSerializer` used to dump instances of the model. + __pydantic_validator__: The `pydantic-core` `SchemaValidator` used to validate instances of the model. + + __pydantic_fields__: A dictionary of field names and their corresponding [`FieldInfo`][pydantic.fields.FieldInfo] objects. + __pydantic_computed_fields__: A dictionary of computed field names and their corresponding [`ComputedFieldInfo`][pydantic.fields.ComputedFieldInfo] objects. + + __pydantic_extra__: A dictionary containing extra values, if [`extra`][pydantic.config.ConfigDict.extra] + is set to `'allow'`. + __pydantic_fields_set__: The names of fields explicitly set during instantiation. + __pydantic_private__: Values of private attributes set on the model instance. + """ + + # Note: Many of the below class vars are defined in the metaclass, but we define them here for type checking purposes. + + model_config: ClassVar[ConfigDict] = ConfigDict() + """ + Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict]. + """ + + __class_vars__: ClassVar[set[str]] + """The names of the class variables defined on the model.""" + + __private_attributes__: ClassVar[Dict[str, ModelPrivateAttr]] # noqa: UP006 + """Metadata about the private attributes of the model.""" + + __signature__: ClassVar[Signature] + """The synthesized `__init__` [`Signature`][inspect.Signature] of the model.""" + + __pydantic_complete__: ClassVar[bool] = False + """Whether model building is completed, or if there are still undefined fields.""" + + __pydantic_core_schema__: ClassVar[CoreSchema] + """The core schema of the model.""" + + __pydantic_custom_init__: ClassVar[bool] + """Whether the model has a custom `__init__` method.""" + + # Must be set for `GenerateSchema.model_schema` to work for a plain `BaseModel` annotation. + __pydantic_decorators__: ClassVar[_decorators.DecoratorInfos] = _decorators.DecoratorInfos() + """Metadata containing the decorators defined on the model. + This replaces `Model.__validators__` and `Model.__root_validators__` from Pydantic V1.""" + + __pydantic_generic_metadata__: ClassVar[_generics.PydanticGenericMetadata] + """A dictionary containing metadata about generic Pydantic models. + + The `origin` and `args` items map to the [`__origin__`][genericalias.__origin__] + and [`__args__`][genericalias.__args__] attributes of [generic aliases][types-genericalias], + and the `parameter` item maps to the `__parameter__` attribute of generic classes. + """ + + __pydantic_parent_namespace__: ClassVar[Dict[str, Any] | None] = None # noqa: UP006 + """Parent namespace of the model, used for automatic rebuilding of models.""" + + __pydantic_post_init__: ClassVar[None | Literal['model_post_init']] + """The name of the post-init method for the model, if defined.""" + + __pydantic_root_model__: ClassVar[bool] = False + """Whether the model is a [`RootModel`][pydantic.root_model.RootModel].""" + + __pydantic_serializer__: ClassVar[SchemaSerializer] + """The `pydantic-core` `SchemaSerializer` used to dump instances of the model.""" + + __pydantic_validator__: ClassVar[SchemaValidator | PluggableSchemaValidator] + """The `pydantic-core` `SchemaValidator` used to validate instances of the model.""" + + __pydantic_fields__: ClassVar[Dict[str, FieldInfo]] # noqa: UP006 + """A dictionary of field names and their corresponding [`FieldInfo`][pydantic.fields.FieldInfo] objects. + This replaces `Model.__fields__` from Pydantic V1. + """ + + __pydantic_setattr_handlers__: ClassVar[Dict[str, Callable[[BaseModel, str, Any], None]]] # noqa: UP006 + """`__setattr__` handlers. Memoizing the handlers leads to a dramatic performance improvement in `__setattr__`""" + + __pydantic_computed_fields__: ClassVar[Dict[str, ComputedFieldInfo]] # noqa: UP006 + """A dictionary of computed field names and their corresponding [`ComputedFieldInfo`][pydantic.fields.ComputedFieldInfo] objects.""" + + __pydantic_extra_info__: ClassVar[PydanticExtraInfo | None] + """A wrapper around the `__pydantic_extra__` annotation, if explicitly annotated on a model. + + This is a private attribute, not meant to be used outside Pydantic. + """ + + __pydantic_extra__: Dict[str, Any] | None = _model_construction.NoInitField(init=False) # noqa: UP006 + """A dictionary containing extra values, if [`extra`][pydantic.config.ConfigDict.extra] is set to `'allow'`.""" + + __pydantic_fields_set__: set[str] = _model_construction.NoInitField(init=False) + """The names of fields explicitly set during instantiation.""" + + __pydantic_private__: Dict[str, Any] | None = _model_construction.NoInitField(init=False) # noqa: UP006 + """Values of private attributes set on the model instance.""" + + if not TYPE_CHECKING: + # Prevent `BaseModel` from being instantiated directly + # (defined in an `if not TYPE_CHECKING` block for clarity and to avoid type checking errors): + __pydantic_core_schema__ = _mock_val_ser.MockCoreSchema( + 'Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly', + code='base-model-instantiated', + ) + __pydantic_validator__ = _mock_val_ser.MockValSer( + 'Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly', + val_or_ser='validator', + code='base-model-instantiated', + ) + __pydantic_serializer__ = _mock_val_ser.MockValSer( + 'Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly', + val_or_ser='serializer', + code='base-model-instantiated', + ) + + __slots__ = '__dict__', '__pydantic_fields_set__', '__pydantic_extra__', '__pydantic_private__' + + def __init__(self, /, **data: Any) -> None: + """Create a new model by parsing and validating input data from keyword arguments. + + Raises [`ValidationError`][pydantic_core.ValidationError] if the input data cannot be + validated to form a valid model. + + `self` is explicitly positional-only to allow `self` as a field name. + """ + # `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks + __tracebackhide__ = True + validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self) + if self is not validated_self: + warnings.warn( + 'A custom validator is returning a value other than `self`.\n' + "Returning anything other than `self` from a top level model validator isn't supported when validating via `__init__`.\n" + 'See the `model_validator` docs (https://docs.pydantic.dev/latest/concepts/validators/#model-validators) for more details.', + stacklevel=2, + ) + + # The following line sets a flag that we use to determine when `__init__` gets overridden by the user + __init__.__pydantic_base_init__ = True # pyright: ignore[reportFunctionMemberAccess] + + @_utils.deprecated_instance_property + @classmethod + def model_fields(cls) -> dict[str, FieldInfo]: + """A mapping of field names to their respective [`FieldInfo`][pydantic.fields.FieldInfo] instances. + + !!! warning + Accessing this attribute from a model instance is deprecated, and will not work in Pydantic V3. + Instead, you should access this attribute from the model class. + """ + return getattr(cls, '__pydantic_fields__', {}) + + @_utils.deprecated_instance_property + @classmethod + def model_computed_fields(cls) -> dict[str, ComputedFieldInfo]: + """A mapping of computed field names to their respective [`ComputedFieldInfo`][pydantic.fields.ComputedFieldInfo] instances. + + !!! warning + Accessing this attribute from a model instance is deprecated, and will not work in Pydantic V3. + Instead, you should access this attribute from the model class. + """ + return getattr(cls, '__pydantic_computed_fields__', {}) + + @property + def model_extra(self) -> dict[str, Any] | None: + """Get extra fields set during validation. + + Returns: + A dictionary of extra fields, or `None` if `config.extra` is not set to `"allow"`. + """ + return self.__pydantic_extra__ + + @property + def model_fields_set(self) -> set[str]: + """Returns the set of fields that have been explicitly set on this model instance. + + Returns: + A set of strings representing the fields that have been set, + i.e. that were not filled from defaults. + """ + return self.__pydantic_fields_set__ + + @classmethod + def model_construct(cls, _fields_set: set[str] | None = None, **values: Any) -> Self: # noqa: C901 + """Creates a new instance of the `Model` class with validated data. + + Creates a new model setting `__dict__` and `__pydantic_fields_set__` from trusted or pre-validated data. + Default values are respected, but no other validation is performed. + + !!! note + `model_construct()` generally respects the `model_config.extra` setting on the provided model. + That is, if `model_config.extra == 'allow'`, then all extra passed values are added to the model instance's `__dict__` + and `__pydantic_extra__` fields. If `model_config.extra == 'ignore'` (the default), then all extra passed values are ignored. + Because no validation is performed with a call to `model_construct()`, having `model_config.extra == 'forbid'` does not result in + an error if extra values are passed, but they will be ignored. + + Args: + _fields_set: A set of field names that were originally explicitly set during instantiation. If provided, + this is directly used for the [`model_fields_set`][pydantic.BaseModel.model_fields_set] attribute. + Otherwise, the field names from the `values` argument will be used. + values: Trusted or pre-validated data dictionary. + + Returns: + A new instance of the `Model` class with validated data. + """ + m = cls.__new__(cls) + fields_values: dict[str, Any] = {} + fields_set = set() + + for name, field in cls.__pydantic_fields__.items(): + if field.alias is not None and field.alias in values: + fields_values[name] = values.pop(field.alias) + fields_set.add(name) + + if (name not in fields_set) and (field.validation_alias is not None): + validation_aliases: list[str | AliasPath] = ( + field.validation_alias.choices + if isinstance(field.validation_alias, AliasChoices) + else [field.validation_alias] + ) + + for alias in validation_aliases: + if isinstance(alias, str) and alias in values: + fields_values[name] = values.pop(alias) + fields_set.add(name) + break + elif isinstance(alias, AliasPath): + value = alias.search_dict_for_path(values) + if value is not PydanticUndefined: + fields_values[name] = value + fields_set.add(name) + break + + if name not in fields_set: + if name in values: + fields_values[name] = values.pop(name) + fields_set.add(name) + elif not field.is_required(): + fields_values[name] = field.get_default(call_default_factory=True, validated_data=fields_values) + if _fields_set is None: + _fields_set = fields_set + + _extra: dict[str, Any] | None = values if cls.model_config.get('extra') == 'allow' else None + _object_setattr(m, '__dict__', fields_values) + _object_setattr(m, '__pydantic_fields_set__', _fields_set) + if not cls.__pydantic_root_model__: + _object_setattr(m, '__pydantic_extra__', _extra) + _object_setattr(m, '__pydantic_private__', None) + + if cls.__pydantic_post_init__: + m.model_post_init(None) + # update private attributes with values set + if hasattr(m, '__pydantic_private__') and m.__pydantic_private__ is not None: + for k, v in values.items(): + if k in m.__private_attributes__: + m.__pydantic_private__[k] = v + + return m + + def model_copy(self, *, update: Mapping[str, Any] | None = None, deep: bool = False) -> Self: + """!!! abstract "Usage Documentation" + [`model_copy`](../concepts/models.md#model-copy) + + Returns a copy of the model. + + !!! note + The underlying instance's [`__dict__`][object.__dict__] attribute is copied. This + might have unexpected side effects if you store anything in it, on top of the model + fields (e.g. the value of [cached properties][functools.cached_property]). + + Args: + update: Values to change/add in the new model. Note: the data is not validated + before creating the new model. You should trust this data. + deep: Set to `True` to make a deep copy of the model. + + Returns: + New model instance. + """ + copied = self.__deepcopy__() if deep else self.__copy__() + if update: + if self.model_config.get('extra') == 'allow': + for k, v in update.items(): + if k in self.__pydantic_fields__: + copied.__dict__[k] = v + else: + if copied.__pydantic_extra__ is None: + copied.__pydantic_extra__ = {} + copied.__pydantic_extra__[k] = v + else: + copied.__dict__.update(update) + copied.__pydantic_fields_set__.update(update.keys()) + return copied + + def model_dump( + self, + *, + mode: Literal['json', 'python'] | str = 'python', + include: IncEx | None = None, + exclude: IncEx | None = None, + context: Any | None = None, + by_alias: bool | None = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + exclude_computed_fields: bool = False, + round_trip: bool = False, + warnings: bool | Literal['none', 'warn', 'error'] = True, + fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, + polymorphic_serialization: bool | None = None, + ) -> dict[str, Any]: + """!!! abstract "Usage Documentation" + [`model_dump`](../concepts/serialization.md#python-mode) + + Generate a dictionary representation of the model, optionally specifying which fields to include or exclude. + + Args: + mode: The mode in which `to_python` should run. + If mode is 'json', the output will only contain JSON serializable types. + If mode is 'python', the output may contain non-JSON-serializable Python objects. + include: A set of fields to include in the output. + exclude: A set of fields to exclude from the output. + context: Additional context to pass to the serializer. + by_alias: Whether to use the field's alias in the dictionary key if defined. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that are set to their default value. + exclude_none: Whether to exclude fields that have a value of `None`. + exclude_computed_fields: Whether to exclude computed fields. + While this can be useful for round-tripping, it is usually recommended to use the dedicated + `round_trip` parameter instead. + round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T]. + warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors, + "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError]. + fallback: A function to call when an unknown value is encountered. If not provided, + a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised. + serialize_as_any: Whether to serialize fields with duck-typing serialization behavior. + polymorphic_serialization: Whether to use model and dataclass polymorphic serialization for this call. + + Returns: + A dictionary representation of the model. + """ + return self.__pydantic_serializer__.to_python( + self, + mode=mode, + by_alias=by_alias, + include=include, + exclude=exclude, + context=context, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + exclude_computed_fields=exclude_computed_fields, + round_trip=round_trip, + warnings=warnings, + fallback=fallback, + serialize_as_any=serialize_as_any, + polymorphic_serialization=polymorphic_serialization, + ) + + def model_dump_json( + self, + *, + indent: int | None = None, + ensure_ascii: bool = False, + include: IncEx | None = None, + exclude: IncEx | None = None, + context: Any | None = None, + by_alias: bool | None = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + exclude_computed_fields: bool = False, + round_trip: bool = False, + warnings: bool | Literal['none', 'warn', 'error'] = True, + fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, + polymorphic_serialization: bool | None = None, + ) -> str: + """!!! abstract "Usage Documentation" + [`model_dump_json`](../concepts/serialization.md#json-mode) + + Generates a JSON representation of the model using Pydantic's `to_json` method. + + Args: + indent: Indentation to use in the JSON output. If None is passed, the output will be compact. + ensure_ascii: If `True`, the output is guaranteed to have all incoming non-ASCII characters escaped. + If `False` (the default), these characters will be output as-is. + include: Field(s) to include in the JSON output. + exclude: Field(s) to exclude from the JSON output. + context: Additional context to pass to the serializer. + by_alias: Whether to serialize using field aliases. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that are set to their default value. + exclude_none: Whether to exclude fields that have a value of `None`. + exclude_computed_fields: Whether to exclude computed fields. + While this can be useful for round-tripping, it is usually recommended to use the dedicated + `round_trip` parameter instead. + round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T]. + warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors, + "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError]. + fallback: A function to call when an unknown value is encountered. If not provided, + a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised. + serialize_as_any: Whether to serialize fields with duck-typing serialization behavior. + polymorphic_serialization: Whether to use model and dataclass polymorphic serialization for this call. + + Returns: + A JSON string representation of the model. + """ + return self.__pydantic_serializer__.to_json( + self, + indent=indent, + ensure_ascii=ensure_ascii, + include=include, + exclude=exclude, + context=context, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + exclude_computed_fields=exclude_computed_fields, + round_trip=round_trip, + warnings=warnings, + fallback=fallback, + serialize_as_any=serialize_as_any, + polymorphic_serialization=polymorphic_serialization, + ).decode() + + @classmethod + def model_json_schema( + cls, + by_alias: bool = True, + ref_template: str = DEFAULT_REF_TEMPLATE, + schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema, + mode: JsonSchemaMode = 'validation', + *, + union_format: Literal['any_of', 'primitive_type_array'] = 'any_of', + ) -> dict[str, Any]: + """Generates a JSON schema for a model class. + + Args: + by_alias: Whether to use attribute aliases or not. + ref_template: The reference template. + union_format: The format to use when combining schemas from unions together. Can be one of: + + - `'any_of'`: Use the [`anyOf`](https://json-schema.org/understanding-json-schema/reference/combining#anyOf) + keyword to combine schemas (the default). + - `'primitive_type_array'`: Use the [`type`](https://json-schema.org/understanding-json-schema/reference/type) + keyword as an array of strings, containing each type of the combination. If any of the schemas is not a primitive + type (`string`, `boolean`, `null`, `integer` or `number`) or contains constraints/metadata, falls back to + `any_of`. + schema_generator: To override the logic used to generate the JSON schema, as a subclass of + `GenerateJsonSchema` with your desired modifications + mode: The mode in which to generate the schema. + + Returns: + The JSON schema for the given model class. + """ + return model_json_schema( + cls, + by_alias=by_alias, + ref_template=ref_template, + union_format=union_format, + schema_generator=schema_generator, + mode=mode, + ) + + @classmethod + def model_parametrized_name(cls, params: tuple[type[Any], ...]) -> str: + """Compute the class name for parametrizations of generic classes. + + This method can be overridden to achieve a custom naming scheme for generic BaseModels. + + Args: + params: Tuple of types of the class. Given a generic class + `Model` with 2 type variables and a concrete model `Model[str, int]`, + the value `(str, int)` would be passed to `params`. + + Returns: + String representing the new class where `params` are passed to `cls` as type variables. + + Raises: + TypeError: Raised when trying to generate concrete names for non-generic models. + """ + if not issubclass(cls, Generic): + raise TypeError('Concrete names should only be generated for generic models.') + + # Any strings received should represent forward references, so we handle them specially below. + # If we eventually move toward wrapping them in a ForwardRef in __class_getitem__ in the future, + # we may be able to remove this special case. + param_names = [param if isinstance(param, str) else _repr.display_as_type(param) for param in params] + params_component = ', '.join(param_names) + return f'{cls.__name__}[{params_component}]' + + def model_post_init(self, context: Any, /) -> None: + """Override this method to perform additional initialization after `__init__` and `model_construct`. + This is useful if you want to do some validation that requires the entire model to be initialized. + """ + + @classmethod + def model_rebuild( + cls, + *, + force: bool = False, + raise_errors: bool = True, + _parent_namespace_depth: int = 2, + _types_namespace: MappingNamespace | None = None, + ) -> bool | None: + """Try to rebuild the pydantic-core schema for the model. + + This may be necessary when one of the annotations is a ForwardRef which could not be resolved during + the initial attempt to build the schema, and automatic rebuilding fails. + + Args: + force: Whether to force the rebuilding of the model schema, defaults to `False`. + raise_errors: Whether to raise errors, defaults to `True`. + _parent_namespace_depth: The depth level of the parent namespace, defaults to 2. + _types_namespace: The types namespace, defaults to `None`. + + Returns: + Returns `None` if the schema is already "complete" and rebuilding was not required. + If rebuilding _was_ required, returns `True` if rebuilding was successful, otherwise `False`. + """ + already_complete = cls.__pydantic_complete__ + if already_complete and not force: + return None + + cls.__pydantic_complete__ = False + + for attr in ('__pydantic_core_schema__', '__pydantic_validator__', '__pydantic_serializer__'): + if attr in cls.__dict__ and not isinstance(getattr(cls, attr), _mock_val_ser.MockValSer): + # Deleting the validator/serializer is necessary as otherwise they can get reused in + # pydantic-core. We do so only if they aren't mock instances, otherwise — as `model_rebuild()` + # isn't thread-safe — concurrent model instantiations can lead to the parent validator being used. + # Same applies for the core schema that can be reused in schema generation. + delattr(cls, attr) + + if _types_namespace is not None: + rebuild_ns = _types_namespace + elif _parent_namespace_depth > 0: + rebuild_ns = _typing_extra.parent_frame_namespace(parent_depth=_parent_namespace_depth, force=True) or {} + else: + rebuild_ns = {} + + parent_ns = _model_construction.unpack_lenient_weakvaluedict(cls.__pydantic_parent_namespace__) or {} + + ns_resolver = _namespace_utils.NsResolver( + parent_namespace={**rebuild_ns, **parent_ns}, + ) + + return _model_construction.complete_model_class( + cls, + _config.ConfigWrapper(cls.model_config, check=False), + ns_resolver, + raise_errors=raise_errors, + # If the model was already complete, we don't need to call the hook again. + call_on_complete_hook=not already_complete, + is_force_rebuild=force, + ) + + @classmethod + def model_validate( + cls, + obj: Any, + *, + strict: bool | None = None, + extra: ExtraValues | None = None, + from_attributes: bool | None = None, + context: Any | None = None, + by_alias: bool | None = None, + by_name: bool | None = None, + ) -> Self: + """Validate a pydantic model instance. + + Args: + obj: The object to validate. + strict: Whether to enforce types strictly. + extra: Whether to ignore, allow, or forbid extra data during model validation. + See the [`extra` configuration value][pydantic.ConfigDict.extra] for details. + from_attributes: Whether to extract data from object attributes. + context: Additional context to pass to the validator. + by_alias: Whether to use the field's alias when validating against the provided input data. + by_name: Whether to use the field's name when validating against the provided input data. + + Raises: + ValidationError: If the object could not be validated. + + Returns: + The validated model instance. + """ + # `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks + __tracebackhide__ = True + + if by_alias is False and by_name is not True: + raise PydanticUserError( + 'At least one of `by_alias` or `by_name` must be set to True.', + code='validate-by-alias-and-name-false', + ) + + return cls.__pydantic_validator__.validate_python( + obj, + strict=strict, + extra=extra, + from_attributes=from_attributes, + context=context, + by_alias=by_alias, + by_name=by_name, + ) + + @classmethod + def model_validate_json( + cls, + json_data: str | bytes | bytearray, + *, + strict: bool | None = None, + extra: ExtraValues | None = None, + context: Any | None = None, + by_alias: bool | None = None, + by_name: bool | None = None, + ) -> Self: + """!!! abstract "Usage Documentation" + [JSON Parsing](../concepts/json.md#json-parsing) + + Validate the given JSON data against the Pydantic model. + + Args: + json_data: The JSON data to validate. + strict: Whether to enforce types strictly. + extra: Whether to ignore, allow, or forbid extra data during model validation. + See the [`extra` configuration value][pydantic.ConfigDict.extra] for details. + context: Extra variables to pass to the validator. + by_alias: Whether to use the field's alias when validating against the provided input data. + by_name: Whether to use the field's name when validating against the provided input data. + + Returns: + The validated Pydantic model. + + Raises: + ValidationError: If `json_data` is not a JSON string or the object could not be validated. + """ + # `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks + __tracebackhide__ = True + + if by_alias is False and by_name is not True: + raise PydanticUserError( + 'At least one of `by_alias` or `by_name` must be set to True.', + code='validate-by-alias-and-name-false', + ) + + return cls.__pydantic_validator__.validate_json( + json_data, strict=strict, extra=extra, context=context, by_alias=by_alias, by_name=by_name + ) + + @classmethod + def model_validate_strings( + cls, + obj: Any, + *, + strict: bool | None = None, + extra: ExtraValues | None = None, + context: Any | None = None, + by_alias: bool | None = None, + by_name: bool | None = None, + ) -> Self: + """Validate the given object with string data against the Pydantic model. + + Args: + obj: The object containing string data to validate. + strict: Whether to enforce types strictly. + extra: Whether to ignore, allow, or forbid extra data during model validation. + See the [`extra` configuration value][pydantic.ConfigDict.extra] for details. + context: Extra variables to pass to the validator. + by_alias: Whether to use the field's alias when validating against the provided input data. + by_name: Whether to use the field's name when validating against the provided input data. + + Returns: + The validated Pydantic model. + """ + # `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks + __tracebackhide__ = True + + if by_alias is False and by_name is not True: + raise PydanticUserError( + 'At least one of `by_alias` or `by_name` must be set to True.', + code='validate-by-alias-and-name-false', + ) + + return cls.__pydantic_validator__.validate_strings( + obj, strict=strict, extra=extra, context=context, by_alias=by_alias, by_name=by_name + ) + + @classmethod + def __get_pydantic_core_schema__(cls, source: type[BaseModel], handler: GetCoreSchemaHandler, /) -> CoreSchema: + # This warning is only emitted when calling `super().__get_pydantic_core_schema__` from a model subclass. + # In the generate schema logic, this method (`BaseModel.__get_pydantic_core_schema__`) is special cased to + # *not* be called if not overridden. + warnings.warn( + 'The `__get_pydantic_core_schema__` method of the `BaseModel` class is deprecated. If you are calling ' + '`super().__get_pydantic_core_schema__` when overriding the method on a Pydantic model, consider using ' + '`handler(source)` instead. However, note that overriding this method on models can lead to unexpected ' + 'side effects.', + PydanticDeprecatedSince211, + stacklevel=2, + ) + # Logic copied over from `GenerateSchema._model_schema`: + schema = cls.__dict__.get('__pydantic_core_schema__') + if schema is not None and not isinstance(schema, _mock_val_ser.MockCoreSchema): + return cls.__pydantic_core_schema__ + + return handler(source) + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + /, + ) -> JsonSchemaValue: + """Hook into generating the model's JSON schema. + + Args: + core_schema: A `pydantic-core` CoreSchema. + You can ignore this argument and call the handler with a new CoreSchema, + wrap this CoreSchema (`{'type': 'nullable', 'schema': current_schema}`), + or just call the handler with the original schema. + handler: Call into Pydantic's internal JSON schema generation. + This will raise a `pydantic.errors.PydanticInvalidForJsonSchema` if JSON schema + generation fails. + Since this gets called by `BaseModel.model_json_schema` you can override the + `schema_generator` argument to that function to change JSON schema generation globally + for a type. + + Returns: + A JSON schema, as a Python object. + """ + return handler(core_schema) + + @classmethod + def __pydantic_init_subclass__(cls, **kwargs: Any) -> None: + """This is intended to behave just like `__init_subclass__`, but is called by `ModelMetaclass` + only after basic class initialization is complete. In particular, attributes like `model_fields` will + be present when this is called, but forward annotations are not guaranteed to be resolved yet, + meaning that creating an instance of the class may fail. + + This is necessary because `__init_subclass__` will always be called by `type.__new__`, + and it would require a prohibitively large refactor to the `ModelMetaclass` to ensure that + `type.__new__` was called in such a manner that the class would already be sufficiently initialized. + + This will receive the same `kwargs` that would be passed to the standard `__init_subclass__`, namely, + any kwargs passed to the class definition that aren't used internally by Pydantic. + + Args: + **kwargs: Any keyword arguments passed to the class definition that aren't used internally + by Pydantic. + + Note: + You may want to override [`__pydantic_on_complete__()`][pydantic.main.BaseModel.__pydantic_on_complete__] + instead, which is called once the class and its fields are fully initialized and ready for validation. + """ + + @classmethod + def __pydantic_on_complete__(cls) -> None: + """This is called once the class and its fields are fully initialized and ready to be used. + + This typically happens when the class is created (just before + [`__pydantic_init_subclass__()`][pydantic.main.BaseModel.__pydantic_init_subclass__] is called on the superclass), + except when forward annotations are used that could not immediately be resolved. + In that case, it will be called later, when the model is rebuilt automatically or explicitly using + [`model_rebuild()`][pydantic.main.BaseModel.model_rebuild]. + """ + + def __class_getitem__( + cls, typevar_values: type[Any] | tuple[type[Any], ...] + ) -> type[BaseModel] | _forward_ref.PydanticRecursiveRef: + cached = _generics.get_cached_generic_type_early(cls, typevar_values) + if cached is not None: + return cached + + if cls is BaseModel: + raise TypeError('Type parameters should be placed on typing.Generic, not BaseModel') + if not hasattr(cls, '__parameters__'): + raise TypeError(f'{cls} cannot be parametrized because it does not inherit from typing.Generic') + if not cls.__pydantic_generic_metadata__['parameters'] and Generic not in cls.__bases__: + raise TypeError(f'{cls} is not a generic class') + + if not isinstance(typevar_values, tuple): + typevar_values = (typevar_values,) + + # For a model `class Model[T, U, V = int](BaseModel): ...` parametrized with `(str, bool)`, + # this gives us `{T: str, U: bool, V: int}`: + typevars_map = _generics.map_generic_model_arguments(cls, typevar_values) + # We also update the provided args to use defaults values (`(str, bool)` becomes `(str, bool, int)`): + typevar_values = tuple(v for v in typevars_map.values()) + + if _utils.all_identical(typevars_map.keys(), typevars_map.values()) and typevars_map: + submodel = cls # if arguments are equal to parameters it's the same object + _generics.set_cached_generic_type(cls, typevar_values, submodel) + else: + parent_args = cls.__pydantic_generic_metadata__['args'] + if not parent_args: + args = typevar_values + else: + args = tuple(_generics.replace_types(arg, typevars_map) for arg in parent_args) + + origin = cls.__pydantic_generic_metadata__['origin'] or cls + model_name = origin.model_parametrized_name(args) + params = tuple( + dict.fromkeys(_generics.iter_contained_typevars(typevars_map.values())) + ) # use dict as ordered set + + with _generics.generic_recursion_self_type(origin, args) as maybe_self_type: + cached = _generics.get_cached_generic_type_late(cls, typevar_values, origin, args) + if cached is not None: + return cached + + if maybe_self_type is not None: + return maybe_self_type + + # Attempt to rebuild the origin in case new types have been defined + try: + # depth 2 gets you above this __class_getitem__ call. + # Note that we explicitly provide the parent ns, otherwise + # `model_rebuild` will use the parent ns no matter if it is the ns of a module. + # We don't want this here, as this has unexpected effects when a model + # is being parametrized during a forward annotation evaluation. + parent_ns = _typing_extra.parent_frame_namespace(parent_depth=2) or {} + origin.model_rebuild(_types_namespace=parent_ns) + except PydanticUndefinedAnnotation: + # It's okay if it fails, it just means there are still undefined types + # that could be evaluated later. + pass + + submodel = _generics.create_generic_submodel(model_name, origin, args, params) + + _generics.set_cached_generic_type(cls, typevar_values, submodel, origin, args) + + return submodel + + def __copy__(self) -> Self: + """Returns a shallow copy of the model.""" + cls = type(self) + m = cls.__new__(cls) + _object_setattr(m, '__dict__', copy(self.__dict__)) + _object_setattr(m, '__pydantic_extra__', copy(self.__pydantic_extra__)) + _object_setattr(m, '__pydantic_fields_set__', copy(self.__pydantic_fields_set__)) + + if not hasattr(self, '__pydantic_private__') or self.__pydantic_private__ is None: + _object_setattr(m, '__pydantic_private__', None) + else: + _object_setattr( + m, + '__pydantic_private__', + {k: v for k, v in self.__pydantic_private__.items() if v is not PydanticUndefined}, + ) + + return m + + def __deepcopy__(self, memo: dict[int, Any] | None = None) -> Self: + """Returns a deep copy of the model.""" + cls = type(self) + m = cls.__new__(cls) + _object_setattr(m, '__dict__', deepcopy(self.__dict__, memo=memo)) + _object_setattr(m, '__pydantic_extra__', deepcopy(self.__pydantic_extra__, memo=memo)) + # This next line doesn't need a deepcopy because __pydantic_fields_set__ is a set[str], + # and attempting a deepcopy would be marginally slower. + _object_setattr(m, '__pydantic_fields_set__', copy(self.__pydantic_fields_set__)) + + if not hasattr(self, '__pydantic_private__') or self.__pydantic_private__ is None: + _object_setattr(m, '__pydantic_private__', None) + else: + _object_setattr( + m, + '__pydantic_private__', + deepcopy({k: v for k, v in self.__pydantic_private__.items() if v is not PydanticUndefined}, memo=memo), + ) + + return m + + if not TYPE_CHECKING: + # We put `__getattr__` in a non-TYPE_CHECKING block because otherwise, mypy allows arbitrary attribute access + # The same goes for __setattr__ and __delattr__, see: https://github.com/pydantic/pydantic/issues/8643 + + def __getattr__(self, item: str) -> Any: + private_attributes = object.__getattribute__(self, '__private_attributes__') + if item in private_attributes: + attribute = private_attributes[item] + if hasattr(attribute, '__get__'): + return attribute.__get__(self, type(self)) # type: ignore + + try: + # Note: self.__pydantic_private__ cannot be None if self.__private_attributes__ has items + return self.__pydantic_private__[item] # type: ignore + except KeyError as exc: + raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}') from exc + else: + # `__pydantic_extra__` can fail to be set if the model is not yet fully initialized. + # See `BaseModel.__repr_args__` for more details + try: + pydantic_extra = object.__getattribute__(self, '__pydantic_extra__') + except AttributeError: + pydantic_extra = None + + if pydantic_extra and item in pydantic_extra: + return pydantic_extra[item] + else: + if hasattr(self.__class__, item): + return super().__getattribute__(item) # Raises AttributeError if appropriate + else: + # this is the current error + raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}') + + def __setattr__(self, name: str, value: Any) -> None: + if (setattr_handler := self.__pydantic_setattr_handlers__.get(name)) is not None: + setattr_handler(self, name, value) + # if None is returned from _setattr_handler, the attribute was set directly + elif (setattr_handler := self._setattr_handler(name, value)) is not None: + setattr_handler(self, name, value) # call here to not memo on possibly unknown fields + self.__pydantic_setattr_handlers__[name] = setattr_handler # memoize the handler for faster access + + def _setattr_handler(self, name: str, value: Any) -> Callable[[BaseModel, str, Any], None] | None: + """Get a handler for setting an attribute on the model instance. + + Returns: + A handler for setting an attribute on the model instance. Used for memoization of the handler. + Memoizing the handlers leads to a dramatic performance improvement in `__setattr__` + Returns `None` when memoization is not safe, then the attribute is set directly. + """ + cls = self.__class__ + if name in cls.__class_vars__: + raise AttributeError( + f'{name!r} is a ClassVar of `{cls.__name__}` and cannot be set on an instance. ' + f'If you want to set a value on the class, use `{cls.__name__}.{name} = value`.' + ) + elif not _fields.is_valid_field_name(name): + if (attribute := cls.__private_attributes__.get(name)) is not None: + if hasattr(attribute, '__set__'): + return lambda model, _name, val: attribute.__set__(model, val) + else: + return _SIMPLE_SETATTR_HANDLERS['private'] + else: + _object_setattr(self, name, value) + return None # Can not return memoized handler with possibly freeform attr names + + attr = getattr(cls, name, None) + # NOTE: We currently special case properties and `cached_property`, but we might need + # to generalize this to all data/non-data descriptors at some point. For non-data descriptors + # (such as `cached_property`), it isn't obvious though. `cached_property` caches the value + # to the instance's `__dict__`, but other non-data descriptors might do things differently. + if isinstance(attr, cached_property): + return _SIMPLE_SETATTR_HANDLERS['cached_property'] + + _check_frozen(cls, name, value) + + # We allow properties to be set only on non frozen models for now (to match dataclasses). + # This can be changed if it ever gets requested. + if isinstance(attr, property): + return lambda model, _name, val: attr.__set__(model, val) + elif cls.model_config.get('validate_assignment'): + return _SIMPLE_SETATTR_HANDLERS['validate_assignment'] + elif name not in cls.__pydantic_fields__: + if cls.model_config.get('extra') != 'allow': + # TODO - matching error + raise ValueError(f'"{cls.__name__}" object has no field "{name}"') + elif attr is None: + # attribute does not exist, so put it in extra + self.__pydantic_extra__[name] = value + self.__pydantic_fields_set__.add(name) + return None # Can not return memoized handler with possibly freeform attr names + else: + # attribute _does_ exist, and was not in extra, so update it + return _SIMPLE_SETATTR_HANDLERS['extra_known'] + else: + return _SIMPLE_SETATTR_HANDLERS['model_field'] + + def __delattr__(self, item: str) -> Any: + cls = self.__class__ + + if item in self.__private_attributes__: + attribute = self.__private_attributes__[item] + if hasattr(attribute, '__delete__'): + attribute.__delete__(self) # type: ignore + return + + try: + # Note: self.__pydantic_private__ cannot be None if self.__private_attributes__ has items + del self.__pydantic_private__[item] # type: ignore + return + except KeyError as exc: + raise AttributeError(f'{cls.__name__!r} object has no attribute {item!r}') from exc + + # Allow cached properties to be deleted (even if the class is frozen): + attr = getattr(cls, item, None) + if isinstance(attr, cached_property): + return object.__delattr__(self, item) + + _check_frozen(cls, name=item, value=None) + + if item in self.__pydantic_fields__: + object.__delattr__(self, item) + elif self.__pydantic_extra__ is not None and item in self.__pydantic_extra__: + del self.__pydantic_extra__[item] + else: + try: + object.__delattr__(self, item) + except AttributeError: + raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}') + + # Because we make use of `@dataclass_transform()`, `__replace__` is already synthesized by + # type checkers, so we define the implementation in this `if not TYPE_CHECKING:` block: + def __replace__(self, **changes: Any) -> Self: + return self.model_copy(update=changes) + + def __getstate__(self) -> dict[Any, Any]: + private = self.__pydantic_private__ + if private: + private = {k: v for k, v in private.items() if v is not PydanticUndefined} + return { + '__dict__': self.__dict__, + '__pydantic_extra__': self.__pydantic_extra__, + '__pydantic_fields_set__': self.__pydantic_fields_set__, + '__pydantic_private__': private, + } + + def __setstate__(self, state: dict[Any, Any]) -> None: + _object_setattr(self, '__pydantic_fields_set__', state.get('__pydantic_fields_set__', {})) + _object_setattr(self, '__pydantic_extra__', state.get('__pydantic_extra__', {})) + _object_setattr(self, '__pydantic_private__', state.get('__pydantic_private__', {})) + _object_setattr(self, '__dict__', state.get('__dict__', {})) + + if not TYPE_CHECKING: + + def __eq__(self, other: Any) -> bool: + if isinstance(other, BaseModel): + # When comparing instances of generic types for equality, as long as all field values are equal, + # only require their generic origin types to be equal, rather than exact type equality. + # This prevents headaches like MyGeneric(x=1) != MyGeneric[Any](x=1). + self_type = self.__pydantic_generic_metadata__['origin'] or self.__class__ + other_type = other.__pydantic_generic_metadata__['origin'] or other.__class__ + + # Perform common checks first + if not ( + self_type is other_type + and getattr(self, '__pydantic_private__', None) == getattr(other, '__pydantic_private__', None) + # We need to assume `None` and `{}` are equivalent, because extra behavior + # can be controlled at validation time: + and (self.__pydantic_extra__ or {}) == (other.__pydantic_extra__ or {}) + ): + return False + + # We only want to compare pydantic fields but ignoring fields is costly. + # We'll perform a fast check first, and fallback only when needed + # See GH-7444 and GH-7825 for rationale and a performance benchmark + + # First, do the fast (and sometimes faulty) __dict__ comparison + if self.__dict__ == other.__dict__: + # If the check above passes, then pydantic fields are equal, we can return early + return True + + # We don't want to trigger unnecessary costly filtering of __dict__ on all unequal objects, so we return + # early if there are no keys to ignore (we would just return False later on anyway) + model_fields = type(self).__pydantic_fields__.keys() + if self.__dict__.keys() <= model_fields and other.__dict__.keys() <= model_fields: + return False + + # If we reach here, there are non-pydantic-fields keys, mapped to unequal values, that we need to ignore + # Resort to costly filtering of the __dict__ objects + # We use operator.itemgetter because it is much faster than dict comprehensions + # NOTE: Contrary to standard python class and instances, when the Model class has a default value for an + # attribute and the model instance doesn't have a corresponding attribute, accessing the missing attribute + # raises an error in BaseModel.__getattr__ instead of returning the class attribute + # So we can use operator.itemgetter() instead of operator.attrgetter() + getter = operator.itemgetter(*model_fields) if model_fields else lambda _: _utils._SENTINEL + try: + return getter(self.__dict__) == getter(other.__dict__) + except KeyError: + # In rare cases (such as when using the deprecated BaseModel.copy() method), + # the __dict__ may not contain all model fields, which is how we can get here. + # getter(self.__dict__) is much faster than any 'safe' method that accounts + # for missing keys, and wrapping it in a `try` doesn't slow things down much + # in the common case. + self_fields_proxy = _utils.SafeGetItemProxy(self.__dict__) + other_fields_proxy = _utils.SafeGetItemProxy(other.__dict__) + return getter(self_fields_proxy) == getter(other_fields_proxy) + + # other instance is not a BaseModel + else: + return NotImplemented # delegate to the other item in the comparison + + if TYPE_CHECKING: + # We put `__init_subclass__` in a TYPE_CHECKING block because, even though we want the type-checking benefits + # described in the signature of `__init_subclass__` below, we don't want to modify the default behavior of + # subclass initialization. + + def __init_subclass__(cls, **kwargs: Unpack[ConfigDict]): + """This signature is included purely to help type-checkers check arguments to class declaration, which + provides a way to conveniently set model_config key/value pairs. + + ```python + from pydantic import BaseModel + + class MyModel(BaseModel, extra='allow'): ... + ``` + + However, this may be deceiving, since the _actual_ calls to `__init_subclass__` will not receive any + of the config arguments, and will only receive any keyword arguments passed during class initialization + that are _not_ expected keys in ConfigDict. (This is due to the way `ModelMetaclass.__new__` works.) + + Args: + **kwargs: Keyword arguments passed to the class definition, which set model_config + + Note: + You may want to override `__pydantic_init_subclass__` instead, which behaves similarly but is called + *after* the class is fully initialized. + """ + + def __iter__(self) -> TupleGenerator: + """So `dict(model)` works.""" + yield from [(k, v) for (k, v) in self.__dict__.items() if not k.startswith('_')] + extra = self.__pydantic_extra__ + if extra: + yield from extra.items() + + def __repr__(self) -> str: + return f'{self.__repr_name__()}({self.__repr_str__(", ")})' + + def __repr_args__(self) -> _repr.ReprArgs: + # Eagerly create the repr of computed fields, as this may trigger access of cached properties and as such + # modify the instance's `__dict__`. If we don't do it now, it could happen when iterating over the `__dict__` + # below if the instance happens to be referenced in a field, and would modify the `__dict__` size *during* iteration. + computed_fields_repr_args = [ + (k, getattr(self, k)) for k, v in self.__pydantic_computed_fields__.items() if v.repr + ] + + for k, v in self.__dict__.items(): + field = self.__pydantic_fields__.get(k) + if field and field.repr: + if v is not self: + yield k, v + else: + yield k, self.__repr_recursion__(v) + # `__pydantic_extra__` can fail to be set if the model is not yet fully initialized. + # This can happen if a `ValidationError` is raised during initialization and the instance's + # repr is generated as part of the exception handling. Therefore, we use `getattr` here + # with a fallback, even though the type hints indicate the attribute will always be present. + try: + pydantic_extra = object.__getattribute__(self, '__pydantic_extra__') + except AttributeError: + pydantic_extra = None + + if pydantic_extra is not None: + yield from ((k, v) for k, v in pydantic_extra.items()) + yield from computed_fields_repr_args + + # take logic from `_repr.Representation` without the side effects of inheritance, see #5740 + __repr_name__ = _repr.Representation.__repr_name__ + __repr_recursion__ = _repr.Representation.__repr_recursion__ + __repr_str__ = _repr.Representation.__repr_str__ + __pretty__ = _repr.Representation.__pretty__ + __rich_repr__ = _repr.Representation.__rich_repr__ + + def __str__(self) -> str: + return self.__repr_str__(' ') + + # ##### Deprecated methods from v1 ##### + @property + @typing_extensions.deprecated( + 'The `__fields__` attribute is deprecated, use the `model_fields` class property instead.', category=None + ) + def __fields__(self) -> dict[str, FieldInfo]: + warnings.warn( + 'The `__fields__` attribute is deprecated, use the `model_fields` class property instead.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + return getattr(type(self), '__pydantic_fields__', {}) + + @property + @typing_extensions.deprecated( + 'The `__fields_set__` attribute is deprecated, use `model_fields_set` instead.', + category=None, + ) + def __fields_set__(self) -> set[str]: + warnings.warn( + 'The `__fields_set__` attribute is deprecated, use `model_fields_set` instead.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + return self.__pydantic_fields_set__ + + @typing_extensions.deprecated('The `dict` method is deprecated; use `model_dump` instead.', category=None) + def dict( # noqa: D102 + self, + *, + include: IncEx | None = None, + exclude: IncEx | None = None, + by_alias: bool = False, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + ) -> Dict[str, Any]: # noqa UP006 + warnings.warn( + 'The `dict` method is deprecated; use `model_dump` instead.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + return self.model_dump( + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + @typing_extensions.deprecated('The `json` method is deprecated; use `model_dump_json` instead.', category=None) + def json( # noqa: D102 + self, + *, + include: IncEx | None = None, + exclude: IncEx | None = None, + by_alias: bool = False, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + encoder: Callable[[Any], Any] | None = PydanticUndefined, # type: ignore[assignment] + models_as_dict: bool = PydanticUndefined, # type: ignore[assignment] + **dumps_kwargs: Any, + ) -> str: + warnings.warn( + 'The `json` method is deprecated; use `model_dump_json` instead.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + if encoder is not PydanticUndefined: + raise TypeError('The `encoder` argument is no longer supported; use field serializers instead.') + if models_as_dict is not PydanticUndefined: + raise TypeError('The `models_as_dict` argument is no longer supported; use a model serializer instead.') + if dumps_kwargs: + raise TypeError('`dumps_kwargs` keyword arguments are no longer supported.') + return self.model_dump_json( + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + @classmethod + @typing_extensions.deprecated('The `parse_obj` method is deprecated; use `model_validate` instead.', category=None) + def parse_obj(cls, obj: Any) -> Self: # noqa: D102 + warnings.warn( + 'The `parse_obj` method is deprecated; use `model_validate` instead.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + return cls.model_validate(obj) + + @classmethod + @typing_extensions.deprecated( + 'The `parse_raw` method is deprecated; if your data is JSON use `model_validate_json`, ' + 'otherwise load the data then use `model_validate` instead.', + category=None, + ) + def parse_raw( # noqa: D102 + cls, + b: str | bytes, + *, + content_type: str | None = None, + encoding: str = 'utf8', + proto: DeprecatedParseProtocol | None = None, + allow_pickle: bool = False, + ) -> Self: # pragma: no cover + warnings.warn( + 'The `parse_raw` method is deprecated; if your data is JSON use `model_validate_json`, ' + 'otherwise load the data then use `model_validate` instead.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + from .deprecated import parse + + try: + obj = parse.load_str_bytes( + b, + proto=proto, + content_type=content_type, + encoding=encoding, + allow_pickle=allow_pickle, + ) + except (ValueError, TypeError) as exc: + import json + + # try to match V1 + if isinstance(exc, UnicodeDecodeError): + type_str = 'value_error.unicodedecode' + elif isinstance(exc, json.JSONDecodeError): + type_str = 'value_error.jsondecode' + elif isinstance(exc, ValueError): + type_str = 'value_error' + else: + type_str = 'type_error' + + # ctx is missing here, but since we've added `input` to the error, we're not pretending it's the same + error: pydantic_core.InitErrorDetails = { + # The type: ignore on the next line is to ignore the requirement of LiteralString + 'type': pydantic_core.PydanticCustomError(type_str, str(exc)), # type: ignore + 'loc': ('__root__',), + 'input': b, + } + raise pydantic_core.ValidationError.from_exception_data(cls.__name__, [error]) + return cls.model_validate(obj) + + @classmethod + @typing_extensions.deprecated( + 'The `parse_file` method is deprecated; load the data from file, then if your data is JSON ' + 'use `model_validate_json`, otherwise `model_validate` instead.', + category=None, + ) + def parse_file( # noqa: D102 + cls, + path: str | Path, + *, + content_type: str | None = None, + encoding: str = 'utf8', + proto: DeprecatedParseProtocol | None = None, + allow_pickle: bool = False, + ) -> Self: + warnings.warn( + 'The `parse_file` method is deprecated; load the data from file, then if your data is JSON ' + 'use `model_validate_json`, otherwise `model_validate` instead.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + from .deprecated import parse + + obj = parse.load_file( + path, + proto=proto, + content_type=content_type, + encoding=encoding, + allow_pickle=allow_pickle, + ) + return cls.parse_obj(obj) + + @classmethod + @typing_extensions.deprecated( + 'The `from_orm` method is deprecated; set ' + "`model_config['from_attributes']=True` and use `model_validate` instead.", + category=None, + ) + def from_orm(cls, obj: Any) -> Self: # noqa: D102 + warnings.warn( + 'The `from_orm` method is deprecated; set ' + "`model_config['from_attributes']=True` and use `model_validate` instead.", + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + if not cls.model_config.get('from_attributes', None): + raise PydanticUserError( + 'You must set the config attribute `from_attributes=True` to use from_orm', code=None + ) + return cls.model_validate(obj) + + @classmethod + @typing_extensions.deprecated('The `construct` method is deprecated; use `model_construct` instead.', category=None) + def construct(cls, _fields_set: set[str] | None = None, **values: Any) -> Self: # noqa: D102 + warnings.warn( + 'The `construct` method is deprecated; use `model_construct` instead.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + return cls.model_construct(_fields_set=_fields_set, **values) + + @typing_extensions.deprecated( + 'The `copy` method is deprecated; use `model_copy` instead. ' + 'See the docstring of `BaseModel.copy` for details about how to handle `include` and `exclude`.', + category=None, + ) + def copy( + self, + *, + include: AbstractSetIntStr | MappingIntStrAny | None = None, + exclude: AbstractSetIntStr | MappingIntStrAny | None = None, + update: Dict[str, Any] | None = None, # noqa UP006 + deep: bool = False, + ) -> Self: # pragma: no cover + """Returns a copy of the model. + + !!! warning "Deprecated" + This method is now deprecated; use `model_copy` instead. + + If you need `include` or `exclude`, use: + + ```python {test="skip" lint="skip"} + data = self.model_dump(include=include, exclude=exclude, round_trip=True) + data = {**data, **(update or {})} + copied = self.model_validate(data) + ``` + + Args: + include: Optional set or mapping specifying which fields to include in the copied model. + exclude: Optional set or mapping specifying which fields to exclude in the copied model. + update: Optional dictionary of field-value pairs to override field values in the copied model. + deep: If True, the values of fields that are Pydantic models will be deep-copied. + + Returns: + A copy of the model with included, excluded and updated fields as specified. + """ + warnings.warn( + 'The `copy` method is deprecated; use `model_copy` instead. ' + 'See the docstring of `BaseModel.copy` for details about how to handle `include` and `exclude`.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + from .deprecated import copy_internals + + values = dict( + copy_internals._iter( + self, to_dict=False, by_alias=False, include=include, exclude=exclude, exclude_unset=False + ), + **(update or {}), + ) + if self.__pydantic_private__ is None: + private = None + else: + private = {k: v for k, v in self.__pydantic_private__.items() if v is not PydanticUndefined} + + if self.__pydantic_extra__ is None: + extra: dict[str, Any] | None = None + else: + extra = self.__pydantic_extra__.copy() + for k in list(self.__pydantic_extra__): + if k not in values: # k was in the exclude + extra.pop(k) + for k in list(values): + if k in self.__pydantic_extra__: # k must have come from extra + extra[k] = values.pop(k) + + # new `__pydantic_fields_set__` can have unset optional fields with a set value in `update` kwarg + if update: + fields_set = self.__pydantic_fields_set__ | update.keys() + else: + fields_set = set(self.__pydantic_fields_set__) + + # removing excluded fields from `__pydantic_fields_set__` + if exclude: + fields_set -= set(exclude) + + return copy_internals._copy_and_set_values(self, values, fields_set, extra, private, deep=deep) + + @classmethod + @typing_extensions.deprecated('The `schema` method is deprecated; use `model_json_schema` instead.', category=None) + def schema( # noqa: D102 + cls, by_alias: bool = True, ref_template: str = DEFAULT_REF_TEMPLATE + ) -> Dict[str, Any]: # noqa UP006 + warnings.warn( + 'The `schema` method is deprecated; use `model_json_schema` instead.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + return cls.model_json_schema(by_alias=by_alias, ref_template=ref_template) + + @classmethod + @typing_extensions.deprecated( + 'The `schema_json` method is deprecated; use `model_json_schema` and json.dumps instead.', + category=None, + ) + def schema_json( # noqa: D102 + cls, *, by_alias: bool = True, ref_template: str = DEFAULT_REF_TEMPLATE, **dumps_kwargs: Any + ) -> str: # pragma: no cover + warnings.warn( + 'The `schema_json` method is deprecated; use `model_json_schema` and json.dumps instead.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + import json + + from .deprecated.json import pydantic_encoder + + return json.dumps( + cls.model_json_schema(by_alias=by_alias, ref_template=ref_template), + default=pydantic_encoder, + **dumps_kwargs, + ) + + @classmethod + @typing_extensions.deprecated('The `validate` method is deprecated; use `model_validate` instead.', category=None) + def validate(cls, value: Any) -> Self: # noqa: D102 + warnings.warn( + 'The `validate` method is deprecated; use `model_validate` instead.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + return cls.model_validate(value) + + @classmethod + @typing_extensions.deprecated( + 'The `update_forward_refs` method is deprecated; use `model_rebuild` instead.', + category=None, + ) + def update_forward_refs(cls, **localns: Any) -> None: # noqa: D102 + warnings.warn( + 'The `update_forward_refs` method is deprecated; use `model_rebuild` instead.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + if localns: # pragma: no cover + raise TypeError('`localns` arguments are not longer accepted.') + cls.model_rebuild(force=True) + + @typing_extensions.deprecated( + 'The private method `_iter` will be removed and should no longer be used.', category=None + ) + def _iter(self, *args: Any, **kwargs: Any) -> Any: + warnings.warn( + 'The private method `_iter` will be removed and should no longer be used.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + from .deprecated import copy_internals + + return copy_internals._iter(self, *args, **kwargs) + + @typing_extensions.deprecated( + 'The private method `_copy_and_set_values` will be removed and should no longer be used.', + category=None, + ) + def _copy_and_set_values(self, *args: Any, **kwargs: Any) -> Any: + warnings.warn( + 'The private method `_copy_and_set_values` will be removed and should no longer be used.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + from .deprecated import copy_internals + + return copy_internals._copy_and_set_values(self, *args, **kwargs) + + @classmethod + @typing_extensions.deprecated( + 'The private method `_get_value` will be removed and should no longer be used.', + category=None, + ) + def _get_value(cls, *args: Any, **kwargs: Any) -> Any: + warnings.warn( + 'The private method `_get_value` will be removed and should no longer be used.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + from .deprecated import copy_internals + + return copy_internals._get_value(cls, *args, **kwargs) + + @typing_extensions.deprecated( + 'The private method `_calculate_keys` will be removed and should no longer be used.', + category=None, + ) + def _calculate_keys(self, *args: Any, **kwargs: Any) -> Any: + warnings.warn( + 'The private method `_calculate_keys` will be removed and should no longer be used.', + category=PydanticDeprecatedSince20, + stacklevel=2, + ) + from .deprecated import copy_internals + + return copy_internals._calculate_keys(self, *args, **kwargs) + + +ModelT = TypeVar('ModelT', bound=BaseModel) + + +@overload +def create_model( + model_name: str, + /, + *, + __config__: ConfigDict | None = None, + __doc__: str | None = None, + __base__: None = None, + __module__: str = __name__, + __validators__: dict[str, Callable[..., Any]] | None = None, + __cls_kwargs__: dict[str, Any] | None = None, + __qualname__: str | None = None, + **field_definitions: Any | tuple[Any, Any], +) -> type[BaseModel]: ... + + +@overload +def create_model( + model_name: str, + /, + *, + __config__: ConfigDict | None = None, + __doc__: str | None = None, + __base__: type[ModelT] | tuple[type[ModelT], ...], + __module__: str = __name__, + __validators__: dict[str, Callable[..., Any]] | None = None, + __cls_kwargs__: dict[str, Any] | None = None, + __qualname__: str | None = None, + **field_definitions: Any | tuple[Any, Any], +) -> type[ModelT]: ... + + +def create_model( # noqa: C901 + model_name: str, + /, + *, + __config__: ConfigDict | None = None, + __doc__: str | None = None, + __base__: type[ModelT] | tuple[type[ModelT], ...] | None = None, + __module__: str | None = None, + __validators__: dict[str, Callable[..., Any]] | None = None, + __cls_kwargs__: dict[str, Any] | None = None, + __qualname__: str | None = None, + # TODO PEP 747: replace `Any` by the TypeForm: + **field_definitions: Any | tuple[Any, Any], +) -> type[ModelT]: + """!!! abstract "Usage Documentation" + [Dynamic Model Creation](../concepts/models.md#dynamic-model-creation) + + Dynamically creates and returns a new Pydantic model, in other words, `create_model` dynamically creates a + subclass of [`BaseModel`][pydantic.BaseModel]. + + !!! warning + This function may execute arbitrary code contained in field annotations, if string references need to be evaluated. + + See [Security implications of introspecting annotations](https://docs.python.org/3/library/annotationlib.html#annotationlib-security) for more information. + + Args: + model_name: The name of the newly created model. + __config__: The configuration of the new model. + __doc__: The docstring of the new model. + __base__: The base class or classes for the new model. + __module__: The name of the module that the model belongs to; + if `None`, the value is taken from `sys._getframe(1)` + __validators__: A dictionary of methods that validate fields. The keys are the names of the validation methods to + be added to the model, and the values are the validation methods themselves. You can read more about functional + validators [here](https://docs.pydantic.dev/2.9/concepts/validators/#field-validators). + __cls_kwargs__: A dictionary of keyword arguments for class creation, such as `metaclass`. + __qualname__: The qualified name of the newly created model. + **field_definitions: Field definitions of the new model. Either: + + - a single element, representing the type annotation of the field. + - a two-tuple, the first element being the type and the second element the assigned value + (either a default or the [`Field()`][pydantic.Field] function). + + Returns: + The new [model][pydantic.BaseModel]. + + Raises: + PydanticUserError: If `__base__` and `__config__` are both passed. + """ + if __base__ is None: + __base__ = (cast('type[ModelT]', BaseModel),) + elif not isinstance(__base__, tuple): + __base__ = (__base__,) + + __cls_kwargs__ = __cls_kwargs__ or {} + + fields: dict[str, Any] = {} + annotations: dict[str, Any] = {} + + for f_name, f_def in field_definitions.items(): + if isinstance(f_def, tuple): + if len(f_def) != 2: + raise PydanticUserError( + f'Field definition for {f_name!r} should a single element representing the type or a two-tuple, the first element ' + 'being the type and the second element the assigned value (either a default or the `Field()` function).', + code='create-model-field-definitions', + ) + + annotations[f_name] = f_def[0] + fields[f_name] = f_def[1] + else: + annotations[f_name] = f_def + + if __module__ is None: + f = sys._getframe(1) + __module__ = f.f_globals['__name__'] + + namespace: dict[str, Any] = {'__annotations__': annotations, '__module__': __module__} + if __doc__: + namespace['__doc__'] = __doc__ + if __qualname__ is not None: + namespace['__qualname__'] = __qualname__ + if __validators__: + namespace.update(__validators__) + namespace.update(fields) + if __config__: + namespace['model_config'] = __config__ + resolved_bases = types.resolve_bases(__base__) + meta, ns, kwds = types.prepare_class(model_name, resolved_bases, kwds=__cls_kwargs__) + if resolved_bases is not __base__: + ns['__orig_bases__'] = __base__ + namespace.update(ns) + + return meta( + model_name, + resolved_bases, + namespace, + __pydantic_reset_parent_namespace__=False, + _create_model_module=__module__, + **kwds, + ) + + +__getattr__ = getattr_migration(__name__) diff --git a/source/target-vendors/cp312/pydantic/mypy.py b/source/target-vendors/cp312/pydantic/mypy.py new file mode 100644 index 0000000..4c73b54 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/mypy.py @@ -0,0 +1,1414 @@ +"""This module includes classes and functions designed specifically for use with the mypy plugin.""" + +from __future__ import annotations + +import sys +from collections.abc import Iterator +from configparser import ConfigParser +from typing import Any, Callable + +# To avoid import error (https://github.com/python/mypy/issues/17726): +import mypy.types # noqa: F401 +from mypy.errorcodes import ErrorCode +from mypy.expandtype import expand_type, expand_type_by_instance +from mypy.nodes import ( + ARG_NAMED, + ARG_NAMED_OPT, + ARG_OPT, + ARG_POS, + ARG_STAR2, + INVARIANT, + MDEF, + Argument, + AssignmentStmt, + Block, + CallExpr, + ClassDef, + Context, + Decorator, + DictExpr, + EllipsisExpr, + Expression, + FuncDef, + IfStmt, + JsonDict, + MemberExpr, + NameExpr, + PassStmt, + PlaceholderNode, + RefExpr, + Statement, + StrExpr, + SymbolTableNode, + TempNode, + TypeAlias, + TypeInfo, + Var, +) +from mypy.options import Options +from mypy.plugin import ( + CheckerPluginInterface, + ClassDefContext, + DynamicClassDefContext, + MethodContext, + Plugin, + ReportConfigContext, + SemanticAnalyzerPluginInterface, +) +from mypy.plugins.common import ( + deserialize_and_fixup_type, +) +from mypy.semanal import set_callable_name +from mypy.server.trigger import make_wildcard_trigger +from mypy.state import state +from mypy.type_visitor import TypeTranslator +from mypy.typeops import map_type_from_supertype +from mypy.types import ( + AnyType, + CallableType, + Instance, + NoneType, + Type, + TypeOfAny, + TypeType, + TypeVarType, + UnionType, + get_proper_type, +) +from mypy.typevars import fill_typevars +from mypy.util import get_unique_redefinition_name +from mypy.version import __version__ as mypy_version + +from pydantic._internal import _fields +from pydantic.version import parse_mypy_version + +CONFIGFILE_KEY = 'pydantic-mypy' +METADATA_KEY = 'pydantic-mypy-metadata' +BASEMODEL_FULLNAME = 'pydantic.main.BaseModel' +CREATE_MODEL_FULLNAME = 'pydantic.main.create_model' +BASESETTINGS_FULLNAME = 'pydantic_settings.main.BaseSettings' +ROOT_MODEL_FULLNAME = 'pydantic.root_model.RootModel' +MODEL_METACLASS_FULLNAME = 'pydantic._internal._model_construction.ModelMetaclass' +FIELD_FULLNAME = 'pydantic.fields.Field' +DATACLASS_FULLNAME = 'pydantic.dataclasses.dataclass' +MODEL_VALIDATOR_FULLNAME = 'pydantic.functional_validators.model_validator' +DECORATOR_FULLNAMES = { + 'pydantic.functional_validators.field_validator', + 'pydantic.functional_validators.model_validator', + 'pydantic.functional_serializers.serializer', + 'pydantic.functional_serializers.model_serializer', + 'pydantic.deprecated.class_validators.validator', + 'pydantic.deprecated.class_validators.root_validator', +} +IMPLICIT_CLASSMETHOD_DECORATOR_FULLNAMES = DECORATOR_FULLNAMES - {'pydantic.functional_serializers.model_serializer'} + + +MYPY_VERSION_TUPLE = parse_mypy_version(mypy_version) +BUILTINS_NAME = 'builtins' + +# Increment version if plugin changes and mypy caches should be invalidated +__version__ = 2 + + +def plugin(version: str) -> type[Plugin]: + """`version` is the mypy version string. + + We might want to use this to print a warning if the mypy version being used is + newer, or especially older, than we expect (or need). + + Args: + version: The mypy version string. + + Return: + The Pydantic mypy plugin type. + """ + return PydanticPlugin + + +class PydanticPlugin(Plugin): + """The Pydantic mypy plugin.""" + + def __init__(self, options: Options) -> None: + self.plugin_config = PydanticPluginConfig(options) + self._plugin_data = self.plugin_config.to_data() + super().__init__(options) + + def get_base_class_hook(self, fullname: str) -> Callable[[ClassDefContext], None] | None: + """Update Pydantic model class.""" + sym = self.lookup_fully_qualified(fullname) + if sym and isinstance(sym.node, TypeInfo): # pragma: no branch + # No branching may occur if the mypy cache has not been cleared + if sym.node.has_base(BASEMODEL_FULLNAME): + return self._pydantic_model_class_maker_callback + return None + + def get_metaclass_hook(self, fullname: str) -> Callable[[ClassDefContext], None] | None: + """Update Pydantic `ModelMetaclass` definition.""" + if fullname == MODEL_METACLASS_FULLNAME: + return self._pydantic_model_metaclass_marker_callback + return None + + def get_method_hook(self, fullname: str) -> Callable[[MethodContext], Type] | None: + """Adjust return type of `from_orm` method call.""" + if fullname.endswith('.from_orm'): + return from_attributes_callback + return None + + def get_dynamic_class_hook(self, fullname: str) -> Callable[[DynamicClassDefContext], None] | None: + """Recognize `create_model()` calls as dynamic BaseModel subclasses.""" + if fullname == CREATE_MODEL_FULLNAME: + return self._pydantic_create_model_callback + return None + + def report_config_data(self, ctx: ReportConfigContext) -> dict[str, Any]: + """Return all plugin config data. + + Used by mypy to determine if cache needs to be discarded. + """ + return self._plugin_data + + def _pydantic_model_class_maker_callback(self, ctx: ClassDefContext) -> None: + transformer = PydanticModelTransformer(ctx.cls, ctx.reason, ctx.api, self.plugin_config) + transformer.transform() + + def _pydantic_model_metaclass_marker_callback(self, ctx: ClassDefContext) -> None: + """Reset dataclass_transform_spec attribute of ModelMetaclass. + + Let the plugin handle it. This behavior can be disabled + if 'debug_dataclass_transform' is set to True', for testing purposes. + """ + if self.plugin_config.debug_dataclass_transform: + return + info_metaclass = ctx.cls.info.declared_metaclass + assert info_metaclass, "callback not passed from 'get_metaclass_hook'" + if getattr(info_metaclass.type, 'dataclass_transform_spec', None): + info_metaclass.type.dataclass_transform_spec = None + + def _pydantic_create_model_callback(self, ctx: DynamicClassDefContext) -> None: + """Make variables assigned from `create_model()` usable as types by mypy.""" + # Determine the base class from __base__ argument if provided + base_fullname = BASEMODEL_FULLNAME + for arg_name, arg_expr in zip(ctx.call.arg_names, ctx.call.args): + if arg_name == '__base__' and isinstance(arg_expr, RefExpr) and arg_expr.node is not None: + if isinstance(arg_expr.node, TypeInfo): + base_fullname = arg_expr.node.fullname + elif isinstance(arg_expr.node, Var) and isinstance(arg_expr.node.type, Instance): + base_fullname = arg_expr.node.type.type.fullname + + base_sym = ctx.api.lookup_fully_qualified_or_none(base_fullname) + if base_sym is None or not isinstance(base_sym.node, TypeInfo): + # Fall back to BaseModel + base_sym = ctx.api.lookup_fully_qualified_or_none(BASEMODEL_FULLNAME) + if base_sym is None or not isinstance(base_sym.node, TypeInfo): + return + + base_info = base_sym.node + base_instance = fill_typevars(base_info) + assert isinstance(base_instance, Instance) + + info = ctx.api.basic_new_typeinfo(ctx.name, base_instance, ctx.call.line) + info.metaclass_type = base_info.metaclass_type + + ctx.api.add_symbol_table_node(ctx.name, SymbolTableNode(MDEF, info)) + + +class PydanticPluginConfig: + """A Pydantic mypy plugin config holder. + + Attributes: + init_forbid_extra: Whether to add a `**kwargs` at the end of the generated `__init__` signature. + init_typed: Whether to annotate fields in the generated `__init__`. + warn_required_dynamic_aliases: Whether to raise required dynamic aliases error. + debug_dataclass_transform: Whether to not reset `dataclass_transform_spec` attribute + of `ModelMetaclass` for testing purposes. + """ + + __slots__ = ( + 'init_forbid_extra', + 'init_typed', + 'warn_required_dynamic_aliases', + 'debug_dataclass_transform', + ) + init_forbid_extra: bool + init_typed: bool + warn_required_dynamic_aliases: bool + debug_dataclass_transform: bool # undocumented + + def __init__(self, options: Options) -> None: + if options.config_file is None: # pragma: no cover + return + + toml_config = parse_toml(options.config_file) + if toml_config is not None: + config = toml_config.get('tool', {}).get('pydantic-mypy', {}) + for key in self.__slots__: + setting = config.get(key, False) + if not isinstance(setting, bool): + raise ValueError(f'Configuration value must be a boolean for key: {key}') + setattr(self, key, setting) + else: + plugin_config = ConfigParser() + plugin_config.read(options.config_file) + for key in self.__slots__: + setting = plugin_config.getboolean(CONFIGFILE_KEY, key, fallback=False) + setattr(self, key, setting) + + def to_data(self) -> dict[str, Any]: + """Returns a dict of config names to their values.""" + return {key: getattr(self, key) for key in self.__slots__} + + +def from_attributes_callback(ctx: MethodContext) -> Type: + """Raise an error if from_attributes is not enabled.""" + model_type: Instance + ctx_type = ctx.type + if isinstance(ctx_type, TypeType): + ctx_type = ctx_type.item + if isinstance(ctx_type, CallableType) and isinstance(ctx_type.ret_type, Instance): + model_type = ctx_type.ret_type # called on the class + elif isinstance(ctx_type, Instance): + model_type = ctx_type # called on an instance (unusual, but still valid) + else: # pragma: no cover + detail = f'ctx.type: {ctx_type} (of type {ctx_type.__class__.__name__})' + error_unexpected_behavior(detail, ctx.api, ctx.context) + return ctx.default_return_type + pydantic_metadata = model_type.type.metadata.get(METADATA_KEY) + if pydantic_metadata is None: + return ctx.default_return_type + if not model_type.type.has_base(BASEMODEL_FULLNAME): + # not a Pydantic v2 model + return ctx.default_return_type + from_attributes = pydantic_metadata.get('config', {}).get('from_attributes') + if from_attributes is not True: + error_from_attributes(model_type.type.name, ctx.api, ctx.context) + return ctx.default_return_type + + +class PydanticModelField: + """Based on mypy.plugins.dataclasses.DataclassAttribute.""" + + def __init__( + self, + name: str, + alias: str | None, + is_frozen: bool, + has_dynamic_alias: bool, + has_default: bool, + strict: bool | None, + line: int, + column: int, + type: Type | None, + info: TypeInfo, + ): + self.name = name + self.alias = alias + self.is_frozen = is_frozen + self.has_dynamic_alias = has_dynamic_alias + self.has_default = has_default + self.strict = strict + self.line = line + self.column = column + self.type = type + self.info = info + + def to_argument( + self, + current_info: TypeInfo, + typed: bool, + model_strict: bool, + force_optional: bool, + use_alias: bool, + api: SemanticAnalyzerPluginInterface, + force_typevars_invariant: bool, + is_root_model_root: bool, + ) -> Argument: + """Based on mypy.plugins.dataclasses.DataclassAttribute.to_argument.""" + variable = self.to_var(current_info, api, use_alias, force_typevars_invariant) + + strict = model_strict if self.strict is None else self.strict + if typed or strict: + type_annotation = self.expand_type(current_info, api, include_root_type=True) + else: + type_annotation = AnyType(TypeOfAny.explicit) + + return Argument( + variable=variable, + type_annotation=type_annotation, + initializer=None, + kind=ARG_OPT + if is_root_model_root + else (ARG_NAMED_OPT if force_optional or self.has_default else ARG_NAMED), + ) + + def expand_type( + self, + current_info: TypeInfo, + api: SemanticAnalyzerPluginInterface, + force_typevars_invariant: bool = False, + include_root_type: bool = False, + ) -> Type | None: + """Based on mypy.plugins.dataclasses.DataclassAttribute.expand_type.""" + if force_typevars_invariant: + # In some cases, mypy will emit an error "Cannot use a covariant type variable as a parameter" + # To prevent that, we add an option to replace typevars with invariant ones while building certain + # method signatures (in particular, `__init__`). There may be a better way to do this, if this causes + # us problems in the future, we should look into why the dataclasses plugin doesn't have this issue. + if isinstance(self.type, TypeVarType): + modified_type = self.type.copy_modified() + modified_type.variance = INVARIANT + self.type = modified_type + + if self.type is not None and self.info.self_type is not None: + # In general, it is not safe to call `expand_type()` during semantic analysis, + # however this plugin is called very late, so all types should be fully ready. + # Also, it is tricky to avoid eager expansion of Self types here (e.g. because + # we serialize attributes). + with state.strict_optional_set(api.options.strict_optional): + filled_with_typevars = fill_typevars(current_info) + # Cannot be TupleType as current_info represents a Pydantic model: + assert isinstance(filled_with_typevars, Instance) + if force_typevars_invariant: + for arg in filled_with_typevars.args: + if isinstance(arg, TypeVarType): + arg.variance = INVARIANT + + expanded_type = expand_type(self.type, {self.info.self_type.id: filled_with_typevars}) + if include_root_type and isinstance(expanded_type, Instance) and is_root_model(expanded_type.type): + # When a root model is used as a field, Pydantic allows both an instance of the root model + # as well as instances of the `root` field type: + root_type = expanded_type.type['root'].type + if root_type is None: + # Happens if the hint for 'root' has unsolved forward references + return expanded_type + expanded_root_type = expand_type_by_instance(root_type, expanded_type) + expanded_type = UnionType([expanded_type, expanded_root_type]) + return expanded_type + return self.type + + def to_var( + self, + current_info: TypeInfo, + api: SemanticAnalyzerPluginInterface, + use_alias: bool, + force_typevars_invariant: bool = False, + ) -> Var: + """Based on mypy.plugins.dataclasses.DataclassAttribute.to_var.""" + if use_alias and self.alias is not None: + name = self.alias + else: + name = self.name + + return Var(name, self.expand_type(current_info, api, force_typevars_invariant)) + + def serialize(self) -> JsonDict: + """Based on mypy.plugins.dataclasses.DataclassAttribute.serialize.""" + assert self.type + return { + 'name': self.name, + 'alias': self.alias, + 'is_frozen': self.is_frozen, + 'has_dynamic_alias': self.has_dynamic_alias, + 'has_default': self.has_default, + 'strict': self.strict, + 'line': self.line, + 'column': self.column, + 'type': self.type.serialize(), + } + + @classmethod + def deserialize(cls, info: TypeInfo, data: JsonDict, api: SemanticAnalyzerPluginInterface) -> PydanticModelField: + """Based on mypy.plugins.dataclasses.DataclassAttribute.deserialize.""" + data = data.copy() + typ = deserialize_and_fixup_type(data.pop('type'), api) + return cls(type=typ, info=info, **data) + + def expand_typevar_from_subtype(self, sub_type: TypeInfo, api: SemanticAnalyzerPluginInterface) -> None: + """Expands type vars in the context of a subtype when an attribute is inherited + from a generic super type. + """ + if self.type is not None: + with state.strict_optional_set(api.options.strict_optional): + self.type = map_type_from_supertype(self.type, sub_type, self.info) + + +class PydanticModelClassVar: + """Based on mypy.plugins.dataclasses.DataclassAttribute. + + ClassVars are ignored by subclasses. + + Attributes: + name: the ClassVar name + """ + + def __init__(self, name): + self.name = name + + @classmethod + def deserialize(cls, data: JsonDict) -> PydanticModelClassVar: + """Based on mypy.plugins.dataclasses.DataclassAttribute.deserialize.""" + data = data.copy() + return cls(**data) + + def serialize(self) -> JsonDict: + """Based on mypy.plugins.dataclasses.DataclassAttribute.serialize.""" + return { + 'name': self.name, + } + + +class PydanticModelTransformer: + """Transform the BaseModel subclass according to the plugin settings. + + Attributes: + tracked_config_fields: A set of field configs that the plugin has to track their value. + """ + + tracked_config_fields: set[str] = { + 'extra', + 'frozen', + 'from_attributes', + 'populate_by_name', + 'validate_by_alias', + 'validate_by_name', + 'alias_generator', + 'strict', + } + + def __init__( + self, + cls: ClassDef, + reason: Expression | Statement, + api: SemanticAnalyzerPluginInterface, + plugin_config: PydanticPluginConfig, + ) -> None: + self._cls = cls + self._reason = reason + self._api = api + + self.plugin_config = plugin_config + + def transform(self) -> bool: + """Configures the BaseModel subclass according to the plugin settings. + + In particular: + + * determines the model config and fields, + * adds a fields-aware signature for the initializer and construct methods + * freezes the class if frozen = True + * stores the fields, config, and if the class is settings in the mypy metadata for access by subclasses + """ + info = self._cls.info + is_a_root_model = is_root_model(info) + config = self.collect_config() + fields, class_vars = self.collect_fields_and_class_vars(config, is_a_root_model) + if fields is None or class_vars is None: + # Some definitions are not ready. We need another pass. + return False + for field in fields: + if field.type is None: + return False + + is_settings = info.has_base(BASESETTINGS_FULLNAME) + self.add_initializer(fields, config, is_settings, is_a_root_model) + self.add_model_construct_method(fields, config, is_settings, is_a_root_model) + self.set_frozen(fields, self._api, frozen=config.frozen is True) + + self.adjust_decorator_signatures() + + info.metadata[METADATA_KEY] = { + 'fields': {field.name: field.serialize() for field in fields}, + 'class_vars': {class_var.name: class_var.serialize() for class_var in class_vars}, + 'config': config.get_values_dict(), + } + + return True + + def adjust_decorator_signatures(self) -> None: + """When we decorate a function `f` with `pydantic.validator(...)`, `pydantic.field_validator` + or `pydantic.serializer(...)`, mypy sees `f` as a regular method taking a `self` instance, + even though pydantic internally wraps `f` with `classmethod` if necessary. + + Teach mypy this by marking any function whose outermost decorator is a `validator()`, + `field_validator()` or `serializer()` call as a `classmethod`. + """ + for sym in self._cls.info.names.values(): + if isinstance(sym.node, Decorator): + first_dec = sym.node.original_decorators[0] + if ( + isinstance(first_dec, CallExpr) + and isinstance(first_dec.callee, NameExpr) + and first_dec.callee.fullname in IMPLICIT_CLASSMETHOD_DECORATOR_FULLNAMES + # @model_validator(mode="after") is an exception, it expects a regular method + and not ( + first_dec.callee.fullname == MODEL_VALIDATOR_FULLNAME + and any( + first_dec.arg_names[i] == 'mode' and isinstance(arg, StrExpr) and arg.value == 'after' + for i, arg in enumerate(first_dec.args) + ) + ) + ): + # TODO: Only do this if the first argument of the decorated function is `cls` + sym.node.func.is_class = True + + def collect_config(self) -> ModelConfigData: # noqa: C901 (ignore complexity) + """Collects the values of the config attributes that are used by the plugin, accounting for parent classes.""" + cls = self._cls + config = ModelConfigData() + + has_config_kwargs = False + has_config_from_namespace = False + + # Handle `class MyModel(BaseModel, =, ...):` + for name, expr in cls.keywords.items(): + config_data = self.get_config_update(name, expr) + if config_data: + has_config_kwargs = True + config.update(config_data) + + # Handle `model_config` + stmt: Statement | None = None + for stmt in cls.defs.body: + if not isinstance(stmt, (AssignmentStmt, ClassDef)): + continue + + if isinstance(stmt, AssignmentStmt): + lhs = stmt.lvalues[0] + if not isinstance(lhs, NameExpr) or lhs.name != 'model_config': + continue + + if isinstance(stmt.rvalue, CallExpr): # calls to `dict` or `ConfigDict` + for arg_name, arg in zip(stmt.rvalue.arg_names, stmt.rvalue.args): + if arg_name is None: + continue + config.update(self.get_config_update(arg_name, arg, lax_extra=True)) + elif isinstance(stmt.rvalue, DictExpr): # dict literals + for key_expr, value_expr in stmt.rvalue.items: + if not isinstance(key_expr, StrExpr): + continue + config.update(self.get_config_update(key_expr.value, value_expr)) + + elif isinstance(stmt, ClassDef): + if stmt.name != 'Config': # 'deprecated' Config-class + continue + for substmt in stmt.defs.body: + if not isinstance(substmt, AssignmentStmt): + continue + lhs = substmt.lvalues[0] + if not isinstance(lhs, NameExpr): + continue + config.update(self.get_config_update(lhs.name, substmt.rvalue)) + + if has_config_kwargs: + self._api.fail( + 'Specifying config in two places is ambiguous, use either Config attribute or class kwargs', + cls, + ) + break + + has_config_from_namespace = True + + if has_config_kwargs or has_config_from_namespace: + if ( + stmt + and config.has_alias_generator + and not (config.validate_by_name or config.populate_by_name) + and self.plugin_config.warn_required_dynamic_aliases + ): + error_required_dynamic_aliases(self._api, stmt) + + for info in cls.info.mro[1:]: # 0 is the current class + if METADATA_KEY not in info.metadata: + continue + + # Each class depends on the set of fields in its ancestors + self._api.add_plugin_dependency(make_wildcard_trigger(info.fullname)) + for name, value in info.metadata[METADATA_KEY]['config'].items(): + config.setdefault(name, value) + return config + + def collect_fields_and_class_vars( + self, model_config: ModelConfigData, is_root_model: bool + ) -> tuple[list[PydanticModelField] | None, list[PydanticModelClassVar] | None]: + """Collects the fields for the model, accounting for parent classes.""" + cls = self._cls + + # First, collect fields and ClassVars belonging to any class in the MRO, ignoring duplicates. + # + # We iterate through the MRO in reverse because attrs defined in the parent must appear + # earlier in the attributes list than attrs defined in the child. See: + # https://docs.python.org/3/library/dataclasses.html#inheritance + # + # However, we also want fields defined in the subtype to override ones defined + # in the parent. We can implement this via a dict without disrupting the attr order + # because dicts preserve insertion order in Python 3.7+. + found_fields: dict[str, PydanticModelField] = {} + found_class_vars: dict[str, PydanticModelClassVar] = {} + for info in reversed(cls.info.mro[1:-1]): # 0 is the current class, -2 is BaseModel, -1 is object + # if BASEMODEL_METADATA_TAG_KEY in info.metadata and BASEMODEL_METADATA_KEY not in info.metadata: + # # We haven't processed the base class yet. Need another pass. + # return None, None + if METADATA_KEY not in info.metadata: + continue + + # Each class depends on the set of attributes in its dataclass ancestors. + self._api.add_plugin_dependency(make_wildcard_trigger(info.fullname)) + + for name, data in info.metadata[METADATA_KEY]['fields'].items(): + field = PydanticModelField.deserialize(info, data, self._api) + # (The following comment comes directly from the dataclasses plugin) + # TODO: We shouldn't be performing type operations during the main + # semantic analysis pass, since some TypeInfo attributes might + # still be in flux. This should be performed in a later phase. + field.expand_typevar_from_subtype(cls.info, self._api) + found_fields[name] = field + + sym_node = cls.info.names.get(name) + if sym_node and sym_node.node and not isinstance(sym_node.node, (Var, PlaceholderNode)): + self._api.fail( + 'BaseModel field may only be overridden by another field', + sym_node.node, + ) + # Collect ClassVars + for name, data in info.metadata[METADATA_KEY]['class_vars'].items(): + found_class_vars[name] = PydanticModelClassVar.deserialize(data) + + # Second, collect fields and ClassVars belonging to the current class. + current_field_names: set[str] = set() + current_class_vars_names: set[str] = set() + for stmt in self._get_assignment_statements_from_block(cls.defs): + maybe_field = self.collect_field_or_class_var_from_stmt(stmt, model_config, found_class_vars) + if maybe_field is None: + continue + + lhs = stmt.lvalues[0] + assert isinstance(lhs, NameExpr) # collect_field_or_class_var_from_stmt guarantees this + if isinstance(maybe_field, PydanticModelField): + if is_root_model and lhs.name != 'root': + error_extra_fields_on_root_model(self._api, stmt) + else: + current_field_names.add(lhs.name) + found_fields[lhs.name] = maybe_field + elif isinstance(maybe_field, PydanticModelClassVar): + current_class_vars_names.add(lhs.name) + found_class_vars[lhs.name] = maybe_field + + return list(found_fields.values()), list(found_class_vars.values()) + + def _get_assignment_statements_from_if_statement(self, stmt: IfStmt) -> Iterator[AssignmentStmt]: + for body in stmt.body: + if not body.is_unreachable: + yield from self._get_assignment_statements_from_block(body) + if stmt.else_body is not None and not stmt.else_body.is_unreachable: + yield from self._get_assignment_statements_from_block(stmt.else_body) + + def _get_assignment_statements_from_block(self, block: Block) -> Iterator[AssignmentStmt]: + for stmt in block.body: + if isinstance(stmt, AssignmentStmt): + yield stmt + elif isinstance(stmt, IfStmt): + yield from self._get_assignment_statements_from_if_statement(stmt) + + def collect_field_or_class_var_from_stmt( # noqa C901 + self, stmt: AssignmentStmt, model_config: ModelConfigData, class_vars: dict[str, PydanticModelClassVar] + ) -> PydanticModelField | PydanticModelClassVar | None: + """Get pydantic model field from statement. + + Args: + stmt: The statement. + model_config: Configuration settings for the model. + class_vars: ClassVars already known to be defined on the model. + + Returns: + A pydantic model field if it could find the field in statement. Otherwise, `None`. + """ + cls = self._cls + + lhs = stmt.lvalues[0] + if not isinstance(lhs, NameExpr) or not _fields.is_valid_field_name(lhs.name) or lhs.name == 'model_config': + return None + + if not stmt.new_syntax: + if ( + isinstance(stmt.rvalue, CallExpr) + and isinstance(stmt.rvalue.callee, CallExpr) + and isinstance(stmt.rvalue.callee.callee, NameExpr) + and stmt.rvalue.callee.callee.fullname in DECORATOR_FULLNAMES + ): + # This is a (possibly-reused) validator or serializer, not a field + # In particular, it looks something like: my_validator = validator('my_field')(f) + # Eventually, we may want to attempt to respect model_config['ignored_types'] + return None + + if lhs.name in class_vars: + # Class vars are not fields and are not required to be annotated + return None + + # The assignment does not have an annotation, and it's not anything else we recognize + error_untyped_fields(self._api, stmt) + return None + + lhs = stmt.lvalues[0] + if not isinstance(lhs, NameExpr): + return None + + if not _fields.is_valid_field_name(lhs.name) or lhs.name == 'model_config': + return None + + sym = cls.info.names.get(lhs.name) + if sym is None: # pragma: no cover + # This is likely due to a star import (see the dataclasses plugin for a more detailed explanation) + # This is the same logic used in the dataclasses plugin + return None + + node = sym.node + if isinstance(node, PlaceholderNode): # pragma: no cover + # See the PlaceholderNode docstring for more detail about how this can occur + # Basically, it is an edge case when dealing with complex import logic + + # The dataclasses plugin now asserts this cannot happen, but I'd rather not error if it does.. + return None + + if isinstance(node, TypeAlias): + self._api.fail( + 'Type aliases inside BaseModel definitions are not supported at runtime', + node, + ) + # Skip processing this node. This doesn't match the runtime behaviour, + # but the only alternative would be to modify the SymbolTable, + # and it's a little hairy to do that in a plugin. + return None + + if not isinstance(node, Var): # pragma: no cover + # Don't know if this edge case still happens with the `is_valid_field` check above + # but better safe than sorry + + # The dataclasses plugin now asserts this cannot happen, but I'd rather not error if it does.. + return None + + # x: ClassVar[int] is not a field + if node.is_classvar: + return PydanticModelClassVar(lhs.name) + + # x: InitVar[int] is not supported in BaseModel + node_type = get_proper_type(node.type) + if isinstance(node_type, Instance) and node_type.type.fullname == 'dataclasses.InitVar': + self._api.fail( + 'InitVar is not supported in BaseModel', + node, + ) + + has_default = self.get_has_default(stmt) + strict = self.get_strict(stmt) + + if sym.type is None and node.is_final and node.is_inferred: + # This follows the logic from the dataclasses plugin. The following comment is taken verbatim: + # + # This is a special case, assignment like x: Final = 42 is classified + # annotated above, but mypy strips the `Final` turning it into x = 42. + # We do not support inferred types in dataclasses, so we can try inferring + # type for simple literals, and otherwise require an explicit type + # argument for Final[...]. + typ = self._api.analyze_simple_literal_type(stmt.rvalue, is_final=True) + if typ: + node.type = typ + else: + self._api.fail( + 'Need type argument for Final[...] with non-literal default in BaseModel', + stmt, + ) + node.type = AnyType(TypeOfAny.from_error) + + if node.is_final and has_default: + # TODO this path should be removed (see https://github.com/pydantic/pydantic/issues/11119) + return PydanticModelClassVar(lhs.name) + + alias, has_dynamic_alias = self.get_alias_info(stmt) + if ( + has_dynamic_alias + and not (model_config.validate_by_name or model_config.populate_by_name) + and self.plugin_config.warn_required_dynamic_aliases + ): + error_required_dynamic_aliases(self._api, stmt) + is_frozen = self.is_field_frozen(stmt) + + init_type = self._infer_dataclass_attr_init_type(sym, lhs.name, stmt) + return PydanticModelField( + name=lhs.name, + has_dynamic_alias=has_dynamic_alias, + has_default=has_default, + strict=strict, + alias=alias, + is_frozen=is_frozen, + line=stmt.line, + column=stmt.column, + type=init_type, + info=cls.info, + ) + + def _infer_dataclass_attr_init_type(self, sym: SymbolTableNode, name: str, context: Context) -> Type | None: + """Infer __init__ argument type for an attribute. + + In particular, possibly use the signature of __set__. + """ + default = sym.type + if sym.implicit: + return default + t = get_proper_type(sym.type) + + # Perform a simple-minded inference from the signature of __set__, if present. + # We can't use mypy.checkmember here, since this plugin runs before type checking. + # We only support some basic scanerios here, which is hopefully sufficient for + # the vast majority of use cases. + if not isinstance(t, Instance): + return default + setter = t.type.get('__set__') + if setter: + if isinstance(setter.node, FuncDef): + super_info = t.type.get_containing_type_info('__set__') + assert super_info + if setter.type: + setter_type = get_proper_type(map_type_from_supertype(setter.type, t.type, super_info)) + else: + return AnyType(TypeOfAny.unannotated) + if isinstance(setter_type, CallableType) and setter_type.arg_kinds == [ + ARG_POS, + ARG_POS, + ARG_POS, + ]: + return expand_type_by_instance(setter_type.arg_types[2], t) + else: + self._api.fail(f'Unsupported signature for "__set__" in "{t.type.name}"', context) + else: + self._api.fail(f'Unsupported "__set__" in "{t.type.name}"', context) + + return default + + def add_initializer( + self, fields: list[PydanticModelField], config: ModelConfigData, is_settings: bool, is_root_model: bool + ) -> None: + """Adds a fields-aware `__init__` method to the class. + + The added `__init__` will be annotated with types vs. all `Any` depending on the plugin settings. + """ + if '__init__' in self._cls.info.names and not self._cls.info.names['__init__'].plugin_generated: + return # Don't generate an __init__ if one already exists + + typed = self.plugin_config.init_typed + model_strict = bool(config.strict) + use_alias = not (config.validate_by_name or config.populate_by_name) and config.validate_by_alias is not False + requires_dynamic_aliases = bool(config.has_alias_generator and not config.validate_by_name) + args = self.get_field_arguments( + fields, + typed=typed, + model_strict=model_strict, + requires_dynamic_aliases=requires_dynamic_aliases, + use_alias=use_alias, + is_settings=is_settings, + is_root_model=is_root_model, + force_typevars_invariant=True, + ) + + if is_settings: + base_settings_node = self._api.lookup_fully_qualified(BASESETTINGS_FULLNAME).node + assert isinstance(base_settings_node, TypeInfo) + if '__init__' in base_settings_node.names: + base_settings_init_node = base_settings_node.names['__init__'].node + assert isinstance(base_settings_init_node, FuncDef) + if base_settings_init_node is not None and base_settings_init_node.type is not None: + func_type = base_settings_init_node.type + assert isinstance(func_type, CallableType) + for arg_idx, arg_name in enumerate(func_type.arg_names): + if arg_name is None or arg_name.startswith('__') or not arg_name.startswith('_'): + continue + analyzed_variable_type = self._api.anal_type(func_type.arg_types[arg_idx]) + if analyzed_variable_type is not None and arg_name in ( + '_cli_settings_source', + '_build_sources', + ): + # These arg names are annotated with types explicitly parameterized with `Any`, and as such + # the Any causes issues with --disallow-any-explicit. As a workaround, change + # the Any type (as if the generic type was left unparameterized): + analyzed_variable_type = analyzed_variable_type.accept( + ChangeExplicitTypeOfAny(TypeOfAny.from_omitted_generics) + ) + variable = Var(arg_name, analyzed_variable_type) + args.append(Argument(variable, analyzed_variable_type, None, ARG_OPT)) + + if not self.should_init_forbid_extra(fields, config): + var = Var('kwargs') + args.append(Argument(var, AnyType(TypeOfAny.explicit), None, ARG_STAR2)) + + add_method(self._api, self._cls, '__init__', args=args, return_type=NoneType()) + + def add_model_construct_method( + self, + fields: list[PydanticModelField], + config: ModelConfigData, + is_settings: bool, + is_root_model: bool, + ) -> None: + """Adds a fully typed `model_construct` classmethod to the class. + + Similar to the fields-aware __init__ method, but always uses the field names (not aliases), + and does not treat settings fields as optional. + """ + set_str = self._api.named_type(f'{BUILTINS_NAME}.set', [self._api.named_type(f'{BUILTINS_NAME}.str')]) + optional_set_str = UnionType([set_str, NoneType()]) + fields_set_argument = Argument(Var('_fields_set', optional_set_str), optional_set_str, None, ARG_OPT) + with state.strict_optional_set(self._api.options.strict_optional): + args = self.get_field_arguments( + fields, + typed=True, + model_strict=bool(config.strict), + requires_dynamic_aliases=False, + use_alias=False, + is_settings=is_settings, + is_root_model=is_root_model, + ) + if not self.should_init_forbid_extra(fields, config): + var = Var('kwargs') + args.append(Argument(var, AnyType(TypeOfAny.explicit), None, ARG_STAR2)) + + args = args + [fields_set_argument] if is_root_model else [fields_set_argument] + args + + add_method( + self._api, + self._cls, + 'model_construct', + args=args, + return_type=fill_typevars(self._cls.info), + is_classmethod=True, + ) + + def set_frozen(self, fields: list[PydanticModelField], api: SemanticAnalyzerPluginInterface, frozen: bool) -> None: + """Marks all fields as properties so that attempts to set them trigger mypy errors. + + This is the same approach used by the attrs and dataclasses plugins. + """ + info = self._cls.info + for field in fields: + sym_node = info.names.get(field.name) + if sym_node is not None: + var = sym_node.node + if isinstance(var, Var): + var.is_property = frozen or field.is_frozen + elif isinstance(var, PlaceholderNode) and not self._api.final_iteration: + # See https://github.com/pydantic/pydantic/issues/5191 to hit this branch for test coverage + self._api.defer() + # `var` can also be a FuncDef or Decorator node (e.g. when overriding a field with a function or property). + # In that case, we don't want to do anything. Mypy will already raise an error that a field was not properly + # overridden. + else: + var = field.to_var(info, api, use_alias=False) + var.info = info + var.is_property = frozen + var._fullname = info.fullname + '.' + var.name + info.names[var.name] = SymbolTableNode(MDEF, var) + + def get_config_update(self, name: str, arg: Expression, lax_extra: bool = False) -> ModelConfigData | None: + """Determines the config update due to a single kwarg in the ConfigDict definition. + + Warns if a tracked config attribute is set to a value the plugin doesn't know how to interpret (e.g., an int) + """ + if name not in self.tracked_config_fields: + return None + if name == 'extra': + if isinstance(arg, StrExpr): + forbid_extra = arg.value == 'forbid' + elif isinstance(arg, MemberExpr): + forbid_extra = arg.name == 'forbid' + else: + if not lax_extra: + # Only emit an error for other types of `arg` (e.g., `NameExpr`, `ConditionalExpr`, etc.) when + # reading from a config class, etc. If a ConfigDict is used, then we don't want to emit an error + # because you'll get type checking from the ConfigDict itself. + # + # It would be nice if we could introspect the types better otherwise, but I don't know what the API + # is to evaluate an expr into its type and then check if that type is compatible with the expected + # type. Note that you can still get proper type checking via: `model_config = ConfigDict(...)`, just + # if you don't use an explicit string, the plugin won't be able to infer whether extra is forbidden. + error_invalid_config_value(name, self._api, arg) + return None + return ModelConfigData(forbid_extra=forbid_extra) + if name == 'alias_generator': + has_alias_generator = True + if isinstance(arg, NameExpr) and arg.fullname == 'builtins.None': + has_alias_generator = False + return ModelConfigData(has_alias_generator=has_alias_generator) + if isinstance(arg, NameExpr) and arg.fullname in ('builtins.True', 'builtins.False'): + return ModelConfigData(**{name: arg.fullname == 'builtins.True'}) + error_invalid_config_value(name, self._api, arg) + return None + + @staticmethod + def get_has_default(stmt: AssignmentStmt) -> bool: + """Returns a boolean indicating whether the field defined in `stmt` is a required field.""" + expr = stmt.rvalue + if isinstance(expr, TempNode): + # TempNode means annotation-only, so has no default + return False + if isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr) and expr.callee.fullname == FIELD_FULLNAME: + # The "default value" is a call to `Field`; at this point, the field has a default if and only if: + # * there is a positional argument that is not `...` + # * there is a keyword argument named "default" that is not `...` + # * there is a "default_factory" that is not `None` + for arg, name in zip(expr.args, expr.arg_names): + # If name is None, then this arg is the default because it is the only positional argument. + if name is None or name == 'default': + return arg.__class__ is not EllipsisExpr + if name == 'default_factory': + return not (isinstance(arg, NameExpr) and arg.fullname == 'builtins.None') + return False + # Has no default if the "default value" is Ellipsis (i.e., `field_name: Annotation = ...`) + return not isinstance(expr, EllipsisExpr) + + @staticmethod + def get_strict(stmt: AssignmentStmt) -> bool | None: + """Returns a the `strict` value of a field if defined, otherwise `None`.""" + expr = stmt.rvalue + if isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr) and expr.callee.fullname == FIELD_FULLNAME: + for arg, name in zip(expr.args, expr.arg_names): + if name != 'strict': + continue + if isinstance(arg, NameExpr): + if arg.fullname == 'builtins.True': + return True + elif arg.fullname == 'builtins.False': + return False + return None + return None + + @staticmethod + def get_alias_info(stmt: AssignmentStmt) -> tuple[str | None, bool]: + """Returns a pair (alias, has_dynamic_alias), extracted from the declaration of the field defined in `stmt`. + + `has_dynamic_alias` is True if and only if an alias is provided, but not as a string literal. + If `has_dynamic_alias` is True, `alias` will be None. + """ + expr = stmt.rvalue + if isinstance(expr, TempNode): + # TempNode means annotation-only + return None, False + + if not ( + isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr) and expr.callee.fullname == FIELD_FULLNAME + ): + # Assigned value is not a call to pydantic.fields.Field + return None, False + + if 'validation_alias' in expr.arg_names: + arg = expr.args[expr.arg_names.index('validation_alias')] + elif 'alias' in expr.arg_names: + arg = expr.args[expr.arg_names.index('alias')] + else: + return None, False + + if isinstance(arg, StrExpr): + return arg.value, False + else: + return None, True + + @staticmethod + def is_field_frozen(stmt: AssignmentStmt) -> bool: + """Returns whether the field is frozen, extracted from the declaration of the field defined in `stmt`. + + Note that this is only whether the field was declared to be frozen in a ` = Field(frozen=True)` + sense; this does not determine whether the field is frozen because the entire model is frozen; that is + handled separately. + """ + expr = stmt.rvalue + if isinstance(expr, TempNode): + # TempNode means annotation-only + return False + + if not ( + isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr) and expr.callee.fullname == FIELD_FULLNAME + ): + # Assigned value is not a call to pydantic.fields.Field + return False + + for i, arg_name in enumerate(expr.arg_names): + if arg_name == 'frozen': + arg = expr.args[i] + return isinstance(arg, NameExpr) and arg.fullname == 'builtins.True' + return False + + def get_field_arguments( + self, + fields: list[PydanticModelField], + typed: bool, + model_strict: bool, + use_alias: bool, + requires_dynamic_aliases: bool, + is_settings: bool, + is_root_model: bool, + force_typevars_invariant: bool = False, + ) -> list[Argument]: + """Helper function used during the construction of the `__init__` and `model_construct` method signatures. + + Returns a list of mypy Argument instances for use in the generated signatures. + """ + info = self._cls.info + arguments = [ + field.to_argument( + info, + typed=typed, + model_strict=model_strict, + force_optional=requires_dynamic_aliases or is_settings, + use_alias=use_alias, + api=self._api, + force_typevars_invariant=force_typevars_invariant, + is_root_model_root=is_root_model and field.name == 'root', + ) + for field in fields + if not (use_alias and field.has_dynamic_alias) + ] + return arguments + + def should_init_forbid_extra(self, fields: list[PydanticModelField], config: ModelConfigData) -> bool: + """Indicates whether the generated `__init__` should get a `**kwargs` at the end of its signature. + + We disallow arbitrary kwargs if the extra config setting is "forbid", or if the plugin config says to, + *unless* a required dynamic alias is present (since then we can't determine a valid signature). + """ + if not (config.validate_by_name or config.populate_by_name): + if self.is_dynamic_alias_present(fields, bool(config.has_alias_generator)): + return False + if config.forbid_extra: + return True + return self.plugin_config.init_forbid_extra + + @staticmethod + def is_dynamic_alias_present(fields: list[PydanticModelField], has_alias_generator: bool) -> bool: + """Returns whether any fields on the model have a "dynamic alias", i.e., an alias that cannot be + determined during static analysis. + """ + for field in fields: + if field.has_dynamic_alias: + return True + if has_alias_generator: + for field in fields: + if field.alias is None: + return True + return False + + +class ChangeExplicitTypeOfAny(TypeTranslator): + """A type translator used to change type of Any's, if explicit.""" + + def __init__(self, type_of_any: int) -> None: + self._type_of_any = type_of_any + super().__init__() + + def visit_any(self, t: AnyType) -> Type: # noqa: D102 + if t.type_of_any == TypeOfAny.explicit: + return t.copy_modified(type_of_any=self._type_of_any) + else: + return t + + +class ModelConfigData: + """Pydantic mypy plugin model config class.""" + + def __init__( + self, + forbid_extra: bool | None = None, + frozen: bool | None = None, + from_attributes: bool | None = None, + populate_by_name: bool | None = None, + validate_by_alias: bool | None = None, + validate_by_name: bool | None = None, + has_alias_generator: bool | None = None, + strict: bool | None = None, + ): + self.forbid_extra = forbid_extra + self.frozen = frozen + self.from_attributes = from_attributes + self.populate_by_name = populate_by_name + self.validate_by_alias = validate_by_alias + self.validate_by_name = validate_by_name + self.has_alias_generator = has_alias_generator + self.strict = strict + + def get_values_dict(self) -> dict[str, Any]: + """Returns a dict of Pydantic model config names to their values. + + It includes the config if config value is not `None`. + """ + return {k: v for k, v in self.__dict__.items() if v is not None} + + def update(self, config: ModelConfigData | None) -> None: + """Update Pydantic model config values.""" + if config is None: + return + for k, v in config.get_values_dict().items(): + setattr(self, k, v) + + def setdefault(self, key: str, value: Any) -> None: + """Set default value for Pydantic model config if config value is `None`.""" + if getattr(self, key) is None: + setattr(self, key, value) + + +def is_root_model(info: TypeInfo) -> bool: + """Return whether the type info is a root model subclass (or the `RootModel` class itself).""" + return info.has_base(ROOT_MODEL_FULLNAME) + + +ERROR_ORM = ErrorCode('pydantic-orm', 'Invalid from_attributes call', 'Pydantic') +ERROR_CONFIG = ErrorCode('pydantic-config', 'Invalid config value', 'Pydantic') +ERROR_ALIAS = ErrorCode('pydantic-alias', 'Dynamic alias disallowed', 'Pydantic') +ERROR_UNEXPECTED = ErrorCode('pydantic-unexpected', 'Unexpected behavior', 'Pydantic') +ERROR_UNTYPED = ErrorCode('pydantic-field', 'Untyped field disallowed', 'Pydantic') +ERROR_FIELD_DEFAULTS = ErrorCode('pydantic-field', 'Invalid Field defaults', 'Pydantic') +ERROR_EXTRA_FIELD_ROOT_MODEL = ErrorCode('pydantic-field', 'Extra field on RootModel subclass', 'Pydantic') + + +def error_from_attributes(model_name: str, api: CheckerPluginInterface, context: Context) -> None: + """Emits an error when the model does not have `from_attributes=True`.""" + api.fail(f'"{model_name}" does not have from_attributes=True', context, code=ERROR_ORM) + + +def error_invalid_config_value(name: str, api: SemanticAnalyzerPluginInterface, context: Context) -> None: + """Emits an error when the config value is invalid.""" + api.fail(f'Invalid value for "Config.{name}"', context, code=ERROR_CONFIG) + + +def error_required_dynamic_aliases(api: SemanticAnalyzerPluginInterface, context: Context) -> None: + """Emits required dynamic aliases error. + + This will be called when `warn_required_dynamic_aliases=True`. + """ + api.fail('Required dynamic aliases disallowed', context, code=ERROR_ALIAS) + + +def error_unexpected_behavior( + detail: str, api: CheckerPluginInterface | SemanticAnalyzerPluginInterface, context: Context +) -> None: # pragma: no cover + """Emits unexpected behavior error.""" + # Can't think of a good way to test this, but I confirmed it renders as desired by adding to a non-error path + link = 'https://github.com/pydantic/pydantic/issues/new/choose' + full_message = f'The pydantic mypy plugin ran into unexpected behavior: {detail}\n' + full_message += f'Please consider reporting this bug at {link} so we can try to fix it!' + api.fail(full_message, context, code=ERROR_UNEXPECTED) + + +def error_untyped_fields(api: SemanticAnalyzerPluginInterface, context: Context) -> None: + """Emits an error when there is an untyped field in the model.""" + api.fail('Untyped fields disallowed', context, code=ERROR_UNTYPED) + + +def error_extra_fields_on_root_model(api: CheckerPluginInterface, context: Context) -> None: + """Emits an error when there is more than just a root field defined for a subclass of RootModel.""" + api.fail('Only `root` is allowed as a field of a `RootModel`', context, code=ERROR_EXTRA_FIELD_ROOT_MODEL) + + +def add_method( + api: SemanticAnalyzerPluginInterface | CheckerPluginInterface, + cls: ClassDef, + name: str, + args: list[Argument], + return_type: Type, + self_type: Type | None = None, + tvar_def: TypeVarType | None = None, + is_classmethod: bool = False, +) -> None: + """Very closely related to `mypy.plugins.common.add_method_to_class`, with a few pydantic-specific changes.""" + info = cls.info + + # First remove any previously generated methods with the same name + # to avoid clashes and problems in the semantic analyzer. + if name in info.names: + sym = info.names[name] + if sym.plugin_generated and isinstance(sym.node, FuncDef): + cls.defs.body.remove(sym.node) # pragma: no cover + + if isinstance(api, SemanticAnalyzerPluginInterface): + function_type = api.named_type('builtins.function') + else: + function_type = api.named_generic_type('builtins.function', []) + + if is_classmethod: + self_type = self_type or TypeType(fill_typevars(info)) + first = [Argument(Var('_cls'), self_type, None, ARG_POS, True)] + else: + self_type = self_type or fill_typevars(info) + # `self` is positional *ONLY* here, but this can't be expressed + # fully in the mypy internal API. ARG_POS is the closest we can get. + # Using ARG_POS will, however, give mypy errors if a `self` field + # is present on a model: + # + # Name "self" already defined (possibly by an import) [no-redef] + # + # As a workaround, we give this argument a name that will + # never conflict. By its positional nature, this name will not + # be used or exposed to users. + first = [Argument(Var('__pydantic_self__'), self_type, None, ARG_POS)] + args = first + args + + arg_types, arg_names, arg_kinds = [], [], [] + for arg in args: + assert arg.type_annotation, 'All arguments must be fully typed.' + arg_types.append(arg.type_annotation) + arg_names.append(arg.variable.name) + arg_kinds.append(arg.kind) + + signature = CallableType( + arg_types, arg_kinds, arg_names, return_type, function_type, variables=[tvar_def] if tvar_def else None + ) + + func = FuncDef(name, args, Block([PassStmt()])) + func.info = info + func.type = set_callable_name(signature, func) + func.is_class = is_classmethod + func._fullname = info.fullname + '.' + name + func.line = info.line + + # NOTE: we would like the plugin generated node to dominate, but we still + # need to keep any existing definitions so they get semantically analyzed. + if name in info.names: + # Get a nice unique name instead. + r_name = get_unique_redefinition_name(name, info.names) + info.names[r_name] = info.names[name] + + # Add decorator for is_classmethod + # The dataclasses plugin claims this is unnecessary for classmethods, but not including it results in a + # signature incompatible with the superclass, which causes mypy errors to occur for every subclass of BaseModel. + if is_classmethod: + func.is_decorated = True + v = Var(name, func.type) + v.info = info + v._fullname = func._fullname + v.is_classmethod = True + dec = Decorator(func, [NameExpr('classmethod')], v) + dec.line = info.line + sym = SymbolTableNode(MDEF, dec) + else: + sym = SymbolTableNode(MDEF, func) + sym.plugin_generated = True + info.names[name] = sym + + info.defn.defs.body.append(func) + + +def parse_toml(config_file: str) -> dict[str, Any] | None: + """Returns a dict of config keys to values. + + It reads configs from toml file and returns `None` if the file is not a toml file. + """ + if not config_file.endswith('.toml'): + return None + + if sys.version_info >= (3, 11): + import tomllib as toml_ + else: + try: + import tomli as toml_ + except ImportError: # pragma: no cover + import warnings + + warnings.warn('No TOML parser installed, cannot read configuration from `pyproject.toml`.', stacklevel=2) + return None + + with open(config_file, 'rb') as rf: + return toml_.load(rf) diff --git a/source/target-vendors/cp312/pydantic/networks.py b/source/target-vendors/cp312/pydantic/networks.py new file mode 100644 index 0000000..4015bad --- /dev/null +++ b/source/target-vendors/cp312/pydantic/networks.py @@ -0,0 +1,1332 @@ +"""The networks module contains types for common network-related fields.""" + +from __future__ import annotations as _annotations + +import dataclasses as _dataclasses +import re +from dataclasses import fields +from functools import lru_cache +from importlib.metadata import version +from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network +from typing import TYPE_CHECKING, Annotated, Any, ClassVar + +from pydantic_core import ( + MultiHostHost, + PydanticCustomError, + PydanticSerializationUnexpectedValue, + SchemaSerializer, + core_schema, +) +from pydantic_core import MultiHostUrl as _CoreMultiHostUrl +from pydantic_core import Url as _CoreUrl +from typing_extensions import Self, TypeAlias + +from pydantic.errors import PydanticUserError + +from ._internal import _repr, _schema_generation_shared +from ._migration import getattr_migration +from .annotated_handlers import GetCoreSchemaHandler +from .json_schema import JsonSchemaValue +from .type_adapter import TypeAdapter + +if TYPE_CHECKING: + import email_validator + + NetworkType: TypeAlias = 'str | bytes | int | tuple[str | bytes | int, str | int]' + +else: + email_validator = None + + +__all__ = [ + 'AnyUrl', + 'AnyHttpUrl', + 'FileUrl', + 'FtpUrl', + 'HttpUrl', + 'WebsocketUrl', + 'AnyWebsocketUrl', + 'UrlConstraints', + 'EmailStr', + 'NameEmail', + 'IPvAnyAddress', + 'IPvAnyInterface', + 'IPvAnyNetwork', + 'PostgresDsn', + 'CockroachDsn', + 'AmqpDsn', + 'RedisDsn', + 'MongoDsn', + 'KafkaDsn', + 'NatsDsn', + 'validate_email', + 'MySQLDsn', + 'MariaDBDsn', + 'ClickHouseDsn', + 'SnowflakeDsn', +] + + +@_dataclasses.dataclass +class UrlConstraints: + """Url constraints. + + Attributes: + max_length: The maximum length of the url. Defaults to `None`. + allowed_schemes: The allowed schemes. Defaults to `None`. + host_required: Whether the host is required. Defaults to `None`. + default_host: The default host. Defaults to `None`. + default_port: The default port. Defaults to `None`. + default_path: The default path. Defaults to `None`. + preserve_empty_path: Whether to preserve empty URL paths. Defaults to `None`. + """ + + max_length: int | None = None + allowed_schemes: list[str] | None = None + host_required: bool | None = None + default_host: str | None = None + default_port: int | None = None + default_path: str | None = None + preserve_empty_path: bool | None = None + + def __hash__(self) -> int: + return hash( + ( + self.max_length, + tuple(self.allowed_schemes) if self.allowed_schemes is not None else None, + self.host_required, + self.default_host, + self.default_port, + self.default_path, + self.preserve_empty_path, + ) + ) + + @property + def defined_constraints(self) -> dict[str, Any]: + """Fetch a key / value mapping of constraints to values that are not None. Used for core schema updates.""" + return {field.name: value for field in fields(self) if (value := getattr(self, field.name)) is not None} + + def __get_pydantic_core_schema__(self, source: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + schema = handler(source) + + # for function-wrap schemas, url constraints is applied to the inner schema + # because when we generate schemas for urls, we wrap a core_schema.url_schema() with a function-wrap schema + # that helps with validation on initialization, see _BaseUrl and _BaseMultiHostUrl below. + schema_to_mutate = schema['schema'] if schema['type'] == 'function-wrap' else schema + if (annotated_type := schema_to_mutate['type']) not in ('url', 'multi-host-url'): + raise PydanticUserError( + f"'UrlConstraints' cannot annotate '{annotated_type}'.", code='invalid-annotated-type' + ) + for constraint_key, constraint_value in self.defined_constraints.items(): + schema_to_mutate[constraint_key] = constraint_value + return schema + + +class _BaseUrl: + _constraints: ClassVar[UrlConstraints] = UrlConstraints() + _url: _CoreUrl + + def __init__(self, url: str | _CoreUrl | _BaseUrl) -> None: + self._url = _build_type_adapter(self.__class__).validate_python(url)._url + + @property + def scheme(self) -> str: + """The scheme part of the URL. + + e.g. `https` in `https://user:pass@host:port/path?query#fragment` + """ + return self._url.scheme + + @property + def username(self) -> str | None: + """The username part of the URL, or `None`. + + e.g. `user` in `https://user:pass@host:port/path?query#fragment` + """ + return self._url.username + + @property + def password(self) -> str | None: + """The password part of the URL, or `None`. + + e.g. `pass` in `https://user:pass@host:port/path?query#fragment` + """ + return self._url.password + + @property + def host(self) -> str | None: + """The host part of the URL, or `None`. + + If the URL must be punycode encoded, this is the encoded host, e.g if the input URL is `https://£££.com`, + `host` will be `xn--9aaa.com` + """ + return self._url.host + + def unicode_host(self) -> str | None: + """The host part of the URL as a unicode string, or `None`. + + e.g. `host` in `https://user:pass@host:port/path?query#fragment` + + If the URL must be punycode encoded, this is the decoded host, e.g if the input URL is `https://£££.com`, + `unicode_host()` will be `£££.com` + """ + return self._url.unicode_host() + + @property + def port(self) -> int | None: + """The port part of the URL, or `None`. + + e.g. `port` in `https://user:pass@host:port/path?query#fragment` + """ + return self._url.port + + @property + def path(self) -> str | None: + """The path part of the URL, or `None`. + + e.g. `/path` in `https://user:pass@host:port/path?query#fragment` + """ + return self._url.path + + @property + def query(self) -> str | None: + """The query part of the URL, or `None`. + + e.g. `query` in `https://user:pass@host:port/path?query#fragment` + """ + return self._url.query + + def query_params(self) -> list[tuple[str, str]]: + """The query part of the URL as a list of key-value pairs. + + e.g. `[('foo', 'bar')]` in `https://user:pass@host:port/path?foo=bar#fragment` + """ + return self._url.query_params() + + @property + def fragment(self) -> str | None: + """The fragment part of the URL, or `None`. + + e.g. `fragment` in `https://user:pass@host:port/path?query#fragment` + """ + return self._url.fragment + + def unicode_string(self) -> str: + """The URL as a unicode string, unlike `__str__()` this will not punycode encode the host. + + If the URL must be punycode encoded, this is the decoded string, e.g if the input URL is `https://£££.com`, + `unicode_string()` will be `https://£££.com` + """ + return self._url.unicode_string() + + def encoded_string(self) -> str: + """The URL's encoded string representation via __str__(). + + This returns the punycode-encoded host version of the URL as a string. + """ + return str(self) + + def __str__(self) -> str: + """The URL as a string, this will punycode encode the host if required.""" + return str(self._url) + + def __repr__(self) -> str: + return f'{self.__class__.__name__}({str(self._url)!r})' + + def __deepcopy__(self, memo: dict) -> Self: + return self.__class__(self._url) + + def __eq__(self, other: Any) -> bool: + return self.__class__ is other.__class__ and self._url == other._url + + def __lt__(self, other: Any) -> bool: + return self.__class__ is other.__class__ and self._url < other._url + + def __gt__(self, other: Any) -> bool: + return self.__class__ is other.__class__ and self._url > other._url + + def __le__(self, other: Any) -> bool: + return self.__class__ is other.__class__ and self._url <= other._url + + def __ge__(self, other: Any) -> bool: + return self.__class__ is other.__class__ and self._url >= other._url + + def __hash__(self) -> int: + return hash(self._url) + + def __len__(self) -> int: + return len(str(self._url)) + + @classmethod + def build( + cls, + *, + scheme: str, + username: str | None = None, + password: str | None = None, + host: str, + port: int | None = None, + path: str | None = None, + query: str | None = None, + fragment: str | None = None, + ) -> Self: + """Build a new `Url` instance from its component parts. + + Args: + scheme: The scheme part of the URL. + username: The username part of the URL, or omit for no username. + password: The password part of the URL, or omit for no password. + host: The host part of the URL. + port: The port part of the URL, or omit for no port. + path: The path part of the URL, or omit for no path. + query: The query part of the URL, or omit for no query. + fragment: The fragment part of the URL, or omit for no fragment. + + Returns: + An instance of URL + """ + return cls( + _CoreUrl.build( + scheme=scheme, + username=username, + password=password, + host=host, + port=port, + path=path, + query=query, + fragment=fragment, + ) + ) + + @classmethod + def serialize_url(cls, url: Any, info: core_schema.SerializationInfo) -> str | Self: + if not isinstance(url, cls): + raise PydanticSerializationUnexpectedValue( + f"Expected `{cls}` but got `{type(url)}` with value `'{url}'` - serialized value may not be as expected." + ) + if info.mode == 'json': + return str(url) + return url + + @classmethod + def __get_pydantic_core_schema__( + cls, source: type[_BaseUrl], handler: GetCoreSchemaHandler + ) -> core_schema.CoreSchema: + def wrap_val(v, h): + if isinstance(v, source): + return v + if isinstance(v, _BaseUrl): + v = str(v) + core_url = h(v) + instance = source.__new__(source) + instance._url = core_url + return instance + + return core_schema.no_info_wrap_validator_function( + wrap_val, + schema=core_schema.url_schema(**cls._constraints.defined_constraints), + serialization=core_schema.plain_serializer_function_ser_schema( + cls.serialize_url, info_arg=True, when_used='always' + ), + ) + + @classmethod + def __get_pydantic_json_schema__( + cls, core_schema: core_schema.CoreSchema, handler: _schema_generation_shared.GetJsonSchemaHandler + ) -> JsonSchemaValue: + # we use the url schema for json schema generation, but we might have to extract it from + # the function-wrap schema we use as a tool for validation on initialization + inner_schema = core_schema['schema'] if core_schema['type'] == 'function-wrap' else core_schema + return handler(inner_schema) + + __pydantic_serializer__ = SchemaSerializer(core_schema.any_schema(serialization=core_schema.to_string_ser_schema())) + + +class _BaseMultiHostUrl: + _constraints: ClassVar[UrlConstraints] = UrlConstraints() + _url: _CoreMultiHostUrl + + def __init__(self, url: str | _CoreMultiHostUrl | _BaseMultiHostUrl) -> None: + self._url = _build_type_adapter(self.__class__).validate_python(url)._url + + @property + def scheme(self) -> str: + """The scheme part of the URL. + + e.g. `https` in `https://foo.com,bar.com/path?query#fragment` + """ + return self._url.scheme + + @property + def path(self) -> str | None: + """The path part of the URL, or `None`. + + e.g. `/path` in `https://foo.com,bar.com/path?query#fragment` + """ + return self._url.path + + @property + def query(self) -> str | None: + """The query part of the URL, or `None`. + + e.g. `query` in `https://foo.com,bar.com/path?query#fragment` + """ + return self._url.query + + def query_params(self) -> list[tuple[str, str]]: + """The query part of the URL as a list of key-value pairs. + + e.g. `[('foo', 'bar')]` in `https://foo.com,bar.com/path?foo=bar#fragment` + """ + return self._url.query_params() + + @property + def fragment(self) -> str | None: + """The fragment part of the URL, or `None`. + + e.g. `fragment` in `https://foo.com,bar.com/path?query#fragment` + """ + return self._url.fragment + + def hosts(self) -> list[MultiHostHost]: + '''The hosts of the `MultiHostUrl` as [`MultiHostHost`][pydantic_core.MultiHostHost] typed dicts. + + ```python + from pydantic_core import MultiHostUrl + + mhu = MultiHostUrl('https://foo.com:123,foo:bar@bar.com/path') + print(mhu.hosts()) + """ + [ + {'username': None, 'password': None, 'host': 'foo.com', 'port': 123}, + {'username': 'foo', 'password': 'bar', 'host': 'bar.com', 'port': 443} + ] + ``` + Returns: + A list of dicts, each representing a host. + ''' + return self._url.hosts() + + def encoded_string(self) -> str: + """The URL's encoded string representation via __str__(). + + This returns the punycode-encoded host version of the URL as a string. + """ + return str(self) + + def unicode_string(self) -> str: + """The URL as a unicode string, unlike `__str__()` this will not punycode encode the hosts.""" + return self._url.unicode_string() + + def __str__(self) -> str: + """The URL as a string, this will punycode encode the host if required.""" + return str(self._url) + + def __repr__(self) -> str: + return f'{self.__class__.__name__}({str(self._url)!r})' + + def __deepcopy__(self, memo: dict) -> Self: + return self.__class__(self._url) + + def __eq__(self, other: Any) -> bool: + return self.__class__ is other.__class__ and self._url == other._url + + def __hash__(self) -> int: + return hash(self._url) + + def __len__(self) -> int: + return len(str(self._url)) + + @classmethod + def build( + cls, + *, + scheme: str, + hosts: list[MultiHostHost] | None = None, + username: str | None = None, + password: str | None = None, + host: str | None = None, + port: int | None = None, + path: str | None = None, + query: str | None = None, + fragment: str | None = None, + ) -> Self: + """Build a new `MultiHostUrl` instance from its component parts. + + This method takes either `hosts` - a list of `MultiHostHost` typed dicts, or the individual components + `username`, `password`, `host` and `port`. + + Args: + scheme: The scheme part of the URL. + hosts: Multiple hosts to build the URL from. + username: The username part of the URL. + password: The password part of the URL. + host: The host part of the URL. + port: The port part of the URL. + path: The path part of the URL. + query: The query part of the URL, or omit for no query. + fragment: The fragment part of the URL, or omit for no fragment. + + Returns: + An instance of `MultiHostUrl` + """ + return cls( + _CoreMultiHostUrl.build( + scheme=scheme, + hosts=hosts, + username=username, + password=password, + host=host, + port=port, + path=path, + query=query, + fragment=fragment, + ) + ) + + @classmethod + def serialize_url(cls, url: Any, info: core_schema.SerializationInfo) -> str | Self: + if not isinstance(url, cls): + raise PydanticSerializationUnexpectedValue( + f"Expected `{cls}` but got `{type(url)}` with value `'{url}'` - serialized value may not be as expected." + ) + if info.mode == 'json': + return str(url) + return url + + @classmethod + def __get_pydantic_core_schema__( + cls, source: type[_BaseMultiHostUrl], handler: GetCoreSchemaHandler + ) -> core_schema.CoreSchema: + def wrap_val(v, h): + if isinstance(v, source): + return v + if isinstance(v, _BaseMultiHostUrl): + v = str(v) + core_url = h(v) + instance = source.__new__(source) + instance._url = core_url + return instance + + return core_schema.no_info_wrap_validator_function( + wrap_val, + schema=core_schema.multi_host_url_schema(**cls._constraints.defined_constraints), + serialization=core_schema.plain_serializer_function_ser_schema( + cls.serialize_url, info_arg=True, when_used='always' + ), + ) + + @classmethod + def __get_pydantic_json_schema__( + cls, core_schema: core_schema.CoreSchema, handler: _schema_generation_shared.GetJsonSchemaHandler + ) -> JsonSchemaValue: + # we use the url schema for json schema generation, but we might have to extract it from + # the function-wrap schema we use as a tool for validation on initialization + inner_schema = core_schema['schema'] if core_schema['type'] == 'function-wrap' else core_schema + return handler(inner_schema) + + __pydantic_serializer__ = SchemaSerializer(core_schema.any_schema(serialization=core_schema.to_string_ser_schema())) + + +@lru_cache +def _build_type_adapter(cls: type[_BaseUrl | _BaseMultiHostUrl]) -> TypeAdapter: + return TypeAdapter(cls) + + +class AnyUrl(_BaseUrl): + """Base type for all URLs. + + * Any scheme allowed + * Top-level domain (TLD) not required + * Host not required + + Assuming an input URL of `http://samuel:pass@example.com:8000/the/path/?query=here#fragment=is;this=bit`, + the types export the following properties: + + - `scheme`: the URL scheme (`http`), always set. + - `host`: the URL host (`example.com`). + - `username`: optional username if included (`samuel`). + - `password`: optional password if included (`pass`). + - `port`: optional port (`8000`). + - `path`: optional path (`/the/path/`). + - `query`: optional URL query (for example, `GET` arguments or "search string", such as `query=here`). + - `fragment`: optional fragment (`fragment=is;this=bit`). + """ + + +# Note: all single host urls inherit from `AnyUrl` to preserve compatibility with pre-v2.10 code +# Where urls were annotated variants of `AnyUrl`, which was an alias to `pydantic_core.Url` + + +class AnyHttpUrl(AnyUrl): + """A type that will accept any http or https URL. + + * TLD not required + * Host not required + """ + + _constraints = UrlConstraints(allowed_schemes=['http', 'https']) + + +class HttpUrl(AnyUrl): + """A type that will accept any http or https URL. + + * TLD not required + * Host not required + * Max length 2083 + + ```python + from pydantic import BaseModel, HttpUrl, ValidationError + + class MyModel(BaseModel): + url: HttpUrl + + m = MyModel(url='http://www.example.com') # (1)! + print(m.url) + #> http://www.example.com/ + + try: + MyModel(url='ftp://invalid.url') + except ValidationError as e: + print(e) + ''' + 1 validation error for MyModel + url + URL scheme should be 'http' or 'https' [type=url_scheme, input_value='ftp://invalid.url', input_type=str] + ''' + + try: + MyModel(url='not a url') + except ValidationError as e: + print(e) + ''' + 1 validation error for MyModel + url + Input should be a valid URL, relative URL without a base [type=url_parsing, input_value='not a url', input_type=str] + ''' + ``` + + 1. Note: mypy would prefer `m = MyModel(url=HttpUrl('http://www.example.com'))`, but Pydantic will convert the string to an HttpUrl instance anyway. + + "International domains" (e.g. a URL where the host or TLD includes non-ascii characters) will be encoded via + [punycode](https://en.wikipedia.org/wiki/Punycode) (see + [this article](https://www.xudongz.com/blog/2017/idn-phishing/) for a good description of why this is important): + + ```python + from pydantic import BaseModel, HttpUrl + + class MyModel(BaseModel): + url: HttpUrl + + m1 = MyModel(url='http://puny£code.com') + print(m1.url) + #> http://xn--punycode-eja.com/ + m2 = MyModel(url='https://www.аррӏе.com/') + print(m2.url) + #> https://www.xn--80ak6aa92e.com/ + m3 = MyModel(url='https://www.example.珠宝/') + print(m3.url) + #> https://www.example.xn--pbt977c/ + ``` + + + !!! warning "Underscores in Hostnames" + In Pydantic, underscores are allowed in all parts of a domain except the TLD. + Technically this might be wrong - in theory the hostname cannot have underscores, but subdomains can. + + To explain this; consider the following two cases: + + - `exam_ple.co.uk`: the hostname is `exam_ple`, which should not be allowed since it contains an underscore. + - `foo_bar.example.com` the hostname is `example`, which should be allowed since the underscore is in the subdomain. + + Without having an exhaustive list of TLDs, it would be impossible to differentiate between these two. Therefore + underscores are allowed, but you can always do further validation in a validator if desired. + + Also, Chrome, Firefox, and Safari all currently accept `http://exam_ple.com` as a URL, so we're in good + (or at least big) company. + """ + + _constraints = UrlConstraints(max_length=2083, allowed_schemes=['http', 'https']) + + +class AnyWebsocketUrl(AnyUrl): + """A type that will accept any ws or wss URL. + + * TLD not required + * Host not required + """ + + _constraints = UrlConstraints(allowed_schemes=['ws', 'wss']) + + +class WebsocketUrl(AnyUrl): + """A type that will accept any ws or wss URL. + + * TLD not required + * Host not required + * Max length 2083 + """ + + _constraints = UrlConstraints(max_length=2083, allowed_schemes=['ws', 'wss']) + + +class FileUrl(AnyUrl): + """A type that will accept any file URL. + + * Host not required + """ + + _constraints = UrlConstraints(allowed_schemes=['file']) + + +class FtpUrl(AnyUrl): + """A type that will accept ftp URL. + + * TLD not required + * Host not required + """ + + _constraints = UrlConstraints(allowed_schemes=['ftp']) + + +class PostgresDsn(_BaseMultiHostUrl): + """A type that will accept any Postgres DSN. + + * User info required + * TLD not required + * Host required + * Supports multiple hosts + + If further validation is required, these properties can be used by validators to enforce specific behaviour: + + ```python + from pydantic import ( + BaseModel, + HttpUrl, + PostgresDsn, + ValidationError, + field_validator, + ) + + class MyModel(BaseModel): + url: HttpUrl + + m = MyModel(url='http://www.example.com') + + # the repr() method for a url will display all properties of the url + print(repr(m.url)) + #> HttpUrl('http://www.example.com/') + print(m.url.scheme) + #> http + print(m.url.host) + #> www.example.com + print(m.url.port) + #> 80 + + class MyDatabaseModel(BaseModel): + db: PostgresDsn + + @field_validator('db') + def check_db_name(cls, v): + assert v.path and len(v.path) > 1, 'database must be provided' + return v + + m = MyDatabaseModel(db='postgres://user:pass@localhost:5432/foobar') + print(m.db) + #> postgres://user:pass@localhost:5432/foobar + + try: + MyDatabaseModel(db='postgres://user:pass@localhost:5432') + except ValidationError as e: + print(e) + ''' + 1 validation error for MyDatabaseModel + db + Assertion failed, database must be provided + assert (None) + + where None = PostgresDsn('postgres://user:pass@localhost:5432').path [type=assertion_error, input_value='postgres://user:pass@localhost:5432', input_type=str] + ''' + ``` + """ + + _constraints = UrlConstraints( + host_required=True, + allowed_schemes=[ + 'postgres', + 'postgresql', + 'postgresql+asyncpg', + 'postgresql+pg8000', + 'postgresql+psycopg', + 'postgresql+psycopg2', + 'postgresql+psycopg2cffi', + 'postgresql+py-postgresql', + 'postgresql+pygresql', + ], + ) + + @property + def host(self) -> str: + """The required URL host.""" + return self._url.host # pyright: ignore[reportAttributeAccessIssue] + + +class CockroachDsn(AnyUrl): + """A type that will accept any Cockroach DSN. + + * User info required + * TLD not required + * Host required + """ + + _constraints = UrlConstraints( + host_required=True, + allowed_schemes=[ + 'cockroachdb', + 'cockroachdb+psycopg2', + 'cockroachdb+asyncpg', + ], + ) + + @property + def host(self) -> str: + """The required URL host.""" + return self._url.host # pyright: ignore[reportReturnType] + + +class AmqpDsn(AnyUrl): + """A type that will accept any AMQP DSN. + + * User info required + * TLD not required + * Host not required + """ + + _constraints = UrlConstraints(allowed_schemes=['amqp', 'amqps']) + + +class RedisDsn(AnyUrl): + """A type that will accept any Redis DSN. + + * User info required + * TLD not required + * Host required (e.g., `rediss://:pass@localhost`) + """ + + _constraints = UrlConstraints( + allowed_schemes=['redis', 'rediss'], + default_host='localhost', + default_port=6379, + default_path='/0', + host_required=True, + ) + + @property + def host(self) -> str: + """The required URL host.""" + return self._url.host # pyright: ignore[reportReturnType] + + +class MongoDsn(_BaseMultiHostUrl): + """A type that will accept any MongoDB DSN. + + * User info not required + * Database name not required + * Port not required + * User info may be passed without user part (e.g., `mongodb://mongodb0.example.com:27017`). + + !!! warning + If a port isn't specified, the default MongoDB port `27017` will be used. If this behavior is + undesirable, you can use the following: + + ```python + from typing import Annotated + + from pydantic_core import MultiHostUrl + + from pydantic import UrlConstraints + + MongoDsnNoDefaultPort = Annotated[ + MultiHostUrl, + UrlConstraints(allowed_schemes=['mongodb', 'mongodb+srv']), + ] + ``` + """ + + _constraints = UrlConstraints(allowed_schemes=['mongodb', 'mongodb+srv'], default_port=27017) + + +class KafkaDsn(AnyUrl): + """A type that will accept any Kafka DSN. + + * User info required + * TLD not required + * Host not required + """ + + _constraints = UrlConstraints(allowed_schemes=['kafka'], default_host='localhost', default_port=9092) + + +class NatsDsn(_BaseMultiHostUrl): + """A type that will accept any NATS DSN. + + NATS is a connective technology built for the ever increasingly hyper-connected world. + It is a single technology that enables applications to securely communicate across + any combination of cloud vendors, on-premise, edge, web and mobile, and devices. + More: https://nats.io + """ + + _constraints = UrlConstraints( + allowed_schemes=['nats', 'tls', 'ws', 'wss'], default_host='localhost', default_port=4222 + ) + + +class MySQLDsn(AnyUrl): + """A type that will accept any MySQL DSN. + + * User info required + * TLD not required + * Host not required + """ + + _constraints = UrlConstraints( + allowed_schemes=[ + 'mysql', + 'mysql+mysqlconnector', + 'mysql+aiomysql', + 'mysql+asyncmy', + 'mysql+mysqldb', + 'mysql+pymysql', + 'mysql+cymysql', + 'mysql+pyodbc', + ], + default_port=3306, + host_required=True, + ) + + +class MariaDBDsn(AnyUrl): + """A type that will accept any MariaDB DSN. + + * User info required + * TLD not required + * Host not required + """ + + _constraints = UrlConstraints( + allowed_schemes=['mariadb', 'mariadb+mariadbconnector', 'mariadb+pymysql'], + default_port=3306, + ) + + +class ClickHouseDsn(AnyUrl): + """A type that will accept any ClickHouse DSN. + + * User info required + * TLD not required + * Host not required + """ + + _constraints = UrlConstraints( + allowed_schemes=[ + 'clickhouse+native', + 'clickhouse+asynch', + 'clickhouse+http', + 'clickhouse', + 'clickhouses', + 'clickhousedb', + ], + default_host='localhost', + default_port=9000, + ) + + +class SnowflakeDsn(AnyUrl): + """A type that will accept any Snowflake DSN. + + * User info required + * TLD not required + * Host required + """ + + _constraints = UrlConstraints( + allowed_schemes=['snowflake'], + host_required=True, + ) + + @property + def host(self) -> str: + """The required URL host.""" + return self._url.host # pyright: ignore[reportReturnType] + + +def import_email_validator() -> None: + global email_validator + try: + import email_validator + except ImportError as e: + raise ImportError("email-validator is not installed, run `pip install 'pydantic[email]'`") from e + if not version('email-validator').partition('.')[0] == '2': + raise ImportError('email-validator version >= 2.0 required, run pip install -U email-validator') + + +if TYPE_CHECKING: + EmailStr = Annotated[str, ...] +else: + + class EmailStr: + """ + Info: + To use this type, you need to install the optional + [`email-validator`](https://github.com/JoshData/python-email-validator) package: + + ```bash + pip install email-validator + ``` + + Validate email addresses. + + ```python + from pydantic import BaseModel, EmailStr + + class Model(BaseModel): + email: EmailStr + + print(Model(email='contact@mail.com')) + #> email='contact@mail.com' + ``` + """ # noqa: D212 + + @classmethod + def __get_pydantic_core_schema__( + cls, + _source: type[Any], + _handler: GetCoreSchemaHandler, + ) -> core_schema.CoreSchema: + import_email_validator() + return core_schema.no_info_after_validator_function(cls._validate, core_schema.str_schema()) + + @classmethod + def __get_pydantic_json_schema__( + cls, core_schema: core_schema.CoreSchema, handler: _schema_generation_shared.GetJsonSchemaHandler + ) -> JsonSchemaValue: + field_schema = handler(core_schema) + field_schema.update(type='string', format='email') + return field_schema + + @classmethod + def _validate(cls, input_value: str, /) -> str: + return validate_email(input_value)[1] + + +class NameEmail(_repr.Representation): + """ + Info: + To use this type, you need to install the optional + [`email-validator`](https://github.com/JoshData/python-email-validator) package: + + ```bash + pip install email-validator + ``` + + Validate a name and email address combination, as specified by + [RFC 5322](https://datatracker.ietf.org/doc/html/rfc5322#section-3.4). + + The `NameEmail` has two properties: `name` and `email`. + In case the `name` is not provided, it's inferred from the email address. + + ```python + from pydantic import BaseModel, NameEmail + + class User(BaseModel): + email: NameEmail + + user = User(email='Fred Bloggs ') + print(user.email) + #> Fred Bloggs + print(user.email.name) + #> Fred Bloggs + + user = User(email='fred.bloggs@example.com') + print(user.email) + #> fred.bloggs + print(user.email.name) + #> fred.bloggs + ``` + """ # noqa: D212 + + __slots__ = 'name', 'email' + + def __init__(self, name: str, email: str): + self.name = name + self.email = email + + def __eq__(self, other: Any) -> bool: + return isinstance(other, NameEmail) and (self.name, self.email) == (other.name, other.email) + + @classmethod + def __get_pydantic_json_schema__( + cls, core_schema: core_schema.CoreSchema, handler: _schema_generation_shared.GetJsonSchemaHandler + ) -> JsonSchemaValue: + field_schema = handler(core_schema) + field_schema.update(type='string', format='name-email') + return field_schema + + @classmethod + def __get_pydantic_core_schema__( + cls, + _source: type[Any], + _handler: GetCoreSchemaHandler, + ) -> core_schema.CoreSchema: + import_email_validator() + + return core_schema.no_info_after_validator_function( + cls._validate, + core_schema.json_or_python_schema( + json_schema=core_schema.str_schema(), + python_schema=core_schema.union_schema( + [core_schema.is_instance_schema(cls), core_schema.str_schema()], + custom_error_type='name_email_type', + custom_error_message='Input is not a valid NameEmail', + ), + serialization=core_schema.to_string_ser_schema(), + ), + ) + + @classmethod + def _validate(cls, input_value: Self | str, /) -> Self: + if isinstance(input_value, str): + name, email = validate_email(input_value) + return cls(name, email) + else: + return input_value + + def __str__(self) -> str: + if '@' in self.name: + return f'"{self.name}" <{self.email}>' + + return f'{self.name} <{self.email}>' + + +IPvAnyAddressType: TypeAlias = 'IPv4Address | IPv6Address' +IPvAnyInterfaceType: TypeAlias = 'IPv4Interface | IPv6Interface' +IPvAnyNetworkType: TypeAlias = 'IPv4Network | IPv6Network' + +if TYPE_CHECKING: + IPvAnyAddress = IPvAnyAddressType + IPvAnyInterface = IPvAnyInterfaceType + IPvAnyNetwork = IPvAnyNetworkType +else: + + class IPvAnyAddress: + """Validate an IPv4 or IPv6 address. + + ```python + from pydantic import BaseModel + from pydantic.networks import IPvAnyAddress + + class IpModel(BaseModel): + ip: IPvAnyAddress + + print(IpModel(ip='127.0.0.1')) + #> ip=IPv4Address('127.0.0.1') + + try: + IpModel(ip='http://www.example.com') + except ValueError as e: + print(e.errors()) + ''' + [ + { + 'type': 'ip_any_address', + 'loc': ('ip',), + 'msg': 'value is not a valid IPv4 or IPv6 address', + 'input': 'http://www.example.com', + } + ] + ''' + ``` + """ + + __slots__ = () + + def __new__(cls, value: Any) -> IPvAnyAddressType: + """Validate an IPv4 or IPv6 address.""" + try: + return IPv4Address(value) + except ValueError: + pass + + try: + return IPv6Address(value) + except ValueError: + raise PydanticCustomError('ip_any_address', 'value is not a valid IPv4 or IPv6 address') + + @classmethod + def __get_pydantic_json_schema__( + cls, core_schema: core_schema.CoreSchema, handler: _schema_generation_shared.GetJsonSchemaHandler + ) -> JsonSchemaValue: + field_schema = {} + field_schema.update(type='string', format='ipvanyaddress') + return field_schema + + @classmethod + def __get_pydantic_core_schema__( + cls, + _source: type[Any], + _handler: GetCoreSchemaHandler, + ) -> core_schema.CoreSchema: + return core_schema.no_info_plain_validator_function( + cls._validate, serialization=core_schema.to_string_ser_schema() + ) + + @classmethod + def _validate(cls, input_value: Any, /) -> IPvAnyAddressType: + return cls(input_value) # type: ignore[return-value] + + class IPvAnyInterface: + """Validate an IPv4 or IPv6 interface.""" + + __slots__ = () + + def __new__(cls, value: NetworkType) -> IPvAnyInterfaceType: + """Validate an IPv4 or IPv6 interface.""" + try: + return IPv4Interface(value) + except ValueError: + pass + + try: + return IPv6Interface(value) + except ValueError: + raise PydanticCustomError('ip_any_interface', 'value is not a valid IPv4 or IPv6 interface') + + @classmethod + def __get_pydantic_json_schema__( + cls, core_schema: core_schema.CoreSchema, handler: _schema_generation_shared.GetJsonSchemaHandler + ) -> JsonSchemaValue: + field_schema = {} + field_schema.update(type='string', format='ipvanyinterface') + return field_schema + + @classmethod + def __get_pydantic_core_schema__( + cls, + _source: type[Any], + _handler: GetCoreSchemaHandler, + ) -> core_schema.CoreSchema: + return core_schema.no_info_plain_validator_function( + cls._validate, serialization=core_schema.to_string_ser_schema() + ) + + @classmethod + def _validate(cls, input_value: NetworkType, /) -> IPvAnyInterfaceType: + return cls(input_value) # type: ignore[return-value] + + class IPvAnyNetwork: + """Validate an IPv4 or IPv6 network.""" + + __slots__ = () + + def __new__(cls, value: NetworkType) -> IPvAnyNetworkType: + """Validate an IPv4 or IPv6 network.""" + # Assume IP Network is defined with a default value for `strict` argument. + # Define your own class if you want to specify network address check strictness. + try: + return IPv4Network(value) + except ValueError: + pass + + try: + return IPv6Network(value) + except ValueError: + raise PydanticCustomError('ip_any_network', 'value is not a valid IPv4 or IPv6 network') + + @classmethod + def __get_pydantic_json_schema__( + cls, core_schema: core_schema.CoreSchema, handler: _schema_generation_shared.GetJsonSchemaHandler + ) -> JsonSchemaValue: + field_schema = {} + field_schema.update(type='string', format='ipvanynetwork') + return field_schema + + @classmethod + def __get_pydantic_core_schema__( + cls, + _source: type[Any], + _handler: GetCoreSchemaHandler, + ) -> core_schema.CoreSchema: + return core_schema.no_info_plain_validator_function( + cls._validate, serialization=core_schema.to_string_ser_schema() + ) + + @classmethod + def _validate(cls, input_value: NetworkType, /) -> IPvAnyNetworkType: + return cls(input_value) # type: ignore[return-value] + + +def _build_pretty_email_regex() -> re.Pattern[str]: + name_chars = r'[\w!#$%&\'*+\-/=?^_`{|}~]' + unquoted_name_group = rf'((?:{name_chars}+\s+)*{name_chars}+)' + quoted_name_group = r'"((?:[^"]|\")+)"' + email_group = r'<(.+)>' + return re.compile(rf'\s*(?:{unquoted_name_group}|{quoted_name_group})?\s*{email_group}\s*') + + +pretty_email_regex = _build_pretty_email_regex() + +MAX_EMAIL_LENGTH = 2048 +"""Maximum length for an email. +A somewhat arbitrary but very generous number compared to what is allowed by most implementations. +""" + + +def validate_email(value: str) -> tuple[str, str]: + """Email address validation using [email-validator](https://pypi.org/project/email-validator/). + + Returns: + A tuple containing the local part of the email (or the name for "pretty" email addresses) + and the normalized email. + + Raises: + PydanticCustomError: If the email is invalid. + + Note: + Note that: + + * Raw IP address (literal) domain parts are not allowed. + * `"John Doe "` style "pretty" email addresses are processed. + * Spaces are striped from the beginning and end of addresses, but no error is raised. + """ + if email_validator is None: + import_email_validator() + + if len(value) > MAX_EMAIL_LENGTH: + raise PydanticCustomError( + 'value_error', + 'value is not a valid email address: {reason}', + {'reason': f'Length must not exceed {MAX_EMAIL_LENGTH} characters'}, + ) + + m = pretty_email_regex.fullmatch(value) + name: str | None = None + if m: + unquoted_name, quoted_name, value = m.groups() + name = unquoted_name or quoted_name + + email = value.strip() + + try: + parts = email_validator.validate_email(email, check_deliverability=False) + except email_validator.EmailNotValidError as e: + raise PydanticCustomError( + 'value_error', 'value is not a valid email address: {reason}', {'reason': str(e.args[0])} + ) from e + + email = parts.normalized + assert email is not None + name = name or parts.local_part + return name, email + + +__getattr__ = getattr_migration(__name__) diff --git a/source/target-vendors/cp312/pydantic/parse.py b/source/target-vendors/cp312/pydantic/parse.py new file mode 100644 index 0000000..68b7f04 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/parse.py @@ -0,0 +1,5 @@ +"""The `parse` module is a backport module from V1.""" + +from ._migration import getattr_migration + +__getattr__ = getattr_migration(__name__) diff --git a/source/target-vendors/cp312/pydantic/plugin/__init__.py b/source/target-vendors/cp312/pydantic/plugin/__init__.py new file mode 100644 index 0000000..840d20a --- /dev/null +++ b/source/target-vendors/cp312/pydantic/plugin/__init__.py @@ -0,0 +1,193 @@ +"""!!! abstract "Usage Documentation" + [Build a Plugin](../concepts/plugins.md#build-a-plugin) + +Plugin interface for Pydantic plugins, and related types. +""" + +from __future__ import annotations + +from typing import Any, Callable, Literal, NamedTuple + +from pydantic_core import CoreConfig, CoreSchema, ValidationError +from typing_extensions import Protocol, TypeAlias + +from pydantic.config import ExtraValues + +__all__ = ( + 'PydanticPluginProtocol', + 'BaseValidateHandlerProtocol', + 'ValidatePythonHandlerProtocol', + 'ValidateJsonHandlerProtocol', + 'ValidateStringsHandlerProtocol', + 'NewSchemaReturns', + 'SchemaTypePath', + 'SchemaKind', +) + +NewSchemaReturns: TypeAlias = 'tuple[ValidatePythonHandlerProtocol | None, ValidateJsonHandlerProtocol | None, ValidateStringsHandlerProtocol | None]' + + +class SchemaTypePath(NamedTuple): + """Path defining where `schema_type` was defined, or where `TypeAdapter` was called.""" + + module: str + name: str + + +SchemaKind: TypeAlias = Literal['BaseModel', 'TypeAdapter', 'dataclass', 'create_model', 'validate_call'] + + +class PydanticPluginProtocol(Protocol): + """Protocol defining the interface for Pydantic plugins.""" + + def new_schema_validator( + self, + schema: CoreSchema, + schema_type: Any, + schema_type_path: SchemaTypePath, + schema_kind: SchemaKind, + config: CoreConfig | None, + plugin_settings: dict[str, object], + ) -> tuple[ + ValidatePythonHandlerProtocol | None, ValidateJsonHandlerProtocol | None, ValidateStringsHandlerProtocol | None + ]: + """This method is called for each plugin every time a new [`SchemaValidator`][pydantic_core.SchemaValidator] + is created. + + It should return an event handler for each of the three validation methods, or `None` if the plugin does not + implement that method. + + Args: + schema: The schema to validate against. + schema_type: The original type which the schema was created from, e.g. the model class. + schema_type_path: Path defining where `schema_type` was defined, or where `TypeAdapter` was called. + schema_kind: The kind of schema to validate against. + config: The config to use for validation. + plugin_settings: Any plugin settings. + + Returns: + A tuple of optional event handlers for each of the three validation methods - + `validate_python`, `validate_json`, `validate_strings`. + """ + raise NotImplementedError('Pydantic plugins should implement `new_schema_validator`.') + + +class BaseValidateHandlerProtocol(Protocol): + """Base class for plugin callbacks protocols. + + You shouldn't implement this protocol directly, instead use one of the subclasses with adds the correctly + typed `on_error` method. + """ + + on_enter: Callable[..., None] + """`on_enter` is changed to be more specific on all subclasses""" + + def on_success(self, result: Any) -> None: + """Callback to be notified of successful validation. + + Args: + result: The result of the validation. + """ + return + + def on_error(self, error: ValidationError) -> None: + """Callback to be notified of validation errors. + + Args: + error: The validation error. + """ + return + + def on_exception(self, exception: Exception) -> None: + """Callback to be notified of validation exceptions. + + Args: + exception: The exception raised during validation. + """ + return + + +class ValidatePythonHandlerProtocol(BaseValidateHandlerProtocol, Protocol): + """Event handler for `SchemaValidator.validate_python`.""" + + def on_enter( + self, + input: Any, + *, + strict: bool | None = None, + extra: ExtraValues | None = None, + from_attributes: bool | None = None, + context: Any | None = None, + self_instance: Any | None = None, + by_alias: bool | None = None, + by_name: bool | None = None, + ) -> None: + """Callback to be notified of validation start, and create an instance of the event handler. + + Args: + input: The input to be validated. + strict: Whether to validate the object in strict mode. + extra: Whether to ignore, allow, or forbid extra data during model validation. + from_attributes: Whether to validate objects as inputs by extracting attributes. + context: The context to use for validation, this is passed to functional validators. + self_instance: An instance of a model to set attributes on from validation, this is used when running + validation from the `__init__` method of a model. + by_alias: Whether to use the field's alias to match the input data to an attribute. + by_name: Whether to use the field's name to match the input data to an attribute. + """ + + +class ValidateJsonHandlerProtocol(BaseValidateHandlerProtocol, Protocol): + """Event handler for `SchemaValidator.validate_json`.""" + + def on_enter( + self, + input: str | bytes | bytearray, + *, + strict: bool | None = None, + extra: ExtraValues | None = None, + context: Any | None = None, + self_instance: Any | None = None, + by_alias: bool | None = None, + by_name: bool | None = None, + ) -> None: + """Callback to be notified of validation start, and create an instance of the event handler. + + Args: + input: The JSON data to be validated. + strict: Whether to validate the object in strict mode. + extra: Whether to ignore, allow, or forbid extra data during model validation. + context: The context to use for validation, this is passed to functional validators. + self_instance: An instance of a model to set attributes on from validation, this is used when running + validation from the `__init__` method of a model. + by_alias: Whether to use the field's alias to match the input data to an attribute. + by_name: Whether to use the field's name to match the input data to an attribute. + """ + + +StringInput: TypeAlias = 'dict[str, StringInput]' + + +class ValidateStringsHandlerProtocol(BaseValidateHandlerProtocol, Protocol): + """Event handler for `SchemaValidator.validate_strings`.""" + + def on_enter( + self, + input: StringInput, + *, + strict: bool | None = None, + extra: ExtraValues | None = None, + context: Any | None = None, + by_alias: bool | None = None, + by_name: bool | None = None, + ) -> None: + """Callback to be notified of validation start, and create an instance of the event handler. + + Args: + input: The string data to be validated. + strict: Whether to validate the object in strict mode. + extra: Whether to ignore, allow, or forbid extra data during model validation. + context: The context to use for validation, this is passed to functional validators. + by_alias: Whether to use the field's alias to match the input data to an attribute. + by_name: Whether to use the field's name to match the input data to an attribute. + """ diff --git a/source/target-vendors/cp312/pydantic/plugin/_loader.py b/source/target-vendors/cp312/pydantic/plugin/_loader.py new file mode 100644 index 0000000..e8226fa --- /dev/null +++ b/source/target-vendors/cp312/pydantic/plugin/_loader.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import importlib.metadata as importlib_metadata +import os +import warnings +from collections.abc import Iterable +from typing import TYPE_CHECKING, Final + +if TYPE_CHECKING: + from . import PydanticPluginProtocol + + +PYDANTIC_ENTRY_POINT_GROUP: Final[str] = 'pydantic' + +# cache of plugins +_plugins: dict[str, PydanticPluginProtocol] | None = None +# return no plugins while loading plugins to avoid recursion and errors while importing plugins +# this means that if plugins use pydantic +_loading_plugins: bool = False + + +def get_plugins() -> Iterable[PydanticPluginProtocol]: + """Load plugins for Pydantic. + + Inspired by: https://github.com/pytest-dev/pluggy/blob/1.3.0/src/pluggy/_manager.py#L376-L402 + """ + disabled_plugins = os.getenv('PYDANTIC_DISABLE_PLUGINS') + global _plugins, _loading_plugins + if _loading_plugins: + # this happens when plugins themselves use pydantic, we return no plugins + return () + elif disabled_plugins in ('__all__', '1', 'true'): + return () + elif _plugins is None: + _plugins = {} + # set _loading_plugins so any plugins that use pydantic don't themselves use plugins + _loading_plugins = True + try: + for dist in importlib_metadata.distributions(): + for entry_point in dist.entry_points: + if entry_point.group != PYDANTIC_ENTRY_POINT_GROUP: + continue + if entry_point.value in _plugins: + continue + if disabled_plugins is not None and entry_point.name in disabled_plugins.split(','): + continue + try: + _plugins[entry_point.value] = entry_point.load() + except (ImportError, AttributeError) as e: + warnings.warn( + f'{e.__class__.__name__} while loading the `{entry_point.name}` Pydantic plugin, ' + f'this plugin will not be installed.\n\n{e!r}', + stacklevel=2, + ) + finally: + _loading_plugins = False + + return _plugins.values() diff --git a/source/target-vendors/cp312/pydantic/plugin/_schema_validator.py b/source/target-vendors/cp312/pydantic/plugin/_schema_validator.py new file mode 100644 index 0000000..6ca5563 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/plugin/_schema_validator.py @@ -0,0 +1,152 @@ +"""Pluggable schema validator for pydantic.""" + +from __future__ import annotations + +import functools +from collections.abc import Iterable +from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar + +from pydantic_core import CoreConfig, CoreSchema, SchemaValidator, ValidationError +from typing_extensions import ParamSpec + +if TYPE_CHECKING: + from . import BaseValidateHandlerProtocol, PydanticPluginProtocol, SchemaKind, SchemaTypePath + + +P = ParamSpec('P') +R = TypeVar('R') +Event = Literal['on_validate_python', 'on_validate_json', 'on_validate_strings'] +events: list[Event] = list(Event.__args__) # type: ignore + + +def create_schema_validator( + schema: CoreSchema, + schema_type: Any, + schema_type_module: str, + schema_type_name: str, + schema_kind: SchemaKind, + config: CoreConfig | None = None, + plugin_settings: dict[str, Any] | None = None, + _use_prebuilt: bool = True, +) -> SchemaValidator | PluggableSchemaValidator: + """Create a `SchemaValidator` or `PluggableSchemaValidator` if plugins are installed. + + Returns: + If plugins are installed then return `PluggableSchemaValidator`, otherwise return `SchemaValidator`. + """ + from . import SchemaTypePath + from ._loader import get_plugins + + plugins = get_plugins() + if plugins: + return PluggableSchemaValidator( + schema, + schema_type, + SchemaTypePath(schema_type_module, schema_type_name), + schema_kind, + config, + plugins, + plugin_settings or {}, + _use_prebuilt=_use_prebuilt, + ) + else: + return SchemaValidator(schema, config, _use_prebuilt=_use_prebuilt) + + +class PluggableSchemaValidator: + """Pluggable schema validator.""" + + __slots__ = '_schema_validator', 'validate_json', 'validate_python', 'validate_strings' + + def __init__( + self, + schema: CoreSchema, + schema_type: Any, + schema_type_path: SchemaTypePath, + schema_kind: SchemaKind, + config: CoreConfig | None, + plugins: Iterable[PydanticPluginProtocol], + plugin_settings: dict[str, Any], + _use_prebuilt: bool = True, + ) -> None: + self._schema_validator = SchemaValidator(schema, config, _use_prebuilt=_use_prebuilt) + + python_event_handlers: list[BaseValidateHandlerProtocol] = [] + json_event_handlers: list[BaseValidateHandlerProtocol] = [] + strings_event_handlers: list[BaseValidateHandlerProtocol] = [] + for plugin in plugins: + try: + p, j, s = plugin.new_schema_validator( + schema, schema_type, schema_type_path, schema_kind, config, plugin_settings + ) + except TypeError as e: # pragma: no cover + raise TypeError(f'Error using plugin `{plugin.__module__}:{plugin.__class__.__name__}`: {e}') from e + if p is not None: + python_event_handlers.append(p) + if j is not None: + json_event_handlers.append(j) + if s is not None: + strings_event_handlers.append(s) + + self.validate_python = build_wrapper(self._schema_validator.validate_python, python_event_handlers) + self.validate_json = build_wrapper(self._schema_validator.validate_json, json_event_handlers) + self.validate_strings = build_wrapper(self._schema_validator.validate_strings, strings_event_handlers) + + # Used to allow reuse of validators in pydantic-core (see pydantic-core/src/validators/prebuilt.rs): + @property + def __pydantic_schema_validator__(self) -> SchemaValidator: + """The underlying pydantic-core `SchemaValidator`. + + This is a private attribute, not meant to be used outside Pydantic. + """ + return self._schema_validator + + def __getattr__(self, name: str) -> Any: + return getattr(self._schema_validator, name) + + +def build_wrapper(func: Callable[P, R], event_handlers: list[BaseValidateHandlerProtocol]) -> Callable[P, R]: + if not event_handlers: + return func + else: + on_enters = tuple(h.on_enter for h in event_handlers if filter_handlers(h, 'on_enter')) + on_successes = tuple(h.on_success for h in event_handlers if filter_handlers(h, 'on_success')) + on_errors = tuple(h.on_error for h in event_handlers if filter_handlers(h, 'on_error')) + on_exceptions = tuple(h.on_exception for h in event_handlers if filter_handlers(h, 'on_exception')) + + @functools.wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + for on_enter_handler in on_enters: + on_enter_handler(*args, **kwargs) + + try: + result = func(*args, **kwargs) + except ValidationError as error: + for on_error_handler in on_errors: + on_error_handler(error) + raise + except Exception as exception: + for on_exception_handler in on_exceptions: + on_exception_handler(exception) + raise + else: + for on_success_handler in on_successes: + on_success_handler(result) + return result + + return wrapper + + +def filter_handlers(handler_cls: BaseValidateHandlerProtocol, method_name: str) -> bool: + """Filter out handler methods which are not implemented by the plugin directly - e.g. those that are missing + or are inherited from the protocol. + """ + handler = getattr(handler_cls, method_name, None) + if handler is None: + return False + elif handler.__module__ == 'pydantic.plugin': + # this is the original handler, from the protocol due to runtime inheritance + # we don't want to call it + return False + else: + return True diff --git a/source/target-vendors/cp312/pydantic/py.typed b/source/target-vendors/cp312/pydantic/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/pydantic/root_model.py b/source/target-vendors/cp312/pydantic/root_model.py new file mode 100644 index 0000000..d290043 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/root_model.py @@ -0,0 +1,157 @@ +"""RootModel class and type definitions.""" + +from __future__ import annotations as _annotations + +from copy import copy, deepcopy +from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar + +from pydantic_core import PydanticUndefined +from typing_extensions import Self, dataclass_transform + +from . import PydanticUserError +from ._internal import _model_construction, _repr +from .main import BaseModel, _object_setattr + +if TYPE_CHECKING: + from .fields import Field as PydanticModelField + from .fields import PrivateAttr as PydanticModelPrivateAttr + + # dataclass_transform could be applied to RootModel directly, but `ModelMetaclass`'s dataclass_transform + # takes priority (at least with pyright). We trick type checkers into thinking we apply dataclass_transform + # on a new metaclass. + @dataclass_transform(kw_only_default=False, field_specifiers=(PydanticModelField, PydanticModelPrivateAttr)) + class _RootModelMetaclass(_model_construction.ModelMetaclass): ... +else: + _RootModelMetaclass = _model_construction.ModelMetaclass + +__all__ = ('RootModel',) + +RootModelRootType = TypeVar('RootModelRootType') + + +class RootModel(BaseModel, Generic[RootModelRootType], metaclass=_RootModelMetaclass): + """!!! abstract "Usage Documentation" + [`RootModel` and Custom Root Types](../concepts/models.md#rootmodel-and-custom-root-types) + + A Pydantic `BaseModel` for the root object of the model. + + Attributes: + root: The root object of the model. + __pydantic_root_model__: Whether the model is a RootModel. + __pydantic_private__: Private fields in the model. + __pydantic_extra__: Extra fields in the model. + + """ + + __pydantic_root_model__ = True + __pydantic_private__ = None + __pydantic_extra__ = None + + root: RootModelRootType + + def __init_subclass__(cls, **kwargs): + extra = cls.model_config.get('extra') + if extra is not None: + raise PydanticUserError( + "`RootModel` does not support setting `model_config['extra']`", code='root-model-extra' + ) + super().__init_subclass__(**kwargs) + + def __init__(self, /, root: RootModelRootType = PydanticUndefined, **data) -> None: # type: ignore + __tracebackhide__ = True + if data: + if root is not PydanticUndefined: + raise ValueError( + '"RootModel.__init__" accepts either a single positional argument or arbitrary keyword arguments' + ) + root = data # type: ignore + self.__pydantic_validator__.validate_python(root, self_instance=self) + + __init__.__pydantic_base_init__ = True # pyright: ignore[reportFunctionMemberAccess] + + @classmethod + def model_construct(cls, root: RootModelRootType, _fields_set: set[str] | None = None) -> Self: # type: ignore + """Create a new model using the provided root object and update fields set. + + Args: + root: The root object of the model. + _fields_set: The set of fields to be updated. + + Returns: + The new model. + + Raises: + NotImplemented: If the model is not a subclass of `RootModel`. + """ + return super().model_construct(root=root, _fields_set=_fields_set) + + def __getstate__(self) -> dict[Any, Any]: + return { + '__dict__': self.__dict__, + '__pydantic_fields_set__': self.__pydantic_fields_set__, + } + + def __setstate__(self, state: dict[Any, Any]) -> None: + _object_setattr(self, '__pydantic_fields_set__', state['__pydantic_fields_set__']) + _object_setattr(self, '__dict__', state['__dict__']) + + def __copy__(self) -> Self: + """Returns a shallow copy of the model.""" + cls = type(self) + m = cls.__new__(cls) + new_dict = copy(self.__dict__) + new_dict['root'] = copy(self.__dict__['root']) + _object_setattr(m, '__dict__', new_dict) + _object_setattr(m, '__pydantic_fields_set__', copy(self.__pydantic_fields_set__)) + return m + + def __deepcopy__(self, memo: dict[int, Any] | None = None) -> Self: + """Returns a deep copy of the model.""" + cls = type(self) + m = cls.__new__(cls) + _object_setattr(m, '__dict__', deepcopy(self.__dict__, memo=memo)) + # This next line doesn't need a deepcopy because __pydantic_fields_set__ is a set[str], + # and attempting a deepcopy would be marginally slower. + _object_setattr(m, '__pydantic_fields_set__', copy(self.__pydantic_fields_set__)) + return m + + if TYPE_CHECKING: + + def model_dump( # type: ignore + self, + *, + mode: Literal['json', 'python'] | str = 'python', + include: Any = None, + exclude: Any = None, + context: dict[str, Any] | None = None, + by_alias: bool | None = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + exclude_computed_fields: bool = False, + round_trip: bool = False, + warnings: bool | Literal['none', 'warn', 'error'] = True, + serialize_as_any: bool = False, + ) -> Any: + """This method is included just to get a more accurate return type for type checkers. + It is included in this `if TYPE_CHECKING:` block since no override is actually necessary. + + See the documentation of `BaseModel.model_dump` for more details about the arguments. + + Generally, this method will have a return type of `RootModelRootType`, assuming that `RootModelRootType` is + not a `BaseModel` subclass. If `RootModelRootType` is a `BaseModel` subclass, then the return + type will likely be `dict[str, Any]`, as `model_dump` calls are recursive. The return type could + even be something different, in the case of a custom serializer. + Thus, `Any` is used here to catch all of these cases. + """ + ... + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, RootModel): + return NotImplemented + return self.__pydantic_fields__['root'].annotation == other.__pydantic_fields__[ + 'root' + ].annotation and super().__eq__(other) + + def __repr_args__(self) -> _repr.ReprArgs: + yield 'root', self.root diff --git a/source/target-vendors/cp312/pydantic/schema.py b/source/target-vendors/cp312/pydantic/schema.py new file mode 100644 index 0000000..a3245a6 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/schema.py @@ -0,0 +1,5 @@ +"""The `schema` module is a backport module from V1.""" + +from ._migration import getattr_migration + +__getattr__ = getattr_migration(__name__) diff --git a/source/target-vendors/cp312/pydantic/tools.py b/source/target-vendors/cp312/pydantic/tools.py new file mode 100644 index 0000000..fdc68c4 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/tools.py @@ -0,0 +1,5 @@ +"""The `tools` module is a backport module from V1.""" + +from ._migration import getattr_migration + +__getattr__ = getattr_migration(__name__) diff --git a/source/target-vendors/cp312/pydantic/type_adapter.py b/source/target-vendors/cp312/pydantic/type_adapter.py new file mode 100644 index 0000000..d962305 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/type_adapter.py @@ -0,0 +1,801 @@ +"""Type adapter specification.""" + +from __future__ import annotations as _annotations + +import sys +import types +from collections.abc import Callable, Iterable +from dataclasses import is_dataclass +from types import FrameType +from typing import ( + Any, + Generic, + Literal, + TypeVar, + cast, + final, + overload, +) + +from pydantic_core import CoreSchema, SchemaSerializer, SchemaValidator, Some +from typing_extensions import ParamSpec, is_typeddict + +from pydantic.errors import PydanticUserError +from pydantic.main import BaseModel, IncEx + +from ._internal import _config, _generate_schema, _mock_val_ser, _namespace_utils, _repr, _typing_extra, _utils +from .config import ConfigDict, ExtraValues +from .errors import PydanticUndefinedAnnotation +from .json_schema import ( + DEFAULT_REF_TEMPLATE, + GenerateJsonSchema, + JsonSchemaKeyT, + JsonSchemaMode, + JsonSchemaValue, +) +from .plugin._schema_validator import PluggableSchemaValidator, create_schema_validator + +T = TypeVar('T') +R = TypeVar('R') +P = ParamSpec('P') +TypeAdapterT = TypeVar('TypeAdapterT', bound='TypeAdapter') + + +def _getattr_no_parents(obj: Any, attribute: str) -> Any: + """Returns the attribute value without attempting to look up attributes from parent types.""" + if hasattr(obj, '__dict__'): + try: + return obj.__dict__[attribute] + except KeyError: + pass + + slots = getattr(obj, '__slots__', None) + if slots is not None and attribute in slots: + return getattr(obj, attribute) + else: + raise AttributeError(attribute) + + +def _type_has_config(type_: Any) -> bool: + """Returns whether the type has config.""" + type_ = _typing_extra.annotated_type(type_) or type_ + try: + return issubclass(type_, BaseModel) or is_dataclass(type_) or is_typeddict(type_) + except TypeError: + # type is not a class + return False + + +@final +class TypeAdapter(Generic[T]): + """!!! abstract "Usage Documentation" + [`TypeAdapter`](../concepts/type_adapter.md) + + Type adapters provide a flexible way to perform validation and serialization based on a Python type. + + A `TypeAdapter` instance exposes some of the functionality from `BaseModel` instance methods + for types that do not have such methods (such as dataclasses, primitive types, and more). + + **Note:** `TypeAdapter` instances are not types, and cannot be used as type annotations for fields. + + Args: + type: The type associated with the `TypeAdapter`. + config: Configuration for the `TypeAdapter`, should be a dictionary conforming to + [`ConfigDict`][pydantic.config.ConfigDict]. + + !!! note + You cannot provide a configuration when instantiating a `TypeAdapter` if the type you're using + has its own config that cannot be overridden (ex: `BaseModel`, `TypedDict`, and `dataclass`). A + [`type-adapter-config-unused`](../errors/usage_errors.md#type-adapter-config-unused) error will + be raised in this case. + _parent_depth: Depth at which to search for the [parent frame][frame-objects]. This frame is used when + resolving forward annotations during schema building, by looking for the globals and locals of this + frame. Defaults to 2, which will result in the frame where the `TypeAdapter` was instantiated. + + !!! note + This parameter is named with an underscore to suggest its private nature and discourage use. + It may be deprecated in a minor version, so we only recommend using it if you're comfortable + with potential change in behavior/support. It's default value is 2 because internally, + the `TypeAdapter` class makes another call to fetch the frame. + module: The module that passes to plugin if provided. + + Attributes: + core_schema: The core schema for the type. + validator: The schema validator for the type. + serializer: The schema serializer for the type. + pydantic_complete: Whether the core schema for the type is successfully built. + + ??? tip "Compatibility with `mypy`" + Depending on the type used, `mypy` might raise an error when instantiating a `TypeAdapter`. As a workaround, you can explicitly + annotate your variable: + + ```py + from typing import Union + + from pydantic import TypeAdapter + + ta: TypeAdapter[Union[str, int]] = TypeAdapter(Union[str, int]) # type: ignore[arg-type] + ``` + + ??? info "Namespace management nuances and implementation details" + + Here, we collect some notes on namespace management, and subtle differences from `BaseModel`: + + `BaseModel` uses its own `__module__` to find out where it was defined + and then looks for symbols to resolve forward references in those globals. + On the other hand, `TypeAdapter` can be initialized with arbitrary objects, + which may not be types and thus do not have a `__module__` available. + So instead we look at the globals in our parent stack frame. + + It is expected that the `ns_resolver` passed to this function will have the correct + namespace for the type we're adapting. See the source code for `TypeAdapter.__init__` + and `TypeAdapter.rebuild` for various ways to construct this namespace. + + This works for the case where this function is called in a module that + has the target of forward references in its scope, but + does not always work for more complex cases. + + For example, take the following: + + ```python {title="a.py"} + IntList = list[int] + OuterDict = dict[str, 'IntList'] + ``` + + ```python {test="skip" title="b.py"} + from a import OuterDict + + from pydantic import TypeAdapter + + IntList = int # replaces the symbol the forward reference is looking for + v = TypeAdapter(OuterDict) + v({'x': 1}) # should fail but doesn't + ``` + + If `OuterDict` were a `BaseModel`, this would work because it would resolve + the forward reference within the `a.py` namespace. + But `TypeAdapter(OuterDict)` can't determine what module `OuterDict` came from. + + In other words, the assumption that _all_ forward references exist in the + module we are being called from is not technically always true. + Although most of the time it is and it works fine for recursive models and such, + `BaseModel`'s behavior isn't perfect either and _can_ break in similar ways, + so there is no right or wrong between the two. + + But at the very least this behavior is _subtly_ different from `BaseModel`'s. + """ + + core_schema: CoreSchema + validator: SchemaValidator | PluggableSchemaValidator + serializer: SchemaSerializer + pydantic_complete: bool + + @overload + def __init__( + self, + type: type[T], + *, + config: ConfigDict | None = ..., + _parent_depth: int = ..., + module: str | None = ..., + ) -> None: ... + + # This second overload is for unsupported special forms (such as Annotated, Union, etc.) + # Currently there is no way to type this correctly + # See https://github.com/python/typing/pull/1618 + @overload + def __init__( + self, + type: Any, + *, + config: ConfigDict | None = ..., + _parent_depth: int = ..., + module: str | None = ..., + ) -> None: ... + + def __init__( + self, + type: Any, + *, + config: ConfigDict | None = None, + _parent_depth: int = 2, + module: str | None = None, + ) -> None: + if _type_has_config(type) and config is not None: + raise PydanticUserError( + 'Cannot use `config` when the type is a BaseModel, dataclass or TypedDict.' + ' These types can have their own config and setting the config via the `config`' + ' parameter to TypeAdapter will not override it, thus the `config` you passed to' + ' TypeAdapter becomes meaningless, which is probably not what you want.', + code='type-adapter-config-unused', + ) + + self._type = type + self._config = config + self._parent_depth = _parent_depth + self.pydantic_complete = False + + parent_frame = self._fetch_parent_frame() + if isinstance(type, types.FunctionType): + # Special case functions, which are *not* pushed to the `NsResolver` stack and without this special case + # would only have access to the parent namespace where the `TypeAdapter` was instantiated (if the function is defined + # in another module, we need to look at that module's globals). + if parent_frame is not None: + # `f_locals` is the namespace where the type adapter was instantiated (~ to `f_globals` if at the module level): + parent_ns = parent_frame.f_locals + else: # pragma: no cover + parent_ns = None + globalns, localns = _namespace_utils.ns_for_function( + type, + parent_namespace=parent_ns, + ) + parent_namespace = None + else: + if parent_frame is not None: + globalns = parent_frame.f_globals + # Do not provide a local ns if the type adapter happens to be instantiated at the module level: + localns = parent_frame.f_locals if parent_frame.f_locals is not globalns else {} + else: # pragma: no cover + globalns = {} + localns = {} + parent_namespace = localns + + self._module_name = module or cast(str, globalns.get('__name__', '')) + self._init_core_attrs( + ns_resolver=_namespace_utils.NsResolver( + namespaces_tuple=_namespace_utils.NamespacesTuple(locals=localns, globals=globalns), + parent_namespace=parent_namespace, + ), + force=False, + ) + + def _fetch_parent_frame(self) -> FrameType | None: + frame = sys._getframe(self._parent_depth) + if frame.f_globals.get('__name__') == 'typing': + # Because `TypeAdapter` is generic, explicitly parametrizing the class results + # in a `typing._GenericAlias` instance, which proxies instantiation calls to the + # "real" `TypeAdapter` class and thus adding an extra frame to the call. To avoid + # pulling anything from the `typing` module, use the correct frame (the one before): + return frame.f_back + + return frame + + def _init_core_attrs( + self, ns_resolver: _namespace_utils.NsResolver, force: bool, raise_errors: bool = False + ) -> bool: + """Initialize the core schema, validator, and serializer for the type. + + Args: + ns_resolver: The namespace resolver to use when building the core schema for the adapted type. + force: Whether to force the construction of the core schema, validator, and serializer. + If `force` is set to `False` and `_defer_build` is `True`, the core schema, validator, and serializer will be set to mocks. + raise_errors: Whether to raise errors if initializing any of the core attrs fails. + + Returns: + `True` if the core schema, validator, and serializer were successfully initialized, otherwise `False`. + + Raises: + PydanticUndefinedAnnotation: If `PydanticUndefinedAnnotation` occurs in`__get_pydantic_core_schema__` + and `raise_errors=True`. + """ + if not force and self._defer_build: + _mock_val_ser.set_type_adapter_mocks(self) + self.pydantic_complete = False + return False + + try: + self.core_schema = _getattr_no_parents(self._type, '__pydantic_core_schema__') + self.validator = _getattr_no_parents(self._type, '__pydantic_validator__') + self.serializer = _getattr_no_parents(self._type, '__pydantic_serializer__') + + # TODO: we don't go through the rebuild logic here directly because we don't want + # to repeat all of the namespace fetching logic that we've already done + # so we simply skip to the block below that does the actual schema generation + if ( + isinstance(self.core_schema, _mock_val_ser.MockCoreSchema) + or isinstance(self.validator, _mock_val_ser.MockValSer) + or isinstance(self.serializer, _mock_val_ser.MockValSer) + ): + raise AttributeError() + except AttributeError: + config_wrapper = _config.ConfigWrapper(self._config) + + schema_generator = _generate_schema.GenerateSchema(config_wrapper, ns_resolver=ns_resolver) + + try: + core_schema = schema_generator.generate_schema(self._type) + except PydanticUndefinedAnnotation: + if raise_errors: + raise + _mock_val_ser.set_type_adapter_mocks(self) + return False + + try: + self.core_schema = schema_generator.clean_schema(core_schema) + except _generate_schema.InvalidSchemaError: + _mock_val_ser.set_type_adapter_mocks(self) + return False + + core_config = config_wrapper.core_config(None) + + self.validator = create_schema_validator( + schema=self.core_schema, + schema_type=self._type, + schema_type_module=self._module_name, + schema_type_name=str(self._type), + schema_kind='TypeAdapter', + config=core_config, + plugin_settings=config_wrapper.plugin_settings, + ) + self.serializer = SchemaSerializer(self.core_schema, core_config) + + self.pydantic_complete = True + return True + + @property + def _defer_build(self) -> bool: + config = self._config if self._config is not None else self._model_config + if config: + return config.get('defer_build') is True + return False + + @property + def _model_config(self) -> ConfigDict | None: + type_: Any = _typing_extra.annotated_type(self._type) or self._type # Eg FastAPI heavily uses Annotated + if _utils.lenient_issubclass(type_, BaseModel): + return type_.model_config + return getattr(type_, '__pydantic_config__', None) + + def __repr__(self) -> str: + return f'TypeAdapter({_repr.display_as_type(self._type)})' + + def rebuild( + self, + *, + force: bool = False, + raise_errors: bool = True, + _parent_namespace_depth: int = 2, + _types_namespace: _namespace_utils.MappingNamespace | None = None, + ) -> bool | None: + """Try to rebuild the pydantic-core schema for the adapter's type. + + This may be necessary when one of the annotations is a ForwardRef which could not be resolved during + the initial attempt to build the schema, and automatic rebuilding fails. + + Args: + force: Whether to force the rebuilding of the type adapter's schema, defaults to `False`. + raise_errors: Whether to raise errors, defaults to `True`. + _parent_namespace_depth: Depth at which to search for the [parent frame][frame-objects]. This + frame is used when resolving forward annotations during schema rebuilding, by looking for + the locals of this frame. Defaults to 2, which will result in the frame where the method + was called. + _types_namespace: An explicit types namespace to use, instead of using the local namespace + from the parent frame. Defaults to `None`. + + Returns: + Returns `None` if the schema is already "complete" and rebuilding was not required. + If rebuilding _was_ required, returns `True` if rebuilding was successful, otherwise `False`. + """ + if not force and self.pydantic_complete: + return None + + if _types_namespace is not None: + rebuild_ns = _types_namespace + elif _parent_namespace_depth > 0: + rebuild_ns = _typing_extra.parent_frame_namespace(parent_depth=_parent_namespace_depth, force=True) or {} + else: + rebuild_ns = {} + + # we have to manually fetch globals here because there's no type on the stack of the NsResolver + # and so we skip the globalns = get_module_ns_of(typ) call that would normally happen + globalns = sys._getframe(max(_parent_namespace_depth - 1, 1)).f_globals + ns_resolver = _namespace_utils.NsResolver( + namespaces_tuple=_namespace_utils.NamespacesTuple(locals=rebuild_ns, globals=globalns), + parent_namespace=rebuild_ns, + ) + return self._init_core_attrs(ns_resolver=ns_resolver, force=True, raise_errors=raise_errors) + + def validate_python( + self, + object: Any, + /, + *, + strict: bool | None = None, + extra: ExtraValues | None = None, + from_attributes: bool | None = None, + context: Any | None = None, + experimental_allow_partial: bool | Literal['off', 'on', 'trailing-strings'] = False, + by_alias: bool | None = None, + by_name: bool | None = None, + ) -> T: + """Validate a Python object against the model. + + Args: + object: The Python object to validate against the model. + strict: Whether to strictly check types. + extra: Whether to ignore, allow, or forbid extra data during model validation. + See the [`extra` configuration value][pydantic.ConfigDict.extra] for details. + from_attributes: Whether to extract data from object attributes. + context: Additional context to pass to the validator. + experimental_allow_partial: **Experimental** whether to enable + [partial validation](../concepts/experimental.md#partial-validation), e.g. to process streams. + * False / 'off': Default behavior, no partial validation. + * True / 'on': Enable partial validation. + * 'trailing-strings': Enable partial validation and allow trailing strings in the input. + by_alias: Whether to use the field's alias when validating against the provided input data. + by_name: Whether to use the field's name when validating against the provided input data. + + !!! note + When using `TypeAdapter` with a Pydantic `dataclass`, the use of the `from_attributes` + argument is not supported. + + Returns: + The validated object. + """ + if by_alias is False and by_name is not True: + raise PydanticUserError( + 'At least one of `by_alias` or `by_name` must be set to True.', + code='validate-by-alias-and-name-false', + ) + + return self.validator.validate_python( + object, + strict=strict, + extra=extra, + from_attributes=from_attributes, + context=context, + allow_partial=experimental_allow_partial, + by_alias=by_alias, + by_name=by_name, + ) + + def validate_json( + self, + data: str | bytes | bytearray, + /, + *, + strict: bool | None = None, + extra: ExtraValues | None = None, + context: Any | None = None, + experimental_allow_partial: bool | Literal['off', 'on', 'trailing-strings'] = False, + by_alias: bool | None = None, + by_name: bool | None = None, + ) -> T: + """!!! abstract "Usage Documentation" + [JSON Parsing](../concepts/json.md#json-parsing) + + Validate a JSON string or bytes against the model. + + Args: + data: The JSON data to validate against the model. + strict: Whether to strictly check types. + extra: Whether to ignore, allow, or forbid extra data during model validation. + See the [`extra` configuration value][pydantic.ConfigDict.extra] for details. + context: Additional context to use during validation. + experimental_allow_partial: **Experimental** whether to enable + [partial validation](../concepts/experimental.md#partial-validation), e.g. to process streams. + * False / 'off': Default behavior, no partial validation. + * True / 'on': Enable partial validation. + * 'trailing-strings': Enable partial validation and allow trailing strings in the input. + by_alias: Whether to use the field's alias when validating against the provided input data. + by_name: Whether to use the field's name when validating against the provided input data. + + Returns: + The validated object. + """ + if by_alias is False and by_name is not True: + raise PydanticUserError( + 'At least one of `by_alias` or `by_name` must be set to True.', + code='validate-by-alias-and-name-false', + ) + + return self.validator.validate_json( + data, + strict=strict, + extra=extra, + context=context, + allow_partial=experimental_allow_partial, + by_alias=by_alias, + by_name=by_name, + ) + + def validate_strings( + self, + obj: Any, + /, + *, + strict: bool | None = None, + extra: ExtraValues | None = None, + context: Any | None = None, + experimental_allow_partial: bool | Literal['off', 'on', 'trailing-strings'] = False, + by_alias: bool | None = None, + by_name: bool | None = None, + ) -> T: + """Validate object contains string data against the model. + + Args: + obj: The object contains string data to validate. + strict: Whether to strictly check types. + extra: Whether to ignore, allow, or forbid extra data during model validation. + See the [`extra` configuration value][pydantic.ConfigDict.extra] for details. + context: Additional context to use during validation. + experimental_allow_partial: **Experimental** whether to enable + [partial validation](../concepts/experimental.md#partial-validation), e.g. to process streams. + * False / 'off': Default behavior, no partial validation. + * True / 'on': Enable partial validation. + * 'trailing-strings': Enable partial validation and allow trailing strings in the input. + by_alias: Whether to use the field's alias when validating against the provided input data. + by_name: Whether to use the field's name when validating against the provided input data. + + Returns: + The validated object. + """ + if by_alias is False and by_name is not True: + raise PydanticUserError( + 'At least one of `by_alias` or `by_name` must be set to True.', + code='validate-by-alias-and-name-false', + ) + + return self.validator.validate_strings( + obj, + strict=strict, + extra=extra, + context=context, + allow_partial=experimental_allow_partial, + by_alias=by_alias, + by_name=by_name, + ) + + def get_default_value(self, *, strict: bool | None = None, context: Any | None = None) -> Some[T] | None: + """Get the default value for the wrapped type. + + Args: + strict: Whether to strictly check types. + context: Additional context to pass to the validator. + + Returns: + The default value wrapped in a `Some` if there is one or None if not. + """ + return self.validator.get_default_value(strict=strict, context=context) + + def dump_python( + self, + instance: T, + /, + *, + mode: Literal['json', 'python'] = 'python', + include: IncEx | None = None, + exclude: IncEx | None = None, + by_alias: bool | None = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + exclude_computed_fields: bool = False, + round_trip: bool = False, + warnings: bool | Literal['none', 'warn', 'error'] = True, + fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, + polymorphic_serialization: bool | None = None, + context: Any | None = None, + ) -> Any: + """Dump an instance of the adapted type to a Python object. + + Args: + instance: The Python object to serialize. + mode: The output format. + include: Fields to include in the output. + exclude: Fields to exclude from the output. + by_alias: Whether to use alias names for field names. + exclude_unset: Whether to exclude unset fields. + exclude_defaults: Whether to exclude fields with default values. + exclude_none: Whether to exclude fields with None values. + exclude_computed_fields: Whether to exclude computed fields. + While this can be useful for round-tripping, it is usually recommended to use the dedicated + `round_trip` parameter instead. + round_trip: Whether to output the serialized data in a way that is compatible with deserialization. + warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors, + "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError]. + fallback: A function to call when an unknown value is encountered. If not provided, + a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised. + serialize_as_any: Whether to serialize fields with duck-typing serialization behavior. + polymorphic_serialization: Whether to use model and dataclass polymorphic serialization for this call. + context: Additional context to pass to the serializer. + + Returns: + The serialized object. + """ + return self.serializer.to_python( + instance, + mode=mode, + by_alias=by_alias, + include=include, + exclude=exclude, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + exclude_computed_fields=exclude_computed_fields, + round_trip=round_trip, + warnings=warnings, + fallback=fallback, + serialize_as_any=serialize_as_any, + polymorphic_serialization=polymorphic_serialization, + context=context, + ) + + def dump_json( + self, + instance: T, + /, + *, + indent: int | None = None, + ensure_ascii: bool = False, + include: IncEx | None = None, + exclude: IncEx | None = None, + by_alias: bool | None = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + exclude_computed_fields: bool = False, + round_trip: bool = False, + warnings: bool | Literal['none', 'warn', 'error'] = True, + fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, + polymorphic_serialization: bool | None = None, + context: Any | None = None, + ) -> bytes: + """!!! abstract "Usage Documentation" + [JSON Serialization](../concepts/json.md#json-serialization) + + Serialize an instance of the adapted type to JSON. + + Args: + instance: The instance to be serialized. + indent: Number of spaces for JSON indentation. + ensure_ascii: If `True`, the output is guaranteed to have all incoming non-ASCII characters escaped. + If `False` (the default), these characters will be output as-is. + include: Fields to include. + exclude: Fields to exclude. + by_alias: Whether to use alias names for field names. + exclude_unset: Whether to exclude unset fields. + exclude_defaults: Whether to exclude fields with default values. + exclude_none: Whether to exclude fields with a value of `None`. + exclude_computed_fields: Whether to exclude computed fields. + While this can be useful for round-tripping, it is usually recommended to use the dedicated + `round_trip` parameter instead. + round_trip: Whether to serialize and deserialize the instance to ensure round-tripping. + warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors, + "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError]. + fallback: A function to call when an unknown value is encountered. If not provided, + a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised. + serialize_as_any: Whether to serialize fields with duck-typing serialization behavior. + polymorphic_serialization: Whether to use model and dataclass polymorphic serialization for this call. + context: Additional context to pass to the serializer. + + Returns: + The JSON representation of the given instance as bytes. + """ + return self.serializer.to_json( + instance, + indent=indent, + ensure_ascii=ensure_ascii, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + exclude_computed_fields=exclude_computed_fields, + round_trip=round_trip, + warnings=warnings, + fallback=fallback, + serialize_as_any=serialize_as_any, + polymorphic_serialization=polymorphic_serialization, + context=context, + ) + + def json_schema( + self, + *, + by_alias: bool = True, + ref_template: str = DEFAULT_REF_TEMPLATE, + union_format: Literal['any_of', 'primitive_type_array'] = 'any_of', + schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema, + mode: JsonSchemaMode = 'validation', + ) -> dict[str, Any]: + """Generate a JSON schema for the adapted type. + + Args: + by_alias: Whether to use alias names for field names. + ref_template: The format string used for generating $ref strings. + union_format: The format to use when combining schemas from unions together. Can be one of: + + - `'any_of'`: Use the [`anyOf`](https://json-schema.org/understanding-json-schema/reference/combining#anyOf) + keyword to combine schemas (the default). + - `'primitive_type_array'`: Use the [`type`](https://json-schema.org/understanding-json-schema/reference/type) + keyword as an array of strings, containing each type of the combination. If any of the schemas is not a primitive + type (`string`, `boolean`, `null`, `integer` or `number`) or contains constraints/metadata, falls back to + `any_of`. + schema_generator: To override the logic used to generate the JSON schema, as a subclass of + `GenerateJsonSchema` with your desired modifications + mode: The mode in which to generate the schema. + schema_generator: The generator class used for creating the schema. + mode: The mode to use for schema generation. + + Returns: + The JSON schema for the model as a dictionary. + """ + schema_generator_instance = schema_generator( + by_alias=by_alias, ref_template=ref_template, union_format=union_format + ) + if isinstance(self.core_schema, _mock_val_ser.MockCoreSchema): + self.core_schema.rebuild() + assert not isinstance(self.core_schema, _mock_val_ser.MockCoreSchema), 'this is a bug! please report it' + return schema_generator_instance.generate(self.core_schema, mode=mode) + + @staticmethod + def json_schemas( + inputs: Iterable[tuple[JsonSchemaKeyT, JsonSchemaMode, TypeAdapter[Any]]], + /, + *, + by_alias: bool = True, + title: str | None = None, + description: str | None = None, + ref_template: str = DEFAULT_REF_TEMPLATE, + union_format: Literal['any_of', 'primitive_type_array'] = 'any_of', + schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema, + ) -> tuple[dict[tuple[JsonSchemaKeyT, JsonSchemaMode], JsonSchemaValue], JsonSchemaValue]: + """Generate a JSON schema including definitions from multiple type adapters. + + Args: + inputs: Inputs to schema generation. The first two items will form the keys of the (first) + output mapping; the type adapters will provide the core schemas that get converted into + definitions in the output JSON schema. + by_alias: Whether to use alias names. + title: The title for the schema. + description: The description for the schema. + ref_template: The format string used for generating $ref strings. + union_format: The format to use when combining schemas from unions together. Can be one of: + + - `'any_of'`: Use the [`anyOf`](https://json-schema.org/understanding-json-schema/reference/combining#anyOf) + keyword to combine schemas (the default). + - `'primitive_type_array'`: Use the [`type`](https://json-schema.org/understanding-json-schema/reference/type) + keyword as an array of strings, containing each type of the combination. If any of the schemas is not a primitive + type (`string`, `boolean`, `null`, `integer` or `number`) or contains constraints/metadata, falls back to + `any_of`. + schema_generator: The generator class used for creating the schema. + + Returns: + A tuple where: + + - The first element is a dictionary whose keys are tuples of JSON schema key type and JSON mode, and + whose values are the JSON schema corresponding to that pair of inputs. (These schemas may have + JsonRef references to definitions that are defined in the second returned element.) + - The second element is a JSON schema containing all definitions referenced in the first returned + element, along with the optional title and description keys. + + """ + schema_generator_instance = schema_generator( + by_alias=by_alias, ref_template=ref_template, union_format=union_format + ) + + inputs_ = [] + for key, mode, adapter in inputs: + # This is the same pattern we follow for model json schemas - we attempt a core schema rebuild if we detect a mock + if isinstance(adapter.core_schema, _mock_val_ser.MockCoreSchema): + adapter.core_schema.rebuild() + assert not isinstance(adapter.core_schema, _mock_val_ser.MockCoreSchema), ( + 'this is a bug! please report it' + ) + inputs_.append((key, mode, adapter.core_schema)) + + json_schemas_map, definitions = schema_generator_instance.generate_definitions(inputs_) + + json_schema: dict[str, Any] = {} + if definitions: + json_schema['$defs'] = definitions + if title: + json_schema['title'] = title + if description: + json_schema['description'] = description + + return json_schemas_map, json_schema diff --git a/source/target-vendors/cp312/pydantic/types.py b/source/target-vendors/cp312/pydantic/types.py new file mode 100644 index 0000000..467bc82 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/types.py @@ -0,0 +1,3310 @@ +"""The types module contains custom types used by pydantic.""" + +from __future__ import annotations as _annotations + +import base64 +import dataclasses as _dataclasses +import re +from collections.abc import Hashable, Iterator +from datetime import date, datetime +from decimal import Decimal +from enum import Enum +from pathlib import Path +from re import Pattern +from types import ModuleType +from typing import ( + TYPE_CHECKING, + Annotated, + Any, + Callable, + ClassVar, + Generic, + Literal, + TypeVar, + Union, + cast, +) +from uuid import UUID + +import annotated_types +from annotated_types import BaseMetadata, MaxLen, MinLen +from pydantic_core import CoreSchema, PydanticCustomError, SchemaSerializer, core_schema +from typing_extensions import Protocol, TypeAlias, TypeAliasType, deprecated, get_args, get_origin + +from ._internal import _fields, _internal_dataclass, _utils, _validators +from ._migration import getattr_migration +from .annotated_handlers import GetCoreSchemaHandler, GetJsonSchemaHandler +from .errors import PydanticUserError +from .json_schema import JsonSchemaValue +from .warnings import PydanticDeprecatedSince20 + +if TYPE_CHECKING: + from ._internal._core_metadata import CoreMetadata + +__all__ = ( + 'Strict', + 'StrictStr', + 'SocketPath', + 'conbytes', + 'conlist', + 'conset', + 'confrozenset', + 'constr', + 'ImportString', + 'conint', + 'PositiveInt', + 'NegativeInt', + 'NonNegativeInt', + 'NonPositiveInt', + 'confloat', + 'PositiveFloat', + 'NegativeFloat', + 'NonNegativeFloat', + 'NonPositiveFloat', + 'FiniteFloat', + 'condecimal', + 'UUID1', + 'UUID3', + 'UUID4', + 'UUID5', + 'UUID6', + 'UUID7', + 'UUID8', + 'FilePath', + 'DirectoryPath', + 'NewPath', + 'Json', + 'Secret', + 'SecretStr', + 'SecretBytes', + 'StrictBool', + 'StrictBytes', + 'StrictInt', + 'StrictFloat', + 'PaymentCardNumber', + 'ByteSize', + 'PastDate', + 'FutureDate', + 'PastDatetime', + 'FutureDatetime', + 'condate', + 'AwareDatetime', + 'NaiveDatetime', + 'AllowInfNan', + 'EncoderProtocol', + 'EncodedBytes', + 'EncodedStr', + 'Base64Encoder', + 'Base64Bytes', + 'Base64Str', + 'Base64UrlBytes', + 'Base64UrlStr', + 'GetPydanticSchema', + 'StringConstraints', + 'Tag', + 'Discriminator', + 'JsonValue', + 'OnErrorOmit', + 'FailFast', +) + + +T = TypeVar('T') + + +@_dataclasses.dataclass +class Strict(_fields.PydanticMetadata, BaseMetadata): + """!!! abstract "Usage Documentation" + [Strict Mode with `Annotated` `Strict`](../concepts/strict_mode.md#strict-mode-with-annotated-strict) + + A field metadata class to indicate that a field should be validated in strict mode. + Use this class as an annotation via [`Annotated`](https://docs.python.org/3/library/typing.html#typing.Annotated), as seen below. + + Attributes: + strict: Whether to validate the field in strict mode. + + Example: + ```python + from typing import Annotated + + from pydantic.types import Strict + + StrictBool = Annotated[bool, Strict()] + ``` + """ + + strict: bool = True + + def __hash__(self) -> int: + return hash(self.strict) + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ BOOLEAN TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +StrictBool = Annotated[bool, Strict()] +"""A boolean that must be either ``True`` or ``False``.""" + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ INTEGER TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +def conint( + *, + strict: bool | None = None, + gt: int | None = None, + ge: int | None = None, + lt: int | None = None, + le: int | None = None, + multiple_of: int | None = None, +) -> type[int]: + """ + !!! warning "Discouraged" + This function is **discouraged** in favor of using + [`Annotated`](https://docs.python.org/3/library/typing.html#typing.Annotated) with + [`Field`][pydantic.fields.Field] instead. + + This function will be **deprecated** in Pydantic 3.0. + + The reason is that `conint` returns a type, which doesn't play well with static analysis tools. + + === ":x: Don't do this" + ```python + from pydantic import BaseModel, conint + + class Foo(BaseModel): + bar: conint(strict=True, gt=0) + ``` + + === ":white_check_mark: Do this" + ```python + from typing import Annotated + + from pydantic import BaseModel, Field + + class Foo(BaseModel): + bar: Annotated[int, Field(strict=True, gt=0)] + ``` + + A wrapper around `int` that allows for additional constraints. + + Args: + strict: Whether to validate the integer in strict mode. Defaults to `None`. + gt: The value must be greater than this. + ge: The value must be greater than or equal to this. + lt: The value must be less than this. + le: The value must be less than or equal to this. + multiple_of: The value must be a multiple of this. + + Returns: + The wrapped integer type. + + ```python + from pydantic import BaseModel, ValidationError, conint + + class ConstrainedExample(BaseModel): + constrained_int: conint(gt=1) + + m = ConstrainedExample(constrained_int=2) + print(repr(m)) + #> ConstrainedExample(constrained_int=2) + + try: + ConstrainedExample(constrained_int=0) + except ValidationError as e: + print(e.errors()) + ''' + [ + { + 'type': 'greater_than', + 'loc': ('constrained_int',), + 'msg': 'Input should be greater than 1', + 'input': 0, + 'ctx': {'gt': 1}, + 'url': 'https://errors.pydantic.dev/2/v/greater_than', + } + ] + ''' + ``` + + """ # noqa: D212 + return Annotated[ # pyright: ignore[reportReturnType] + int, + Strict(strict) if strict is not None else None, + annotated_types.Interval(gt=gt, ge=ge, lt=lt, le=le), + annotated_types.MultipleOf(multiple_of) if multiple_of is not None else None, + ] + + +PositiveInt = Annotated[int, annotated_types.Gt(0)] +"""An integer that must be greater than zero. + +```python +from pydantic import BaseModel, PositiveInt, ValidationError + +class Model(BaseModel): + positive_int: PositiveInt + +m = Model(positive_int=1) +print(repr(m)) +#> Model(positive_int=1) + +try: + Model(positive_int=-1) +except ValidationError as e: + print(e.errors()) + ''' + [ + { + 'type': 'greater_than', + 'loc': ('positive_int',), + 'msg': 'Input should be greater than 0', + 'input': -1, + 'ctx': {'gt': 0}, + 'url': 'https://errors.pydantic.dev/2/v/greater_than', + } + ] + ''' +``` +""" +NegativeInt = Annotated[int, annotated_types.Lt(0)] +"""An integer that must be less than zero. + +```python +from pydantic import BaseModel, NegativeInt, ValidationError + +class Model(BaseModel): + negative_int: NegativeInt + +m = Model(negative_int=-1) +print(repr(m)) +#> Model(negative_int=-1) + +try: + Model(negative_int=1) +except ValidationError as e: + print(e.errors()) + ''' + [ + { + 'type': 'less_than', + 'loc': ('negative_int',), + 'msg': 'Input should be less than 0', + 'input': 1, + 'ctx': {'lt': 0}, + 'url': 'https://errors.pydantic.dev/2/v/less_than', + } + ] + ''' +``` +""" +NonPositiveInt = Annotated[int, annotated_types.Le(0)] +"""An integer that must be less than or equal to zero. + +```python +from pydantic import BaseModel, NonPositiveInt, ValidationError + +class Model(BaseModel): + non_positive_int: NonPositiveInt + +m = Model(non_positive_int=0) +print(repr(m)) +#> Model(non_positive_int=0) + +try: + Model(non_positive_int=1) +except ValidationError as e: + print(e.errors()) + ''' + [ + { + 'type': 'less_than_equal', + 'loc': ('non_positive_int',), + 'msg': 'Input should be less than or equal to 0', + 'input': 1, + 'ctx': {'le': 0}, + 'url': 'https://errors.pydantic.dev/2/v/less_than_equal', + } + ] + ''' +``` +""" +NonNegativeInt = Annotated[int, annotated_types.Ge(0)] +"""An integer that must be greater than or equal to zero. + +```python +from pydantic import BaseModel, NonNegativeInt, ValidationError + +class Model(BaseModel): + non_negative_int: NonNegativeInt + +m = Model(non_negative_int=0) +print(repr(m)) +#> Model(non_negative_int=0) + +try: + Model(non_negative_int=-1) +except ValidationError as e: + print(e.errors()) + ''' + [ + { + 'type': 'greater_than_equal', + 'loc': ('non_negative_int',), + 'msg': 'Input should be greater than or equal to 0', + 'input': -1, + 'ctx': {'ge': 0}, + 'url': 'https://errors.pydantic.dev/2/v/greater_than_equal', + } + ] + ''' +``` +""" +StrictInt = Annotated[int, Strict()] +"""An integer that must be validated in strict mode. + +```python +from pydantic import BaseModel, StrictInt, ValidationError + +class StrictIntModel(BaseModel): + strict_int: StrictInt + +try: + StrictIntModel(strict_int=3.14159) +except ValidationError as e: + print(e) + ''' + 1 validation error for StrictIntModel + strict_int + Input should be a valid integer [type=int_type, input_value=3.14159, input_type=float] + ''' +``` +""" + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ FLOAT TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +@_dataclasses.dataclass +class AllowInfNan(_fields.PydanticMetadata): + """A field metadata class to indicate that a field should allow `-inf`, `inf`, and `nan`. + + Use this class as an annotation via [`Annotated`](https://docs.python.org/3/library/typing.html#typing.Annotated), as seen below. + + Attributes: + allow_inf_nan: Whether to allow `-inf`, `inf`, and `nan`. Defaults to `True`. + + Example: + ```python + from typing import Annotated + + from pydantic.types import AllowInfNan + + LaxFloat = Annotated[float, AllowInfNan()] + ``` + """ + + allow_inf_nan: bool = True + + def __hash__(self) -> int: + return hash(self.allow_inf_nan) + + +def confloat( + *, + strict: bool | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + multiple_of: float | None = None, + allow_inf_nan: bool | None = None, +) -> type[float]: + """ + !!! warning "Discouraged" + This function is **discouraged** in favor of using + [`Annotated`](https://docs.python.org/3/library/typing.html#typing.Annotated) with + [`Field`][pydantic.fields.Field] instead. + + This function will be **deprecated** in Pydantic 3.0. + + The reason is that `confloat` returns a type, which doesn't play well with static analysis tools. + + === ":x: Don't do this" + ```python + from pydantic import BaseModel, confloat + + class Foo(BaseModel): + bar: confloat(strict=True, gt=0) + ``` + + === ":white_check_mark: Do this" + ```python + from typing import Annotated + + from pydantic import BaseModel, Field + + class Foo(BaseModel): + bar: Annotated[float, Field(strict=True, gt=0)] + ``` + + A wrapper around `float` that allows for additional constraints. + + Args: + strict: Whether to validate the float in strict mode. + gt: The value must be greater than this. + ge: The value must be greater than or equal to this. + lt: The value must be less than this. + le: The value must be less than or equal to this. + multiple_of: The value must be a multiple of this. + allow_inf_nan: Whether to allow `-inf`, `inf`, and `nan`. + + Returns: + The wrapped float type. + + ```python + from pydantic import BaseModel, ValidationError, confloat + + class ConstrainedExample(BaseModel): + constrained_float: confloat(gt=1.0) + + m = ConstrainedExample(constrained_float=1.1) + print(repr(m)) + #> ConstrainedExample(constrained_float=1.1) + + try: + ConstrainedExample(constrained_float=0.9) + except ValidationError as e: + print(e.errors()) + ''' + [ + { + 'type': 'greater_than', + 'loc': ('constrained_float',), + 'msg': 'Input should be greater than 1', + 'input': 0.9, + 'ctx': {'gt': 1.0}, + 'url': 'https://errors.pydantic.dev/2/v/greater_than', + } + ] + ''' + ``` + """ # noqa: D212 + return Annotated[ # pyright: ignore[reportReturnType] + float, + Strict(strict) if strict is not None else None, + annotated_types.Interval(gt=gt, ge=ge, lt=lt, le=le), + annotated_types.MultipleOf(multiple_of) if multiple_of is not None else None, + AllowInfNan(allow_inf_nan) if allow_inf_nan is not None else None, + ] + + +PositiveFloat = Annotated[float, annotated_types.Gt(0)] +"""A float that must be greater than zero. + +```python +from pydantic import BaseModel, PositiveFloat, ValidationError + +class Model(BaseModel): + positive_float: PositiveFloat + +m = Model(positive_float=1.0) +print(repr(m)) +#> Model(positive_float=1.0) + +try: + Model(positive_float=-1.0) +except ValidationError as e: + print(e.errors()) + ''' + [ + { + 'type': 'greater_than', + 'loc': ('positive_float',), + 'msg': 'Input should be greater than 0', + 'input': -1.0, + 'ctx': {'gt': 0.0}, + 'url': 'https://errors.pydantic.dev/2/v/greater_than', + } + ] + ''' +``` +""" +NegativeFloat = Annotated[float, annotated_types.Lt(0)] +"""A float that must be less than zero. + +```python +from pydantic import BaseModel, NegativeFloat, ValidationError + +class Model(BaseModel): + negative_float: NegativeFloat + +m = Model(negative_float=-1.0) +print(repr(m)) +#> Model(negative_float=-1.0) + +try: + Model(negative_float=1.0) +except ValidationError as e: + print(e.errors()) + ''' + [ + { + 'type': 'less_than', + 'loc': ('negative_float',), + 'msg': 'Input should be less than 0', + 'input': 1.0, + 'ctx': {'lt': 0.0}, + 'url': 'https://errors.pydantic.dev/2/v/less_than', + } + ] + ''' +``` +""" +NonPositiveFloat = Annotated[float, annotated_types.Le(0)] +"""A float that must be less than or equal to zero. + +```python +from pydantic import BaseModel, NonPositiveFloat, ValidationError + +class Model(BaseModel): + non_positive_float: NonPositiveFloat + +m = Model(non_positive_float=0.0) +print(repr(m)) +#> Model(non_positive_float=0.0) + +try: + Model(non_positive_float=1.0) +except ValidationError as e: + print(e.errors()) + ''' + [ + { + 'type': 'less_than_equal', + 'loc': ('non_positive_float',), + 'msg': 'Input should be less than or equal to 0', + 'input': 1.0, + 'ctx': {'le': 0.0}, + 'url': 'https://errors.pydantic.dev/2/v/less_than_equal', + } + ] + ''' +``` +""" +NonNegativeFloat = Annotated[float, annotated_types.Ge(0)] +"""A float that must be greater than or equal to zero. + +```python +from pydantic import BaseModel, NonNegativeFloat, ValidationError + +class Model(BaseModel): + non_negative_float: NonNegativeFloat + +m = Model(non_negative_float=0.0) +print(repr(m)) +#> Model(non_negative_float=0.0) + +try: + Model(non_negative_float=-1.0) +except ValidationError as e: + print(e.errors()) + ''' + [ + { + 'type': 'greater_than_equal', + 'loc': ('non_negative_float',), + 'msg': 'Input should be greater than or equal to 0', + 'input': -1.0, + 'ctx': {'ge': 0.0}, + 'url': 'https://errors.pydantic.dev/2/v/greater_than_equal', + } + ] + ''' +``` +""" +StrictFloat = Annotated[float, Strict(True)] +"""A float that must be validated in strict mode. + +```python +from pydantic import BaseModel, StrictFloat, ValidationError + +class StrictFloatModel(BaseModel): + strict_float: StrictFloat + +try: + StrictFloatModel(strict_float='1.0') +except ValidationError as e: + print(e) + ''' + 1 validation error for StrictFloatModel + strict_float + Input should be a valid number [type=float_type, input_value='1.0', input_type=str] + ''' +``` +""" +FiniteFloat = Annotated[float, AllowInfNan(False)] +"""A float that must be finite (not ``-inf``, ``inf``, or ``nan``). + +```python +from pydantic import BaseModel, FiniteFloat + +class Model(BaseModel): + finite: FiniteFloat + +m = Model(finite=1.0) +print(m) +#> finite=1.0 +``` +""" + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ BYTES TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +def conbytes( + *, + min_length: int | None = None, + max_length: int | None = None, + strict: bool | None = None, +) -> type[bytes]: + """A wrapper around `bytes` that allows for additional constraints. + + Args: + min_length: The minimum length of the bytes. + max_length: The maximum length of the bytes. + strict: Whether to validate the bytes in strict mode. + + Returns: + The wrapped bytes type. + """ + return Annotated[ # pyright: ignore[reportReturnType] + bytes, + Strict(strict) if strict is not None else None, + annotated_types.Len(min_length or 0, max_length), + ] + + +StrictBytes = Annotated[bytes, Strict()] +"""A bytes that must be validated in strict mode.""" + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ STRING TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +@_dataclasses.dataclass(frozen=True) +class StringConstraints(annotated_types.GroupedMetadata): + """!!! abstract "Usage Documentation" + [String types](./standard_library_types.md#strings) + + A field metadata class to apply constraints to `str` types. + Use this class as an annotation via [`Annotated`](https://docs.python.org/3/library/typing.html#typing.Annotated), as seen below. + + Attributes: + strip_whitespace: Whether to remove leading and trailing whitespace. + to_upper: Whether to convert the string to uppercase. + to_lower: Whether to convert the string to lowercase. + strict: Whether to validate the string in strict mode. + min_length: The minimum length of the string. + max_length: The maximum length of the string. + pattern: A regex pattern that the string must match. + ascii_only: Whether the string should contain only ASCII characters. + + Example: + ```python + from typing import Annotated + + from pydantic.types import StringConstraints + + ConstrainedStr = Annotated[str, StringConstraints(min_length=1, max_length=10)] + ``` + """ + + strip_whitespace: bool | None = None + to_upper: bool | None = None + to_lower: bool | None = None + strict: bool | None = None + min_length: int | None = None + max_length: int | None = None + pattern: str | Pattern[str] | None = None + ascii_only: bool | None = None + + def __iter__(self) -> Iterator[BaseMetadata]: + if self.min_length is not None: + yield MinLen(self.min_length) + if self.max_length is not None: + yield MaxLen(self.max_length) + if self.strict is not None: + yield Strict(self.strict) + if ( + self.strip_whitespace is not None + or self.pattern is not None + or self.to_lower is not None + or self.to_upper is not None + or self.ascii_only is not None + ): + yield _fields.pydantic_general_metadata( + strip_whitespace=self.strip_whitespace, + to_upper=self.to_upper, + to_lower=self.to_lower, + pattern=self.pattern, + ascii_only=self.ascii_only, + ) + + +def constr( + *, + strip_whitespace: bool | None = None, + to_upper: bool | None = None, + to_lower: bool | None = None, + strict: bool | None = None, + min_length: int | None = None, + max_length: int | None = None, + pattern: str | Pattern[str] | None = None, + ascii_only: bool | None = None, +) -> type[str]: + """ + !!! warning "Discouraged" + This function is **discouraged** in favor of using + [`Annotated`](https://docs.python.org/3/library/typing.html#typing.Annotated) with + [`StringConstraints`][pydantic.types.StringConstraints] instead. + + This function will be **deprecated** in Pydantic 3.0. + + The reason is that `constr` returns a type, which doesn't play well with static analysis tools. + + === ":x: Don't do this" + ```python + from pydantic import BaseModel, constr + + class Foo(BaseModel): + bar: constr(strip_whitespace=True, to_upper=True, pattern=r'^[A-Z]+$') + ``` + + === ":white_check_mark: Do this" + ```python + from typing import Annotated + + from pydantic import BaseModel, StringConstraints + + class Foo(BaseModel): + bar: Annotated[ + str, + StringConstraints( + strip_whitespace=True, to_upper=True, pattern=r'^[A-Z]+$' + ), + ] + ``` + + A wrapper around `str` that allows for additional constraints. + + ```python + from pydantic import BaseModel, constr + + class Foo(BaseModel): + bar: constr(strip_whitespace=True, to_upper=True) + + foo = Foo(bar=' hello ') + print(foo) + #> bar='HELLO' + ``` + + Args: + strip_whitespace: Whether to remove leading and trailing whitespace. + to_upper: Whether to turn all characters to uppercase. + to_lower: Whether to turn all characters to lowercase. + strict: Whether to validate the string in strict mode. + min_length: The minimum length of the string. + max_length: The maximum length of the string. + pattern: A regex pattern to validate the string against. + ascii_only: Whether the string should contain only ASCII characters. + + Returns: + The wrapped string type. + """ # noqa: D212 + return Annotated[ # pyright: ignore[reportReturnType] + str, + StringConstraints( + strip_whitespace=strip_whitespace, + to_upper=to_upper, + to_lower=to_lower, + strict=strict, + min_length=min_length, + max_length=max_length, + pattern=pattern, + ascii_only=ascii_only, + ), + ] + + +StrictStr = Annotated[str, Strict()] +"""A string that must be validated in strict mode.""" + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ COLLECTION TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +HashableItemType = TypeVar('HashableItemType', bound=Hashable) + + +def conset( + item_type: type[HashableItemType], *, min_length: int | None = None, max_length: int | None = None +) -> type[set[HashableItemType]]: + """A wrapper around `typing.Set` that allows for additional constraints. + + Args: + item_type: The type of the items in the set. + min_length: The minimum length of the set. + max_length: The maximum length of the set. + + Returns: + The wrapped set type. + """ + return Annotated[set[item_type], annotated_types.Len(min_length or 0, max_length)] # pyright: ignore[reportReturnType] + + +def confrozenset( + item_type: type[HashableItemType], *, min_length: int | None = None, max_length: int | None = None +) -> type[frozenset[HashableItemType]]: + """A wrapper around `typing.FrozenSet` that allows for additional constraints. + + Args: + item_type: The type of the items in the frozenset. + min_length: The minimum length of the frozenset. + max_length: The maximum length of the frozenset. + + Returns: + The wrapped frozenset type. + """ + return Annotated[frozenset[item_type], annotated_types.Len(min_length or 0, max_length)] # pyright: ignore[reportReturnType] + + +AnyItemType = TypeVar('AnyItemType') + + +def conlist( + item_type: type[AnyItemType], + *, + min_length: int | None = None, + max_length: int | None = None, + unique_items: bool | None = None, +) -> type[list[AnyItemType]]: + """A wrapper around [`list`][] that adds validation. + + Args: + item_type: The type of the items in the list. + min_length: The minimum length of the list. Defaults to None. + max_length: The maximum length of the list. Defaults to None. + unique_items: Whether the items in the list must be unique. Defaults to None. + !!! warning Deprecated + The `unique_items` parameter is deprecated, use `Set` instead. + See [this issue](https://github.com/pydantic/pydantic-core/issues/296) for more details. + + Returns: + The wrapped list type. + """ + if unique_items is not None: + raise PydanticUserError( + ( + '`unique_items` is removed, use `Set` instead' + '(this feature is discussed in https://github.com/pydantic/pydantic-core/issues/296)' + ), + code='removed-kwargs', + ) + return Annotated[list[item_type], annotated_types.Len(min_length or 0, max_length)] # pyright: ignore[reportReturnType] + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~ IMPORT STRING TYPE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +AnyType = TypeVar('AnyType') +if TYPE_CHECKING: + ImportString = Annotated[AnyType, ...] +else: + + class ImportString: + """A type that can be used to import a Python object from a string. + + `ImportString` expects a string and loads the Python object importable at that dotted path. + Attributes of modules may be separated from the module by `:` or `.`, e.g. if `'math:cos'` is provided, + the resulting field value would be the function `cos`. If a `.` is used and both an attribute and submodule + are present at the same path, the module will be preferred. + + On model instantiation, pointers will be evaluated and imported. There is + some nuance to this behavior, demonstrated in the examples below. + + ```python + import math + + from pydantic import BaseModel, Field, ImportString, ValidationError + + class ImportThings(BaseModel): + obj: ImportString + + # A string value will cause an automatic import + my_cos = ImportThings(obj='math.cos') + + # You can use the imported function as you would expect + cos_of_0 = my_cos.obj(0) + assert cos_of_0 == 1 + + # A string whose value cannot be imported will raise an error + try: + ImportThings(obj='foo.bar') + except ValidationError as e: + print(e) + ''' + 1 validation error for ImportThings + obj + Invalid python path: No module named 'foo' [type=import_error, input_value='foo.bar', input_type=str] + ''' + + # Actual python objects can be assigned as well + my_cos = ImportThings(obj=math.cos) + my_cos_2 = ImportThings(obj='math.cos') + my_cos_3 = ImportThings(obj='math:cos') + assert my_cos == my_cos_2 == my_cos_3 + + # You can set default field value either as Python object: + class ImportThingsDefaultPyObj(BaseModel): + obj: ImportString = math.cos + + # or as a string value (but only if used with `validate_default=True`) + class ImportThingsDefaultString(BaseModel): + obj: ImportString = Field(default='math.cos', validate_default=True) + + my_cos_default1 = ImportThingsDefaultPyObj() + my_cos_default2 = ImportThingsDefaultString() + assert my_cos_default1.obj == my_cos_default2.obj == math.cos + + # note: this will not work! + class ImportThingsMissingValidateDefault(BaseModel): + obj: ImportString = 'math.cos' + + my_cos_default3 = ImportThingsMissingValidateDefault() + assert my_cos_default3.obj == 'math.cos' # just string, not evaluated + ``` + + Serializing an `ImportString` type to json is also possible. + + ```python + from pydantic import BaseModel, ImportString + + class ImportThings(BaseModel): + obj: ImportString + + # Create an instance + m = ImportThings(obj='math.cos') + print(m) + #> obj= + print(m.model_dump_json()) + #> {"obj":"math.cos"} + ``` + """ + + @classmethod + def __class_getitem__(cls, item: AnyType) -> AnyType: + return Annotated[item, cls()] + + @classmethod + def __get_pydantic_core_schema__( + cls, source: type[Any], handler: GetCoreSchemaHandler + ) -> core_schema.CoreSchema: + serializer = core_schema.plain_serializer_function_ser_schema(cls._serialize, when_used='json') + if cls is source: + # Treat bare usage of ImportString (`schema is None`) as the same as ImportString[Any] + return core_schema.no_info_plain_validator_function( + function=_validators.import_string, serialization=serializer + ) + else: + return core_schema.no_info_before_validator_function( + function=_validators.import_string, schema=handler(source), serialization=serializer + ) + + @classmethod + def __get_pydantic_json_schema__(cls, cs: CoreSchema, handler: GetJsonSchemaHandler) -> JsonSchemaValue: + return handler(core_schema.str_schema()) + + @staticmethod + def _serialize(v: Any) -> str: + if isinstance(v, ModuleType): + return v.__name__ + elif hasattr(v, '__module__') and hasattr(v, '__name__'): + return f'{v.__module__}.{v.__name__}' + # Handle special cases for sys.XXX streams + # if we see more of these, we should consider a more general solution + elif hasattr(v, 'name'): + if v.name == '': + return 'sys.stdout' + elif v.name == '': + return 'sys.stdin' + elif v.name == '': + return 'sys.stderr' + return v + + def __repr__(self) -> str: + return 'ImportString' + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DECIMAL TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +def condecimal( + *, + strict: bool | None = None, + gt: int | Decimal | None = None, + ge: int | Decimal | None = None, + lt: int | Decimal | None = None, + le: int | Decimal | None = None, + multiple_of: int | Decimal | None = None, + max_digits: int | None = None, + decimal_places: int | None = None, + allow_inf_nan: bool | None = None, +) -> type[Decimal]: + """ + !!! warning "Discouraged" + This function is **discouraged** in favor of using + [`Annotated`](https://docs.python.org/3/library/typing.html#typing.Annotated) with + [`Field`][pydantic.fields.Field] instead. + + This function will be **deprecated** in Pydantic 3.0. + + The reason is that `condecimal` returns a type, which doesn't play well with static analysis tools. + + === ":x: Don't do this" + ```python + from pydantic import BaseModel, condecimal + + class Foo(BaseModel): + bar: condecimal(strict=True, allow_inf_nan=True) + ``` + + === ":white_check_mark: Do this" + ```python + from decimal import Decimal + from typing import Annotated + + from pydantic import BaseModel, Field + + class Foo(BaseModel): + bar: Annotated[Decimal, Field(strict=True, allow_inf_nan=True)] + ``` + + A wrapper around Decimal that adds validation. + + Args: + strict: Whether to validate the value in strict mode. Defaults to `None`. + gt: The value must be greater than this. Defaults to `None`. + ge: The value must be greater than or equal to this. Defaults to `None`. + lt: The value must be less than this. Defaults to `None`. + le: The value must be less than or equal to this. Defaults to `None`. + multiple_of: The value must be a multiple of this. Defaults to `None`. + max_digits: The maximum number of digits. Defaults to `None`. + decimal_places: The number of decimal places. Defaults to `None`. + allow_inf_nan: Whether to allow infinity and NaN. Defaults to `None`. + + ```python + from decimal import Decimal + + from pydantic import BaseModel, ValidationError, condecimal + + class ConstrainedExample(BaseModel): + constrained_decimal: condecimal(gt=Decimal('1.0')) + + m = ConstrainedExample(constrained_decimal=Decimal('1.1')) + print(repr(m)) + #> ConstrainedExample(constrained_decimal=Decimal('1.1')) + + try: + ConstrainedExample(constrained_decimal=Decimal('0.9')) + except ValidationError as e: + print(e.errors()) + ''' + [ + { + 'type': 'greater_than', + 'loc': ('constrained_decimal',), + 'msg': 'Input should be greater than 1.0', + 'input': Decimal('0.9'), + 'ctx': {'gt': Decimal('1.0')}, + 'url': 'https://errors.pydantic.dev/2/v/greater_than', + } + ] + ''' + ``` + """ # noqa: D212 + return Annotated[ # pyright: ignore[reportReturnType] + Decimal, + Strict(strict) if strict is not None else None, + annotated_types.Interval(gt=gt, ge=ge, lt=lt, le=le), + annotated_types.MultipleOf(multiple_of) if multiple_of is not None else None, + _fields.pydantic_general_metadata(max_digits=max_digits, decimal_places=decimal_places), + AllowInfNan(allow_inf_nan) if allow_inf_nan is not None else None, + ] + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ UUID TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +@_dataclasses.dataclass(**_internal_dataclass.slots_true) +class UuidVersion: + """A field metadata class to indicate a [UUID](https://docs.python.org/3/library/uuid.html) version. + + Use this class as an annotation via [`Annotated`](https://docs.python.org/3/library/typing.html#typing.Annotated), as seen below. + + Attributes: + uuid_version: The version of the UUID. Must be one of 1, 3, 4, 5, 6, 7 or 8. + + Example: + ```python + from typing import Annotated + from uuid import UUID + + from pydantic.types import UuidVersion + + UUID1 = Annotated[UUID, UuidVersion(1)] + ``` + """ + + uuid_version: Literal[1, 3, 4, 5, 6, 7, 8] + + def __get_pydantic_json_schema__( + self, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler + ) -> JsonSchemaValue: + field_schema = handler(core_schema) + field_schema.pop('anyOf', None) # remove the bytes/str union + field_schema.update(type='string', format=f'uuid{self.uuid_version}') + return field_schema + + def __get_pydantic_core_schema__(self, source: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + schema = handler(source) + _check_annotated_type(schema['type'], 'uuid', self.__class__.__name__) + schema['version'] = self.uuid_version # type: ignore + return schema + + def __hash__(self) -> int: + return hash(self.uuid_version) + + +UUID1 = Annotated[UUID, UuidVersion(1)] +"""A [UUID](https://docs.python.org/3/library/uuid.html) that must be version 1. + +```python +import uuid + +from pydantic import UUID1, BaseModel + +class Model(BaseModel): + uuid1: UUID1 + +Model(uuid1=uuid.uuid1()) +``` +""" +UUID3 = Annotated[UUID, UuidVersion(3)] +"""A [UUID](https://docs.python.org/3/library/uuid.html) that must be version 3. + +```python +import uuid + +from pydantic import UUID3, BaseModel + +class Model(BaseModel): + uuid3: UUID3 + +Model(uuid3=uuid.uuid3(uuid.NAMESPACE_DNS, 'pydantic.org')) +``` +""" +UUID4 = Annotated[UUID, UuidVersion(4)] +"""A [UUID](https://docs.python.org/3/library/uuid.html) that must be version 4. + +```python +import uuid + +from pydantic import UUID4, BaseModel + +class Model(BaseModel): + uuid4: UUID4 + +Model(uuid4=uuid.uuid4()) +``` +""" +UUID5 = Annotated[UUID, UuidVersion(5)] +"""A [UUID](https://docs.python.org/3/library/uuid.html) that must be version 5. + +```python +import uuid + +from pydantic import UUID5, BaseModel + +class Model(BaseModel): + uuid5: UUID5 + +Model(uuid5=uuid.uuid5(uuid.NAMESPACE_DNS, 'pydantic.org')) +``` +""" +UUID6 = Annotated[UUID, UuidVersion(6)] +"""A [UUID](https://docs.python.org/3/library/uuid.html) that must be version 6. + +```python +import uuid + +from pydantic import UUID6, BaseModel + +class Model(BaseModel): + uuid6: UUID6 + +Model(uuid6=uuid.UUID('1efea953-c2d6-6790-aa0a-69db8c87df97')) +``` +""" +UUID7 = Annotated[UUID, UuidVersion(7)] +"""A [UUID](https://docs.python.org/3/library/uuid.html) that must be version 7. + +```python +import uuid + +from pydantic import UUID7, BaseModel + +class Model(BaseModel): + uuid7: UUID7 + +Model(uuid7=uuid.UUID('0194fdcb-1c47-7a09-b52c-561154de0b4a')) +``` +""" +UUID8 = Annotated[UUID, UuidVersion(8)] +"""A [UUID](https://docs.python.org/3/library/uuid.html) that must be version 8. + +```python +import uuid + +from pydantic import UUID8, BaseModel + +class Model(BaseModel): + uuid8: UUID8 + +Model(uuid8=uuid.UUID('81a0b92e-6078-8551-9c81-8ccb666bdab8')) +``` +""" + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PATH TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +@_dataclasses.dataclass +class PathType: + path_type: Literal['file', 'dir', 'new', 'socket'] + + def __get_pydantic_json_schema__( + self, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler + ) -> JsonSchemaValue: + field_schema = handler(core_schema) + format_conversion = {'file': 'file-path', 'dir': 'directory-path'} + field_schema.update(format=format_conversion.get(self.path_type, 'path'), type='string') + return field_schema + + def __get_pydantic_core_schema__(self, source: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + function_lookup = { + 'file': cast(core_schema.WithInfoValidatorFunction, self.validate_file), + 'dir': cast(core_schema.WithInfoValidatorFunction, self.validate_directory), + 'new': cast(core_schema.WithInfoValidatorFunction, self.validate_new), + 'socket': cast(core_schema.WithInfoValidatorFunction, self.validate_socket), + } + + return core_schema.with_info_after_validator_function( + function_lookup[self.path_type], + handler(source), + ) + + @staticmethod + def validate_file(path: Path, _: core_schema.ValidationInfo) -> Path: + if path.is_file(): + return path + else: + raise PydanticCustomError('path_not_file', 'Path does not point to a file') + + @staticmethod + def validate_socket(path: Path, _: core_schema.ValidationInfo) -> Path: + if path.is_socket(): + return path + else: + raise PydanticCustomError('path_not_socket', 'Path does not point to a socket') + + @staticmethod + def validate_directory(path: Path, _: core_schema.ValidationInfo) -> Path: + if path.is_dir(): + return path + else: + raise PydanticCustomError('path_not_directory', 'Path does not point to a directory') + + @staticmethod + def validate_new(path: Path, _: core_schema.ValidationInfo) -> Path: + if path.exists(): + raise PydanticCustomError('path_exists', 'Path already exists') + elif not path.parent.exists(): + raise PydanticCustomError('parent_does_not_exist', 'Parent directory does not exist') + else: + return path + + def __hash__(self) -> int: + return hash(self.path_type) + + +FilePath = Annotated[Path, PathType('file')] +"""A path that must point to a file. + +```python +from pathlib import Path + +from pydantic import BaseModel, FilePath, ValidationError + +class Model(BaseModel): + f: FilePath + +path = Path('text.txt') +path.touch() +m = Model(f='text.txt') +print(m.model_dump()) +#> {'f': PosixPath('text.txt')} +path.unlink() + +path = Path('directory') +path.mkdir(exist_ok=True) +try: + Model(f='directory') # directory +except ValidationError as e: + print(e) + ''' + 1 validation error for Model + f + Path does not point to a file [type=path_not_file, input_value='directory', input_type=str] + ''' +path.rmdir() + +try: + Model(f='not-exists-file') +except ValidationError as e: + print(e) + ''' + 1 validation error for Model + f + Path does not point to a file [type=path_not_file, input_value='not-exists-file', input_type=str] + ''' +``` +""" +DirectoryPath = Annotated[Path, PathType('dir')] +"""A path that must point to a directory. + +```python +from pathlib import Path + +from pydantic import BaseModel, DirectoryPath, ValidationError + +class Model(BaseModel): + f: DirectoryPath + +path = Path('directory/') +path.mkdir() +m = Model(f='directory/') +print(m.model_dump()) +#> {'f': PosixPath('directory')} +path.rmdir() + +path = Path('file.txt') +path.touch() +try: + Model(f='file.txt') # file +except ValidationError as e: + print(e) + ''' + 1 validation error for Model + f + Path does not point to a directory [type=path_not_directory, input_value='file.txt', input_type=str] + ''' +path.unlink() + +try: + Model(f='not-exists-directory') +except ValidationError as e: + print(e) + ''' + 1 validation error for Model + f + Path does not point to a directory [type=path_not_directory, input_value='not-exists-directory', input_type=str] + ''' +``` +""" +NewPath = Annotated[Path, PathType('new')] +"""A path for a new file or directory that must not already exist. The parent directory must already exist.""" + +SocketPath = Annotated[Path, PathType('socket')] +"""A path to an existing socket file""" + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON TYPE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +if TYPE_CHECKING: + # Json[list[str]] will be recognized by type checkers as list[str] + Json = Annotated[AnyType, ...] + +else: + + class Json: + """A special type wrapper which loads JSON before parsing. + + You can use the `Json` data type to make Pydantic first load a raw JSON string before + validating the loaded data into the parametrized type: + + ```python + from typing import Any + + from pydantic import BaseModel, Json, ValidationError + + class AnyJsonModel(BaseModel): + json_obj: Json[Any] + + class ConstrainedJsonModel(BaseModel): + json_obj: Json[list[int]] + + print(AnyJsonModel(json_obj='{"b": 1}')) + #> json_obj={'b': 1} + print(ConstrainedJsonModel(json_obj='[1, 2, 3]')) + #> json_obj=[1, 2, 3] + + try: + ConstrainedJsonModel(json_obj=12) + except ValidationError as e: + print(e) + ''' + 1 validation error for ConstrainedJsonModel + json_obj + JSON input should be string, bytes or bytearray [type=json_type, input_value=12, input_type=int] + ''' + + try: + ConstrainedJsonModel(json_obj='[a, b]') + except ValidationError as e: + print(e) + ''' + 1 validation error for ConstrainedJsonModel + json_obj + Invalid JSON: expected value at line 1 column 2 [type=json_invalid, input_value='[a, b]', input_type=str] + ''' + + try: + ConstrainedJsonModel(json_obj='["a", "b"]') + except ValidationError as e: + print(e) + ''' + 2 validation errors for ConstrainedJsonModel + json_obj.0 + Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='a', input_type=str] + json_obj.1 + Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='b', input_type=str] + ''' + ``` + + When you dump the model using `model_dump` or `model_dump_json`, the dumped value will be the result of validation, + not the original JSON string. However, you can use the argument `round_trip=True` to get the original JSON string back: + + ```python + from pydantic import BaseModel, Json + + class ConstrainedJsonModel(BaseModel): + json_obj: Json[list[int]] + + print(ConstrainedJsonModel(json_obj='[1, 2, 3]').model_dump_json()) + #> {"json_obj":[1,2,3]} + print( + ConstrainedJsonModel(json_obj='[1, 2, 3]').model_dump_json(round_trip=True) + ) + #> {"json_obj":"[1,2,3]"} + ``` + """ + + @classmethod + def __class_getitem__(cls, item: AnyType) -> AnyType: + return Annotated[item, cls()] + + @classmethod + def __get_pydantic_core_schema__(cls, source: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + if cls is source: + return core_schema.json_schema(None) + else: + return core_schema.json_schema(handler(source)) + + def __repr__(self) -> str: + return 'Json' + + def __hash__(self) -> int: + return hash(type(self)) + + def __eq__(self, other: Any) -> bool: + return type(other) is type(self) + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SECRET TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +# The `Secret` class being conceptually immutable, make the type variable covariant: +SecretType = TypeVar('SecretType', covariant=True) + + +class _SecretBase(Generic[SecretType]): + def __init__(self, secret_value: SecretType) -> None: + self._secret_value: SecretType = secret_value + + def get_secret_value(self) -> SecretType: + """Get the secret value. + + Returns: + The secret value. + """ + return self._secret_value + + def __eq__(self, other: Any) -> bool: + return isinstance(other, self.__class__) and self.get_secret_value() == other.get_secret_value() + + def __hash__(self) -> int: + return hash(self.get_secret_value()) + + def __str__(self) -> str: + return str(self._display()) + + def __repr__(self) -> str: + return f'{self.__class__.__name__}({self._display()!r})' + + def _display(self) -> str | bytes: + raise NotImplementedError + + +def _serialize_secret(value: Secret[SecretType], info: core_schema.SerializationInfo) -> str | Secret[SecretType]: + if info.mode == 'json': + return str(value) + else: + return value + + +class Secret(_SecretBase[SecretType]): + """A generic base class used for defining a field with sensitive information that you do not want to be visible in logging or tracebacks. + + You may either directly parametrize `Secret` with a type, or subclass from `Secret` with a parametrized type. The benefit of subclassing + is that you can define a custom `_display` method, which will be used for `repr()` and `str()` methods. The examples below demonstrate both + ways of using `Secret` to create a new secret type. + + 1. Directly parametrizing `Secret` with a type: + + ```python + from pydantic import BaseModel, Secret + + SecretBool = Secret[bool] + + class Model(BaseModel): + secret_bool: SecretBool + + m = Model(secret_bool=True) + print(m.model_dump()) + #> {'secret_bool': Secret('**********')} + + print(m.model_dump_json()) + #> {"secret_bool":"**********"} + + print(m.secret_bool.get_secret_value()) + #> True + ``` + + 2. Subclassing from parametrized `Secret`: + + ```python + from datetime import date + + from pydantic import BaseModel, Secret + + class SecretDate(Secret[date]): + def _display(self) -> str: + return '****/**/**' + + class Model(BaseModel): + secret_date: SecretDate + + m = Model(secret_date=date(2022, 1, 1)) + print(m.model_dump()) + #> {'secret_date': SecretDate('****/**/**')} + + print(m.model_dump_json()) + #> {"secret_date":"****/**/**"} + + print(m.secret_date.get_secret_value()) + #> 2022-01-01 + ``` + + The value returned by the `_display` method will be used for `repr()` and `str()`. + + You can enforce constraints on the underlying type through annotations: + For example: + + ```python + from typing import Annotated + + from pydantic import BaseModel, Field, Secret, ValidationError + + SecretPosInt = Secret[Annotated[int, Field(gt=0, strict=True)]] + + class Model(BaseModel): + sensitive_int: SecretPosInt + + m = Model(sensitive_int=42) + print(m.model_dump()) + #> {'sensitive_int': Secret('**********')} + + try: + m = Model(sensitive_int=-42) # (1)! + except ValidationError as exc_info: + print(exc_info.errors(include_url=False, include_input=False)) + ''' + [ + { + 'type': 'greater_than', + 'loc': ('sensitive_int',), + 'msg': 'Input should be greater than 0', + 'ctx': {'gt': 0}, + } + ] + ''' + + try: + m = Model(sensitive_int='42') # (2)! + except ValidationError as exc_info: + print(exc_info.errors(include_url=False, include_input=False)) + ''' + [ + { + 'type': 'int_type', + 'loc': ('sensitive_int',), + 'msg': 'Input should be a valid integer', + } + ] + ''' + ``` + + 1. The input value is not greater than 0, so it raises a validation error. + 2. The input value is not an integer, so it raises a validation error because the `SecretPosInt` type has strict mode enabled. + """ + + def _display(self) -> str | bytes: + return '**********' if self.get_secret_value() else '' + + @classmethod + def __get_pydantic_core_schema__(cls, source: type[Any], handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + inner_type = None + # if origin_type is Secret, then cls is a GenericAlias, and we can extract the inner type directly + origin_type = get_origin(source) + if origin_type is not None: + inner_type = get_args(source)[0] + # otherwise, we need to get the inner type from the base class + else: + bases = getattr(cls, '__orig_bases__', getattr(cls, '__bases__', [])) + for base in bases: + if get_origin(base) is Secret: + inner_type = get_args(base)[0] + if bases == [] or inner_type is None: + raise TypeError( + f"Can't get secret type from {cls.__name__}. " + 'Please use Secret[], or subclass from Secret[] instead.' + ) + + inner_schema = handler.generate_schema(inner_type) # type: ignore + + def validate_secret_value(value, handler) -> Secret[SecretType]: + if isinstance(value, Secret): + value = value.get_secret_value() + validated_inner = handler(value) + return cls(validated_inner) + + return core_schema.json_or_python_schema( + python_schema=core_schema.no_info_wrap_validator_function( + validate_secret_value, + inner_schema, + ), + json_schema=core_schema.no_info_after_validator_function(lambda x: cls(x), inner_schema), + serialization=core_schema.plain_serializer_function_ser_schema( + _serialize_secret, + info_arg=True, + when_used='always', + ), + ) + + __pydantic_serializer__ = SchemaSerializer( + core_schema.any_schema( + serialization=core_schema.plain_serializer_function_ser_schema( + _serialize_secret, + info_arg=True, + when_used='always', + ) + ) + ) + + +def _secret_display(value: SecretType) -> str: # type: ignore + return '**********' if value else '' + + +def _serialize_secret_field( + value: _SecretField[SecretType], info: core_schema.SerializationInfo +) -> str | _SecretField[SecretType]: + if info.mode == 'json': + # we want the output to always be string without the `b'` prefix for bytes, + # hence we just use `secret_display` + return _secret_display(value.get_secret_value()) + else: + return value + + +class _SecretField(_SecretBase[SecretType]): + _inner_schema: ClassVar[CoreSchema] + _error_kind: ClassVar[str] + + @classmethod + def __get_pydantic_core_schema__(cls, source: type[Any], handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + def get_json_schema(_core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler) -> JsonSchemaValue: + json_schema = handler(cls._inner_schema) + _utils.update_not_none( + json_schema, + type='string', + writeOnly=True, + format='password', + ) + return json_schema + + def get_secret_schema(strict: bool) -> CoreSchema: + inner_schema = {**cls._inner_schema, 'strict': strict} + json_schema = core_schema.no_info_after_validator_function( + source, # construct the type + inner_schema, # pyright: ignore[reportArgumentType] + ) + return core_schema.json_or_python_schema( + python_schema=core_schema.union_schema( + [ + core_schema.is_instance_schema(source), + json_schema, + ], + custom_error_type=cls._error_kind, + ), + json_schema=json_schema, + serialization=core_schema.plain_serializer_function_ser_schema( + _serialize_secret_field, + info_arg=True, + when_used='always', + ), + ) + + return core_schema.lax_or_strict_schema( + lax_schema=get_secret_schema(strict=False), + strict_schema=get_secret_schema(strict=True), + metadata={'pydantic_js_functions': [get_json_schema]}, + ) + + __pydantic_serializer__ = SchemaSerializer( + core_schema.any_schema( + serialization=core_schema.plain_serializer_function_ser_schema( + _serialize_secret_field, + info_arg=True, + when_used='always', + ) + ) + ) + + +class SecretStr(_SecretField[str]): + """A string used for storing sensitive information that you do not want to be visible in logging or tracebacks. + + When the secret value is nonempty, it is displayed as `'**********'` instead of the underlying value in + calls to `repr()` and `str()`. If the value _is_ empty, it is displayed as `''`. + + ```python + from pydantic import BaseModel, SecretStr + + class User(BaseModel): + username: str + password: SecretStr + + user = User(username='scolvin', password='password1') + + print(user) + #> username='scolvin' password=SecretStr('**********') + print(user.password.get_secret_value()) + #> password1 + print((SecretStr('password'), SecretStr(''))) + #> (SecretStr('**********'), SecretStr('')) + ``` + + As seen above, by default, [`SecretStr`][pydantic.types.SecretStr] (and [`SecretBytes`][pydantic.types.SecretBytes]) + will be serialized as `**********` when serializing to json. + + You can use the [`field_serializer`][pydantic.functional_serializers.field_serializer] to dump the + secret as plain-text when serializing to json. + + ```python + from pydantic import BaseModel, SecretBytes, SecretStr, field_serializer + + class Model(BaseModel): + password: SecretStr + password_bytes: SecretBytes + + @field_serializer('password', 'password_bytes', when_used='json') + def dump_secret(self, v): + return v.get_secret_value() + + model = Model(password='IAmSensitive', password_bytes=b'IAmSensitiveBytes') + print(model) + #> password=SecretStr('**********') password_bytes=SecretBytes(b'**********') + print(model.password) + #> ********** + print(model.model_dump()) + ''' + { + 'password': SecretStr('**********'), + 'password_bytes': SecretBytes(b'**********'), + } + ''' + print(model.model_dump_json()) + #> {"password":"IAmSensitive","password_bytes":"IAmSensitiveBytes"} + ``` + """ + + _inner_schema: ClassVar[CoreSchema] = core_schema.str_schema() + _error_kind: ClassVar[str] = 'string_type' + + def __len__(self) -> int: + return len(self._secret_value) + + def _display(self) -> str: + return _secret_display(self._secret_value) + + +class SecretBytes(_SecretField[bytes]): + """A bytes used for storing sensitive information that you do not want to be visible in logging or tracebacks. + + It displays `b'**********'` instead of the string value on `repr()` and `str()` calls. + When the secret value is nonempty, it is displayed as `b'**********'` instead of the underlying value in + calls to `repr()` and `str()`. If the value _is_ empty, it is displayed as `b''`. + + ```python + from pydantic import BaseModel, SecretBytes + + class User(BaseModel): + username: str + password: SecretBytes + + user = User(username='scolvin', password=b'password1') + #> username='scolvin' password=SecretBytes(b'**********') + print(user.password.get_secret_value()) + #> b'password1' + print((SecretBytes(b'password'), SecretBytes(b''))) + #> (SecretBytes(b'**********'), SecretBytes(b'')) + ``` + """ + + _inner_schema: ClassVar[CoreSchema] = core_schema.bytes_schema() + _error_kind: ClassVar[str] = 'bytes_type' + + def __len__(self) -> int: + return len(self._secret_value) + + def _display(self) -> bytes: + return _secret_display(self._secret_value).encode() + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PAYMENT CARD TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +class PaymentCardBrand(str, Enum): + amex = 'American Express' + mastercard = 'Mastercard' + visa = 'Visa' + other = 'other' + + def __str__(self) -> str: + return self.value + + +@deprecated( + 'The `PaymentCardNumber` class is deprecated, use `pydantic_extra_types` instead. ' + 'See https://docs.pydantic.dev/latest/api/pydantic_extra_types_payment/#pydantic_extra_types.payment.PaymentCardNumber.', + category=PydanticDeprecatedSince20, +) +class PaymentCardNumber(str): + """Based on: https://en.wikipedia.org/wiki/Payment_card_number.""" + + strip_whitespace: ClassVar[bool] = True + min_length: ClassVar[int] = 12 + max_length: ClassVar[int] = 19 + bin: str + last4: str + brand: PaymentCardBrand + + def __init__(self, card_number: str): + self.validate_digits(card_number) + + card_number = self.validate_luhn_check_digit(card_number) + + self.bin = card_number[:6] + self.last4 = card_number[-4:] + self.brand = self.validate_brand(card_number) + + @classmethod + def __get_pydantic_core_schema__(cls, source: type[Any], handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + return core_schema.with_info_after_validator_function( + cls.validate, + core_schema.str_schema( + min_length=cls.min_length, max_length=cls.max_length, strip_whitespace=cls.strip_whitespace + ), + ) + + @classmethod + def validate(cls, input_value: str, /, _: core_schema.ValidationInfo) -> PaymentCardNumber: + """Validate the card number and return a `PaymentCardNumber` instance.""" + return cls(input_value) + + @property + def masked(self) -> str: + """Mask all but the last 4 digits of the card number. + + Returns: + A masked card number string. + """ + num_masked = len(self) - 10 # len(bin) + len(last4) == 10 + return f'{self.bin}{"*" * num_masked}{self.last4}' + + @classmethod + def validate_digits(cls, card_number: str) -> None: + """Validate that the card number is all digits.""" + if not card_number.isdigit(): + raise PydanticCustomError('payment_card_number_digits', 'Card number is not all digits') + + @classmethod + def validate_luhn_check_digit(cls, card_number: str) -> str: + """Based on: https://en.wikipedia.org/wiki/Luhn_algorithm.""" + sum_ = int(card_number[-1]) + length = len(card_number) + parity = length % 2 + for i in range(length - 1): + digit = int(card_number[i]) + if i % 2 == parity: + digit *= 2 + if digit > 9: + digit -= 9 + sum_ += digit + valid = sum_ % 10 == 0 + if not valid: + raise PydanticCustomError('payment_card_number_luhn', 'Card number is not luhn valid') + return card_number + + @staticmethod + def validate_brand(card_number: str) -> PaymentCardBrand: + """Validate length based on BIN for major brands: + https://en.wikipedia.org/wiki/Payment_card_number#Issuer_identification_number_(IIN). + """ + if card_number[0] == '4': + brand = PaymentCardBrand.visa + elif 51 <= int(card_number[:2]) <= 55: + brand = PaymentCardBrand.mastercard + elif card_number[:2] in {'34', '37'}: + brand = PaymentCardBrand.amex + else: + brand = PaymentCardBrand.other + + required_length: None | int | str = None + if brand in PaymentCardBrand.mastercard: + required_length = 16 + valid = len(card_number) == required_length + elif brand == PaymentCardBrand.visa: + required_length = '13, 16 or 19' + valid = len(card_number) in {13, 16, 19} + elif brand == PaymentCardBrand.amex: + required_length = 15 + valid = len(card_number) == required_length + else: + valid = True + + if not valid: + raise PydanticCustomError( + 'payment_card_number_brand', + 'Length for a {brand} card must be {required_length}', + {'brand': brand, 'required_length': required_length}, + ) + return brand + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ BYTE SIZE TYPE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +class ByteSize(int): + """Converts a string representing a number of bytes with units (such as `'1KB'` or `'11.5MiB'`) into an integer. + + You can use the `ByteSize` data type to (case-insensitively) convert a string representation of a number of bytes into + an integer, and also to print out human-readable strings representing a number of bytes. + + In conformance with [IEC 80000-13 Standard](https://en.wikipedia.org/wiki/ISO/IEC_80000) we interpret `'1KB'` to mean 1000 bytes, + and `'1KiB'` to mean 1024 bytes. In general, including a middle `'i'` will cause the unit to be interpreted as a power of 2, + rather than a power of 10 (so, for example, `'1 MB'` is treated as `1_000_000` bytes, whereas `'1 MiB'` is treated as `1_048_576` bytes). + + !!! info + Note that `1b` will be parsed as "1 byte" and not "1 bit". + + ```python + from pydantic import BaseModel, ByteSize + + class MyModel(BaseModel): + size: ByteSize + + print(MyModel(size=52000).size) + #> 52000 + print(MyModel(size='3000 KiB').size) + #> 3072000 + + m = MyModel(size='50 PB') + print(m.size.human_readable()) + #> 44.4PiB + print(m.size.human_readable(decimal=True)) + #> 50.0PB + print(m.size.human_readable(separator=' ')) + #> 44.4 PiB + + print(m.size.to('TiB')) + #> 45474.73508864641 + ``` + """ + + byte_sizes = { + 'b': 1, + 'kb': 10**3, + 'mb': 10**6, + 'gb': 10**9, + 'tb': 10**12, + 'pb': 10**15, + 'eb': 10**18, + 'kib': 2**10, + 'mib': 2**20, + 'gib': 2**30, + 'tib': 2**40, + 'pib': 2**50, + 'eib': 2**60, + 'bit': 1 / 8, + 'kbit': 10**3 / 8, + 'mbit': 10**6 / 8, + 'gbit': 10**9 / 8, + 'tbit': 10**12 / 8, + 'pbit': 10**15 / 8, + 'ebit': 10**18 / 8, + 'kibit': 2**10 / 8, + 'mibit': 2**20 / 8, + 'gibit': 2**30 / 8, + 'tibit': 2**40 / 8, + 'pibit': 2**50 / 8, + 'eibit': 2**60 / 8, + } + byte_sizes.update({k.lower()[0]: v for k, v in byte_sizes.items() if 'i' not in k}) + + byte_string_pattern = r'^\s*(\d*\.?\d+)\s*(\w+)?' + byte_string_re = re.compile(byte_string_pattern, re.IGNORECASE) + + @classmethod + def __get_pydantic_core_schema__(cls, source: type[Any], handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + return core_schema.with_info_after_validator_function( + function=cls._validate, + schema=core_schema.union_schema( + [ + core_schema.str_schema(pattern=cls.byte_string_pattern), + core_schema.int_schema(ge=0), + ], + custom_error_type='byte_size', + custom_error_message='could not parse value and unit from byte string', + ), + serialization=core_schema.plain_serializer_function_ser_schema( + int, return_schema=core_schema.int_schema(ge=0) + ), + ) + + @classmethod + def _validate(cls, input_value: Any, /, _: core_schema.ValidationInfo) -> ByteSize: + try: + return cls(int(input_value)) + except ValueError: + pass + + str_match = cls.byte_string_re.match(str(input_value)) + if str_match is None: + raise PydanticCustomError('byte_size', 'could not parse value and unit from byte string') + + scalar, unit = str_match.groups() + if unit is None: + unit = 'b' + + try: + unit_mult = cls.byte_sizes[unit.lower()] + except KeyError: + raise PydanticCustomError('byte_size_unit', 'could not interpret byte unit: {unit}', {'unit': unit}) + + return cls(int(float(scalar) * unit_mult)) + + def human_readable(self, decimal: bool = False, separator: str = '') -> str: + """Converts a byte size to a human readable string. + + Args: + decimal: If True, use decimal units (e.g. 1000 bytes per KB). If False, use binary units + (e.g. 1024 bytes per KiB). + separator: A string used to split the value and unit. Defaults to an empty string (''). + + Returns: + A human readable string representation of the byte size. + """ + if decimal: + divisor = 1000 + units = 'B', 'KB', 'MB', 'GB', 'TB', 'PB' + final_unit = 'EB' + else: + divisor = 1024 + units = 'B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB' + final_unit = 'EiB' + + num = float(self) + for unit in units: + if abs(num) < divisor: + if unit == 'B': + return f'{num:0.0f}{separator}{unit}' + else: + return f'{num:0.1f}{separator}{unit}' + num /= divisor + + return f'{num:0.1f}{separator}{final_unit}' + + def to(self, unit: str) -> float: + """Converts a byte size to another unit, including both byte and bit units. + + Args: + unit: The unit to convert to. Must be one of the following: B, KB, MB, GB, TB, PB, EB, + KiB, MiB, GiB, TiB, PiB, EiB (byte units) and + bit, kbit, mbit, gbit, tbit, pbit, ebit, + kibit, mibit, gibit, tibit, pibit, eibit (bit units). + + Returns: + The byte size in the new unit. + """ + try: + unit_div = self.byte_sizes[unit.lower()] + except KeyError: + raise PydanticCustomError('byte_size_unit', 'Could not interpret byte unit: {unit}', {'unit': unit}) + + return self / unit_div + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DATE TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +def _check_annotated_type(annotated_type: str, expected_type: str, annotation: str) -> None: + if annotated_type != expected_type: + raise PydanticUserError(f"'{annotation}' cannot annotate '{annotated_type}'.", code='invalid-annotated-type') + + +if TYPE_CHECKING: + PastDate = Annotated[date, ...] + FutureDate = Annotated[date, ...] +else: + + class PastDate: + """A date in the past.""" + + @classmethod + def __get_pydantic_core_schema__( + cls, source: type[Any], handler: GetCoreSchemaHandler + ) -> core_schema.CoreSchema: + if cls is source: + # used directly as a type + return core_schema.date_schema(now_op='past') + else: + schema = handler(source) + _check_annotated_type(schema['type'], 'date', cls.__name__) + schema['now_op'] = 'past' + return schema + + def __repr__(self) -> str: + return 'PastDate' + + class FutureDate: + """A date in the future.""" + + @classmethod + def __get_pydantic_core_schema__( + cls, source: type[Any], handler: GetCoreSchemaHandler + ) -> core_schema.CoreSchema: + if cls is source: + # used directly as a type + return core_schema.date_schema(now_op='future') + else: + schema = handler(source) + _check_annotated_type(schema['type'], 'date', cls.__name__) + schema['now_op'] = 'future' + return schema + + def __repr__(self) -> str: + return 'FutureDate' + + +def condate( + *, + strict: bool | None = None, + gt: date | None = None, + ge: date | None = None, + lt: date | None = None, + le: date | None = None, +) -> type[date]: + """A wrapper for date that adds constraints. + + Args: + strict: Whether to validate the date value in strict mode. Defaults to `None`. + gt: The value must be greater than this. Defaults to `None`. + ge: The value must be greater than or equal to this. Defaults to `None`. + lt: The value must be less than this. Defaults to `None`. + le: The value must be less than or equal to this. Defaults to `None`. + + Returns: + A date type with the specified constraints. + """ + return Annotated[ # pyright: ignore[reportReturnType] + date, + Strict(strict) if strict is not None else None, + annotated_types.Interval(gt=gt, ge=ge, lt=lt, le=le), + ] + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DATETIME TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +if TYPE_CHECKING: + AwareDatetime = Annotated[datetime, ...] + NaiveDatetime = Annotated[datetime, ...] + PastDatetime = Annotated[datetime, ...] + FutureDatetime = Annotated[datetime, ...] + +else: + + class AwareDatetime: + """A datetime that requires timezone info.""" + + @classmethod + def __get_pydantic_core_schema__( + cls, source: type[Any], handler: GetCoreSchemaHandler + ) -> core_schema.CoreSchema: + if cls is source: + # used directly as a type + return core_schema.datetime_schema(tz_constraint='aware') + else: + schema = handler(source) + _check_annotated_type(schema['type'], 'datetime', cls.__name__) + schema['tz_constraint'] = 'aware' + return schema + + def __repr__(self) -> str: + return 'AwareDatetime' + + class NaiveDatetime: + """A datetime that doesn't require timezone info.""" + + @classmethod + def __get_pydantic_core_schema__( + cls, source: type[Any], handler: GetCoreSchemaHandler + ) -> core_schema.CoreSchema: + if cls is source: + # used directly as a type + return core_schema.datetime_schema(tz_constraint='naive') + else: + schema = handler(source) + _check_annotated_type(schema['type'], 'datetime', cls.__name__) + schema['tz_constraint'] = 'naive' + return schema + + def __repr__(self) -> str: + return 'NaiveDatetime' + + class PastDatetime: + """A datetime that must be in the past.""" + + @classmethod + def __get_pydantic_core_schema__( + cls, source: type[Any], handler: GetCoreSchemaHandler + ) -> core_schema.CoreSchema: + if cls is source: + # used directly as a type + return core_schema.datetime_schema(now_op='past') + else: + schema = handler(source) + _check_annotated_type(schema['type'], 'datetime', cls.__name__) + schema['now_op'] = 'past' + return schema + + def __repr__(self) -> str: + return 'PastDatetime' + + class FutureDatetime: + """A datetime that must be in the future.""" + + @classmethod + def __get_pydantic_core_schema__( + cls, source: type[Any], handler: GetCoreSchemaHandler + ) -> core_schema.CoreSchema: + if cls is source: + # used directly as a type + return core_schema.datetime_schema(now_op='future') + else: + schema = handler(source) + _check_annotated_type(schema['type'], 'datetime', cls.__name__) + schema['now_op'] = 'future' + return schema + + def __repr__(self) -> str: + return 'FutureDatetime' + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Encoded TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +class EncoderProtocol(Protocol): + """Protocol for encoding and decoding data to and from bytes.""" + + @classmethod + def decode(cls, data: bytes) -> bytes: + """Decode the data using the encoder. + + Args: + data: The data to decode. + + Returns: + The decoded data. + """ + ... + + @classmethod + def encode(cls, value: bytes) -> bytes: + """Encode the data using the encoder. + + Args: + value: The data to encode. + + Returns: + The encoded data. + """ + ... + + @classmethod + def get_json_format(cls) -> str: + """Get the JSON format for the encoded data. + + Returns: + The JSON format for the encoded data. + """ + ... + + +class Base64Encoder(EncoderProtocol): + """Standard (non-URL-safe) Base64 encoder.""" + + @classmethod + def decode(cls, data: bytes) -> bytes: + """Decode the data from base64 encoded bytes to original bytes data. + + Args: + data: The data to decode. + + Returns: + The decoded data. + """ + try: + return base64.b64decode(data) + except ValueError as e: + raise PydanticCustomError('base64_decode', "Base64 decoding error: '{error}'", {'error': str(e)}) + + @classmethod + def encode(cls, value: bytes) -> bytes: + """Encode the data from bytes to a base64 encoded bytes. + + Args: + value: The data to encode. + + Returns: + The encoded data. + """ + return base64.b64encode(value) + + @classmethod + def get_json_format(cls) -> Literal['base64']: + """Get the JSON format for the encoded data. + + Returns: + The JSON format for the encoded data. + """ + return 'base64' + + +class Base64UrlEncoder(EncoderProtocol): + """URL-safe Base64 encoder.""" + + @classmethod + def decode(cls, data: bytes) -> bytes: + """Decode the data from base64 encoded bytes to original bytes data. + + Args: + data: The data to decode. + + Returns: + The decoded data. + """ + try: + return base64.urlsafe_b64decode(data) + except ValueError as e: + raise PydanticCustomError('base64_decode', "Base64 decoding error: '{error}'", {'error': str(e)}) + + @classmethod + def encode(cls, value: bytes) -> bytes: + """Encode the data from bytes to a base64 encoded bytes. + + Args: + value: The data to encode. + + Returns: + The encoded data. + """ + return base64.urlsafe_b64encode(value) + + @classmethod + def get_json_format(cls) -> Literal['base64url']: + """Get the JSON format for the encoded data. + + Returns: + The JSON format for the encoded data. + """ + return 'base64url' + + +@_dataclasses.dataclass(**_internal_dataclass.slots_true) +class EncodedBytes: + """A bytes type that is encoded and decoded using the specified encoder. + + `EncodedBytes` needs an encoder that implements `EncoderProtocol` to operate. + + ```python + from typing import Annotated + + from pydantic import BaseModel, EncodedBytes, EncoderProtocol, ValidationError + + class MyEncoder(EncoderProtocol): + @classmethod + def decode(cls, data: bytes) -> bytes: + if data == b'**undecodable**': + raise ValueError('Cannot decode data') + return data[13:] + + @classmethod + def encode(cls, value: bytes) -> bytes: + return b'**encoded**: ' + value + + @classmethod + def get_json_format(cls) -> str: + return 'my-encoder' + + MyEncodedBytes = Annotated[bytes, EncodedBytes(encoder=MyEncoder)] + + class Model(BaseModel): + my_encoded_bytes: MyEncodedBytes + + # Initialize the model with encoded data + m = Model(my_encoded_bytes=b'**encoded**: some bytes') + + # Access decoded value + print(m.my_encoded_bytes) + #> b'some bytes' + + # Serialize into the encoded form + print(m.model_dump()) + #> {'my_encoded_bytes': b'**encoded**: some bytes'} + + # Validate encoded data + try: + Model(my_encoded_bytes=b'**undecodable**') + except ValidationError as e: + print(e) + ''' + 1 validation error for Model + my_encoded_bytes + Value error, Cannot decode data [type=value_error, input_value=b'**undecodable**', input_type=bytes] + ''' + ``` + """ + + encoder: type[EncoderProtocol] + + def __get_pydantic_json_schema__( + self, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler + ) -> JsonSchemaValue: + field_schema = handler(core_schema) + field_schema.update(type='string', format=self.encoder.get_json_format()) + return field_schema + + def __get_pydantic_core_schema__(self, source: type[Any], handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + schema = handler(source) + _check_annotated_type(schema['type'], 'bytes', self.__class__.__name__) + return core_schema.with_info_after_validator_function( + function=self.decode, + schema=schema, + serialization=core_schema.plain_serializer_function_ser_schema(function=self.encode), + ) + + def decode(self, data: bytes, _: core_schema.ValidationInfo) -> bytes: + """Decode the data using the specified encoder. + + Args: + data: The data to decode. + + Returns: + The decoded data. + """ + return self.encoder.decode(data) + + def encode(self, value: bytes) -> bytes: + """Encode the data using the specified encoder. + + Args: + value: The data to encode. + + Returns: + The encoded data. + """ + return self.encoder.encode(value) + + def __hash__(self) -> int: + return hash(self.encoder) + + +@_dataclasses.dataclass(**_internal_dataclass.slots_true) +class EncodedStr: + """A str type that is encoded and decoded using the specified encoder. + + `EncodedStr` needs an encoder that implements `EncoderProtocol` to operate. + + ```python + from typing import Annotated + + from pydantic import BaseModel, EncodedStr, EncoderProtocol, ValidationError + + class MyEncoder(EncoderProtocol): + @classmethod + def decode(cls, data: bytes) -> bytes: + if data == b'**undecodable**': + raise ValueError('Cannot decode data') + return data[13:] + + @classmethod + def encode(cls, value: bytes) -> bytes: + return b'**encoded**: ' + value + + @classmethod + def get_json_format(cls) -> str: + return 'my-encoder' + + MyEncodedStr = Annotated[str, EncodedStr(encoder=MyEncoder)] + + class Model(BaseModel): + my_encoded_str: MyEncodedStr + + # Initialize the model with encoded data + m = Model(my_encoded_str='**encoded**: some str') + + # Access decoded value + print(m.my_encoded_str) + #> some str + + # Serialize into the encoded form + print(m.model_dump()) + #> {'my_encoded_str': '**encoded**: some str'} + + # Validate encoded data + try: + Model(my_encoded_str='**undecodable**') + except ValidationError as e: + print(e) + ''' + 1 validation error for Model + my_encoded_str + Value error, Cannot decode data [type=value_error, input_value='**undecodable**', input_type=str] + ''' + ``` + """ + + encoder: type[EncoderProtocol] + + def __get_pydantic_json_schema__( + self, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler + ) -> JsonSchemaValue: + field_schema = handler(core_schema) + field_schema.update(type='string', format=self.encoder.get_json_format()) + return field_schema + + def __get_pydantic_core_schema__(self, source: type[Any], handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + schema = handler(source) + _check_annotated_type(schema['type'], 'str', self.__class__.__name__) + return core_schema.with_info_after_validator_function( + function=self.decode_str, + schema=schema, + serialization=core_schema.plain_serializer_function_ser_schema(function=self.encode_str), + ) + + def decode_str(self, data: str, _: core_schema.ValidationInfo) -> str: + """Decode the data using the specified encoder. + + Args: + data: The data to decode. + + Returns: + The decoded data. + """ + return self.encoder.decode(data.encode()).decode() + + def encode_str(self, value: str) -> str: + """Encode the data using the specified encoder. + + Args: + value: The data to encode. + + Returns: + The encoded data. + """ + return self.encoder.encode(value.encode()).decode() # noqa: UP008 + + def __hash__(self) -> int: + return hash(self.encoder) + + +Base64Bytes = Annotated[bytes, EncodedBytes(encoder=Base64Encoder)] +"""A bytes type that is encoded and decoded using the standard (non-URL-safe) base64 encoder. + +Note: + Under the hood, `Base64Bytes` uses the standard library [`base64.b64encode()`][base64.b64encode] and [`base64.b64decode()`][base64.b64decode] functions. + + As a result, attempting to decode url-safe base64 data using the `Base64Bytes` type may fail or produce an incorrect + decoding. + +/// version-changed | v2.10 +`Base64Bytes` now uses [`base64.b64encode()`][base64.b64encode] and [`base64.b64decode()`][base64.b64decode] +instead of [`base64.encodebytes()`][base64.encodebytes] and [`base64.decodebytes()`][base64.decodebytes]. + +These methods are considered legacy implementation. If you'd still like to use these legacy encoders/decoders, +you can achieve this by creating a custom annotated type, like follows: +```python +import base64 +from typing import Annotated, Literal + +from pydantic_core import PydanticCustomError + +from pydantic import EncodedBytes, EncoderProtocol + +class LegacyBase64Encoder(EncoderProtocol): + @classmethod + def decode(cls, data: bytes) -> bytes: + try: + return base64.decodebytes(data) + except ValueError as e: + raise PydanticCustomError( + 'base64_decode', + "Base64 decoding error: '{error}'", + {'error': str(e)}, + ) + + @classmethod + def encode(cls, value: bytes) -> bytes: + return base64.encodebytes(value) + + @classmethod + def get_json_format(cls) -> Literal['base64']: + return 'base64' + +LegacyBase64Bytes = Annotated[bytes, EncodedBytes(encoder=LegacyBase64Encoder)] +``` +/// + +```python +from pydantic import Base64Bytes, BaseModel, ValidationError + +class Model(BaseModel): + base64_bytes: Base64Bytes + +# Initialize the model with base64 data +m = Model(base64_bytes=b'VGhpcyBpcyB0aGUgd2F5') + +# Access decoded value +print(m.base64_bytes) +#> b'This is the way' + +# Serialize into the base64 form +print(m.model_dump()) +#> {'base64_bytes': b'VGhpcyBpcyB0aGUgd2F5'} + +# Validate base64 data +try: + print(Model(base64_bytes=b'undecodable').base64_bytes) +except ValidationError as e: + print(e) + ''' + 1 validation error for Model + base64_bytes + Base64 decoding error: 'Incorrect padding' [type=base64_decode, input_value=b'undecodable', input_type=bytes] + ''' +``` +""" +Base64Str = Annotated[str, EncodedStr(encoder=Base64Encoder)] +"""A string type that is encoded and decoded using the standard (non-URL-safe) base64 encoder. + +Note: + Under the hood, `Base64Str` uses the standard library [`base64.b64encode()`][base64.b64encode] and [`base64.b64decode()`][base64.b64decode] functions. + + As a result, attempting to decode url-safe base64 data using the `Base64Str` type may fail or produce an incorrect + decoding. + +/// version-changed | v2.10 +`Base64Str` now uses [`base64.b64encode()`][base64.b64encode] and [`base64.b64decode()`][base64.b64decode] +instead of [`base64.encodebytes()`][base64.encodebytes] and [`base64.decodebytes()`][base64.decodebytes]. + +These methods are considered legacy implementation. See the documentation about the [`Base64Bytes`][pydantic.types.Base64Bytes] type +for more information on how to replicate the old behavior with the legacy encoders/decoders. +/// + + +```python +from pydantic import Base64Str, BaseModel, ValidationError + +class Model(BaseModel): + base64_str: Base64Str + +# Initialize the model with base64 data +m = Model(base64_str='VGhlc2UgYXJlbid0IHRoZSBkcm9pZHMgeW91J3JlIGxvb2tpbmcgZm9y') + +# Access decoded value +print(m.base64_str) +#> These aren't the droids you're looking for + +# Serialize into the base64 form +print(m.model_dump()) +#> {'base64_str': 'VGhlc2UgYXJlbid0IHRoZSBkcm9pZHMgeW91J3JlIGxvb2tpbmcgZm9y'} + +# Validate base64 data +try: + print(Model(base64_str='undecodable').base64_str) +except ValidationError as e: + print(e) + ''' + 1 validation error for Model + base64_str + Base64 decoding error: 'Incorrect padding' [type=base64_decode, input_value='undecodable', input_type=str] + ''' +``` +""" +Base64UrlBytes = Annotated[bytes, EncodedBytes(encoder=Base64UrlEncoder)] +"""A bytes type that is encoded and decoded using the URL-safe base64 encoder. + +Note: + Under the hood, `Base64UrlBytes` use standard library `base64.urlsafe_b64encode` and `base64.urlsafe_b64decode` + functions. + + As a result, the `Base64UrlBytes` type can be used to faithfully decode "vanilla" base64 data + (using `'+'` and `'/'`). + +```python +from pydantic import Base64UrlBytes, BaseModel + +class Model(BaseModel): + base64url_bytes: Base64UrlBytes + +# Initialize the model with base64 data +m = Model(base64url_bytes=b'SHc_dHc-TXc==') +print(m) +#> base64url_bytes=b'Hw?tw>Mw' +``` +""" +Base64UrlStr = Annotated[str, EncodedStr(encoder=Base64UrlEncoder)] +"""A str type that is encoded and decoded using the URL-safe base64 encoder. + +Note: + Under the hood, `Base64UrlStr` use standard library `base64.urlsafe_b64encode` and `base64.urlsafe_b64decode` + functions. + + As a result, the `Base64UrlStr` type can be used to faithfully decode "vanilla" base64 data (using `'+'` and `'/'`). + +```python +from pydantic import Base64UrlStr, BaseModel + +class Model(BaseModel): + base64url_str: Base64UrlStr + +# Initialize the model with base64 data +m = Model(base64url_str='SHc_dHc-TXc==') +print(m) +#> base64url_str='Hw?tw>Mw' +``` +""" + + +__getattr__ = getattr_migration(__name__) + + +@_dataclasses.dataclass(**_internal_dataclass.slots_true) +class GetPydanticSchema: + """!!! abstract "Usage Documentation" + [Using `GetPydanticSchema` to Reduce Boilerplate](../concepts/types.md#using-getpydanticschema-to-reduce-boilerplate) + + A convenience class for creating an annotation that provides pydantic custom type hooks. + + This class is intended to eliminate the need to create a custom "marker" which defines the + `__get_pydantic_core_schema__` and `__get_pydantic_json_schema__` custom hook methods. + + For example, to have a field treated by type checkers as `int`, but by pydantic as `Any`, you can do: + ```python + from typing import Annotated, Any + + from pydantic import BaseModel, GetPydanticSchema + + HandleAsAny = GetPydanticSchema(lambda _s, h: h(Any)) + + class Model(BaseModel): + x: Annotated[int, HandleAsAny] # pydantic sees `x: Any` + + print(repr(Model(x='abc').x)) + #> 'abc' + ``` + """ + + get_pydantic_core_schema: Callable[[Any, GetCoreSchemaHandler], CoreSchema] | None = None + get_pydantic_json_schema: Callable[[Any, GetJsonSchemaHandler], JsonSchemaValue] | None = None + + # Note: we may want to consider adding a convenience staticmethod `def for_type(type_: Any) -> GetPydanticSchema:` + # which returns `GetPydanticSchema(lambda _s, h: h(type_))` + + if not TYPE_CHECKING: + # We put `__getattr__` in a non-TYPE_CHECKING block because otherwise, mypy allows arbitrary attribute access + + def __getattr__(self, item: str) -> Any: + """Use this rather than defining `__get_pydantic_core_schema__` etc. to reduce the number of nested calls.""" + if item == '__get_pydantic_core_schema__' and self.get_pydantic_core_schema: + return self.get_pydantic_core_schema + elif item == '__get_pydantic_json_schema__' and self.get_pydantic_json_schema: + return self.get_pydantic_json_schema + else: + return object.__getattribute__(self, item) + + __hash__ = object.__hash__ + + +@_dataclasses.dataclass(**_internal_dataclass.slots_true, frozen=True) +class Tag: + """Provides a way to specify the expected tag to use for a case of a (callable) discriminated union. + + Also provides a way to label a union case in error messages. + + When using a callable `Discriminator`, attach a `Tag` to each case in the `Union` to specify the tag that + should be used to identify that case. For example, in the below example, the `Tag` is used to specify that + if `get_discriminator_value` returns `'apple'`, the input should be validated as an `ApplePie`, and if it + returns `'pumpkin'`, the input should be validated as a `PumpkinPie`. + + The primary role of the `Tag` here is to map the return value from the callable `Discriminator` function to + the appropriate member of the `Union` in question. + + ```python + from typing import Annotated, Any, Literal, Union + + from pydantic import BaseModel, Discriminator, Tag + + class Pie(BaseModel): + time_to_cook: int + num_ingredients: int + + class ApplePie(Pie): + fruit: Literal['apple'] = 'apple' + + class PumpkinPie(Pie): + filling: Literal['pumpkin'] = 'pumpkin' + + def get_discriminator_value(v: Any) -> str: + if isinstance(v, dict): + return v.get('fruit', v.get('filling')) + return getattr(v, 'fruit', getattr(v, 'filling', None)) + + class ThanksgivingDinner(BaseModel): + dessert: Annotated[ + Union[ + Annotated[ApplePie, Tag('apple')], + Annotated[PumpkinPie, Tag('pumpkin')], + ], + Discriminator(get_discriminator_value), + ] + + apple_variation = ThanksgivingDinner.model_validate( + {'dessert': {'fruit': 'apple', 'time_to_cook': 60, 'num_ingredients': 8}} + ) + print(repr(apple_variation)) + ''' + ThanksgivingDinner(dessert=ApplePie(time_to_cook=60, num_ingredients=8, fruit='apple')) + ''' + + pumpkin_variation = ThanksgivingDinner.model_validate( + { + 'dessert': { + 'filling': 'pumpkin', + 'time_to_cook': 40, + 'num_ingredients': 6, + } + } + ) + print(repr(pumpkin_variation)) + ''' + ThanksgivingDinner(dessert=PumpkinPie(time_to_cook=40, num_ingredients=6, filling='pumpkin')) + ''' + ``` + + !!! note + You must specify a `Tag` for every case in a `Tag` that is associated with a + callable `Discriminator`. Failing to do so will result in a `PydanticUserError` with code + [`callable-discriminator-no-tag`](../errors/usage_errors.md#callable-discriminator-no-tag). + + See the [Discriminated Unions] concepts docs for more details on how to use `Tag`s. + + [Discriminated Unions]: ../concepts/unions.md#discriminated-unions + """ + + tag: str + + def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema: + schema = handler(source_type) + metadata = cast('CoreMetadata', schema.setdefault('metadata', {})) + metadata['pydantic_internal_union_tag_key'] = self.tag + return schema + + +@_dataclasses.dataclass(**_internal_dataclass.slots_true, frozen=True) +class Discriminator: + """!!! abstract "Usage Documentation" + [Discriminated Unions with `Callable` `Discriminator`](../concepts/unions.md#discriminated-unions-with-callable-discriminator) + + Provides a way to use a custom callable as the way to extract the value of a union discriminator. + + This allows you to get validation behavior like you'd get from `Field(discriminator=)`, + but without needing to have a single shared field across all the union choices. This also makes it + possible to handle unions of models and primitive types with discriminated-union-style validation errors. + Finally, this allows you to use a custom callable as the way to identify which member of a union a value + belongs to, while still seeing all the performance benefits of a discriminated union. + + Consider this example, which is much more performant with the use of `Discriminator` and thus a `TaggedUnion` + than it would be as a normal `Union`. + + ```python + from typing import Annotated, Any, Literal, Union + + from pydantic import BaseModel, Discriminator, Tag + + class Pie(BaseModel): + time_to_cook: int + num_ingredients: int + + class ApplePie(Pie): + fruit: Literal['apple'] = 'apple' + + class PumpkinPie(Pie): + filling: Literal['pumpkin'] = 'pumpkin' + + def get_discriminator_value(v: Any) -> str: + if isinstance(v, dict): + return v.get('fruit', v.get('filling')) + return getattr(v, 'fruit', getattr(v, 'filling', None)) + + class ThanksgivingDinner(BaseModel): + dessert: Annotated[ + Union[ + Annotated[ApplePie, Tag('apple')], + Annotated[PumpkinPie, Tag('pumpkin')], + ], + Discriminator(get_discriminator_value), + ] + + apple_variation = ThanksgivingDinner.model_validate( + {'dessert': {'fruit': 'apple', 'time_to_cook': 60, 'num_ingredients': 8}} + ) + print(repr(apple_variation)) + ''' + ThanksgivingDinner(dessert=ApplePie(time_to_cook=60, num_ingredients=8, fruit='apple')) + ''' + + pumpkin_variation = ThanksgivingDinner.model_validate( + { + 'dessert': { + 'filling': 'pumpkin', + 'time_to_cook': 40, + 'num_ingredients': 6, + } + } + ) + print(repr(pumpkin_variation)) + ''' + ThanksgivingDinner(dessert=PumpkinPie(time_to_cook=40, num_ingredients=6, filling='pumpkin')) + ''' + ``` + + See the [Discriminated Unions] concepts docs for more details on how to use `Discriminator`s. + + [Discriminated Unions]: ../concepts/unions.md#discriminated-unions + """ + + discriminator: str | Callable[[Any], Hashable] + """The callable or field name for discriminating the type in a tagged union. + + A `Callable` discriminator must extract the value of the discriminator from the input. + A `str` discriminator must be the name of a field to discriminate against. + """ + custom_error_type: str | None = None + """Type to use in [custom errors](../errors/errors.md) replacing the standard discriminated union + validation errors. + """ + custom_error_message: str | None = None + """Message to use in custom errors.""" + custom_error_context: dict[str, int | str | float] | None = None + """Context to use in custom errors.""" + + def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema: + if isinstance(self.discriminator, str): + from pydantic import Field + + return handler(Annotated[source_type, Field(discriminator=self.discriminator)]) + else: + original_schema = handler(source_type) + return self._convert_schema(original_schema, handler) + + def _convert_schema( + self, original_schema: core_schema.CoreSchema, handler: GetCoreSchemaHandler | None = None + ) -> core_schema.TaggedUnionSchema: + if handler is not None and original_schema['type'] == 'definition-ref': + # Same logic as `_ApplyInferredDiscriminator._apply_to_root()` + try: + def_schema = handler.resolve_ref_schema(original_schema) + except LookupError: # pragma: no cover + from pydantic._internal._discriminated_union import MissingDefinitionForUnionRef + + raise MissingDefinitionForUnionRef(original_schema['schema_ref']) + + # If using a referenceable union as discriminated (e.g. `type Pet = Cat | Dog; field: Pet = Field(discriminator=...)`): + if def_schema['type'] == 'union': + original_schema = def_schema.copy() + original_schema.pop('ref') + + if original_schema['type'] != 'union': + # This likely indicates that the schema was a single-item union that was simplified. + # In this case, we do the same thing we do in + # `pydantic._internal._discriminated_union._ApplyInferredDiscriminator._apply_to_root`, namely, + # package the generated schema back into a single-item union. + original_schema = core_schema.union_schema([original_schema]) + + tagged_union_choices = {} + for choice in original_schema['choices']: + tag = None + if isinstance(choice, tuple): + choice, tag = choice + metadata = cast('CoreMetadata | None', choice.get('metadata')) + if metadata is not None: + tag = metadata.get('pydantic_internal_union_tag_key') or tag + if tag is None: + # `handler` is None when this method is called from `apply_discriminator()` (deferred discriminators) + if handler is not None and choice['type'] == 'definition-ref': + # If choice was built from a PEP 695 type alias, try to resolve the def: + try: + choice = handler.resolve_ref_schema(choice) + except LookupError: + pass + else: + metadata = cast('CoreMetadata | None', choice.get('metadata')) + if metadata is not None: + tag = metadata.get('pydantic_internal_union_tag_key') + + if tag is None: + raise PydanticUserError( + f'`Tag` not provided for choice {choice} used with `Discriminator`', + code='callable-discriminator-no-tag', + ) + tagged_union_choices[tag] = choice + + # Have to do these verbose checks to ensure falsy values ('' and {}) don't get ignored + custom_error_type = self.custom_error_type + if custom_error_type is None: + custom_error_type = original_schema.get('custom_error_type') + + custom_error_message = self.custom_error_message + if custom_error_message is None: + custom_error_message = original_schema.get('custom_error_message') + + custom_error_context = self.custom_error_context + if custom_error_context is None: + custom_error_context = original_schema.get('custom_error_context') + + custom_error_type = original_schema.get('custom_error_type') if custom_error_type is None else custom_error_type + return core_schema.tagged_union_schema( + tagged_union_choices, + self.discriminator, + custom_error_type=custom_error_type, + custom_error_message=custom_error_message, + custom_error_context=custom_error_context, + strict=original_schema.get('strict'), + ref=original_schema.get('ref'), + metadata=original_schema.get('metadata'), + serialization=original_schema.get('serialization'), + ) + + +_JSON_TYPES = {int, float, str, bool, list, dict, type(None)} + + +def _get_type_name(x: Any) -> str: + type_ = type(x) + if type_ in _JSON_TYPES: + return type_.__name__ + + # Handle proper subclasses; note we don't need to handle None or bool here + if isinstance(x, int): + return 'int' + if isinstance(x, float): + return 'float' + if isinstance(x, str): + return 'str' + if isinstance(x, list): + return 'list' + if isinstance(x, dict): + return 'dict' + + # Fail by returning the type's actual name + return getattr(type_, '__name__', '') + + +class _AllowAnyJson: + @classmethod + def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema: + python_schema = handler(source_type) + return core_schema.json_or_python_schema(json_schema=core_schema.any_schema(), python_schema=python_schema) + + +if TYPE_CHECKING: + # This seems to only be necessary for mypy + JsonValue: TypeAlias = Union[ + list['JsonValue'], + dict[str, 'JsonValue'], + str, + bool, + int, + float, + None, + ] + """A `JsonValue` is used to represent a value that can be serialized to JSON. + + It may be one of: + + * `list['JsonValue']` + * `dict[str, 'JsonValue']` + * `str` + * `bool` + * `int` + * `float` + * `None` + + The following example demonstrates how to use `JsonValue` to validate JSON data, + and what kind of errors to expect when input data is not json serializable. + + ```python + import json + + from pydantic import BaseModel, JsonValue, ValidationError + + class Model(BaseModel): + j: JsonValue + + valid_json_data = {'j': {'a': {'b': {'c': 1, 'd': [2, None]}}}} + invalid_json_data = {'j': {'a': {'b': ...}}} + + print(repr(Model.model_validate(valid_json_data))) + #> Model(j={'a': {'b': {'c': 1, 'd': [2, None]}}}) + print(repr(Model.model_validate_json(json.dumps(valid_json_data)))) + #> Model(j={'a': {'b': {'c': 1, 'd': [2, None]}}}) + + try: + Model.model_validate(invalid_json_data) + except ValidationError as e: + print(e) + ''' + 1 validation error for Model + j.dict.a.dict.b + input was not a valid JSON value [type=invalid-json-value, input_value=Ellipsis, input_type=ellipsis] + ''' + ``` + """ + +else: + JsonValue = TypeAliasType( + 'JsonValue', + Annotated[ + Union[ + Annotated[list['JsonValue'], Tag('list')], + Annotated[dict[str, 'JsonValue'], Tag('dict')], + Annotated[str, Tag('str')], + Annotated[bool, Tag('bool')], + Annotated[int, Tag('int')], + Annotated[float, Tag('float')], + Annotated[None, Tag('NoneType')], + ], + Discriminator( + _get_type_name, + custom_error_type='invalid-json-value', + custom_error_message='input was not a valid JSON value', + ), + _AllowAnyJson, + ], + ) + + +class _OnErrorOmit: + @classmethod + def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema: + # there is no actual default value here but we use with_default_schema since it already has the on_error + # behavior implemented and it would be no more efficient to implement it on every other validator + # or as a standalone validator + return core_schema.with_default_schema(schema=handler(source_type), on_error='omit') + + +OnErrorOmit = Annotated[T, _OnErrorOmit] +""" +When used as an item in a list, the key type in a dict, optional values of a TypedDict, etc. +this annotation omits the item from the iteration if there is any error validating it. +That is, instead of a [`ValidationError`][pydantic_core.ValidationError] being propagated up and the entire iterable being discarded +any invalid items are discarded and the valid ones are returned. +""" + + +@_dataclasses.dataclass +class FailFast(_fields.PydanticMetadata, BaseMetadata): + """A `FailFast` annotation can be used to specify that validation should stop at the first error. + + This can be useful when you want to validate a large amount of data and you only need to know if it's valid or not. + + You might want to enable this setting if you want to validate your data faster (basically, if you use this, + validation will be more performant with the caveat that you get less information). + + ```python + from typing import Annotated + + from pydantic import BaseModel, FailFast, ValidationError + + class Model(BaseModel): + x: Annotated[list[int], FailFast()] + + # This will raise a single error for the first invalid value and stop validation + try: + obj = Model(x=[1, 2, 'a', 4, 5, 'b', 7, 8, 9, 'c']) + except ValidationError as e: + print(e) + ''' + 1 validation error for Model + x.2 + Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='a', input_type=str] + ''' + ``` + """ + + fail_fast: bool = True diff --git a/source/target-vendors/cp312/pydantic/typing.py b/source/target-vendors/cp312/pydantic/typing.py new file mode 100644 index 0000000..0bda22d --- /dev/null +++ b/source/target-vendors/cp312/pydantic/typing.py @@ -0,0 +1,5 @@ +"""`typing` module is a backport module from V1.""" + +from ._migration import getattr_migration + +__getattr__ = getattr_migration(__name__) diff --git a/source/target-vendors/cp312/pydantic/utils.py b/source/target-vendors/cp312/pydantic/utils.py new file mode 100644 index 0000000..8d1e2a8 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/utils.py @@ -0,0 +1,5 @@ +"""The `utils` module is a backport module from V1.""" + +from ._migration import getattr_migration + +__getattr__ = getattr_migration(__name__) diff --git a/source/target-vendors/cp312/pydantic/v1/__init__.py b/source/target-vendors/cp312/pydantic/v1/__init__.py new file mode 100644 index 0000000..6ad3f46 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/__init__.py @@ -0,0 +1,131 @@ +# flake8: noqa +from pydantic.v1 import dataclasses +from pydantic.v1.annotated_types import create_model_from_namedtuple, create_model_from_typeddict +from pydantic.v1.class_validators import root_validator, validator +from pydantic.v1.config import BaseConfig, ConfigDict, Extra +from pydantic.v1.decorator import validate_arguments +from pydantic.v1.env_settings import BaseSettings +from pydantic.v1.error_wrappers import ValidationError +from pydantic.v1.errors import * +from pydantic.v1.fields import Field, PrivateAttr, Required +from pydantic.v1.main import * +from pydantic.v1.networks import * +from pydantic.v1.parse import Protocol +from pydantic.v1.tools import * +from pydantic.v1.types import * +from pydantic.v1.version import VERSION, compiled + +__version__ = VERSION + +# WARNING __all__ from pydantic.errors is not included here, it will be removed as an export here in v2 +# please use "from pydantic.v1.errors import ..." instead +__all__ = [ + # annotated types utils + 'create_model_from_namedtuple', + 'create_model_from_typeddict', + # dataclasses + 'dataclasses', + # class_validators + 'root_validator', + 'validator', + # config + 'BaseConfig', + 'ConfigDict', + 'Extra', + # decorator + 'validate_arguments', + # env_settings + 'BaseSettings', + # error_wrappers + 'ValidationError', + # fields + 'Field', + 'Required', + # main + 'BaseModel', + 'create_model', + 'validate_model', + # network + 'AnyUrl', + 'AnyHttpUrl', + 'FileUrl', + 'HttpUrl', + 'stricturl', + 'EmailStr', + 'NameEmail', + 'IPvAnyAddress', + 'IPvAnyInterface', + 'IPvAnyNetwork', + 'PostgresDsn', + 'CockroachDsn', + 'AmqpDsn', + 'RedisDsn', + 'MongoDsn', + 'KafkaDsn', + 'validate_email', + # parse + 'Protocol', + # tools + 'parse_file_as', + 'parse_obj_as', + 'parse_raw_as', + 'schema_of', + 'schema_json_of', + # types + 'NoneStr', + 'NoneBytes', + 'StrBytes', + 'NoneStrBytes', + 'StrictStr', + 'ConstrainedBytes', + 'conbytes', + 'ConstrainedList', + 'conlist', + 'ConstrainedSet', + 'conset', + 'ConstrainedFrozenSet', + 'confrozenset', + 'ConstrainedStr', + 'constr', + 'PyObject', + 'ConstrainedInt', + 'conint', + 'PositiveInt', + 'NegativeInt', + 'NonNegativeInt', + 'NonPositiveInt', + 'ConstrainedFloat', + 'confloat', + 'PositiveFloat', + 'NegativeFloat', + 'NonNegativeFloat', + 'NonPositiveFloat', + 'FiniteFloat', + 'ConstrainedDecimal', + 'condecimal', + 'ConstrainedDate', + 'condate', + 'UUID1', + 'UUID3', + 'UUID4', + 'UUID5', + 'FilePath', + 'DirectoryPath', + 'Json', + 'JsonWrapper', + 'SecretField', + 'SecretStr', + 'SecretBytes', + 'StrictBool', + 'StrictBytes', + 'StrictInt', + 'StrictFloat', + 'PaymentCardNumber', + 'PrivateAttr', + 'ByteSize', + 'PastDate', + 'FutureDate', + # version + 'compiled', + 'VERSION', +] diff --git a/source/target-vendors/cp312/pydantic/v1/_hypothesis_plugin.py b/source/target-vendors/cp312/pydantic/v1/_hypothesis_plugin.py new file mode 100644 index 0000000..b62234d --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/_hypothesis_plugin.py @@ -0,0 +1,391 @@ +""" +Register Hypothesis strategies for Pydantic custom types. + +This enables fully-automatic generation of test data for most Pydantic classes. + +Note that this module has *no* runtime impact on Pydantic itself; instead it +is registered as a setuptools entry point and Hypothesis will import it if +Pydantic is installed. See also: + +https://hypothesis.readthedocs.io/en/latest/strategies.html#registering-strategies-via-setuptools-entry-points +https://hypothesis.readthedocs.io/en/latest/data.html#hypothesis.strategies.register_type_strategy +https://hypothesis.readthedocs.io/en/latest/strategies.html#interaction-with-pytest-cov +https://docs.pydantic.dev/usage/types/#pydantic-types + +Note that because our motivation is to *improve user experience*, the strategies +are always sound (never generate invalid data) but sacrifice completeness for +maintainability (ie may be unable to generate some tricky but valid data). + +Finally, this module makes liberal use of `# type: ignore[]` pragmas. +This is because Hypothesis annotates `register_type_strategy()` with +`(T, SearchStrategy[T])`, but in most cases we register e.g. `ConstrainedInt` +to generate instances of the builtin `int` type which match the constraints. +""" + +import contextlib +import datetime +import ipaddress +import json +import math +from fractions import Fraction +from typing import Callable, Dict, Type, Union, cast, overload + +import hypothesis.strategies as st + +import pydantic +import pydantic.color +import pydantic.types +from pydantic.v1.utils import lenient_issubclass + +# FilePath and DirectoryPath are explicitly unsupported, as we'd have to create +# them on-disk, and that's unsafe in general without being told *where* to do so. +# +# URLs are unsupported because it's easy for users to define their own strategy for +# "normal" URLs, and hard for us to define a general strategy which includes "weird" +# URLs but doesn't also have unpredictable performance problems. +# +# conlist() and conset() are unsupported for now, because the workarounds for +# Cython and Hypothesis to handle parametrized generic types are incompatible. +# We are rethinking Hypothesis compatibility in Pydantic v2. + +# Emails +try: + import email_validator +except ImportError: # pragma: no cover + pass +else: + + def is_valid_email(s: str) -> bool: + # Hypothesis' st.emails() occasionally generates emails like 0@A0--0.ac + # that are invalid according to email-validator, so we filter those out. + try: + email_validator.validate_email(s, check_deliverability=False) + return True + except email_validator.EmailNotValidError: # pragma: no cover + return False + + # Note that these strategies deliberately stay away from any tricky Unicode + # or other encoding issues; we're just trying to generate *something* valid. + st.register_type_strategy(pydantic.EmailStr, st.emails().filter(is_valid_email)) # type: ignore[arg-type] + st.register_type_strategy( + pydantic.NameEmail, + st.builds( + '{} <{}>'.format, # type: ignore[arg-type] + st.from_regex('[A-Za-z0-9_]+( [A-Za-z0-9_]+){0,5}', fullmatch=True), + st.emails().filter(is_valid_email), + ), + ) + +# PyObject - dotted names, in this case taken from the math module. +st.register_type_strategy( + pydantic.PyObject, # type: ignore[arg-type] + st.sampled_from( + [cast(pydantic.PyObject, f'math.{name}') for name in sorted(vars(math)) if not name.startswith('_')] + ), +) + +# CSS3 Colors; as name, hex, rgb(a) tuples or strings, or hsl strings +_color_regexes = ( + '|'.join( + ( + pydantic.color.r_hex_short, + pydantic.color.r_hex_long, + pydantic.color.r_rgb, + pydantic.color.r_rgba, + pydantic.color.r_hsl, + pydantic.color.r_hsla, + ) + ) + # Use more precise regex patterns to avoid value-out-of-range errors + .replace(pydantic.color._r_sl, r'(?:(\d\d?(?:\.\d+)?|100(?:\.0+)?)%)') + .replace(pydantic.color._r_alpha, r'(?:(0(?:\.\d+)?|1(?:\.0+)?|\.\d+|\d{1,2}%))') + .replace(pydantic.color._r_255, r'(?:((?:\d|\d\d|[01]\d\d|2[0-4]\d|25[0-4])(?:\.\d+)?|255(?:\.0+)?))') +) +st.register_type_strategy( + pydantic.color.Color, + st.one_of( + st.sampled_from(sorted(pydantic.color.COLORS_BY_NAME)), + st.tuples( + st.integers(0, 255), + st.integers(0, 255), + st.integers(0, 255), + st.none() | st.floats(0, 1) | st.floats(0, 100).map('{}%'.format), + ), + st.from_regex(_color_regexes, fullmatch=True), + ), +) + + +# Card numbers, valid according to the Luhn algorithm + + +def add_luhn_digit(card_number: str) -> str: + # See https://en.wikipedia.org/wiki/Luhn_algorithm + for digit in '0123456789': + with contextlib.suppress(Exception): + pydantic.PaymentCardNumber.validate_luhn_check_digit(card_number + digit) + return card_number + digit + raise AssertionError('Unreachable') # pragma: no cover + + +card_patterns = ( + # Note that these patterns omit the Luhn check digit; that's added by the function above + '4[0-9]{14}', # Visa + '5[12345][0-9]{13}', # Mastercard + '3[47][0-9]{12}', # American Express + '[0-26-9][0-9]{10,17}', # other (incomplete to avoid overlap) +) +st.register_type_strategy( + pydantic.PaymentCardNumber, + st.from_regex('|'.join(card_patterns), fullmatch=True).map(add_luhn_digit), # type: ignore[arg-type] +) + +# UUIDs +st.register_type_strategy(pydantic.UUID1, st.uuids(version=1)) +st.register_type_strategy(pydantic.UUID3, st.uuids(version=3)) +st.register_type_strategy(pydantic.UUID4, st.uuids(version=4)) +st.register_type_strategy(pydantic.UUID5, st.uuids(version=5)) + +# Secrets +st.register_type_strategy(pydantic.SecretBytes, st.binary().map(pydantic.SecretBytes)) +st.register_type_strategy(pydantic.SecretStr, st.text().map(pydantic.SecretStr)) + +# IP addresses, networks, and interfaces +st.register_type_strategy(pydantic.IPvAnyAddress, st.ip_addresses()) # type: ignore[arg-type] +st.register_type_strategy( + pydantic.IPvAnyInterface, + st.from_type(ipaddress.IPv4Interface) | st.from_type(ipaddress.IPv6Interface), # type: ignore[arg-type] +) +st.register_type_strategy( + pydantic.IPvAnyNetwork, + st.from_type(ipaddress.IPv4Network) | st.from_type(ipaddress.IPv6Network), # type: ignore[arg-type] +) + +# We hook into the con***() functions and the ConstrainedNumberMeta metaclass, +# so here we only have to register subclasses for other constrained types which +# don't go via those mechanisms. Then there are the registration hooks below. +st.register_type_strategy(pydantic.StrictBool, st.booleans()) +st.register_type_strategy(pydantic.StrictStr, st.text()) + + +# FutureDate, PastDate +st.register_type_strategy(pydantic.FutureDate, st.dates(min_value=datetime.date.today() + datetime.timedelta(days=1))) +st.register_type_strategy(pydantic.PastDate, st.dates(max_value=datetime.date.today() - datetime.timedelta(days=1))) + + +# Constrained-type resolver functions +# +# For these ones, we actually want to inspect the type in order to work out a +# satisfying strategy. First up, the machinery for tracking resolver functions: + +RESOLVERS: Dict[type, Callable[[type], st.SearchStrategy]] = {} # type: ignore[type-arg] + + +@overload +def _registered(typ: Type[pydantic.types.T]) -> Type[pydantic.types.T]: + pass + + +@overload +def _registered(typ: pydantic.types.ConstrainedNumberMeta) -> pydantic.types.ConstrainedNumberMeta: + pass + + +def _registered( + typ: Union[Type[pydantic.types.T], pydantic.types.ConstrainedNumberMeta] +) -> Union[Type[pydantic.types.T], pydantic.types.ConstrainedNumberMeta]: + # This function replaces the version in `pydantic.types`, in order to + # effect the registration of new constrained types so that Hypothesis + # can generate valid examples. + pydantic.types._DEFINED_TYPES.add(typ) + for supertype, resolver in RESOLVERS.items(): + if issubclass(typ, supertype): + st.register_type_strategy(typ, resolver(typ)) # type: ignore + return typ + raise NotImplementedError(f'Unknown type {typ!r} has no resolver to register') # pragma: no cover + + +def resolves( + typ: Union[type, pydantic.types.ConstrainedNumberMeta] +) -> Callable[[Callable[..., st.SearchStrategy]], Callable[..., st.SearchStrategy]]: # type: ignore[type-arg] + def inner(f): # type: ignore + assert f not in RESOLVERS + RESOLVERS[typ] = f + return f + + return inner + + +# Type-to-strategy resolver functions + + +@resolves(pydantic.JsonWrapper) +def resolve_json(cls): # type: ignore[no-untyped-def] + try: + inner = st.none() if cls.inner_type is None else st.from_type(cls.inner_type) + except Exception: # pragma: no cover + finite = st.floats(allow_infinity=False, allow_nan=False) + inner = st.recursive( + base=st.one_of(st.none(), st.booleans(), st.integers(), finite, st.text()), + extend=lambda x: st.lists(x) | st.dictionaries(st.text(), x), # type: ignore + ) + inner_type = getattr(cls, 'inner_type', None) + return st.builds( + cls.inner_type.json if lenient_issubclass(inner_type, pydantic.BaseModel) else json.dumps, + inner, + ensure_ascii=st.booleans(), + indent=st.none() | st.integers(0, 16), + sort_keys=st.booleans(), + ) + + +@resolves(pydantic.ConstrainedBytes) +def resolve_conbytes(cls): # type: ignore[no-untyped-def] # pragma: no cover + min_size = cls.min_length or 0 + max_size = cls.max_length + if not cls.strip_whitespace: + return st.binary(min_size=min_size, max_size=max_size) + # Fun with regex to ensure we neither start nor end with whitespace + repeats = '{{{},{}}}'.format( + min_size - 2 if min_size > 2 else 0, + max_size - 2 if (max_size or 0) > 2 else '', + ) + if min_size >= 2: + pattern = rf'\W.{repeats}\W' + elif min_size == 1: + pattern = rf'\W(.{repeats}\W)?' + else: + assert min_size == 0 + pattern = rf'(\W(.{repeats}\W)?)?' + return st.from_regex(pattern.encode(), fullmatch=True) + + +@resolves(pydantic.ConstrainedDecimal) +def resolve_condecimal(cls): # type: ignore[no-untyped-def] + min_value = cls.ge + max_value = cls.le + if cls.gt is not None: + assert min_value is None, 'Set `gt` or `ge`, but not both' + min_value = cls.gt + if cls.lt is not None: + assert max_value is None, 'Set `lt` or `le`, but not both' + max_value = cls.lt + s = st.decimals(min_value, max_value, allow_nan=False, places=cls.decimal_places) + if cls.lt is not None: + s = s.filter(lambda d: d < cls.lt) + if cls.gt is not None: + s = s.filter(lambda d: cls.gt < d) + return s + + +@resolves(pydantic.ConstrainedFloat) +def resolve_confloat(cls): # type: ignore[no-untyped-def] + min_value = cls.ge + max_value = cls.le + exclude_min = False + exclude_max = False + + if cls.gt is not None: + assert min_value is None, 'Set `gt` or `ge`, but not both' + min_value = cls.gt + exclude_min = True + if cls.lt is not None: + assert max_value is None, 'Set `lt` or `le`, but not both' + max_value = cls.lt + exclude_max = True + + if cls.multiple_of is None: + return st.floats(min_value, max_value, exclude_min=exclude_min, exclude_max=exclude_max, allow_nan=False) + + if min_value is not None: + min_value = math.ceil(min_value / cls.multiple_of) + if exclude_min: + min_value = min_value + 1 + if max_value is not None: + assert max_value >= cls.multiple_of, 'Cannot build model with max value smaller than multiple of' + max_value = math.floor(max_value / cls.multiple_of) + if exclude_max: + max_value = max_value - 1 + + return st.integers(min_value, max_value).map(lambda x: x * cls.multiple_of) + + +@resolves(pydantic.ConstrainedInt) +def resolve_conint(cls): # type: ignore[no-untyped-def] + min_value = cls.ge + max_value = cls.le + if cls.gt is not None: + assert min_value is None, 'Set `gt` or `ge`, but not both' + min_value = cls.gt + 1 + if cls.lt is not None: + assert max_value is None, 'Set `lt` or `le`, but not both' + max_value = cls.lt - 1 + + if cls.multiple_of is None or cls.multiple_of == 1: + return st.integers(min_value, max_value) + + # These adjustments and the .map handle integer-valued multiples, while the + # .filter handles trickier cases as for confloat. + if min_value is not None: + min_value = math.ceil(Fraction(min_value) / Fraction(cls.multiple_of)) + if max_value is not None: + max_value = math.floor(Fraction(max_value) / Fraction(cls.multiple_of)) + return st.integers(min_value, max_value).map(lambda x: x * cls.multiple_of) + + +@resolves(pydantic.ConstrainedDate) +def resolve_condate(cls): # type: ignore[no-untyped-def] + if cls.ge is not None: + assert cls.gt is None, 'Set `gt` or `ge`, but not both' + min_value = cls.ge + elif cls.gt is not None: + min_value = cls.gt + datetime.timedelta(days=1) + else: + min_value = datetime.date.min + if cls.le is not None: + assert cls.lt is None, 'Set `lt` or `le`, but not both' + max_value = cls.le + elif cls.lt is not None: + max_value = cls.lt - datetime.timedelta(days=1) + else: + max_value = datetime.date.max + return st.dates(min_value, max_value) + + +@resolves(pydantic.ConstrainedStr) +def resolve_constr(cls): # type: ignore[no-untyped-def] # pragma: no cover + min_size = cls.min_length or 0 + max_size = cls.max_length + + if cls.regex is None and not cls.strip_whitespace: + return st.text(min_size=min_size, max_size=max_size) + + if cls.regex is not None: + strategy = st.from_regex(cls.regex) + if cls.strip_whitespace: + strategy = strategy.filter(lambda s: s == s.strip()) + elif cls.strip_whitespace: + repeats = '{{{},{}}}'.format( + min_size - 2 if min_size > 2 else 0, + max_size - 2 if (max_size or 0) > 2 else '', + ) + if min_size >= 2: + strategy = st.from_regex(rf'\W.{repeats}\W') + elif min_size == 1: + strategy = st.from_regex(rf'\W(.{repeats}\W)?') + else: + assert min_size == 0 + strategy = st.from_regex(rf'(\W(.{repeats}\W)?)?') + + if min_size == 0 and max_size is None: + return strategy + elif max_size is None: + return strategy.filter(lambda s: min_size <= len(s)) + return strategy.filter(lambda s: min_size <= len(s) <= max_size) + + +# Finally, register all previously-defined types, and patch in our new function +for typ in list(pydantic.types._DEFINED_TYPES): + _registered(typ) +pydantic.types._registered = _registered +st.register_type_strategy(pydantic.Json, resolve_json) diff --git a/source/target-vendors/cp312/pydantic/v1/annotated_types.py b/source/target-vendors/cp312/pydantic/v1/annotated_types.py new file mode 100644 index 0000000..d9eaaaf --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/annotated_types.py @@ -0,0 +1,72 @@ +import sys +from typing import TYPE_CHECKING, Any, Dict, FrozenSet, NamedTuple, Type + +from pydantic.v1.fields import Required +from pydantic.v1.main import BaseModel, create_model +from pydantic.v1.typing import is_typeddict, is_typeddict_special + +if TYPE_CHECKING: + from typing_extensions import TypedDict + +if sys.version_info < (3, 11): + + def is_legacy_typeddict(typeddict_cls: Type['TypedDict']) -> bool: # type: ignore[valid-type] + return is_typeddict(typeddict_cls) and type(typeddict_cls).__module__ == 'typing' + +else: + + def is_legacy_typeddict(_: Any) -> Any: + return False + + +def create_model_from_typeddict( + # Mypy bug: `Type[TypedDict]` is resolved as `Any` https://github.com/python/mypy/issues/11030 + typeddict_cls: Type['TypedDict'], # type: ignore[valid-type] + **kwargs: Any, +) -> Type['BaseModel']: + """ + Create a `BaseModel` based on the fields of a `TypedDict`. + Since `typing.TypedDict` in Python 3.8 does not store runtime information about optional keys, + we raise an error if this happens (see https://bugs.python.org/issue38834). + """ + field_definitions: Dict[str, Any] + + # Best case scenario: with python 3.9+ or when `TypedDict` is imported from `typing_extensions` + if not hasattr(typeddict_cls, '__required_keys__'): + raise TypeError( + 'You should use `typing_extensions.TypedDict` instead of `typing.TypedDict` with Python < 3.9.2. ' + 'Without it, there is no way to differentiate required and optional fields when subclassed.' + ) + + if is_legacy_typeddict(typeddict_cls) and any( + is_typeddict_special(t) for t in typeddict_cls.__annotations__.values() + ): + raise TypeError( + 'You should use `typing_extensions.TypedDict` instead of `typing.TypedDict` with Python < 3.11. ' + 'Without it, there is no way to reflect Required/NotRequired keys.' + ) + + required_keys: FrozenSet[str] = typeddict_cls.__required_keys__ # type: ignore[attr-defined] + field_definitions = { + field_name: (field_type, Required if field_name in required_keys else None) + for field_name, field_type in typeddict_cls.__annotations__.items() + } + + return create_model(typeddict_cls.__name__, **kwargs, **field_definitions) + + +def create_model_from_namedtuple(namedtuple_cls: Type['NamedTuple'], **kwargs: Any) -> Type['BaseModel']: + """ + Create a `BaseModel` based on the fields of a named tuple. + A named tuple can be created with `typing.NamedTuple` and declared annotations + but also with `collections.namedtuple`, in this case we consider all fields + to have type `Any`. + """ + # With python 3.10+, `__annotations__` always exists but can be empty hence the `getattr... or...` logic + namedtuple_annotations: Dict[str, Type[Any]] = getattr(namedtuple_cls, '__annotations__', None) or { + k: Any for k in namedtuple_cls._fields + } + field_definitions: Dict[str, Any] = { + field_name: (field_type, Required) for field_name, field_type in namedtuple_annotations.items() + } + return create_model(namedtuple_cls.__name__, **kwargs, **field_definitions) diff --git a/source/target-vendors/cp312/pydantic/v1/class_validators.py b/source/target-vendors/cp312/pydantic/v1/class_validators.py new file mode 100644 index 0000000..2f68fc8 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/class_validators.py @@ -0,0 +1,361 @@ +import warnings +from collections import ChainMap +from functools import partial, partialmethod, wraps +from itertools import chain +from types import FunctionType +from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Type, Union, overload + +from pydantic.v1.errors import ConfigError +from pydantic.v1.typing import AnyCallable +from pydantic.v1.utils import ROOT_KEY, in_ipython + +if TYPE_CHECKING: + from pydantic.v1.typing import AnyClassMethod + + +class Validator: + __slots__ = 'func', 'pre', 'each_item', 'always', 'check_fields', 'skip_on_failure' + + def __init__( + self, + func: AnyCallable, + pre: bool = False, + each_item: bool = False, + always: bool = False, + check_fields: bool = False, + skip_on_failure: bool = False, + ): + self.func = func + self.pre = pre + self.each_item = each_item + self.always = always + self.check_fields = check_fields + self.skip_on_failure = skip_on_failure + + +if TYPE_CHECKING: + from inspect import Signature + + from pydantic.v1.config import BaseConfig + from pydantic.v1.fields import ModelField + from pydantic.v1.types import ModelOrDc + + ValidatorCallable = Callable[[Optional[ModelOrDc], Any, Dict[str, Any], ModelField, Type[BaseConfig]], Any] + ValidatorsList = List[ValidatorCallable] + ValidatorListDict = Dict[str, List[Validator]] + +_FUNCS: Set[str] = set() +VALIDATOR_CONFIG_KEY = '__validator_config__' +ROOT_VALIDATOR_CONFIG_KEY = '__root_validator_config__' + + +def validator( + *fields: str, + pre: bool = False, + each_item: bool = False, + always: bool = False, + check_fields: bool = True, + whole: Optional[bool] = None, + allow_reuse: bool = False, +) -> Callable[[AnyCallable], 'AnyClassMethod']: + """ + Decorate methods on the class indicating that they should be used to validate fields + :param fields: which field(s) the method should be called on + :param pre: whether or not this validator should be called before the standard validators (else after) + :param each_item: for complex objects (sets, lists etc.) whether to validate individual elements rather than the + whole object + :param always: whether this method and other validators should be called even if the value is missing + :param check_fields: whether to check that the fields actually exist on the model + :param allow_reuse: whether to track and raise an error if another validator refers to the decorated function + """ + if not fields: + raise ConfigError('validator with no fields specified') + elif isinstance(fields[0], FunctionType): + raise ConfigError( + "validators should be used with fields and keyword arguments, not bare. " # noqa: Q000 + "E.g. usage should be `@validator('', ...)`" + ) + elif not all(isinstance(field, str) for field in fields): + raise ConfigError( + "validator fields should be passed as separate string args. " # noqa: Q000 + "E.g. usage should be `@validator('', '', ...)`" + ) + + if whole is not None: + warnings.warn( + 'The "whole" keyword argument is deprecated, use "each_item" (inverse meaning, default False) instead', + DeprecationWarning, + ) + assert each_item is False, '"each_item" and "whole" conflict, remove "whole"' + each_item = not whole + + def dec(f: AnyCallable) -> 'AnyClassMethod': + f_cls = _prepare_validator(f, allow_reuse) + setattr( + f_cls, + VALIDATOR_CONFIG_KEY, + ( + fields, + Validator(func=f_cls.__func__, pre=pre, each_item=each_item, always=always, check_fields=check_fields), + ), + ) + return f_cls + + return dec + + +@overload +def root_validator(_func: AnyCallable) -> 'AnyClassMethod': + ... + + +@overload +def root_validator( + *, pre: bool = False, allow_reuse: bool = False, skip_on_failure: bool = False +) -> Callable[[AnyCallable], 'AnyClassMethod']: + ... + + +def root_validator( + _func: Optional[AnyCallable] = None, *, pre: bool = False, allow_reuse: bool = False, skip_on_failure: bool = False +) -> Union['AnyClassMethod', Callable[[AnyCallable], 'AnyClassMethod']]: + """ + Decorate methods on a model indicating that they should be used to validate (and perhaps modify) data either + before or after standard model parsing/validation is performed. + """ + if _func: + f_cls = _prepare_validator(_func, allow_reuse) + setattr( + f_cls, ROOT_VALIDATOR_CONFIG_KEY, Validator(func=f_cls.__func__, pre=pre, skip_on_failure=skip_on_failure) + ) + return f_cls + + def dec(f: AnyCallable) -> 'AnyClassMethod': + f_cls = _prepare_validator(f, allow_reuse) + setattr( + f_cls, ROOT_VALIDATOR_CONFIG_KEY, Validator(func=f_cls.__func__, pre=pre, skip_on_failure=skip_on_failure) + ) + return f_cls + + return dec + + +def _prepare_validator(function: AnyCallable, allow_reuse: bool) -> 'AnyClassMethod': + """ + Avoid validators with duplicated names since without this, validators can be overwritten silently + which generally isn't the intended behaviour, don't run in ipython (see #312) or if allow_reuse is False. + """ + f_cls = function if isinstance(function, classmethod) else classmethod(function) + if not in_ipython() and not allow_reuse: + ref = ( + getattr(f_cls.__func__, '__module__', '') + + '.' + + getattr(f_cls.__func__, '__qualname__', f'') + ) + if ref in _FUNCS: + raise ConfigError(f'duplicate validator function "{ref}"; if this is intended, set `allow_reuse=True`') + _FUNCS.add(ref) + return f_cls + + +class ValidatorGroup: + def __init__(self, validators: 'ValidatorListDict') -> None: + self.validators = validators + self.used_validators = {'*'} + + def get_validators(self, name: str) -> Optional[Dict[str, Validator]]: + self.used_validators.add(name) + validators = self.validators.get(name, []) + if name != ROOT_KEY: + validators += self.validators.get('*', []) + if validators: + return {getattr(v.func, '__name__', f''): v for v in validators} + else: + return None + + def check_for_unused(self) -> None: + unused_validators = set( + chain.from_iterable( + ( + getattr(v.func, '__name__', f'') + for v in self.validators[f] + if v.check_fields + ) + for f in (self.validators.keys() - self.used_validators) + ) + ) + if unused_validators: + fn = ', '.join(unused_validators) + raise ConfigError( + f"Validators defined with incorrect fields: {fn} " # noqa: Q000 + f"(use check_fields=False if you're inheriting from the model and intended this)" + ) + + +def extract_validators(namespace: Dict[str, Any]) -> Dict[str, List[Validator]]: + validators: Dict[str, List[Validator]] = {} + for var_name, value in namespace.items(): + validator_config = getattr(value, VALIDATOR_CONFIG_KEY, None) + if validator_config: + fields, v = validator_config + for field in fields: + if field in validators: + validators[field].append(v) + else: + validators[field] = [v] + return validators + + +def extract_root_validators(namespace: Dict[str, Any]) -> Tuple[List[AnyCallable], List[Tuple[bool, AnyCallable]]]: + from inspect import signature + + pre_validators: List[AnyCallable] = [] + post_validators: List[Tuple[bool, AnyCallable]] = [] + for name, value in namespace.items(): + validator_config: Optional[Validator] = getattr(value, ROOT_VALIDATOR_CONFIG_KEY, None) + if validator_config: + sig = signature(validator_config.func) + args = list(sig.parameters.keys()) + if args[0] == 'self': + raise ConfigError( + f'Invalid signature for root validator {name}: {sig}, "self" not permitted as first argument, ' + f'should be: (cls, values).' + ) + if len(args) != 2: + raise ConfigError(f'Invalid signature for root validator {name}: {sig}, should be: (cls, values).') + # check function signature + if validator_config.pre: + pre_validators.append(validator_config.func) + else: + post_validators.append((validator_config.skip_on_failure, validator_config.func)) + return pre_validators, post_validators + + +def inherit_validators(base_validators: 'ValidatorListDict', validators: 'ValidatorListDict') -> 'ValidatorListDict': + for field, field_validators in base_validators.items(): + if field not in validators: + validators[field] = [] + validators[field] += field_validators + return validators + + +def make_generic_validator(validator: AnyCallable) -> 'ValidatorCallable': + """ + Make a generic function which calls a validator with the right arguments. + + Unfortunately other approaches (eg. return a partial of a function that builds the arguments) is slow, + hence this laborious way of doing things. + + It's done like this so validators don't all need **kwargs in their signature, eg. any combination of + the arguments "values", "fields" and/or "config" are permitted. + """ + from inspect import signature + + if not isinstance(validator, (partial, partialmethod)): + # This should be the default case, so overhead is reduced + sig = signature(validator) + args = list(sig.parameters.keys()) + else: + # Fix the generated argument lists of partial methods + sig = signature(validator.func) + args = [ + k + for k in signature(validator.func).parameters.keys() + if k not in validator.args | validator.keywords.keys() + ] + + first_arg = args.pop(0) + if first_arg == 'self': + raise ConfigError( + f'Invalid signature for validator {validator}: {sig}, "self" not permitted as first argument, ' + f'should be: (cls, value, values, config, field), "values", "config" and "field" are all optional.' + ) + elif first_arg == 'cls': + # assume the second argument is value + return wraps(validator)(_generic_validator_cls(validator, sig, set(args[1:]))) + else: + # assume the first argument was value which has already been removed + return wraps(validator)(_generic_validator_basic(validator, sig, set(args))) + + +def prep_validators(v_funcs: Iterable[AnyCallable]) -> 'ValidatorsList': + return [make_generic_validator(f) for f in v_funcs if f] + + +all_kwargs = {'values', 'field', 'config'} + + +def _generic_validator_cls(validator: AnyCallable, sig: 'Signature', args: Set[str]) -> 'ValidatorCallable': + # assume the first argument is value + has_kwargs = False + if 'kwargs' in args: + has_kwargs = True + args -= {'kwargs'} + + if not args.issubset(all_kwargs): + raise ConfigError( + f'Invalid signature for validator {validator}: {sig}, should be: ' + f'(cls, value, values, config, field), "values", "config" and "field" are all optional.' + ) + + if has_kwargs: + return lambda cls, v, values, field, config: validator(cls, v, values=values, field=field, config=config) + elif args == set(): + return lambda cls, v, values, field, config: validator(cls, v) + elif args == {'values'}: + return lambda cls, v, values, field, config: validator(cls, v, values=values) + elif args == {'field'}: + return lambda cls, v, values, field, config: validator(cls, v, field=field) + elif args == {'config'}: + return lambda cls, v, values, field, config: validator(cls, v, config=config) + elif args == {'values', 'field'}: + return lambda cls, v, values, field, config: validator(cls, v, values=values, field=field) + elif args == {'values', 'config'}: + return lambda cls, v, values, field, config: validator(cls, v, values=values, config=config) + elif args == {'field', 'config'}: + return lambda cls, v, values, field, config: validator(cls, v, field=field, config=config) + else: + # args == {'values', 'field', 'config'} + return lambda cls, v, values, field, config: validator(cls, v, values=values, field=field, config=config) + + +def _generic_validator_basic(validator: AnyCallable, sig: 'Signature', args: Set[str]) -> 'ValidatorCallable': + has_kwargs = False + if 'kwargs' in args: + has_kwargs = True + args -= {'kwargs'} + + if not args.issubset(all_kwargs): + raise ConfigError( + f'Invalid signature for validator {validator}: {sig}, should be: ' + f'(value, values, config, field), "values", "config" and "field" are all optional.' + ) + + if has_kwargs: + return lambda cls, v, values, field, config: validator(v, values=values, field=field, config=config) + elif args == set(): + return lambda cls, v, values, field, config: validator(v) + elif args == {'values'}: + return lambda cls, v, values, field, config: validator(v, values=values) + elif args == {'field'}: + return lambda cls, v, values, field, config: validator(v, field=field) + elif args == {'config'}: + return lambda cls, v, values, field, config: validator(v, config=config) + elif args == {'values', 'field'}: + return lambda cls, v, values, field, config: validator(v, values=values, field=field) + elif args == {'values', 'config'}: + return lambda cls, v, values, field, config: validator(v, values=values, config=config) + elif args == {'field', 'config'}: + return lambda cls, v, values, field, config: validator(v, field=field, config=config) + else: + # args == {'values', 'field', 'config'} + return lambda cls, v, values, field, config: validator(v, values=values, field=field, config=config) + + +def gather_all_validators(type_: 'ModelOrDc') -> Dict[str, 'AnyClassMethod']: + all_attributes = ChainMap(*[cls.__dict__ for cls in type_.__mro__]) # type: ignore[arg-type,var-annotated] + return { + k: v + for k, v in all_attributes.items() + if hasattr(v, VALIDATOR_CONFIG_KEY) or hasattr(v, ROOT_VALIDATOR_CONFIG_KEY) + } diff --git a/source/target-vendors/cp312/pydantic/v1/color.py b/source/target-vendors/cp312/pydantic/v1/color.py new file mode 100644 index 0000000..b0bbf78 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/color.py @@ -0,0 +1,494 @@ +""" +Color definitions are used as per CSS3 specification: +http://www.w3.org/TR/css3-color/#svg-color + +A few colors have multiple names referring to the sames colors, eg. `grey` and `gray` or `aqua` and `cyan`. + +In these cases the LAST color when sorted alphabetically takes preferences, +eg. Color((0, 255, 255)).as_named() == 'cyan' because "cyan" comes after "aqua". +""" +import math +import re +from colorsys import hls_to_rgb, rgb_to_hls +from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union, cast + +from pydantic.v1.errors import ColorError +from pydantic.v1.utils import Representation, almost_equal_floats + +if TYPE_CHECKING: + from pydantic.v1.typing import CallableGenerator, ReprArgs + +ColorTuple = Union[Tuple[int, int, int], Tuple[int, int, int, float]] +ColorType = Union[ColorTuple, str] +HslColorTuple = Union[Tuple[float, float, float], Tuple[float, float, float, float]] + + +class RGBA: + """ + Internal use only as a representation of a color. + """ + + __slots__ = 'r', 'g', 'b', 'alpha', '_tuple' + + def __init__(self, r: float, g: float, b: float, alpha: Optional[float]): + self.r = r + self.g = g + self.b = b + self.alpha = alpha + + self._tuple: Tuple[float, float, float, Optional[float]] = (r, g, b, alpha) + + def __getitem__(self, item: Any) -> Any: + return self._tuple[item] + + +# these are not compiled here to avoid import slowdown, they'll be compiled the first time they're used, then cached +r_hex_short = r'\s*(?:#|0x)?([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])?\s*' +r_hex_long = r'\s*(?:#|0x)?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?\s*' +_r_255 = r'(\d{1,3}(?:\.\d+)?)' +_r_comma = r'\s*,\s*' +r_rgb = fr'\s*rgb\(\s*{_r_255}{_r_comma}{_r_255}{_r_comma}{_r_255}\)\s*' +_r_alpha = r'(\d(?:\.\d+)?|\.\d+|\d{1,2}%)' +r_rgba = fr'\s*rgba\(\s*{_r_255}{_r_comma}{_r_255}{_r_comma}{_r_255}{_r_comma}{_r_alpha}\s*\)\s*' +_r_h = r'(-?\d+(?:\.\d+)?|-?\.\d+)(deg|rad|turn)?' +_r_sl = r'(\d{1,3}(?:\.\d+)?)%' +r_hsl = fr'\s*hsl\(\s*{_r_h}{_r_comma}{_r_sl}{_r_comma}{_r_sl}\s*\)\s*' +r_hsla = fr'\s*hsl\(\s*{_r_h}{_r_comma}{_r_sl}{_r_comma}{_r_sl}{_r_comma}{_r_alpha}\s*\)\s*' + +# colors where the two hex characters are the same, if all colors match this the short version of hex colors can be used +repeat_colors = {int(c * 2, 16) for c in '0123456789abcdef'} +rads = 2 * math.pi + + +class Color(Representation): + __slots__ = '_original', '_rgba' + + def __init__(self, value: ColorType) -> None: + self._rgba: RGBA + self._original: ColorType + if isinstance(value, (tuple, list)): + self._rgba = parse_tuple(value) + elif isinstance(value, str): + self._rgba = parse_str(value) + elif isinstance(value, Color): + self._rgba = value._rgba + value = value._original + else: + raise ColorError(reason='value must be a tuple, list or string') + + # if we've got here value must be a valid color + self._original = value + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update(type='string', format='color') + + def original(self) -> ColorType: + """ + Original value passed to Color + """ + return self._original + + def as_named(self, *, fallback: bool = False) -> str: + if self._rgba.alpha is None: + rgb = cast(Tuple[int, int, int], self.as_rgb_tuple()) + try: + return COLORS_BY_VALUE[rgb] + except KeyError as e: + if fallback: + return self.as_hex() + else: + raise ValueError('no named color found, use fallback=True, as_hex() or as_rgb()') from e + else: + return self.as_hex() + + def as_hex(self) -> str: + """ + Hex string representing the color can be 3, 4, 6 or 8 characters depending on whether the string + a "short" representation of the color is possible and whether there's an alpha channel. + """ + values = [float_to_255(c) for c in self._rgba[:3]] + if self._rgba.alpha is not None: + values.append(float_to_255(self._rgba.alpha)) + + as_hex = ''.join(f'{v:02x}' for v in values) + if all(c in repeat_colors for c in values): + as_hex = ''.join(as_hex[c] for c in range(0, len(as_hex), 2)) + return '#' + as_hex + + def as_rgb(self) -> str: + """ + Color as an rgb(, , ) or rgba(, , , ) string. + """ + if self._rgba.alpha is None: + return f'rgb({float_to_255(self._rgba.r)}, {float_to_255(self._rgba.g)}, {float_to_255(self._rgba.b)})' + else: + return ( + f'rgba({float_to_255(self._rgba.r)}, {float_to_255(self._rgba.g)}, {float_to_255(self._rgba.b)}, ' + f'{round(self._alpha_float(), 2)})' + ) + + def as_rgb_tuple(self, *, alpha: Optional[bool] = None) -> ColorTuple: + """ + Color as an RGB or RGBA tuple; red, green and blue are in the range 0 to 255, alpha if included is + in the range 0 to 1. + + :param alpha: whether to include the alpha channel, options are + None - (default) include alpha only if it's set (e.g. not None) + True - always include alpha, + False - always omit alpha, + """ + r, g, b = (float_to_255(c) for c in self._rgba[:3]) + if alpha is None: + if self._rgba.alpha is None: + return r, g, b + else: + return r, g, b, self._alpha_float() + elif alpha: + return r, g, b, self._alpha_float() + else: + # alpha is False + return r, g, b + + def as_hsl(self) -> str: + """ + Color as an hsl(, , ) or hsl(, , , ) string. + """ + if self._rgba.alpha is None: + h, s, li = self.as_hsl_tuple(alpha=False) # type: ignore + return f'hsl({h * 360:0.0f}, {s:0.0%}, {li:0.0%})' + else: + h, s, li, a = self.as_hsl_tuple(alpha=True) # type: ignore + return f'hsl({h * 360:0.0f}, {s:0.0%}, {li:0.0%}, {round(a, 2)})' + + def as_hsl_tuple(self, *, alpha: Optional[bool] = None) -> HslColorTuple: + """ + Color as an HSL or HSLA tuple, e.g. hue, saturation, lightness and optionally alpha; all elements are in + the range 0 to 1. + + NOTE: this is HSL as used in HTML and most other places, not HLS as used in python's colorsys. + + :param alpha: whether to include the alpha channel, options are + None - (default) include alpha only if it's set (e.g. not None) + True - always include alpha, + False - always omit alpha, + """ + h, l, s = rgb_to_hls(self._rgba.r, self._rgba.g, self._rgba.b) + if alpha is None: + if self._rgba.alpha is None: + return h, s, l + else: + return h, s, l, self._alpha_float() + if alpha: + return h, s, l, self._alpha_float() + else: + # alpha is False + return h, s, l + + def _alpha_float(self) -> float: + return 1 if self._rgba.alpha is None else self._rgba.alpha + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield cls + + def __str__(self) -> str: + return self.as_named(fallback=True) + + def __repr_args__(self) -> 'ReprArgs': + return [(None, self.as_named(fallback=True))] + [('rgb', self.as_rgb_tuple())] # type: ignore + + def __eq__(self, other: Any) -> bool: + return isinstance(other, Color) and self.as_rgb_tuple() == other.as_rgb_tuple() + + def __hash__(self) -> int: + return hash(self.as_rgb_tuple()) + + +def parse_tuple(value: Tuple[Any, ...]) -> RGBA: + """ + Parse a tuple or list as a color. + """ + if len(value) == 3: + r, g, b = (parse_color_value(v) for v in value) + return RGBA(r, g, b, None) + elif len(value) == 4: + r, g, b = (parse_color_value(v) for v in value[:3]) + return RGBA(r, g, b, parse_float_alpha(value[3])) + else: + raise ColorError(reason='tuples must have length 3 or 4') + + +def parse_str(value: str) -> RGBA: + """ + Parse a string to an RGBA tuple, trying the following formats (in this order): + * named color, see COLORS_BY_NAME below + * hex short eg. `fff` (prefix can be `#`, `0x` or nothing) + * hex long eg. `ffffff` (prefix can be `#`, `0x` or nothing) + * `rgb(, , ) ` + * `rgba(, , , )` + """ + value_lower = value.lower() + try: + r, g, b = COLORS_BY_NAME[value_lower] + except KeyError: + pass + else: + return ints_to_rgba(r, g, b, None) + + m = re.fullmatch(r_hex_short, value_lower) + if m: + *rgb, a = m.groups() + r, g, b = (int(v * 2, 16) for v in rgb) + if a: + alpha: Optional[float] = int(a * 2, 16) / 255 + else: + alpha = None + return ints_to_rgba(r, g, b, alpha) + + m = re.fullmatch(r_hex_long, value_lower) + if m: + *rgb, a = m.groups() + r, g, b = (int(v, 16) for v in rgb) + if a: + alpha = int(a, 16) / 255 + else: + alpha = None + return ints_to_rgba(r, g, b, alpha) + + m = re.fullmatch(r_rgb, value_lower) + if m: + return ints_to_rgba(*m.groups(), None) # type: ignore + + m = re.fullmatch(r_rgba, value_lower) + if m: + return ints_to_rgba(*m.groups()) # type: ignore + + m = re.fullmatch(r_hsl, value_lower) + if m: + h, h_units, s, l_ = m.groups() + return parse_hsl(h, h_units, s, l_) + + m = re.fullmatch(r_hsla, value_lower) + if m: + h, h_units, s, l_, a = m.groups() + return parse_hsl(h, h_units, s, l_, parse_float_alpha(a)) + + raise ColorError(reason='string not recognised as a valid color') + + +def ints_to_rgba(r: Union[int, str], g: Union[int, str], b: Union[int, str], alpha: Optional[float]) -> RGBA: + return RGBA(parse_color_value(r), parse_color_value(g), parse_color_value(b), parse_float_alpha(alpha)) + + +def parse_color_value(value: Union[int, str], max_val: int = 255) -> float: + """ + Parse a value checking it's a valid int in the range 0 to max_val and divide by max_val to give a number + in the range 0 to 1 + """ + try: + color = float(value) + except ValueError: + raise ColorError(reason='color values must be a valid number') + if 0 <= color <= max_val: + return color / max_val + else: + raise ColorError(reason=f'color values must be in the range 0 to {max_val}') + + +def parse_float_alpha(value: Union[None, str, float, int]) -> Optional[float]: + """ + Parse a value checking it's a valid float in the range 0 to 1 + """ + if value is None: + return None + try: + if isinstance(value, str) and value.endswith('%'): + alpha = float(value[:-1]) / 100 + else: + alpha = float(value) + except ValueError: + raise ColorError(reason='alpha values must be a valid float') + + if almost_equal_floats(alpha, 1): + return None + elif 0 <= alpha <= 1: + return alpha + else: + raise ColorError(reason='alpha values must be in the range 0 to 1') + + +def parse_hsl(h: str, h_units: str, sat: str, light: str, alpha: Optional[float] = None) -> RGBA: + """ + Parse raw hue, saturation, lightness and alpha values and convert to RGBA. + """ + s_value, l_value = parse_color_value(sat, 100), parse_color_value(light, 100) + + h_value = float(h) + if h_units in {None, 'deg'}: + h_value = h_value % 360 / 360 + elif h_units == 'rad': + h_value = h_value % rads / rads + else: + # turns + h_value = h_value % 1 + + r, g, b = hls_to_rgb(h_value, l_value, s_value) + return RGBA(r, g, b, alpha) + + +def float_to_255(c: float) -> int: + return int(round(c * 255)) + + +COLORS_BY_NAME = { + 'aliceblue': (240, 248, 255), + 'antiquewhite': (250, 235, 215), + 'aqua': (0, 255, 255), + 'aquamarine': (127, 255, 212), + 'azure': (240, 255, 255), + 'beige': (245, 245, 220), + 'bisque': (255, 228, 196), + 'black': (0, 0, 0), + 'blanchedalmond': (255, 235, 205), + 'blue': (0, 0, 255), + 'blueviolet': (138, 43, 226), + 'brown': (165, 42, 42), + 'burlywood': (222, 184, 135), + 'cadetblue': (95, 158, 160), + 'chartreuse': (127, 255, 0), + 'chocolate': (210, 105, 30), + 'coral': (255, 127, 80), + 'cornflowerblue': (100, 149, 237), + 'cornsilk': (255, 248, 220), + 'crimson': (220, 20, 60), + 'cyan': (0, 255, 255), + 'darkblue': (0, 0, 139), + 'darkcyan': (0, 139, 139), + 'darkgoldenrod': (184, 134, 11), + 'darkgray': (169, 169, 169), + 'darkgreen': (0, 100, 0), + 'darkgrey': (169, 169, 169), + 'darkkhaki': (189, 183, 107), + 'darkmagenta': (139, 0, 139), + 'darkolivegreen': (85, 107, 47), + 'darkorange': (255, 140, 0), + 'darkorchid': (153, 50, 204), + 'darkred': (139, 0, 0), + 'darksalmon': (233, 150, 122), + 'darkseagreen': (143, 188, 143), + 'darkslateblue': (72, 61, 139), + 'darkslategray': (47, 79, 79), + 'darkslategrey': (47, 79, 79), + 'darkturquoise': (0, 206, 209), + 'darkviolet': (148, 0, 211), + 'deeppink': (255, 20, 147), + 'deepskyblue': (0, 191, 255), + 'dimgray': (105, 105, 105), + 'dimgrey': (105, 105, 105), + 'dodgerblue': (30, 144, 255), + 'firebrick': (178, 34, 34), + 'floralwhite': (255, 250, 240), + 'forestgreen': (34, 139, 34), + 'fuchsia': (255, 0, 255), + 'gainsboro': (220, 220, 220), + 'ghostwhite': (248, 248, 255), + 'gold': (255, 215, 0), + 'goldenrod': (218, 165, 32), + 'gray': (128, 128, 128), + 'green': (0, 128, 0), + 'greenyellow': (173, 255, 47), + 'grey': (128, 128, 128), + 'honeydew': (240, 255, 240), + 'hotpink': (255, 105, 180), + 'indianred': (205, 92, 92), + 'indigo': (75, 0, 130), + 'ivory': (255, 255, 240), + 'khaki': (240, 230, 140), + 'lavender': (230, 230, 250), + 'lavenderblush': (255, 240, 245), + 'lawngreen': (124, 252, 0), + 'lemonchiffon': (255, 250, 205), + 'lightblue': (173, 216, 230), + 'lightcoral': (240, 128, 128), + 'lightcyan': (224, 255, 255), + 'lightgoldenrodyellow': (250, 250, 210), + 'lightgray': (211, 211, 211), + 'lightgreen': (144, 238, 144), + 'lightgrey': (211, 211, 211), + 'lightpink': (255, 182, 193), + 'lightsalmon': (255, 160, 122), + 'lightseagreen': (32, 178, 170), + 'lightskyblue': (135, 206, 250), + 'lightslategray': (119, 136, 153), + 'lightslategrey': (119, 136, 153), + 'lightsteelblue': (176, 196, 222), + 'lightyellow': (255, 255, 224), + 'lime': (0, 255, 0), + 'limegreen': (50, 205, 50), + 'linen': (250, 240, 230), + 'magenta': (255, 0, 255), + 'maroon': (128, 0, 0), + 'mediumaquamarine': (102, 205, 170), + 'mediumblue': (0, 0, 205), + 'mediumorchid': (186, 85, 211), + 'mediumpurple': (147, 112, 219), + 'mediumseagreen': (60, 179, 113), + 'mediumslateblue': (123, 104, 238), + 'mediumspringgreen': (0, 250, 154), + 'mediumturquoise': (72, 209, 204), + 'mediumvioletred': (199, 21, 133), + 'midnightblue': (25, 25, 112), + 'mintcream': (245, 255, 250), + 'mistyrose': (255, 228, 225), + 'moccasin': (255, 228, 181), + 'navajowhite': (255, 222, 173), + 'navy': (0, 0, 128), + 'oldlace': (253, 245, 230), + 'olive': (128, 128, 0), + 'olivedrab': (107, 142, 35), + 'orange': (255, 165, 0), + 'orangered': (255, 69, 0), + 'orchid': (218, 112, 214), + 'palegoldenrod': (238, 232, 170), + 'palegreen': (152, 251, 152), + 'paleturquoise': (175, 238, 238), + 'palevioletred': (219, 112, 147), + 'papayawhip': (255, 239, 213), + 'peachpuff': (255, 218, 185), + 'peru': (205, 133, 63), + 'pink': (255, 192, 203), + 'plum': (221, 160, 221), + 'powderblue': (176, 224, 230), + 'purple': (128, 0, 128), + 'red': (255, 0, 0), + 'rosybrown': (188, 143, 143), + 'royalblue': (65, 105, 225), + 'saddlebrown': (139, 69, 19), + 'salmon': (250, 128, 114), + 'sandybrown': (244, 164, 96), + 'seagreen': (46, 139, 87), + 'seashell': (255, 245, 238), + 'sienna': (160, 82, 45), + 'silver': (192, 192, 192), + 'skyblue': (135, 206, 235), + 'slateblue': (106, 90, 205), + 'slategray': (112, 128, 144), + 'slategrey': (112, 128, 144), + 'snow': (255, 250, 250), + 'springgreen': (0, 255, 127), + 'steelblue': (70, 130, 180), + 'tan': (210, 180, 140), + 'teal': (0, 128, 128), + 'thistle': (216, 191, 216), + 'tomato': (255, 99, 71), + 'turquoise': (64, 224, 208), + 'violet': (238, 130, 238), + 'wheat': (245, 222, 179), + 'white': (255, 255, 255), + 'whitesmoke': (245, 245, 245), + 'yellow': (255, 255, 0), + 'yellowgreen': (154, 205, 50), +} + +COLORS_BY_VALUE = {v: k for k, v in COLORS_BY_NAME.items()} diff --git a/source/target-vendors/cp312/pydantic/v1/config.py b/source/target-vendors/cp312/pydantic/v1/config.py new file mode 100644 index 0000000..18f7c99 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/config.py @@ -0,0 +1,191 @@ +import json +from enum import Enum +from typing import TYPE_CHECKING, Any, Callable, Dict, ForwardRef, Optional, Tuple, Type, Union + +from typing_extensions import Literal, Protocol + +from pydantic.v1.typing import AnyArgTCallable, AnyCallable +from pydantic.v1.utils import GetterDict +from pydantic.v1.version import compiled + +if TYPE_CHECKING: + from typing import overload + + from pydantic.v1.fields import ModelField + from pydantic.v1.main import BaseModel + + ConfigType = Type['BaseConfig'] + + class SchemaExtraCallable(Protocol): + @overload + def __call__(self, schema: Dict[str, Any]) -> None: + pass + + @overload + def __call__(self, schema: Dict[str, Any], model_class: Type[BaseModel]) -> None: + pass + +else: + SchemaExtraCallable = Callable[..., None] + +__all__ = 'BaseConfig', 'ConfigDict', 'get_config', 'Extra', 'inherit_config', 'prepare_config' + + +class Extra(str, Enum): + allow = 'allow' + ignore = 'ignore' + forbid = 'forbid' + + +# https://github.com/cython/cython/issues/4003 +# Fixed in Cython 3 and Pydantic v1 won't support Cython 3. +# Pydantic v2 doesn't depend on Cython at all. +if not compiled: + from typing_extensions import TypedDict + + class ConfigDict(TypedDict, total=False): + title: Optional[str] + anystr_lower: bool + anystr_strip_whitespace: bool + min_anystr_length: int + max_anystr_length: Optional[int] + validate_all: bool + extra: Extra + allow_mutation: bool + frozen: bool + allow_population_by_field_name: bool + use_enum_values: bool + fields: Dict[str, Union[str, Dict[str, str]]] + validate_assignment: bool + error_msg_templates: Dict[str, str] + arbitrary_types_allowed: bool + orm_mode: bool + getter_dict: Type[GetterDict] + alias_generator: Optional[Callable[[str], str]] + keep_untouched: Tuple[type, ...] + schema_extra: Union[Dict[str, object], 'SchemaExtraCallable'] + json_loads: Callable[[str], object] + json_dumps: AnyArgTCallable[str] + json_encoders: Dict[Type[object], AnyCallable] + underscore_attrs_are_private: bool + allow_inf_nan: bool + copy_on_model_validation: Literal['none', 'deep', 'shallow'] + # whether dataclass `__post_init__` should be run after validation + post_init_call: Literal['before_validation', 'after_validation'] + +else: + ConfigDict = dict # type: ignore + + +class BaseConfig: + title: Optional[str] = None + anystr_lower: bool = False + anystr_upper: bool = False + anystr_strip_whitespace: bool = False + min_anystr_length: int = 0 + max_anystr_length: Optional[int] = None + validate_all: bool = False + extra: Extra = Extra.ignore + allow_mutation: bool = True + frozen: bool = False + allow_population_by_field_name: bool = False + use_enum_values: bool = False + fields: Dict[str, Union[str, Dict[str, str]]] = {} + validate_assignment: bool = False + error_msg_templates: Dict[str, str] = {} + arbitrary_types_allowed: bool = False + orm_mode: bool = False + getter_dict: Type[GetterDict] = GetterDict + alias_generator: Optional[Callable[[str], str]] = None + keep_untouched: Tuple[type, ...] = () + schema_extra: Union[Dict[str, Any], 'SchemaExtraCallable'] = {} + json_loads: Callable[[str], Any] = json.loads + json_dumps: Callable[..., str] = json.dumps + json_encoders: Dict[Union[Type[Any], str, ForwardRef], AnyCallable] = {} + underscore_attrs_are_private: bool = False + allow_inf_nan: bool = True + + # whether inherited models as fields should be reconstructed as base model, + # and whether such a copy should be shallow or deep + copy_on_model_validation: Literal['none', 'deep', 'shallow'] = 'shallow' + + # whether `Union` should check all allowed types before even trying to coerce + smart_union: bool = False + # whether dataclass `__post_init__` should be run before or after validation + post_init_call: Literal['before_validation', 'after_validation'] = 'before_validation' + + @classmethod + def get_field_info(cls, name: str) -> Dict[str, Any]: + """ + Get properties of FieldInfo from the `fields` property of the config class. + """ + + fields_value = cls.fields.get(name) + + if isinstance(fields_value, str): + field_info: Dict[str, Any] = {'alias': fields_value} + elif isinstance(fields_value, dict): + field_info = fields_value + else: + field_info = {} + + if 'alias' in field_info: + field_info.setdefault('alias_priority', 2) + + if field_info.get('alias_priority', 0) <= 1 and cls.alias_generator: + alias = cls.alias_generator(name) + if not isinstance(alias, str): + raise TypeError(f'Config.alias_generator must return str, not {alias.__class__}') + field_info.update(alias=alias, alias_priority=1) + return field_info + + @classmethod + def prepare_field(cls, field: 'ModelField') -> None: + """ + Optional hook to check or modify fields during model creation. + """ + pass + + +def get_config(config: Union[ConfigDict, Type[object], None]) -> Type[BaseConfig]: + if config is None: + return BaseConfig + + else: + config_dict = ( + config + if isinstance(config, dict) + else {k: getattr(config, k) for k in dir(config) if not k.startswith('__')} + ) + + class Config(BaseConfig): + ... + + for k, v in config_dict.items(): + setattr(Config, k, v) + return Config + + +def inherit_config(self_config: 'ConfigType', parent_config: 'ConfigType', **namespace: Any) -> 'ConfigType': + if not self_config: + base_classes: Tuple['ConfigType', ...] = (parent_config,) + elif self_config == parent_config: + base_classes = (self_config,) + else: + base_classes = self_config, parent_config + + namespace['json_encoders'] = { + **getattr(parent_config, 'json_encoders', {}), + **getattr(self_config, 'json_encoders', {}), + **namespace.get('json_encoders', {}), + } + + return type('Config', base_classes, namespace) + + +def prepare_config(config: Type[BaseConfig], cls_name: str) -> None: + if not isinstance(config.extra, Extra): + try: + config.extra = Extra(config.extra) + except ValueError: + raise ValueError(f'"{cls_name}": {config.extra} is not a valid value for "extra"') diff --git a/source/target-vendors/cp312/pydantic/v1/dataclasses.py b/source/target-vendors/cp312/pydantic/v1/dataclasses.py new file mode 100644 index 0000000..bd16702 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/dataclasses.py @@ -0,0 +1,500 @@ +""" +The main purpose is to enhance stdlib dataclasses by adding validation +A pydantic dataclass can be generated from scratch or from a stdlib one. + +Behind the scene, a pydantic dataclass is just like a regular one on which we attach +a `BaseModel` and magic methods to trigger the validation of the data. +`__init__` and `__post_init__` are hence overridden and have extra logic to be +able to validate input data. + +When a pydantic dataclass is generated from scratch, it's just a plain dataclass +with validation triggered at initialization + +The tricky part if for stdlib dataclasses that are converted after into pydantic ones e.g. + +```py +@dataclasses.dataclass +class M: + x: int + +ValidatedM = pydantic.dataclasses.dataclass(M) +``` + +We indeed still want to support equality, hashing, repr, ... as if it was the stdlib one! + +```py +assert isinstance(ValidatedM(x=1), M) +assert ValidatedM(x=1) == M(x=1) +``` + +This means we **don't want to create a new dataclass that inherits from it** +The trick is to create a wrapper around `M` that will act as a proxy to trigger +validation without altering default `M` behaviour. +""" +import copy +import dataclasses +import sys +from contextlib import contextmanager +from functools import wraps + +try: + from functools import cached_property +except ImportError: + # cached_property available only for python3.8+ + pass + +from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, Generator, Optional, Type, TypeVar, Union, overload + +from typing_extensions import dataclass_transform + +from pydantic.v1.class_validators import gather_all_validators +from pydantic.v1.config import BaseConfig, ConfigDict, Extra, get_config +from pydantic.v1.error_wrappers import ValidationError +from pydantic.v1.errors import DataclassTypeError +from pydantic.v1.fields import Field, FieldInfo, Required, Undefined +from pydantic.v1.main import create_model, validate_model +from pydantic.v1.utils import ClassAttribute + +if TYPE_CHECKING: + from pydantic.v1.main import BaseModel + from pydantic.v1.typing import CallableGenerator, NoArgAnyCallable + + DataclassT = TypeVar('DataclassT', bound='Dataclass') + + DataclassClassOrWrapper = Union[Type['Dataclass'], 'DataclassProxy'] + + class Dataclass: + # stdlib attributes + __dataclass_fields__: ClassVar[Dict[str, Any]] + __dataclass_params__: ClassVar[Any] # in reality `dataclasses._DataclassParams` + __post_init__: ClassVar[Callable[..., None]] + + # Added by pydantic + __pydantic_run_validation__: ClassVar[bool] + __post_init_post_parse__: ClassVar[Callable[..., None]] + __pydantic_initialised__: ClassVar[bool] + __pydantic_model__: ClassVar[Type[BaseModel]] + __pydantic_validate_values__: ClassVar[Callable[['Dataclass'], None]] + __pydantic_has_field_info_default__: ClassVar[bool] # whether a `pydantic.Field` is used as default value + + def __init__(self, *args: object, **kwargs: object) -> None: + pass + + @classmethod + def __get_validators__(cls: Type['Dataclass']) -> 'CallableGenerator': + pass + + @classmethod + def __validate__(cls: Type['DataclassT'], v: Any) -> 'DataclassT': + pass + + +__all__ = [ + 'dataclass', + 'set_validation', + 'create_pydantic_model_from_dataclass', + 'is_builtin_dataclass', + 'make_dataclass_validator', +] + +_T = TypeVar('_T') + +if sys.version_info >= (3, 10): + + @dataclass_transform(field_specifiers=(dataclasses.field, Field)) + @overload + def dataclass( + *, + init: bool = True, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + config: Union[ConfigDict, Type[object], None] = None, + validate_on_init: Optional[bool] = None, + use_proxy: Optional[bool] = None, + kw_only: bool = ..., + ) -> Callable[[Type[_T]], 'DataclassClassOrWrapper']: + ... + + @dataclass_transform(field_specifiers=(dataclasses.field, Field)) + @overload + def dataclass( + _cls: Type[_T], + *, + init: bool = True, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + config: Union[ConfigDict, Type[object], None] = None, + validate_on_init: Optional[bool] = None, + use_proxy: Optional[bool] = None, + kw_only: bool = ..., + ) -> 'DataclassClassOrWrapper': + ... + +else: + + @dataclass_transform(field_specifiers=(dataclasses.field, Field)) + @overload + def dataclass( + *, + init: bool = True, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + config: Union[ConfigDict, Type[object], None] = None, + validate_on_init: Optional[bool] = None, + use_proxy: Optional[bool] = None, + ) -> Callable[[Type[_T]], 'DataclassClassOrWrapper']: + ... + + @dataclass_transform(field_specifiers=(dataclasses.field, Field)) + @overload + def dataclass( + _cls: Type[_T], + *, + init: bool = True, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + config: Union[ConfigDict, Type[object], None] = None, + validate_on_init: Optional[bool] = None, + use_proxy: Optional[bool] = None, + ) -> 'DataclassClassOrWrapper': + ... + + +@dataclass_transform(field_specifiers=(dataclasses.field, Field)) +def dataclass( + _cls: Optional[Type[_T]] = None, + *, + init: bool = True, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + config: Union[ConfigDict, Type[object], None] = None, + validate_on_init: Optional[bool] = None, + use_proxy: Optional[bool] = None, + kw_only: bool = False, +) -> Union[Callable[[Type[_T]], 'DataclassClassOrWrapper'], 'DataclassClassOrWrapper']: + """ + Like the python standard lib dataclasses but with type validation. + The result is either a pydantic dataclass that will validate input data + or a wrapper that will trigger validation around a stdlib dataclass + to avoid modifying it directly + """ + the_config = get_config(config) + + def wrap(cls: Type[Any]) -> 'DataclassClassOrWrapper': + should_use_proxy = ( + use_proxy + if use_proxy is not None + else ( + is_builtin_dataclass(cls) + and (cls.__bases__[0] is object or set(dir(cls)) == set(dir(cls.__bases__[0]))) + ) + ) + if should_use_proxy: + dc_cls_doc = '' + dc_cls = DataclassProxy(cls) + default_validate_on_init = False + else: + dc_cls_doc = cls.__doc__ or '' # needs to be done before generating dataclass + if sys.version_info >= (3, 10): + dc_cls = dataclasses.dataclass( + cls, + init=init, + repr=repr, + eq=eq, + order=order, + unsafe_hash=unsafe_hash, + frozen=frozen, + kw_only=kw_only, + ) + else: + dc_cls = dataclasses.dataclass( # type: ignore + cls, init=init, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen + ) + default_validate_on_init = True + + should_validate_on_init = default_validate_on_init if validate_on_init is None else validate_on_init + _add_pydantic_validation_attributes(cls, the_config, should_validate_on_init, dc_cls_doc) + dc_cls.__pydantic_model__.__try_update_forward_refs__(**{cls.__name__: cls}) + return dc_cls + + if _cls is None: + return wrap + + return wrap(_cls) + + +@contextmanager +def set_validation(cls: Type['DataclassT'], value: bool) -> Generator[Type['DataclassT'], None, None]: + original_run_validation = cls.__pydantic_run_validation__ + try: + cls.__pydantic_run_validation__ = value + yield cls + finally: + cls.__pydantic_run_validation__ = original_run_validation + + +class DataclassProxy: + __slots__ = '__dataclass__' + + def __init__(self, dc_cls: Type['Dataclass']) -> None: + object.__setattr__(self, '__dataclass__', dc_cls) + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + with set_validation(self.__dataclass__, True): + return self.__dataclass__(*args, **kwargs) + + def __getattr__(self, name: str) -> Any: + return getattr(self.__dataclass__, name) + + def __setattr__(self, __name: str, __value: Any) -> None: + return setattr(self.__dataclass__, __name, __value) + + def __instancecheck__(self, instance: Any) -> bool: + return isinstance(instance, self.__dataclass__) + + def __copy__(self) -> 'DataclassProxy': + return DataclassProxy(copy.copy(self.__dataclass__)) + + def __deepcopy__(self, memo: Any) -> 'DataclassProxy': + return DataclassProxy(copy.deepcopy(self.__dataclass__, memo)) + + +def _add_pydantic_validation_attributes( # noqa: C901 (ignore complexity) + dc_cls: Type['Dataclass'], + config: Type[BaseConfig], + validate_on_init: bool, + dc_cls_doc: str, +) -> None: + """ + We need to replace the right method. If no `__post_init__` has been set in the stdlib dataclass + it won't even exist (code is generated on the fly by `dataclasses`) + By default, we run validation after `__init__` or `__post_init__` if defined + """ + init = dc_cls.__init__ + + @wraps(init) + def handle_extra_init(self: 'Dataclass', *args: Any, **kwargs: Any) -> None: + if config.extra == Extra.ignore: + init(self, *args, **{k: v for k, v in kwargs.items() if k in self.__dataclass_fields__}) + + elif config.extra == Extra.allow: + for k, v in kwargs.items(): + self.__dict__.setdefault(k, v) + init(self, *args, **{k: v for k, v in kwargs.items() if k in self.__dataclass_fields__}) + + else: + init(self, *args, **kwargs) + + if hasattr(dc_cls, '__post_init__'): + try: + post_init = dc_cls.__post_init__.__wrapped__ # type: ignore[attr-defined] + except AttributeError: + post_init = dc_cls.__post_init__ + + @wraps(post_init) + def new_post_init(self: 'Dataclass', *args: Any, **kwargs: Any) -> None: + if config.post_init_call == 'before_validation': + post_init(self, *args, **kwargs) + + if self.__class__.__pydantic_run_validation__: + self.__pydantic_validate_values__() + if hasattr(self, '__post_init_post_parse__'): + self.__post_init_post_parse__(*args, **kwargs) + + if config.post_init_call == 'after_validation': + post_init(self, *args, **kwargs) + + setattr(dc_cls, '__init__', handle_extra_init) + setattr(dc_cls, '__post_init__', new_post_init) + + else: + + @wraps(init) + def new_init(self: 'Dataclass', *args: Any, **kwargs: Any) -> None: + handle_extra_init(self, *args, **kwargs) + + if self.__class__.__pydantic_run_validation__: + self.__pydantic_validate_values__() + + if hasattr(self, '__post_init_post_parse__'): + # We need to find again the initvars. To do that we use `__dataclass_fields__` instead of + # public method `dataclasses.fields` + + # get all initvars and their default values + initvars_and_values: Dict[str, Any] = {} + for i, f in enumerate(self.__class__.__dataclass_fields__.values()): + if f._field_type is dataclasses._FIELD_INITVAR: # type: ignore[attr-defined] + try: + # set arg value by default + initvars_and_values[f.name] = args[i] + except IndexError: + initvars_and_values[f.name] = kwargs.get(f.name, f.default) + + self.__post_init_post_parse__(**initvars_and_values) + + setattr(dc_cls, '__init__', new_init) + + setattr(dc_cls, '__pydantic_run_validation__', ClassAttribute('__pydantic_run_validation__', validate_on_init)) + setattr(dc_cls, '__pydantic_initialised__', False) + setattr(dc_cls, '__pydantic_model__', create_pydantic_model_from_dataclass(dc_cls, config, dc_cls_doc)) + setattr(dc_cls, '__pydantic_validate_values__', _dataclass_validate_values) + setattr(dc_cls, '__validate__', classmethod(_validate_dataclass)) + setattr(dc_cls, '__get_validators__', classmethod(_get_validators)) + + if dc_cls.__pydantic_model__.__config__.validate_assignment and not dc_cls.__dataclass_params__.frozen: + setattr(dc_cls, '__setattr__', _dataclass_validate_assignment_setattr) + + +def _get_validators(cls: 'DataclassClassOrWrapper') -> 'CallableGenerator': + yield cls.__validate__ + + +def _validate_dataclass(cls: Type['DataclassT'], v: Any) -> 'DataclassT': + with set_validation(cls, True): + if isinstance(v, cls): + v.__pydantic_validate_values__() + return v + elif isinstance(v, (list, tuple)): + return cls(*v) + elif isinstance(v, dict): + return cls(**v) + else: + raise DataclassTypeError(class_name=cls.__name__) + + +def create_pydantic_model_from_dataclass( + dc_cls: Type['Dataclass'], + config: Type[Any] = BaseConfig, + dc_cls_doc: Optional[str] = None, +) -> Type['BaseModel']: + field_definitions: Dict[str, Any] = {} + for field in dataclasses.fields(dc_cls): + default: Any = Undefined + default_factory: Optional['NoArgAnyCallable'] = None + field_info: FieldInfo + + if field.default is not dataclasses.MISSING: + default = field.default + elif field.default_factory is not dataclasses.MISSING: + default_factory = field.default_factory + else: + default = Required + + if isinstance(default, FieldInfo): + field_info = default + dc_cls.__pydantic_has_field_info_default__ = True + else: + field_info = Field(default=default, default_factory=default_factory, **field.metadata) + + field_definitions[field.name] = (field.type, field_info) + + validators = gather_all_validators(dc_cls) + model: Type['BaseModel'] = create_model( + dc_cls.__name__, + __config__=config, + __module__=dc_cls.__module__, + __validators__=validators, + __cls_kwargs__={'__resolve_forward_refs__': False}, + **field_definitions, + ) + model.__doc__ = dc_cls_doc if dc_cls_doc is not None else dc_cls.__doc__ or '' + return model + + +if sys.version_info >= (3, 8): + + def _is_field_cached_property(obj: 'Dataclass', k: str) -> bool: + return isinstance(getattr(type(obj), k, None), cached_property) + +else: + + def _is_field_cached_property(obj: 'Dataclass', k: str) -> bool: + return False + + +def _dataclass_validate_values(self: 'Dataclass') -> None: + # validation errors can occur if this function is called twice on an already initialised dataclass. + # for example if Extra.forbid is enabled, it would consider __pydantic_initialised__ an invalid extra property + if getattr(self, '__pydantic_initialised__'): + return + if getattr(self, '__pydantic_has_field_info_default__', False): + # We need to remove `FieldInfo` values since they are not valid as input + # It's ok to do that because they are obviously the default values! + input_data = { + k: v + for k, v in self.__dict__.items() + if not (isinstance(v, FieldInfo) or _is_field_cached_property(self, k)) + } + else: + input_data = {k: v for k, v in self.__dict__.items() if not _is_field_cached_property(self, k)} + d, _, validation_error = validate_model(self.__pydantic_model__, input_data, cls=self.__class__) + if validation_error: + raise validation_error + self.__dict__.update(d) + object.__setattr__(self, '__pydantic_initialised__', True) + + +def _dataclass_validate_assignment_setattr(self: 'Dataclass', name: str, value: Any) -> None: + if self.__pydantic_initialised__: + d = dict(self.__dict__) + d.pop(name, None) + known_field = self.__pydantic_model__.__fields__.get(name, None) + if known_field: + value, error_ = known_field.validate(value, d, loc=name, cls=self.__class__) + if error_: + raise ValidationError([error_], self.__class__) + + object.__setattr__(self, name, value) + + +def is_builtin_dataclass(_cls: Type[Any]) -> bool: + """ + Whether a class is a stdlib dataclass + (useful to discriminated a pydantic dataclass that is actually a wrapper around a stdlib dataclass) + + we check that + - `_cls` is a dataclass + - `_cls` is not a processed pydantic dataclass (with a basemodel attached) + - `_cls` is not a pydantic dataclass inheriting directly from a stdlib dataclass + e.g. + ``` + @dataclasses.dataclass + class A: + x: int + + @pydantic.dataclasses.dataclass + class B(A): + y: int + ``` + In this case, when we first check `B`, we make an extra check and look at the annotations ('y'), + which won't be a superset of all the dataclass fields (only the stdlib fields i.e. 'x') + """ + return ( + dataclasses.is_dataclass(_cls) + and not hasattr(_cls, '__pydantic_model__') + and set(_cls.__dataclass_fields__).issuperset(set(getattr(_cls, '__annotations__', {}))) + ) + + +def make_dataclass_validator(dc_cls: Type['Dataclass'], config: Type[BaseConfig]) -> 'CallableGenerator': + """ + Create a pydantic.dataclass from a builtin dataclass to add type validation + and yield the validators + It retrieves the parameters of the dataclass and forwards them to the newly created dataclass + """ + yield from _get_validators(dataclass(dc_cls, config=config, use_proxy=True)) diff --git a/source/target-vendors/cp312/pydantic/v1/datetime_parse.py b/source/target-vendors/cp312/pydantic/v1/datetime_parse.py new file mode 100644 index 0000000..a7598fc --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/datetime_parse.py @@ -0,0 +1,248 @@ +""" +Functions to parse datetime objects. + +We're using regular expressions rather than time.strptime because: +- They provide both validation and parsing. +- They're more flexible for datetimes. +- The date/datetime/time constructors produce friendlier error messages. + +Stolen from https://raw.githubusercontent.com/django/django/main/django/utils/dateparse.py at +9718fa2e8abe430c3526a9278dd976443d4ae3c6 + +Changed to: +* use standard python datetime types not django.utils.timezone +* raise ValueError when regex doesn't match rather than returning None +* support parsing unix timestamps for dates and datetimes +""" +import re +from datetime import date, datetime, time, timedelta, timezone +from typing import Dict, Optional, Type, Union + +from pydantic.v1 import errors + +date_expr = r'(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})' +time_expr = ( + r'(?P\d{1,2}):(?P\d{1,2})' + r'(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d{0,6})?)?' + r'(?PZ|[+-]\d{2}(?::?\d{2})?)?$' +) + +date_re = re.compile(f'{date_expr}$') +time_re = re.compile(time_expr) +datetime_re = re.compile(f'{date_expr}[T ]{time_expr}') + +standard_duration_re = re.compile( + r'^' + r'(?:(?P-?\d+) (days?, )?)?' + r'((?:(?P-?\d+):)(?=\d+:\d+))?' + r'(?:(?P-?\d+):)?' + r'(?P-?\d+)' + r'(?:\.(?P\d{1,6})\d{0,6})?' + r'$' +) + +# Support the sections of ISO 8601 date representation that are accepted by timedelta +iso8601_duration_re = re.compile( + r'^(?P[-+]?)' + r'P' + r'(?:(?P\d+(.\d+)?)D)?' + r'(?:T' + r'(?:(?P\d+(.\d+)?)H)?' + r'(?:(?P\d+(.\d+)?)M)?' + r'(?:(?P\d+(.\d+)?)S)?' + r')?' + r'$' +) + +EPOCH = datetime(1970, 1, 1) +# if greater than this, the number is in ms, if less than or equal it's in seconds +# (in seconds this is 11th October 2603, in ms it's 20th August 1970) +MS_WATERSHED = int(2e10) +# slightly more than datetime.max in ns - (datetime.max - EPOCH).total_seconds() * 1e9 +MAX_NUMBER = int(3e20) +StrBytesIntFloat = Union[str, bytes, int, float] + + +def get_numeric(value: StrBytesIntFloat, native_expected_type: str) -> Union[None, int, float]: + if isinstance(value, (int, float)): + return value + try: + return float(value) + except ValueError: + return None + except TypeError: + raise TypeError(f'invalid type; expected {native_expected_type}, string, bytes, int or float') + + +def from_unix_seconds(seconds: Union[int, float]) -> datetime: + if seconds > MAX_NUMBER: + return datetime.max + elif seconds < -MAX_NUMBER: + return datetime.min + + while abs(seconds) > MS_WATERSHED: + seconds /= 1000 + dt = EPOCH + timedelta(seconds=seconds) + return dt.replace(tzinfo=timezone.utc) + + +def _parse_timezone(value: Optional[str], error: Type[Exception]) -> Union[None, int, timezone]: + if value == 'Z': + return timezone.utc + elif value is not None: + offset_mins = int(value[-2:]) if len(value) > 3 else 0 + offset = 60 * int(value[1:3]) + offset_mins + if value[0] == '-': + offset = -offset + try: + return timezone(timedelta(minutes=offset)) + except ValueError: + raise error() + else: + return None + + +def parse_date(value: Union[date, StrBytesIntFloat]) -> date: + """ + Parse a date/int/float/string and return a datetime.date. + + Raise ValueError if the input is well formatted but not a valid date. + Raise ValueError if the input isn't well formatted. + """ + if isinstance(value, date): + if isinstance(value, datetime): + return value.date() + else: + return value + + number = get_numeric(value, 'date') + if number is not None: + return from_unix_seconds(number).date() + + if isinstance(value, bytes): + value = value.decode() + + match = date_re.match(value) # type: ignore + if match is None: + raise errors.DateError() + + kw = {k: int(v) for k, v in match.groupdict().items()} + + try: + return date(**kw) + except ValueError: + raise errors.DateError() + + +def parse_time(value: Union[time, StrBytesIntFloat]) -> time: + """ + Parse a time/string and return a datetime.time. + + Raise ValueError if the input is well formatted but not a valid time. + Raise ValueError if the input isn't well formatted, in particular if it contains an offset. + """ + if isinstance(value, time): + return value + + number = get_numeric(value, 'time') + if number is not None: + if number >= 86400: + # doesn't make sense since the time time loop back around to 0 + raise errors.TimeError() + return (datetime.min + timedelta(seconds=number)).time() + + if isinstance(value, bytes): + value = value.decode() + + match = time_re.match(value) # type: ignore + if match is None: + raise errors.TimeError() + + kw = match.groupdict() + if kw['microsecond']: + kw['microsecond'] = kw['microsecond'].ljust(6, '0') + + tzinfo = _parse_timezone(kw.pop('tzinfo'), errors.TimeError) + kw_: Dict[str, Union[None, int, timezone]] = {k: int(v) for k, v in kw.items() if v is not None} + kw_['tzinfo'] = tzinfo + + try: + return time(**kw_) # type: ignore + except ValueError: + raise errors.TimeError() + + +def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: + """ + Parse a datetime/int/float/string and return a datetime.datetime. + + This function supports time zone offsets. When the input contains one, + the output uses a timezone with a fixed offset from UTC. + + Raise ValueError if the input is well formatted but not a valid datetime. + Raise ValueError if the input isn't well formatted. + """ + if isinstance(value, datetime): + return value + + number = get_numeric(value, 'datetime') + if number is not None: + return from_unix_seconds(number) + + if isinstance(value, bytes): + value = value.decode() + + match = datetime_re.match(value) # type: ignore + if match is None: + raise errors.DateTimeError() + + kw = match.groupdict() + if kw['microsecond']: + kw['microsecond'] = kw['microsecond'].ljust(6, '0') + + tzinfo = _parse_timezone(kw.pop('tzinfo'), errors.DateTimeError) + kw_: Dict[str, Union[None, int, timezone]] = {k: int(v) for k, v in kw.items() if v is not None} + kw_['tzinfo'] = tzinfo + + try: + return datetime(**kw_) # type: ignore + except ValueError: + raise errors.DateTimeError() + + +def parse_duration(value: StrBytesIntFloat) -> timedelta: + """ + Parse a duration int/float/string and return a datetime.timedelta. + + The preferred format for durations in Django is '%d %H:%M:%S.%f'. + + Also supports ISO 8601 representation. + """ + if isinstance(value, timedelta): + return value + + if isinstance(value, (int, float)): + # below code requires a string + value = f'{value:f}' + elif isinstance(value, bytes): + value = value.decode() + + try: + match = standard_duration_re.match(value) or iso8601_duration_re.match(value) + except TypeError: + raise TypeError('invalid type; expected timedelta, string, bytes, int or float') + + if not match: + raise errors.DurationError() + + kw = match.groupdict() + sign = -1 if kw.pop('sign', '+') == '-' else 1 + if kw.get('microseconds'): + kw['microseconds'] = kw['microseconds'].ljust(6, '0') + + if kw.get('seconds') and kw.get('microseconds') and kw['seconds'].startswith('-'): + kw['microseconds'] = '-' + kw['microseconds'] + + kw_ = {k: float(v) for k, v in kw.items() if v is not None} + + return sign * timedelta(**kw_) diff --git a/source/target-vendors/cp312/pydantic/v1/decorator.py b/source/target-vendors/cp312/pydantic/v1/decorator.py new file mode 100644 index 0000000..2c7c2c2 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/decorator.py @@ -0,0 +1,264 @@ +from functools import wraps +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Mapping, Optional, Tuple, Type, TypeVar, Union, overload + +from pydantic.v1 import validator +from pydantic.v1.config import Extra +from pydantic.v1.errors import ConfigError +from pydantic.v1.main import BaseModel, create_model +from pydantic.v1.typing import get_all_type_hints +from pydantic.v1.utils import to_camel + +__all__ = ('validate_arguments',) + +if TYPE_CHECKING: + from pydantic.v1.typing import AnyCallable + + AnyCallableT = TypeVar('AnyCallableT', bound=AnyCallable) + ConfigType = Union[None, Type[Any], Dict[str, Any]] + + +@overload +def validate_arguments(func: None = None, *, config: 'ConfigType' = None) -> Callable[['AnyCallableT'], 'AnyCallableT']: + ... + + +@overload +def validate_arguments(func: 'AnyCallableT') -> 'AnyCallableT': + ... + + +def validate_arguments(func: Optional['AnyCallableT'] = None, *, config: 'ConfigType' = None) -> Any: + """ + Decorator to validate the arguments passed to a function. + """ + + def validate(_func: 'AnyCallable') -> 'AnyCallable': + vd = ValidatedFunction(_func, config) + + @wraps(_func) + def wrapper_function(*args: Any, **kwargs: Any) -> Any: + return vd.call(*args, **kwargs) + + wrapper_function.vd = vd # type: ignore + wrapper_function.validate = vd.init_model_instance # type: ignore + wrapper_function.raw_function = vd.raw_function # type: ignore + wrapper_function.model = vd.model # type: ignore + return wrapper_function + + if func: + return validate(func) + else: + return validate + + +ALT_V_ARGS = 'v__args' +ALT_V_KWARGS = 'v__kwargs' +V_POSITIONAL_ONLY_NAME = 'v__positional_only' +V_DUPLICATE_KWARGS = 'v__duplicate_kwargs' + + +class ValidatedFunction: + def __init__(self, function: 'AnyCallableT', config: 'ConfigType'): # noqa C901 + from inspect import Parameter, signature + + parameters: Mapping[str, Parameter] = signature(function).parameters + + if parameters.keys() & {ALT_V_ARGS, ALT_V_KWARGS, V_POSITIONAL_ONLY_NAME, V_DUPLICATE_KWARGS}: + raise ConfigError( + f'"{ALT_V_ARGS}", "{ALT_V_KWARGS}", "{V_POSITIONAL_ONLY_NAME}" and "{V_DUPLICATE_KWARGS}" ' + f'are not permitted as argument names when using the "{validate_arguments.__name__}" decorator' + ) + + self.raw_function = function + self.arg_mapping: Dict[int, str] = {} + self.positional_only_args = set() + self.v_args_name = 'args' + self.v_kwargs_name = 'kwargs' + + type_hints = get_all_type_hints(function) + takes_args = False + takes_kwargs = False + fields: Dict[str, Tuple[Any, Any]] = {} + for i, (name, p) in enumerate(parameters.items()): + if p.annotation is p.empty: + annotation = Any + else: + annotation = type_hints[name] + + default = ... if p.default is p.empty else p.default + if p.kind == Parameter.POSITIONAL_ONLY: + self.arg_mapping[i] = name + fields[name] = annotation, default + fields[V_POSITIONAL_ONLY_NAME] = List[str], None + self.positional_only_args.add(name) + elif p.kind == Parameter.POSITIONAL_OR_KEYWORD: + self.arg_mapping[i] = name + fields[name] = annotation, default + fields[V_DUPLICATE_KWARGS] = List[str], None + elif p.kind == Parameter.KEYWORD_ONLY: + fields[name] = annotation, default + elif p.kind == Parameter.VAR_POSITIONAL: + self.v_args_name = name + fields[name] = Tuple[annotation, ...], None + takes_args = True + else: + assert p.kind == Parameter.VAR_KEYWORD, p.kind + self.v_kwargs_name = name + fields[name] = Dict[str, annotation], None # type: ignore + takes_kwargs = True + + # these checks avoid a clash between "args" and a field with that name + if not takes_args and self.v_args_name in fields: + self.v_args_name = ALT_V_ARGS + + # same with "kwargs" + if not takes_kwargs and self.v_kwargs_name in fields: + self.v_kwargs_name = ALT_V_KWARGS + + if not takes_args: + # we add the field so validation below can raise the correct exception + fields[self.v_args_name] = List[Any], None + + if not takes_kwargs: + # same with kwargs + fields[self.v_kwargs_name] = Dict[Any, Any], None + + self.create_model(fields, takes_args, takes_kwargs, config) + + def init_model_instance(self, *args: Any, **kwargs: Any) -> BaseModel: + values = self.build_values(args, kwargs) + return self.model(**values) + + def call(self, *args: Any, **kwargs: Any) -> Any: + m = self.init_model_instance(*args, **kwargs) + return self.execute(m) + + def build_values(self, args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> Dict[str, Any]: + values: Dict[str, Any] = {} + if args: + arg_iter = enumerate(args) + while True: + try: + i, a = next(arg_iter) + except StopIteration: + break + arg_name = self.arg_mapping.get(i) + if arg_name is not None: + values[arg_name] = a + else: + values[self.v_args_name] = [a] + [a for _, a in arg_iter] + break + + var_kwargs: Dict[str, Any] = {} + wrong_positional_args = [] + duplicate_kwargs = [] + fields_alias = [ + field.alias + for name, field in self.model.__fields__.items() + if name not in (self.v_args_name, self.v_kwargs_name) + ] + non_var_fields = set(self.model.__fields__) - {self.v_args_name, self.v_kwargs_name} + for k, v in kwargs.items(): + if k in non_var_fields or k in fields_alias: + if k in self.positional_only_args: + wrong_positional_args.append(k) + if k in values: + duplicate_kwargs.append(k) + values[k] = v + else: + var_kwargs[k] = v + + if var_kwargs: + values[self.v_kwargs_name] = var_kwargs + if wrong_positional_args: + values[V_POSITIONAL_ONLY_NAME] = wrong_positional_args + if duplicate_kwargs: + values[V_DUPLICATE_KWARGS] = duplicate_kwargs + return values + + def execute(self, m: BaseModel) -> Any: + d = {k: v for k, v in m._iter() if k in m.__fields_set__ or m.__fields__[k].default_factory} + var_kwargs = d.pop(self.v_kwargs_name, {}) + + if self.v_args_name in d: + args_: List[Any] = [] + in_kwargs = False + kwargs = {} + for name, value in d.items(): + if in_kwargs: + kwargs[name] = value + elif name == self.v_args_name: + args_ += value + in_kwargs = True + else: + args_.append(value) + return self.raw_function(*args_, **kwargs, **var_kwargs) + elif self.positional_only_args: + args_ = [] + kwargs = {} + for name, value in d.items(): + if name in self.positional_only_args: + args_.append(value) + else: + kwargs[name] = value + return self.raw_function(*args_, **kwargs, **var_kwargs) + else: + return self.raw_function(**d, **var_kwargs) + + def create_model(self, fields: Dict[str, Any], takes_args: bool, takes_kwargs: bool, config: 'ConfigType') -> None: + pos_args = len(self.arg_mapping) + + class CustomConfig: + pass + + if not TYPE_CHECKING: # pragma: no branch + if isinstance(config, dict): + CustomConfig = type('Config', (), config) # noqa: F811 + elif config is not None: + CustomConfig = config # noqa: F811 + + if hasattr(CustomConfig, 'fields') or hasattr(CustomConfig, 'alias_generator'): + raise ConfigError( + 'Setting the "fields" and "alias_generator" property on custom Config for ' + '@validate_arguments is not yet supported, please remove.' + ) + + class DecoratorBaseModel(BaseModel): + @validator(self.v_args_name, check_fields=False, allow_reuse=True) + def check_args(cls, v: Optional[List[Any]]) -> Optional[List[Any]]: + if takes_args or v is None: + return v + + raise TypeError(f'{pos_args} positional arguments expected but {pos_args + len(v)} given') + + @validator(self.v_kwargs_name, check_fields=False, allow_reuse=True) + def check_kwargs(cls, v: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: + if takes_kwargs or v is None: + return v + + plural = '' if len(v) == 1 else 's' + keys = ', '.join(map(repr, v.keys())) + raise TypeError(f'unexpected keyword argument{plural}: {keys}') + + @validator(V_POSITIONAL_ONLY_NAME, check_fields=False, allow_reuse=True) + def check_positional_only(cls, v: Optional[List[str]]) -> None: + if v is None: + return + + plural = '' if len(v) == 1 else 's' + keys = ', '.join(map(repr, v)) + raise TypeError(f'positional-only argument{plural} passed as keyword argument{plural}: {keys}') + + @validator(V_DUPLICATE_KWARGS, check_fields=False, allow_reuse=True) + def check_duplicate_kwargs(cls, v: Optional[List[str]]) -> None: + if v is None: + return + + plural = '' if len(v) == 1 else 's' + keys = ', '.join(map(repr, v)) + raise TypeError(f'multiple values for argument{plural}: {keys}') + + class Config(CustomConfig): + extra = getattr(CustomConfig, 'extra', Extra.forbid) + + self.model = create_model(to_camel(self.raw_function.__name__), __base__=DecoratorBaseModel, **fields) diff --git a/source/target-vendors/cp312/pydantic/v1/env_settings.py b/source/target-vendors/cp312/pydantic/v1/env_settings.py new file mode 100644 index 0000000..5f6f217 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/env_settings.py @@ -0,0 +1,350 @@ +import os +import warnings +from pathlib import Path +from typing import AbstractSet, Any, Callable, ClassVar, Dict, List, Mapping, Optional, Tuple, Type, Union + +from pydantic.v1.config import BaseConfig, Extra +from pydantic.v1.fields import ModelField +from pydantic.v1.main import BaseModel +from pydantic.v1.types import JsonWrapper +from pydantic.v1.typing import StrPath, display_as_type, get_origin, is_union +from pydantic.v1.utils import deep_update, lenient_issubclass, path_type, sequence_like + +env_file_sentinel = str(object()) + +SettingsSourceCallable = Callable[['BaseSettings'], Dict[str, Any]] +DotenvType = Union[StrPath, List[StrPath], Tuple[StrPath, ...]] + + +class SettingsError(ValueError): + pass + + +class BaseSettings(BaseModel): + """ + Base class for settings, allowing values to be overridden by environment variables. + + This is useful in production for secrets you do not wish to save in code, it plays nicely with docker(-compose), + Heroku and any 12 factor app design. + """ + + def __init__( + __pydantic_self__, + _env_file: Optional[DotenvType] = env_file_sentinel, + _env_file_encoding: Optional[str] = None, + _env_nested_delimiter: Optional[str] = None, + _secrets_dir: Optional[StrPath] = None, + **values: Any, + ) -> None: + # Uses something other than `self` the first arg to allow "self" as a settable attribute + super().__init__( + **__pydantic_self__._build_values( + values, + _env_file=_env_file, + _env_file_encoding=_env_file_encoding, + _env_nested_delimiter=_env_nested_delimiter, + _secrets_dir=_secrets_dir, + ) + ) + + def _build_values( + self, + init_kwargs: Dict[str, Any], + _env_file: Optional[DotenvType] = None, + _env_file_encoding: Optional[str] = None, + _env_nested_delimiter: Optional[str] = None, + _secrets_dir: Optional[StrPath] = None, + ) -> Dict[str, Any]: + # Configure built-in sources + init_settings = InitSettingsSource(init_kwargs=init_kwargs) + env_settings = EnvSettingsSource( + env_file=(_env_file if _env_file != env_file_sentinel else self.__config__.env_file), + env_file_encoding=( + _env_file_encoding if _env_file_encoding is not None else self.__config__.env_file_encoding + ), + env_nested_delimiter=( + _env_nested_delimiter if _env_nested_delimiter is not None else self.__config__.env_nested_delimiter + ), + env_prefix_len=len(self.__config__.env_prefix), + ) + file_secret_settings = SecretsSettingsSource(secrets_dir=_secrets_dir or self.__config__.secrets_dir) + # Provide a hook to set built-in sources priority and add / remove sources + sources = self.__config__.customise_sources( + init_settings=init_settings, env_settings=env_settings, file_secret_settings=file_secret_settings + ) + if sources: + return deep_update(*reversed([source(self) for source in sources])) + else: + # no one should mean to do this, but I think returning an empty dict is marginally preferable + # to an informative error and much better than a confusing error + return {} + + class Config(BaseConfig): + env_prefix: str = '' + env_file: Optional[DotenvType] = None + env_file_encoding: Optional[str] = None + env_nested_delimiter: Optional[str] = None + secrets_dir: Optional[StrPath] = None + validate_all: bool = True + extra: Extra = Extra.forbid + arbitrary_types_allowed: bool = True + case_sensitive: bool = False + + @classmethod + def prepare_field(cls, field: ModelField) -> None: + env_names: Union[List[str], AbstractSet[str]] + field_info_from_config = cls.get_field_info(field.name) + + env = field_info_from_config.get('env') or field.field_info.extra.get('env') + if env is None: + if field.has_alias: + warnings.warn( + 'aliases are no longer used by BaseSettings to define which environment variables to read. ' + 'Instead use the "env" field setting. ' + 'See https://pydantic-docs.helpmanual.io/usage/settings/#environment-variable-names', + FutureWarning, + ) + env_names = {cls.env_prefix + field.name} + elif isinstance(env, str): + env_names = {env} + elif isinstance(env, (set, frozenset)): + env_names = env + elif sequence_like(env): + env_names = list(env) + else: + raise TypeError(f'invalid field env: {env!r} ({display_as_type(env)}); should be string, list or set') + + if not cls.case_sensitive: + env_names = env_names.__class__(n.lower() for n in env_names) + field.field_info.extra['env_names'] = env_names + + @classmethod + def customise_sources( + cls, + init_settings: SettingsSourceCallable, + env_settings: SettingsSourceCallable, + file_secret_settings: SettingsSourceCallable, + ) -> Tuple[SettingsSourceCallable, ...]: + return init_settings, env_settings, file_secret_settings + + @classmethod + def parse_env_var(cls, field_name: str, raw_val: str) -> Any: + return cls.json_loads(raw_val) + + # populated by the metaclass using the Config class defined above, annotated here to help IDEs only + __config__: ClassVar[Type[Config]] + + +class InitSettingsSource: + __slots__ = ('init_kwargs',) + + def __init__(self, init_kwargs: Dict[str, Any]): + self.init_kwargs = init_kwargs + + def __call__(self, settings: BaseSettings) -> Dict[str, Any]: + return self.init_kwargs + + def __repr__(self) -> str: + return f'InitSettingsSource(init_kwargs={self.init_kwargs!r})' + + +class EnvSettingsSource: + __slots__ = ('env_file', 'env_file_encoding', 'env_nested_delimiter', 'env_prefix_len') + + def __init__( + self, + env_file: Optional[DotenvType], + env_file_encoding: Optional[str], + env_nested_delimiter: Optional[str] = None, + env_prefix_len: int = 0, + ): + self.env_file: Optional[DotenvType] = env_file + self.env_file_encoding: Optional[str] = env_file_encoding + self.env_nested_delimiter: Optional[str] = env_nested_delimiter + self.env_prefix_len: int = env_prefix_len + + def __call__(self, settings: BaseSettings) -> Dict[str, Any]: # noqa C901 + """ + Build environment variables suitable for passing to the Model. + """ + d: Dict[str, Any] = {} + + if settings.__config__.case_sensitive: + env_vars: Mapping[str, Optional[str]] = os.environ + else: + env_vars = {k.lower(): v for k, v in os.environ.items()} + + dotenv_vars = self._read_env_files(settings.__config__.case_sensitive) + if dotenv_vars: + env_vars = {**dotenv_vars, **env_vars} + + for field in settings.__fields__.values(): + env_val: Optional[str] = None + for env_name in field.field_info.extra['env_names']: + env_val = env_vars.get(env_name) + if env_val is not None: + break + + is_complex, allow_parse_failure = self.field_is_complex(field) + if is_complex: + if env_val is None: + # field is complex but no value found so far, try explode_env_vars + env_val_built = self.explode_env_vars(field, env_vars) + if env_val_built: + d[field.alias] = env_val_built + else: + # field is complex and there's a value, decode that as JSON, then add explode_env_vars + try: + env_val = settings.__config__.parse_env_var(field.name, env_val) + except ValueError as e: + if not allow_parse_failure: + raise SettingsError(f'error parsing env var "{env_name}"') from e + + if isinstance(env_val, dict): + d[field.alias] = deep_update(env_val, self.explode_env_vars(field, env_vars)) + else: + d[field.alias] = env_val + elif env_val is not None: + # simplest case, field is not complex, we only need to add the value if it was found + d[field.alias] = env_val + + return d + + def _read_env_files(self, case_sensitive: bool) -> Dict[str, Optional[str]]: + env_files = self.env_file + if env_files is None: + return {} + + if isinstance(env_files, (str, os.PathLike)): + env_files = [env_files] + + dotenv_vars = {} + for env_file in env_files: + env_path = Path(env_file).expanduser() + if env_path.is_file(): + dotenv_vars.update( + read_env_file(env_path, encoding=self.env_file_encoding, case_sensitive=case_sensitive) + ) + + return dotenv_vars + + def field_is_complex(self, field: ModelField) -> Tuple[bool, bool]: + """ + Find out if a field is complex, and if so whether JSON errors should be ignored + """ + if lenient_issubclass(field.annotation, JsonWrapper): + return False, False + + if field.is_complex(): + allow_parse_failure = False + elif is_union(get_origin(field.type_)) and field.sub_fields and any(f.is_complex() for f in field.sub_fields): + allow_parse_failure = True + else: + return False, False + + return True, allow_parse_failure + + def explode_env_vars(self, field: ModelField, env_vars: Mapping[str, Optional[str]]) -> Dict[str, Any]: + """ + Process env_vars and extract the values of keys containing env_nested_delimiter into nested dictionaries. + + This is applied to a single field, hence filtering by env_var prefix. + """ + prefixes = [f'{env_name}{self.env_nested_delimiter}' for env_name in field.field_info.extra['env_names']] + result: Dict[str, Any] = {} + for env_name, env_val in env_vars.items(): + if not any(env_name.startswith(prefix) for prefix in prefixes): + continue + # we remove the prefix before splitting in case the prefix has characters in common with the delimiter + env_name_without_prefix = env_name[self.env_prefix_len :] + _, *keys, last_key = env_name_without_prefix.split(self.env_nested_delimiter) + env_var = result + for key in keys: + env_var = env_var.setdefault(key, {}) + env_var[last_key] = env_val + + return result + + def __repr__(self) -> str: + return ( + f'EnvSettingsSource(env_file={self.env_file!r}, env_file_encoding={self.env_file_encoding!r}, ' + f'env_nested_delimiter={self.env_nested_delimiter!r})' + ) + + +class SecretsSettingsSource: + __slots__ = ('secrets_dir',) + + def __init__(self, secrets_dir: Optional[StrPath]): + self.secrets_dir: Optional[StrPath] = secrets_dir + + def __call__(self, settings: BaseSettings) -> Dict[str, Any]: + """ + Build fields from "secrets" files. + """ + secrets: Dict[str, Optional[str]] = {} + + if self.secrets_dir is None: + return secrets + + secrets_path = Path(self.secrets_dir).expanduser() + + if not secrets_path.exists(): + warnings.warn(f'directory "{secrets_path}" does not exist') + return secrets + + if not secrets_path.is_dir(): + raise SettingsError(f'secrets_dir must reference a directory, not a {path_type(secrets_path)}') + + for field in settings.__fields__.values(): + for env_name in field.field_info.extra['env_names']: + path = find_case_path(secrets_path, env_name, settings.__config__.case_sensitive) + if not path: + # path does not exist, we currently don't return a warning for this + continue + + if path.is_file(): + secret_value = path.read_text().strip() + if field.is_complex(): + try: + secret_value = settings.__config__.parse_env_var(field.name, secret_value) + except ValueError as e: + raise SettingsError(f'error parsing env var "{env_name}"') from e + + secrets[field.alias] = secret_value + else: + warnings.warn( + f'attempted to load secret file "{path}" but found a {path_type(path)} instead.', + stacklevel=4, + ) + return secrets + + def __repr__(self) -> str: + return f'SecretsSettingsSource(secrets_dir={self.secrets_dir!r})' + + +def read_env_file( + file_path: StrPath, *, encoding: str = None, case_sensitive: bool = False +) -> Dict[str, Optional[str]]: + try: + from dotenv import dotenv_values + except ImportError as e: + raise ImportError('python-dotenv is not installed, run `pip install pydantic[dotenv]`') from e + + file_vars: Dict[str, Optional[str]] = dotenv_values(file_path, encoding=encoding or 'utf8') + if not case_sensitive: + return {k.lower(): v for k, v in file_vars.items()} + else: + return file_vars + + +def find_case_path(dir_path: Path, file_name: str, case_sensitive: bool) -> Optional[Path]: + """ + Find a file within path's directory matching filename, optionally ignoring case. + """ + for f in dir_path.iterdir(): + if f.name == file_name: + return f + elif not case_sensitive and f.name.lower() == file_name.lower(): + return f + return None diff --git a/source/target-vendors/cp312/pydantic/v1/error_wrappers.py b/source/target-vendors/cp312/pydantic/v1/error_wrappers.py new file mode 100644 index 0000000..bc7f263 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/error_wrappers.py @@ -0,0 +1,161 @@ +import json +from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Sequence, Tuple, Type, Union + +from pydantic.v1.json import pydantic_encoder +from pydantic.v1.utils import Representation + +if TYPE_CHECKING: + from typing_extensions import TypedDict + + from pydantic.v1.config import BaseConfig + from pydantic.v1.types import ModelOrDc + from pydantic.v1.typing import ReprArgs + + Loc = Tuple[Union[int, str], ...] + + class _ErrorDictRequired(TypedDict): + loc: Loc + msg: str + type: str + + class ErrorDict(_ErrorDictRequired, total=False): + ctx: Dict[str, Any] + + +__all__ = 'ErrorWrapper', 'ValidationError' + + +class ErrorWrapper(Representation): + __slots__ = 'exc', '_loc' + + def __init__(self, exc: Exception, loc: Union[str, 'Loc']) -> None: + self.exc = exc + self._loc = loc + + def loc_tuple(self) -> 'Loc': + if isinstance(self._loc, tuple): + return self._loc + else: + return (self._loc,) + + def __repr_args__(self) -> 'ReprArgs': + return [('exc', self.exc), ('loc', self.loc_tuple())] + + +# ErrorList is something like Union[List[Union[List[ErrorWrapper], ErrorWrapper]], ErrorWrapper] +# but recursive, therefore just use: +ErrorList = Union[Sequence[Any], ErrorWrapper] + + +class ValidationError(Representation, ValueError): + __slots__ = 'raw_errors', 'model', '_error_cache' + + def __init__(self, errors: Sequence[ErrorList], model: 'ModelOrDc') -> None: + self.raw_errors = errors + self.model = model + self._error_cache: Optional[List['ErrorDict']] = None + + def errors(self) -> List['ErrorDict']: + if self._error_cache is None: + try: + config = self.model.__config__ # type: ignore + except AttributeError: + config = self.model.__pydantic_model__.__config__ # type: ignore + self._error_cache = list(flatten_errors(self.raw_errors, config)) + return self._error_cache + + def json(self, *, indent: Union[None, int, str] = 2) -> str: + return json.dumps(self.errors(), indent=indent, default=pydantic_encoder) + + def __str__(self) -> str: + errors = self.errors() + no_errors = len(errors) + return ( + f'{no_errors} validation error{"" if no_errors == 1 else "s"} for {self.model.__name__}\n' + f'{display_errors(errors)}' + ) + + def __repr_args__(self) -> 'ReprArgs': + return [('model', self.model.__name__), ('errors', self.errors())] + + +def display_errors(errors: List['ErrorDict']) -> str: + return '\n'.join(f'{_display_error_loc(e)}\n {e["msg"]} ({_display_error_type_and_ctx(e)})' for e in errors) + + +def _display_error_loc(error: 'ErrorDict') -> str: + return ' -> '.join(str(e) for e in error['loc']) + + +def _display_error_type_and_ctx(error: 'ErrorDict') -> str: + t = 'type=' + error['type'] + ctx = error.get('ctx') + if ctx: + return t + ''.join(f'; {k}={v}' for k, v in ctx.items()) + else: + return t + + +def flatten_errors( + errors: Sequence[Any], config: Type['BaseConfig'], loc: Optional['Loc'] = None +) -> Generator['ErrorDict', None, None]: + for error in errors: + if isinstance(error, ErrorWrapper): + if loc: + error_loc = loc + error.loc_tuple() + else: + error_loc = error.loc_tuple() + + if isinstance(error.exc, ValidationError): + yield from flatten_errors(error.exc.raw_errors, config, error_loc) + else: + yield error_dict(error.exc, config, error_loc) + elif isinstance(error, list): + yield from flatten_errors(error, config, loc=loc) + else: + raise RuntimeError(f'Unknown error object: {error}') + + +def error_dict(exc: Exception, config: Type['BaseConfig'], loc: 'Loc') -> 'ErrorDict': + type_ = get_exc_type(exc.__class__) + msg_template = config.error_msg_templates.get(type_) or getattr(exc, 'msg_template', None) + ctx = exc.__dict__ + if msg_template: + msg = msg_template.format(**ctx) + else: + msg = str(exc) + + d: 'ErrorDict' = {'loc': loc, 'msg': msg, 'type': type_} + + if ctx: + d['ctx'] = ctx + + return d + + +_EXC_TYPE_CACHE: Dict[Type[Exception], str] = {} + + +def get_exc_type(cls: Type[Exception]) -> str: + # slightly more efficient than using lru_cache since we don't need to worry about the cache filling up + try: + return _EXC_TYPE_CACHE[cls] + except KeyError: + r = _get_exc_type(cls) + _EXC_TYPE_CACHE[cls] = r + return r + + +def _get_exc_type(cls: Type[Exception]) -> str: + if issubclass(cls, AssertionError): + return 'assertion_error' + + base_name = 'type_error' if issubclass(cls, TypeError) else 'value_error' + if cls in (TypeError, ValueError): + # just TypeError or ValueError, no extra code + return base_name + + # if it's not a TypeError or ValueError, we just take the lowercase of the exception name + # no chaining or snake case logic, use "code" for more complex error types. + code = getattr(cls, 'code', None) or cls.__name__.replace('Error', '').lower() + return base_name + '.' + code diff --git a/source/target-vendors/cp312/pydantic/v1/errors.py b/source/target-vendors/cp312/pydantic/v1/errors.py new file mode 100644 index 0000000..6e86442 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/errors.py @@ -0,0 +1,646 @@ +from decimal import Decimal +from pathlib import Path +from typing import TYPE_CHECKING, Any, Callable, Sequence, Set, Tuple, Type, Union + +from pydantic.v1.typing import display_as_type + +if TYPE_CHECKING: + from pydantic.v1.typing import DictStrAny + +# explicitly state exports to avoid "from pydantic.v1.errors import *" also importing Decimal, Path etc. +__all__ = ( + 'PydanticTypeError', + 'PydanticValueError', + 'ConfigError', + 'MissingError', + 'ExtraError', + 'NoneIsNotAllowedError', + 'NoneIsAllowedError', + 'WrongConstantError', + 'NotNoneError', + 'BoolError', + 'BytesError', + 'DictError', + 'EmailError', + 'UrlError', + 'UrlSchemeError', + 'UrlSchemePermittedError', + 'UrlUserInfoError', + 'UrlHostError', + 'UrlHostTldError', + 'UrlPortError', + 'UrlExtraError', + 'EnumError', + 'IntEnumError', + 'EnumMemberError', + 'IntegerError', + 'FloatError', + 'PathError', + 'PathNotExistsError', + 'PathNotAFileError', + 'PathNotADirectoryError', + 'PyObjectError', + 'SequenceError', + 'ListError', + 'SetError', + 'FrozenSetError', + 'TupleError', + 'TupleLengthError', + 'ListMinLengthError', + 'ListMaxLengthError', + 'ListUniqueItemsError', + 'SetMinLengthError', + 'SetMaxLengthError', + 'FrozenSetMinLengthError', + 'FrozenSetMaxLengthError', + 'AnyStrMinLengthError', + 'AnyStrMaxLengthError', + 'StrError', + 'StrRegexError', + 'NumberNotGtError', + 'NumberNotGeError', + 'NumberNotLtError', + 'NumberNotLeError', + 'NumberNotMultipleError', + 'DecimalError', + 'DecimalIsNotFiniteError', + 'DecimalMaxDigitsError', + 'DecimalMaxPlacesError', + 'DecimalWholeDigitsError', + 'DateTimeError', + 'DateError', + 'DateNotInThePastError', + 'DateNotInTheFutureError', + 'TimeError', + 'DurationError', + 'HashableError', + 'UUIDError', + 'UUIDVersionError', + 'ArbitraryTypeError', + 'ClassError', + 'SubclassError', + 'JsonError', + 'JsonTypeError', + 'PatternError', + 'DataclassTypeError', + 'CallableError', + 'IPvAnyAddressError', + 'IPvAnyInterfaceError', + 'IPvAnyNetworkError', + 'IPv4AddressError', + 'IPv6AddressError', + 'IPv4NetworkError', + 'IPv6NetworkError', + 'IPv4InterfaceError', + 'IPv6InterfaceError', + 'ColorError', + 'StrictBoolError', + 'NotDigitError', + 'LuhnValidationError', + 'InvalidLengthForBrand', + 'InvalidByteSize', + 'InvalidByteSizeUnit', + 'MissingDiscriminator', + 'InvalidDiscriminator', +) + + +def cls_kwargs(cls: Type['PydanticErrorMixin'], ctx: 'DictStrAny') -> 'PydanticErrorMixin': + """ + For built-in exceptions like ValueError or TypeError, we need to implement + __reduce__ to override the default behaviour (instead of __getstate__/__setstate__) + By default pickle protocol 2 calls `cls.__new__(cls, *args)`. + Since we only use kwargs, we need a little constructor to change that. + Note: the callable can't be a lambda as pickle looks in the namespace to find it + """ + return cls(**ctx) + + +class PydanticErrorMixin: + code: str + msg_template: str + + def __init__(self, **ctx: Any) -> None: + self.__dict__ = ctx + + def __str__(self) -> str: + return self.msg_template.format(**self.__dict__) + + def __reduce__(self) -> Tuple[Callable[..., 'PydanticErrorMixin'], Tuple[Type['PydanticErrorMixin'], 'DictStrAny']]: + return cls_kwargs, (self.__class__, self.__dict__) + + +class PydanticTypeError(PydanticErrorMixin, TypeError): + pass + + +class PydanticValueError(PydanticErrorMixin, ValueError): + pass + + +class ConfigError(RuntimeError): + pass + + +class MissingError(PydanticValueError): + msg_template = 'field required' + + +class ExtraError(PydanticValueError): + msg_template = 'extra fields not permitted' + + +class NoneIsNotAllowedError(PydanticTypeError): + code = 'none.not_allowed' + msg_template = 'none is not an allowed value' + + +class NoneIsAllowedError(PydanticTypeError): + code = 'none.allowed' + msg_template = 'value is not none' + + +class WrongConstantError(PydanticValueError): + code = 'const' + + def __str__(self) -> str: + permitted = ', '.join(repr(v) for v in self.permitted) # type: ignore + return f'unexpected value; permitted: {permitted}' + + +class NotNoneError(PydanticTypeError): + code = 'not_none' + msg_template = 'value is not None' + + +class BoolError(PydanticTypeError): + msg_template = 'value could not be parsed to a boolean' + + +class BytesError(PydanticTypeError): + msg_template = 'byte type expected' + + +class DictError(PydanticTypeError): + msg_template = 'value is not a valid dict' + + +class EmailError(PydanticValueError): + msg_template = 'value is not a valid email address' + + +class UrlError(PydanticValueError): + code = 'url' + + +class UrlSchemeError(UrlError): + code = 'url.scheme' + msg_template = 'invalid or missing URL scheme' + + +class UrlSchemePermittedError(UrlError): + code = 'url.scheme' + msg_template = 'URL scheme not permitted' + + def __init__(self, allowed_schemes: Set[str]): + super().__init__(allowed_schemes=allowed_schemes) + + +class UrlUserInfoError(UrlError): + code = 'url.userinfo' + msg_template = 'userinfo required in URL but missing' + + +class UrlHostError(UrlError): + code = 'url.host' + msg_template = 'URL host invalid' + + +class UrlHostTldError(UrlError): + code = 'url.host' + msg_template = 'URL host invalid, top level domain required' + + +class UrlPortError(UrlError): + code = 'url.port' + msg_template = 'URL port invalid, port cannot exceed 65535' + + +class UrlExtraError(UrlError): + code = 'url.extra' + msg_template = 'URL invalid, extra characters found after valid URL: {extra!r}' + + +class EnumMemberError(PydanticTypeError): + code = 'enum' + + def __str__(self) -> str: + permitted = ', '.join(repr(v.value) for v in self.enum_values) # type: ignore + return f'value is not a valid enumeration member; permitted: {permitted}' + + +class IntegerError(PydanticTypeError): + msg_template = 'value is not a valid integer' + + +class FloatError(PydanticTypeError): + msg_template = 'value is not a valid float' + + +class PathError(PydanticTypeError): + msg_template = 'value is not a valid path' + + +class _PathValueError(PydanticValueError): + def __init__(self, *, path: Path) -> None: + super().__init__(path=str(path)) + + +class PathNotExistsError(_PathValueError): + code = 'path.not_exists' + msg_template = 'file or directory at path "{path}" does not exist' + + +class PathNotAFileError(_PathValueError): + code = 'path.not_a_file' + msg_template = 'path "{path}" does not point to a file' + + +class PathNotADirectoryError(_PathValueError): + code = 'path.not_a_directory' + msg_template = 'path "{path}" does not point to a directory' + + +class PyObjectError(PydanticTypeError): + msg_template = 'ensure this value contains valid import path or valid callable: {error_message}' + + +class SequenceError(PydanticTypeError): + msg_template = 'value is not a valid sequence' + + +class IterableError(PydanticTypeError): + msg_template = 'value is not a valid iterable' + + +class ListError(PydanticTypeError): + msg_template = 'value is not a valid list' + + +class SetError(PydanticTypeError): + msg_template = 'value is not a valid set' + + +class FrozenSetError(PydanticTypeError): + msg_template = 'value is not a valid frozenset' + + +class DequeError(PydanticTypeError): + msg_template = 'value is not a valid deque' + + +class TupleError(PydanticTypeError): + msg_template = 'value is not a valid tuple' + + +class TupleLengthError(PydanticValueError): + code = 'tuple.length' + msg_template = 'wrong tuple length {actual_length}, expected {expected_length}' + + def __init__(self, *, actual_length: int, expected_length: int) -> None: + super().__init__(actual_length=actual_length, expected_length=expected_length) + + +class ListMinLengthError(PydanticValueError): + code = 'list.min_items' + msg_template = 'ensure this value has at least {limit_value} items' + + def __init__(self, *, limit_value: int) -> None: + super().__init__(limit_value=limit_value) + + +class ListMaxLengthError(PydanticValueError): + code = 'list.max_items' + msg_template = 'ensure this value has at most {limit_value} items' + + def __init__(self, *, limit_value: int) -> None: + super().__init__(limit_value=limit_value) + + +class ListUniqueItemsError(PydanticValueError): + code = 'list.unique_items' + msg_template = 'the list has duplicated items' + + +class SetMinLengthError(PydanticValueError): + code = 'set.min_items' + msg_template = 'ensure this value has at least {limit_value} items' + + def __init__(self, *, limit_value: int) -> None: + super().__init__(limit_value=limit_value) + + +class SetMaxLengthError(PydanticValueError): + code = 'set.max_items' + msg_template = 'ensure this value has at most {limit_value} items' + + def __init__(self, *, limit_value: int) -> None: + super().__init__(limit_value=limit_value) + + +class FrozenSetMinLengthError(PydanticValueError): + code = 'frozenset.min_items' + msg_template = 'ensure this value has at least {limit_value} items' + + def __init__(self, *, limit_value: int) -> None: + super().__init__(limit_value=limit_value) + + +class FrozenSetMaxLengthError(PydanticValueError): + code = 'frozenset.max_items' + msg_template = 'ensure this value has at most {limit_value} items' + + def __init__(self, *, limit_value: int) -> None: + super().__init__(limit_value=limit_value) + + +class AnyStrMinLengthError(PydanticValueError): + code = 'any_str.min_length' + msg_template = 'ensure this value has at least {limit_value} characters' + + def __init__(self, *, limit_value: int) -> None: + super().__init__(limit_value=limit_value) + + +class AnyStrMaxLengthError(PydanticValueError): + code = 'any_str.max_length' + msg_template = 'ensure this value has at most {limit_value} characters' + + def __init__(self, *, limit_value: int) -> None: + super().__init__(limit_value=limit_value) + + +class StrError(PydanticTypeError): + msg_template = 'str type expected' + + +class StrRegexError(PydanticValueError): + code = 'str.regex' + msg_template = 'string does not match regex "{pattern}"' + + def __init__(self, *, pattern: str) -> None: + super().__init__(pattern=pattern) + + +class _NumberBoundError(PydanticValueError): + def __init__(self, *, limit_value: Union[int, float, Decimal]) -> None: + super().__init__(limit_value=limit_value) + + +class NumberNotGtError(_NumberBoundError): + code = 'number.not_gt' + msg_template = 'ensure this value is greater than {limit_value}' + + +class NumberNotGeError(_NumberBoundError): + code = 'number.not_ge' + msg_template = 'ensure this value is greater than or equal to {limit_value}' + + +class NumberNotLtError(_NumberBoundError): + code = 'number.not_lt' + msg_template = 'ensure this value is less than {limit_value}' + + +class NumberNotLeError(_NumberBoundError): + code = 'number.not_le' + msg_template = 'ensure this value is less than or equal to {limit_value}' + + +class NumberNotFiniteError(PydanticValueError): + code = 'number.not_finite_number' + msg_template = 'ensure this value is a finite number' + + +class NumberNotMultipleError(PydanticValueError): + code = 'number.not_multiple' + msg_template = 'ensure this value is a multiple of {multiple_of}' + + def __init__(self, *, multiple_of: Union[int, float, Decimal]) -> None: + super().__init__(multiple_of=multiple_of) + + +class DecimalError(PydanticTypeError): + msg_template = 'value is not a valid decimal' + + +class DecimalIsNotFiniteError(PydanticValueError): + code = 'decimal.not_finite' + msg_template = 'value is not a valid decimal' + + +class DecimalMaxDigitsError(PydanticValueError): + code = 'decimal.max_digits' + msg_template = 'ensure that there are no more than {max_digits} digits in total' + + def __init__(self, *, max_digits: int) -> None: + super().__init__(max_digits=max_digits) + + +class DecimalMaxPlacesError(PydanticValueError): + code = 'decimal.max_places' + msg_template = 'ensure that there are no more than {decimal_places} decimal places' + + def __init__(self, *, decimal_places: int) -> None: + super().__init__(decimal_places=decimal_places) + + +class DecimalWholeDigitsError(PydanticValueError): + code = 'decimal.whole_digits' + msg_template = 'ensure that there are no more than {whole_digits} digits before the decimal point' + + def __init__(self, *, whole_digits: int) -> None: + super().__init__(whole_digits=whole_digits) + + +class DateTimeError(PydanticValueError): + msg_template = 'invalid datetime format' + + +class DateError(PydanticValueError): + msg_template = 'invalid date format' + + +class DateNotInThePastError(PydanticValueError): + code = 'date.not_in_the_past' + msg_template = 'date is not in the past' + + +class DateNotInTheFutureError(PydanticValueError): + code = 'date.not_in_the_future' + msg_template = 'date is not in the future' + + +class TimeError(PydanticValueError): + msg_template = 'invalid time format' + + +class DurationError(PydanticValueError): + msg_template = 'invalid duration format' + + +class HashableError(PydanticTypeError): + msg_template = 'value is not a valid hashable' + + +class UUIDError(PydanticTypeError): + msg_template = 'value is not a valid uuid' + + +class UUIDVersionError(PydanticValueError): + code = 'uuid.version' + msg_template = 'uuid version {required_version} expected' + + def __init__(self, *, required_version: int) -> None: + super().__init__(required_version=required_version) + + +class ArbitraryTypeError(PydanticTypeError): + code = 'arbitrary_type' + msg_template = 'instance of {expected_arbitrary_type} expected' + + def __init__(self, *, expected_arbitrary_type: Type[Any]) -> None: + super().__init__(expected_arbitrary_type=display_as_type(expected_arbitrary_type)) + + +class ClassError(PydanticTypeError): + code = 'class' + msg_template = 'a class is expected' + + +class SubclassError(PydanticTypeError): + code = 'subclass' + msg_template = 'subclass of {expected_class} expected' + + def __init__(self, *, expected_class: Type[Any]) -> None: + super().__init__(expected_class=display_as_type(expected_class)) + + +class JsonError(PydanticValueError): + msg_template = 'Invalid JSON' + + +class JsonTypeError(PydanticTypeError): + code = 'json' + msg_template = 'JSON object must be str, bytes or bytearray' + + +class PatternError(PydanticValueError): + code = 'regex_pattern' + msg_template = 'Invalid regular expression' + + +class DataclassTypeError(PydanticTypeError): + code = 'dataclass' + msg_template = 'instance of {class_name}, tuple or dict expected' + + +class CallableError(PydanticTypeError): + msg_template = '{value} is not callable' + + +class EnumError(PydanticTypeError): + code = 'enum_instance' + msg_template = '{value} is not a valid Enum instance' + + +class IntEnumError(PydanticTypeError): + code = 'int_enum_instance' + msg_template = '{value} is not a valid IntEnum instance' + + +class IPvAnyAddressError(PydanticValueError): + msg_template = 'value is not a valid IPv4 or IPv6 address' + + +class IPvAnyInterfaceError(PydanticValueError): + msg_template = 'value is not a valid IPv4 or IPv6 interface' + + +class IPvAnyNetworkError(PydanticValueError): + msg_template = 'value is not a valid IPv4 or IPv6 network' + + +class IPv4AddressError(PydanticValueError): + msg_template = 'value is not a valid IPv4 address' + + +class IPv6AddressError(PydanticValueError): + msg_template = 'value is not a valid IPv6 address' + + +class IPv4NetworkError(PydanticValueError): + msg_template = 'value is not a valid IPv4 network' + + +class IPv6NetworkError(PydanticValueError): + msg_template = 'value is not a valid IPv6 network' + + +class IPv4InterfaceError(PydanticValueError): + msg_template = 'value is not a valid IPv4 interface' + + +class IPv6InterfaceError(PydanticValueError): + msg_template = 'value is not a valid IPv6 interface' + + +class ColorError(PydanticValueError): + msg_template = 'value is not a valid color: {reason}' + + +class StrictBoolError(PydanticValueError): + msg_template = 'value is not a valid boolean' + + +class NotDigitError(PydanticValueError): + code = 'payment_card_number.digits' + msg_template = 'card number is not all digits' + + +class LuhnValidationError(PydanticValueError): + code = 'payment_card_number.luhn_check' + msg_template = 'card number is not luhn valid' + + +class InvalidLengthForBrand(PydanticValueError): + code = 'payment_card_number.invalid_length_for_brand' + msg_template = 'Length for a {brand} card must be {required_length}' + + +class InvalidByteSize(PydanticValueError): + msg_template = 'could not parse value and unit from byte string' + + +class InvalidByteSizeUnit(PydanticValueError): + msg_template = 'could not interpret byte unit: {unit}' + + +class MissingDiscriminator(PydanticValueError): + code = 'discriminated_union.missing_discriminator' + msg_template = 'Discriminator {discriminator_key!r} is missing in value' + + +class InvalidDiscriminator(PydanticValueError): + code = 'discriminated_union.invalid_discriminator' + msg_template = ( + 'No match for discriminator {discriminator_key!r} and value {discriminator_value!r} ' + '(allowed values: {allowed_values})' + ) + + def __init__(self, *, discriminator_key: str, discriminator_value: Any, allowed_values: Sequence[Any]) -> None: + super().__init__( + discriminator_key=discriminator_key, + discriminator_value=discriminator_value, + allowed_values=', '.join(map(repr, allowed_values)), + ) diff --git a/source/target-vendors/cp312/pydantic/v1/fields.py b/source/target-vendors/cp312/pydantic/v1/fields.py new file mode 100644 index 0000000..002b60c --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/fields.py @@ -0,0 +1,1253 @@ +import copy +import re +from collections import Counter as CollectionCounter, defaultdict, deque +from collections.abc import Callable, Hashable as CollectionsHashable, Iterable as CollectionsIterable +from typing import ( + TYPE_CHECKING, + Any, + Counter, + DefaultDict, + Deque, + Dict, + ForwardRef, + FrozenSet, + Generator, + Iterable, + Iterator, + List, + Mapping, + Optional, + Pattern, + Sequence, + Set, + Tuple, + Type, + TypeVar, + Union, +) + +from typing_extensions import Annotated, Final + +from pydantic.v1 import errors as errors_ +from pydantic.v1.class_validators import Validator, make_generic_validator, prep_validators +from pydantic.v1.error_wrappers import ErrorWrapper +from pydantic.v1.errors import ConfigError, InvalidDiscriminator, MissingDiscriminator, NoneIsNotAllowedError +from pydantic.v1.types import Json, JsonWrapper +from pydantic.v1.typing import ( + NoArgAnyCallable, + convert_generics, + display_as_type, + get_args, + get_origin, + is_finalvar, + is_literal_type, + is_new_type, + is_none_type, + is_typeddict, + is_typeddict_special, + is_union, + new_type_supertype, +) +from pydantic.v1.utils import ( + PyObjectStr, + Representation, + ValueItems, + get_discriminator_alias_and_values, + get_unique_discriminator_alias, + lenient_isinstance, + lenient_issubclass, + sequence_like, + smart_deepcopy, +) +from pydantic.v1.validators import constant_validator, dict_validator, find_validators, validate_json + +Required: Any = Ellipsis + +T = TypeVar('T') + + +class UndefinedType: + def __repr__(self) -> str: + return 'PydanticUndefined' + + def __copy__(self: T) -> T: + return self + + def __reduce__(self) -> str: + return 'Undefined' + + def __deepcopy__(self: T, _: Any) -> T: + return self + + +Undefined = UndefinedType() + +if TYPE_CHECKING: + from pydantic.v1.class_validators import ValidatorsList + from pydantic.v1.config import BaseConfig + from pydantic.v1.error_wrappers import ErrorList + from pydantic.v1.types import ModelOrDc + from pydantic.v1.typing import AbstractSetIntStr, MappingIntStrAny, ReprArgs + + ValidateReturn = Tuple[Optional[Any], Optional[ErrorList]] + LocStr = Union[Tuple[Union[int, str], ...], str] + BoolUndefined = Union[bool, UndefinedType] + + +class FieldInfo(Representation): + """ + Captures extra information about a field. + """ + + __slots__ = ( + 'default', + 'default_factory', + 'alias', + 'alias_priority', + 'title', + 'description', + 'exclude', + 'include', + 'const', + 'gt', + 'ge', + 'lt', + 'le', + 'multiple_of', + 'allow_inf_nan', + 'max_digits', + 'decimal_places', + 'min_items', + 'max_items', + 'unique_items', + 'min_length', + 'max_length', + 'allow_mutation', + 'repr', + 'regex', + 'discriminator', + 'extra', + ) + + # field constraints with the default value, it's also used in update_from_config below + __field_constraints__ = { + 'min_length': None, + 'max_length': None, + 'regex': None, + 'gt': None, + 'lt': None, + 'ge': None, + 'le': None, + 'multiple_of': None, + 'allow_inf_nan': None, + 'max_digits': None, + 'decimal_places': None, + 'min_items': None, + 'max_items': None, + 'unique_items': None, + 'allow_mutation': True, + } + + def __init__(self, default: Any = Undefined, **kwargs: Any) -> None: + self.default = default + self.default_factory = kwargs.pop('default_factory', None) + self.alias = kwargs.pop('alias', None) + self.alias_priority = kwargs.pop('alias_priority', 2 if self.alias is not None else None) + self.title = kwargs.pop('title', None) + self.description = kwargs.pop('description', None) + self.exclude = kwargs.pop('exclude', None) + self.include = kwargs.pop('include', None) + self.const = kwargs.pop('const', None) + self.gt = kwargs.pop('gt', None) + self.ge = kwargs.pop('ge', None) + self.lt = kwargs.pop('lt', None) + self.le = kwargs.pop('le', None) + self.multiple_of = kwargs.pop('multiple_of', None) + self.allow_inf_nan = kwargs.pop('allow_inf_nan', None) + self.max_digits = kwargs.pop('max_digits', None) + self.decimal_places = kwargs.pop('decimal_places', None) + self.min_items = kwargs.pop('min_items', None) + self.max_items = kwargs.pop('max_items', None) + self.unique_items = kwargs.pop('unique_items', None) + self.min_length = kwargs.pop('min_length', None) + self.max_length = kwargs.pop('max_length', None) + self.allow_mutation = kwargs.pop('allow_mutation', True) + self.regex = kwargs.pop('regex', None) + self.discriminator = kwargs.pop('discriminator', None) + self.repr = kwargs.pop('repr', True) + self.extra = kwargs + + def __repr_args__(self) -> 'ReprArgs': + field_defaults_to_hide: Dict[str, Any] = { + 'repr': True, + **self.__field_constraints__, + } + + attrs = ((s, getattr(self, s)) for s in self.__slots__) + return [(a, v) for a, v in attrs if v != field_defaults_to_hide.get(a, None)] + + def get_constraints(self) -> Set[str]: + """ + Gets the constraints set on the field by comparing the constraint value with its default value + + :return: the constraints set on field_info + """ + return {attr for attr, default in self.__field_constraints__.items() if getattr(self, attr) != default} + + def update_from_config(self, from_config: Dict[str, Any]) -> None: + """ + Update this FieldInfo based on a dict from get_field_info, only fields which have not been set are dated. + """ + for attr_name, value in from_config.items(): + try: + current_value = getattr(self, attr_name) + except AttributeError: + # attr_name is not an attribute of FieldInfo, it should therefore be added to extra + # (except if extra already has this value!) + self.extra.setdefault(attr_name, value) + else: + if current_value is self.__field_constraints__.get(attr_name, None): + setattr(self, attr_name, value) + elif attr_name == 'exclude': + self.exclude = ValueItems.merge(value, current_value) + elif attr_name == 'include': + self.include = ValueItems.merge(value, current_value, intersect=True) + + def _validate(self) -> None: + if self.default is not Undefined and self.default_factory is not None: + raise ValueError('cannot specify both default and default_factory') + + +def Field( + default: Any = Undefined, + *, + default_factory: Optional[NoArgAnyCallable] = None, + alias: Optional[str] = None, + title: Optional[str] = None, + description: Optional[str] = None, + exclude: Optional[Union['AbstractSetIntStr', 'MappingIntStrAny', Any]] = None, + include: Optional[Union['AbstractSetIntStr', 'MappingIntStrAny', Any]] = None, + const: Optional[bool] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + multiple_of: Optional[float] = None, + allow_inf_nan: Optional[bool] = None, + max_digits: Optional[int] = None, + decimal_places: Optional[int] = None, + min_items: Optional[int] = None, + max_items: Optional[int] = None, + unique_items: Optional[bool] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + allow_mutation: bool = True, + regex: Optional[str] = None, + discriminator: Optional[str] = None, + repr: bool = True, + **extra: Any, +) -> Any: + """ + Used to provide extra information about a field, either for the model schema or complex validation. Some arguments + apply only to number fields (``int``, ``float``, ``Decimal``) and some apply only to ``str``. + + :param default: since this is replacing the field’s default, its first argument is used + to set the default, use ellipsis (``...``) to indicate the field is required + :param default_factory: callable that will be called when a default value is needed for this field + If both `default` and `default_factory` are set, an error is raised. + :param alias: the public name of the field + :param title: can be any string, used in the schema + :param description: can be any string, used in the schema + :param exclude: exclude this field while dumping. + Takes same values as the ``include`` and ``exclude`` arguments on the ``.dict`` method. + :param include: include this field while dumping. + Takes same values as the ``include`` and ``exclude`` arguments on the ``.dict`` method. + :param const: this field is required and *must* take it's default value + :param gt: only applies to numbers, requires the field to be "greater than". The schema + will have an ``exclusiveMinimum`` validation keyword + :param ge: only applies to numbers, requires the field to be "greater than or equal to". The + schema will have a ``minimum`` validation keyword + :param lt: only applies to numbers, requires the field to be "less than". The schema + will have an ``exclusiveMaximum`` validation keyword + :param le: only applies to numbers, requires the field to be "less than or equal to". The + schema will have a ``maximum`` validation keyword + :param multiple_of: only applies to numbers, requires the field to be "a multiple of". The + schema will have a ``multipleOf`` validation keyword + :param allow_inf_nan: only applies to numbers, allows the field to be NaN or infinity (+inf or -inf), + which is a valid Python float. Default True, set to False for compatibility with JSON. + :param max_digits: only applies to Decimals, requires the field to have a maximum number + of digits within the decimal. It does not include a zero before the decimal point or trailing decimal zeroes. + :param decimal_places: only applies to Decimals, requires the field to have at most a number of decimal places + allowed. It does not include trailing decimal zeroes. + :param min_items: only applies to lists, requires the field to have a minimum number of + elements. The schema will have a ``minItems`` validation keyword + :param max_items: only applies to lists, requires the field to have a maximum number of + elements. The schema will have a ``maxItems`` validation keyword + :param unique_items: only applies to lists, requires the field not to have duplicated + elements. The schema will have a ``uniqueItems`` validation keyword + :param min_length: only applies to strings, requires the field to have a minimum length. The + schema will have a ``minLength`` validation keyword + :param max_length: only applies to strings, requires the field to have a maximum length. The + schema will have a ``maxLength`` validation keyword + :param allow_mutation: a boolean which defaults to True. When False, the field raises a TypeError if the field is + assigned on an instance. The BaseModel Config must set validate_assignment to True + :param regex: only applies to strings, requires the field match against a regular expression + pattern string. The schema will have a ``pattern`` validation keyword + :param discriminator: only useful with a (discriminated a.k.a. tagged) `Union` of sub models with a common field. + The `discriminator` is the name of this common field to shorten validation and improve generated schema + :param repr: show this field in the representation + :param **extra: any additional keyword arguments will be added as is to the schema + """ + field_info = FieldInfo( + default, + default_factory=default_factory, + alias=alias, + title=title, + description=description, + exclude=exclude, + include=include, + const=const, + gt=gt, + ge=ge, + lt=lt, + le=le, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + min_items=min_items, + max_items=max_items, + unique_items=unique_items, + min_length=min_length, + max_length=max_length, + allow_mutation=allow_mutation, + regex=regex, + discriminator=discriminator, + repr=repr, + **extra, + ) + field_info._validate() + return field_info + + +# used to be an enum but changed to int's for small performance improvement as less access overhead +SHAPE_SINGLETON = 1 +SHAPE_LIST = 2 +SHAPE_SET = 3 +SHAPE_MAPPING = 4 +SHAPE_TUPLE = 5 +SHAPE_TUPLE_ELLIPSIS = 6 +SHAPE_SEQUENCE = 7 +SHAPE_FROZENSET = 8 +SHAPE_ITERABLE = 9 +SHAPE_GENERIC = 10 +SHAPE_DEQUE = 11 +SHAPE_DICT = 12 +SHAPE_DEFAULTDICT = 13 +SHAPE_COUNTER = 14 +SHAPE_NAME_LOOKUP = { + SHAPE_LIST: 'List[{}]', + SHAPE_SET: 'Set[{}]', + SHAPE_TUPLE_ELLIPSIS: 'Tuple[{}, ...]', + SHAPE_SEQUENCE: 'Sequence[{}]', + SHAPE_FROZENSET: 'FrozenSet[{}]', + SHAPE_ITERABLE: 'Iterable[{}]', + SHAPE_DEQUE: 'Deque[{}]', + SHAPE_DICT: 'Dict[{}]', + SHAPE_DEFAULTDICT: 'DefaultDict[{}]', + SHAPE_COUNTER: 'Counter[{}]', +} + +MAPPING_LIKE_SHAPES: Set[int] = {SHAPE_DEFAULTDICT, SHAPE_DICT, SHAPE_MAPPING, SHAPE_COUNTER} + + +class ModelField(Representation): + __slots__ = ( + 'type_', + 'outer_type_', + 'annotation', + 'sub_fields', + 'sub_fields_mapping', + 'key_field', + 'validators', + 'pre_validators', + 'post_validators', + 'default', + 'default_factory', + 'required', + 'final', + 'model_config', + 'name', + 'alias', + 'has_alias', + 'field_info', + 'discriminator_key', + 'discriminator_alias', + 'validate_always', + 'allow_none', + 'shape', + 'class_validators', + 'parse_json', + ) + + def __init__( + self, + *, + name: str, + type_: Type[Any], + class_validators: Optional[Dict[str, Validator]], + model_config: Type['BaseConfig'], + default: Any = None, + default_factory: Optional[NoArgAnyCallable] = None, + required: 'BoolUndefined' = Undefined, + final: bool = False, + alias: Optional[str] = None, + field_info: Optional[FieldInfo] = None, + ) -> None: + self.name: str = name + self.has_alias: bool = alias is not None + self.alias: str = alias if alias is not None else name + self.annotation = type_ + self.type_: Any = convert_generics(type_) + self.outer_type_: Any = type_ + self.class_validators = class_validators or {} + self.default: Any = default + self.default_factory: Optional[NoArgAnyCallable] = default_factory + self.required: 'BoolUndefined' = required + self.final: bool = final + self.model_config = model_config + self.field_info: FieldInfo = field_info or FieldInfo(default) + self.discriminator_key: Optional[str] = self.field_info.discriminator + self.discriminator_alias: Optional[str] = self.discriminator_key + + self.allow_none: bool = False + self.validate_always: bool = False + self.sub_fields: Optional[List[ModelField]] = None + self.sub_fields_mapping: Optional[Dict[str, 'ModelField']] = None # used for discriminated union + self.key_field: Optional[ModelField] = None + self.validators: 'ValidatorsList' = [] + self.pre_validators: Optional['ValidatorsList'] = None + self.post_validators: Optional['ValidatorsList'] = None + self.parse_json: bool = False + self.shape: int = SHAPE_SINGLETON + self.model_config.prepare_field(self) + self.prepare() + + def get_default(self) -> Any: + return smart_deepcopy(self.default) if self.default_factory is None else self.default_factory() + + @staticmethod + def _get_field_info( + field_name: str, annotation: Any, value: Any, config: Type['BaseConfig'] + ) -> Tuple[FieldInfo, Any]: + """ + Get a FieldInfo from a root typing.Annotated annotation, value, or config default. + + The FieldInfo may be set in typing.Annotated or the value, but not both. If neither contain + a FieldInfo, a new one will be created using the config. + + :param field_name: name of the field for use in error messages + :param annotation: a type hint such as `str` or `Annotated[str, Field(..., min_length=5)]` + :param value: the field's assigned value + :param config: the model's config object + :return: the FieldInfo contained in the `annotation`, the value, or a new one from the config. + """ + field_info_from_config = config.get_field_info(field_name) + + field_info = None + if get_origin(annotation) is Annotated: + field_infos = [arg for arg in get_args(annotation)[1:] if isinstance(arg, FieldInfo)] + if len(field_infos) > 1: + raise ValueError(f'cannot specify multiple `Annotated` `Field`s for {field_name!r}') + field_info = next(iter(field_infos), None) + if field_info is not None: + field_info = copy.copy(field_info) + field_info.update_from_config(field_info_from_config) + if field_info.default not in (Undefined, Required): + raise ValueError(f'`Field` default cannot be set in `Annotated` for {field_name!r}') + if value is not Undefined and value is not Required: + # check also `Required` because of `validate_arguments` that sets `...` as default value + field_info.default = value + + if isinstance(value, FieldInfo): + if field_info is not None: + raise ValueError(f'cannot specify `Annotated` and value `Field`s together for {field_name!r}') + field_info = value + field_info.update_from_config(field_info_from_config) + elif field_info is None: + field_info = FieldInfo(value, **field_info_from_config) + value = None if field_info.default_factory is not None else field_info.default + field_info._validate() + return field_info, value + + @classmethod + def infer( + cls, + *, + name: str, + value: Any, + annotation: Any, + class_validators: Optional[Dict[str, Validator]], + config: Type['BaseConfig'], + ) -> 'ModelField': + from pydantic.v1.schema import get_annotation_from_field_info + + field_info, value = cls._get_field_info(name, annotation, value, config) + required: 'BoolUndefined' = Undefined + if value is Required: + required = True + value = None + elif value is not Undefined: + required = False + annotation = get_annotation_from_field_info(annotation, field_info, name, config.validate_assignment) + + return cls( + name=name, + type_=annotation, + alias=field_info.alias, + class_validators=class_validators, + default=value, + default_factory=field_info.default_factory, + required=required, + model_config=config, + field_info=field_info, + ) + + def set_config(self, config: Type['BaseConfig']) -> None: + self.model_config = config + info_from_config = config.get_field_info(self.name) + config.prepare_field(self) + new_alias = info_from_config.get('alias') + new_alias_priority = info_from_config.get('alias_priority') or 0 + if new_alias and new_alias_priority >= (self.field_info.alias_priority or 0): + self.field_info.alias = new_alias + self.field_info.alias_priority = new_alias_priority + self.alias = new_alias + new_exclude = info_from_config.get('exclude') + if new_exclude is not None: + self.field_info.exclude = ValueItems.merge(self.field_info.exclude, new_exclude) + new_include = info_from_config.get('include') + if new_include is not None: + self.field_info.include = ValueItems.merge(self.field_info.include, new_include, intersect=True) + + @property + def alt_alias(self) -> bool: + return self.name != self.alias + + def prepare(self) -> None: + """ + Prepare the field but inspecting self.default, self.type_ etc. + + Note: this method is **not** idempotent (because _type_analysis is not idempotent), + e.g. calling it it multiple times may modify the field and configure it incorrectly. + """ + self._set_default_and_type() + if self.type_.__class__ is ForwardRef or self.type_.__class__ is DeferredType: + # self.type_ is currently a ForwardRef and there's nothing we can do now, + # user will need to call model.update_forward_refs() + return + + self._type_analysis() + if self.required is Undefined: + self.required = True + if self.default is Undefined and self.default_factory is None: + self.default = None + self.populate_validators() + + def _set_default_and_type(self) -> None: + """ + Set the default value, infer the type if needed and check if `None` value is valid. + """ + if self.default_factory is not None: + if self.type_ is Undefined: + raise errors_.ConfigError( + f'you need to set the type of field {self.name!r} when using `default_factory`' + ) + return + + default_value = self.get_default() + + if default_value is not None and self.type_ is Undefined: + self.type_ = default_value.__class__ + self.outer_type_ = self.type_ + self.annotation = self.type_ + + if self.type_ is Undefined: + raise errors_.ConfigError(f'unable to infer type for attribute "{self.name}"') + + if self.required is False and default_value is None: + self.allow_none = True + + def _type_analysis(self) -> None: # noqa: C901 (ignore complexity) + # typing interface is horrible, we have to do some ugly checks + if lenient_issubclass(self.type_, JsonWrapper): + self.type_ = self.type_.inner_type + self.parse_json = True + elif lenient_issubclass(self.type_, Json): + self.type_ = Any + self.parse_json = True + elif isinstance(self.type_, TypeVar): + if self.type_.__bound__: + self.type_ = self.type_.__bound__ + elif self.type_.__constraints__: + self.type_ = Union[self.type_.__constraints__] + else: + self.type_ = Any + elif is_new_type(self.type_): + self.type_ = new_type_supertype(self.type_) + + if self.type_ is Any or self.type_ is object: + if self.required is Undefined: + self.required = False + self.allow_none = True + return + elif self.type_ is Pattern or self.type_ is re.Pattern: + # python 3.7 only, Pattern is a typing object but without sub fields + return + elif is_literal_type(self.type_): + return + elif is_typeddict(self.type_): + return + + if is_finalvar(self.type_): + self.final = True + + if self.type_ is Final: + self.type_ = Any + else: + self.type_ = get_args(self.type_)[0] + + self._type_analysis() + return + + origin = get_origin(self.type_) + + if origin is Annotated or is_typeddict_special(origin): + self.type_ = get_args(self.type_)[0] + self._type_analysis() + return + + if self.discriminator_key is not None and not is_union(origin): + raise TypeError('`discriminator` can only be used with `Union` type with more than one variant') + + # add extra check for `collections.abc.Hashable` for python 3.10+ where origin is not `None` + if origin is None or origin is CollectionsHashable: + # field is not "typing" object eg. Union, Dict, List etc. + # allow None for virtual superclasses of NoneType, e.g. Hashable + if isinstance(self.type_, type) and isinstance(None, self.type_): + self.allow_none = True + return + elif origin is Callable: + return + elif is_union(origin): + types_ = [] + for type_ in get_args(self.type_): + if is_none_type(type_) or type_ is Any or type_ is object: + if self.required is Undefined: + self.required = False + self.allow_none = True + if is_none_type(type_): + continue + types_.append(type_) + + if len(types_) == 1: + # Optional[] + self.type_ = types_[0] + # this is the one case where the "outer type" isn't just the original type + self.outer_type_ = self.type_ + # re-run to correctly interpret the new self.type_ + self._type_analysis() + else: + self.sub_fields = [self._create_sub_type(t, f'{self.name}_{display_as_type(t)}') for t in types_] + + if self.discriminator_key is not None: + self.prepare_discriminated_union_sub_fields() + return + elif issubclass(origin, Tuple): # type: ignore + # origin == Tuple without item type + args = get_args(self.type_) + if not args: # plain tuple + self.type_ = Any + self.shape = SHAPE_TUPLE_ELLIPSIS + elif len(args) == 2 and args[1] is Ellipsis: # e.g. Tuple[int, ...] + self.type_ = args[0] + self.shape = SHAPE_TUPLE_ELLIPSIS + self.sub_fields = [self._create_sub_type(args[0], f'{self.name}_0')] + elif args == ((),): # Tuple[()] means empty tuple + self.shape = SHAPE_TUPLE + self.type_ = Any + self.sub_fields = [] + else: + self.shape = SHAPE_TUPLE + self.sub_fields = [self._create_sub_type(t, f'{self.name}_{i}') for i, t in enumerate(args)] + return + elif issubclass(origin, List): + # Create self validators + get_validators = getattr(self.type_, '__get_validators__', None) + if get_validators: + self.class_validators.update( + {f'list_{i}': Validator(validator, pre=True) for i, validator in enumerate(get_validators())} + ) + + self.type_ = get_args(self.type_)[0] + self.shape = SHAPE_LIST + elif issubclass(origin, Set): + # Create self validators + get_validators = getattr(self.type_, '__get_validators__', None) + if get_validators: + self.class_validators.update( + {f'set_{i}': Validator(validator, pre=True) for i, validator in enumerate(get_validators())} + ) + + self.type_ = get_args(self.type_)[0] + self.shape = SHAPE_SET + elif issubclass(origin, FrozenSet): + # Create self validators + get_validators = getattr(self.type_, '__get_validators__', None) + if get_validators: + self.class_validators.update( + {f'frozenset_{i}': Validator(validator, pre=True) for i, validator in enumerate(get_validators())} + ) + + self.type_ = get_args(self.type_)[0] + self.shape = SHAPE_FROZENSET + elif issubclass(origin, Deque): + self.type_ = get_args(self.type_)[0] + self.shape = SHAPE_DEQUE + elif issubclass(origin, Sequence): + self.type_ = get_args(self.type_)[0] + self.shape = SHAPE_SEQUENCE + # priority to most common mapping: dict + elif origin is dict or origin is Dict: + self.key_field = self._create_sub_type(get_args(self.type_)[0], 'key_' + self.name, for_keys=True) + self.type_ = get_args(self.type_)[1] + self.shape = SHAPE_DICT + elif issubclass(origin, DefaultDict): + self.key_field = self._create_sub_type(get_args(self.type_)[0], 'key_' + self.name, for_keys=True) + self.type_ = get_args(self.type_)[1] + self.shape = SHAPE_DEFAULTDICT + elif issubclass(origin, Counter): + self.key_field = self._create_sub_type(get_args(self.type_)[0], 'key_' + self.name, for_keys=True) + self.type_ = int + self.shape = SHAPE_COUNTER + elif issubclass(origin, Mapping): + self.key_field = self._create_sub_type(get_args(self.type_)[0], 'key_' + self.name, for_keys=True) + self.type_ = get_args(self.type_)[1] + self.shape = SHAPE_MAPPING + # Equality check as almost everything inherits form Iterable, including str + # check for Iterable and CollectionsIterable, as it could receive one even when declared with the other + elif origin in {Iterable, CollectionsIterable}: + self.type_ = get_args(self.type_)[0] + self.shape = SHAPE_ITERABLE + self.sub_fields = [self._create_sub_type(self.type_, f'{self.name}_type')] + elif issubclass(origin, Type): # type: ignore + return + elif hasattr(origin, '__get_validators__') or self.model_config.arbitrary_types_allowed: + # Is a Pydantic-compatible generic that handles itself + # or we have arbitrary_types_allowed = True + self.shape = SHAPE_GENERIC + self.sub_fields = [self._create_sub_type(t, f'{self.name}_{i}') for i, t in enumerate(get_args(self.type_))] + self.type_ = origin + return + else: + raise TypeError(f'Fields of type "{origin}" are not supported.') + + # type_ has been refined eg. as the type of a List and sub_fields needs to be populated + self.sub_fields = [self._create_sub_type(self.type_, '_' + self.name)] + + def prepare_discriminated_union_sub_fields(self) -> None: + """ + Prepare the mapping -> and update `sub_fields` + Note that this process can be aborted if a `ForwardRef` is encountered + """ + assert self.discriminator_key is not None + + if self.type_.__class__ is DeferredType: + return + + assert self.sub_fields is not None + sub_fields_mapping: Dict[str, 'ModelField'] = {} + all_aliases: Set[str] = set() + + for sub_field in self.sub_fields: + t = sub_field.type_ + if t.__class__ is ForwardRef: + # Stopping everything...will need to call `update_forward_refs` + return + + alias, discriminator_values = get_discriminator_alias_and_values(t, self.discriminator_key) + all_aliases.add(alias) + for discriminator_value in discriminator_values: + sub_fields_mapping[discriminator_value] = sub_field + + self.sub_fields_mapping = sub_fields_mapping + self.discriminator_alias = get_unique_discriminator_alias(all_aliases, self.discriminator_key) + + def _create_sub_type(self, type_: Type[Any], name: str, *, for_keys: bool = False) -> 'ModelField': + if for_keys: + class_validators = None + else: + # validators for sub items should not have `each_item` as we want to check only the first sublevel + class_validators = { + k: Validator( + func=v.func, + pre=v.pre, + each_item=False, + always=v.always, + check_fields=v.check_fields, + skip_on_failure=v.skip_on_failure, + ) + for k, v in self.class_validators.items() + if v.each_item + } + + field_info, _ = self._get_field_info(name, type_, None, self.model_config) + + return self.__class__( + type_=type_, + name=name, + class_validators=class_validators, + model_config=self.model_config, + field_info=field_info, + ) + + def populate_validators(self) -> None: + """ + Prepare self.pre_validators, self.validators, and self.post_validators based on self.type_'s __get_validators__ + and class validators. This method should be idempotent, e.g. it should be safe to call multiple times + without mis-configuring the field. + """ + self.validate_always = getattr(self.type_, 'validate_always', False) or any( + v.always for v in self.class_validators.values() + ) + + class_validators_ = self.class_validators.values() + if not self.sub_fields or self.shape == SHAPE_GENERIC: + get_validators = getattr(self.type_, '__get_validators__', None) + v_funcs = ( + *[v.func for v in class_validators_ if v.each_item and v.pre], + *(get_validators() if get_validators else list(find_validators(self.type_, self.model_config))), + *[v.func for v in class_validators_ if v.each_item and not v.pre], + ) + self.validators = prep_validators(v_funcs) + + self.pre_validators = [] + self.post_validators = [] + + if self.field_info and self.field_info.const: + self.post_validators.append(make_generic_validator(constant_validator)) + + if class_validators_: + self.pre_validators += prep_validators(v.func for v in class_validators_ if not v.each_item and v.pre) + self.post_validators += prep_validators(v.func for v in class_validators_ if not v.each_item and not v.pre) + + if self.parse_json: + self.pre_validators.append(make_generic_validator(validate_json)) + + self.pre_validators = self.pre_validators or None + self.post_validators = self.post_validators or None + + def validate( + self, v: Any, values: Dict[str, Any], *, loc: 'LocStr', cls: Optional['ModelOrDc'] = None + ) -> 'ValidateReturn': + assert self.type_.__class__ is not DeferredType + + if self.type_.__class__ is ForwardRef: + assert cls is not None + raise ConfigError( + f'field "{self.name}" not yet prepared so type is still a ForwardRef, ' + f'you might need to call {cls.__name__}.update_forward_refs().' + ) + + errors: Optional['ErrorList'] + if self.pre_validators: + v, errors = self._apply_validators(v, values, loc, cls, self.pre_validators) + if errors: + return v, errors + + if v is None: + if is_none_type(self.type_): + # keep validating + pass + elif self.allow_none: + if self.post_validators: + return self._apply_validators(v, values, loc, cls, self.post_validators) + else: + return None, None + else: + return v, ErrorWrapper(NoneIsNotAllowedError(), loc) + + if self.shape == SHAPE_SINGLETON: + v, errors = self._validate_singleton(v, values, loc, cls) + elif self.shape in MAPPING_LIKE_SHAPES: + v, errors = self._validate_mapping_like(v, values, loc, cls) + elif self.shape == SHAPE_TUPLE: + v, errors = self._validate_tuple(v, values, loc, cls) + elif self.shape == SHAPE_ITERABLE: + v, errors = self._validate_iterable(v, values, loc, cls) + elif self.shape == SHAPE_GENERIC: + v, errors = self._apply_validators(v, values, loc, cls, self.validators) + else: + # sequence, list, set, generator, tuple with ellipsis, frozen set + v, errors = self._validate_sequence_like(v, values, loc, cls) + + if not errors and self.post_validators: + v, errors = self._apply_validators(v, values, loc, cls, self.post_validators) + return v, errors + + def _validate_sequence_like( # noqa: C901 (ignore complexity) + self, v: Any, values: Dict[str, Any], loc: 'LocStr', cls: Optional['ModelOrDc'] + ) -> 'ValidateReturn': + """ + Validate sequence-like containers: lists, tuples, sets and generators + Note that large if-else blocks are necessary to enable Cython + optimization, which is why we disable the complexity check above. + """ + if not sequence_like(v): + e: errors_.PydanticTypeError + if self.shape == SHAPE_LIST: + e = errors_.ListError() + elif self.shape in (SHAPE_TUPLE, SHAPE_TUPLE_ELLIPSIS): + e = errors_.TupleError() + elif self.shape == SHAPE_SET: + e = errors_.SetError() + elif self.shape == SHAPE_FROZENSET: + e = errors_.FrozenSetError() + else: + e = errors_.SequenceError() + return v, ErrorWrapper(e, loc) + + loc = loc if isinstance(loc, tuple) else (loc,) + result = [] + errors: List[ErrorList] = [] + for i, v_ in enumerate(v): + v_loc = *loc, i + r, ee = self._validate_singleton(v_, values, v_loc, cls) + if ee: + errors.append(ee) + else: + result.append(r) + + if errors: + return v, errors + + converted: Union[List[Any], Set[Any], FrozenSet[Any], Tuple[Any, ...], Iterator[Any], Deque[Any]] = result + + if self.shape == SHAPE_SET: + converted = set(result) + elif self.shape == SHAPE_FROZENSET: + converted = frozenset(result) + elif self.shape == SHAPE_TUPLE_ELLIPSIS: + converted = tuple(result) + elif self.shape == SHAPE_DEQUE: + converted = deque(result, maxlen=getattr(v, 'maxlen', None)) + elif self.shape == SHAPE_SEQUENCE: + if isinstance(v, tuple): + converted = tuple(result) + elif isinstance(v, set): + converted = set(result) + elif isinstance(v, Generator): + converted = iter(result) + elif isinstance(v, deque): + converted = deque(result, maxlen=getattr(v, 'maxlen', None)) + return converted, None + + def _validate_iterable( + self, v: Any, values: Dict[str, Any], loc: 'LocStr', cls: Optional['ModelOrDc'] + ) -> 'ValidateReturn': + """ + Validate Iterables. + + This intentionally doesn't validate values to allow infinite generators. + """ + + try: + iterable = iter(v) + except TypeError: + return v, ErrorWrapper(errors_.IterableError(), loc) + return iterable, None + + def _validate_tuple( + self, v: Any, values: Dict[str, Any], loc: 'LocStr', cls: Optional['ModelOrDc'] + ) -> 'ValidateReturn': + e: Optional[Exception] = None + if not sequence_like(v): + e = errors_.TupleError() + else: + actual_length, expected_length = len(v), len(self.sub_fields) # type: ignore + if actual_length != expected_length: + e = errors_.TupleLengthError(actual_length=actual_length, expected_length=expected_length) + + if e: + return v, ErrorWrapper(e, loc) + + loc = loc if isinstance(loc, tuple) else (loc,) + result = [] + errors: List[ErrorList] = [] + for i, (v_, field) in enumerate(zip(v, self.sub_fields)): # type: ignore + v_loc = *loc, i + r, ee = field.validate(v_, values, loc=v_loc, cls=cls) + if ee: + errors.append(ee) + else: + result.append(r) + + if errors: + return v, errors + else: + return tuple(result), None + + def _validate_mapping_like( + self, v: Any, values: Dict[str, Any], loc: 'LocStr', cls: Optional['ModelOrDc'] + ) -> 'ValidateReturn': + try: + v_iter = dict_validator(v) + except TypeError as exc: + return v, ErrorWrapper(exc, loc) + + loc = loc if isinstance(loc, tuple) else (loc,) + result, errors = {}, [] + for k, v_ in v_iter.items(): + v_loc = *loc, '__key__' + key_result, key_errors = self.key_field.validate(k, values, loc=v_loc, cls=cls) # type: ignore + if key_errors: + errors.append(key_errors) + continue + + v_loc = *loc, k + value_result, value_errors = self._validate_singleton(v_, values, v_loc, cls) + if value_errors: + errors.append(value_errors) + continue + + result[key_result] = value_result + if errors: + return v, errors + elif self.shape == SHAPE_DICT: + return result, None + elif self.shape == SHAPE_DEFAULTDICT: + return defaultdict(self.type_, result), None + elif self.shape == SHAPE_COUNTER: + return CollectionCounter(result), None + else: + return self._get_mapping_value(v, result), None + + def _get_mapping_value(self, original: T, converted: Dict[Any, Any]) -> Union[T, Dict[Any, Any]]: + """ + When type is `Mapping[KT, KV]` (or another unsupported mapping), we try to avoid + coercing to `dict` unwillingly. + """ + original_cls = original.__class__ + + if original_cls == dict or original_cls == Dict: + return converted + elif original_cls in {defaultdict, DefaultDict}: + return defaultdict(self.type_, converted) + else: + try: + # Counter, OrderedDict, UserDict, ... + return original_cls(converted) # type: ignore + except TypeError: + raise RuntimeError(f'Could not convert dictionary to {original_cls.__name__!r}') from None + + def _validate_singleton( + self, v: Any, values: Dict[str, Any], loc: 'LocStr', cls: Optional['ModelOrDc'] + ) -> 'ValidateReturn': + if self.sub_fields: + if self.discriminator_key is not None: + return self._validate_discriminated_union(v, values, loc, cls) + + errors = [] + + if self.model_config.smart_union and is_union(get_origin(self.type_)): + # 1st pass: check if the value is an exact instance of one of the Union types + # (e.g. to avoid coercing a bool into an int) + for field in self.sub_fields: + if v.__class__ is field.outer_type_: + return v, None + + # 2nd pass: check if the value is an instance of any subclass of the Union types + for field in self.sub_fields: + # This whole logic will be improved later on to support more complex `isinstance` checks + # It will probably be done once a strict mode is added and be something like: + # ``` + # value, error = field.validate(v, values, strict=True) + # if error is None: + # return value, None + # ``` + try: + if isinstance(v, field.outer_type_): + return v, None + except TypeError: + # compound type + if lenient_isinstance(v, get_origin(field.outer_type_)): + value, error = field.validate(v, values, loc=loc, cls=cls) + if not error: + return value, None + + # 1st pass by default or 3rd pass with `smart_union` enabled: + # check if the value can be coerced into one of the Union types + for field in self.sub_fields: + value, error = field.validate(v, values, loc=loc, cls=cls) + if error: + errors.append(error) + else: + return value, None + return v, errors + else: + return self._apply_validators(v, values, loc, cls, self.validators) + + def _validate_discriminated_union( + self, v: Any, values: Dict[str, Any], loc: 'LocStr', cls: Optional['ModelOrDc'] + ) -> 'ValidateReturn': + assert self.discriminator_key is not None + assert self.discriminator_alias is not None + + try: + try: + discriminator_value = v[self.discriminator_alias] + except KeyError: + if self.model_config.allow_population_by_field_name: + discriminator_value = v[self.discriminator_key] + else: + raise + except KeyError: + return v, ErrorWrapper(MissingDiscriminator(discriminator_key=self.discriminator_key), loc) + except TypeError: + try: + # BaseModel or dataclass + discriminator_value = getattr(v, self.discriminator_key) + except (AttributeError, TypeError): + return v, ErrorWrapper(MissingDiscriminator(discriminator_key=self.discriminator_key), loc) + + if self.sub_fields_mapping is None: + assert cls is not None + raise ConfigError( + f'field "{self.name}" not yet prepared so type is still a ForwardRef, ' + f'you might need to call {cls.__name__}.update_forward_refs().' + ) + + try: + sub_field = self.sub_fields_mapping[discriminator_value] + except (KeyError, TypeError): + # KeyError: `discriminator_value` is not in the dictionary. + # TypeError: `discriminator_value` is unhashable. + assert self.sub_fields_mapping is not None + return v, ErrorWrapper( + InvalidDiscriminator( + discriminator_key=self.discriminator_key, + discriminator_value=discriminator_value, + allowed_values=list(self.sub_fields_mapping), + ), + loc, + ) + else: + if not isinstance(loc, tuple): + loc = (loc,) + return sub_field.validate(v, values, loc=(*loc, display_as_type(sub_field.type_)), cls=cls) + + def _apply_validators( + self, v: Any, values: Dict[str, Any], loc: 'LocStr', cls: Optional['ModelOrDc'], validators: 'ValidatorsList' + ) -> 'ValidateReturn': + for validator in validators: + try: + v = validator(cls, v, values, self, self.model_config) + except (ValueError, TypeError, AssertionError) as exc: + return v, ErrorWrapper(exc, loc) + return v, None + + def is_complex(self) -> bool: + """ + Whether the field is "complex" eg. env variables should be parsed as JSON. + """ + from pydantic.v1.main import BaseModel + + return ( + self.shape != SHAPE_SINGLETON + or hasattr(self.type_, '__pydantic_model__') + or lenient_issubclass(self.type_, (BaseModel, list, set, frozenset, dict)) + ) + + def _type_display(self) -> PyObjectStr: + t = display_as_type(self.type_) + + if self.shape in MAPPING_LIKE_SHAPES: + t = f'Mapping[{display_as_type(self.key_field.type_)}, {t}]' # type: ignore + elif self.shape == SHAPE_TUPLE: + t = 'Tuple[{}]'.format(', '.join(display_as_type(f.type_) for f in self.sub_fields)) # type: ignore + elif self.shape == SHAPE_GENERIC: + assert self.sub_fields + t = '{}[{}]'.format( + display_as_type(self.type_), ', '.join(display_as_type(f.type_) for f in self.sub_fields) + ) + elif self.shape != SHAPE_SINGLETON: + t = SHAPE_NAME_LOOKUP[self.shape].format(t) + + if self.allow_none and (self.shape != SHAPE_SINGLETON or not self.sub_fields): + t = f'Optional[{t}]' + return PyObjectStr(t) + + def __repr_args__(self) -> 'ReprArgs': + args = [('name', self.name), ('type', self._type_display()), ('required', self.required)] + + if not self.required: + if self.default_factory is not None: + args.append(('default_factory', f'')) + else: + args.append(('default', self.default)) + + if self.alt_alias: + args.append(('alias', self.alias)) + return args + + +class ModelPrivateAttr(Representation): + __slots__ = ('default', 'default_factory') + + def __init__(self, default: Any = Undefined, *, default_factory: Optional[NoArgAnyCallable] = None) -> None: + self.default = default + self.default_factory = default_factory + + def get_default(self) -> Any: + return smart_deepcopy(self.default) if self.default_factory is None else self.default_factory() + + def __eq__(self, other: Any) -> bool: + return isinstance(other, self.__class__) and (self.default, self.default_factory) == ( + other.default, + other.default_factory, + ) + + +def PrivateAttr( + default: Any = Undefined, + *, + default_factory: Optional[NoArgAnyCallable] = None, +) -> Any: + """ + Indicates that attribute is only used internally and never mixed with regular fields. + + Types or values of private attrs are not checked by pydantic and it's up to you to keep them relevant. + + Private attrs are stored in model __slots__. + + :param default: the attribute’s default value + :param default_factory: callable that will be called when a default value is needed for this attribute + If both `default` and `default_factory` are set, an error is raised. + """ + if default is not Undefined and default_factory is not None: + raise ValueError('cannot specify both default and default_factory') + + return ModelPrivateAttr( + default, + default_factory=default_factory, + ) + + +class DeferredType: + """ + Used to postpone field preparation, while creating recursive generic models. + """ + + +def is_finalvar_with_default_val(type_: Type[Any], val: Any) -> bool: + return is_finalvar(type_) and val is not Undefined and not isinstance(val, FieldInfo) diff --git a/source/target-vendors/cp312/pydantic/v1/generics.py b/source/target-vendors/cp312/pydantic/v1/generics.py new file mode 100644 index 0000000..fa1dcec --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/generics.py @@ -0,0 +1,400 @@ +import functools +import operator +import sys +import types +import typing +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Dict, + ForwardRef, + Generic, + Iterator, + List, + Mapping, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, +) +from weakref import WeakKeyDictionary, WeakValueDictionary + +from typing_extensions import Annotated, Literal as ExtLiteral + +from pydantic.v1.class_validators import gather_all_validators +from pydantic.v1.fields import DeferredType +from pydantic.v1.main import BaseModel, create_model +from pydantic.v1.types import JsonWrapper +from pydantic.v1.typing import display_as_type, get_all_type_hints, get_args, get_origin, typing_base +from pydantic.v1.utils import all_identical, lenient_issubclass + +if sys.version_info >= (3, 8): + from typing import Literal + +GenericModelT = TypeVar('GenericModelT', bound='GenericModel') +TypeVarType = Any # since mypy doesn't allow the use of TypeVar as a type + +CacheKey = Tuple[Type[Any], Any, Tuple[Any, ...]] +Parametrization = Mapping[TypeVarType, Type[Any]] + +# weak dictionaries allow the dynamically created parametrized versions of generic models to get collected +# once they are no longer referenced by the caller. +if sys.version_info >= (3, 9): # Typing for weak dictionaries available at 3.9 + GenericTypesCache = WeakValueDictionary[CacheKey, Type[BaseModel]] + AssignedParameters = WeakKeyDictionary[Type[BaseModel], Parametrization] +else: + GenericTypesCache = WeakValueDictionary + AssignedParameters = WeakKeyDictionary + +# _generic_types_cache is a Mapping from __class_getitem__ arguments to the parametrized version of generic models. +# This ensures multiple calls of e.g. A[B] return always the same class. +_generic_types_cache = GenericTypesCache() + +# _assigned_parameters is a Mapping from parametrized version of generic models to assigned types of parametrizations +# as captured during construction of the class (not instances). +# E.g., for generic model `Model[A, B]`, when parametrized model `Model[int, str]` is created, +# `Model[int, str]`: {A: int, B: str}` will be stored in `_assigned_parameters`. +# (This information is only otherwise available after creation from the class name string). +_assigned_parameters = AssignedParameters() + + +class GenericModel(BaseModel): + __slots__ = () + __concrete__: ClassVar[bool] = False + + if TYPE_CHECKING: + # Putting this in a TYPE_CHECKING block allows us to replace `if Generic not in cls.__bases__` with + # `not hasattr(cls, "__parameters__")`. This means we don't need to force non-concrete subclasses of + # `GenericModel` to also inherit from `Generic`, which would require changes to the use of `create_model` below. + __parameters__: ClassVar[Tuple[TypeVarType, ...]] + + # Setting the return type as Type[Any] instead of Type[BaseModel] prevents PyCharm warnings + def __class_getitem__(cls: Type[GenericModelT], params: Union[Type[Any], Tuple[Type[Any], ...]]) -> Type[Any]: + """Instantiates a new class from a generic class `cls` and type variables `params`. + + :param params: Tuple of types the class . Given a generic class + `Model` with 2 type variables and a concrete model `Model[str, int]`, + the value `(str, int)` would be passed to `params`. + :return: New model class inheriting from `cls` with instantiated + types described by `params`. If no parameters are given, `cls` is + returned as is. + + """ + + def _cache_key(_params: Any) -> CacheKey: + args = get_args(_params) + # python returns a list for Callables, which is not hashable + if len(args) == 2 and isinstance(args[0], list): + args = (tuple(args[0]), args[1]) + return cls, _params, args + + cached = _generic_types_cache.get(_cache_key(params)) + if cached is not None: + return cached + if cls.__concrete__ and Generic not in cls.__bases__: + raise TypeError('Cannot parameterize a concrete instantiation of a generic model') + if not isinstance(params, tuple): + params = (params,) + if cls is GenericModel and any(isinstance(param, TypeVar) for param in params): + raise TypeError('Type parameters should be placed on typing.Generic, not GenericModel') + if not hasattr(cls, '__parameters__'): + raise TypeError(f'Type {cls.__name__} must inherit from typing.Generic before being parameterized') + + check_parameters_count(cls, params) + # Build map from generic typevars to passed params + typevars_map: Dict[TypeVarType, Type[Any]] = dict(zip(cls.__parameters__, params)) + if all_identical(typevars_map.keys(), typevars_map.values()) and typevars_map: + return cls # if arguments are equal to parameters it's the same object + + # Create new model with original model as parent inserting fields with DeferredType. + model_name = cls.__concrete_name__(params) + validators = gather_all_validators(cls) + + type_hints = get_all_type_hints(cls).items() + instance_type_hints = {k: v for k, v in type_hints if get_origin(v) is not ClassVar} + + fields = {k: (DeferredType(), cls.__fields__[k].field_info) for k in instance_type_hints if k in cls.__fields__} + + model_module, called_globally = get_caller_frame_info() + created_model = cast( + Type[GenericModel], # casting ensures mypy is aware of the __concrete__ and __parameters__ attributes + create_model( + model_name, + __module__=model_module or cls.__module__, + __base__=(cls,) + tuple(cls.__parameterized_bases__(typevars_map)), + __config__=None, + __validators__=validators, + __cls_kwargs__=None, + **fields, + ), + ) + + _assigned_parameters[created_model] = typevars_map + + if called_globally: # create global reference and therefore allow pickling + object_by_reference = None + reference_name = model_name + reference_module_globals = sys.modules[created_model.__module__].__dict__ + while object_by_reference is not created_model: + object_by_reference = reference_module_globals.setdefault(reference_name, created_model) + reference_name += '_' + + created_model.Config = cls.Config + + # Find any typevars that are still present in the model. + # If none are left, the model is fully "concrete", otherwise the new + # class is a generic class as well taking the found typevars as + # parameters. + new_params = tuple( + {param: None for param in iter_contained_typevars(typevars_map.values())} + ) # use dict as ordered set + created_model.__concrete__ = not new_params + if new_params: + created_model.__parameters__ = new_params + + # Save created model in cache so we don't end up creating duplicate + # models that should be identical. + _generic_types_cache[_cache_key(params)] = created_model + if len(params) == 1: + _generic_types_cache[_cache_key(params[0])] = created_model + + # Recursively walk class type hints and replace generic typevars + # with concrete types that were passed. + _prepare_model_fields(created_model, fields, instance_type_hints, typevars_map) + + return created_model + + @classmethod + def __concrete_name__(cls: Type[Any], params: Tuple[Type[Any], ...]) -> str: + """Compute class name for child classes. + + :param params: Tuple of types the class . Given a generic class + `Model` with 2 type variables and a concrete model `Model[str, int]`, + the value `(str, int)` would be passed to `params`. + :return: String representing a the new class where `params` are + passed to `cls` as type variables. + + This method can be overridden to achieve a custom naming scheme for GenericModels. + """ + param_names = [display_as_type(param) for param in params] + params_component = ', '.join(param_names) + return f'{cls.__name__}[{params_component}]' + + @classmethod + def __parameterized_bases__(cls, typevars_map: Parametrization) -> Iterator[Type[Any]]: + """ + Returns unbound bases of cls parameterised to given type variables + + :param typevars_map: Dictionary of type applications for binding subclasses. + Given a generic class `Model` with 2 type variables [S, T] + and a concrete model `Model[str, int]`, + the value `{S: str, T: int}` would be passed to `typevars_map`. + :return: an iterator of generic sub classes, parameterised by `typevars_map` + and other assigned parameters of `cls` + + e.g.: + ``` + class A(GenericModel, Generic[T]): + ... + + class B(A[V], Generic[V]): + ... + + assert A[int] in B.__parameterized_bases__({V: int}) + ``` + """ + + def build_base_model( + base_model: Type[GenericModel], mapped_types: Parametrization + ) -> Iterator[Type[GenericModel]]: + base_parameters = tuple(mapped_types[param] for param in base_model.__parameters__) + parameterized_base = base_model.__class_getitem__(base_parameters) + if parameterized_base is base_model or parameterized_base is cls: + # Avoid duplication in MRO + return + yield parameterized_base + + for base_model in cls.__bases__: + if not issubclass(base_model, GenericModel): + # not a class that can be meaningfully parameterized + continue + elif not getattr(base_model, '__parameters__', None): + # base_model is "GenericModel" (and has no __parameters__) + # or + # base_model is already concrete, and will be included transitively via cls. + continue + elif cls in _assigned_parameters: + if base_model in _assigned_parameters: + # cls is partially parameterised but not from base_model + # e.g. cls = B[S], base_model = A[S] + # B[S][int] should subclass A[int], (and will be transitively via B[int]) + # but it's not viable to consistently subclass types with arbitrary construction + # So don't attempt to include A[S][int] + continue + else: # base_model not in _assigned_parameters: + # cls is partially parameterized, base_model is original generic + # e.g. cls = B[str, T], base_model = B[S, T] + # Need to determine the mapping for the base_model parameters + mapped_types: Parametrization = { + key: typevars_map.get(value, value) for key, value in _assigned_parameters[cls].items() + } + yield from build_base_model(base_model, mapped_types) + else: + # cls is base generic, so base_class has a distinct base + # can construct the Parameterised base model using typevars_map directly + yield from build_base_model(base_model, typevars_map) + + +def replace_types(type_: Any, type_map: Mapping[Any, Any]) -> Any: + """Return type with all occurrences of `type_map` keys recursively replaced with their values. + + :param type_: Any type, class or generic alias + :param type_map: Mapping from `TypeVar` instance to concrete types. + :return: New type representing the basic structure of `type_` with all + `typevar_map` keys recursively replaced. + + >>> replace_types(Tuple[str, Union[List[str], float]], {str: int}) + Tuple[int, Union[List[int], float]] + + """ + if not type_map: + return type_ + + type_args = get_args(type_) + origin_type = get_origin(type_) + + if origin_type is Annotated: + annotated_type, *annotations = type_args + return Annotated[replace_types(annotated_type, type_map), tuple(annotations)] + + if (origin_type is ExtLiteral) or (sys.version_info >= (3, 8) and origin_type is Literal): + return type_map.get(type_, type_) + # Having type args is a good indicator that this is a typing module + # class instantiation or a generic alias of some sort. + if type_args: + resolved_type_args = tuple(replace_types(arg, type_map) for arg in type_args) + if all_identical(type_args, resolved_type_args): + # If all arguments are the same, there is no need to modify the + # type or create a new object at all + return type_ + if ( + origin_type is not None + and isinstance(type_, typing_base) + and not isinstance(origin_type, typing_base) + and getattr(type_, '_name', None) is not None + ): + # In python < 3.9 generic aliases don't exist so any of these like `list`, + # `type` or `collections.abc.Callable` need to be translated. + # See: https://www.python.org/dev/peps/pep-0585 + origin_type = getattr(typing, type_._name) + assert origin_type is not None + # PEP-604 syntax (Ex.: list | str) is represented with a types.UnionType object that does not have __getitem__. + # We also cannot use isinstance() since we have to compare types. + if sys.version_info >= (3, 10) and origin_type is types.UnionType: # noqa: E721 + return functools.reduce(operator.or_, resolved_type_args) + return origin_type[resolved_type_args] + + # We handle pydantic generic models separately as they don't have the same + # semantics as "typing" classes or generic aliases + if not origin_type and lenient_issubclass(type_, GenericModel) and not type_.__concrete__: + type_args = type_.__parameters__ + resolved_type_args = tuple(replace_types(t, type_map) for t in type_args) + if all_identical(type_args, resolved_type_args): + return type_ + return type_[resolved_type_args] + + # Handle special case for typehints that can have lists as arguments. + # `typing.Callable[[int, str], int]` is an example for this. + if isinstance(type_, (List, list)): + resolved_list = list(replace_types(element, type_map) for element in type_) + if all_identical(type_, resolved_list): + return type_ + return resolved_list + + # For JsonWrapperValue, need to handle its inner type to allow correct parsing + # of generic Json arguments like Json[T] + if not origin_type and lenient_issubclass(type_, JsonWrapper): + type_.inner_type = replace_types(type_.inner_type, type_map) + return type_ + + # If all else fails, we try to resolve the type directly and otherwise just + # return the input with no modifications. + new_type = type_map.get(type_, type_) + # Convert string to ForwardRef + if isinstance(new_type, str): + return ForwardRef(new_type) + else: + return new_type + + +def check_parameters_count(cls: Type[GenericModel], parameters: Tuple[Any, ...]) -> None: + actual = len(parameters) + expected = len(cls.__parameters__) + if actual != expected: + description = 'many' if actual > expected else 'few' + raise TypeError(f'Too {description} parameters for {cls.__name__}; actual {actual}, expected {expected}') + + +DictValues: Type[Any] = {}.values().__class__ + + +def iter_contained_typevars(v: Any) -> Iterator[TypeVarType]: + """Recursively iterate through all subtypes and type args of `v` and yield any typevars that are found.""" + if isinstance(v, TypeVar): + yield v + elif hasattr(v, '__parameters__') and not get_origin(v) and lenient_issubclass(v, GenericModel): + yield from v.__parameters__ + elif isinstance(v, (DictValues, list)): + for var in v: + yield from iter_contained_typevars(var) + else: + args = get_args(v) + for arg in args: + yield from iter_contained_typevars(arg) + + +def get_caller_frame_info() -> Tuple[Optional[str], bool]: + """ + Used inside a function to check whether it was called globally + + Will only work against non-compiled code, therefore used only in pydantic.generics + + :returns Tuple[module_name, called_globally] + """ + try: + previous_caller_frame = sys._getframe(2) + except ValueError as e: + raise RuntimeError('This function must be used inside another function') from e + except AttributeError: # sys module does not have _getframe function, so there's nothing we can do about it + return None, False + frame_globals = previous_caller_frame.f_globals + return frame_globals.get('__name__'), previous_caller_frame.f_locals is frame_globals + + +def _prepare_model_fields( + created_model: Type[GenericModel], + fields: Mapping[str, Any], + instance_type_hints: Mapping[str, type], + typevars_map: Mapping[Any, type], +) -> None: + """ + Replace DeferredType fields with concrete type hints and prepare them. + """ + + for key, field in created_model.__fields__.items(): + if key not in fields: + assert field.type_.__class__ is not DeferredType + # https://github.com/nedbat/coveragepy/issues/198 + continue # pragma: no cover + + assert field.type_.__class__ is DeferredType, field.type_.__class__ + + field_type_hint = instance_type_hints[key] + concrete_type = replace_types(field_type_hint, typevars_map) + field.type_ = concrete_type + field.outer_type_ = concrete_type + field.prepare() + created_model.__annotations__[key] = concrete_type diff --git a/source/target-vendors/cp312/pydantic/v1/json.py b/source/target-vendors/cp312/pydantic/v1/json.py new file mode 100644 index 0000000..41d0d5f --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/json.py @@ -0,0 +1,112 @@ +import datetime +from collections import deque +from decimal import Decimal +from enum import Enum +from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network +from pathlib import Path +from re import Pattern +from types import GeneratorType +from typing import Any, Callable, Dict, Type, Union +from uuid import UUID + +from pydantic.v1.color import Color +from pydantic.v1.networks import NameEmail +from pydantic.v1.types import SecretBytes, SecretStr + +__all__ = 'pydantic_encoder', 'custom_pydantic_encoder', 'timedelta_isoformat' + + +def isoformat(o: Union[datetime.date, datetime.time]) -> str: + return o.isoformat() + + +def decimal_encoder(dec_value: Decimal) -> Union[int, float]: + """ + Encodes a Decimal as int of there's no exponent, otherwise float + + This is useful when we use ConstrainedDecimal to represent Numeric(x,0) + where a integer (but not int typed) is used. Encoding this as a float + results in failed round-tripping between encode and parse. + Our Id type is a prime example of this. + + >>> decimal_encoder(Decimal("1.0")) + 1.0 + + >>> decimal_encoder(Decimal("1")) + 1 + """ + if dec_value.as_tuple().exponent >= 0: + return int(dec_value) + else: + return float(dec_value) + + +ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = { + bytes: lambda o: o.decode(), + Color: str, + datetime.date: isoformat, + datetime.datetime: isoformat, + datetime.time: isoformat, + datetime.timedelta: lambda td: td.total_seconds(), + Decimal: decimal_encoder, + Enum: lambda o: o.value, + frozenset: list, + deque: list, + GeneratorType: list, + IPv4Address: str, + IPv4Interface: str, + IPv4Network: str, + IPv6Address: str, + IPv6Interface: str, + IPv6Network: str, + NameEmail: str, + Path: str, + Pattern: lambda o: o.pattern, + SecretBytes: str, + SecretStr: str, + set: list, + UUID: str, +} + + +def pydantic_encoder(obj: Any) -> Any: + from dataclasses import asdict, is_dataclass + + from pydantic.v1.main import BaseModel + + if isinstance(obj, BaseModel): + return obj.dict() + elif is_dataclass(obj): + return asdict(obj) + + # Check the class type and its superclasses for a matching encoder + for base in obj.__class__.__mro__[:-1]: + try: + encoder = ENCODERS_BY_TYPE[base] + except KeyError: + continue + return encoder(obj) + else: # We have exited the for loop without finding a suitable encoder + raise TypeError(f"Object of type '{obj.__class__.__name__}' is not JSON serializable") + + +def custom_pydantic_encoder(type_encoders: Dict[Any, Callable[[Type[Any]], Any]], obj: Any) -> Any: + # Check the class type and its superclasses for a matching encoder + for base in obj.__class__.__mro__[:-1]: + try: + encoder = type_encoders[base] + except KeyError: + continue + + return encoder(obj) + else: # We have exited the for loop without finding a suitable encoder + return pydantic_encoder(obj) + + +def timedelta_isoformat(td: datetime.timedelta) -> str: + """ + ISO 8601 encoding for Python timedelta object. + """ + minutes, seconds = divmod(td.seconds, 60) + hours, minutes = divmod(minutes, 60) + return f'{"-" if td.days < 0 else ""}P{abs(td.days)}DT{hours:d}H{minutes:d}M{seconds:d}.{td.microseconds:06d}S' diff --git a/source/target-vendors/cp312/pydantic/v1/main.py b/source/target-vendors/cp312/pydantic/v1/main.py new file mode 100644 index 0000000..36e9c44 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/main.py @@ -0,0 +1,1130 @@ +import sys +import warnings +from abc import ABCMeta +from copy import deepcopy +from enum import Enum +from functools import partial +from pathlib import Path +from types import FunctionType, prepare_class, resolve_bases +from typing import ( + TYPE_CHECKING, + AbstractSet, + Any, + Callable, + ClassVar, + Dict, + List, + Mapping, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, + no_type_check, + overload, +) + +from typing_extensions import dataclass_transform + +from pydantic.v1.class_validators import ValidatorGroup, extract_root_validators, extract_validators, inherit_validators +from pydantic.v1.config import BaseConfig, Extra, inherit_config, prepare_config +from pydantic.v1.error_wrappers import ErrorWrapper, ValidationError +from pydantic.v1.errors import ConfigError, DictError, ExtraError, MissingError +from pydantic.v1.fields import ( + MAPPING_LIKE_SHAPES, + Field, + ModelField, + ModelPrivateAttr, + PrivateAttr, + Undefined, + is_finalvar_with_default_val, +) +from pydantic.v1.json import custom_pydantic_encoder, pydantic_encoder +from pydantic.v1.parse import Protocol, load_file, load_str_bytes +from pydantic.v1.schema import default_ref_template, model_schema +from pydantic.v1.types import PyObject, StrBytes +from pydantic.v1.typing import ( + AnyCallable, + get_args, + get_origin, + is_classvar, + is_namedtuple, + is_union, + resolve_annotations, + update_model_forward_refs, +) +from pydantic.v1.utils import ( + DUNDER_ATTRIBUTES, + ROOT_KEY, + ClassAttribute, + GetterDict, + Representation, + ValueItems, + generate_model_signature, + is_valid_field, + is_valid_private_name, + lenient_issubclass, + sequence_like, + smart_deepcopy, + unique_list, + validate_field_name, +) + +if TYPE_CHECKING: + from inspect import Signature + + from pydantic.v1.class_validators import ValidatorListDict + from pydantic.v1.types import ModelOrDc + from pydantic.v1.typing import ( + AbstractSetIntStr, + AnyClassMethod, + CallableGenerator, + DictAny, + DictStrAny, + MappingIntStrAny, + ReprArgs, + SetStr, + TupleGenerator, + ) + + Model = TypeVar('Model', bound='BaseModel') + +__all__ = 'BaseModel', 'create_model', 'validate_model' + +_T = TypeVar('_T') + + +def validate_custom_root_type(fields: Dict[str, ModelField]) -> None: + if len(fields) > 1: + raise ValueError(f'{ROOT_KEY} cannot be mixed with other fields') + + +def generate_hash_function(frozen: bool) -> Optional[Callable[[Any], int]]: + def hash_function(self_: Any) -> int: + return hash(self_.__class__) + hash(tuple(self_.__dict__.values())) + + return hash_function if frozen else None + + +# If a field is of type `Callable`, its default value should be a function and cannot to ignored. +ANNOTATED_FIELD_UNTOUCHED_TYPES: Tuple[Any, ...] = (property, type, classmethod, staticmethod) +# When creating a `BaseModel` instance, we bypass all the methods, properties... added to the model +UNTOUCHED_TYPES: Tuple[Any, ...] = (FunctionType,) + ANNOTATED_FIELD_UNTOUCHED_TYPES +# Note `ModelMetaclass` refers to `BaseModel`, but is also used to *create* `BaseModel`, so we need to add this extra +# (somewhat hacky) boolean to keep track of whether we've created the `BaseModel` class yet, and therefore whether it's +# safe to refer to it. If it *hasn't* been created, we assume that the `__new__` call we're in the middle of is for +# the `BaseModel` class, since that's defined immediately after the metaclass. +_is_base_model_class_defined = False + + +@dataclass_transform(kw_only_default=True, field_specifiers=(Field,)) +class ModelMetaclass(ABCMeta): + @no_type_check # noqa C901 + def __new__(mcs, name, bases, namespace, **kwargs): # noqa C901 + fields: Dict[str, ModelField] = {} + config = BaseConfig + validators: 'ValidatorListDict' = {} + + pre_root_validators, post_root_validators = [], [] + private_attributes: Dict[str, ModelPrivateAttr] = {} + base_private_attributes: Dict[str, ModelPrivateAttr] = {} + slots: SetStr = namespace.get('__slots__', ()) + slots = {slots} if isinstance(slots, str) else set(slots) + class_vars: SetStr = set() + hash_func: Optional[Callable[[Any], int]] = None + + for base in reversed(bases): + if _is_base_model_class_defined and issubclass(base, BaseModel) and base != BaseModel: + fields.update(smart_deepcopy(base.__fields__)) + config = inherit_config(base.__config__, config) + validators = inherit_validators(base.__validators__, validators) + pre_root_validators += base.__pre_root_validators__ + post_root_validators += base.__post_root_validators__ + base_private_attributes.update(base.__private_attributes__) + class_vars.update(base.__class_vars__) + hash_func = base.__hash__ + + resolve_forward_refs = kwargs.pop('__resolve_forward_refs__', True) + allowed_config_kwargs: SetStr = { + key + for key in dir(config) + if not (key.startswith('__') and key.endswith('__')) # skip dunder methods and attributes + } + config_kwargs = {key: kwargs.pop(key) for key in kwargs.keys() & allowed_config_kwargs} + config_from_namespace = namespace.get('Config') + if config_kwargs and config_from_namespace: + raise TypeError('Specifying config in two places is ambiguous, use either Config attribute or class kwargs') + config = inherit_config(config_from_namespace, config, **config_kwargs) + + validators = inherit_validators(extract_validators(namespace), validators) + vg = ValidatorGroup(validators) + + for f in fields.values(): + f.set_config(config) + extra_validators = vg.get_validators(f.name) + if extra_validators: + f.class_validators.update(extra_validators) + # re-run prepare to add extra validators + f.populate_validators() + + prepare_config(config, name) + + untouched_types = ANNOTATED_FIELD_UNTOUCHED_TYPES + + def is_untouched(v: Any) -> bool: + return isinstance(v, untouched_types) or v.__class__.__name__ == 'cython_function_or_method' + + if (namespace.get('__module__'), namespace.get('__qualname__')) != ('pydantic.main', 'BaseModel'): + if sys.version_info >= (3, 14): + if '__annotations__' in namespace: + # `from __future__ import annotations` was used in the model's module + raw_annotations = namespace['__annotations__'] + else: + # See https://docs.python.org/3/library/annotationlib.html#using-annotations-in-a-metaclass: + from annotationlib import Format, call_annotate_function, get_annotate_from_class_namespace + + annotate = get_annotate_from_class_namespace(namespace) + if annotate is not None: + raw_annotations = call_annotate_function(annotate, format=Format.FORWARDREF) + else: + raw_annotations = {} + else: + raw_annotations = namespace.get('__annotations__', {}) + + annotations = resolve_annotations(raw_annotations, namespace.get('__module__', None)) + # annotation only fields need to come first in fields + for ann_name, ann_type in annotations.items(): + if is_classvar(ann_type): + class_vars.add(ann_name) + elif is_finalvar_with_default_val(ann_type, namespace.get(ann_name, Undefined)): + class_vars.add(ann_name) + elif is_valid_field(ann_name): + validate_field_name(bases, ann_name) + value = namespace.get(ann_name, Undefined) + allowed_types = get_args(ann_type) if is_union(get_origin(ann_type)) else (ann_type,) + if ( + is_untouched(value) + and ann_type != PyObject + and not any( + lenient_issubclass(get_origin(allowed_type), Type) for allowed_type in allowed_types + ) + ): + continue + fields[ann_name] = ModelField.infer( + name=ann_name, + value=value, + annotation=ann_type, + class_validators=vg.get_validators(ann_name), + config=config, + ) + elif ann_name not in namespace and config.underscore_attrs_are_private: + private_attributes[ann_name] = PrivateAttr() + + untouched_types = UNTOUCHED_TYPES + config.keep_untouched + for var_name, value in namespace.items(): + can_be_changed = var_name not in class_vars and not is_untouched(value) + if isinstance(value, ModelPrivateAttr): + if not is_valid_private_name(var_name): + raise NameError( + f'Private attributes "{var_name}" must not be a valid field name; ' + f'Use sunder or dunder names, e. g. "_{var_name}" or "__{var_name}__"' + ) + private_attributes[var_name] = value + elif config.underscore_attrs_are_private and is_valid_private_name(var_name) and can_be_changed: + private_attributes[var_name] = PrivateAttr(default=value) + elif is_valid_field(var_name) and var_name not in annotations and can_be_changed: + validate_field_name(bases, var_name) + inferred = ModelField.infer( + name=var_name, + value=value, + annotation=annotations.get(var_name, Undefined), + class_validators=vg.get_validators(var_name), + config=config, + ) + if var_name in fields: + if lenient_issubclass(inferred.type_, fields[var_name].type_): + inferred.type_ = fields[var_name].type_ + else: + raise TypeError( + f'The type of {name}.{var_name} differs from the new default value; ' + f'if you wish to change the type of this field, please use a type annotation' + ) + fields[var_name] = inferred + + _custom_root_type = ROOT_KEY in fields + if _custom_root_type: + validate_custom_root_type(fields) + vg.check_for_unused() + if config.json_encoders: + json_encoder = partial(custom_pydantic_encoder, config.json_encoders) + else: + json_encoder = pydantic_encoder + pre_rv_new, post_rv_new = extract_root_validators(namespace) + + if hash_func is None: + hash_func = generate_hash_function(config.frozen) + + exclude_from_namespace = fields | private_attributes.keys() | {'__slots__'} + new_namespace = { + '__config__': config, + '__fields__': fields, + '__exclude_fields__': { + name: field.field_info.exclude for name, field in fields.items() if field.field_info.exclude is not None + } + or None, + '__include_fields__': { + name: field.field_info.include for name, field in fields.items() if field.field_info.include is not None + } + or None, + '__validators__': vg.validators, + '__pre_root_validators__': unique_list( + pre_root_validators + pre_rv_new, + name_factory=lambda v: v.__name__, + ), + '__post_root_validators__': unique_list( + post_root_validators + post_rv_new, + name_factory=lambda skip_on_failure_and_v: skip_on_failure_and_v[1].__name__, + ), + '__schema_cache__': {}, + '__json_encoder__': staticmethod(json_encoder), + '__custom_root_type__': _custom_root_type, + '__private_attributes__': {**base_private_attributes, **private_attributes}, + '__slots__': slots | private_attributes.keys(), + '__hash__': hash_func, + '__class_vars__': class_vars, + **{n: v for n, v in namespace.items() if n not in exclude_from_namespace}, + } + + cls = super().__new__(mcs, name, bases, new_namespace, **kwargs) + # set __signature__ attr only for model class, but not for its instances + cls.__signature__ = ClassAttribute('__signature__', generate_model_signature(cls.__init__, fields, config)) + + if not _is_base_model_class_defined: + # Cython does not understand the `if TYPE_CHECKING:` condition in the + # BaseModel's body (where annotations are set), so clear them manually: + getattr(cls, '__annotations__', {}).clear() + + if resolve_forward_refs: + cls.__try_update_forward_refs__() + + # preserve `__set_name__` protocol defined in https://peps.python.org/pep-0487 + # for attributes not in `new_namespace` (e.g. private attributes) + for name, obj in namespace.items(): + if name not in new_namespace: + set_name = getattr(obj, '__set_name__', None) + if callable(set_name): + set_name(cls, name) + + return cls + + def __instancecheck__(self, instance: Any) -> bool: + """ + Avoid calling ABC _abc_subclasscheck unless we're pretty sure. + + See #3829 and python/cpython#92810 + """ + return hasattr(instance, '__post_root_validators__') and super().__instancecheck__(instance) + + +object_setattr = object.__setattr__ + + +class BaseModel(Representation, metaclass=ModelMetaclass): + if TYPE_CHECKING: + # populated by the metaclass, defined here to help IDEs only + __fields__: ClassVar[Dict[str, ModelField]] = {} + __include_fields__: ClassVar[Optional[Mapping[str, Any]]] = None + __exclude_fields__: ClassVar[Optional[Mapping[str, Any]]] = None + __validators__: ClassVar[Dict[str, AnyCallable]] = {} + __pre_root_validators__: ClassVar[List[AnyCallable]] + __post_root_validators__: ClassVar[List[Tuple[bool, AnyCallable]]] + __config__: ClassVar[Type[BaseConfig]] = BaseConfig + __json_encoder__: ClassVar[Callable[[Any], Any]] = lambda x: x + __schema_cache__: ClassVar['DictAny'] = {} + __custom_root_type__: ClassVar[bool] = False + __signature__: ClassVar['Signature'] + __private_attributes__: ClassVar[Dict[str, ModelPrivateAttr]] + __class_vars__: ClassVar[SetStr] + __fields_set__: ClassVar[SetStr] = set() + + Config = BaseConfig + __slots__ = ('__dict__', '__fields_set__') + __doc__ = '' # Null out the Representation docstring + + def __init__(__pydantic_self__, **data: Any) -> None: + """ + Create a new model by parsing and validating input data from keyword arguments. + + Raises ValidationError if the input data cannot be parsed to form a valid model. + """ + # Uses something other than `self` the first arg to allow "self" as a settable attribute + values, fields_set, validation_error = validate_model(__pydantic_self__.__class__, data) + if validation_error: + raise validation_error + try: + object_setattr(__pydantic_self__, '__dict__', values) + except TypeError as e: + raise TypeError( + 'Model values must be a dict; you may not have returned a dictionary from a root validator' + ) from e + object_setattr(__pydantic_self__, '__fields_set__', fields_set) + __pydantic_self__._init_private_attributes() + + @no_type_check + def __setattr__(self, name, value): # noqa: C901 (ignore complexity) + if name in self.__private_attributes__ or name in DUNDER_ATTRIBUTES: + return object_setattr(self, name, value) + + if self.__config__.extra is not Extra.allow and name not in self.__fields__: + raise ValueError(f'"{self.__class__.__name__}" object has no field "{name}"') + elif not self.__config__.allow_mutation or self.__config__.frozen: + raise TypeError(f'"{self.__class__.__name__}" is immutable and does not support item assignment') + elif name in self.__fields__ and self.__fields__[name].final: + raise TypeError( + f'"{self.__class__.__name__}" object "{name}" field is final and does not support reassignment' + ) + elif self.__config__.validate_assignment: + new_values = {**self.__dict__, name: value} + + for validator in self.__pre_root_validators__: + try: + new_values = validator(self.__class__, new_values) + except (ValueError, TypeError, AssertionError) as exc: + raise ValidationError([ErrorWrapper(exc, loc=ROOT_KEY)], self.__class__) + + known_field = self.__fields__.get(name, None) + if known_field: + # We want to + # - make sure validators are called without the current value for this field inside `values` + # - keep other values (e.g. submodels) untouched (using `BaseModel.dict()` will change them into dicts) + # - keep the order of the fields + if not known_field.field_info.allow_mutation: + raise TypeError(f'"{known_field.name}" has allow_mutation set to False and cannot be assigned') + dict_without_original_value = {k: v for k, v in self.__dict__.items() if k != name} + value, error_ = known_field.validate(value, dict_without_original_value, loc=name, cls=self.__class__) + if error_: + raise ValidationError([error_], self.__class__) + else: + new_values[name] = value + + errors = [] + for skip_on_failure, validator in self.__post_root_validators__: + if skip_on_failure and errors: + continue + try: + new_values = validator(self.__class__, new_values) + except (ValueError, TypeError, AssertionError) as exc: + errors.append(ErrorWrapper(exc, loc=ROOT_KEY)) + if errors: + raise ValidationError(errors, self.__class__) + + # update the whole __dict__ as other values than just `value` + # may be changed (e.g. with `root_validator`) + object_setattr(self, '__dict__', new_values) + else: + self.__dict__[name] = value + + self.__fields_set__.add(name) + + def __getstate__(self) -> 'DictAny': + private_attrs = ((k, getattr(self, k, Undefined)) for k in self.__private_attributes__) + return { + '__dict__': self.__dict__, + '__fields_set__': self.__fields_set__, + '__private_attribute_values__': {k: v for k, v in private_attrs if v is not Undefined}, + } + + def __setstate__(self, state: 'DictAny') -> None: + object_setattr(self, '__dict__', state['__dict__']) + object_setattr(self, '__fields_set__', state['__fields_set__']) + for name, value in state.get('__private_attribute_values__', {}).items(): + object_setattr(self, name, value) + + def _init_private_attributes(self) -> None: + for name, private_attr in self.__private_attributes__.items(): + default = private_attr.get_default() + if default is not Undefined: + object_setattr(self, name, default) + + def dict( + self, + *, + include: Optional[Union['AbstractSetIntStr', 'MappingIntStrAny']] = None, + exclude: Optional[Union['AbstractSetIntStr', 'MappingIntStrAny']] = None, + by_alias: bool = False, + skip_defaults: Optional[bool] = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + ) -> 'DictStrAny': + """ + Generate a dictionary representation of the model, optionally specifying which fields to include or exclude. + + """ + if skip_defaults is not None: + warnings.warn( + f'{self.__class__.__name__}.dict(): "skip_defaults" is deprecated and replaced by "exclude_unset"', + DeprecationWarning, + ) + exclude_unset = skip_defaults + + return dict( + self._iter( + to_dict=True, + by_alias=by_alias, + include=include, + exclude=exclude, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + ) + + def json( + self, + *, + include: Optional[Union['AbstractSetIntStr', 'MappingIntStrAny']] = None, + exclude: Optional[Union['AbstractSetIntStr', 'MappingIntStrAny']] = None, + by_alias: bool = False, + skip_defaults: Optional[bool] = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + encoder: Optional[Callable[[Any], Any]] = None, + models_as_dict: bool = True, + **dumps_kwargs: Any, + ) -> str: + """ + Generate a JSON representation of the model, `include` and `exclude` arguments as per `dict()`. + + `encoder` is an optional function to supply as `default` to json.dumps(), other arguments as per `json.dumps()`. + """ + if skip_defaults is not None: + warnings.warn( + f'{self.__class__.__name__}.json(): "skip_defaults" is deprecated and replaced by "exclude_unset"', + DeprecationWarning, + ) + exclude_unset = skip_defaults + encoder = cast(Callable[[Any], Any], encoder or self.__json_encoder__) + + # We don't directly call `self.dict()`, which does exactly this with `to_dict=True` + # because we want to be able to keep raw `BaseModel` instances and not as `dict`. + # This allows users to write custom JSON encoders for given `BaseModel` classes. + data = dict( + self._iter( + to_dict=models_as_dict, + by_alias=by_alias, + include=include, + exclude=exclude, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + ) + if self.__custom_root_type__: + data = data[ROOT_KEY] + return self.__config__.json_dumps(data, default=encoder, **dumps_kwargs) + + @classmethod + def _enforce_dict_if_root(cls, obj: Any) -> Any: + if cls.__custom_root_type__ and ( + not (isinstance(obj, dict) and obj.keys() == {ROOT_KEY}) + and not (isinstance(obj, BaseModel) and obj.__fields__.keys() == {ROOT_KEY}) + or cls.__fields__[ROOT_KEY].shape in MAPPING_LIKE_SHAPES + ): + return {ROOT_KEY: obj} + else: + return obj + + @classmethod + def parse_obj(cls: Type['Model'], obj: Any) -> 'Model': + obj = cls._enforce_dict_if_root(obj) + if not isinstance(obj, dict): + try: + obj = dict(obj) + except (TypeError, ValueError) as e: + exc = TypeError(f'{cls.__name__} expected dict not {obj.__class__.__name__}') + raise ValidationError([ErrorWrapper(exc, loc=ROOT_KEY)], cls) from e + return cls(**obj) + + @classmethod + def parse_raw( + cls: Type['Model'], + b: StrBytes, + *, + content_type: str = None, + encoding: str = 'utf8', + proto: Protocol = None, + allow_pickle: bool = False, + ) -> 'Model': + try: + obj = load_str_bytes( + b, + proto=proto, + content_type=content_type, + encoding=encoding, + allow_pickle=allow_pickle, + json_loads=cls.__config__.json_loads, + ) + except (ValueError, TypeError, UnicodeDecodeError) as e: + raise ValidationError([ErrorWrapper(e, loc=ROOT_KEY)], cls) + return cls.parse_obj(obj) + + @classmethod + def parse_file( + cls: Type['Model'], + path: Union[str, Path], + *, + content_type: str = None, + encoding: str = 'utf8', + proto: Protocol = None, + allow_pickle: bool = False, + ) -> 'Model': + obj = load_file( + path, + proto=proto, + content_type=content_type, + encoding=encoding, + allow_pickle=allow_pickle, + json_loads=cls.__config__.json_loads, + ) + return cls.parse_obj(obj) + + @classmethod + def from_orm(cls: Type['Model'], obj: Any) -> 'Model': + if not cls.__config__.orm_mode: + raise ConfigError('You must have the config attribute orm_mode=True to use from_orm') + obj = {ROOT_KEY: obj} if cls.__custom_root_type__ else cls._decompose_class(obj) + m = cls.__new__(cls) + values, fields_set, validation_error = validate_model(cls, obj) + if validation_error: + raise validation_error + object_setattr(m, '__dict__', values) + object_setattr(m, '__fields_set__', fields_set) + m._init_private_attributes() + return m + + @classmethod + def construct(cls: Type['Model'], _fields_set: Optional['SetStr'] = None, **values: Any) -> 'Model': + """ + Creates a new model setting __dict__ and __fields_set__ from trusted or pre-validated data. + Default values are respected, but no other validation is performed. + Behaves as if `Config.extra = 'allow'` was set since it adds all passed values + """ + m = cls.__new__(cls) + fields_values: Dict[str, Any] = {} + for name, field in cls.__fields__.items(): + if field.alt_alias and field.alias in values: + fields_values[name] = values[field.alias] + elif name in values: + fields_values[name] = values[name] + elif not field.required: + fields_values[name] = field.get_default() + fields_values.update(values) + object_setattr(m, '__dict__', fields_values) + if _fields_set is None: + _fields_set = set(values.keys()) + object_setattr(m, '__fields_set__', _fields_set) + m._init_private_attributes() + return m + + def _copy_and_set_values(self: 'Model', values: 'DictStrAny', fields_set: 'SetStr', *, deep: bool) -> 'Model': + if deep: + # chances of having empty dict here are quite low for using smart_deepcopy + values = deepcopy(values) + + cls = self.__class__ + m = cls.__new__(cls) + object_setattr(m, '__dict__', values) + object_setattr(m, '__fields_set__', fields_set) + for name in self.__private_attributes__: + value = getattr(self, name, Undefined) + if value is not Undefined: + if deep: + value = deepcopy(value) + object_setattr(m, name, value) + + return m + + def copy( + self: 'Model', + *, + include: Optional[Union['AbstractSetIntStr', 'MappingIntStrAny']] = None, + exclude: Optional[Union['AbstractSetIntStr', 'MappingIntStrAny']] = None, + update: Optional['DictStrAny'] = None, + deep: bool = False, + ) -> 'Model': + """ + Duplicate a model, optionally choose which fields to include, exclude and change. + + :param include: fields to include in new model + :param exclude: fields to exclude from new model, as with values this takes precedence over include + :param update: values to change/add in the new model. Note: the data is not validated before creating + the new model: you should trust this data + :param deep: set to `True` to make a deep copy of the model + :return: new model instance + """ + + values = dict( + self._iter(to_dict=False, by_alias=False, include=include, exclude=exclude, exclude_unset=False), + **(update or {}), + ) + + # new `__fields_set__` can have unset optional fields with a set value in `update` kwarg + if update: + fields_set = self.__fields_set__ | update.keys() + else: + fields_set = set(self.__fields_set__) + + return self._copy_and_set_values(values, fields_set, deep=deep) + + @classmethod + def schema(cls, by_alias: bool = True, ref_template: str = default_ref_template) -> 'DictStrAny': + cached = cls.__schema_cache__.get((by_alias, ref_template)) + if cached is not None: + return cached + s = model_schema(cls, by_alias=by_alias, ref_template=ref_template) + cls.__schema_cache__[(by_alias, ref_template)] = s + return s + + @classmethod + def schema_json( + cls, *, by_alias: bool = True, ref_template: str = default_ref_template, **dumps_kwargs: Any + ) -> str: + from pydantic.v1.json import pydantic_encoder + + return cls.__config__.json_dumps( + cls.schema(by_alias=by_alias, ref_template=ref_template), default=pydantic_encoder, **dumps_kwargs + ) + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield cls.validate + + @classmethod + def validate(cls: Type['Model'], value: Any) -> 'Model': + if isinstance(value, cls): + copy_on_model_validation = cls.__config__.copy_on_model_validation + # whether to deep or shallow copy the model on validation, None means do not copy + deep_copy: Optional[bool] = None + if copy_on_model_validation not in {'deep', 'shallow', 'none'}: + # Warn about deprecated behavior + warnings.warn( + "`copy_on_model_validation` should be a string: 'deep', 'shallow' or 'none'", DeprecationWarning + ) + if copy_on_model_validation: + deep_copy = False + + if copy_on_model_validation == 'shallow': + # shallow copy + deep_copy = False + elif copy_on_model_validation == 'deep': + # deep copy + deep_copy = True + + if deep_copy is None: + return value + else: + return value._copy_and_set_values(value.__dict__, value.__fields_set__, deep=deep_copy) + + value = cls._enforce_dict_if_root(value) + + if isinstance(value, dict): + return cls(**value) + elif cls.__config__.orm_mode: + return cls.from_orm(value) + else: + try: + value_as_dict = dict(value) + except (TypeError, ValueError) as e: + raise DictError() from e + return cls(**value_as_dict) + + @classmethod + def _decompose_class(cls: Type['Model'], obj: Any) -> GetterDict: + if isinstance(obj, GetterDict): + return obj + return cls.__config__.getter_dict(obj) + + @classmethod + @no_type_check + def _get_value( + cls, + v: Any, + to_dict: bool, + by_alias: bool, + include: Optional[Union['AbstractSetIntStr', 'MappingIntStrAny']], + exclude: Optional[Union['AbstractSetIntStr', 'MappingIntStrAny']], + exclude_unset: bool, + exclude_defaults: bool, + exclude_none: bool, + ) -> Any: + if isinstance(v, BaseModel): + if to_dict: + v_dict = v.dict( + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + include=include, + exclude=exclude, + exclude_none=exclude_none, + ) + if ROOT_KEY in v_dict: + return v_dict[ROOT_KEY] + return v_dict + else: + return v.copy(include=include, exclude=exclude) + + value_exclude = ValueItems(v, exclude) if exclude else None + value_include = ValueItems(v, include) if include else None + + if isinstance(v, dict): + return { + k_: cls._get_value( + v_, + to_dict=to_dict, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + include=value_include and value_include.for_element(k_), + exclude=value_exclude and value_exclude.for_element(k_), + exclude_none=exclude_none, + ) + for k_, v_ in v.items() + if (not value_exclude or not value_exclude.is_excluded(k_)) + and (not value_include or value_include.is_included(k_)) + } + + elif sequence_like(v): + seq_args = ( + cls._get_value( + v_, + to_dict=to_dict, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + include=value_include and value_include.for_element(i), + exclude=value_exclude and value_exclude.for_element(i), + exclude_none=exclude_none, + ) + for i, v_ in enumerate(v) + if (not value_exclude or not value_exclude.is_excluded(i)) + and (not value_include or value_include.is_included(i)) + ) + + return v.__class__(*seq_args) if is_namedtuple(v.__class__) else v.__class__(seq_args) + + elif isinstance(v, Enum) and getattr(cls.Config, 'use_enum_values', False): + return v.value + + else: + return v + + @classmethod + def __try_update_forward_refs__(cls, **localns: Any) -> None: + """ + Same as update_forward_refs but will not raise exception + when forward references are not defined. + """ + update_model_forward_refs(cls, cls.__fields__.values(), cls.__config__.json_encoders, localns, (NameError,)) + + @classmethod + def update_forward_refs(cls, **localns: Any) -> None: + """ + Try to update ForwardRefs on fields based on this Model, globalns and localns. + """ + update_model_forward_refs(cls, cls.__fields__.values(), cls.__config__.json_encoders, localns) + + def __iter__(self) -> 'TupleGenerator': + """ + so `dict(model)` works + """ + yield from self.__dict__.items() + + def _iter( + self, + to_dict: bool = False, + by_alias: bool = False, + include: Optional[Union['AbstractSetIntStr', 'MappingIntStrAny']] = None, + exclude: Optional[Union['AbstractSetIntStr', 'MappingIntStrAny']] = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + ) -> 'TupleGenerator': + # Merge field set excludes with explicit exclude parameter with explicit overriding field set options. + # The extra "is not None" guards are not logically necessary but optimizes performance for the simple case. + if exclude is not None or self.__exclude_fields__ is not None: + exclude = ValueItems.merge(self.__exclude_fields__, exclude) + + if include is not None or self.__include_fields__ is not None: + include = ValueItems.merge(self.__include_fields__, include, intersect=True) + + allowed_keys = self._calculate_keys( + include=include, exclude=exclude, exclude_unset=exclude_unset # type: ignore + ) + if allowed_keys is None and not (to_dict or by_alias or exclude_unset or exclude_defaults or exclude_none): + # huge boost for plain _iter() + yield from self.__dict__.items() + return + + value_exclude = ValueItems(self, exclude) if exclude is not None else None + value_include = ValueItems(self, include) if include is not None else None + + for field_key, v in self.__dict__.items(): + if (allowed_keys is not None and field_key not in allowed_keys) or (exclude_none and v is None): + continue + + if exclude_defaults: + model_field = self.__fields__.get(field_key) + if not getattr(model_field, 'required', True) and getattr(model_field, 'default', _missing) == v: + continue + + if by_alias and field_key in self.__fields__: + dict_key = self.__fields__[field_key].alias + else: + dict_key = field_key + + if to_dict or value_include or value_exclude: + v = self._get_value( + v, + to_dict=to_dict, + by_alias=by_alias, + include=value_include and value_include.for_element(field_key), + exclude=value_exclude and value_exclude.for_element(field_key), + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + yield dict_key, v + + def _calculate_keys( + self, + include: Optional['MappingIntStrAny'], + exclude: Optional['MappingIntStrAny'], + exclude_unset: bool, + update: Optional['DictStrAny'] = None, + ) -> Optional[AbstractSet[str]]: + if include is None and exclude is None and exclude_unset is False: + return None + + keys: AbstractSet[str] + if exclude_unset: + keys = self.__fields_set__.copy() + else: + keys = self.__dict__.keys() + + if include is not None: + keys &= include.keys() + + if update: + keys -= update.keys() + + if exclude: + keys -= {k for k, v in exclude.items() if ValueItems.is_true(v)} + + return keys + + def __eq__(self, other: Any) -> bool: + if isinstance(other, BaseModel): + return self.dict() == other.dict() + else: + return self.dict() == other + + def __repr_args__(self) -> 'ReprArgs': + return [ + (k, v) + for k, v in self.__dict__.items() + if k not in DUNDER_ATTRIBUTES and (k not in self.__fields__ or self.__fields__[k].field_info.repr) + ] + + +_is_base_model_class_defined = True + + +@overload +def create_model( + __model_name: str, + *, + __config__: Optional[Type[BaseConfig]] = None, + __base__: None = None, + __module__: str = __name__, + __validators__: Dict[str, 'AnyClassMethod'] = None, + __cls_kwargs__: Dict[str, Any] = None, + **field_definitions: Any, +) -> Type['BaseModel']: + ... + + +@overload +def create_model( + __model_name: str, + *, + __config__: Optional[Type[BaseConfig]] = None, + __base__: Union[Type['Model'], Tuple[Type['Model'], ...]], + __module__: str = __name__, + __validators__: Dict[str, 'AnyClassMethod'] = None, + __cls_kwargs__: Dict[str, Any] = None, + **field_definitions: Any, +) -> Type['Model']: + ... + + +def create_model( + __model_name: str, + *, + __config__: Optional[Type[BaseConfig]] = None, + __base__: Union[None, Type['Model'], Tuple[Type['Model'], ...]] = None, + __module__: str = __name__, + __validators__: Dict[str, 'AnyClassMethod'] = None, + __cls_kwargs__: Dict[str, Any] = None, + __slots__: Optional[Tuple[str, ...]] = None, + **field_definitions: Any, +) -> Type['Model']: + """ + Dynamically create a model. + :param __model_name: name of the created model + :param __config__: config class to use for the new model + :param __base__: base class for the new model to inherit from + :param __module__: module of the created model + :param __validators__: a dict of method names and @validator class methods + :param __cls_kwargs__: a dict for class creation + :param __slots__: Deprecated, `__slots__` should not be passed to `create_model` + :param field_definitions: fields of the model (or extra fields if a base is supplied) + in the format `=(, )` or `=, e.g. + `foobar=(str, ...)` or `foobar=123`, or, for complex use-cases, in the format + `=` or `=(, )`, e.g. + `foo=Field(datetime, default_factory=datetime.utcnow, alias='bar')` or + `foo=(str, FieldInfo(title='Foo'))` + """ + if __slots__ is not None: + # __slots__ will be ignored from here on + warnings.warn('__slots__ should not be passed to create_model', RuntimeWarning) + + if __base__ is not None: + if __config__ is not None: + raise ConfigError('to avoid confusion __config__ and __base__ cannot be used together') + if not isinstance(__base__, tuple): + __base__ = (__base__,) + else: + __base__ = (cast(Type['Model'], BaseModel),) + + __cls_kwargs__ = __cls_kwargs__ or {} + + fields = {} + annotations = {} + + for f_name, f_def in field_definitions.items(): + if not is_valid_field(f_name): + warnings.warn(f'fields may not start with an underscore, ignoring "{f_name}"', RuntimeWarning) + if isinstance(f_def, tuple): + try: + f_annotation, f_value = f_def + except ValueError as e: + raise ConfigError( + 'field definitions should either be a tuple of (, ) or just a ' + 'default value, unfortunately this means tuples as ' + 'default values are not allowed' + ) from e + else: + f_annotation, f_value = None, f_def + + if f_annotation: + annotations[f_name] = f_annotation + fields[f_name] = f_value + + namespace: 'DictStrAny' = {'__annotations__': annotations, '__module__': __module__} + if __validators__: + namespace.update(__validators__) + namespace.update(fields) + if __config__: + namespace['Config'] = inherit_config(__config__, BaseConfig) + resolved_bases = resolve_bases(__base__) + meta, ns, kwds = prepare_class(__model_name, resolved_bases, kwds=__cls_kwargs__) + if resolved_bases is not __base__: + ns['__orig_bases__'] = __base__ + namespace.update(ns) + return meta(__model_name, resolved_bases, namespace, **kwds) + + +_missing = object() + + +def validate_model( # noqa: C901 (ignore complexity) + model: Type[BaseModel], input_data: 'DictStrAny', cls: 'ModelOrDc' = None +) -> Tuple['DictStrAny', 'SetStr', Optional[ValidationError]]: + """ + validate data against a model. + """ + values = {} + errors = [] + # input_data names, possibly alias + names_used = set() + # field names, never aliases + fields_set = set() + config = model.__config__ + check_extra = config.extra is not Extra.ignore + cls_ = cls or model + + for validator in model.__pre_root_validators__: + try: + input_data = validator(cls_, input_data) + except (ValueError, TypeError, AssertionError) as exc: + return {}, set(), ValidationError([ErrorWrapper(exc, loc=ROOT_KEY)], cls_) + + for name, field in model.__fields__.items(): + value = input_data.get(field.alias, _missing) + using_name = False + if value is _missing and config.allow_population_by_field_name and field.alt_alias: + value = input_data.get(field.name, _missing) + using_name = True + + if value is _missing: + if field.required: + errors.append(ErrorWrapper(MissingError(), loc=field.alias)) + continue + + value = field.get_default() + + if not config.validate_all and not field.validate_always: + values[name] = value + continue + else: + fields_set.add(name) + if check_extra: + names_used.add(field.name if using_name else field.alias) + + v_, errors_ = field.validate(value, values, loc=field.alias, cls=cls_) + if isinstance(errors_, ErrorWrapper): + errors.append(errors_) + elif isinstance(errors_, list): + errors.extend(errors_) + else: + values[name] = v_ + + if check_extra: + if isinstance(input_data, GetterDict): + extra = input_data.extra_keys() - names_used + else: + extra = input_data.keys() - names_used + if extra: + fields_set |= extra + if config.extra is Extra.allow: + for f in extra: + values[f] = input_data[f] + else: + for f in sorted(extra): + errors.append(ErrorWrapper(ExtraError(), loc=f)) + + for skip_on_failure, validator in model.__post_root_validators__: + if skip_on_failure and errors: + continue + try: + values = validator(cls_, values) + except (ValueError, TypeError, AssertionError) as exc: + errors.append(ErrorWrapper(exc, loc=ROOT_KEY)) + + if errors: + return values, fields_set, ValidationError(errors, cls_) + else: + return values, fields_set, None diff --git a/source/target-vendors/cp312/pydantic/v1/mypy.py b/source/target-vendors/cp312/pydantic/v1/mypy.py new file mode 100644 index 0000000..0a77569 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/mypy.py @@ -0,0 +1,949 @@ +import sys +from configparser import ConfigParser +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type as TypingType, Union + +from mypy.errorcodes import ErrorCode +from mypy.nodes import ( + ARG_NAMED, + ARG_NAMED_OPT, + ARG_OPT, + ARG_POS, + ARG_STAR2, + MDEF, + Argument, + AssignmentStmt, + Block, + CallExpr, + ClassDef, + Context, + Decorator, + EllipsisExpr, + FuncBase, + FuncDef, + JsonDict, + MemberExpr, + NameExpr, + PassStmt, + PlaceholderNode, + RefExpr, + StrExpr, + SymbolNode, + SymbolTableNode, + TempNode, + TypeInfo, + TypeVarExpr, + Var, +) +from mypy.options import Options +from mypy.plugin import ( + CheckerPluginInterface, + ClassDefContext, + FunctionContext, + MethodContext, + Plugin, + ReportConfigContext, + SemanticAnalyzerPluginInterface, +) +from mypy.plugins import dataclasses +from mypy.semanal import set_callable_name # type: ignore +from mypy.server.trigger import make_wildcard_trigger +from mypy.types import ( + AnyType, + CallableType, + Instance, + NoneType, + Overloaded, + ProperType, + Type, + TypeOfAny, + TypeType, + TypeVarId, + TypeVarType, + UnionType, + get_proper_type, +) +from mypy.typevars import fill_typevars +from mypy.util import get_unique_redefinition_name +from mypy.version import __version__ as mypy_version + +from pydantic.v1.utils import is_valid_field + +try: + from mypy.types import TypeVarDef # type: ignore[attr-defined] +except ImportError: # pragma: no cover + # Backward-compatible with TypeVarDef from Mypy 0.910. + from mypy.types import TypeVarType as TypeVarDef + +CONFIGFILE_KEY = 'pydantic-mypy' +METADATA_KEY = 'pydantic-mypy-metadata' +_NAMESPACE = __name__[:-5] # 'pydantic' in 1.10.X, 'pydantic.v1' in v2.X +BASEMODEL_FULLNAME = f'{_NAMESPACE}.main.BaseModel' +BASESETTINGS_FULLNAME = f'{_NAMESPACE}.env_settings.BaseSettings' +MODEL_METACLASS_FULLNAME = f'{_NAMESPACE}.main.ModelMetaclass' +FIELD_FULLNAME = f'{_NAMESPACE}.fields.Field' +DATACLASS_FULLNAME = f'{_NAMESPACE}.dataclasses.dataclass' + + +def parse_mypy_version(version: str) -> Tuple[int, ...]: + return tuple(map(int, version.partition('+')[0].split('.'))) + + +MYPY_VERSION_TUPLE = parse_mypy_version(mypy_version) +BUILTINS_NAME = 'builtins' if MYPY_VERSION_TUPLE >= (0, 930) else '__builtins__' + +# Increment version if plugin changes and mypy caches should be invalidated +__version__ = 2 + + +def plugin(version: str) -> 'TypingType[Plugin]': + """ + `version` is the mypy version string + + We might want to use this to print a warning if the mypy version being used is + newer, or especially older, than we expect (or need). + """ + return PydanticPlugin + + +class PydanticPlugin(Plugin): + def __init__(self, options: Options) -> None: + self.plugin_config = PydanticPluginConfig(options) + self._plugin_data = self.plugin_config.to_data() + super().__init__(options) + + def get_base_class_hook(self, fullname: str) -> 'Optional[Callable[[ClassDefContext], None]]': + sym = self.lookup_fully_qualified(fullname) + if sym and isinstance(sym.node, TypeInfo): # pragma: no branch + # No branching may occur if the mypy cache has not been cleared + if any(get_fullname(base) == BASEMODEL_FULLNAME for base in sym.node.mro): + return self._pydantic_model_class_maker_callback + return None + + def get_metaclass_hook(self, fullname: str) -> Optional[Callable[[ClassDefContext], None]]: + if fullname == MODEL_METACLASS_FULLNAME: + return self._pydantic_model_metaclass_marker_callback + return None + + def get_function_hook(self, fullname: str) -> 'Optional[Callable[[FunctionContext], Type]]': + sym = self.lookup_fully_qualified(fullname) + if sym and sym.fullname == FIELD_FULLNAME: + return self._pydantic_field_callback + return None + + def get_method_hook(self, fullname: str) -> Optional[Callable[[MethodContext], Type]]: + if fullname.endswith('.from_orm'): + return from_orm_callback + return None + + def get_class_decorator_hook(self, fullname: str) -> Optional[Callable[[ClassDefContext], None]]: + """Mark pydantic.dataclasses as dataclass. + + Mypy version 1.1.1 added support for `@dataclass_transform` decorator. + """ + if fullname == DATACLASS_FULLNAME and MYPY_VERSION_TUPLE < (1, 1): + return dataclasses.dataclass_class_maker_callback # type: ignore[return-value] + return None + + def report_config_data(self, ctx: ReportConfigContext) -> Dict[str, Any]: + """Return all plugin config data. + + Used by mypy to determine if cache needs to be discarded. + """ + return self._plugin_data + + def _pydantic_model_class_maker_callback(self, ctx: ClassDefContext) -> None: + transformer = PydanticModelTransformer(ctx, self.plugin_config) + transformer.transform() + + def _pydantic_model_metaclass_marker_callback(self, ctx: ClassDefContext) -> None: + """Reset dataclass_transform_spec attribute of ModelMetaclass. + + Let the plugin handle it. This behavior can be disabled + if 'debug_dataclass_transform' is set to True', for testing purposes. + """ + if self.plugin_config.debug_dataclass_transform: + return + info_metaclass = ctx.cls.info.declared_metaclass + assert info_metaclass, "callback not passed from 'get_metaclass_hook'" + if getattr(info_metaclass.type, 'dataclass_transform_spec', None): + info_metaclass.type.dataclass_transform_spec = None # type: ignore[attr-defined] + + def _pydantic_field_callback(self, ctx: FunctionContext) -> 'Type': + """ + Extract the type of the `default` argument from the Field function, and use it as the return type. + + In particular: + * Check whether the default and default_factory argument is specified. + * Output an error if both are specified. + * Retrieve the type of the argument which is specified, and use it as return type for the function. + """ + default_any_type = ctx.default_return_type + + assert ctx.callee_arg_names[0] == 'default', '"default" is no longer first argument in Field()' + assert ctx.callee_arg_names[1] == 'default_factory', '"default_factory" is no longer second argument in Field()' + default_args = ctx.args[0] + default_factory_args = ctx.args[1] + + if default_args and default_factory_args: + error_default_and_default_factory_specified(ctx.api, ctx.context) + return default_any_type + + if default_args: + default_type = ctx.arg_types[0][0] + default_arg = default_args[0] + + # Fallback to default Any type if the field is required + if not isinstance(default_arg, EllipsisExpr): + return default_type + + elif default_factory_args: + default_factory_type = ctx.arg_types[1][0] + + # Functions which use `ParamSpec` can be overloaded, exposing the callable's types as a parameter + # Pydantic calls the default factory without any argument, so we retrieve the first item + if isinstance(default_factory_type, Overloaded): + if MYPY_VERSION_TUPLE > (0, 910): + default_factory_type = default_factory_type.items[0] + else: + # Mypy0.910 exposes the items of overloaded types in a function + default_factory_type = default_factory_type.items()[0] # type: ignore[operator] + + if isinstance(default_factory_type, CallableType): + ret_type = get_proper_type(default_factory_type.ret_type) + if ( + isinstance(ret_type, Instance) + and ret_type.args + and all(isinstance(arg, TypeVarType) for arg in ret_type.args) + ): + # Looks like the default factory is a type like `list` or `dict`, replace all args with `Any` + ret_type = ret_type.copy_modified(args=[default_any_type] * len(ret_type.args)) + return ret_type + + return default_any_type + + +class PydanticPluginConfig: + __slots__ = ( + 'init_forbid_extra', + 'init_typed', + 'warn_required_dynamic_aliases', + 'warn_untyped_fields', + 'debug_dataclass_transform', + ) + init_forbid_extra: bool + init_typed: bool + warn_required_dynamic_aliases: bool + warn_untyped_fields: bool + debug_dataclass_transform: bool # undocumented + + def __init__(self, options: Options) -> None: + if options.config_file is None: # pragma: no cover + return + + toml_config = parse_toml(options.config_file) + if toml_config is not None: + config = toml_config.get('tool', {}).get('pydantic-mypy', {}) + for key in self.__slots__: + setting = config.get(key, False) + if not isinstance(setting, bool): + raise ValueError(f'Configuration value must be a boolean for key: {key}') + setattr(self, key, setting) + else: + plugin_config = ConfigParser() + plugin_config.read(options.config_file) + for key in self.__slots__: + setting = plugin_config.getboolean(CONFIGFILE_KEY, key, fallback=False) + setattr(self, key, setting) + + def to_data(self) -> Dict[str, Any]: + return {key: getattr(self, key) for key in self.__slots__} + + +def from_orm_callback(ctx: MethodContext) -> Type: + """ + Raise an error if orm_mode is not enabled + """ + model_type: Instance + ctx_type = ctx.type + if isinstance(ctx_type, TypeType): + ctx_type = ctx_type.item + if isinstance(ctx_type, CallableType) and isinstance(ctx_type.ret_type, Instance): + model_type = ctx_type.ret_type # called on the class + elif isinstance(ctx_type, Instance): + model_type = ctx_type # called on an instance (unusual, but still valid) + else: # pragma: no cover + detail = f'ctx.type: {ctx_type} (of type {ctx_type.__class__.__name__})' + error_unexpected_behavior(detail, ctx.api, ctx.context) + return ctx.default_return_type + pydantic_metadata = model_type.type.metadata.get(METADATA_KEY) + if pydantic_metadata is None: + return ctx.default_return_type + orm_mode = pydantic_metadata.get('config', {}).get('orm_mode') + if orm_mode is not True: + error_from_orm(get_name(model_type.type), ctx.api, ctx.context) + return ctx.default_return_type + + +class PydanticModelTransformer: + tracked_config_fields: Set[str] = { + 'extra', + 'allow_mutation', + 'frozen', + 'orm_mode', + 'allow_population_by_field_name', + 'alias_generator', + } + + def __init__(self, ctx: ClassDefContext, plugin_config: PydanticPluginConfig) -> None: + self._ctx = ctx + self.plugin_config = plugin_config + + def transform(self) -> None: + """ + Configures the BaseModel subclass according to the plugin settings. + + In particular: + * determines the model config and fields, + * adds a fields-aware signature for the initializer and construct methods + * freezes the class if allow_mutation = False or frozen = True + * stores the fields, config, and if the class is settings in the mypy metadata for access by subclasses + """ + ctx = self._ctx + info = ctx.cls.info + + self.adjust_validator_signatures() + config = self.collect_config() + fields = self.collect_fields(config) + is_settings = any(get_fullname(base) == BASESETTINGS_FULLNAME for base in info.mro[:-1]) + self.add_initializer(fields, config, is_settings) + self.add_construct_method(fields) + self.set_frozen(fields, frozen=config.allow_mutation is False or config.frozen is True) + info.metadata[METADATA_KEY] = { + 'fields': {field.name: field.serialize() for field in fields}, + 'config': config.set_values_dict(), + } + + def adjust_validator_signatures(self) -> None: + """When we decorate a function `f` with `pydantic.validator(...), mypy sees + `f` as a regular method taking a `self` instance, even though pydantic + internally wraps `f` with `classmethod` if necessary. + + Teach mypy this by marking any function whose outermost decorator is a + `validator()` call as a classmethod. + """ + for name, sym in self._ctx.cls.info.names.items(): + if isinstance(sym.node, Decorator): + first_dec = sym.node.original_decorators[0] + if ( + isinstance(first_dec, CallExpr) + and isinstance(first_dec.callee, NameExpr) + and first_dec.callee.fullname == f'{_NAMESPACE}.class_validators.validator' + ): + sym.node.func.is_class = True + + def collect_config(self) -> 'ModelConfigData': + """ + Collects the values of the config attributes that are used by the plugin, accounting for parent classes. + """ + ctx = self._ctx + cls = ctx.cls + config = ModelConfigData() + for stmt in cls.defs.body: + if not isinstance(stmt, ClassDef): + continue + if stmt.name == 'Config': + for substmt in stmt.defs.body: + if not isinstance(substmt, AssignmentStmt): + continue + config.update(self.get_config_update(substmt)) + if ( + config.has_alias_generator + and not config.allow_population_by_field_name + and self.plugin_config.warn_required_dynamic_aliases + ): + error_required_dynamic_aliases(ctx.api, stmt) + for info in cls.info.mro[1:]: # 0 is the current class + if METADATA_KEY not in info.metadata: + continue + + # Each class depends on the set of fields in its ancestors + ctx.api.add_plugin_dependency(make_wildcard_trigger(get_fullname(info))) + for name, value in info.metadata[METADATA_KEY]['config'].items(): + config.setdefault(name, value) + return config + + def collect_fields(self, model_config: 'ModelConfigData') -> List['PydanticModelField']: + """ + Collects the fields for the model, accounting for parent classes + """ + # First, collect fields belonging to the current class. + ctx = self._ctx + cls = self._ctx.cls + fields = [] # type: List[PydanticModelField] + known_fields = set() # type: Set[str] + for stmt in cls.defs.body: + if not isinstance(stmt, AssignmentStmt): # `and stmt.new_syntax` to require annotation + continue + + lhs = stmt.lvalues[0] + if not isinstance(lhs, NameExpr) or not is_valid_field(lhs.name): + continue + + if not stmt.new_syntax and self.plugin_config.warn_untyped_fields: + error_untyped_fields(ctx.api, stmt) + + # if lhs.name == '__config__': # BaseConfig not well handled; I'm not sure why yet + # continue + + sym = cls.info.names.get(lhs.name) + if sym is None: # pragma: no cover + # This is likely due to a star import (see the dataclasses plugin for a more detailed explanation) + # This is the same logic used in the dataclasses plugin + continue + + node = sym.node + if isinstance(node, PlaceholderNode): # pragma: no cover + # See the PlaceholderNode docstring for more detail about how this can occur + # Basically, it is an edge case when dealing with complex import logic + # This is the same logic used in the dataclasses plugin + continue + if not isinstance(node, Var): # pragma: no cover + # Don't know if this edge case still happens with the `is_valid_field` check above + # but better safe than sorry + continue + + # x: ClassVar[int] is ignored by dataclasses. + if node.is_classvar: + continue + + is_required = self.get_is_required(cls, stmt, lhs) + alias, has_dynamic_alias = self.get_alias_info(stmt) + if ( + has_dynamic_alias + and not model_config.allow_population_by_field_name + and self.plugin_config.warn_required_dynamic_aliases + ): + error_required_dynamic_aliases(ctx.api, stmt) + fields.append( + PydanticModelField( + name=lhs.name, + is_required=is_required, + alias=alias, + has_dynamic_alias=has_dynamic_alias, + line=stmt.line, + column=stmt.column, + ) + ) + known_fields.add(lhs.name) + all_fields = fields.copy() + for info in cls.info.mro[1:]: # 0 is the current class, -2 is BaseModel, -1 is object + if METADATA_KEY not in info.metadata: + continue + + superclass_fields = [] + # Each class depends on the set of fields in its ancestors + ctx.api.add_plugin_dependency(make_wildcard_trigger(get_fullname(info))) + + for name, data in info.metadata[METADATA_KEY]['fields'].items(): + if name not in known_fields: + field = PydanticModelField.deserialize(info, data) + known_fields.add(name) + superclass_fields.append(field) + else: + (field,) = (a for a in all_fields if a.name == name) + all_fields.remove(field) + superclass_fields.append(field) + all_fields = superclass_fields + all_fields + return all_fields + + def add_initializer(self, fields: List['PydanticModelField'], config: 'ModelConfigData', is_settings: bool) -> None: + """ + Adds a fields-aware `__init__` method to the class. + + The added `__init__` will be annotated with types vs. all `Any` depending on the plugin settings. + """ + ctx = self._ctx + typed = self.plugin_config.init_typed + use_alias = config.allow_population_by_field_name is not True + force_all_optional = is_settings or bool( + config.has_alias_generator and not config.allow_population_by_field_name + ) + init_arguments = self.get_field_arguments( + fields, typed=typed, force_all_optional=force_all_optional, use_alias=use_alias + ) + if not self.should_init_forbid_extra(fields, config): + var = Var('kwargs') + init_arguments.append(Argument(var, AnyType(TypeOfAny.explicit), None, ARG_STAR2)) + + if '__init__' not in ctx.cls.info.names: + add_method(ctx, '__init__', init_arguments, NoneType()) + + def add_construct_method(self, fields: List['PydanticModelField']) -> None: + """ + Adds a fully typed `construct` classmethod to the class. + + Similar to the fields-aware __init__ method, but always uses the field names (not aliases), + and does not treat settings fields as optional. + """ + ctx = self._ctx + set_str = ctx.api.named_type(f'{BUILTINS_NAME}.set', [ctx.api.named_type(f'{BUILTINS_NAME}.str')]) + optional_set_str = UnionType([set_str, NoneType()]) + fields_set_argument = Argument(Var('_fields_set', optional_set_str), optional_set_str, None, ARG_OPT) + construct_arguments = self.get_field_arguments(fields, typed=True, force_all_optional=False, use_alias=False) + construct_arguments = [fields_set_argument] + construct_arguments + + obj_type = ctx.api.named_type(f'{BUILTINS_NAME}.object') + self_tvar_name = '_PydanticBaseModel' # Make sure it does not conflict with other names in the class + tvar_fullname = ctx.cls.fullname + '.' + self_tvar_name + if MYPY_VERSION_TUPLE >= (1, 4): + tvd = TypeVarType( + self_tvar_name, + tvar_fullname, + ( + TypeVarId(-1, namespace=ctx.cls.fullname + '.construct') + if MYPY_VERSION_TUPLE >= (1, 11) + else TypeVarId(-1) + ), + [], + obj_type, + AnyType(TypeOfAny.from_omitted_generics), # type: ignore[arg-type] + ) + self_tvar_expr = TypeVarExpr( + self_tvar_name, + tvar_fullname, + [], + obj_type, + AnyType(TypeOfAny.from_omitted_generics), # type: ignore[arg-type] + ) + else: + tvd = TypeVarDef(self_tvar_name, tvar_fullname, -1, [], obj_type) + self_tvar_expr = TypeVarExpr(self_tvar_name, tvar_fullname, [], obj_type) + ctx.cls.info.names[self_tvar_name] = SymbolTableNode(MDEF, self_tvar_expr) + + # Backward-compatible with TypeVarDef from Mypy 0.910. + if isinstance(tvd, TypeVarType): + self_type = tvd + else: + self_type = TypeVarType(tvd) + + add_method( + ctx, + 'construct', + construct_arguments, + return_type=self_type, + self_type=self_type, + tvar_def=tvd, + is_classmethod=True, + ) + + def set_frozen(self, fields: List['PydanticModelField'], frozen: bool) -> None: + """ + Marks all fields as properties so that attempts to set them trigger mypy errors. + + This is the same approach used by the attrs and dataclasses plugins. + """ + ctx = self._ctx + info = ctx.cls.info + for field in fields: + sym_node = info.names.get(field.name) + if sym_node is not None: + var = sym_node.node + if isinstance(var, Var): + var.is_property = frozen + elif isinstance(var, PlaceholderNode) and not ctx.api.final_iteration: + # See https://github.com/pydantic/pydantic/issues/5191 to hit this branch for test coverage + ctx.api.defer() + else: # pragma: no cover + # I don't know whether it's possible to hit this branch, but I've added it for safety + try: + var_str = str(var) + except TypeError: + # This happens for PlaceholderNode; perhaps it will happen for other types in the future.. + var_str = repr(var) + detail = f'sym_node.node: {var_str} (of type {var.__class__})' + error_unexpected_behavior(detail, ctx.api, ctx.cls) + else: + var = field.to_var(info, use_alias=False) + var.info = info + var.is_property = frozen + var._fullname = get_fullname(info) + '.' + get_name(var) + info.names[get_name(var)] = SymbolTableNode(MDEF, var) + + def get_config_update(self, substmt: AssignmentStmt) -> Optional['ModelConfigData']: + """ + Determines the config update due to a single statement in the Config class definition. + + Warns if a tracked config attribute is set to a value the plugin doesn't know how to interpret (e.g., an int) + """ + lhs = substmt.lvalues[0] + if not (isinstance(lhs, NameExpr) and lhs.name in self.tracked_config_fields): + return None + if lhs.name == 'extra': + if isinstance(substmt.rvalue, StrExpr): + forbid_extra = substmt.rvalue.value == 'forbid' + elif isinstance(substmt.rvalue, MemberExpr): + forbid_extra = substmt.rvalue.name == 'forbid' + else: + error_invalid_config_value(lhs.name, self._ctx.api, substmt) + return None + return ModelConfigData(forbid_extra=forbid_extra) + if lhs.name == 'alias_generator': + has_alias_generator = True + if isinstance(substmt.rvalue, NameExpr) and substmt.rvalue.fullname == 'builtins.None': + has_alias_generator = False + return ModelConfigData(has_alias_generator=has_alias_generator) + if isinstance(substmt.rvalue, NameExpr) and substmt.rvalue.fullname in ('builtins.True', 'builtins.False'): + return ModelConfigData(**{lhs.name: substmt.rvalue.fullname == 'builtins.True'}) + error_invalid_config_value(lhs.name, self._ctx.api, substmt) + return None + + @staticmethod + def get_is_required(cls: ClassDef, stmt: AssignmentStmt, lhs: NameExpr) -> bool: + """ + Returns a boolean indicating whether the field defined in `stmt` is a required field. + """ + expr = stmt.rvalue + if isinstance(expr, TempNode): + # TempNode means annotation-only, so only non-required if Optional + value_type = get_proper_type(cls.info[lhs.name].type) + return not PydanticModelTransformer.type_has_implicit_default(value_type) + if isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr) and expr.callee.fullname == FIELD_FULLNAME: + # The "default value" is a call to `Field`; at this point, the field is + # only required if default is Ellipsis (i.e., `field_name: Annotation = Field(...)`) or if default_factory + # is specified. + for arg, name in zip(expr.args, expr.arg_names): + # If name is None, then this arg is the default because it is the only positional argument. + if name is None or name == 'default': + return arg.__class__ is EllipsisExpr + if name == 'default_factory': + return False + # In this case, default and default_factory are not specified, so we need to look at the annotation + value_type = get_proper_type(cls.info[lhs.name].type) + return not PydanticModelTransformer.type_has_implicit_default(value_type) + # Only required if the "default value" is Ellipsis (i.e., `field_name: Annotation = ...`) + return isinstance(expr, EllipsisExpr) + + @staticmethod + def type_has_implicit_default(type_: Optional[ProperType]) -> bool: + """ + Returns True if the passed type will be given an implicit default value. + + In pydantic v1, this is the case for Optional types and Any (with default value None). + """ + if isinstance(type_, AnyType): + # Annotated as Any + return True + if isinstance(type_, UnionType) and any( + isinstance(item, NoneType) or isinstance(item, AnyType) for item in type_.items + ): + # Annotated as Optional, or otherwise having NoneType or AnyType in the union + return True + return False + + @staticmethod + def get_alias_info(stmt: AssignmentStmt) -> Tuple[Optional[str], bool]: + """ + Returns a pair (alias, has_dynamic_alias), extracted from the declaration of the field defined in `stmt`. + + `has_dynamic_alias` is True if and only if an alias is provided, but not as a string literal. + If `has_dynamic_alias` is True, `alias` will be None. + """ + expr = stmt.rvalue + if isinstance(expr, TempNode): + # TempNode means annotation-only + return None, False + + if not ( + isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr) and expr.callee.fullname == FIELD_FULLNAME + ): + # Assigned value is not a call to pydantic.fields.Field + return None, False + + for i, arg_name in enumerate(expr.arg_names): + if arg_name != 'alias': + continue + arg = expr.args[i] + if isinstance(arg, StrExpr): + return arg.value, False + else: + return None, True + return None, False + + def get_field_arguments( + self, fields: List['PydanticModelField'], typed: bool, force_all_optional: bool, use_alias: bool + ) -> List[Argument]: + """ + Helper function used during the construction of the `__init__` and `construct` method signatures. + + Returns a list of mypy Argument instances for use in the generated signatures. + """ + info = self._ctx.cls.info + arguments = [ + field.to_argument(info, typed=typed, force_optional=force_all_optional, use_alias=use_alias) + for field in fields + if not (use_alias and field.has_dynamic_alias) + ] + return arguments + + def should_init_forbid_extra(self, fields: List['PydanticModelField'], config: 'ModelConfigData') -> bool: + """ + Indicates whether the generated `__init__` should get a `**kwargs` at the end of its signature + + We disallow arbitrary kwargs if the extra config setting is "forbid", or if the plugin config says to, + *unless* a required dynamic alias is present (since then we can't determine a valid signature). + """ + if not config.allow_population_by_field_name: + if self.is_dynamic_alias_present(fields, bool(config.has_alias_generator)): + return False + if config.forbid_extra: + return True + return self.plugin_config.init_forbid_extra + + @staticmethod + def is_dynamic_alias_present(fields: List['PydanticModelField'], has_alias_generator: bool) -> bool: + """ + Returns whether any fields on the model have a "dynamic alias", i.e., an alias that cannot be + determined during static analysis. + """ + for field in fields: + if field.has_dynamic_alias: + return True + if has_alias_generator: + for field in fields: + if field.alias is None: + return True + return False + + +class PydanticModelField: + def __init__( + self, name: str, is_required: bool, alias: Optional[str], has_dynamic_alias: bool, line: int, column: int + ): + self.name = name + self.is_required = is_required + self.alias = alias + self.has_dynamic_alias = has_dynamic_alias + self.line = line + self.column = column + + def to_var(self, info: TypeInfo, use_alias: bool) -> Var: + name = self.name + if use_alias and self.alias is not None: + name = self.alias + return Var(name, info[self.name].type) + + def to_argument(self, info: TypeInfo, typed: bool, force_optional: bool, use_alias: bool) -> Argument: + if typed and info[self.name].type is not None: + type_annotation = info[self.name].type + else: + type_annotation = AnyType(TypeOfAny.explicit) + return Argument( + variable=self.to_var(info, use_alias), + type_annotation=type_annotation, + initializer=None, + kind=ARG_NAMED_OPT if force_optional or not self.is_required else ARG_NAMED, + ) + + def serialize(self) -> JsonDict: + return self.__dict__ + + @classmethod + def deserialize(cls, info: TypeInfo, data: JsonDict) -> 'PydanticModelField': + return cls(**data) + + +class ModelConfigData: + def __init__( + self, + forbid_extra: Optional[bool] = None, + allow_mutation: Optional[bool] = None, + frozen: Optional[bool] = None, + orm_mode: Optional[bool] = None, + allow_population_by_field_name: Optional[bool] = None, + has_alias_generator: Optional[bool] = None, + ): + self.forbid_extra = forbid_extra + self.allow_mutation = allow_mutation + self.frozen = frozen + self.orm_mode = orm_mode + self.allow_population_by_field_name = allow_population_by_field_name + self.has_alias_generator = has_alias_generator + + def set_values_dict(self) -> Dict[str, Any]: + return {k: v for k, v in self.__dict__.items() if v is not None} + + def update(self, config: Optional['ModelConfigData']) -> None: + if config is None: + return + for k, v in config.set_values_dict().items(): + setattr(self, k, v) + + def setdefault(self, key: str, value: Any) -> None: + if getattr(self, key) is None: + setattr(self, key, value) + + +ERROR_ORM = ErrorCode('pydantic-orm', 'Invalid from_orm call', 'Pydantic') +ERROR_CONFIG = ErrorCode('pydantic-config', 'Invalid config value', 'Pydantic') +ERROR_ALIAS = ErrorCode('pydantic-alias', 'Dynamic alias disallowed', 'Pydantic') +ERROR_UNEXPECTED = ErrorCode('pydantic-unexpected', 'Unexpected behavior', 'Pydantic') +ERROR_UNTYPED = ErrorCode('pydantic-field', 'Untyped field disallowed', 'Pydantic') +ERROR_FIELD_DEFAULTS = ErrorCode('pydantic-field', 'Invalid Field defaults', 'Pydantic') + + +def error_from_orm(model_name: str, api: CheckerPluginInterface, context: Context) -> None: + api.fail(f'"{model_name}" does not have orm_mode=True', context, code=ERROR_ORM) + + +def error_invalid_config_value(name: str, api: SemanticAnalyzerPluginInterface, context: Context) -> None: + api.fail(f'Invalid value for "Config.{name}"', context, code=ERROR_CONFIG) + + +def error_required_dynamic_aliases(api: SemanticAnalyzerPluginInterface, context: Context) -> None: + api.fail('Required dynamic aliases disallowed', context, code=ERROR_ALIAS) + + +def error_unexpected_behavior( + detail: str, api: Union[CheckerPluginInterface, SemanticAnalyzerPluginInterface], context: Context +) -> None: # pragma: no cover + # Can't think of a good way to test this, but I confirmed it renders as desired by adding to a non-error path + link = 'https://github.com/pydantic/pydantic/issues/new/choose' + full_message = f'The pydantic mypy plugin ran into unexpected behavior: {detail}\n' + full_message += f'Please consider reporting this bug at {link} so we can try to fix it!' + api.fail(full_message, context, code=ERROR_UNEXPECTED) + + +def error_untyped_fields(api: SemanticAnalyzerPluginInterface, context: Context) -> None: + api.fail('Untyped fields disallowed', context, code=ERROR_UNTYPED) + + +def error_default_and_default_factory_specified(api: CheckerPluginInterface, context: Context) -> None: + api.fail('Field default and default_factory cannot be specified together', context, code=ERROR_FIELD_DEFAULTS) + + +def add_method( + ctx: ClassDefContext, + name: str, + args: List[Argument], + return_type: Type, + self_type: Optional[Type] = None, + tvar_def: Optional[TypeVarDef] = None, + is_classmethod: bool = False, + is_new: bool = False, + # is_staticmethod: bool = False, +) -> None: + """ + Adds a new method to a class. + + This can be dropped if/when https://github.com/python/mypy/issues/7301 is merged + """ + info = ctx.cls.info + + # First remove any previously generated methods with the same name + # to avoid clashes and problems in the semantic analyzer. + if name in info.names: + sym = info.names[name] + if sym.plugin_generated and isinstance(sym.node, FuncDef): + ctx.cls.defs.body.remove(sym.node) # pragma: no cover + + self_type = self_type or fill_typevars(info) + if is_classmethod or is_new: + first = [Argument(Var('_cls'), TypeType.make_normalized(self_type), None, ARG_POS)] + # elif is_staticmethod: + # first = [] + else: + self_type = self_type or fill_typevars(info) + first = [Argument(Var('__pydantic_self__'), self_type, None, ARG_POS)] + args = first + args + arg_types, arg_names, arg_kinds = [], [], [] + for arg in args: + assert arg.type_annotation, 'All arguments must be fully typed.' + arg_types.append(arg.type_annotation) + arg_names.append(get_name(arg.variable)) + arg_kinds.append(arg.kind) + + function_type = ctx.api.named_type(f'{BUILTINS_NAME}.function') + signature = CallableType( + arg_types, arg_kinds, arg_names, return_type, function_type, variables=[tvar_def] if tvar_def else None + ) + + func = FuncDef(name, args, Block([PassStmt()])) + func.info = info + func.type = set_callable_name(signature, func) + func.is_class = is_classmethod + # func.is_static = is_staticmethod + func._fullname = get_fullname(info) + '.' + name + func.line = info.line + + # NOTE: we would like the plugin generated node to dominate, but we still + # need to keep any existing definitions so they get semantically analyzed. + if name in info.names: + # Get a nice unique name instead. + r_name = get_unique_redefinition_name(name, info.names) + info.names[r_name] = info.names[name] + + if is_classmethod: # or is_staticmethod: + func.is_decorated = True + v = Var(name, func.type) + v.info = info + v._fullname = func._fullname + # if is_classmethod: + v.is_classmethod = True + dec = Decorator(func, [NameExpr('classmethod')], v) + # else: + # v.is_staticmethod = True + # dec = Decorator(func, [NameExpr('staticmethod')], v) + + dec.line = info.line + sym = SymbolTableNode(MDEF, dec) + else: + sym = SymbolTableNode(MDEF, func) + sym.plugin_generated = True + + info.names[name] = sym + info.defn.defs.body.append(func) + + +def get_fullname(x: Union[FuncBase, SymbolNode]) -> str: + """ + Used for compatibility with mypy 0.740; can be dropped once support for 0.740 is dropped. + """ + fn = x.fullname + if callable(fn): # pragma: no cover + return fn() + return fn + + +def get_name(x: Union[FuncBase, SymbolNode]) -> str: + """ + Used for compatibility with mypy 0.740; can be dropped once support for 0.740 is dropped. + """ + fn = x.name + if callable(fn): # pragma: no cover + return fn() + return fn + + +def parse_toml(config_file: str) -> Optional[Dict[str, Any]]: + if not config_file.endswith('.toml'): + return None + + read_mode = 'rb' + if sys.version_info >= (3, 11): + import tomllib as toml_ + else: + try: + import tomli as toml_ + except ImportError: + # older versions of mypy have toml as a dependency, not tomli + read_mode = 'r' + try: + import toml as toml_ # type: ignore[no-redef] + except ImportError: # pragma: no cover + import warnings + + warnings.warn('No TOML parser installed, cannot read configuration from `pyproject.toml`.') + return None + + with open(config_file, read_mode) as rf: + return toml_.load(rf) # type: ignore[arg-type] diff --git a/source/target-vendors/cp312/pydantic/v1/networks.py b/source/target-vendors/cp312/pydantic/v1/networks.py new file mode 100644 index 0000000..ba07b74 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/networks.py @@ -0,0 +1,747 @@ +import re +from ipaddress import ( + IPv4Address, + IPv4Interface, + IPv4Network, + IPv6Address, + IPv6Interface, + IPv6Network, + _BaseAddress, + _BaseNetwork, +) +from typing import ( + TYPE_CHECKING, + Any, + Collection, + Dict, + Generator, + List, + Match, + Optional, + Pattern, + Set, + Tuple, + Type, + Union, + cast, + no_type_check, +) + +from pydantic.v1 import errors +from pydantic.v1.utils import Representation, update_not_none +from pydantic.v1.validators import constr_length_validator, str_validator + +if TYPE_CHECKING: + import email_validator + from typing_extensions import TypedDict + + from pydantic.v1.config import BaseConfig + from pydantic.v1.fields import ModelField + from pydantic.v1.typing import AnyCallable + + CallableGenerator = Generator[AnyCallable, None, None] + + class Parts(TypedDict, total=False): + scheme: str + user: Optional[str] + password: Optional[str] + ipv4: Optional[str] + ipv6: Optional[str] + domain: Optional[str] + port: Optional[str] + path: Optional[str] + query: Optional[str] + fragment: Optional[str] + + class HostParts(TypedDict, total=False): + host: str + tld: Optional[str] + host_type: Optional[str] + port: Optional[str] + rebuild: bool + +else: + email_validator = None + + class Parts(dict): + pass + + +NetworkType = Union[str, bytes, int, Tuple[Union[str, bytes, int], Union[str, int]]] + +__all__ = [ + 'AnyUrl', + 'AnyHttpUrl', + 'FileUrl', + 'HttpUrl', + 'stricturl', + 'EmailStr', + 'NameEmail', + 'IPvAnyAddress', + 'IPvAnyInterface', + 'IPvAnyNetwork', + 'PostgresDsn', + 'CockroachDsn', + 'AmqpDsn', + 'RedisDsn', + 'MongoDsn', + 'KafkaDsn', + 'validate_email', +] + +_url_regex_cache = None +_multi_host_url_regex_cache = None +_ascii_domain_regex_cache = None +_int_domain_regex_cache = None +_host_regex_cache = None + +_host_regex = ( + r'(?:' + r'(?P(?:\d{1,3}\.){3}\d{1,3})(?=$|[/:#?])|' # ipv4 + r'(?P\[[A-F0-9]*:[A-F0-9:]+\])(?=$|[/:#?])|' # ipv6 + r'(?P[^\s/:?#]+)' # domain, validation occurs later + r')?' + r'(?::(?P\d+))?' # port +) +_scheme_regex = r'(?:(?P[a-z][a-z0-9+\-.]+)://)?' # scheme https://tools.ietf.org/html/rfc3986#appendix-A +_user_info_regex = r'(?:(?P[^\s:/]*)(?::(?P[^\s/]*))?@)?' +_path_regex = r'(?P/[^\s?#]*)?' +_query_regex = r'(?:\?(?P[^\s#]*))?' +_fragment_regex = r'(?:#(?P[^\s#]*))?' + + +def url_regex() -> Pattern[str]: + global _url_regex_cache + if _url_regex_cache is None: + _url_regex_cache = re.compile( + rf'{_scheme_regex}{_user_info_regex}{_host_regex}{_path_regex}{_query_regex}{_fragment_regex}', + re.IGNORECASE, + ) + return _url_regex_cache + + +def multi_host_url_regex() -> Pattern[str]: + """ + Compiled multi host url regex. + + Additionally to `url_regex` it allows to match multiple hosts. + E.g. host1.db.net,host2.db.net + """ + global _multi_host_url_regex_cache + if _multi_host_url_regex_cache is None: + _multi_host_url_regex_cache = re.compile( + rf'{_scheme_regex}{_user_info_regex}' + r'(?P([^/]*))' # validation occurs later + rf'{_path_regex}{_query_regex}{_fragment_regex}', + re.IGNORECASE, + ) + return _multi_host_url_regex_cache + + +def ascii_domain_regex() -> Pattern[str]: + global _ascii_domain_regex_cache + if _ascii_domain_regex_cache is None: + ascii_chunk = r'[_0-9a-z](?:[-_0-9a-z]{0,61}[_0-9a-z])?' + ascii_domain_ending = r'(?P\.[a-z]{2,63})?\.?' + _ascii_domain_regex_cache = re.compile( + fr'(?:{ascii_chunk}\.)*?{ascii_chunk}{ascii_domain_ending}', re.IGNORECASE + ) + return _ascii_domain_regex_cache + + +def int_domain_regex() -> Pattern[str]: + global _int_domain_regex_cache + if _int_domain_regex_cache is None: + int_chunk = r'[_0-9a-\U00040000](?:[-_0-9a-\U00040000]{0,61}[_0-9a-\U00040000])?' + int_domain_ending = r'(?P(\.[^\W\d_]{2,63})|(\.(?:xn--)[_0-9a-z-]{2,63}))?\.?' + _int_domain_regex_cache = re.compile(fr'(?:{int_chunk}\.)*?{int_chunk}{int_domain_ending}', re.IGNORECASE) + return _int_domain_regex_cache + + +def host_regex() -> Pattern[str]: + global _host_regex_cache + if _host_regex_cache is None: + _host_regex_cache = re.compile( + _host_regex, + re.IGNORECASE, + ) + return _host_regex_cache + + +class AnyUrl(str): + strip_whitespace = True + min_length = 1 + max_length = 2**16 + allowed_schemes: Optional[Collection[str]] = None + tld_required: bool = False + user_required: bool = False + host_required: bool = True + hidden_parts: Set[str] = set() + + __slots__ = ('scheme', 'user', 'password', 'host', 'tld', 'host_type', 'port', 'path', 'query', 'fragment') + + @no_type_check + def __new__(cls, url: Optional[str], **kwargs) -> object: + return str.__new__(cls, cls.build(**kwargs) if url is None else url) + + def __init__( + self, + url: str, + *, + scheme: str, + user: Optional[str] = None, + password: Optional[str] = None, + host: Optional[str] = None, + tld: Optional[str] = None, + host_type: str = 'domain', + port: Optional[str] = None, + path: Optional[str] = None, + query: Optional[str] = None, + fragment: Optional[str] = None, + ) -> None: + str.__init__(url) + self.scheme = scheme + self.user = user + self.password = password + self.host = host + self.tld = tld + self.host_type = host_type + self.port = port + self.path = path + self.query = query + self.fragment = fragment + + @classmethod + def build( + cls, + *, + scheme: str, + user: Optional[str] = None, + password: Optional[str] = None, + host: str, + port: Optional[str] = None, + path: Optional[str] = None, + query: Optional[str] = None, + fragment: Optional[str] = None, + **_kwargs: str, + ) -> str: + parts = Parts( + scheme=scheme, + user=user, + password=password, + host=host, + port=port, + path=path, + query=query, + fragment=fragment, + **_kwargs, # type: ignore[misc] + ) + + url = scheme + '://' + if user: + url += user + if password: + url += ':' + password + if user or password: + url += '@' + url += host + if port and ('port' not in cls.hidden_parts or cls.get_default_parts(parts).get('port') != port): + url += ':' + port + if path: + url += path + if query: + url += '?' + query + if fragment: + url += '#' + fragment + return url + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + update_not_none(field_schema, minLength=cls.min_length, maxLength=cls.max_length, format='uri') + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield cls.validate + + @classmethod + def validate(cls, value: Any, field: 'ModelField', config: 'BaseConfig') -> 'AnyUrl': + if value.__class__ == cls: + return value + value = str_validator(value) + if cls.strip_whitespace: + value = value.strip() + url: str = cast(str, constr_length_validator(value, field, config)) + + m = cls._match_url(url) + # the regex should always match, if it doesn't please report with details of the URL tried + assert m, 'URL regex failed unexpectedly' + + original_parts = cast('Parts', m.groupdict()) + parts = cls.apply_default_parts(original_parts) + parts = cls.validate_parts(parts) + + if m.end() != len(url): + raise errors.UrlExtraError(extra=url[m.end() :]) + + return cls._build_url(m, url, parts) + + @classmethod + def _build_url(cls, m: Match[str], url: str, parts: 'Parts') -> 'AnyUrl': + """ + Validate hosts and build the AnyUrl object. Split from `validate` so this method + can be altered in `MultiHostDsn`. + """ + host, tld, host_type, rebuild = cls.validate_host(parts) + + return cls( + None if rebuild else url, + scheme=parts['scheme'], + user=parts['user'], + password=parts['password'], + host=host, + tld=tld, + host_type=host_type, + port=parts['port'], + path=parts['path'], + query=parts['query'], + fragment=parts['fragment'], + ) + + @staticmethod + def _match_url(url: str) -> Optional[Match[str]]: + return url_regex().match(url) + + @staticmethod + def _validate_port(port: Optional[str]) -> None: + if port is not None and int(port) > 65_535: + raise errors.UrlPortError() + + @classmethod + def validate_parts(cls, parts: 'Parts', validate_port: bool = True) -> 'Parts': + """ + A method used to validate parts of a URL. + Could be overridden to set default values for parts if missing + """ + scheme = parts['scheme'] + if scheme is None: + raise errors.UrlSchemeError() + + if cls.allowed_schemes and scheme.lower() not in cls.allowed_schemes: + raise errors.UrlSchemePermittedError(set(cls.allowed_schemes)) + + if validate_port: + cls._validate_port(parts['port']) + + user = parts['user'] + if cls.user_required and user is None: + raise errors.UrlUserInfoError() + + return parts + + @classmethod + def validate_host(cls, parts: 'Parts') -> Tuple[str, Optional[str], str, bool]: + tld, host_type, rebuild = None, None, False + for f in ('domain', 'ipv4', 'ipv6'): + host = parts[f] # type: ignore[literal-required] + if host: + host_type = f + break + + if host is None: + if cls.host_required: + raise errors.UrlHostError() + elif host_type == 'domain': + is_international = False + d = ascii_domain_regex().fullmatch(host) + if d is None: + d = int_domain_regex().fullmatch(host) + if d is None: + raise errors.UrlHostError() + is_international = True + + tld = d.group('tld') + if tld is None and not is_international: + d = int_domain_regex().fullmatch(host) + assert d is not None + tld = d.group('tld') + is_international = True + + if tld is not None: + tld = tld[1:] + elif cls.tld_required: + raise errors.UrlHostTldError() + + if is_international: + host_type = 'int_domain' + rebuild = True + host = host.encode('idna').decode('ascii') + if tld is not None: + tld = tld.encode('idna').decode('ascii') + + return host, tld, host_type, rebuild # type: ignore + + @staticmethod + def get_default_parts(parts: 'Parts') -> 'Parts': + return {} + + @classmethod + def apply_default_parts(cls, parts: 'Parts') -> 'Parts': + for key, value in cls.get_default_parts(parts).items(): + if not parts[key]: # type: ignore[literal-required] + parts[key] = value # type: ignore[literal-required] + return parts + + def __repr__(self) -> str: + extra = ', '.join(f'{n}={getattr(self, n)!r}' for n in self.__slots__ if getattr(self, n) is not None) + return f'{self.__class__.__name__}({super().__repr__()}, {extra})' + + +class AnyHttpUrl(AnyUrl): + allowed_schemes = {'http', 'https'} + + __slots__ = () + + +class HttpUrl(AnyHttpUrl): + tld_required = True + # https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers + max_length = 2083 + hidden_parts = {'port'} + + @staticmethod + def get_default_parts(parts: 'Parts') -> 'Parts': + return {'port': '80' if parts['scheme'] == 'http' else '443'} + + +class FileUrl(AnyUrl): + allowed_schemes = {'file'} + host_required = False + + __slots__ = () + + +class MultiHostDsn(AnyUrl): + __slots__ = AnyUrl.__slots__ + ('hosts',) + + def __init__(self, *args: Any, hosts: Optional[List['HostParts']] = None, **kwargs: Any): + super().__init__(*args, **kwargs) + self.hosts = hosts + + @staticmethod + def _match_url(url: str) -> Optional[Match[str]]: + return multi_host_url_regex().match(url) + + @classmethod + def validate_parts(cls, parts: 'Parts', validate_port: bool = True) -> 'Parts': + return super().validate_parts(parts, validate_port=False) + + @classmethod + def _build_url(cls, m: Match[str], url: str, parts: 'Parts') -> 'MultiHostDsn': + hosts_parts: List['HostParts'] = [] + host_re = host_regex() + for host in m.groupdict()['hosts'].split(','): + d: Parts = host_re.match(host).groupdict() # type: ignore + host, tld, host_type, rebuild = cls.validate_host(d) + port = d.get('port') + cls._validate_port(port) + hosts_parts.append( + { + 'host': host, + 'host_type': host_type, + 'tld': tld, + 'rebuild': rebuild, + 'port': port, + } + ) + + if len(hosts_parts) > 1: + return cls( + None if any([hp['rebuild'] for hp in hosts_parts]) else url, + scheme=parts['scheme'], + user=parts['user'], + password=parts['password'], + path=parts['path'], + query=parts['query'], + fragment=parts['fragment'], + host_type=None, + hosts=hosts_parts, + ) + else: + # backwards compatibility with single host + host_part = hosts_parts[0] + return cls( + None if host_part['rebuild'] else url, + scheme=parts['scheme'], + user=parts['user'], + password=parts['password'], + host=host_part['host'], + tld=host_part['tld'], + host_type=host_part['host_type'], + port=host_part.get('port'), + path=parts['path'], + query=parts['query'], + fragment=parts['fragment'], + ) + + +class PostgresDsn(MultiHostDsn): + allowed_schemes = { + 'postgres', + 'postgresql', + 'postgresql+asyncpg', + 'postgresql+pg8000', + 'postgresql+psycopg', + 'postgresql+psycopg2', + 'postgresql+psycopg2cffi', + 'postgresql+py-postgresql', + 'postgresql+pygresql', + } + user_required = True + + __slots__ = () + + +class CockroachDsn(AnyUrl): + allowed_schemes = { + 'cockroachdb', + 'cockroachdb+psycopg2', + 'cockroachdb+asyncpg', + } + user_required = True + + +class AmqpDsn(AnyUrl): + allowed_schemes = {'amqp', 'amqps'} + host_required = False + + +class RedisDsn(AnyUrl): + __slots__ = () + allowed_schemes = {'redis', 'rediss'} + host_required = False + + @staticmethod + def get_default_parts(parts: 'Parts') -> 'Parts': + return { + 'domain': 'localhost' if not (parts['ipv4'] or parts['ipv6']) else '', + 'port': '6379', + 'path': '/0', + } + + +class MongoDsn(AnyUrl): + allowed_schemes = {'mongodb'} + + # TODO: Needed to generic "Parts" for "Replica Set", "Sharded Cluster", and other mongodb deployment modes + @staticmethod + def get_default_parts(parts: 'Parts') -> 'Parts': + return { + 'port': '27017', + } + + +class KafkaDsn(AnyUrl): + allowed_schemes = {'kafka'} + + @staticmethod + def get_default_parts(parts: 'Parts') -> 'Parts': + return { + 'domain': 'localhost', + 'port': '9092', + } + + +def stricturl( + *, + strip_whitespace: bool = True, + min_length: int = 1, + max_length: int = 2**16, + tld_required: bool = True, + host_required: bool = True, + allowed_schemes: Optional[Collection[str]] = None, +) -> Type[AnyUrl]: + # use kwargs then define conf in a dict to aid with IDE type hinting + namespace = dict( + strip_whitespace=strip_whitespace, + min_length=min_length, + max_length=max_length, + tld_required=tld_required, + host_required=host_required, + allowed_schemes=allowed_schemes, + ) + return type('UrlValue', (AnyUrl,), namespace) + + +def import_email_validator() -> None: + global email_validator + try: + import email_validator + except ImportError as e: + raise ImportError('email-validator is not installed, run `pip install pydantic[email]`') from e + + +class EmailStr(str): + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update(type='string', format='email') + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + # included here and below so the error happens straight away + import_email_validator() + + yield str_validator + yield cls.validate + + @classmethod + def validate(cls, value: Union[str]) -> str: + return validate_email(value)[1] + + +class NameEmail(Representation): + __slots__ = 'name', 'email' + + def __init__(self, name: str, email: str): + self.name = name + self.email = email + + def __eq__(self, other: Any) -> bool: + return isinstance(other, NameEmail) and (self.name, self.email) == (other.name, other.email) + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update(type='string', format='name-email') + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + import_email_validator() + + yield cls.validate + + @classmethod + def validate(cls, value: Any) -> 'NameEmail': + if value.__class__ == cls: + return value + value = str_validator(value) + return cls(*validate_email(value)) + + def __str__(self) -> str: + return f'{self.name} <{self.email}>' + + +class IPvAnyAddress(_BaseAddress): + __slots__ = () + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update(type='string', format='ipvanyaddress') + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield cls.validate + + @classmethod + def validate(cls, value: Union[str, bytes, int]) -> Union[IPv4Address, IPv6Address]: + try: + return IPv4Address(value) + except ValueError: + pass + + try: + return IPv6Address(value) + except ValueError: + raise errors.IPvAnyAddressError() + + +class IPvAnyInterface(_BaseAddress): + __slots__ = () + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update(type='string', format='ipvanyinterface') + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield cls.validate + + @classmethod + def validate(cls, value: NetworkType) -> Union[IPv4Interface, IPv6Interface]: + try: + return IPv4Interface(value) + except ValueError: + pass + + try: + return IPv6Interface(value) + except ValueError: + raise errors.IPvAnyInterfaceError() + + +class IPvAnyNetwork(_BaseNetwork): # type: ignore + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update(type='string', format='ipvanynetwork') + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield cls.validate + + @classmethod + def validate(cls, value: NetworkType) -> Union[IPv4Network, IPv6Network]: + # Assume IP Network is defined with a default value for ``strict`` argument. + # Define your own class if you want to specify network address check strictness. + try: + return IPv4Network(value) + except ValueError: + pass + + try: + return IPv6Network(value) + except ValueError: + raise errors.IPvAnyNetworkError() + + +pretty_email_regex = re.compile(r'([\w ]*?) *<(.*)> *') +MAX_EMAIL_LENGTH = 2048 +"""Maximum length for an email. +A somewhat arbitrary but very generous number compared to what is allowed by most implementations. +""" + + +def validate_email(value: Union[str]) -> Tuple[str, str]: + """ + Email address validation using https://pypi.org/project/email-validator/ + Notes: + * raw ip address (literal) domain parts are not allowed. + * "John Doe " style "pretty" email addresses are processed + * spaces are striped from the beginning and end of addresses but no error is raised + """ + if email_validator is None: + import_email_validator() + + if len(value) > MAX_EMAIL_LENGTH: + raise errors.EmailError() + + m = pretty_email_regex.fullmatch(value) + name: Union[str, None] = None + if m: + name, value = m.groups() + email = value.strip() + try: + parts = email_validator.validate_email(email, check_deliverability=False) + except email_validator.EmailNotValidError as e: + raise errors.EmailError from e + + if hasattr(parts, 'normalized'): + # email-validator >= 2 + email = parts.normalized + assert email is not None + name = name or parts.local_part + return name, email + else: + # email-validator >1, <2 + at_index = email.index('@') + local_part = email[:at_index] # RFC 5321, local part must be case-sensitive. + global_part = email[at_index:].lower() + + return name or local_part, local_part + global_part diff --git a/source/target-vendors/cp312/pydantic/v1/parse.py b/source/target-vendors/cp312/pydantic/v1/parse.py new file mode 100644 index 0000000..431d75a --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/parse.py @@ -0,0 +1,66 @@ +import json +import pickle +from enum import Enum +from pathlib import Path +from typing import Any, Callable, Union + +from pydantic.v1.types import StrBytes + + +class Protocol(str, Enum): + json = 'json' + pickle = 'pickle' + + +def load_str_bytes( + b: StrBytes, + *, + content_type: str = None, + encoding: str = 'utf8', + proto: Protocol = None, + allow_pickle: bool = False, + json_loads: Callable[[str], Any] = json.loads, +) -> Any: + if proto is None and content_type: + if content_type.endswith(('json', 'javascript')): + pass + elif allow_pickle and content_type.endswith('pickle'): + proto = Protocol.pickle + else: + raise TypeError(f'Unknown content-type: {content_type}') + + proto = proto or Protocol.json + + if proto == Protocol.json: + if isinstance(b, bytes): + b = b.decode(encoding) + return json_loads(b) + elif proto == Protocol.pickle: + if not allow_pickle: + raise RuntimeError('Trying to decode with pickle with allow_pickle=False') + bb = b if isinstance(b, bytes) else b.encode() + return pickle.loads(bb) + else: + raise TypeError(f'Unknown protocol: {proto}') + + +def load_file( + path: Union[str, Path], + *, + content_type: str = None, + encoding: str = 'utf8', + proto: Protocol = None, + allow_pickle: bool = False, + json_loads: Callable[[str], Any] = json.loads, +) -> Any: + path = Path(path) + b = path.read_bytes() + if content_type is None: + if path.suffix in ('.js', '.json'): + proto = Protocol.json + elif path.suffix == '.pkl': + proto = Protocol.pickle + + return load_str_bytes( + b, proto=proto, content_type=content_type, encoding=encoding, allow_pickle=allow_pickle, json_loads=json_loads + ) diff --git a/source/target-vendors/cp312/pydantic/v1/py.typed b/source/target-vendors/cp312/pydantic/v1/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/pydantic/v1/schema.py b/source/target-vendors/cp312/pydantic/v1/schema.py new file mode 100644 index 0000000..a91fe2c --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/schema.py @@ -0,0 +1,1163 @@ +import re +import warnings +from collections import defaultdict +from dataclasses import is_dataclass +from datetime import date, datetime, time, timedelta +from decimal import Decimal +from enum import Enum +from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + ForwardRef, + FrozenSet, + Generic, + Iterable, + List, + Optional, + Pattern, + Sequence, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, +) +from uuid import UUID + +from typing_extensions import Annotated, Literal + +from pydantic.v1.fields import ( + MAPPING_LIKE_SHAPES, + SHAPE_DEQUE, + SHAPE_FROZENSET, + SHAPE_GENERIC, + SHAPE_ITERABLE, + SHAPE_LIST, + SHAPE_SEQUENCE, + SHAPE_SET, + SHAPE_SINGLETON, + SHAPE_TUPLE, + SHAPE_TUPLE_ELLIPSIS, + FieldInfo, + ModelField, +) +from pydantic.v1.json import pydantic_encoder +from pydantic.v1.networks import AnyUrl, EmailStr +from pydantic.v1.types import ( + ConstrainedDecimal, + ConstrainedFloat, + ConstrainedFrozenSet, + ConstrainedInt, + ConstrainedList, + ConstrainedSet, + ConstrainedStr, + SecretBytes, + SecretStr, + StrictBytes, + StrictStr, + conbytes, + condecimal, + confloat, + confrozenset, + conint, + conlist, + conset, + constr, +) +from pydantic.v1.typing import ( + all_literal_values, + get_args, + get_origin, + get_sub_types, + is_callable_type, + is_literal_type, + is_namedtuple, + is_none_type, + is_union, +) +from pydantic.v1.utils import ROOT_KEY, get_model, lenient_issubclass + +if TYPE_CHECKING: + from pydantic.v1.dataclasses import Dataclass + from pydantic.v1.main import BaseModel + +default_prefix = '#/definitions/' +default_ref_template = '#/definitions/{model}' + +TypeModelOrEnum = Union[Type['BaseModel'], Type[Enum]] +TypeModelSet = Set[TypeModelOrEnum] + + +def _apply_modify_schema( + modify_schema: Callable[..., None], field: Optional[ModelField], field_schema: Dict[str, Any] +) -> None: + from inspect import signature + + sig = signature(modify_schema) + args = set(sig.parameters.keys()) + if 'field' in args or 'kwargs' in args: + modify_schema(field_schema, field=field) + else: + modify_schema(field_schema) + + +def schema( + models: Sequence[Union[Type['BaseModel'], Type['Dataclass']]], + *, + by_alias: bool = True, + title: Optional[str] = None, + description: Optional[str] = None, + ref_prefix: Optional[str] = None, + ref_template: str = default_ref_template, +) -> Dict[str, Any]: + """ + Process a list of models and generate a single JSON Schema with all of them defined in the ``definitions`` + top-level JSON key, including their sub-models. + + :param models: a list of models to include in the generated JSON Schema + :param by_alias: generate the schemas using the aliases defined, if any + :param title: title for the generated schema that includes the definitions + :param description: description for the generated schema + :param ref_prefix: the JSON Pointer prefix for schema references with ``$ref``, if None, will be set to the + default of ``#/definitions/``. Update it if you want the schemas to reference the definitions somewhere + else, e.g. for OpenAPI use ``#/components/schemas/``. The resulting generated schemas will still be at the + top-level key ``definitions``, so you can extract them from there. But all the references will have the set + prefix. + :param ref_template: Use a ``string.format()`` template for ``$ref`` instead of a prefix. This can be useful + for references that cannot be represented by ``ref_prefix`` such as a definition stored in another file. For + a sibling json file in a ``/schemas`` directory use ``"/schemas/${model}.json#"``. + :return: dict with the JSON Schema with a ``definitions`` top-level key including the schema definitions for + the models and sub-models passed in ``models``. + """ + clean_models = [get_model(model) for model in models] + flat_models = get_flat_models_from_models(clean_models) + model_name_map = get_model_name_map(flat_models) + definitions = {} + output_schema: Dict[str, Any] = {} + if title: + output_schema['title'] = title + if description: + output_schema['description'] = description + for model in clean_models: + m_schema, m_definitions, m_nested_models = model_process_schema( + model, + by_alias=by_alias, + model_name_map=model_name_map, + ref_prefix=ref_prefix, + ref_template=ref_template, + ) + definitions.update(m_definitions) + model_name = model_name_map[model] + definitions[model_name] = m_schema + if definitions: + output_schema['definitions'] = definitions + return output_schema + + +def model_schema( + model: Union[Type['BaseModel'], Type['Dataclass']], + by_alias: bool = True, + ref_prefix: Optional[str] = None, + ref_template: str = default_ref_template, +) -> Dict[str, Any]: + """ + Generate a JSON Schema for one model. With all the sub-models defined in the ``definitions`` top-level + JSON key. + + :param model: a Pydantic model (a class that inherits from BaseModel) + :param by_alias: generate the schemas using the aliases defined, if any + :param ref_prefix: the JSON Pointer prefix for schema references with ``$ref``, if None, will be set to the + default of ``#/definitions/``. Update it if you want the schemas to reference the definitions somewhere + else, e.g. for OpenAPI use ``#/components/schemas/``. The resulting generated schemas will still be at the + top-level key ``definitions``, so you can extract them from there. But all the references will have the set + prefix. + :param ref_template: Use a ``string.format()`` template for ``$ref`` instead of a prefix. This can be useful for + references that cannot be represented by ``ref_prefix`` such as a definition stored in another file. For a + sibling json file in a ``/schemas`` directory use ``"/schemas/${model}.json#"``. + :return: dict with the JSON Schema for the passed ``model`` + """ + model = get_model(model) + flat_models = get_flat_models_from_model(model) + model_name_map = get_model_name_map(flat_models) + model_name = model_name_map[model] + m_schema, m_definitions, nested_models = model_process_schema( + model, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix, ref_template=ref_template + ) + if model_name in nested_models: + # model_name is in Nested models, it has circular references + m_definitions[model_name] = m_schema + m_schema = get_schema_ref(model_name, ref_prefix, ref_template, False) + if m_definitions: + m_schema.update({'definitions': m_definitions}) + return m_schema + + +def get_field_info_schema(field: ModelField, schema_overrides: bool = False) -> Tuple[Dict[str, Any], bool]: + # If no title is explicitly set, we don't set title in the schema for enums. + # The behaviour is the same as `BaseModel` reference, where the default title + # is in the definitions part of the schema. + schema_: Dict[str, Any] = {} + if field.field_info.title or not lenient_issubclass(field.type_, Enum): + schema_['title'] = field.field_info.title or field.alias.title().replace('_', ' ') + + if field.field_info.title: + schema_overrides = True + + if field.field_info.description: + schema_['description'] = field.field_info.description + schema_overrides = True + + if not field.required and field.default is not None and not is_callable_type(field.outer_type_): + schema_['default'] = encode_default(field.default) + schema_overrides = True + + return schema_, schema_overrides + + +def field_schema( + field: ModelField, + *, + by_alias: bool = True, + model_name_map: Dict[TypeModelOrEnum, str], + ref_prefix: Optional[str] = None, + ref_template: str = default_ref_template, + known_models: Optional[TypeModelSet] = None, +) -> Tuple[Dict[str, Any], Dict[str, Any], Set[str]]: + """ + Process a Pydantic field and return a tuple with a JSON Schema for it as the first item. + Also return a dictionary of definitions with models as keys and their schemas as values. If the passed field + is a model and has sub-models, and those sub-models don't have overrides (as ``title``, ``default``, etc), they + will be included in the definitions and referenced in the schema instead of included recursively. + + :param field: a Pydantic ``ModelField`` + :param by_alias: use the defined alias (if any) in the returned schema + :param model_name_map: used to generate the JSON Schema references to other models included in the definitions + :param ref_prefix: the JSON Pointer prefix to use for references to other schemas, if None, the default of + #/definitions/ will be used + :param ref_template: Use a ``string.format()`` template for ``$ref`` instead of a prefix. This can be useful for + references that cannot be represented by ``ref_prefix`` such as a definition stored in another file. For a + sibling json file in a ``/schemas`` directory use ``"/schemas/${model}.json#"``. + :param known_models: used to solve circular references + :return: tuple of the schema for this field and additional definitions + """ + s, schema_overrides = get_field_info_schema(field) + + validation_schema = get_field_schema_validations(field) + if validation_schema: + s.update(validation_schema) + schema_overrides = True + + f_schema, f_definitions, f_nested_models = field_type_schema( + field, + by_alias=by_alias, + model_name_map=model_name_map, + schema_overrides=schema_overrides, + ref_prefix=ref_prefix, + ref_template=ref_template, + known_models=known_models or set(), + ) + + # $ref will only be returned when there are no schema_overrides + if '$ref' in f_schema: + return f_schema, f_definitions, f_nested_models + else: + s.update(f_schema) + return s, f_definitions, f_nested_models + + +numeric_types = (int, float, Decimal) +_str_types_attrs: Tuple[Tuple[str, Union[type, Tuple[type, ...]], str], ...] = ( + ('max_length', numeric_types, 'maxLength'), + ('min_length', numeric_types, 'minLength'), + ('regex', str, 'pattern'), +) + +_numeric_types_attrs: Tuple[Tuple[str, Union[type, Tuple[type, ...]], str], ...] = ( + ('gt', numeric_types, 'exclusiveMinimum'), + ('lt', numeric_types, 'exclusiveMaximum'), + ('ge', numeric_types, 'minimum'), + ('le', numeric_types, 'maximum'), + ('multiple_of', numeric_types, 'multipleOf'), +) + + +def get_field_schema_validations(field: ModelField) -> Dict[str, Any]: + """ + Get the JSON Schema validation keywords for a ``field`` with an annotation of + a Pydantic ``FieldInfo`` with validation arguments. + """ + f_schema: Dict[str, Any] = {} + + if lenient_issubclass(field.type_, Enum): + # schema is already updated by `enum_process_schema`; just update with field extra + if field.field_info.extra: + f_schema.update(field.field_info.extra) + return f_schema + + if lenient_issubclass(field.type_, (str, bytes)): + for attr_name, t, keyword in _str_types_attrs: + attr = getattr(field.field_info, attr_name, None) + if isinstance(attr, t): + f_schema[keyword] = attr + if lenient_issubclass(field.type_, numeric_types) and not issubclass(field.type_, bool): + for attr_name, t, keyword in _numeric_types_attrs: + attr = getattr(field.field_info, attr_name, None) + if isinstance(attr, t): + f_schema[keyword] = attr + if field.field_info is not None and field.field_info.const: + f_schema['const'] = field.default + if field.field_info.extra: + f_schema.update(field.field_info.extra) + modify_schema = getattr(field.outer_type_, '__modify_schema__', None) + if modify_schema: + _apply_modify_schema(modify_schema, field, f_schema) + return f_schema + + +def get_model_name_map(unique_models: TypeModelSet) -> Dict[TypeModelOrEnum, str]: + """ + Process a set of models and generate unique names for them to be used as keys in the JSON Schema + definitions. By default the names are the same as the class name. But if two models in different Python + modules have the same name (e.g. "users.Model" and "items.Model"), the generated names will be + based on the Python module path for those conflicting models to prevent name collisions. + + :param unique_models: a Python set of models + :return: dict mapping models to names + """ + name_model_map = {} + conflicting_names: Set[str] = set() + for model in unique_models: + model_name = normalize_name(model.__name__) + if model_name in conflicting_names: + model_name = get_long_model_name(model) + name_model_map[model_name] = model + elif model_name in name_model_map: + conflicting_names.add(model_name) + conflicting_model = name_model_map.pop(model_name) + name_model_map[get_long_model_name(conflicting_model)] = conflicting_model + name_model_map[get_long_model_name(model)] = model + else: + name_model_map[model_name] = model + return {v: k for k, v in name_model_map.items()} + + +def get_flat_models_from_model(model: Type['BaseModel'], known_models: Optional[TypeModelSet] = None) -> TypeModelSet: + """ + Take a single ``model`` and generate a set with itself and all the sub-models in the tree. I.e. if you pass + model ``Foo`` (subclass of Pydantic ``BaseModel``) as ``model``, and it has a field of type ``Bar`` (also + subclass of ``BaseModel``) and that model ``Bar`` has a field of type ``Baz`` (also subclass of ``BaseModel``), + the return value will be ``set([Foo, Bar, Baz])``. + + :param model: a Pydantic ``BaseModel`` subclass + :param known_models: used to solve circular references + :return: a set with the initial model and all its sub-models + """ + known_models = known_models or set() + flat_models: TypeModelSet = set() + flat_models.add(model) + known_models |= flat_models + fields = cast(Sequence[ModelField], model.__fields__.values()) + flat_models |= get_flat_models_from_fields(fields, known_models=known_models) + return flat_models + + +def get_flat_models_from_field(field: ModelField, known_models: TypeModelSet) -> TypeModelSet: + """ + Take a single Pydantic ``ModelField`` (from a model) that could have been declared as a subclass of BaseModel + (so, it could be a submodel), and generate a set with its model and all the sub-models in the tree. + I.e. if you pass a field that was declared to be of type ``Foo`` (subclass of BaseModel) as ``field``, and that + model ``Foo`` has a field of type ``Bar`` (also subclass of ``BaseModel``) and that model ``Bar`` has a field of + type ``Baz`` (also subclass of ``BaseModel``), the return value will be ``set([Foo, Bar, Baz])``. + + :param field: a Pydantic ``ModelField`` + :param known_models: used to solve circular references + :return: a set with the model used in the declaration for this field, if any, and all its sub-models + """ + from pydantic.v1.main import BaseModel + + flat_models: TypeModelSet = set() + + field_type = field.type_ + if lenient_issubclass(getattr(field_type, '__pydantic_model__', None), BaseModel): + field_type = field_type.__pydantic_model__ + + if field.sub_fields and not lenient_issubclass(field_type, BaseModel): + flat_models |= get_flat_models_from_fields(field.sub_fields, known_models=known_models) + elif lenient_issubclass(field_type, BaseModel) and field_type not in known_models: + flat_models |= get_flat_models_from_model(field_type, known_models=known_models) + elif lenient_issubclass(field_type, Enum): + flat_models.add(field_type) + return flat_models + + +def get_flat_models_from_fields(fields: Sequence[ModelField], known_models: TypeModelSet) -> TypeModelSet: + """ + Take a list of Pydantic ``ModelField``s (from a model) that could have been declared as subclasses of ``BaseModel`` + (so, any of them could be a submodel), and generate a set with their models and all the sub-models in the tree. + I.e. if you pass a the fields of a model ``Foo`` (subclass of ``BaseModel``) as ``fields``, and on of them has a + field of type ``Bar`` (also subclass of ``BaseModel``) and that model ``Bar`` has a field of type ``Baz`` (also + subclass of ``BaseModel``), the return value will be ``set([Foo, Bar, Baz])``. + + :param fields: a list of Pydantic ``ModelField``s + :param known_models: used to solve circular references + :return: a set with any model declared in the fields, and all their sub-models + """ + flat_models: TypeModelSet = set() + for field in fields: + flat_models |= get_flat_models_from_field(field, known_models=known_models) + return flat_models + + +def get_flat_models_from_models(models: Sequence[Type['BaseModel']]) -> TypeModelSet: + """ + Take a list of ``models`` and generate a set with them and all their sub-models in their trees. I.e. if you pass + a list of two models, ``Foo`` and ``Bar``, both subclasses of Pydantic ``BaseModel`` as models, and ``Bar`` has + a field of type ``Baz`` (also subclass of ``BaseModel``), the return value will be ``set([Foo, Bar, Baz])``. + """ + flat_models: TypeModelSet = set() + for model in models: + flat_models |= get_flat_models_from_model(model) + return flat_models + + +def get_long_model_name(model: TypeModelOrEnum) -> str: + return f'{model.__module__}__{model.__qualname__}'.replace('.', '__') + + +def field_type_schema( + field: ModelField, + *, + by_alias: bool, + model_name_map: Dict[TypeModelOrEnum, str], + ref_template: str, + schema_overrides: bool = False, + ref_prefix: Optional[str] = None, + known_models: TypeModelSet, +) -> Tuple[Dict[str, Any], Dict[str, Any], Set[str]]: + """ + Used by ``field_schema()``, you probably should be using that function. + + Take a single ``field`` and generate the schema for its type only, not including additional + information as title, etc. Also return additional schema definitions, from sub-models. + """ + from pydantic.v1.main import BaseModel # noqa: F811 + + definitions = {} + nested_models: Set[str] = set() + f_schema: Dict[str, Any] + if field.shape in { + SHAPE_LIST, + SHAPE_TUPLE_ELLIPSIS, + SHAPE_SEQUENCE, + SHAPE_SET, + SHAPE_FROZENSET, + SHAPE_ITERABLE, + SHAPE_DEQUE, + }: + items_schema, f_definitions, f_nested_models = field_singleton_schema( + field, + by_alias=by_alias, + model_name_map=model_name_map, + ref_prefix=ref_prefix, + ref_template=ref_template, + known_models=known_models, + ) + definitions.update(f_definitions) + nested_models.update(f_nested_models) + f_schema = {'type': 'array', 'items': items_schema} + if field.shape in {SHAPE_SET, SHAPE_FROZENSET}: + f_schema['uniqueItems'] = True + + elif field.shape in MAPPING_LIKE_SHAPES: + f_schema = {'type': 'object'} + key_field = cast(ModelField, field.key_field) + regex = getattr(key_field.type_, 'regex', None) + items_schema, f_definitions, f_nested_models = field_singleton_schema( + field, + by_alias=by_alias, + model_name_map=model_name_map, + ref_prefix=ref_prefix, + ref_template=ref_template, + known_models=known_models, + ) + definitions.update(f_definitions) + nested_models.update(f_nested_models) + if regex: + # Dict keys have a regex pattern + # items_schema might be a schema or empty dict, add it either way + f_schema['patternProperties'] = {ConstrainedStr._get_pattern(regex): items_schema} + if items_schema: + # The dict values are not simply Any, so they need a schema + f_schema['additionalProperties'] = items_schema + elif field.shape == SHAPE_TUPLE or (field.shape == SHAPE_GENERIC and not issubclass(field.type_, BaseModel)): + sub_schema = [] + sub_fields = cast(List[ModelField], field.sub_fields) + for sf in sub_fields: + sf_schema, sf_definitions, sf_nested_models = field_type_schema( + sf, + by_alias=by_alias, + model_name_map=model_name_map, + ref_prefix=ref_prefix, + ref_template=ref_template, + known_models=known_models, + ) + definitions.update(sf_definitions) + nested_models.update(sf_nested_models) + sub_schema.append(sf_schema) + + sub_fields_len = len(sub_fields) + if field.shape == SHAPE_GENERIC: + all_of_schemas = sub_schema[0] if sub_fields_len == 1 else {'type': 'array', 'items': sub_schema} + f_schema = {'allOf': [all_of_schemas]} + else: + f_schema = { + 'type': 'array', + 'minItems': sub_fields_len, + 'maxItems': sub_fields_len, + } + if sub_fields_len >= 1: + f_schema['items'] = sub_schema + else: + assert field.shape in {SHAPE_SINGLETON, SHAPE_GENERIC}, field.shape + f_schema, f_definitions, f_nested_models = field_singleton_schema( + field, + by_alias=by_alias, + model_name_map=model_name_map, + schema_overrides=schema_overrides, + ref_prefix=ref_prefix, + ref_template=ref_template, + known_models=known_models, + ) + definitions.update(f_definitions) + nested_models.update(f_nested_models) + + # check field type to avoid repeated calls to the same __modify_schema__ method + if field.type_ != field.outer_type_: + if field.shape == SHAPE_GENERIC: + field_type = field.type_ + else: + field_type = field.outer_type_ + modify_schema = getattr(field_type, '__modify_schema__', None) + if modify_schema: + _apply_modify_schema(modify_schema, field, f_schema) + return f_schema, definitions, nested_models + + +def model_process_schema( + model: TypeModelOrEnum, + *, + by_alias: bool = True, + model_name_map: Dict[TypeModelOrEnum, str], + ref_prefix: Optional[str] = None, + ref_template: str = default_ref_template, + known_models: Optional[TypeModelSet] = None, + field: Optional[ModelField] = None, +) -> Tuple[Dict[str, Any], Dict[str, Any], Set[str]]: + """ + Used by ``model_schema()``, you probably should be using that function. + + Take a single ``model`` and generate its schema. Also return additional schema definitions, from sub-models. The + sub-models of the returned schema will be referenced, but their definitions will not be included in the schema. All + the definitions are returned as the second value. + """ + from inspect import getdoc, signature + + known_models = known_models or set() + if lenient_issubclass(model, Enum): + model = cast(Type[Enum], model) + s = enum_process_schema(model, field=field) + return s, {}, set() + model = cast(Type['BaseModel'], model) + s = {'title': model.__config__.title or model.__name__} + doc = getdoc(model) + if doc: + s['description'] = doc + known_models.add(model) + m_schema, m_definitions, nested_models = model_type_schema( + model, + by_alias=by_alias, + model_name_map=model_name_map, + ref_prefix=ref_prefix, + ref_template=ref_template, + known_models=known_models, + ) + s.update(m_schema) + schema_extra = model.__config__.schema_extra + if callable(schema_extra): + if len(signature(schema_extra).parameters) == 1: + schema_extra(s) + else: + schema_extra(s, model) + else: + s.update(schema_extra) + return s, m_definitions, nested_models + + +def model_type_schema( + model: Type['BaseModel'], + *, + by_alias: bool, + model_name_map: Dict[TypeModelOrEnum, str], + ref_template: str, + ref_prefix: Optional[str] = None, + known_models: TypeModelSet, +) -> Tuple[Dict[str, Any], Dict[str, Any], Set[str]]: + """ + You probably should be using ``model_schema()``, this function is indirectly used by that function. + + Take a single ``model`` and generate the schema for its type only, not including additional + information as title, etc. Also return additional schema definitions, from sub-models. + """ + properties = {} + required = [] + definitions: Dict[str, Any] = {} + nested_models: Set[str] = set() + for k, f in model.__fields__.items(): + try: + f_schema, f_definitions, f_nested_models = field_schema( + f, + by_alias=by_alias, + model_name_map=model_name_map, + ref_prefix=ref_prefix, + ref_template=ref_template, + known_models=known_models, + ) + except SkipField as skip: + warnings.warn(skip.message, UserWarning) + continue + definitions.update(f_definitions) + nested_models.update(f_nested_models) + if by_alias: + properties[f.alias] = f_schema + if f.required: + required.append(f.alias) + else: + properties[k] = f_schema + if f.required: + required.append(k) + if ROOT_KEY in properties: + out_schema = properties[ROOT_KEY] + out_schema['title'] = model.__config__.title or model.__name__ + else: + out_schema = {'type': 'object', 'properties': properties} + if required: + out_schema['required'] = required + if model.__config__.extra == 'forbid': + out_schema['additionalProperties'] = False + return out_schema, definitions, nested_models + + +def enum_process_schema(enum: Type[Enum], *, field: Optional[ModelField] = None) -> Dict[str, Any]: + """ + Take a single `enum` and generate its schema. + + This is similar to the `model_process_schema` function, but applies to ``Enum`` objects. + """ + import inspect + + schema_: Dict[str, Any] = { + 'title': enum.__name__, + # Python assigns all enums a default docstring value of 'An enumeration', so + # all enums will have a description field even if not explicitly provided. + 'description': inspect.cleandoc(enum.__doc__ or 'An enumeration.'), + # Add enum values and the enum field type to the schema. + 'enum': [item.value for item in cast(Iterable[Enum], enum)], + } + + add_field_type_to_schema(enum, schema_) + + modify_schema = getattr(enum, '__modify_schema__', None) + if modify_schema: + _apply_modify_schema(modify_schema, field, schema_) + + return schema_ + + +def field_singleton_sub_fields_schema( + field: ModelField, + *, + by_alias: bool, + model_name_map: Dict[TypeModelOrEnum, str], + ref_template: str, + schema_overrides: bool = False, + ref_prefix: Optional[str] = None, + known_models: TypeModelSet, +) -> Tuple[Dict[str, Any], Dict[str, Any], Set[str]]: + """ + This function is indirectly used by ``field_schema()``, you probably should be using that function. + + Take a list of Pydantic ``ModelField`` from the declaration of a type with parameters, and generate their + schema. I.e., fields used as "type parameters", like ``str`` and ``int`` in ``Tuple[str, int]``. + """ + sub_fields = cast(List[ModelField], field.sub_fields) + definitions = {} + nested_models: Set[str] = set() + if len(sub_fields) == 1: + return field_type_schema( + sub_fields[0], + by_alias=by_alias, + model_name_map=model_name_map, + schema_overrides=schema_overrides, + ref_prefix=ref_prefix, + ref_template=ref_template, + known_models=known_models, + ) + else: + s: Dict[str, Any] = {} + # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#discriminator-object + field_has_discriminator: bool = field.discriminator_key is not None + if field_has_discriminator: + assert field.sub_fields_mapping is not None + + discriminator_models_refs: Dict[str, Union[str, Dict[str, Any]]] = {} + + for discriminator_value, sub_field in field.sub_fields_mapping.items(): + if isinstance(discriminator_value, Enum): + discriminator_value = str(discriminator_value.value) + # sub_field is either a `BaseModel` or directly an `Annotated` `Union` of many + if is_union(get_origin(sub_field.type_)): + sub_models = get_sub_types(sub_field.type_) + discriminator_models_refs[discriminator_value] = { + model_name_map[sub_model]: get_schema_ref( + model_name_map[sub_model], ref_prefix, ref_template, False + ) + for sub_model in sub_models + } + else: + sub_field_type = sub_field.type_ + if hasattr(sub_field_type, '__pydantic_model__'): + sub_field_type = sub_field_type.__pydantic_model__ + + discriminator_model_name = model_name_map[sub_field_type] + discriminator_model_ref = get_schema_ref(discriminator_model_name, ref_prefix, ref_template, False) + discriminator_models_refs[discriminator_value] = discriminator_model_ref['$ref'] + + s['discriminator'] = { + 'propertyName': field.discriminator_alias if by_alias else field.discriminator_key, + 'mapping': discriminator_models_refs, + } + + sub_field_schemas = [] + for sf in sub_fields: + sub_schema, sub_definitions, sub_nested_models = field_type_schema( + sf, + by_alias=by_alias, + model_name_map=model_name_map, + schema_overrides=schema_overrides, + ref_prefix=ref_prefix, + ref_template=ref_template, + known_models=known_models, + ) + definitions.update(sub_definitions) + if schema_overrides and 'allOf' in sub_schema: + # if the sub_field is a referenced schema we only need the referenced + # object. Otherwise we will end up with several allOf inside anyOf/oneOf. + # See https://github.com/pydantic/pydantic/issues/1209 + sub_schema = sub_schema['allOf'][0] + + if sub_schema.keys() == {'discriminator', 'oneOf'}: + # we don't want discriminator information inside oneOf choices, this is dealt with elsewhere + sub_schema.pop('discriminator') + sub_field_schemas.append(sub_schema) + nested_models.update(sub_nested_models) + s['oneOf' if field_has_discriminator else 'anyOf'] = sub_field_schemas + return s, definitions, nested_models + + +# Order is important, e.g. subclasses of str must go before str +# this is used only for standard library types, custom types should use __modify_schema__ instead +field_class_to_schema: Tuple[Tuple[Any, Dict[str, Any]], ...] = ( + (Path, {'type': 'string', 'format': 'path'}), + (datetime, {'type': 'string', 'format': 'date-time'}), + (date, {'type': 'string', 'format': 'date'}), + (time, {'type': 'string', 'format': 'time'}), + (timedelta, {'type': 'number', 'format': 'time-delta'}), + (IPv4Network, {'type': 'string', 'format': 'ipv4network'}), + (IPv6Network, {'type': 'string', 'format': 'ipv6network'}), + (IPv4Interface, {'type': 'string', 'format': 'ipv4interface'}), + (IPv6Interface, {'type': 'string', 'format': 'ipv6interface'}), + (IPv4Address, {'type': 'string', 'format': 'ipv4'}), + (IPv6Address, {'type': 'string', 'format': 'ipv6'}), + (Pattern, {'type': 'string', 'format': 'regex'}), + (str, {'type': 'string'}), + (bytes, {'type': 'string', 'format': 'binary'}), + (bool, {'type': 'boolean'}), + (int, {'type': 'integer'}), + (float, {'type': 'number'}), + (Decimal, {'type': 'number'}), + (UUID, {'type': 'string', 'format': 'uuid'}), + (dict, {'type': 'object'}), + (list, {'type': 'array', 'items': {}}), + (tuple, {'type': 'array', 'items': {}}), + (set, {'type': 'array', 'items': {}, 'uniqueItems': True}), + (frozenset, {'type': 'array', 'items': {}, 'uniqueItems': True}), +) + +json_scheme = {'type': 'string', 'format': 'json-string'} + + +def add_field_type_to_schema(field_type: Any, schema_: Dict[str, Any]) -> None: + """ + Update the given `schema` with the type-specific metadata for the given `field_type`. + + This function looks through `field_class_to_schema` for a class that matches the given `field_type`, + and then modifies the given `schema` with the information from that type. + """ + for type_, t_schema in field_class_to_schema: + # Fallback for `typing.Pattern` and `re.Pattern` as they are not a valid class + if lenient_issubclass(field_type, type_) or field_type is type_ is Pattern: + schema_.update(t_schema) + break + + +def get_schema_ref(name: str, ref_prefix: Optional[str], ref_template: str, schema_overrides: bool) -> Dict[str, Any]: + if ref_prefix: + schema_ref = {'$ref': ref_prefix + name} + else: + schema_ref = {'$ref': ref_template.format(model=name)} + return {'allOf': [schema_ref]} if schema_overrides else schema_ref + + +def field_singleton_schema( # noqa: C901 (ignore complexity) + field: ModelField, + *, + by_alias: bool, + model_name_map: Dict[TypeModelOrEnum, str], + ref_template: str, + schema_overrides: bool = False, + ref_prefix: Optional[str] = None, + known_models: TypeModelSet, +) -> Tuple[Dict[str, Any], Dict[str, Any], Set[str]]: + """ + This function is indirectly used by ``field_schema()``, you should probably be using that function. + + Take a single Pydantic ``ModelField``, and return its schema and any additional definitions from sub-models. + """ + from pydantic.v1.main import BaseModel + + definitions: Dict[str, Any] = {} + nested_models: Set[str] = set() + field_type = field.type_ + + # Recurse into this field if it contains sub_fields and is NOT a + # BaseModel OR that BaseModel is a const + if field.sub_fields and ( + (field.field_info and field.field_info.const) or not lenient_issubclass(field_type, BaseModel) + ): + return field_singleton_sub_fields_schema( + field, + by_alias=by_alias, + model_name_map=model_name_map, + schema_overrides=schema_overrides, + ref_prefix=ref_prefix, + ref_template=ref_template, + known_models=known_models, + ) + if field_type is Any or field_type is object or field_type.__class__ == TypeVar or get_origin(field_type) is type: + return {}, definitions, nested_models # no restrictions + if is_none_type(field_type): + return {'type': 'null'}, definitions, nested_models + if is_callable_type(field_type): + raise SkipField(f'Callable {field.name} was excluded from schema since JSON schema has no equivalent type.') + f_schema: Dict[str, Any] = {} + if field.field_info is not None and field.field_info.const: + f_schema['const'] = field.default + + if is_literal_type(field_type): + values = tuple(x.value if isinstance(x, Enum) else x for x in all_literal_values(field_type)) + + if len({v.__class__ for v in values}) > 1: + return field_schema( + multitypes_literal_field_for_schema(values, field), + by_alias=by_alias, + model_name_map=model_name_map, + ref_prefix=ref_prefix, + ref_template=ref_template, + known_models=known_models, + ) + + # All values have the same type + field_type = values[0].__class__ + f_schema['enum'] = list(values) + add_field_type_to_schema(field_type, f_schema) + elif lenient_issubclass(field_type, Enum): + enum_name = model_name_map[field_type] + f_schema, schema_overrides = get_field_info_schema(field, schema_overrides) + f_schema.update(get_schema_ref(enum_name, ref_prefix, ref_template, schema_overrides)) + definitions[enum_name] = enum_process_schema(field_type, field=field) + elif is_namedtuple(field_type): + sub_schema, *_ = model_process_schema( + field_type.__pydantic_model__, + by_alias=by_alias, + model_name_map=model_name_map, + ref_prefix=ref_prefix, + ref_template=ref_template, + known_models=known_models, + field=field, + ) + items_schemas = list(sub_schema['properties'].values()) + f_schema.update( + { + 'type': 'array', + 'items': items_schemas, + 'minItems': len(items_schemas), + 'maxItems': len(items_schemas), + } + ) + elif not hasattr(field_type, '__pydantic_model__'): + add_field_type_to_schema(field_type, f_schema) + + modify_schema = getattr(field_type, '__modify_schema__', None) + if modify_schema: + _apply_modify_schema(modify_schema, field, f_schema) + + if f_schema: + return f_schema, definitions, nested_models + + # Handle dataclass-based models + if lenient_issubclass(getattr(field_type, '__pydantic_model__', None), BaseModel): + field_type = field_type.__pydantic_model__ + + if issubclass(field_type, BaseModel): + model_name = model_name_map[field_type] + if field_type not in known_models: + sub_schema, sub_definitions, sub_nested_models = model_process_schema( + field_type, + by_alias=by_alias, + model_name_map=model_name_map, + ref_prefix=ref_prefix, + ref_template=ref_template, + known_models=known_models, + field=field, + ) + definitions.update(sub_definitions) + definitions[model_name] = sub_schema + nested_models.update(sub_nested_models) + else: + nested_models.add(model_name) + schema_ref = get_schema_ref(model_name, ref_prefix, ref_template, schema_overrides) + return schema_ref, definitions, nested_models + + # For generics with no args + args = get_args(field_type) + if args is not None and not args and Generic in field_type.__bases__: + return f_schema, definitions, nested_models + + raise ValueError(f'Value not declarable with JSON Schema, field: {field}') + + +def multitypes_literal_field_for_schema(values: Tuple[Any, ...], field: ModelField) -> ModelField: + """ + To support `Literal` with values of different types, we split it into multiple `Literal` with same type + e.g. `Literal['qwe', 'asd', 1, 2]` becomes `Union[Literal['qwe', 'asd'], Literal[1, 2]]` + """ + literal_distinct_types = defaultdict(list) + for v in values: + literal_distinct_types[v.__class__].append(v) + distinct_literals = (Literal[tuple(same_type_values)] for same_type_values in literal_distinct_types.values()) + + return ModelField( + name=field.name, + type_=Union[tuple(distinct_literals)], # type: ignore + class_validators=field.class_validators, + model_config=field.model_config, + default=field.default, + required=field.required, + alias=field.alias, + field_info=field.field_info, + ) + + +def encode_default(dft: Any) -> Any: + from pydantic.v1.main import BaseModel + + if isinstance(dft, BaseModel) or is_dataclass(dft): + dft = cast('dict[str, Any]', pydantic_encoder(dft)) + + if isinstance(dft, dict): + return {encode_default(k): encode_default(v) for k, v in dft.items()} + elif isinstance(dft, Enum): + return dft.value + elif isinstance(dft, (int, float, str)): + return dft + elif isinstance(dft, (list, tuple)): + t = dft.__class__ + seq_args = (encode_default(v) for v in dft) + return t(*seq_args) if is_namedtuple(t) else t(seq_args) + elif dft is None: + return None + else: + return pydantic_encoder(dft) + + +_map_types_constraint: Dict[Any, Callable[..., type]] = {int: conint, float: confloat, Decimal: condecimal} + + +def get_annotation_from_field_info( + annotation: Any, field_info: FieldInfo, field_name: str, validate_assignment: bool = False +) -> Type[Any]: + """ + Get an annotation with validation implemented for numbers and strings based on the field_info. + :param annotation: an annotation from a field specification, as ``str``, ``ConstrainedStr`` + :param field_info: an instance of FieldInfo, possibly with declarations for validations and JSON Schema + :param field_name: name of the field for use in error messages + :param validate_assignment: default False, flag for BaseModel Config value of validate_assignment + :return: the same ``annotation`` if unmodified or a new annotation with validation in place + """ + constraints = field_info.get_constraints() + used_constraints: Set[str] = set() + if constraints: + annotation, used_constraints = get_annotation_with_constraints(annotation, field_info) + if validate_assignment: + used_constraints.add('allow_mutation') + + unused_constraints = constraints - used_constraints + if unused_constraints: + raise ValueError( + f'On field "{field_name}" the following field constraints are set but not enforced: ' + f'{", ".join(unused_constraints)}. ' + f'\nFor more details see https://docs.pydantic.dev/usage/schema/#unenforced-field-constraints' + ) + + return annotation + + +def get_annotation_with_constraints(annotation: Any, field_info: FieldInfo) -> Tuple[Type[Any], Set[str]]: # noqa: C901 + """ + Get an annotation with used constraints implemented for numbers and strings based on the field_info. + + :param annotation: an annotation from a field specification, as ``str``, ``ConstrainedStr`` + :param field_info: an instance of FieldInfo, possibly with declarations for validations and JSON Schema + :return: the same ``annotation`` if unmodified or a new annotation along with the used constraints. + """ + used_constraints: Set[str] = set() + + def go(type_: Any) -> Type[Any]: + if ( + is_literal_type(type_) + or isinstance(type_, ForwardRef) + or lenient_issubclass(type_, (ConstrainedList, ConstrainedSet, ConstrainedFrozenSet)) + ): + return type_ + origin = get_origin(type_) + if origin is not None: + args: Tuple[Any, ...] = get_args(type_) + if any(isinstance(a, ForwardRef) for a in args): + # forward refs cause infinite recursion below + return type_ + + if origin is Annotated: + return go(args[0]) + if is_union(origin): + return Union[tuple(go(a) for a in args)] # type: ignore + + if issubclass(origin, List) and ( + field_info.min_items is not None + or field_info.max_items is not None + or field_info.unique_items is not None + ): + used_constraints.update({'min_items', 'max_items', 'unique_items'}) + return conlist( + go(args[0]), + min_items=field_info.min_items, + max_items=field_info.max_items, + unique_items=field_info.unique_items, + ) + + if issubclass(origin, Set) and (field_info.min_items is not None or field_info.max_items is not None): + used_constraints.update({'min_items', 'max_items'}) + return conset(go(args[0]), min_items=field_info.min_items, max_items=field_info.max_items) + + if issubclass(origin, FrozenSet) and (field_info.min_items is not None or field_info.max_items is not None): + used_constraints.update({'min_items', 'max_items'}) + return confrozenset(go(args[0]), min_items=field_info.min_items, max_items=field_info.max_items) + + for t in (Tuple, List, Set, FrozenSet, Sequence): + if issubclass(origin, t): # type: ignore + return t[tuple(go(a) for a in args)] # type: ignore + + if issubclass(origin, Dict): + return Dict[args[0], go(args[1])] # type: ignore + + attrs: Optional[Tuple[str, ...]] = None + constraint_func: Optional[Callable[..., type]] = None + if isinstance(type_, type): + if issubclass(type_, (SecretStr, SecretBytes)): + attrs = ('max_length', 'min_length') + + def constraint_func(**kw: Any) -> Type[Any]: # noqa: F811 + return type(type_.__name__, (type_,), kw) + + elif issubclass(type_, str) and not issubclass(type_, (EmailStr, AnyUrl)): + attrs = ('max_length', 'min_length', 'regex') + if issubclass(type_, StrictStr): + + def constraint_func(**kw: Any) -> Type[Any]: + return type(type_.__name__, (type_,), kw) + + else: + constraint_func = constr + elif issubclass(type_, bytes): + attrs = ('max_length', 'min_length', 'regex') + if issubclass(type_, StrictBytes): + + def constraint_func(**kw: Any) -> Type[Any]: + return type(type_.__name__, (type_,), kw) + + else: + constraint_func = conbytes + elif issubclass(type_, numeric_types) and not issubclass( + type_, + ( + ConstrainedInt, + ConstrainedFloat, + ConstrainedDecimal, + ConstrainedList, + ConstrainedSet, + ConstrainedFrozenSet, + bool, + ), + ): + # Is numeric type + attrs = ('gt', 'lt', 'ge', 'le', 'multiple_of') + if issubclass(type_, float): + attrs += ('allow_inf_nan',) + if issubclass(type_, Decimal): + attrs += ('max_digits', 'decimal_places') + numeric_type = next(t for t in numeric_types if issubclass(type_, t)) # pragma: no branch + constraint_func = _map_types_constraint[numeric_type] + + if attrs: + used_constraints.update(set(attrs)) + kwargs = { + attr_name: attr + for attr_name, attr in ((attr_name, getattr(field_info, attr_name)) for attr_name in attrs) + if attr is not None + } + if kwargs: + constraint_func = cast(Callable[..., type], constraint_func) + return constraint_func(**kwargs) + return type_ + + return go(annotation), used_constraints + + +def normalize_name(name: str) -> str: + """ + Normalizes the given name. This can be applied to either a model *or* enum. + """ + return re.sub(r'[^a-zA-Z0-9.\-_]', '_', name) + + +class SkipField(Exception): + """ + Utility exception used to exclude fields from schema. + """ + + def __init__(self, message: str) -> None: + self.message = message diff --git a/source/target-vendors/cp312/pydantic/v1/tools.py b/source/target-vendors/cp312/pydantic/v1/tools.py new file mode 100644 index 0000000..6838a23 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/tools.py @@ -0,0 +1,92 @@ +import json +from functools import lru_cache +from pathlib import Path +from typing import TYPE_CHECKING, Any, Callable, Optional, Type, TypeVar, Union + +from pydantic.v1.parse import Protocol, load_file, load_str_bytes +from pydantic.v1.types import StrBytes +from pydantic.v1.typing import display_as_type + +__all__ = ('parse_file_as', 'parse_obj_as', 'parse_raw_as', 'schema_of', 'schema_json_of') + +NameFactory = Union[str, Callable[[Type[Any]], str]] + +if TYPE_CHECKING: + from pydantic.v1.typing import DictStrAny + + +def _generate_parsing_type_name(type_: Any) -> str: + return f'ParsingModel[{display_as_type(type_)}]' + + +@lru_cache(maxsize=2048) +def _get_parsing_type(type_: Any, *, type_name: Optional[NameFactory] = None) -> Any: + from pydantic.v1.main import create_model + + if type_name is None: + type_name = _generate_parsing_type_name + if not isinstance(type_name, str): + type_name = type_name(type_) + return create_model(type_name, __root__=(type_, ...)) + + +T = TypeVar('T') + + +def parse_obj_as(type_: Type[T], obj: Any, *, type_name: Optional[NameFactory] = None) -> T: + model_type = _get_parsing_type(type_, type_name=type_name) # type: ignore[arg-type] + return model_type(__root__=obj).__root__ + + +def parse_file_as( + type_: Type[T], + path: Union[str, Path], + *, + content_type: str = None, + encoding: str = 'utf8', + proto: Protocol = None, + allow_pickle: bool = False, + json_loads: Callable[[str], Any] = json.loads, + type_name: Optional[NameFactory] = None, +) -> T: + obj = load_file( + path, + proto=proto, + content_type=content_type, + encoding=encoding, + allow_pickle=allow_pickle, + json_loads=json_loads, + ) + return parse_obj_as(type_, obj, type_name=type_name) + + +def parse_raw_as( + type_: Type[T], + b: StrBytes, + *, + content_type: str = None, + encoding: str = 'utf8', + proto: Protocol = None, + allow_pickle: bool = False, + json_loads: Callable[[str], Any] = json.loads, + type_name: Optional[NameFactory] = None, +) -> T: + obj = load_str_bytes( + b, + proto=proto, + content_type=content_type, + encoding=encoding, + allow_pickle=allow_pickle, + json_loads=json_loads, + ) + return parse_obj_as(type_, obj, type_name=type_name) + + +def schema_of(type_: Any, *, title: Optional[NameFactory] = None, **schema_kwargs: Any) -> 'DictStrAny': + """Generate a JSON schema (as dict) for the passed model or dynamically generated one""" + return _get_parsing_type(type_, type_name=title).schema(**schema_kwargs) + + +def schema_json_of(type_: Any, *, title: Optional[NameFactory] = None, **schema_json_kwargs: Any) -> str: + """Generate a JSON schema (as JSON) for the passed model or dynamically generated one""" + return _get_parsing_type(type_, type_name=title).schema_json(**schema_json_kwargs) diff --git a/source/target-vendors/cp312/pydantic/v1/types.py b/source/target-vendors/cp312/pydantic/v1/types.py new file mode 100644 index 0000000..e1840d9 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/types.py @@ -0,0 +1,1205 @@ +import abc +import math +import re +import warnings +from datetime import date +from decimal import Decimal, InvalidOperation +from enum import Enum +from pathlib import Path +from types import new_class +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Dict, + FrozenSet, + List, + Optional, + Pattern, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, + overload, +) +from uuid import UUID +from weakref import WeakSet + +from pydantic.v1 import errors +from pydantic.v1.datetime_parse import parse_date +from pydantic.v1.utils import import_string, update_not_none +from pydantic.v1.validators import ( + bytes_validator, + constr_length_validator, + constr_lower, + constr_strip_whitespace, + constr_upper, + decimal_validator, + float_finite_validator, + float_validator, + frozenset_validator, + int_validator, + list_validator, + number_multiple_validator, + number_size_validator, + path_exists_validator, + path_validator, + set_validator, + str_validator, + strict_bytes_validator, + strict_float_validator, + strict_int_validator, + strict_str_validator, +) + +__all__ = [ + 'NoneStr', + 'NoneBytes', + 'StrBytes', + 'NoneStrBytes', + 'StrictStr', + 'ConstrainedBytes', + 'conbytes', + 'ConstrainedList', + 'conlist', + 'ConstrainedSet', + 'conset', + 'ConstrainedFrozenSet', + 'confrozenset', + 'ConstrainedStr', + 'constr', + 'PyObject', + 'ConstrainedInt', + 'conint', + 'PositiveInt', + 'NegativeInt', + 'NonNegativeInt', + 'NonPositiveInt', + 'ConstrainedFloat', + 'confloat', + 'PositiveFloat', + 'NegativeFloat', + 'NonNegativeFloat', + 'NonPositiveFloat', + 'FiniteFloat', + 'ConstrainedDecimal', + 'condecimal', + 'UUID1', + 'UUID3', + 'UUID4', + 'UUID5', + 'FilePath', + 'DirectoryPath', + 'Json', + 'JsonWrapper', + 'SecretField', + 'SecretStr', + 'SecretBytes', + 'StrictBool', + 'StrictBytes', + 'StrictInt', + 'StrictFloat', + 'PaymentCardNumber', + 'ByteSize', + 'PastDate', + 'FutureDate', + 'ConstrainedDate', + 'condate', +] + +NoneStr = Optional[str] +NoneBytes = Optional[bytes] +StrBytes = Union[str, bytes] +NoneStrBytes = Optional[StrBytes] +OptionalInt = Optional[int] +OptionalIntFloat = Union[OptionalInt, float] +OptionalIntFloatDecimal = Union[OptionalIntFloat, Decimal] +OptionalDate = Optional[date] +StrIntFloat = Union[str, int, float] + +if TYPE_CHECKING: + from typing_extensions import Annotated + + from pydantic.v1.dataclasses import Dataclass + from pydantic.v1.main import BaseModel + from pydantic.v1.typing import CallableGenerator + + ModelOrDc = Type[Union[BaseModel, Dataclass]] + +T = TypeVar('T') +_DEFINED_TYPES: 'WeakSet[type]' = WeakSet() + + +@overload +def _registered(typ: Type[T]) -> Type[T]: + pass + + +@overload +def _registered(typ: 'ConstrainedNumberMeta') -> 'ConstrainedNumberMeta': + pass + + +def _registered(typ: Union[Type[T], 'ConstrainedNumberMeta']) -> Union[Type[T], 'ConstrainedNumberMeta']: + # In order to generate valid examples of constrained types, Hypothesis needs + # to inspect the type object - so we keep a weakref to each contype object + # until it can be registered. When (or if) our Hypothesis plugin is loaded, + # it monkeypatches this function. + # If Hypothesis is never used, the total effect is to keep a weak reference + # which has minimal memory usage and doesn't even affect garbage collection. + _DEFINED_TYPES.add(typ) + return typ + + +class ConstrainedNumberMeta(type): + def __new__(cls, name: str, bases: Any, dct: Dict[str, Any]) -> 'ConstrainedInt': # type: ignore + new_cls = cast('ConstrainedInt', type.__new__(cls, name, bases, dct)) + + if new_cls.gt is not None and new_cls.ge is not None: + raise errors.ConfigError('bounds gt and ge cannot be specified at the same time') + if new_cls.lt is not None and new_cls.le is not None: + raise errors.ConfigError('bounds lt and le cannot be specified at the same time') + + return _registered(new_cls) # type: ignore + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ BOOLEAN TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +if TYPE_CHECKING: + StrictBool = bool +else: + + class StrictBool(int): + """ + StrictBool to allow for bools which are not type-coerced. + """ + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update(type='boolean') + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield cls.validate + + @classmethod + def validate(cls, value: Any) -> bool: + """ + Ensure that we only allow bools. + """ + if isinstance(value, bool): + return value + + raise errors.StrictBoolError() + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ INTEGER TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +class ConstrainedInt(int, metaclass=ConstrainedNumberMeta): + strict: bool = False + gt: OptionalInt = None + ge: OptionalInt = None + lt: OptionalInt = None + le: OptionalInt = None + multiple_of: OptionalInt = None + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + update_not_none( + field_schema, + exclusiveMinimum=cls.gt, + exclusiveMaximum=cls.lt, + minimum=cls.ge, + maximum=cls.le, + multipleOf=cls.multiple_of, + ) + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield strict_int_validator if cls.strict else int_validator + yield number_size_validator + yield number_multiple_validator + + +def conint( + *, + strict: bool = False, + gt: Optional[int] = None, + ge: Optional[int] = None, + lt: Optional[int] = None, + le: Optional[int] = None, + multiple_of: Optional[int] = None, +) -> Type[int]: + # use kwargs then define conf in a dict to aid with IDE type hinting + namespace = dict(strict=strict, gt=gt, ge=ge, lt=lt, le=le, multiple_of=multiple_of) + return type('ConstrainedIntValue', (ConstrainedInt,), namespace) + + +if TYPE_CHECKING: + PositiveInt = int + NegativeInt = int + NonPositiveInt = int + NonNegativeInt = int + StrictInt = int +else: + + class PositiveInt(ConstrainedInt): + gt = 0 + + class NegativeInt(ConstrainedInt): + lt = 0 + + class NonPositiveInt(ConstrainedInt): + le = 0 + + class NonNegativeInt(ConstrainedInt): + ge = 0 + + class StrictInt(ConstrainedInt): + strict = True + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ FLOAT TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +class ConstrainedFloat(float, metaclass=ConstrainedNumberMeta): + strict: bool = False + gt: OptionalIntFloat = None + ge: OptionalIntFloat = None + lt: OptionalIntFloat = None + le: OptionalIntFloat = None + multiple_of: OptionalIntFloat = None + allow_inf_nan: Optional[bool] = None + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + update_not_none( + field_schema, + exclusiveMinimum=cls.gt, + exclusiveMaximum=cls.lt, + minimum=cls.ge, + maximum=cls.le, + multipleOf=cls.multiple_of, + ) + # Modify constraints to account for differences between IEEE floats and JSON + if field_schema.get('exclusiveMinimum') == -math.inf: + del field_schema['exclusiveMinimum'] + if field_schema.get('minimum') == -math.inf: + del field_schema['minimum'] + if field_schema.get('exclusiveMaximum') == math.inf: + del field_schema['exclusiveMaximum'] + if field_schema.get('maximum') == math.inf: + del field_schema['maximum'] + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield strict_float_validator if cls.strict else float_validator + yield number_size_validator + yield number_multiple_validator + yield float_finite_validator + + +def confloat( + *, + strict: bool = False, + gt: float = None, + ge: float = None, + lt: float = None, + le: float = None, + multiple_of: float = None, + allow_inf_nan: Optional[bool] = None, +) -> Type[float]: + # use kwargs then define conf in a dict to aid with IDE type hinting + namespace = dict(strict=strict, gt=gt, ge=ge, lt=lt, le=le, multiple_of=multiple_of, allow_inf_nan=allow_inf_nan) + return type('ConstrainedFloatValue', (ConstrainedFloat,), namespace) + + +if TYPE_CHECKING: + PositiveFloat = float + NegativeFloat = float + NonPositiveFloat = float + NonNegativeFloat = float + StrictFloat = float + FiniteFloat = float +else: + + class PositiveFloat(ConstrainedFloat): + gt = 0 + + class NegativeFloat(ConstrainedFloat): + lt = 0 + + class NonPositiveFloat(ConstrainedFloat): + le = 0 + + class NonNegativeFloat(ConstrainedFloat): + ge = 0 + + class StrictFloat(ConstrainedFloat): + strict = True + + class FiniteFloat(ConstrainedFloat): + allow_inf_nan = False + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ BYTES TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +class ConstrainedBytes(bytes): + strip_whitespace = False + to_upper = False + to_lower = False + min_length: OptionalInt = None + max_length: OptionalInt = None + strict: bool = False + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + update_not_none(field_schema, minLength=cls.min_length, maxLength=cls.max_length) + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield strict_bytes_validator if cls.strict else bytes_validator + yield constr_strip_whitespace + yield constr_upper + yield constr_lower + yield constr_length_validator + + +def conbytes( + *, + strip_whitespace: bool = False, + to_upper: bool = False, + to_lower: bool = False, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + strict: bool = False, +) -> Type[bytes]: + # use kwargs then define conf in a dict to aid with IDE type hinting + namespace = dict( + strip_whitespace=strip_whitespace, + to_upper=to_upper, + to_lower=to_lower, + min_length=min_length, + max_length=max_length, + strict=strict, + ) + return _registered(type('ConstrainedBytesValue', (ConstrainedBytes,), namespace)) + + +if TYPE_CHECKING: + StrictBytes = bytes +else: + + class StrictBytes(ConstrainedBytes): + strict = True + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ STRING TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +class ConstrainedStr(str): + strip_whitespace = False + to_upper = False + to_lower = False + min_length: OptionalInt = None + max_length: OptionalInt = None + curtail_length: OptionalInt = None + regex: Optional[Union[str, Pattern[str]]] = None + strict = False + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + update_not_none( + field_schema, + minLength=cls.min_length, + maxLength=cls.max_length, + pattern=cls.regex and cls._get_pattern(cls.regex), + ) + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield strict_str_validator if cls.strict else str_validator + yield constr_strip_whitespace + yield constr_upper + yield constr_lower + yield constr_length_validator + yield cls.validate + + @classmethod + def validate(cls, value: Union[str]) -> Union[str]: + if cls.curtail_length and len(value) > cls.curtail_length: + value = value[: cls.curtail_length] + + if cls.regex: + if not re.match(cls.regex, value): + raise errors.StrRegexError(pattern=cls._get_pattern(cls.regex)) + + return value + + @staticmethod + def _get_pattern(regex: Union[str, Pattern[str]]) -> str: + return regex if isinstance(regex, str) else regex.pattern + + +def constr( + *, + strip_whitespace: bool = False, + to_upper: bool = False, + to_lower: bool = False, + strict: bool = False, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + curtail_length: Optional[int] = None, + regex: Optional[str] = None, +) -> Type[str]: + # use kwargs then define conf in a dict to aid with IDE type hinting + namespace = dict( + strip_whitespace=strip_whitespace, + to_upper=to_upper, + to_lower=to_lower, + strict=strict, + min_length=min_length, + max_length=max_length, + curtail_length=curtail_length, + regex=regex and re.compile(regex), + ) + return _registered(type('ConstrainedStrValue', (ConstrainedStr,), namespace)) + + +if TYPE_CHECKING: + StrictStr = str +else: + + class StrictStr(ConstrainedStr): + strict = True + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SET TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +# This types superclass should be Set[T], but cython chokes on that... +class ConstrainedSet(set): # type: ignore + # Needed for pydantic to detect that this is a set + __origin__ = set + __args__: Set[Type[T]] # type: ignore + + min_items: Optional[int] = None + max_items: Optional[int] = None + item_type: Type[T] # type: ignore + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield cls.set_length_validator + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + update_not_none(field_schema, minItems=cls.min_items, maxItems=cls.max_items) + + @classmethod + def set_length_validator(cls, v: 'Optional[Set[T]]') -> 'Optional[Set[T]]': + if v is None: + return None + + v = set_validator(v) + v_len = len(v) + + if cls.min_items is not None and v_len < cls.min_items: + raise errors.SetMinLengthError(limit_value=cls.min_items) + + if cls.max_items is not None and v_len > cls.max_items: + raise errors.SetMaxLengthError(limit_value=cls.max_items) + + return v + + +def conset(item_type: Type[T], *, min_items: Optional[int] = None, max_items: Optional[int] = None) -> Type[Set[T]]: + # __args__ is needed to conform to typing generics api + namespace = {'min_items': min_items, 'max_items': max_items, 'item_type': item_type, '__args__': [item_type]} + # We use new_class to be able to deal with Generic types + return new_class('ConstrainedSetValue', (ConstrainedSet,), {}, lambda ns: ns.update(namespace)) + + +# This types superclass should be FrozenSet[T], but cython chokes on that... +class ConstrainedFrozenSet(frozenset): # type: ignore + # Needed for pydantic to detect that this is a set + __origin__ = frozenset + __args__: FrozenSet[Type[T]] # type: ignore + + min_items: Optional[int] = None + max_items: Optional[int] = None + item_type: Type[T] # type: ignore + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield cls.frozenset_length_validator + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + update_not_none(field_schema, minItems=cls.min_items, maxItems=cls.max_items) + + @classmethod + def frozenset_length_validator(cls, v: 'Optional[FrozenSet[T]]') -> 'Optional[FrozenSet[T]]': + if v is None: + return None + + v = frozenset_validator(v) + v_len = len(v) + + if cls.min_items is not None and v_len < cls.min_items: + raise errors.FrozenSetMinLengthError(limit_value=cls.min_items) + + if cls.max_items is not None and v_len > cls.max_items: + raise errors.FrozenSetMaxLengthError(limit_value=cls.max_items) + + return v + + +def confrozenset( + item_type: Type[T], *, min_items: Optional[int] = None, max_items: Optional[int] = None +) -> Type[FrozenSet[T]]: + # __args__ is needed to conform to typing generics api + namespace = {'min_items': min_items, 'max_items': max_items, 'item_type': item_type, '__args__': [item_type]} + # We use new_class to be able to deal with Generic types + return new_class('ConstrainedFrozenSetValue', (ConstrainedFrozenSet,), {}, lambda ns: ns.update(namespace)) + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LIST TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +# This types superclass should be List[T], but cython chokes on that... +class ConstrainedList(list): # type: ignore + # Needed for pydantic to detect that this is a list + __origin__ = list + __args__: Tuple[Type[T], ...] # type: ignore + + min_items: Optional[int] = None + max_items: Optional[int] = None + unique_items: Optional[bool] = None + item_type: Type[T] # type: ignore + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield cls.list_length_validator + if cls.unique_items: + yield cls.unique_items_validator + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + update_not_none(field_schema, minItems=cls.min_items, maxItems=cls.max_items, uniqueItems=cls.unique_items) + + @classmethod + def list_length_validator(cls, v: 'Optional[List[T]]') -> 'Optional[List[T]]': + if v is None: + return None + + v = list_validator(v) + v_len = len(v) + + if cls.min_items is not None and v_len < cls.min_items: + raise errors.ListMinLengthError(limit_value=cls.min_items) + + if cls.max_items is not None and v_len > cls.max_items: + raise errors.ListMaxLengthError(limit_value=cls.max_items) + + return v + + @classmethod + def unique_items_validator(cls, v: 'Optional[List[T]]') -> 'Optional[List[T]]': + if v is None: + return None + + for i, value in enumerate(v, start=1): + if value in v[i:]: + raise errors.ListUniqueItemsError() + + return v + + +def conlist( + item_type: Type[T], *, min_items: Optional[int] = None, max_items: Optional[int] = None, unique_items: bool = None +) -> Type[List[T]]: + # __args__ is needed to conform to typing generics api + namespace = dict( + min_items=min_items, max_items=max_items, unique_items=unique_items, item_type=item_type, __args__=(item_type,) + ) + # We use new_class to be able to deal with Generic types + return new_class('ConstrainedListValue', (ConstrainedList,), {}, lambda ns: ns.update(namespace)) + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PYOBJECT TYPE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +if TYPE_CHECKING: + PyObject = Callable[..., Any] +else: + + class PyObject: + validate_always = True + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield cls.validate + + @classmethod + def validate(cls, value: Any) -> Any: + if isinstance(value, Callable): + return value + + try: + value = str_validator(value) + except errors.StrError: + raise errors.PyObjectError(error_message='value is neither a valid import path not a valid callable') + + try: + return import_string(value) + except ImportError as e: + raise errors.PyObjectError(error_message=str(e)) + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DECIMAL TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +class ConstrainedDecimal(Decimal, metaclass=ConstrainedNumberMeta): + gt: OptionalIntFloatDecimal = None + ge: OptionalIntFloatDecimal = None + lt: OptionalIntFloatDecimal = None + le: OptionalIntFloatDecimal = None + max_digits: OptionalInt = None + decimal_places: OptionalInt = None + multiple_of: OptionalIntFloatDecimal = None + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + update_not_none( + field_schema, + exclusiveMinimum=cls.gt, + exclusiveMaximum=cls.lt, + minimum=cls.ge, + maximum=cls.le, + multipleOf=cls.multiple_of, + ) + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield decimal_validator + yield number_size_validator + yield number_multiple_validator + yield cls.validate + + @classmethod + def validate(cls, value: Decimal) -> Decimal: + try: + normalized_value = value.normalize() + except InvalidOperation: + normalized_value = value + digit_tuple, exponent = normalized_value.as_tuple()[1:] + if exponent in {'F', 'n', 'N'}: + raise errors.DecimalIsNotFiniteError() + + if exponent >= 0: + # A positive exponent adds that many trailing zeros. + digits = len(digit_tuple) + exponent + decimals = 0 + else: + # If the absolute value of the negative exponent is larger than the + # number of digits, then it's the same as the number of digits, + # because it'll consume all of the digits in digit_tuple and then + # add abs(exponent) - len(digit_tuple) leading zeros after the + # decimal point. + if abs(exponent) > len(digit_tuple): + digits = decimals = abs(exponent) + else: + digits = len(digit_tuple) + decimals = abs(exponent) + whole_digits = digits - decimals + + if cls.max_digits is not None and digits > cls.max_digits: + raise errors.DecimalMaxDigitsError(max_digits=cls.max_digits) + + if cls.decimal_places is not None and decimals > cls.decimal_places: + raise errors.DecimalMaxPlacesError(decimal_places=cls.decimal_places) + + if cls.max_digits is not None and cls.decimal_places is not None: + expected = cls.max_digits - cls.decimal_places + if whole_digits > expected: + raise errors.DecimalWholeDigitsError(whole_digits=expected) + + return value + + +def condecimal( + *, + gt: Decimal = None, + ge: Decimal = None, + lt: Decimal = None, + le: Decimal = None, + max_digits: Optional[int] = None, + decimal_places: Optional[int] = None, + multiple_of: Decimal = None, +) -> Type[Decimal]: + # use kwargs then define conf in a dict to aid with IDE type hinting + namespace = dict( + gt=gt, ge=ge, lt=lt, le=le, max_digits=max_digits, decimal_places=decimal_places, multiple_of=multiple_of + ) + return type('ConstrainedDecimalValue', (ConstrainedDecimal,), namespace) + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ UUID TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +if TYPE_CHECKING: + UUID1 = UUID + UUID3 = UUID + UUID4 = UUID + UUID5 = UUID +else: + + class UUID1(UUID): + _required_version = 1 + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update(type='string', format=f'uuid{cls._required_version}') + + class UUID3(UUID1): + _required_version = 3 + + class UUID4(UUID1): + _required_version = 4 + + class UUID5(UUID1): + _required_version = 5 + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PATH TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +if TYPE_CHECKING: + FilePath = Path + DirectoryPath = Path +else: + + class FilePath(Path): + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update(format='file-path') + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield path_validator + yield path_exists_validator + yield cls.validate + + @classmethod + def validate(cls, value: Path) -> Path: + if not value.is_file(): + raise errors.PathNotAFileError(path=value) + + return value + + class DirectoryPath(Path): + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update(format='directory-path') + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield path_validator + yield path_exists_validator + yield cls.validate + + @classmethod + def validate(cls, value: Path) -> Path: + if not value.is_dir(): + raise errors.PathNotADirectoryError(path=value) + + return value + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON TYPE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +class JsonWrapper: + pass + + +class JsonMeta(type): + def __getitem__(self, t: Type[Any]) -> Type[JsonWrapper]: + if t is Any: + return Json # allow Json[Any] to replicate plain Json + return _registered(type('JsonWrapperValue', (JsonWrapper,), {'inner_type': t})) + + +if TYPE_CHECKING: + Json = Annotated[T, ...] # Json[list[str]] will be recognized by type checkers as list[str] + +else: + + class Json(metaclass=JsonMeta): + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update(type='string', format='json-string') + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SECRET TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +class SecretField(abc.ABC): + """ + Note: this should be implemented as a generic like `SecretField(ABC, Generic[T])`, + the `__init__()` should be part of the abstract class and the + `get_secret_value()` method should use the generic `T` type. + + However Cython doesn't support very well generics at the moment and + the generated code fails to be imported (see + https://github.com/cython/cython/issues/2753). + """ + + def __eq__(self, other: Any) -> bool: + return isinstance(other, self.__class__) and self.get_secret_value() == other.get_secret_value() + + def __str__(self) -> str: + return '**********' if self.get_secret_value() else '' + + def __hash__(self) -> int: + return hash(self.get_secret_value()) + + @abc.abstractmethod + def get_secret_value(self) -> Any: # pragma: no cover + ... + + +class SecretStr(SecretField): + min_length: OptionalInt = None + max_length: OptionalInt = None + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + update_not_none( + field_schema, + type='string', + writeOnly=True, + format='password', + minLength=cls.min_length, + maxLength=cls.max_length, + ) + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield cls.validate + yield constr_length_validator + + @classmethod + def validate(cls, value: Any) -> 'SecretStr': + if isinstance(value, cls): + return value + value = str_validator(value) + return cls(value) + + def __init__(self, value: str): + self._secret_value = value + + def __repr__(self) -> str: + return f"SecretStr('{self}')" + + def __len__(self) -> int: + return len(self._secret_value) + + def display(self) -> str: + warnings.warn('`secret_str.display()` is deprecated, use `str(secret_str)` instead', DeprecationWarning) + return str(self) + + def get_secret_value(self) -> str: + return self._secret_value + + +class SecretBytes(SecretField): + min_length: OptionalInt = None + max_length: OptionalInt = None + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + update_not_none( + field_schema, + type='string', + writeOnly=True, + format='password', + minLength=cls.min_length, + maxLength=cls.max_length, + ) + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield cls.validate + yield constr_length_validator + + @classmethod + def validate(cls, value: Any) -> 'SecretBytes': + if isinstance(value, cls): + return value + value = bytes_validator(value) + return cls(value) + + def __init__(self, value: bytes): + self._secret_value = value + + def __repr__(self) -> str: + return f"SecretBytes(b'{self}')" + + def __len__(self) -> int: + return len(self._secret_value) + + def display(self) -> str: + warnings.warn('`secret_bytes.display()` is deprecated, use `str(secret_bytes)` instead', DeprecationWarning) + return str(self) + + def get_secret_value(self) -> bytes: + return self._secret_value + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PAYMENT CARD TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +class PaymentCardBrand(str, Enum): + # If you add another card type, please also add it to the + # Hypothesis strategy in `pydantic._hypothesis_plugin`. + amex = 'American Express' + mastercard = 'Mastercard' + visa = 'Visa' + other = 'other' + + def __str__(self) -> str: + return self.value + + +class PaymentCardNumber(str): + """ + Based on: https://en.wikipedia.org/wiki/Payment_card_number + """ + + strip_whitespace: ClassVar[bool] = True + min_length: ClassVar[int] = 12 + max_length: ClassVar[int] = 19 + bin: str + last4: str + brand: PaymentCardBrand + + def __init__(self, card_number: str): + self.bin = card_number[:6] + self.last4 = card_number[-4:] + self.brand = self._get_brand(card_number) + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield str_validator + yield constr_strip_whitespace + yield constr_length_validator + yield cls.validate_digits + yield cls.validate_luhn_check_digit + yield cls + yield cls.validate_length_for_brand + + @property + def masked(self) -> str: + num_masked = len(self) - 10 # len(bin) + len(last4) == 10 + return f'{self.bin}{"*" * num_masked}{self.last4}' + + @classmethod + def validate_digits(cls, card_number: str) -> str: + if not card_number.isdigit(): + raise errors.NotDigitError + return card_number + + @classmethod + def validate_luhn_check_digit(cls, card_number: str) -> str: + """ + Based on: https://en.wikipedia.org/wiki/Luhn_algorithm + """ + sum_ = int(card_number[-1]) + length = len(card_number) + parity = length % 2 + for i in range(length - 1): + digit = int(card_number[i]) + if i % 2 == parity: + digit *= 2 + if digit > 9: + digit -= 9 + sum_ += digit + valid = sum_ % 10 == 0 + if not valid: + raise errors.LuhnValidationError + return card_number + + @classmethod + def validate_length_for_brand(cls, card_number: 'PaymentCardNumber') -> 'PaymentCardNumber': + """ + Validate length based on BIN for major brands: + https://en.wikipedia.org/wiki/Payment_card_number#Issuer_identification_number_(IIN) + """ + required_length: Union[None, int, str] = None + if card_number.brand in PaymentCardBrand.mastercard: + required_length = 16 + valid = len(card_number) == required_length + elif card_number.brand == PaymentCardBrand.visa: + required_length = '13, 16 or 19' + valid = len(card_number) in {13, 16, 19} + elif card_number.brand == PaymentCardBrand.amex: + required_length = 15 + valid = len(card_number) == required_length + else: + valid = True + if not valid: + raise errors.InvalidLengthForBrand(brand=card_number.brand, required_length=required_length) + return card_number + + @staticmethod + def _get_brand(card_number: str) -> PaymentCardBrand: + if card_number[0] == '4': + brand = PaymentCardBrand.visa + elif 51 <= int(card_number[:2]) <= 55: + brand = PaymentCardBrand.mastercard + elif card_number[:2] in {'34', '37'}: + brand = PaymentCardBrand.amex + else: + brand = PaymentCardBrand.other + return brand + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ BYTE SIZE TYPE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +BYTE_SIZES = { + 'b': 1, + 'kb': 10**3, + 'mb': 10**6, + 'gb': 10**9, + 'tb': 10**12, + 'pb': 10**15, + 'eb': 10**18, + 'kib': 2**10, + 'mib': 2**20, + 'gib': 2**30, + 'tib': 2**40, + 'pib': 2**50, + 'eib': 2**60, +} +BYTE_SIZES.update({k.lower()[0]: v for k, v in BYTE_SIZES.items() if 'i' not in k}) +byte_string_re = re.compile(r'^\s*(\d*\.?\d+)\s*(\w+)?', re.IGNORECASE) + + +class ByteSize(int): + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield cls.validate + + @classmethod + def validate(cls, v: StrIntFloat) -> 'ByteSize': + try: + return cls(int(v)) + except ValueError: + pass + + str_match = byte_string_re.match(str(v)) + if str_match is None: + raise errors.InvalidByteSize() + + scalar, unit = str_match.groups() + if unit is None: + unit = 'b' + + try: + unit_mult = BYTE_SIZES[unit.lower()] + except KeyError: + raise errors.InvalidByteSizeUnit(unit=unit) + + return cls(int(float(scalar) * unit_mult)) + + def human_readable(self, decimal: bool = False) -> str: + if decimal: + divisor = 1000 + units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] + final_unit = 'EB' + else: + divisor = 1024 + units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'] + final_unit = 'EiB' + + num = float(self) + for unit in units: + if abs(num) < divisor: + return f'{num:0.1f}{unit}' + num /= divisor + + return f'{num:0.1f}{final_unit}' + + def to(self, unit: str) -> float: + try: + unit_div = BYTE_SIZES[unit.lower()] + except KeyError: + raise errors.InvalidByteSizeUnit(unit=unit) + + return self / unit_div + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DATE TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +if TYPE_CHECKING: + PastDate = date + FutureDate = date +else: + + class PastDate(date): + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield parse_date + yield cls.validate + + @classmethod + def validate(cls, value: date) -> date: + if value >= date.today(): + raise errors.DateNotInThePastError() + + return value + + class FutureDate(date): + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield parse_date + yield cls.validate + + @classmethod + def validate(cls, value: date) -> date: + if value <= date.today(): + raise errors.DateNotInTheFutureError() + + return value + + +class ConstrainedDate(date, metaclass=ConstrainedNumberMeta): + gt: OptionalDate = None + ge: OptionalDate = None + lt: OptionalDate = None + le: OptionalDate = None + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + update_not_none(field_schema, exclusiveMinimum=cls.gt, exclusiveMaximum=cls.lt, minimum=cls.ge, maximum=cls.le) + + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield parse_date + yield number_size_validator + + +def condate( + *, + gt: date = None, + ge: date = None, + lt: date = None, + le: date = None, +) -> Type[date]: + # use kwargs then define conf in a dict to aid with IDE type hinting + namespace = dict(gt=gt, ge=ge, lt=lt, le=le) + return type('ConstrainedDateValue', (ConstrainedDate,), namespace) diff --git a/source/target-vendors/cp312/pydantic/v1/typing.py b/source/target-vendors/cp312/pydantic/v1/typing.py new file mode 100644 index 0000000..c5c5979 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/typing.py @@ -0,0 +1,627 @@ +import functools +import operator +import sys +import typing +from collections.abc import Callable +from os import PathLike +from typing import ( # type: ignore + TYPE_CHECKING, + AbstractSet, + Any, + Callable as TypingCallable, + ClassVar, + Dict, + ForwardRef, + Generator, + Iterable, + List, + Mapping, + NewType, + Optional, + Sequence, + Set, + Tuple, + Type, + TypeVar, + Union, + _eval_type, + cast, + get_type_hints, +) + +from typing_extensions import ( + Annotated, + Final, + Literal, + NotRequired as TypedDictNotRequired, + Required as TypedDictRequired, +) + +try: + from typing import _TypingBase as typing_base # type: ignore +except ImportError: + from typing import _Final as typing_base # type: ignore + +try: + from typing import GenericAlias as TypingGenericAlias # type: ignore +except ImportError: + # python < 3.9 does not have GenericAlias (list[int], tuple[str, ...] and so on) + TypingGenericAlias = () + +try: + from types import UnionType as TypesUnionType # type: ignore +except ImportError: + # python < 3.10 does not have UnionType (str | int, byte | bool and so on) + TypesUnionType = () + + +if sys.version_info < (3, 9): + + def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any: + return type_._evaluate(globalns, localns) + +elif sys.version_info < (3, 12, 4): + + def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any: + # Even though it is the right signature for python 3.9, mypy complains with + # `error: Too many arguments for "_evaluate" of "ForwardRef"` hence the cast... + # Python 3.13/3.12.4+ made `recursive_guard` a kwarg, so name it explicitly to avoid: + # TypeError: ForwardRef._evaluate() missing 1 required keyword-only argument: 'recursive_guard' + return cast(Any, type_)._evaluate(globalns, localns, recursive_guard=set()) + +elif sys.version_info < (3, 14): + + def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any: + # Pydantic 1.x will not support PEP 695 syntax, but provide `type_params` to avoid + # warnings: + return cast(Any, type_)._evaluate(globalns, localns, type_params=(), recursive_guard=set()) + +else: + + def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any: + # Pydantic 1.x will not support PEP 695 syntax, but provide `type_params` to avoid + # warnings: + return typing.evaluate_forward_ref( + type_, + globals=globalns, + locals=localns, + type_params=(), + _recursive_guard=set(), + ) + + +if sys.version_info < (3, 9): + # Ensure we always get all the whole `Annotated` hint, not just the annotated type. + # For 3.7 to 3.8, `get_type_hints` doesn't recognize `typing_extensions.Annotated`, + # so it already returns the full annotation + get_all_type_hints = get_type_hints + +else: + + def get_all_type_hints(obj: Any, globalns: Any = None, localns: Any = None) -> Any: + return get_type_hints(obj, globalns, localns, include_extras=True) + + +_T = TypeVar('_T') + +AnyCallable = TypingCallable[..., Any] +NoArgAnyCallable = TypingCallable[[], Any] + +# workaround for https://github.com/python/mypy/issues/9496 +AnyArgTCallable = TypingCallable[..., _T] + + +# Annotated[...] is implemented by returning an instance of one of these classes, depending on +# python/typing_extensions version. +AnnotatedTypeNames = {'AnnotatedMeta', '_AnnotatedAlias'} + + +LITERAL_TYPES: Set[Any] = {Literal} +if hasattr(typing, 'Literal'): + LITERAL_TYPES.add(typing.Literal) + + +if sys.version_info < (3, 8): + + def get_origin(t: Type[Any]) -> Optional[Type[Any]]: + if type(t).__name__ in AnnotatedTypeNames: + # weirdly this is a runtime requirement, as well as for mypy + return cast(Type[Any], Annotated) + return getattr(t, '__origin__', None) + +else: + from typing import get_origin as _typing_get_origin + + def get_origin(tp: Type[Any]) -> Optional[Type[Any]]: + """ + We can't directly use `typing.get_origin` since we need a fallback to support + custom generic classes like `ConstrainedList` + It should be useless once https://github.com/cython/cython/issues/3537 is + solved and https://github.com/pydantic/pydantic/pull/1753 is merged. + """ + if type(tp).__name__ in AnnotatedTypeNames: + return cast(Type[Any], Annotated) # mypy complains about _SpecialForm + return _typing_get_origin(tp) or getattr(tp, '__origin__', None) + + +if sys.version_info < (3, 8): + from typing import _GenericAlias + + def get_args(t: Type[Any]) -> Tuple[Any, ...]: + """Compatibility version of get_args for python 3.7. + + Mostly compatible with the python 3.8 `typing` module version + and able to handle almost all use cases. + """ + if type(t).__name__ in AnnotatedTypeNames: + return t.__args__ + t.__metadata__ + if isinstance(t, _GenericAlias): + res = t.__args__ + if t.__origin__ is Callable and res and res[0] is not Ellipsis: + res = (list(res[:-1]), res[-1]) + return res + return getattr(t, '__args__', ()) + +else: + from typing import get_args as _typing_get_args + + def _generic_get_args(tp: Type[Any]) -> Tuple[Any, ...]: + """ + In python 3.9, `typing.Dict`, `typing.List`, ... + do have an empty `__args__` by default (instead of the generic ~T for example). + In order to still support `Dict` for example and consider it as `Dict[Any, Any]`, + we retrieve the `_nparams` value that tells us how many parameters it needs. + """ + if hasattr(tp, '_nparams'): + return (Any,) * tp._nparams + # Special case for `tuple[()]`, which used to return ((),) with `typing.Tuple` + # in python 3.10- but now returns () for `tuple` and `Tuple`. + # This will probably be clarified in pydantic v2 + try: + if tp == Tuple[()] or sys.version_info >= (3, 9) and tp == tuple[()]: # type: ignore[misc] + return ((),) + # there is a TypeError when compiled with cython + except TypeError: # pragma: no cover + pass + return () + + def get_args(tp: Type[Any]) -> Tuple[Any, ...]: + """Get type arguments with all substitutions performed. + + For unions, basic simplifications used by Union constructor are performed. + Examples:: + get_args(Dict[str, int]) == (str, int) + get_args(int) == () + get_args(Union[int, Union[T, int], str][int]) == (int, str) + get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int]) + get_args(Callable[[], T][int]) == ([], int) + """ + if type(tp).__name__ in AnnotatedTypeNames: + return tp.__args__ + tp.__metadata__ + # the fallback is needed for the same reasons as `get_origin` (see above) + return _typing_get_args(tp) or getattr(tp, '__args__', ()) or _generic_get_args(tp) + + +if sys.version_info < (3, 9): + + def convert_generics(tp: Type[Any]) -> Type[Any]: + """Python 3.9 and older only supports generics from `typing` module. + They convert strings to ForwardRef automatically. + + Examples:: + typing.List['Hero'] == typing.List[ForwardRef('Hero')] + """ + return tp + +else: + + def convert_generics(tp: Type[Any]) -> Type[Any]: + """ + Recursively searches for `str` type hints and replaces them with ForwardRef. + + Examples:: + convert_generics(list['Hero']) == list[ForwardRef('Hero')] + convert_generics(dict['Hero', 'Team']) == dict[ForwardRef('Hero'), ForwardRef('Team')] + convert_generics(typing.Dict['Hero', 'Team']) == typing.Dict[ForwardRef('Hero'), ForwardRef('Team')] + convert_generics(list[str | 'Hero'] | int) == list[str | ForwardRef('Hero')] | int + """ + origin = get_origin(tp) + if not origin or not hasattr(tp, '__args__'): + return tp + + args = get_args(tp) + + # typing.Annotated needs special treatment + if origin is Annotated: + return Annotated[(convert_generics(args[0]), *args[1:])] # type: ignore + + # recursively replace `str` instances inside of `GenericAlias` with `ForwardRef(arg)` + converted = tuple( + ForwardRef(arg) if isinstance(arg, str) and isinstance(tp, TypingGenericAlias) else convert_generics(arg) + for arg in args + ) + + if converted == args: + return tp + elif isinstance(tp, TypingGenericAlias): + return TypingGenericAlias(origin, converted) + elif isinstance(tp, TypesUnionType): + # recreate types.UnionType (PEP604, Python >= 3.10) + return functools.reduce(operator.or_, converted) # type: ignore + else: + try: + setattr(tp, '__args__', converted) + except AttributeError: + pass + return tp + + +if sys.version_info < (3, 10): + + def is_union(tp: Optional[Type[Any]]) -> bool: + return tp is Union + + WithArgsTypes = (TypingGenericAlias,) + +else: + import types + import typing + + def is_union(tp: Optional[Type[Any]]) -> bool: + return tp is Union or tp is types.UnionType # noqa: E721 + + WithArgsTypes = (typing._GenericAlias, types.GenericAlias, types.UnionType) + + +StrPath = Union[str, PathLike] + + +if TYPE_CHECKING: + from pydantic.v1.fields import ModelField + + TupleGenerator = Generator[Tuple[str, Any], None, None] + DictStrAny = Dict[str, Any] + DictAny = Dict[Any, Any] + SetStr = Set[str] + ListStr = List[str] + IntStr = Union[int, str] + AbstractSetIntStr = AbstractSet[IntStr] + DictIntStrAny = Dict[IntStr, Any] + MappingIntStrAny = Mapping[IntStr, Any] + CallableGenerator = Generator[AnyCallable, None, None] + ReprArgs = Sequence[Tuple[Optional[str], Any]] + + MYPY = False + if MYPY: + AnyClassMethod = classmethod[Any] + else: + # classmethod[TargetType, CallableParamSpecType, CallableReturnType] + AnyClassMethod = classmethod[Any, Any, Any] + +__all__ = ( + 'AnyCallable', + 'NoArgAnyCallable', + 'NoneType', + 'is_none_type', + 'display_as_type', + 'resolve_annotations', + 'is_callable_type', + 'is_literal_type', + 'all_literal_values', + 'is_namedtuple', + 'is_typeddict', + 'is_typeddict_special', + 'is_new_type', + 'new_type_supertype', + 'is_classvar', + 'is_finalvar', + 'update_field_forward_refs', + 'update_model_forward_refs', + 'TupleGenerator', + 'DictStrAny', + 'DictAny', + 'SetStr', + 'ListStr', + 'IntStr', + 'AbstractSetIntStr', + 'DictIntStrAny', + 'CallableGenerator', + 'ReprArgs', + 'AnyClassMethod', + 'CallableGenerator', + 'WithArgsTypes', + 'get_args', + 'get_origin', + 'get_sub_types', + 'typing_base', + 'get_all_type_hints', + 'is_union', + 'StrPath', + 'MappingIntStrAny', +) + + +NoneType = None.__class__ + + +NONE_TYPES: Tuple[Any, Any, Any] = (None, NoneType, Literal[None]) + + +if sys.version_info < (3, 8): + # Even though this implementation is slower, we need it for python 3.7: + # In python 3.7 "Literal" is not a builtin type and uses a different + # mechanism. + # for this reason `Literal[None] is Literal[None]` evaluates to `False`, + # breaking the faster implementation used for the other python versions. + + def is_none_type(type_: Any) -> bool: + return type_ in NONE_TYPES + +elif sys.version_info[:2] == (3, 8): + + def is_none_type(type_: Any) -> bool: + for none_type in NONE_TYPES: + if type_ is none_type: + return True + # With python 3.8, specifically 3.8.10, Literal "is" check sare very flakey + # can change on very subtle changes like use of types in other modules, + # hopefully this check avoids that issue. + if is_literal_type(type_): # pragma: no cover + return all_literal_values(type_) == (None,) + return False + +else: + + def is_none_type(type_: Any) -> bool: + return type_ in NONE_TYPES + + +def display_as_type(v: Type[Any]) -> str: + if not isinstance(v, typing_base) and not isinstance(v, WithArgsTypes) and not isinstance(v, type): + v = v.__class__ + + if is_union(get_origin(v)): + return f'Union[{", ".join(map(display_as_type, get_args(v)))}]' + + if isinstance(v, WithArgsTypes): + # Generic alias are constructs like `list[int]` + return str(v).replace('typing.', '') + + try: + return v.__name__ + except AttributeError: + # happens with typing objects + return str(v).replace('typing.', '') + + +def resolve_annotations(raw_annotations: Dict[str, Type[Any]], module_name: Optional[str]) -> Dict[str, Type[Any]]: + """ + Partially taken from typing.get_type_hints. + + Resolve string or ForwardRef annotations into type objects if possible. + """ + base_globals: Optional[Dict[str, Any]] = None + if module_name: + try: + module = sys.modules[module_name] + except KeyError: + # happens occasionally, see https://github.com/pydantic/pydantic/issues/2363 + pass + else: + base_globals = module.__dict__ + + annotations = {} + for name, value in raw_annotations.items(): + if isinstance(value, str): + if (3, 10) > sys.version_info >= (3, 9, 8) or sys.version_info >= (3, 10, 1): + value = ForwardRef(value, is_argument=False, is_class=True) + else: + value = ForwardRef(value, is_argument=False) + try: + if sys.version_info >= (3, 13): + value = _eval_type(value, base_globals, None, type_params=()) + else: + value = _eval_type(value, base_globals, None) + except NameError: + # this is ok, it can be fixed with update_forward_refs + pass + annotations[name] = value + return annotations + + +def is_callable_type(type_: Type[Any]) -> bool: + return type_ is Callable or get_origin(type_) is Callable + + +def is_literal_type(type_: Type[Any]) -> bool: + return Literal is not None and get_origin(type_) in LITERAL_TYPES + + +def literal_values(type_: Type[Any]) -> Tuple[Any, ...]: + return get_args(type_) + + +def all_literal_values(type_: Type[Any]) -> Tuple[Any, ...]: + """ + This method is used to retrieve all Literal values as + Literal can be used recursively (see https://www.python.org/dev/peps/pep-0586) + e.g. `Literal[Literal[Literal[1, 2, 3], "foo"], 5, None]` + """ + if not is_literal_type(type_): + return (type_,) + + values = literal_values(type_) + return tuple(x for value in values for x in all_literal_values(value)) + + +def is_namedtuple(type_: Type[Any]) -> bool: + """ + Check if a given class is a named tuple. + It can be either a `typing.NamedTuple` or `collections.namedtuple` + """ + from pydantic.v1.utils import lenient_issubclass + + return lenient_issubclass(type_, tuple) and hasattr(type_, '_fields') + + +def is_typeddict(type_: Type[Any]) -> bool: + """ + Check if a given class is a typed dict (from `typing` or `typing_extensions`) + In 3.10, there will be a public method (https://docs.python.org/3.10/library/typing.html#typing.is_typeddict) + """ + from pydantic.v1.utils import lenient_issubclass + + return lenient_issubclass(type_, dict) and hasattr(type_, '__total__') + + +def _check_typeddict_special(type_: Any) -> bool: + return type_ is TypedDictRequired or type_ is TypedDictNotRequired + + +def is_typeddict_special(type_: Any) -> bool: + """ + Check if type is a TypedDict special form (Required or NotRequired). + """ + return _check_typeddict_special(type_) or _check_typeddict_special(get_origin(type_)) + + +test_type = NewType('test_type', str) + + +def is_new_type(type_: Type[Any]) -> bool: + """ + Check whether type_ was created using typing.NewType + """ + return isinstance(type_, test_type.__class__) and hasattr(type_, '__supertype__') # type: ignore + + +def new_type_supertype(type_: Type[Any]) -> Type[Any]: + while hasattr(type_, '__supertype__'): + type_ = type_.__supertype__ + return type_ + + +def _check_classvar(v: Optional[Type[Any]]) -> bool: + if v is None: + return False + + return v.__class__ == ClassVar.__class__ and getattr(v, '_name', None) == 'ClassVar' + + +def _check_finalvar(v: Optional[Type[Any]]) -> bool: + """ + Check if a given type is a `typing.Final` type. + """ + if v is None: + return False + + return v.__class__ == Final.__class__ and (sys.version_info < (3, 8) or getattr(v, '_name', None) == 'Final') + + +def is_classvar(ann_type: Type[Any]) -> bool: + if _check_classvar(ann_type) or _check_classvar(get_origin(ann_type)): + return True + + # this is an ugly workaround for class vars that contain forward references and are therefore themselves + # forward references, see #3679 + if ann_type.__class__ == ForwardRef and ann_type.__forward_arg__.startswith('ClassVar['): + return True + + return False + + +def is_finalvar(ann_type: Type[Any]) -> bool: + return _check_finalvar(ann_type) or _check_finalvar(get_origin(ann_type)) + + +def update_field_forward_refs(field: 'ModelField', globalns: Any, localns: Any) -> None: + """ + Try to update ForwardRefs on fields based on this ModelField, globalns and localns. + """ + prepare = False + if field.type_.__class__ == ForwardRef: + prepare = True + field.type_ = evaluate_forwardref(field.type_, globalns, localns or None) + if field.outer_type_.__class__ == ForwardRef: + prepare = True + field.outer_type_ = evaluate_forwardref(field.outer_type_, globalns, localns or None) + if prepare: + field.prepare() + + if field.sub_fields: + for sub_f in field.sub_fields: + update_field_forward_refs(sub_f, globalns=globalns, localns=localns) + + if field.discriminator_key is not None: + field.prepare_discriminated_union_sub_fields() + + +def update_model_forward_refs( + model: Type[Any], + fields: Iterable['ModelField'], + json_encoders: Dict[Union[Type[Any], str, ForwardRef], AnyCallable], + localns: 'DictStrAny', + exc_to_suppress: Tuple[Type[BaseException], ...] = (), +) -> None: + """ + Try to update model fields ForwardRefs based on model and localns. + """ + if model.__module__ in sys.modules: + globalns = sys.modules[model.__module__].__dict__.copy() + else: + globalns = {} + + globalns.setdefault(model.__name__, model) + + for f in fields: + try: + update_field_forward_refs(f, globalns=globalns, localns=localns) + except exc_to_suppress: + pass + + for key in set(json_encoders.keys()): + if isinstance(key, str): + fr: ForwardRef = ForwardRef(key) + elif isinstance(key, ForwardRef): + fr = key + else: + continue + + try: + new_key = evaluate_forwardref(fr, globalns, localns or None) + except exc_to_suppress: # pragma: no cover + continue + + json_encoders[new_key] = json_encoders.pop(key) + + +def get_class(type_: Type[Any]) -> Union[None, bool, Type[Any]]: + """ + Tries to get the class of a Type[T] annotation. Returns True if Type is used + without brackets. Otherwise returns None. + """ + if type_ is type: + return True + + if get_origin(type_) is None: + return None + + args = get_args(type_) + if not args or not isinstance(args[0], type): + return True + else: + return args[0] + + +def get_sub_types(tp: Any) -> List[Any]: + """ + Return all the types that are allowed by type `tp` + `tp` can be a `Union` of allowed types or an `Annotated` type + """ + origin = get_origin(tp) + if origin is Annotated: + return get_sub_types(get_args(tp)[0]) + elif is_union(origin): + return [x for t in get_args(tp) for x in get_sub_types(t)] + else: + return [tp] diff --git a/source/target-vendors/cp312/pydantic/v1/utils.py b/source/target-vendors/cp312/pydantic/v1/utils.py new file mode 100644 index 0000000..2094e84 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/utils.py @@ -0,0 +1,807 @@ +import keyword +import warnings +import weakref +from collections import OrderedDict, defaultdict, deque +from copy import deepcopy +from itertools import islice, zip_longest +from types import BuiltinFunctionType, CodeType, FunctionType, GeneratorType, LambdaType, ModuleType +from typing import ( + TYPE_CHECKING, + AbstractSet, + Any, + Callable, + Collection, + Dict, + Generator, + Iterable, + Iterator, + List, + Mapping, + NoReturn, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union, +) + +from typing_extensions import Annotated + +from pydantic.v1.errors import ConfigError +from pydantic.v1.typing import ( + NoneType, + WithArgsTypes, + all_literal_values, + display_as_type, + get_args, + get_origin, + is_literal_type, + is_union, +) +from pydantic.v1.version import version_info + +if TYPE_CHECKING: + from inspect import Signature + from pathlib import Path + + from pydantic.v1.config import BaseConfig + from pydantic.v1.dataclasses import Dataclass + from pydantic.v1.fields import ModelField + from pydantic.v1.main import BaseModel + from pydantic.v1.typing import AbstractSetIntStr, DictIntStrAny, IntStr, MappingIntStrAny, ReprArgs + + RichReprResult = Iterable[Union[Any, Tuple[Any], Tuple[str, Any], Tuple[str, Any, Any]]] + +__all__ = ( + 'import_string', + 'sequence_like', + 'validate_field_name', + 'lenient_isinstance', + 'lenient_issubclass', + 'in_ipython', + 'is_valid_identifier', + 'deep_update', + 'update_not_none', + 'almost_equal_floats', + 'get_model', + 'to_camel', + 'to_lower_camel', + 'is_valid_field', + 'smart_deepcopy', + 'PyObjectStr', + 'Representation', + 'GetterDict', + 'ValueItems', + 'version_info', # required here to match behaviour in v1.3 + 'ClassAttribute', + 'path_type', + 'ROOT_KEY', + 'get_unique_discriminator_alias', + 'get_discriminator_alias_and_values', + 'DUNDER_ATTRIBUTES', +) + +ROOT_KEY = '__root__' +# these are types that are returned unchanged by deepcopy +IMMUTABLE_NON_COLLECTIONS_TYPES: Set[Type[Any]] = { + int, + float, + complex, + str, + bool, + bytes, + type, + NoneType, + FunctionType, + BuiltinFunctionType, + LambdaType, + weakref.ref, + CodeType, + # note: including ModuleType will differ from behaviour of deepcopy by not producing error. + # It might be not a good idea in general, but considering that this function used only internally + # against default values of fields, this will allow to actually have a field with module as default value + ModuleType, + NotImplemented.__class__, + Ellipsis.__class__, +} + +# these are types that if empty, might be copied with simple copy() instead of deepcopy() +BUILTIN_COLLECTIONS: Set[Type[Any]] = { + list, + set, + tuple, + frozenset, + dict, + OrderedDict, + defaultdict, + deque, +} + + +def import_string(dotted_path: str) -> Any: + """ + Stolen approximately from django. Import a dotted module path and return the attribute/class designated by the + last name in the path. Raise ImportError if the import fails. + """ + from importlib import import_module + + try: + module_path, class_name = dotted_path.strip(' ').rsplit('.', 1) + except ValueError as e: + raise ImportError(f'"{dotted_path}" doesn\'t look like a module path') from e + + module = import_module(module_path) + try: + return getattr(module, class_name) + except AttributeError as e: + raise ImportError(f'Module "{module_path}" does not define a "{class_name}" attribute') from e + + +def truncate(v: Union[str], *, max_len: int = 80) -> str: + """ + Truncate a value and add a unicode ellipsis (three dots) to the end if it was too long + """ + warnings.warn('`truncate` is no-longer used by pydantic and is deprecated', DeprecationWarning) + if isinstance(v, str) and len(v) > (max_len - 2): + # -3 so quote + string + … + quote has correct length + return (v[: (max_len - 3)] + '…').__repr__() + try: + v = v.__repr__() + except TypeError: + v = v.__class__.__repr__(v) # in case v is a type + if len(v) > max_len: + v = v[: max_len - 1] + '…' + return v + + +def sequence_like(v: Any) -> bool: + return isinstance(v, (list, tuple, set, frozenset, GeneratorType, deque)) + + +def validate_field_name(bases: Iterable[Type[Any]], field_name: str) -> None: + """ + Ensure that the field's name does not shadow an existing attribute of the model. + """ + for base in bases: + if getattr(base, field_name, None): + raise NameError( + f'Field name "{field_name}" shadows a BaseModel attribute; ' + f'use a different field name with "alias=\'{field_name}\'".' + ) + + +def lenient_isinstance(o: Any, class_or_tuple: Union[Type[Any], Tuple[Type[Any], ...], None]) -> bool: + try: + return isinstance(o, class_or_tuple) # type: ignore[arg-type] + except TypeError: + return False + + +def lenient_issubclass(cls: Any, class_or_tuple: Union[Type[Any], Tuple[Type[Any], ...], None]) -> bool: + try: + return isinstance(cls, type) and issubclass(cls, class_or_tuple) # type: ignore[arg-type] + except TypeError: + if isinstance(cls, WithArgsTypes): + return False + raise # pragma: no cover + + +def in_ipython() -> bool: + """ + Check whether we're in an ipython environment, including jupyter notebooks. + """ + try: + eval('__IPYTHON__') + except NameError: + return False + else: # pragma: no cover + return True + + +def is_valid_identifier(identifier: str) -> bool: + """ + Checks that a string is a valid identifier and not a Python keyword. + :param identifier: The identifier to test. + :return: True if the identifier is valid. + """ + return identifier.isidentifier() and not keyword.iskeyword(identifier) + + +KeyType = TypeVar('KeyType') + + +def deep_update(mapping: Dict[KeyType, Any], *updating_mappings: Dict[KeyType, Any]) -> Dict[KeyType, Any]: + updated_mapping = mapping.copy() + for updating_mapping in updating_mappings: + for k, v in updating_mapping.items(): + if k in updated_mapping and isinstance(updated_mapping[k], dict) and isinstance(v, dict): + updated_mapping[k] = deep_update(updated_mapping[k], v) + else: + updated_mapping[k] = v + return updated_mapping + + +def update_not_none(mapping: Dict[Any, Any], **update: Any) -> None: + mapping.update({k: v for k, v in update.items() if v is not None}) + + +def almost_equal_floats(value_1: float, value_2: float, *, delta: float = 1e-8) -> bool: + """ + Return True if two floats are almost equal + """ + return abs(value_1 - value_2) <= delta + + +def generate_model_signature( + init: Callable[..., None], fields: Dict[str, 'ModelField'], config: Type['BaseConfig'] +) -> 'Signature': + """ + Generate signature for model based on its fields + """ + from inspect import Parameter, Signature, signature + + from pydantic.v1.config import Extra + + present_params = signature(init).parameters.values() + merged_params: Dict[str, Parameter] = {} + var_kw = None + use_var_kw = False + + for param in islice(present_params, 1, None): # skip self arg + if param.kind is param.VAR_KEYWORD: + var_kw = param + continue + merged_params[param.name] = param + + if var_kw: # if custom init has no var_kw, fields which are not declared in it cannot be passed through + allow_names = config.allow_population_by_field_name + for field_name, field in fields.items(): + param_name = field.alias + if field_name in merged_params or param_name in merged_params: + continue + elif not is_valid_identifier(param_name): + if allow_names and is_valid_identifier(field_name): + param_name = field_name + else: + use_var_kw = True + continue + + # TODO: replace annotation with actual expected types once #1055 solved + kwargs = {'default': field.default} if not field.required else {} + merged_params[param_name] = Parameter( + param_name, Parameter.KEYWORD_ONLY, annotation=field.annotation, **kwargs + ) + + if config.extra is Extra.allow: + use_var_kw = True + + if var_kw and use_var_kw: + # Make sure the parameter for extra kwargs + # does not have the same name as a field + default_model_signature = [ + ('__pydantic_self__', Parameter.POSITIONAL_OR_KEYWORD), + ('data', Parameter.VAR_KEYWORD), + ] + if [(p.name, p.kind) for p in present_params] == default_model_signature: + # if this is the standard model signature, use extra_data as the extra args name + var_kw_name = 'extra_data' + else: + # else start from var_kw + var_kw_name = var_kw.name + + # generate a name that's definitely unique + while var_kw_name in fields: + var_kw_name += '_' + merged_params[var_kw_name] = var_kw.replace(name=var_kw_name) + + return Signature(parameters=list(merged_params.values()), return_annotation=None) + + +def get_model(obj: Union[Type['BaseModel'], Type['Dataclass']]) -> Type['BaseModel']: + from pydantic.v1.main import BaseModel + + try: + model_cls = obj.__pydantic_model__ # type: ignore + except AttributeError: + model_cls = obj + + if not issubclass(model_cls, BaseModel): + raise TypeError('Unsupported type, must be either BaseModel or dataclass') + return model_cls + + +def to_camel(string: str) -> str: + return ''.join(word.capitalize() for word in string.split('_')) + + +def to_lower_camel(string: str) -> str: + if len(string) >= 1: + pascal_string = to_camel(string) + return pascal_string[0].lower() + pascal_string[1:] + return string.lower() + + +T = TypeVar('T') + + +def unique_list( + input_list: Union[List[T], Tuple[T, ...]], + *, + name_factory: Callable[[T], str] = str, +) -> List[T]: + """ + Make a list unique while maintaining order. + We update the list if another one with the same name is set + (e.g. root validator overridden in subclass) + """ + result: List[T] = [] + result_names: List[str] = [] + for v in input_list: + v_name = name_factory(v) + if v_name not in result_names: + result_names.append(v_name) + result.append(v) + else: + result[result_names.index(v_name)] = v + + return result + + +class PyObjectStr(str): + """ + String class where repr doesn't include quotes. Useful with Representation when you want to return a string + representation of something that valid (or pseudo-valid) python. + """ + + def __repr__(self) -> str: + return str(self) + + +class Representation: + """ + Mixin to provide __str__, __repr__, and __pretty__ methods. See #884 for more details. + + __pretty__ is used by [devtools](https://python-devtools.helpmanual.io/) to provide human readable representations + of objects. + """ + + __slots__: Tuple[str, ...] = tuple() + + def __repr_args__(self) -> 'ReprArgs': + """ + Returns the attributes to show in __str__, __repr__, and __pretty__ this is generally overridden. + + Can either return: + * name - value pairs, e.g.: `[('foo_name', 'foo'), ('bar_name', ['b', 'a', 'r'])]` + * or, just values, e.g.: `[(None, 'foo'), (None, ['b', 'a', 'r'])]` + """ + attrs = ((s, getattr(self, s)) for s in self.__slots__) + return [(a, v) for a, v in attrs if v is not None] + + def __repr_name__(self) -> str: + """ + Name of the instance's class, used in __repr__. + """ + return self.__class__.__name__ + + def __repr_str__(self, join_str: str) -> str: + return join_str.join(repr(v) if a is None else f'{a}={v!r}' for a, v in self.__repr_args__()) + + def __pretty__(self, fmt: Callable[[Any], Any], **kwargs: Any) -> Generator[Any, None, None]: + """ + Used by devtools (https://python-devtools.helpmanual.io/) to provide a human readable representations of objects + """ + yield self.__repr_name__() + '(' + yield 1 + for name, value in self.__repr_args__(): + if name is not None: + yield name + '=' + yield fmt(value) + yield ',' + yield 0 + yield -1 + yield ')' + + def __str__(self) -> str: + return self.__repr_str__(' ') + + def __repr__(self) -> str: + return f'{self.__repr_name__()}({self.__repr_str__(", ")})' + + def __rich_repr__(self) -> 'RichReprResult': + """Get fields for Rich library""" + for name, field_repr in self.__repr_args__(): + if name is None: + yield field_repr + else: + yield name, field_repr + + +class GetterDict(Representation): + """ + Hack to make object's smell just enough like dicts for validate_model. + + We can't inherit from Mapping[str, Any] because it upsets cython so we have to implement all methods ourselves. + """ + + __slots__ = ('_obj',) + + def __init__(self, obj: Any): + self._obj = obj + + def __getitem__(self, key: str) -> Any: + try: + return getattr(self._obj, key) + except AttributeError as e: + raise KeyError(key) from e + + def get(self, key: Any, default: Any = None) -> Any: + return getattr(self._obj, key, default) + + def extra_keys(self) -> Set[Any]: + """ + We don't want to get any other attributes of obj if the model didn't explicitly ask for them + """ + return set() + + def keys(self) -> List[Any]: + """ + Keys of the pseudo dictionary, uses a list not set so order information can be maintained like python + dictionaries. + """ + return list(self) + + def values(self) -> List[Any]: + return [self[k] for k in self] + + def items(self) -> Iterator[Tuple[str, Any]]: + for k in self: + yield k, self.get(k) + + def __iter__(self) -> Iterator[str]: + for name in dir(self._obj): + if not name.startswith('_'): + yield name + + def __len__(self) -> int: + return sum(1 for _ in self) + + def __contains__(self, item: Any) -> bool: + return item in self.keys() + + def __eq__(self, other: Any) -> bool: + return dict(self) == dict(other.items()) + + def __repr_args__(self) -> 'ReprArgs': + return [(None, dict(self))] + + def __repr_name__(self) -> str: + return f'GetterDict[{display_as_type(self._obj)}]' + + +class ValueItems(Representation): + """ + Class for more convenient calculation of excluded or included fields on values. + """ + + __slots__ = ('_items', '_type') + + def __init__(self, value: Any, items: Union['AbstractSetIntStr', 'MappingIntStrAny']) -> None: + items = self._coerce_items(items) + + if isinstance(value, (list, tuple)): + items = self._normalize_indexes(items, len(value)) + + self._items: 'MappingIntStrAny' = items + + def is_excluded(self, item: Any) -> bool: + """ + Check if item is fully excluded. + + :param item: key or index of a value + """ + return self.is_true(self._items.get(item)) + + def is_included(self, item: Any) -> bool: + """ + Check if value is contained in self._items + + :param item: key or index of value + """ + return item in self._items + + def for_element(self, e: 'IntStr') -> Optional[Union['AbstractSetIntStr', 'MappingIntStrAny']]: + """ + :param e: key or index of element on value + :return: raw values for element if self._items is dict and contain needed element + """ + + item = self._items.get(e) + return item if not self.is_true(item) else None + + def _normalize_indexes(self, items: 'MappingIntStrAny', v_length: int) -> 'DictIntStrAny': + """ + :param items: dict or set of indexes which will be normalized + :param v_length: length of sequence indexes of which will be + + >>> self._normalize_indexes({0: True, -2: True, -1: True}, 4) + {0: True, 2: True, 3: True} + >>> self._normalize_indexes({'__all__': True}, 4) + {0: True, 1: True, 2: True, 3: True} + """ + + normalized_items: 'DictIntStrAny' = {} + all_items = None + for i, v in items.items(): + if not (isinstance(v, Mapping) or isinstance(v, AbstractSet) or self.is_true(v)): + raise TypeError(f'Unexpected type of exclude value for index "{i}" {v.__class__}') + if i == '__all__': + all_items = self._coerce_value(v) + continue + if not isinstance(i, int): + raise TypeError( + 'Excluding fields from a sequence of sub-models or dicts must be performed index-wise: ' + 'expected integer keys or keyword "__all__"' + ) + normalized_i = v_length + i if i < 0 else i + normalized_items[normalized_i] = self.merge(v, normalized_items.get(normalized_i)) + + if not all_items: + return normalized_items + if self.is_true(all_items): + for i in range(v_length): + normalized_items.setdefault(i, ...) + return normalized_items + for i in range(v_length): + normalized_item = normalized_items.setdefault(i, {}) + if not self.is_true(normalized_item): + normalized_items[i] = self.merge(all_items, normalized_item) + return normalized_items + + @classmethod + def merge(cls, base: Any, override: Any, intersect: bool = False) -> Any: + """ + Merge a ``base`` item with an ``override`` item. + + Both ``base`` and ``override`` are converted to dictionaries if possible. + Sets are converted to dictionaries with the sets entries as keys and + Ellipsis as values. + + Each key-value pair existing in ``base`` is merged with ``override``, + while the rest of the key-value pairs are updated recursively with this function. + + Merging takes place based on the "union" of keys if ``intersect`` is + set to ``False`` (default) and on the intersection of keys if + ``intersect`` is set to ``True``. + """ + override = cls._coerce_value(override) + base = cls._coerce_value(base) + if override is None: + return base + if cls.is_true(base) or base is None: + return override + if cls.is_true(override): + return base if intersect else override + + # intersection or union of keys while preserving ordering: + if intersect: + merge_keys = [k for k in base if k in override] + [k for k in override if k in base] + else: + merge_keys = list(base) + [k for k in override if k not in base] + + merged: 'DictIntStrAny' = {} + for k in merge_keys: + merged_item = cls.merge(base.get(k), override.get(k), intersect=intersect) + if merged_item is not None: + merged[k] = merged_item + + return merged + + @staticmethod + def _coerce_items(items: Union['AbstractSetIntStr', 'MappingIntStrAny']) -> 'MappingIntStrAny': + if isinstance(items, Mapping): + pass + elif isinstance(items, AbstractSet): + items = dict.fromkeys(items, ...) + else: + class_name = getattr(items, '__class__', '???') + assert_never( + items, + f'Unexpected type of exclude value {class_name}', + ) + return items + + @classmethod + def _coerce_value(cls, value: Any) -> Any: + if value is None or cls.is_true(value): + return value + return cls._coerce_items(value) + + @staticmethod + def is_true(v: Any) -> bool: + return v is True or v is ... + + def __repr_args__(self) -> 'ReprArgs': + return [(None, self._items)] + + +class ClassAttribute: + """ + Hide class attribute from its instances + """ + + __slots__ = ( + 'name', + 'value', + ) + + def __init__(self, name: str, value: Any) -> None: + self.name = name + self.value = value + + def __get__(self, instance: Any, owner: Type[Any]) -> None: + if instance is None: + return self.value + raise AttributeError(f'{self.name!r} attribute of {owner.__name__!r} is class-only') + + +path_types = { + 'is_dir': 'directory', + 'is_file': 'file', + 'is_mount': 'mount point', + 'is_symlink': 'symlink', + 'is_block_device': 'block device', + 'is_char_device': 'char device', + 'is_fifo': 'FIFO', + 'is_socket': 'socket', +} + + +def path_type(p: 'Path') -> str: + """ + Find out what sort of thing a path is. + """ + assert p.exists(), 'path does not exist' + for method, name in path_types.items(): + if getattr(p, method)(): + return name + + return 'unknown' + + +Obj = TypeVar('Obj') + + +def smart_deepcopy(obj: Obj) -> Obj: + """ + Return type as is for immutable built-in types + Use obj.copy() for built-in empty collections + Use copy.deepcopy() for non-empty collections and unknown objects + """ + + obj_type = obj.__class__ + if obj_type in IMMUTABLE_NON_COLLECTIONS_TYPES: + return obj # fastest case: obj is immutable and not collection therefore will not be copied anyway + try: + if not obj and obj_type in BUILTIN_COLLECTIONS: + # faster way for empty collections, no need to copy its members + return obj if obj_type is tuple else obj.copy() # type: ignore # tuple doesn't have copy method + except (TypeError, ValueError, RuntimeError): + # do we really dare to catch ALL errors? Seems a bit risky + pass + + return deepcopy(obj) # slowest way when we actually might need a deepcopy + + +def is_valid_field(name: str) -> bool: + if not name.startswith('_'): + return True + return ROOT_KEY == name + + +DUNDER_ATTRIBUTES = { + '__annotations__', + '__classcell__', + '__doc__', + '__module__', + '__orig_bases__', + '__orig_class__', + '__qualname__', + '__firstlineno__', + '__static_attributes__', + '__classdictcell__', +} + + +def is_valid_private_name(name: str) -> bool: + return not is_valid_field(name) and name not in DUNDER_ATTRIBUTES + + +_EMPTY = object() + + +def all_identical(left: Iterable[Any], right: Iterable[Any]) -> bool: + """ + Check that the items of `left` are the same objects as those in `right`. + + >>> a, b = object(), object() + >>> all_identical([a, b, a], [a, b, a]) + True + >>> all_identical([a, b, [a]], [a, b, [a]]) # new list object, while "equal" is not "identical" + False + """ + for left_item, right_item in zip_longest(left, right, fillvalue=_EMPTY): + if left_item is not right_item: + return False + return True + + +def assert_never(obj: NoReturn, msg: str) -> NoReturn: + """ + Helper to make sure that we have covered all possible types. + + This is mostly useful for ``mypy``, docs: + https://mypy.readthedocs.io/en/latest/literal_types.html#exhaustive-checks + """ + raise TypeError(msg) + + +def get_unique_discriminator_alias(all_aliases: Collection[str], discriminator_key: str) -> str: + """Validate that all aliases are the same and if that's the case return the alias""" + unique_aliases = set(all_aliases) + if len(unique_aliases) > 1: + raise ConfigError( + f'Aliases for discriminator {discriminator_key!r} must be the same (got {", ".join(sorted(all_aliases))})' + ) + return unique_aliases.pop() + + +def get_discriminator_alias_and_values(tp: Any, discriminator_key: str) -> Tuple[str, Tuple[str, ...]]: + """ + Get alias and all valid values in the `Literal` type of the discriminator field + `tp` can be a `BaseModel` class or directly an `Annotated` `Union` of many. + """ + is_root_model = getattr(tp, '__custom_root_type__', False) + + if get_origin(tp) is Annotated: + tp = get_args(tp)[0] + + if hasattr(tp, '__pydantic_model__'): + tp = tp.__pydantic_model__ + + if is_union(get_origin(tp)): + alias, all_values = _get_union_alias_and_all_values(tp, discriminator_key) + return alias, tuple(v for values in all_values for v in values) + elif is_root_model: + union_type = tp.__fields__[ROOT_KEY].type_ + alias, all_values = _get_union_alias_and_all_values(union_type, discriminator_key) + + if len(set(all_values)) > 1: + raise ConfigError( + f'Field {discriminator_key!r} is not the same for all submodels of {display_as_type(tp)!r}' + ) + + return alias, all_values[0] + + else: + try: + t_discriminator_type = tp.__fields__[discriminator_key].type_ + except AttributeError as e: + raise TypeError(f'Type {tp.__name__!r} is not a valid `BaseModel` or `dataclass`') from e + except KeyError as e: + raise ConfigError(f'Model {tp.__name__!r} needs a discriminator field for key {discriminator_key!r}') from e + + if not is_literal_type(t_discriminator_type): + raise ConfigError(f'Field {discriminator_key!r} of model {tp.__name__!r} needs to be a `Literal`') + + return tp.__fields__[discriminator_key].alias, all_literal_values(t_discriminator_type) + + +def _get_union_alias_and_all_values( + union_type: Type[Any], discriminator_key: str +) -> Tuple[str, Tuple[Tuple[str, ...], ...]]: + zipped_aliases_values = [get_discriminator_alias_and_values(t, discriminator_key) for t in get_args(union_type)] + # unzip: [('alias_a',('v1', 'v2)), ('alias_b', ('v3',))] => [('alias_a', 'alias_b'), (('v1', 'v2'), ('v3',))] + all_aliases, all_values = zip(*zipped_aliases_values) + return get_unique_discriminator_alias(all_aliases, discriminator_key), all_values diff --git a/source/target-vendors/cp312/pydantic/v1/validators.py b/source/target-vendors/cp312/pydantic/v1/validators.py new file mode 100644 index 0000000..c0940e8 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/validators.py @@ -0,0 +1,768 @@ +import math +import re +from collections import OrderedDict, deque +from collections.abc import Hashable as CollectionsHashable +from datetime import date, datetime, time, timedelta +from decimal import Decimal, DecimalException +from enum import Enum, IntEnum +from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Deque, + Dict, + ForwardRef, + FrozenSet, + Generator, + Hashable, + List, + NamedTuple, + Pattern, + Set, + Tuple, + Type, + TypeVar, + Union, +) +from uuid import UUID +from warnings import warn + +from pydantic.v1 import errors +from pydantic.v1.datetime_parse import parse_date, parse_datetime, parse_duration, parse_time +from pydantic.v1.typing import ( + AnyCallable, + all_literal_values, + display_as_type, + get_class, + is_callable_type, + is_literal_type, + is_namedtuple, + is_none_type, + is_typeddict, +) +from pydantic.v1.utils import almost_equal_floats, lenient_issubclass, sequence_like + +if TYPE_CHECKING: + from typing_extensions import Literal, TypedDict + + from pydantic.v1.config import BaseConfig + from pydantic.v1.fields import ModelField + from pydantic.v1.types import ConstrainedDecimal, ConstrainedFloat, ConstrainedInt + + ConstrainedNumber = Union[ConstrainedDecimal, ConstrainedFloat, ConstrainedInt] + AnyOrderedDict = OrderedDict[Any, Any] + Number = Union[int, float, Decimal] + StrBytes = Union[str, bytes] + + +def str_validator(v: Any) -> Union[str]: + if isinstance(v, str): + if isinstance(v, Enum): + return v.value + else: + return v + elif isinstance(v, (float, int, Decimal)): + # is there anything else we want to add here? If you think so, create an issue. + return str(v) + elif isinstance(v, (bytes, bytearray)): + return v.decode() + else: + raise errors.StrError() + + +def strict_str_validator(v: Any) -> Union[str]: + if isinstance(v, str) and not isinstance(v, Enum): + return v + raise errors.StrError() + + +def bytes_validator(v: Any) -> Union[bytes]: + if isinstance(v, bytes): + return v + elif isinstance(v, bytearray): + return bytes(v) + elif isinstance(v, str): + return v.encode() + elif isinstance(v, (float, int, Decimal)): + return str(v).encode() + else: + raise errors.BytesError() + + +def strict_bytes_validator(v: Any) -> Union[bytes]: + if isinstance(v, bytes): + return v + elif isinstance(v, bytearray): + return bytes(v) + else: + raise errors.BytesError() + + +BOOL_FALSE = {0, '0', 'off', 'f', 'false', 'n', 'no'} +BOOL_TRUE = {1, '1', 'on', 't', 'true', 'y', 'yes'} + + +def bool_validator(v: Any) -> bool: + if v is True or v is False: + return v + if isinstance(v, bytes): + v = v.decode() + if isinstance(v, str): + v = v.lower() + try: + if v in BOOL_TRUE: + return True + if v in BOOL_FALSE: + return False + except TypeError: + raise errors.BoolError() + raise errors.BoolError() + + +# matches the default limit cpython, see https://github.com/python/cpython/pull/96500 +max_str_int = 4_300 + + +def int_validator(v: Any) -> int: + if isinstance(v, int) and not (v is True or v is False): + return v + + # see https://github.com/pydantic/pydantic/issues/1477 and in turn, https://github.com/python/cpython/issues/95778 + # this check should be unnecessary once patch releases are out for 3.7, 3.8, 3.9 and 3.10 + # but better to check here until then. + # NOTICE: this does not fully protect user from the DOS risk since the standard library JSON implementation + # (and other std lib modules like xml) use `int()` and are likely called before this, the best workaround is to + # 1. update to the latest patch release of python once released, 2. use a different JSON library like ujson + if isinstance(v, (str, bytes, bytearray)) and len(v) > max_str_int: + raise errors.IntegerError() + + try: + return int(v) + except (TypeError, ValueError, OverflowError): + raise errors.IntegerError() + + +def strict_int_validator(v: Any) -> int: + if isinstance(v, int) and not (v is True or v is False): + return v + raise errors.IntegerError() + + +def float_validator(v: Any) -> float: + if isinstance(v, float): + return v + + try: + return float(v) + except (TypeError, ValueError): + raise errors.FloatError() + + +def strict_float_validator(v: Any) -> float: + if isinstance(v, float): + return v + raise errors.FloatError() + + +def float_finite_validator(v: 'Number', field: 'ModelField', config: 'BaseConfig') -> 'Number': + allow_inf_nan = getattr(field.type_, 'allow_inf_nan', None) + if allow_inf_nan is None: + allow_inf_nan = config.allow_inf_nan + + if allow_inf_nan is False and (math.isnan(v) or math.isinf(v)): + raise errors.NumberNotFiniteError() + return v + + +def number_multiple_validator(v: 'Number', field: 'ModelField') -> 'Number': + field_type: ConstrainedNumber = field.type_ + if field_type.multiple_of is not None: + mod = float(v) / float(field_type.multiple_of) % 1 + if not almost_equal_floats(mod, 0.0) and not almost_equal_floats(mod, 1.0): + raise errors.NumberNotMultipleError(multiple_of=field_type.multiple_of) + return v + + +def number_size_validator(v: 'Number', field: 'ModelField') -> 'Number': + field_type: ConstrainedNumber = field.type_ + if field_type.gt is not None and not v > field_type.gt: + raise errors.NumberNotGtError(limit_value=field_type.gt) + elif field_type.ge is not None and not v >= field_type.ge: + raise errors.NumberNotGeError(limit_value=field_type.ge) + + if field_type.lt is not None and not v < field_type.lt: + raise errors.NumberNotLtError(limit_value=field_type.lt) + if field_type.le is not None and not v <= field_type.le: + raise errors.NumberNotLeError(limit_value=field_type.le) + + return v + + +def constant_validator(v: 'Any', field: 'ModelField') -> 'Any': + """Validate ``const`` fields. + + The value provided for a ``const`` field must be equal to the default value + of the field. This is to support the keyword of the same name in JSON + Schema. + """ + if v != field.default: + raise errors.WrongConstantError(given=v, permitted=[field.default]) + + return v + + +def anystr_length_validator(v: 'StrBytes', config: 'BaseConfig') -> 'StrBytes': + v_len = len(v) + + min_length = config.min_anystr_length + if v_len < min_length: + raise errors.AnyStrMinLengthError(limit_value=min_length) + + max_length = config.max_anystr_length + if max_length is not None and v_len > max_length: + raise errors.AnyStrMaxLengthError(limit_value=max_length) + + return v + + +def anystr_strip_whitespace(v: 'StrBytes') -> 'StrBytes': + return v.strip() + + +def anystr_upper(v: 'StrBytes') -> 'StrBytes': + return v.upper() + + +def anystr_lower(v: 'StrBytes') -> 'StrBytes': + return v.lower() + + +def ordered_dict_validator(v: Any) -> 'AnyOrderedDict': + if isinstance(v, OrderedDict): + return v + + try: + return OrderedDict(v) + except (TypeError, ValueError): + raise errors.DictError() + + +def dict_validator(v: Any) -> Dict[Any, Any]: + if isinstance(v, dict): + return v + + try: + return dict(v) + except (TypeError, ValueError): + raise errors.DictError() + + +def list_validator(v: Any) -> List[Any]: + if isinstance(v, list): + return v + elif sequence_like(v): + return list(v) + else: + raise errors.ListError() + + +def tuple_validator(v: Any) -> Tuple[Any, ...]: + if isinstance(v, tuple): + return v + elif sequence_like(v): + return tuple(v) + else: + raise errors.TupleError() + + +def set_validator(v: Any) -> Set[Any]: + if isinstance(v, set): + return v + elif sequence_like(v): + return set(v) + else: + raise errors.SetError() + + +def frozenset_validator(v: Any) -> FrozenSet[Any]: + if isinstance(v, frozenset): + return v + elif sequence_like(v): + return frozenset(v) + else: + raise errors.FrozenSetError() + + +def deque_validator(v: Any) -> Deque[Any]: + if isinstance(v, deque): + return v + elif sequence_like(v): + return deque(v) + else: + raise errors.DequeError() + + +def enum_member_validator(v: Any, field: 'ModelField', config: 'BaseConfig') -> Enum: + try: + enum_v = field.type_(v) + except ValueError: + # field.type_ should be an enum, so will be iterable + raise errors.EnumMemberError(enum_values=list(field.type_)) + return enum_v.value if config.use_enum_values else enum_v + + +def uuid_validator(v: Any, field: 'ModelField') -> UUID: + try: + if isinstance(v, str): + v = UUID(v) + elif isinstance(v, (bytes, bytearray)): + try: + v = UUID(v.decode()) + except ValueError: + # 16 bytes in big-endian order as the bytes argument fail + # the above check + v = UUID(bytes=v) + except ValueError: + raise errors.UUIDError() + + if not isinstance(v, UUID): + raise errors.UUIDError() + + required_version = getattr(field.type_, '_required_version', None) + if required_version and v.version != required_version: + raise errors.UUIDVersionError(required_version=required_version) + + return v + + +def decimal_validator(v: Any) -> Decimal: + if isinstance(v, Decimal): + return v + elif isinstance(v, (bytes, bytearray)): + v = v.decode() + + v = str(v).strip() + + try: + v = Decimal(v) + except DecimalException: + raise errors.DecimalError() + + if not v.is_finite(): + raise errors.DecimalIsNotFiniteError() + + return v + + +def hashable_validator(v: Any) -> Hashable: + if isinstance(v, Hashable): + return v + + raise errors.HashableError() + + +def ip_v4_address_validator(v: Any) -> IPv4Address: + if isinstance(v, IPv4Address): + return v + + try: + return IPv4Address(v) + except ValueError: + raise errors.IPv4AddressError() + + +def ip_v6_address_validator(v: Any) -> IPv6Address: + if isinstance(v, IPv6Address): + return v + + try: + return IPv6Address(v) + except ValueError: + raise errors.IPv6AddressError() + + +def ip_v4_network_validator(v: Any) -> IPv4Network: + """ + Assume IPv4Network initialised with a default ``strict`` argument + + See more: + https://docs.python.org/library/ipaddress.html#ipaddress.IPv4Network + """ + if isinstance(v, IPv4Network): + return v + + try: + return IPv4Network(v) + except ValueError: + raise errors.IPv4NetworkError() + + +def ip_v6_network_validator(v: Any) -> IPv6Network: + """ + Assume IPv6Network initialised with a default ``strict`` argument + + See more: + https://docs.python.org/library/ipaddress.html#ipaddress.IPv6Network + """ + if isinstance(v, IPv6Network): + return v + + try: + return IPv6Network(v) + except ValueError: + raise errors.IPv6NetworkError() + + +def ip_v4_interface_validator(v: Any) -> IPv4Interface: + if isinstance(v, IPv4Interface): + return v + + try: + return IPv4Interface(v) + except ValueError: + raise errors.IPv4InterfaceError() + + +def ip_v6_interface_validator(v: Any) -> IPv6Interface: + if isinstance(v, IPv6Interface): + return v + + try: + return IPv6Interface(v) + except ValueError: + raise errors.IPv6InterfaceError() + + +def path_validator(v: Any) -> Path: + if isinstance(v, Path): + return v + + try: + return Path(v) + except TypeError: + raise errors.PathError() + + +def path_exists_validator(v: Any) -> Path: + if not v.exists(): + raise errors.PathNotExistsError(path=v) + + return v + + +def callable_validator(v: Any) -> AnyCallable: + """ + Perform a simple check if the value is callable. + + Note: complete matching of argument type hints and return types is not performed + """ + if callable(v): + return v + + raise errors.CallableError(value=v) + + +def enum_validator(v: Any) -> Enum: + if isinstance(v, Enum): + return v + + raise errors.EnumError(value=v) + + +def int_enum_validator(v: Any) -> IntEnum: + if isinstance(v, IntEnum): + return v + + raise errors.IntEnumError(value=v) + + +def make_literal_validator(type_: Any) -> Callable[[Any], Any]: + permitted_choices = all_literal_values(type_) + + # To have a O(1) complexity and still return one of the values set inside the `Literal`, + # we create a dict with the set values (a set causes some problems with the way intersection works). + # In some cases the set value and checked value can indeed be different (see `test_literal_validator_str_enum`) + allowed_choices = {v: v for v in permitted_choices} + + def literal_validator(v: Any) -> Any: + try: + return allowed_choices[v] + except (KeyError, TypeError): + raise errors.WrongConstantError(given=v, permitted=permitted_choices) + + return literal_validator + + +def constr_length_validator(v: 'StrBytes', field: 'ModelField', config: 'BaseConfig') -> 'StrBytes': + v_len = len(v) + + min_length = field.type_.min_length if field.type_.min_length is not None else config.min_anystr_length + if v_len < min_length: + raise errors.AnyStrMinLengthError(limit_value=min_length) + + max_length = field.type_.max_length if field.type_.max_length is not None else config.max_anystr_length + if max_length is not None and v_len > max_length: + raise errors.AnyStrMaxLengthError(limit_value=max_length) + + return v + + +def constr_strip_whitespace(v: 'StrBytes', field: 'ModelField', config: 'BaseConfig') -> 'StrBytes': + strip_whitespace = field.type_.strip_whitespace or config.anystr_strip_whitespace + if strip_whitespace: + v = v.strip() + + return v + + +def constr_upper(v: 'StrBytes', field: 'ModelField', config: 'BaseConfig') -> 'StrBytes': + upper = field.type_.to_upper or config.anystr_upper + if upper: + v = v.upper() + + return v + + +def constr_lower(v: 'StrBytes', field: 'ModelField', config: 'BaseConfig') -> 'StrBytes': + lower = field.type_.to_lower or config.anystr_lower + if lower: + v = v.lower() + return v + + +def validate_json(v: Any, config: 'BaseConfig') -> Any: + if v is None: + # pass None through to other validators + return v + try: + return config.json_loads(v) # type: ignore + except ValueError: + raise errors.JsonError() + except TypeError: + raise errors.JsonTypeError() + + +T = TypeVar('T') + + +def make_arbitrary_type_validator(type_: Type[T]) -> Callable[[T], T]: + def arbitrary_type_validator(v: Any) -> T: + if isinstance(v, type_): + return v + raise errors.ArbitraryTypeError(expected_arbitrary_type=type_) + + return arbitrary_type_validator + + +def make_class_validator(type_: Type[T]) -> Callable[[Any], Type[T]]: + def class_validator(v: Any) -> Type[T]: + if lenient_issubclass(v, type_): + return v + raise errors.SubclassError(expected_class=type_) + + return class_validator + + +def any_class_validator(v: Any) -> Type[T]: + if isinstance(v, type): + return v + raise errors.ClassError() + + +def none_validator(v: Any) -> 'Literal[None]': + if v is None: + return v + raise errors.NotNoneError() + + +def pattern_validator(v: Any) -> Pattern[str]: + if isinstance(v, Pattern): + return v + + str_value = str_validator(v) + + try: + return re.compile(str_value) + except re.error: + raise errors.PatternError() + + +NamedTupleT = TypeVar('NamedTupleT', bound=NamedTuple) + + +def make_namedtuple_validator( + namedtuple_cls: Type[NamedTupleT], config: Type['BaseConfig'] +) -> Callable[[Tuple[Any, ...]], NamedTupleT]: + from pydantic.v1.annotated_types import create_model_from_namedtuple + + NamedTupleModel = create_model_from_namedtuple( + namedtuple_cls, + __config__=config, + __module__=namedtuple_cls.__module__, + ) + namedtuple_cls.__pydantic_model__ = NamedTupleModel # type: ignore[attr-defined] + + def namedtuple_validator(values: Tuple[Any, ...]) -> NamedTupleT: + annotations = NamedTupleModel.__annotations__ + + if len(values) > len(annotations): + raise errors.ListMaxLengthError(limit_value=len(annotations)) + + dict_values: Dict[str, Any] = dict(zip(annotations, values)) + validated_dict_values: Dict[str, Any] = dict(NamedTupleModel(**dict_values)) + return namedtuple_cls(**validated_dict_values) + + return namedtuple_validator + + +def make_typeddict_validator( + typeddict_cls: Type['TypedDict'], config: Type['BaseConfig'] # type: ignore[valid-type] +) -> Callable[[Any], Dict[str, Any]]: + from pydantic.v1.annotated_types import create_model_from_typeddict + + TypedDictModel = create_model_from_typeddict( + typeddict_cls, + __config__=config, + __module__=typeddict_cls.__module__, + ) + typeddict_cls.__pydantic_model__ = TypedDictModel # type: ignore[attr-defined] + + def typeddict_validator(values: 'TypedDict') -> Dict[str, Any]: # type: ignore[valid-type] + return TypedDictModel.parse_obj(values).dict(exclude_unset=True) + + return typeddict_validator + + +class IfConfig: + def __init__(self, validator: AnyCallable, *config_attr_names: str, ignored_value: Any = False) -> None: + self.validator = validator + self.config_attr_names = config_attr_names + self.ignored_value = ignored_value + + def check(self, config: Type['BaseConfig']) -> bool: + return any(getattr(config, name) not in {None, self.ignored_value} for name in self.config_attr_names) + + +# order is important here, for example: bool is a subclass of int so has to come first, datetime before date same, +# IPv4Interface before IPv4Address, etc +_VALIDATORS: List[Tuple[Type[Any], List[Any]]] = [ + (IntEnum, [int_validator, enum_member_validator]), + (Enum, [enum_member_validator]), + ( + str, + [ + str_validator, + IfConfig(anystr_strip_whitespace, 'anystr_strip_whitespace'), + IfConfig(anystr_upper, 'anystr_upper'), + IfConfig(anystr_lower, 'anystr_lower'), + IfConfig(anystr_length_validator, 'min_anystr_length', 'max_anystr_length'), + ], + ), + ( + bytes, + [ + bytes_validator, + IfConfig(anystr_strip_whitespace, 'anystr_strip_whitespace'), + IfConfig(anystr_upper, 'anystr_upper'), + IfConfig(anystr_lower, 'anystr_lower'), + IfConfig(anystr_length_validator, 'min_anystr_length', 'max_anystr_length'), + ], + ), + (bool, [bool_validator]), + (int, [int_validator]), + (float, [float_validator, IfConfig(float_finite_validator, 'allow_inf_nan', ignored_value=True)]), + (Path, [path_validator]), + (datetime, [parse_datetime]), + (date, [parse_date]), + (time, [parse_time]), + (timedelta, [parse_duration]), + (OrderedDict, [ordered_dict_validator]), + (dict, [dict_validator]), + (list, [list_validator]), + (tuple, [tuple_validator]), + (set, [set_validator]), + (frozenset, [frozenset_validator]), + (deque, [deque_validator]), + (UUID, [uuid_validator]), + (Decimal, [decimal_validator]), + (IPv4Interface, [ip_v4_interface_validator]), + (IPv6Interface, [ip_v6_interface_validator]), + (IPv4Address, [ip_v4_address_validator]), + (IPv6Address, [ip_v6_address_validator]), + (IPv4Network, [ip_v4_network_validator]), + (IPv6Network, [ip_v6_network_validator]), +] + + +def find_validators( # noqa: C901 (ignore complexity) + type_: Type[Any], config: Type['BaseConfig'] +) -> Generator[AnyCallable, None, None]: + from pydantic.v1.dataclasses import is_builtin_dataclass, make_dataclass_validator + + if type_ is Any or type_ is object: + return + type_type = type_.__class__ + if type_type == ForwardRef or type_type == TypeVar: + return + + if is_none_type(type_): + yield none_validator + return + if type_ is Pattern or type_ is re.Pattern: + yield pattern_validator + return + if type_ is Hashable or type_ is CollectionsHashable: + yield hashable_validator + return + if is_callable_type(type_): + yield callable_validator + return + if is_literal_type(type_): + yield make_literal_validator(type_) + return + if is_builtin_dataclass(type_): + yield from make_dataclass_validator(type_, config) + return + if type_ is Enum: + yield enum_validator + return + if type_ is IntEnum: + yield int_enum_validator + return + if is_namedtuple(type_): + yield tuple_validator + yield make_namedtuple_validator(type_, config) + return + if is_typeddict(type_): + yield make_typeddict_validator(type_, config) + return + + class_ = get_class(type_) + if class_ is not None: + if class_ is not Any and isinstance(class_, type): + yield make_class_validator(class_) + else: + yield any_class_validator + return + + for val_type, validators in _VALIDATORS: + try: + if issubclass(type_, val_type): + for v in validators: + if isinstance(v, IfConfig): + if v.check(config): + yield v.validator + else: + yield v + return + except TypeError: + raise RuntimeError(f'error checking inheritance of {type_!r} (type: {display_as_type(type_)})') + + if config.arbitrary_types_allowed: + yield make_arbitrary_type_validator(type_) + else: + if hasattr(type_, '__pydantic_core_schema__'): + warn(f'Mixing V1 and V2 models is not supported. `{type_.__name__}` is a V2 model.', UserWarning) + raise RuntimeError(f'no validator found for {type_}, see `arbitrary_types_allowed` in Config') diff --git a/source/target-vendors/cp312/pydantic/v1/version.py b/source/target-vendors/cp312/pydantic/v1/version.py new file mode 100644 index 0000000..6ba1cb0 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/v1/version.py @@ -0,0 +1,38 @@ +__all__ = 'compiled', 'VERSION', 'version_info' + +VERSION = '1.10.26' + +try: + import cython # type: ignore +except ImportError: + compiled: bool = False +else: # pragma: no cover + try: + compiled = cython.compiled + except AttributeError: + compiled = False + + +def version_info() -> str: + import platform + import sys + from importlib import import_module + from pathlib import Path + + optional_deps = [] + for p in ('devtools', 'dotenv', 'email-validator', 'typing-extensions'): + try: + import_module(p.replace('-', '_')) + except ImportError: + continue + optional_deps.append(p) + + info = { + 'pydantic version': VERSION, + 'pydantic compiled': compiled, + 'install path': Path(__file__).resolve().parent, + 'python version': sys.version, + 'platform': platform.platform(), + 'optional deps. installed': optional_deps, + } + return '\n'.join('{:>30} {}'.format(k + ':', str(v).replace('\n', ' ')) for k, v in info.items()) diff --git a/source/target-vendors/cp312/pydantic/validate_call_decorator.py b/source/target-vendors/cp312/pydantic/validate_call_decorator.py new file mode 100644 index 0000000..4bc8991 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/validate_call_decorator.py @@ -0,0 +1,116 @@ +"""Decorator for validating function calls.""" + +from __future__ import annotations as _annotations + +import inspect +from functools import partial +from types import BuiltinFunctionType +from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast, overload + +from ._internal import _generate_schema, _typing_extra, _validate_call +from .errors import PydanticUserError + +__all__ = ('validate_call',) + +if TYPE_CHECKING: + from .config import ConfigDict + + AnyCallableT = TypeVar('AnyCallableT', bound=Callable[..., Any]) + + +_INVALID_TYPE_ERROR_CODE = 'validate-call-type' + + +def _check_function_type(function: object) -> None: + """Check if the input function is a supported type for `validate_call`.""" + if isinstance(function, _generate_schema.VALIDATE_CALL_SUPPORTED_TYPES): + try: + _typing_extra.signature_no_eval(cast(_generate_schema.ValidateCallSupportedTypes, function)) + except (ValueError, TypeError): + raise PydanticUserError( + f"Input function `{function}` doesn't have a valid signature", code=_INVALID_TYPE_ERROR_CODE + ) + + if isinstance(function, partial): + try: + assert not isinstance(partial.func, partial), 'Partial of partial' + _check_function_type(function.func) + except PydanticUserError as e: + raise PydanticUserError( + f'Partial of `{function.func}` is invalid because the type of `{function.func}` is not supported by `validate_call`', + code=_INVALID_TYPE_ERROR_CODE, + ) from e + + return + + if isinstance(function, BuiltinFunctionType): + raise PydanticUserError(f'Input built-in function `{function}` is not supported', code=_INVALID_TYPE_ERROR_CODE) + if isinstance(function, (classmethod, staticmethod, property)): + name = type(function).__name__ + raise PydanticUserError( + f'The `@{name}` decorator should be applied after `@validate_call` (put `@{name}` on top)', + code=_INVALID_TYPE_ERROR_CODE, + ) + + if inspect.isclass(function): + raise PydanticUserError( + f'Unable to validate {function}: `validate_call` should be applied to functions, not classes (put `@validate_call` on top of `__init__` or `__new__` instead)', + code=_INVALID_TYPE_ERROR_CODE, + ) + if callable(function): + raise PydanticUserError( + f'Unable to validate {function}: `validate_call` should be applied to functions, not instances or other callables. Use `validate_call` explicitly on `__call__` instead.', + code=_INVALID_TYPE_ERROR_CODE, + ) + + raise PydanticUserError( + f'Unable to validate {function}: `validate_call` should be applied to one of the following: function, method, partial, or lambda', + code=_INVALID_TYPE_ERROR_CODE, + ) + + +@overload +def validate_call( + *, config: ConfigDict | None = None, validate_return: bool = False +) -> Callable[[AnyCallableT], AnyCallableT]: ... + + +@overload +def validate_call(func: AnyCallableT, /) -> AnyCallableT: ... + + +def validate_call( + func: AnyCallableT | None = None, + /, + *, + config: ConfigDict | None = None, + validate_return: bool = False, +) -> AnyCallableT | Callable[[AnyCallableT], AnyCallableT]: + """!!! abstract "Usage Documentation" + [Validation Decorator](../concepts/validation_decorator.md) + + Returns a decorated wrapper around the function that validates the arguments and, optionally, the return value. + + Usage may be either as a plain decorator `@validate_call` or with arguments `@validate_call(...)`. + + Args: + func: The function to be decorated. + config: The configuration dictionary. + validate_return: Whether to validate the return value. + + Returns: + The decorated function. + """ + parent_namespace = _typing_extra.parent_frame_namespace() + + def validate(function: AnyCallableT) -> AnyCallableT: + _check_function_type(function) + validate_call_wrapper = _validate_call.ValidateCallWrapper( + cast(_generate_schema.ValidateCallSupportedTypes, function), config, validate_return, parent_namespace + ) + return _validate_call.update_wrapper_attributes(function, validate_call_wrapper.__call__) # type: ignore + + if func is not None: + return validate(func) + else: + return validate diff --git a/source/target-vendors/cp312/pydantic/validators.py b/source/target-vendors/cp312/pydantic/validators.py new file mode 100644 index 0000000..7921b04 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/validators.py @@ -0,0 +1,5 @@ +"""The `validators` module is a backport module from V1.""" + +from ._migration import getattr_migration + +__getattr__ = getattr_migration(__name__) diff --git a/source/target-vendors/cp312/pydantic/version.py b/source/target-vendors/cp312/pydantic/version.py new file mode 100644 index 0000000..560f34a --- /dev/null +++ b/source/target-vendors/cp312/pydantic/version.py @@ -0,0 +1,113 @@ +"""The `version` module holds the version information for Pydantic.""" + +from __future__ import annotations as _annotations + +import sys + +from pydantic_core import __version__ as __pydantic_core_version__ + +__all__ = 'VERSION', 'version_info' + +VERSION = '2.13.5' +"""The version of Pydantic. + +This version specifier is guaranteed to be compliant with the [specification], +introduced by [PEP 440]. + +[specification]: https://packaging.python.org/en/latest/specifications/version-specifiers/ +[PEP 440]: https://peps.python.org/pep-0440/ +""" + +# Keep this in sync with the version constraint in the `pyproject.toml` dependencies: +_COMPATIBLE_PYDANTIC_CORE_VERSION = '2.46.5' + + +def version_short() -> str: + """Return the `major.minor` part of Pydantic version. + + It returns '2.1' if Pydantic version is '2.1.1'. + """ + return '.'.join(VERSION.split('.')[:2]) + + +def version_info() -> str: + """Return complete version information for Pydantic and its dependencies.""" + import importlib.metadata + import platform + from pathlib import Path + + import pydantic_core._pydantic_core as pdc + + from ._internal import _git as git + + # get data about packages that are closely related to pydantic, use pydantic or often conflict with pydantic + package_names = { + 'email-validator', + 'fastapi', + 'mypy', + 'pydantic-extra-types', + 'pydantic-settings', + 'pyright', + 'typing_extensions', + } + related_packages = [] + + for dist in importlib.metadata.distributions(): + name = dist.metadata['Name'] + if name in package_names: + related_packages.append(f'{name}-{dist.version}') + + pydantic_dir = Path(__file__).parents[1].resolve() + most_recent_commit = ( + git.git_revision(pydantic_dir) if git.is_git_repo(pydantic_dir) and git.have_git() else 'unknown' + ) + + info = { + 'pydantic version': VERSION, + 'pydantic-core version': __pydantic_core_version__, + 'pydantic-core build': getattr(pdc, 'build_info', None) or pdc.build_profile, # pyright: ignore[reportPrivateImportUsage] + 'python version': sys.version, + 'platform': platform.platform(), + 'related packages': ' '.join(related_packages), + 'commit': most_recent_commit, + } + return '\n'.join('{:>30} {}'.format(k + ':', str(v).replace('\n', ' ')) for k, v in info.items()) + + +def check_pydantic_core_version() -> bool: + """Check that the installed `pydantic-core` dependency is compatible.""" + return __pydantic_core_version__ == _COMPATIBLE_PYDANTIC_CORE_VERSION + + +def _ensure_pydantic_core_version() -> None: # pragma: no cover + if not check_pydantic_core_version(): + raise_error = True + # Do not raise the error if pydantic is installed in editable mode (i.e. in development): + if sys.version_info >= (3, 13): # origin property added in 3.13 + from importlib.metadata import distribution + + dist = distribution('pydantic') + if getattr(getattr(dist.origin, 'dir_info', None), 'editable', False): + raise_error = False + + if raise_error: + raise SystemError( + f'The installed pydantic-core version ({__pydantic_core_version__}) is incompatible ' + f'with the current pydantic version, which requires {_COMPATIBLE_PYDANTIC_CORE_VERSION}. ' + "If you encounter this error, make sure that you haven't upgraded pydantic-core manually." + ) + + +def parse_mypy_version(version: str) -> tuple[int, int, int]: + """Parse `mypy` string version to a 3-tuple of ints. + + It parses normal version like `1.11.0` and extra info followed by a `+` sign + like `1.11.0+dev.d6d9d8cd4f27c52edac1f537e236ec48a01e54cb.dirty`. + + Args: + version: The mypy version string. + + Returns: + A triple of ints, e.g. `(1, 11, 0)`. + """ + return tuple(map(int, version.partition('+')[0].split('.'))) # pyright: ignore[reportReturnType] diff --git a/source/target-vendors/cp312/pydantic/warnings.py b/source/target-vendors/cp312/pydantic/warnings.py new file mode 100644 index 0000000..02323b8 --- /dev/null +++ b/source/target-vendors/cp312/pydantic/warnings.py @@ -0,0 +1,122 @@ +"""Pydantic-specific warnings.""" + +from __future__ import annotations as _annotations + +from .version import version_short + +__all__ = ( + 'PydanticDeprecatedSince20', + 'PydanticDeprecatedSince26', + 'PydanticDeprecatedSince29', + 'PydanticDeprecatedSince210', + 'PydanticDeprecatedSince211', + 'PydanticDeprecatedSince212', + 'PydanticDeprecationWarning', + 'PydanticExperimentalWarning', + 'ArbitraryTypeWarning', + 'UnsupportedFieldAttributeWarning', + 'TypedDictExtraConfigWarning', +) + + +class PydanticDeprecationWarning(DeprecationWarning): + """A Pydantic specific deprecation warning. + + This warning is raised when using deprecated functionality in Pydantic. It provides information on when the + deprecation was introduced and the expected version in which the corresponding functionality will be removed. + + Attributes: + message: Description of the warning. + since: Pydantic version in what the deprecation was introduced. + expected_removal: Pydantic version in what the corresponding functionality expected to be removed. + """ + + message: str + since: tuple[int, int] + expected_removal: tuple[int, int] + + def __init__( + self, message: str, *args: object, since: tuple[int, int], expected_removal: tuple[int, int] | None = None + ) -> None: + super().__init__(message, *args) + self.message = message.rstrip('.') + self.since = since + self.expected_removal = expected_removal if expected_removal is not None else (since[0] + 1, 0) + + def __str__(self) -> str: + message = ( + f'{self.message}. Deprecated in Pydantic V{self.since[0]}.{self.since[1]}' + f' to be removed in V{self.expected_removal[0]}.{self.expected_removal[1]}.' + ) + if self.since == (2, 0): + message += f' See Pydantic V2 Migration Guide at https://errors.pydantic.dev/{version_short()}/migration/' + return message + + +class PydanticDeprecatedSince20(PydanticDeprecationWarning): + """A specific `PydanticDeprecationWarning` subclass defining functionality deprecated since Pydantic 2.0.""" + + def __init__(self, message: str, *args: object) -> None: + super().__init__(message, *args, since=(2, 0), expected_removal=(3, 0)) + + +class PydanticDeprecatedSince26(PydanticDeprecationWarning): # pragma: no cover + """A specific `PydanticDeprecationWarning` subclass defining functionality deprecated since Pydantic 2.6.""" + + def __init__(self, message: str, *args: object) -> None: + super().__init__(message, *args, since=(2, 6), expected_removal=(3, 0)) + + +class PydanticDeprecatedSince29(PydanticDeprecationWarning): + """A specific `PydanticDeprecationWarning` subclass defining functionality deprecated since Pydantic 2.9.""" + + def __init__(self, message: str, *args: object) -> None: + super().__init__(message, *args, since=(2, 9), expected_removal=(3, 0)) + + +class PydanticDeprecatedSince210(PydanticDeprecationWarning): + """A specific `PydanticDeprecationWarning` subclass defining functionality deprecated since Pydantic 2.10.""" + + def __init__(self, message: str, *args: object) -> None: + super().__init__(message, *args, since=(2, 10), expected_removal=(3, 0)) + + +class PydanticDeprecatedSince211(PydanticDeprecationWarning): + """A specific `PydanticDeprecationWarning` subclass defining functionality deprecated since Pydantic 2.11.""" + + def __init__(self, message: str, *args: object) -> None: + super().__init__(message, *args, since=(2, 11), expected_removal=(3, 0)) + + +class PydanticDeprecatedSince212(PydanticDeprecationWarning): + """A specific `PydanticDeprecationWarning` subclass defining functionality deprecated since Pydantic 2.12.""" + + def __init__(self, message: str, *args: object) -> None: + super().__init__(message, *args, since=(2, 12), expected_removal=(3, 0)) + + +class GenericBeforeBaseModelWarning(Warning): + pass + + +class PydanticExperimentalWarning(Warning): + """A Pydantic specific experimental functionality warning. + + It is raised to warn users that the functionality may change or be removed in future versions of Pydantic. + """ + + +class CoreSchemaGenerationWarning(UserWarning): + """A warning raised during core schema generation.""" + + +class ArbitraryTypeWarning(CoreSchemaGenerationWarning): + """A warning raised when Pydantic fails to generate a core schema for an arbitrary type.""" + + +class UnsupportedFieldAttributeWarning(CoreSchemaGenerationWarning): + """A warning raised when a `Field()` attribute isn't supported in the context it is used.""" + + +class TypedDictExtraConfigWarning(CoreSchemaGenerationWarning): + """A warning raised when the [`extra`][pydantic.ConfigDict.extra] configuration is incompatible with the `closed` or `extra_items` specification.""" diff --git a/source/target-vendors/cp312/pydantic_core-2.46.5.dist-info/METADATA b/source/target-vendors/cp312/pydantic_core-2.46.5.dist-info/METADATA new file mode 100644 index 0000000..d3f72db --- /dev/null +++ b/source/target-vendors/cp312/pydantic_core-2.46.5.dist-info/METADATA @@ -0,0 +1,173 @@ +Metadata-Version: 2.4 +Name: pydantic_core +Version: 2.46.5 +Classifier: Development Status :: 3 - Alpha +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Programming Language :: Python :: Implementation :: GraalPy +Classifier: Programming Language :: Rust +Classifier: Framework :: Pydantic +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Information Technology +Classifier: Operating System :: POSIX :: Linux +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: MacOS +Classifier: Typing :: Typed +Requires-Dist: typing-extensions>=4.14.1 +License-File: LICENSE +Summary: Core functionality for Pydantic validation and serialization +Home-Page: https://github.com/pydantic/pydantic +Author-email: Samuel Colvin , Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>, David Montague , David Hewitt , Sydney Runkle , Victorien Plot +License-Expression: MIT +Requires-Python: >=3.9 +Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM +Project-URL: Funding, https://github.com/sponsors/samuelcolvin +Project-URL: Homepage, https://github.com/pydantic +Project-URL: Source, https://github.com/pydantic/pydantic/tree/main/pydantic-core + +# pydantic-core + +[![CI](https://github.com/pydantic/pydantic-core/workflows/ci/badge.svg?event=push)](https://github.com/pydantic/pydantic-core/actions?query=event%3Apush+branch%3Amain+workflow%3Aci) +[![Coverage](https://codecov.io/gh/pydantic/pydantic-core/branch/main/graph/badge.svg)](https://codecov.io/gh/pydantic/pydantic-core) +[![pypi](https://img.shields.io/pypi/v/pydantic-core.svg)](https://pypi.python.org/pypi/pydantic-core) +[![versions](https://img.shields.io/pypi/pyversions/pydantic-core.svg)](https://github.com/pydantic/pydantic-core) +[![license](https://img.shields.io/github/license/pydantic/pydantic-core.svg)](https://github.com/pydantic/pydantic-core/blob/main/LICENSE) + +This package provides the core functionality for [pydantic](https://docs.pydantic.dev) validation and serialization. + +Pydantic-core is currently around 17x faster than pydantic V1. +See [`tests/benchmarks/`](./tests/benchmarks/) for details. + +## Example of direct usage + +*NOTE: You should not need to use pydantic-core directly; instead, use pydantic, which in turn uses pydantic-core.* + +```py +from pydantic_core import SchemaValidator, ValidationError + + +v = SchemaValidator( + { + 'type': 'typed-dict', + 'fields': { + 'name': { + 'type': 'typed-dict-field', + 'schema': { + 'type': 'str', + }, + }, + 'age': { + 'type': 'typed-dict-field', + 'schema': { + 'type': 'int', + 'ge': 18, + }, + }, + 'is_developer': { + 'type': 'typed-dict-field', + 'schema': { + 'type': 'default', + 'schema': {'type': 'bool'}, + 'default': True, + }, + }, + }, + } +) + +r1 = v.validate_python({'name': 'Samuel', 'age': 35}) +assert r1 == {'name': 'Samuel', 'age': 35, 'is_developer': True} + +# pydantic-core can also validate JSON directly +r2 = v.validate_json('{"name": "Samuel", "age": 35}') +assert r1 == r2 + +try: + v.validate_python({'name': 'Samuel', 'age': 11}) +except ValidationError as e: + print(e) + """ + 1 validation error for model + age + Input should be greater than or equal to 18 + [type=greater_than_equal, context={ge: 18}, input_value=11, input_type=int] + """ +``` + +## Getting Started + +### Prerequisites + +You'll need: + +1. **[Rust](https://rustup.rs/)** - Rust stable (or nightly for coverage) +2. **[uv](https://docs.astral.sh/uv/getting-started/installation/)** - Fast Python package manager (will install Python 3.9+ automatically) +3. **[git](https://git-scm.com/)** - For version control +4. **[make](https://www.gnu.org/software/make/)** - For running development commands (or use `nmake` on Windows) + +### Quick Start + +```bash +# Clone the repository (or from your fork) +git clone git@github.com:pydantic/pydantic-core.git +cd pydantic-core + +# Install all dependencies using uv, setup pre-commit hooks, and build the development version +make install +``` + +Verify your installation by running: + +```bash +make +``` + +This runs a full development cycle: formatting, building, linting, and testing + +### Development Commands + +Run `make help` to see all available commands, or use these common ones: + +```bash +make build-dev # to build the package during development +make build-prod # to perform an optimised build for benchmarking +make test # to run the tests +make testcov # to run the tests and generate a coverage report +make lint # to run the linter +make format # to format python and rust code +make all # to run to run build-dev + format + lint + test +``` + +### Useful Resources + +* [`python/pydantic_core/_pydantic_core.pyi`](./python/pydantic_core/_pydantic_core.pyi) - Python API types +* [`python/pydantic_core/core_schema.py`](./python/pydantic_core/core_schema.py) - Core schema definitions +* [`tests/`](./tests) - Comprehensive usage examples + +## Profiling + +It's possible to profile the code using the [`flamegraph` utility from `flamegraph-rs`](https://github.com/flamegraph-rs/flamegraph). (Tested on Linux.) You can install this with `cargo install flamegraph`. + +Run `make build-profiling` to install a release build with debugging symbols included (needed for profiling). + +Once that is built, you can profile pytest benchmarks with (e.g.): + +```bash +flamegraph -- pytest tests/benchmarks/test_micro_benchmarks.py -k test_list_of_ints_core_py --benchmark-enable +``` + +The `flamegraph` command will produce an interactive SVG at `flamegraph.svg`. + +## Releasing + +TBC (needs to be integrated into `pydantic` repository release process). + diff --git a/source/target-vendors/cp312/pydantic_core-2.46.5.dist-info/RECORD b/source/target-vendors/cp312/pydantic_core-2.46.5.dist-info/RECORD new file mode 100644 index 0000000..4197d83 --- /dev/null +++ b/source/target-vendors/cp312/pydantic_core-2.46.5.dist-info/RECORD @@ -0,0 +1,10 @@ +pydantic_core/__init__.py,sha256=nK1ikrdSVK9gapcKrpv_blrp8LCAic1jrK-jkbYHlNI,5115 +pydantic_core/_pydantic_core.cpython-312-x86_64-linux-gnu.so,sha256=lfaO8lNaUQKgNELgc9SQbAEhGyCJAvVuJOwjzqQCtaA,4692312 +pydantic_core/_pydantic_core.pyi,sha256=H_lG8iKiv3Bpfd3rixY7xzs67gE-s0eiAqJaSbnF8Ss,45932 +pydantic_core/core_schema.py,sha256=1PM31yf_kGpm_H7GLnNFRkIPAQGPL9zh6rf2SxohVxg,155574 +pydantic_core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pydantic_core-2.46.5.dist-info/METADATA,sha256=KXaMA4wLs1ZMCofvUO940YojckdL3vC6NWqMD9S9YCg,6573 +pydantic_core-2.46.5.dist-info/WHEEL,sha256=cdZhYY2bcH9hrDcqDVEEdpL0M5tjmmAr-lwUq15CEdY,147 +pydantic_core-2.46.5.dist-info/licenses/LICENSE,sha256=Kv3TDVS01itvSIprzBVG6E7FBh8T9CCcA9ASNIeDeVo,1080 +pydantic_core-2.46.5.dist-info/sboms/pydantic-core.cyclonedx.json,sha256=T0DHkPDLOEp5NmMzGQQRepFKUO8iCxEsOZ138UY-PdU,125376 +pydantic_core-2.46.5.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/pydantic_core-2.46.5.dist-info/WHEEL b/source/target-vendors/cp312/pydantic_core-2.46.5.dist-info/WHEEL new file mode 100644 index 0000000..8cb0d6d --- /dev/null +++ b/source/target-vendors/cp312/pydantic_core-2.46.5.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: maturin (1.15.0) +Root-Is-Purelib: false +Tag: cp312-cp312-manylinux_2_17_x86_64 +Tag: cp312-cp312-manylinux2014_x86_64 diff --git a/source/target-vendors/cp312/pydantic_core-2.46.5.dist-info/licenses/LICENSE b/source/target-vendors/cp312/pydantic_core-2.46.5.dist-info/licenses/LICENSE new file mode 100644 index 0000000..0716871 --- /dev/null +++ b/source/target-vendors/cp312/pydantic_core-2.46.5.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2022 Samuel Colvin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/source/target-vendors/cp312/pydantic_core-2.46.5.dist-info/sboms/pydantic-core.cyclonedx.json b/source/target-vendors/cp312/pydantic_core-2.46.5.dist-info/sboms/pydantic-core.cyclonedx.json new file mode 100644 index 0000000..33f43e2 --- /dev/null +++ b/source/target-vendors/cp312/pydantic_core-2.46.5.dist-info/sboms/pydantic-core.cyclonedx.json @@ -0,0 +1,3961 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "urn:uuid:ebbad0c8-2245-43ff-8954-129abd5895b6", + "metadata": { + "timestamp": "2026-08-28T09:11:36.556516169Z", + "tools": [ + { + "vendor": "CycloneDX", + "name": "cargo-cyclonedx", + "version": "0.5.9" + } + ], + "component": { + "type": "library", + "bom-ref": "path+file:///home/runner/work/pydantic/pydantic/pydantic-core#2.46.5", + "name": "pydantic-core", + "version": "2.46.5", + "scope": "required", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/pydantic-core@2.46.5?download_url=file://.", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/pydantic/pydantic" + }, + { + "type": "vcs", + "url": "https://github.com/pydantic/pydantic.git" + } + ], + "components": [ + { + "type": "library", + "bom-ref": "path+file:///home/runner/work/pydantic/pydantic/pydantic-core#2.46.5 bin-target-0", + "name": "_pydantic_core", + "version": "2.46.5", + "purl": "pkg:cargo/pydantic-core@2.46.5?download_url=file://.#src/lib.rs" + } + ] + }, + "properties": [ + { + "name": "cdx:rustc:sbom:target:all_targets", + "value": "true" + } + ] + }, + "components": [ + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#ahash@0.8.12", + "author": "Tom Kaitchuck ", + "name": "ahash", + "version": "0.8.12", + "description": "A non-cryptographic hash function using AES-NI for high performance", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/ahash@0.8.12", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/ahash" + }, + { + "type": "vcs", + "url": "https://github.com/tkaitchuck/ahash" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.3", + "author": "Andrew Gallant ", + "name": "aho-corasick", + "version": "1.1.3", + "description": "Fast multiple substring searching.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" + } + ], + "licenses": [ + { + "expression": "Unlicense OR MIT" + } + ], + "purl": "pkg:cargo/aho-corasick@1.1.3", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/BurntSushi/aho-corasick" + }, + { + "type": "vcs", + "url": "https://github.com/BurntSushi/aho-corasick" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#allocator-api2@0.2.21", + "author": "Zakarum ", + "name": "allocator-api2", + "version": "0.2.21", + "description": "Mirror of Rust's allocator API", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/allocator-api2@0.2.21", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/allocator-api2" + }, + { + "type": "website", + "url": "https://github.com/zakarumych/allocator-api2" + }, + { + "type": "vcs", + "url": "https://github.com/zakarumych/allocator-api2" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#autocfg@1.3.0", + "author": "Josh Stone ", + "name": "autocfg", + "version": "1.3.0", + "description": "Automatic cfg for Rust compiler features", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/autocfg@1.3.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/autocfg/" + }, + { + "type": "vcs", + "url": "https://github.com/cuviper/autocfg" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#base64@0.22.1", + "author": "Marshall Pierce ", + "name": "base64", + "version": "0.22.1", + "description": "encodes and decodes base64 as bytes or utf8", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/base64@0.22.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/base64" + }, + { + "type": "vcs", + "url": "https://github.com/marshallpierce/rust-base64" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#bitflags@2.9.1", + "author": "The Rust Project Developers", + "name": "bitflags", + "version": "2.9.1", + "description": "A macro to generate structures which behave like bitflags. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/bitflags@2.9.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/bitflags" + }, + { + "type": "website", + "url": "https://github.com/bitflags/bitflags" + }, + { + "type": "vcs", + "url": "https://github.com/bitflags/bitflags" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#bitvec@1.0.1", + "name": "bitvec", + "version": "1.0.1", + "description": "Addresses memory by bits, for packed collections and bitfields", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/bitvec@1.0.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/bitvec/latest/bitvec" + }, + { + "type": "website", + "url": "https://bitvecto-rs.github.io/bitvec" + }, + { + "type": "vcs", + "url": "https://github.com/bitvecto-rs/bitvec" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#bumpalo@3.19.0", + "author": "Nick Fitzgerald ", + "name": "bumpalo", + "version": "3.19.0", + "description": "A fast bump allocation arena for Rust.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/bumpalo@3.19.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/bumpalo" + }, + { + "type": "vcs", + "url": "https://github.com/fitzgen/bumpalo" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#cc@1.0.101", + "author": "Alex Crichton ", + "name": "cc", + "version": "1.0.101", + "description": "A build-time dependency for Cargo build scripts to assist in invoking the native C compiler to compile native C code into a static archive to be linked into Rust code. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/cc@1.0.101", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/cc" + }, + { + "type": "website", + "url": "https://github.com/rust-lang/cc-rs" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/cc-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.0", + "author": "Alex Crichton ", + "name": "cfg-if", + "version": "1.0.0", + "description": "A macro to ergonomically define an item depending on a large number of #[cfg] parameters. Structured like an if-else chain, the first matching branch is the item that gets emitted. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/cfg-if@1.0.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/cfg-if" + }, + { + "type": "website", + "url": "https://github.com/alexcrichton/cfg-if" + }, + { + "type": "vcs", + "url": "https://github.com/alexcrichton/cfg-if" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#displaydoc@0.2.5", + "author": "Jane Lusby ", + "name": "displaydoc", + "version": "0.2.5", + "description": "A derive macro for implementing the display Trait via a doc comment and string interpolation ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/displaydoc@0.2.5", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/displaydoc" + }, + { + "type": "website", + "url": "https://github.com/yaahc/displaydoc" + }, + { + "type": "vcs", + "url": "https://github.com/yaahc/displaydoc" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#enum_dispatch@0.3.13", + "author": "Anton Lazarev ", + "name": "enum_dispatch", + "version": "0.3.13", + "description": "Near drop-in replacement for dynamic-dispatched method calls with up to 10x the speed", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/enum_dispatch@0.3.13", + "externalReferences": [ + { + "type": "vcs", + "url": "https://gitlab.com/antonok/enum_dispatch" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#equivalent@1.0.2", + "name": "equivalent", + "version": "1.0.2", + "description": "Traits for key comparison in maps.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/equivalent@1.0.2", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/indexmap-rs/equivalent" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#foldhash@0.2.0", + "author": "Orson Peters ", + "name": "foldhash", + "version": "0.2.0", + "description": "A fast, non-cryptographic, minimally DoS-resistant hashing algorithm.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + } + ], + "licenses": [ + { + "expression": "Zlib" + } + ], + "purl": "pkg:cargo/foldhash@0.2.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/orlp/foldhash" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#form_urlencoded@1.2.2", + "author": "The rust-url developers", + "name": "form_urlencoded", + "version": "1.2.2", + "description": "Parser and serializer for the application/x-www-form-urlencoded syntax, as used by HTML forms.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/form_urlencoded@1.2.2", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/servo/rust-url" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#funty@2.0.0", + "author": "myrrlyn ", + "name": "funty", + "version": "2.0.0", + "description": "Trait generalization over the primitive types", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/funty@2.0.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/funty" + }, + { + "type": "vcs", + "url": "https://github.com/myrrlyn/funty" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.3.3", + "author": "The Rand Project Developers", + "name": "getrandom", + "version": "0.3.3", + "description": "A small cross-platform library for retrieving random data from system source", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/getrandom@0.3.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/getrandom" + }, + { + "type": "vcs", + "url": "https://github.com/rust-random/getrandom" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.16.1", + "author": "Amanieu d'Antras ", + "name": "hashbrown", + "version": "0.16.1", + "description": "A Rust port of Google's SwissTable hash map", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/hashbrown@0.16.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-lang/hashbrown" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0", + "name": "heck", + "version": "0.5.0", + "description": "heck is a case conversion library.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/heck@0.5.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/withoutboats/heck" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#hex@0.4.3", + "author": "KokaKiwi ", + "name": "hex", + "version": "0.4.3", + "description": "Encoding and decoding data into/from hexadecimal representation.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/hex@0.4.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/hex/" + }, + { + "type": "vcs", + "url": "https://github.com/KokaKiwi/rust-hex" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#icu_collections@1.5.0", + "author": "The ICU4X Project Developers", + "name": "icu_collections", + "version": "1.5.0", + "description": "Collection of API for use in ICU libraries.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/icu_collections@1.5.0", + "externalReferences": [ + { + "type": "website", + "url": "https://icu4x.unicode.org" + }, + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#icu_locid@1.5.0", + "author": "The ICU4X Project Developers", + "name": "icu_locid", + "version": "1.5.0", + "description": "API for managing Unicode Language and Locale Identifiers", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/icu_locid@1.5.0", + "externalReferences": [ + { + "type": "website", + "url": "https://icu4x.unicode.org" + }, + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#icu_locid_transform@1.5.0", + "author": "The ICU4X Project Developers", + "name": "icu_locid_transform", + "version": "1.5.0", + "description": "API for Unicode Language and Locale Identifiers canonicalization", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/icu_locid_transform@1.5.0", + "externalReferences": [ + { + "type": "website", + "url": "https://icu4x.unicode.org" + }, + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#icu_locid_transform_data@1.5.0", + "author": "The ICU4X Project Developers", + "name": "icu_locid_transform_data", + "version": "1.5.0", + "description": "Data for the icu_locid_transform crate", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/icu_locid_transform_data@1.5.0", + "externalReferences": [ + { + "type": "website", + "url": "https://icu4x.unicode.org" + }, + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#icu_normalizer@1.5.0", + "author": "The ICU4X Project Developers", + "name": "icu_normalizer", + "version": "1.5.0", + "description": "API for normalizing text into Unicode Normalization Forms", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/icu_normalizer@1.5.0", + "externalReferences": [ + { + "type": "website", + "url": "https://icu4x.unicode.org" + }, + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#icu_normalizer_data@1.5.0", + "author": "The ICU4X Project Developers", + "name": "icu_normalizer_data", + "version": "1.5.0", + "description": "Data for the icu_normalizer crate", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/icu_normalizer_data@1.5.0", + "externalReferences": [ + { + "type": "website", + "url": "https://icu4x.unicode.org" + }, + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#icu_properties@1.5.1", + "author": "The ICU4X Project Developers", + "name": "icu_properties", + "version": "1.5.1", + "description": "Definitions for Unicode properties", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/icu_properties@1.5.1", + "externalReferences": [ + { + "type": "website", + "url": "https://icu4x.unicode.org" + }, + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#icu_properties_data@1.5.0", + "author": "The ICU4X Project Developers", + "name": "icu_properties_data", + "version": "1.5.0", + "description": "Data for the icu_properties crate", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/icu_properties_data@1.5.0", + "externalReferences": [ + { + "type": "website", + "url": "https://icu4x.unicode.org" + }, + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#icu_provider@1.5.0", + "author": "The ICU4X Project Developers", + "name": "icu_provider", + "version": "1.5.0", + "description": "Trait and struct definitions for the ICU data provider", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/icu_provider@1.5.0", + "externalReferences": [ + { + "type": "website", + "url": "https://icu4x.unicode.org" + }, + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#icu_provider_macros@1.5.0", + "author": "The ICU4X Project Developers", + "name": "icu_provider_macros", + "version": "1.5.0", + "description": "Proc macros for ICU data providers", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/icu_provider_macros@1.5.0", + "externalReferences": [ + { + "type": "website", + "url": "https://icu4x.unicode.org" + }, + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#idna@1.1.0", + "author": "The rust-url developers", + "name": "idna", + "version": "1.1.0", + "description": "IDNA (Internationalizing Domain Names in Applications) and Punycode.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/idna@1.1.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/servo/rust-url/" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#idna_adapter@1.2.0", + "author": "The rust-url developers", + "name": "idna_adapter", + "version": "1.2.0", + "description": "Back end adapter for idna", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/idna_adapter@1.2.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/idna_adapter/latest/idna_adapter/" + }, + { + "type": "website", + "url": "https://docs.rs/crate/idna_adapter/latest" + }, + { + "type": "vcs", + "url": "https://github.com/hsivonen/idna_adapter" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.11", + "author": "David Tolnay ", + "name": "itoa", + "version": "1.0.11", + "description": "Fast integer primitive to string conversion", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/itoa@1.0.11", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/itoa" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/itoa" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#jiter@0.14.0", + "author": "Samuel Colvin ", + "name": "jiter", + "version": "0.14.0", + "description": "Fast Iterable JSON parser", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "b6f3b5d3f84b36f4ad09fd1da896d23d9852a1aa86556578dd0289f43dce311d" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/jiter@0.14.0", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/pydantic/jiter/" + }, + { + "type": "vcs", + "url": "https://github.com/pydantic/jiter/" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#js-sys@0.3.77", + "author": "The wasm-bindgen Developers", + "name": "js-sys", + "version": "0.3.77", + "description": "Bindings for all JS global objects and functions in all JS environments like Node.js and browsers, built on `#[wasm_bindgen]` using the `wasm-bindgen` crate. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/js-sys@0.3.77", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/js-sys" + }, + { + "type": "website", + "url": "https://rustwasm.github.io/wasm-bindgen/" + }, + { + "type": "vcs", + "url": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/js-sys" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#lexical-parse-float@1.0.5", + "author": "Alex Huszagh ", + "name": "lexical-parse-float", + "version": "1.0.5", + "description": "Efficient parsing of floats from strings.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "de6f9cb01fb0b08060209a057c048fcbab8717b4c1ecd2eac66ebfe39a65b0f2" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/lexical-parse-float@1.0.5", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/Alexhuszagh/rust-lexical" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#lexical-parse-integer@1.0.5", + "author": "Alex Huszagh ", + "name": "lexical-parse-integer", + "version": "1.0.5", + "description": "Efficient parsing of integers from strings.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "72207aae22fc0a121ba7b6d479e42cbfea549af1479c3f3a4f12c70dd66df12e" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/lexical-parse-integer@1.0.5", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/Alexhuszagh/rust-lexical" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#lexical-util@1.0.6", + "author": "Alex Huszagh ", + "name": "lexical-util", + "version": "1.0.6", + "description": "Shared utilities for lexical creates.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "5a82e24bf537fd24c177ffbbdc6ebcc8d54732c35b50a3f28cc3f4e4c949a0b3" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/lexical-util@1.0.6", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/Alexhuszagh/rust-lexical" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.185", + "author": "The Rust Project Developers", + "name": "libc", + "version": "0.2.185", + "description": "Raw FFI bindings to platform libraries like libc.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/libc@0.2.185", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-lang/libc" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#litemap@0.7.3", + "author": "The ICU4X Project Developers", + "name": "litemap", + "version": "0.7.3", + "description": "A key-value Map implementation based on a flat, sorted Vec.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/litemap@0.7.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/litemap" + }, + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#log@0.4.27", + "author": "The Rust Project Developers", + "name": "log", + "version": "0.4.27", + "description": "A lightweight logging facade for Rust ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/log@0.4.27", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/log" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/log" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#lru@0.16.3", + "author": "Jerome Froelich ", + "name": "lru", + "version": "0.16.3", + "description": "A LRU cache implementation", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/lru@0.16.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/lru/" + }, + { + "type": "website", + "url": "https://github.com/jeromefroe/lru-rs" + }, + { + "type": "vcs", + "url": "https://github.com/jeromefroe/lru-rs.git" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#memchr@2.7.4", + "author": "Andrew Gallant , bluss", + "name": "memchr", + "version": "2.7.4", + "description": "Provides extremely fast (uses SIMD on x86_64, aarch64 and wasm32) routines for 1, 2 or 3 byte search and single substring search. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + } + ], + "licenses": [ + { + "expression": "Unlicense OR MIT" + } + ], + "purl": "pkg:cargo/memchr@2.7.4", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/memchr/" + }, + { + "type": "website", + "url": "https://github.com/BurntSushi/memchr" + }, + { + "type": "vcs", + "url": "https://github.com/BurntSushi/memchr" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#num-bigint@0.4.6", + "author": "The Rust Project Developers", + "name": "num-bigint", + "version": "0.4.6", + "description": "Big integer implementation for Rust", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/num-bigint@0.4.6", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/num-bigint" + }, + { + "type": "website", + "url": "https://github.com/rust-num/num-bigint" + }, + { + "type": "vcs", + "url": "https://github.com/rust-num/num-bigint" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#num-integer@0.1.46", + "author": "The Rust Project Developers", + "name": "num-integer", + "version": "0.1.46", + "description": "Integer traits and functions", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/num-integer@0.1.46", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/num-integer" + }, + { + "type": "website", + "url": "https://github.com/rust-num/num-integer" + }, + { + "type": "vcs", + "url": "https://github.com/rust-num/num-integer" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19", + "author": "The Rust Project Developers", + "name": "num-traits", + "version": "0.2.19", + "description": "Numeric traits for generic mathematics", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/num-traits@0.2.19", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/num-traits" + }, + { + "type": "website", + "url": "https://github.com/rust-num/num-traits" + }, + { + "type": "vcs", + "url": "https://github.com/rust-num/num-traits" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "author": "Aleksey Kladov ", + "name": "once_cell", + "version": "1.21.3", + "description": "Single assignment cells and lazy values.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/once_cell@1.21.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/once_cell" + }, + { + "type": "vcs", + "url": "https://github.com/matklad/once_cell" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2", + "author": "The rust-url developers", + "name": "percent-encoding", + "version": "2.3.2", + "description": "Percent encoding and decoding", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/percent-encoding@2.3.2", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/servo/rust-url/" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#portable-atomic@1.6.0", + "name": "portable-atomic", + "version": "1.6.0", + "description": "Portable atomic types including support for 128-bit atomics, atomic float, etc. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/portable-atomic@1.6.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/taiki-e/portable-atomic" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.86", + "author": "David Tolnay , Alex Crichton ", + "name": "proc-macro2", + "version": "1.0.86", + "description": "A substitute implementation of the compiler's `proc_macro` API to decouple token-based libraries from the procedural macro use case.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/proc-macro2@1.0.86", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/proc-macro2" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/proc-macro2" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.28.3", + "author": "PyO3 Project and Contributors ", + "name": "pyo3-build-config", + "version": "0.28.3", + "description": "Build configuration for the PyO3 ecosystem", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pyo3-build-config@0.28.3", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/pyo3/pyo3" + }, + { + "type": "vcs", + "url": "https://github.com/pyo3/pyo3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-ffi@0.28.3", + "author": "PyO3 Project and Contributors ", + "name": "pyo3-ffi", + "version": "0.28.3", + "description": "Python-API bindings for the PyO3 ecosystem", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pyo3-ffi@0.28.3", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/pyo3/pyo3" + }, + { + "type": "other", + "url": "python" + }, + { + "type": "vcs", + "url": "https://github.com/pyo3/pyo3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros-backend@0.28.3", + "author": "PyO3 Project and Contributors ", + "name": "pyo3-macros-backend", + "version": "0.28.3", + "description": "Code generation for PyO3 package", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pyo3-macros-backend@0.28.3", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/pyo3/pyo3" + }, + { + "type": "vcs", + "url": "https://github.com/pyo3/pyo3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros@0.28.3", + "author": "PyO3 Project and Contributors ", + "name": "pyo3-macros", + "version": "0.28.3", + "description": "Proc macros for PyO3 package", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pyo3-macros@0.28.3", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/pyo3/pyo3" + }, + { + "type": "vcs", + "url": "https://github.com/pyo3/pyo3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3@0.28.3", + "author": "PyO3 Project and Contributors ", + "name": "pyo3", + "version": "0.28.3", + "description": "Bindings to Python interpreter", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pyo3@0.28.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/crate/pyo3/" + }, + { + "type": "website", + "url": "https://github.com/pyo3/pyo3" + }, + { + "type": "vcs", + "url": "https://github.com/pyo3/pyo3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#python3-dll-a@0.2.14", + "author": "Sergey Kvachonok , messense , Adam Reichold ", + "name": "python3-dll-a", + "version": "0.2.14", + "description": "Standalone python3(y)(t).dll import library generator", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "d381ef313ae70b4da5f95f8a4de773c6aa5cd28f73adec4b4a31df70b66780d8" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/python3-dll-a@0.2.14", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/PyO3/python3-dll-a" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "author": "David Tolnay ", + "name": "quote", + "version": "1.0.44", + "description": "Quasi-quoting macro quote!(...)", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/quote@1.0.44", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/quote/" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/quote" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#r-efi@5.2.0", + "name": "r-efi", + "version": "5.2.0", + "description": "UEFI Reference Specification Protocol Constants and Definitions", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0 OR LGPL-2.1-or-later" + } + ], + "purl": "pkg:cargo/r-efi@5.2.0", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/r-efi/r-efi/wiki" + }, + { + "type": "vcs", + "url": "https://github.com/r-efi/r-efi" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#radium@0.7.0", + "author": "Nika Layzell , myrrlyn ", + "name": "radium", + "version": "0.7.0", + "description": "Portable interfaces for maybe-atomic types", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/radium@0.7.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/radium" + }, + { + "type": "website", + "url": "https://github.com/bitvecto-rs/radium" + }, + { + "type": "vcs", + "url": "https://github.com/bitvecto-rs/radium" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.13", + "author": "The Rust Project Developers, Andrew Gallant ", + "name": "regex-automata", + "version": "0.4.13", + "description": "Automata construction and matching using regular expressions.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/regex-automata@0.4.13", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/regex-automata" + }, + { + "type": "website", + "url": "https://github.com/rust-lang/regex/tree/master/regex-automata" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/regex" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.5", + "author": "The Rust Project Developers, Andrew Gallant ", + "name": "regex-syntax", + "version": "0.8.5", + "description": "A regular expression parser.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/regex-syntax@0.8.5", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/regex-syntax" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/regex/tree/master/regex-syntax" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#regex@1.12.3", + "author": "The Rust Project Developers, Andrew Gallant ", + "name": "regex", + "version": "1.12.3", + "description": "An implementation of regular expressions for Rust. This implementation uses finite automata and guarantees linear time matching on all inputs. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/regex@1.12.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/regex" + }, + { + "type": "website", + "url": "https://github.com/rust-lang/regex" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/regex" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rustversion@1.0.17", + "author": "David Tolnay ", + "name": "rustversion", + "version": "1.0.17", + "description": "Conditional compilation according to rustc compiler version", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/rustversion@1.0.17", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/rustversion" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/rustversion" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "author": "Erick Tryzelaar , David Tolnay ", + "name": "serde", + "version": "1.0.228", + "description": "A generic serialization/deserialization framework", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/serde@1.0.228", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/serde" + }, + { + "type": "website", + "url": "https://serde.rs" + }, + { + "type": "vcs", + "url": "https://github.com/serde-rs/serde" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228", + "author": "Erick Tryzelaar , David Tolnay ", + "name": "serde_core", + "version": "1.0.228", + "description": "Serde traits only, with no support for derive -- use the `serde` crate instead", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/serde_core@1.0.228", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/serde_core" + }, + { + "type": "website", + "url": "https://serde.rs" + }, + { + "type": "vcs", + "url": "https://github.com/serde-rs/serde" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.228", + "author": "Erick Tryzelaar , David Tolnay ", + "name": "serde_derive", + "version": "1.0.228", + "description": "Macros 1.1 implementation of #[derive(Serialize, Deserialize)]", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/serde_derive@1.0.228", + "externalReferences": [ + { + "type": "documentation", + "url": "https://serde.rs/derive.html" + }, + { + "type": "website", + "url": "https://serde.rs" + }, + { + "type": "vcs", + "url": "https://github.com/serde-rs/serde" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149", + "author": "Erick Tryzelaar , David Tolnay ", + "name": "serde_json", + "version": "1.0.149", + "description": "A JSON serialization file format", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/serde_json@1.0.149", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/serde_json" + }, + { + "type": "vcs", + "url": "https://github.com/serde-rs/json" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1", + "author": "The Servo Project Developers", + "name": "smallvec", + "version": "1.15.1", + "description": "'Small vector' optimization: store up to a small number of items on the stack", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/smallvec@1.15.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/smallvec/" + }, + { + "type": "vcs", + "url": "https://github.com/servo/rust-smallvec" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#speedate@0.17.0", + "author": "Samuel Colvin ", + "name": "speedate", + "version": "0.17.0", + "description": "Fast and simple datetime, date, time and duration parsing", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "aba069c070b5e213f2a094deb7e5ed50ecb092be36102a4f4042e8d2056d060e" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/speedate@0.17.0", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/pydantic/speedate/" + }, + { + "type": "vcs", + "url": "https://github.com/pydantic/speedate/" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#stable_deref_trait@1.2.0", + "author": "Robert Grosse ", + "name": "stable_deref_trait", + "version": "1.2.0", + "description": "An unsafe marker trait for types like Box and Rc that dereference to a stable address even when moved, and hence can be used with libraries such as owning_ref and rental. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/stable_deref_trait@1.2.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/stable_deref_trait/1.2.0/stable_deref_trait" + }, + { + "type": "vcs", + "url": "https://github.com/storyyeller/stable_deref_trait" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#static_assertions@1.1.0", + "author": "Nikolai Vazquez", + "name": "static_assertions", + "version": "1.1.0", + "description": "Compile-time assertions to ensure that invariants are met.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/static_assertions@1.1.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/static_assertions/" + }, + { + "type": "website", + "url": "https://github.com/nvzqz/static-assertions-rs" + }, + { + "type": "vcs", + "url": "https://github.com/nvzqz/static-assertions-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#strum@0.27.2", + "author": "Peter Glotfelty ", + "name": "strum", + "version": "0.27.2", + "description": "Helpful macros for working with enums and strings", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/strum@0.27.2", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/strum" + }, + { + "type": "website", + "url": "https://github.com/Peternator7/strum" + }, + { + "type": "vcs", + "url": "https://github.com/Peternator7/strum" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#strum_macros@0.27.2", + "author": "Peter Glotfelty ", + "name": "strum_macros", + "version": "0.27.2", + "description": "Helpful macros for working with enums and strings", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/strum_macros@0.27.2", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/strum" + }, + { + "type": "website", + "url": "https://github.com/Peternator7/strum" + }, + { + "type": "vcs", + "url": "https://github.com/Peternator7/strum" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.82", + "author": "David Tolnay ", + "name": "syn", + "version": "2.0.82", + "description": "Parser for Rust source code", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/syn@2.0.82", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/syn" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/syn" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#synstructure@0.13.1", + "author": "Nika Layzell ", + "name": "synstructure", + "version": "0.13.1", + "description": "Helper methods and macros for custom derives", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/synstructure@0.13.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/synstructure" + }, + { + "type": "vcs", + "url": "https://github.com/mystor/synstructure" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tap@1.0.1", + "author": "Elliott Linder , myrrlyn ", + "name": "tap", + "version": "1.0.1", + "description": "Generic extensions for tapping values in Rust", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tap@1.0.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/tap" + }, + { + "type": "website", + "url": "https://github.com/myrrlyn/tap" + }, + { + "type": "vcs", + "url": "https://github.com/myrrlyn/tap" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#target-lexicon@0.13.4", + "author": "Dan Gohman ", + "name": "target-lexicon", + "version": "0.13.4", + "description": "LLVM target triple types", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "b1dd07eb858a2067e2f3c7155d54e929265c264e6f37efe3ee7a8d1b5a1dd0ba" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 WITH LLVM-exception" + } + ], + "purl": "pkg:cargo/target-lexicon@0.13.4", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/target-lexicon/" + }, + { + "type": "vcs", + "url": "https://github.com/bytecodealliance/target-lexicon" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tinystr@0.7.6", + "author": "The ICU4X Project Developers", + "name": "tinystr", + "version": "0.7.6", + "description": "A small ASCII-only bounded length string representation.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/tinystr@0.7.6", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.12", + "author": "David Tolnay ", + "name": "unicode-ident", + "version": "1.0.12", + "description": "Determine whether characters have the XID_Start or XID_Continue properties according to Unicode Standard Annex #31", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + } + ], + "licenses": [ + { + "expression": "(MIT OR Apache-2.0) AND Unicode-DFS-2016" + } + ], + "purl": "pkg:cargo/unicode-ident@1.0.12", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/unicode-ident" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/unicode-ident" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#url@2.5.8", + "author": "The rust-url developers", + "name": "url", + "version": "2.5.8", + "description": "URL library for Rust, based on the WHATWG URL Standard", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/url@2.5.8", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/url" + }, + { + "type": "vcs", + "url": "https://github.com/servo/rust-url" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#utf16_iter@1.0.5", + "author": "Henri Sivonen ", + "name": "utf16_iter", + "version": "1.0.5", + "description": "Iterator by char over potentially-invalid UTF-16 in &[u16]", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/utf16_iter@1.0.5", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/utf16_iter/" + }, + { + "type": "website", + "url": "https://docs.rs/utf16_iter/" + }, + { + "type": "vcs", + "url": "https://github.com/hsivonen/utf16_iter" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#utf8_iter@1.0.4", + "author": "Henri Sivonen ", + "name": "utf8_iter", + "version": "1.0.4", + "description": "Iterator by char over potentially-invalid UTF-8 in &[u8]", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/utf8_iter@1.0.4", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/utf8_iter/" + }, + { + "type": "website", + "url": "https://docs.rs/utf8_iter/" + }, + { + "type": "vcs", + "url": "https://github.com/hsivonen/utf8_iter" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#uuid@1.23.0", + "author": "Ashley Mannix, Dylan DPC, Hunar Roop Kahlon", + "name": "uuid", + "version": "1.23.0", + "description": "A library to generate and parse UUIDs.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/uuid@1.23.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/uuid" + }, + { + "type": "website", + "url": "https://github.com/uuid-rs/uuid" + }, + { + "type": "vcs", + "url": "https://github.com/uuid-rs/uuid" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#version_check@0.9.5", + "author": "Sergio Benitez ", + "name": "version_check", + "version": "0.9.5", + "description": "Tiny crate to check the version of the installed/running rustc.", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/version_check@0.9.5", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/version_check/" + }, + { + "type": "vcs", + "url": "https://github.com/SergioBenitez/version_check" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#wasi@0.14.2+wasi-0.2.4", + "author": "The Cranelift Project Developers", + "name": "wasi", + "version": "0.14.2+wasi-0.2.4", + "description": "WASI API bindings for Rust", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/wasi@0.14.2+wasi-0.2.4", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/wasi" + }, + { + "type": "vcs", + "url": "https://github.com/bytecodealliance/wasi-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-backend@0.2.100", + "author": "The wasm-bindgen Developers", + "name": "wasm-bindgen-backend", + "version": "0.2.100", + "description": "Backend code generation of the wasm-bindgen tool ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/wasm-bindgen-backend@0.2.100", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/wasm-bindgen-backend" + }, + { + "type": "website", + "url": "https://rustwasm.github.io/wasm-bindgen/" + }, + { + "type": "vcs", + "url": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/backend" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-macro-support@0.2.100", + "author": "The wasm-bindgen Developers", + "name": "wasm-bindgen-macro-support", + "version": "0.2.100", + "description": "The part of the implementation of the `#[wasm_bindgen]` attribute that is not in the shared backend crate ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/wasm-bindgen-macro-support@0.2.100", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/wasm-bindgen" + }, + { + "type": "website", + "url": "https://rustwasm.github.io/wasm-bindgen/" + }, + { + "type": "vcs", + "url": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/macro-support" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-macro@0.2.100", + "author": "The wasm-bindgen Developers", + "name": "wasm-bindgen-macro", + "version": "0.2.100", + "description": "Definition of the `#[wasm_bindgen]` attribute, an internal dependency ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/wasm-bindgen-macro@0.2.100", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/wasm-bindgen" + }, + { + "type": "website", + "url": "https://rustwasm.github.io/wasm-bindgen/" + }, + { + "type": "vcs", + "url": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/macro" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-shared@0.2.100", + "author": "The wasm-bindgen Developers", + "name": "wasm-bindgen-shared", + "version": "0.2.100", + "description": "Shared support between wasm-bindgen and wasm-bindgen cli, an internal dependency. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/wasm-bindgen-shared@0.2.100", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/wasm-bindgen-shared" + }, + { + "type": "website", + "url": "https://rustwasm.github.io/wasm-bindgen/" + }, + { + "type": "other", + "url": "wasm_bindgen" + }, + { + "type": "vcs", + "url": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/shared" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen@0.2.100", + "author": "The wasm-bindgen Developers", + "name": "wasm-bindgen", + "version": "0.2.100", + "description": "Easy support for interacting between JS and Rust. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/wasm-bindgen@0.2.100", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/wasm-bindgen" + }, + { + "type": "website", + "url": "https://rustwasm.github.io/" + }, + { + "type": "vcs", + "url": "https://github.com/rustwasm/wasm-bindgen" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#wit-bindgen-rt@0.39.0", + "name": "wit-bindgen-rt", + "version": "0.39.0", + "description": "Runtime support for the `wit-bindgen` crate ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/wit-bindgen-rt@0.39.0", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/bytecodealliance/wit-bindgen" + }, + { + "type": "vcs", + "url": "https://github.com/bytecodealliance/wit-bindgen" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#write16@1.0.0", + "name": "write16", + "version": "1.0.0", + "description": "A UTF-16 analog of the Write trait", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/write16@1.0.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/write16/" + }, + { + "type": "website", + "url": "https://docs.rs/write16/" + }, + { + "type": "vcs", + "url": "https://github.com/hsivonen/write16" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#writeable@0.5.5", + "author": "The ICU4X Project Developers", + "name": "writeable", + "version": "0.5.5", + "description": "A more efficient alternative to fmt::Display", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/writeable@0.5.5", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#wyz@0.5.1", + "author": "myrrlyn ", + "name": "wyz", + "version": "0.5.1", + "description": "myrrlyn’s utility collection", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/wyz@0.5.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/wyz" + }, + { + "type": "website", + "url": "https://myrrlyn.net/crates/wyz" + }, + { + "type": "vcs", + "url": "https://github.com/myrrlyn/wyz" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#yoke-derive@0.7.4", + "author": "Manish Goregaokar ", + "name": "yoke-derive", + "version": "0.7.4", + "description": "Custom derive for the yoke crate", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/yoke-derive@0.7.4", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#yoke@0.7.4", + "author": "Manish Goregaokar ", + "name": "yoke", + "version": "0.7.4", + "description": "Abstraction allowing borrowed data to be carried along with the backing data it borrows from", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/yoke@0.7.4", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#zerocopy-derive@0.8.25", + "author": "Joshua Liebow-Feeser , Jack Wrenn ", + "name": "zerocopy-derive", + "version": "0.8.25", + "description": "Custom derive for traits from the zerocopy crate", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" + } + ], + "licenses": [ + { + "expression": "BSD-2-Clause OR Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/zerocopy-derive@0.8.25", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/google/zerocopy" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.8.25", + "author": "Joshua Liebow-Feeser , Jack Wrenn ", + "name": "zerocopy", + "version": "0.8.25", + "description": "Zerocopy makes zero-cost memory manipulation effortless. We write \"unsafe\" so you don't have to.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" + } + ], + "licenses": [ + { + "expression": "BSD-2-Clause OR Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/zerocopy@0.8.25", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/google/zerocopy" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#zerofrom-derive@0.1.4", + "author": "Manish Goregaokar ", + "name": "zerofrom-derive", + "version": "0.1.4", + "description": "Custom derive for the zerofrom crate", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/zerofrom-derive@0.1.4", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#zerofrom@0.1.4", + "author": "Manish Goregaokar ", + "name": "zerofrom", + "version": "0.1.4", + "description": "ZeroFrom trait for constructing", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/zerofrom@0.1.4", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#zerovec-derive@0.10.3", + "author": "Manish Goregaokar ", + "name": "zerovec-derive", + "version": "0.10.3", + "description": "Custom derive for the zerovec crate", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/zerovec-derive@0.10.3", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#zerovec@0.10.4", + "author": "The ICU4X Project Developers", + "name": "zerovec", + "version": "0.10.4", + "description": "Zero-copy vector backed by a byte array", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/zerovec@0.10.4", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#zmij@1.0.6", + "author": "David Tolnay ", + "name": "zmij", + "version": "1.0.6", + "description": "A double-to-string conversion algorithm based on Schubfach and yy", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "aac060176f7020d62c3bcc1cdbcec619d54f48b07ad1963a3f80ce7a0c17755f" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/zmij@1.0.6", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/zmij" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/zmij" + } + ] + } + ], + "dependencies": [ + { + "ref": "path+file:///home/runner/work/pydantic/pydantic/pydantic-core#2.46.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#ahash@0.8.12", + "registry+https://github.com/rust-lang/crates.io-index#base64@0.22.1", + "registry+https://github.com/rust-lang/crates.io-index#enum_dispatch@0.3.13", + "registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.16.1", + "registry+https://github.com/rust-lang/crates.io-index#hex@0.4.3", + "registry+https://github.com/rust-lang/crates.io-index#idna@1.1.0", + "registry+https://github.com/rust-lang/crates.io-index#jiter@0.14.0", + "registry+https://github.com/rust-lang/crates.io-index#lru@0.16.3", + "registry+https://github.com/rust-lang/crates.io-index#num-bigint@0.4.6", + "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19", + "registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2", + "registry+https://github.com/rust-lang/crates.io-index#pyo3@0.28.3", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.28.3", + "registry+https://github.com/rust-lang/crates.io-index#regex@1.12.3", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149", + "registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1", + "registry+https://github.com/rust-lang/crates.io-index#speedate@0.17.0", + "registry+https://github.com/rust-lang/crates.io-index#strum@0.27.2", + "registry+https://github.com/rust-lang/crates.io-index#strum_macros@0.27.2", + "registry+https://github.com/rust-lang/crates.io-index#url@2.5.8", + "registry+https://github.com/rust-lang/crates.io-index#uuid@1.23.0", + "registry+https://github.com/rust-lang/crates.io-index#version_check@0.9.5" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#ahash@0.8.12", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.0", + "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.3.3", + "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "registry+https://github.com/rust-lang/crates.io-index#version_check@0.9.5", + "registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.8.25" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.7.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#allocator-api2@0.2.21" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#autocfg@1.3.0" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#base64@0.22.1" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#bitflags@2.9.1" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#bitvec@1.0.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#funty@2.0.0", + "registry+https://github.com/rust-lang/crates.io-index#radium@0.7.0", + "registry+https://github.com/rust-lang/crates.io-index#tap@1.0.1", + "registry+https://github.com/rust-lang/crates.io-index#wyz@0.5.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#bumpalo@3.19.0" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#cc@1.0.101" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.0" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#displaydoc@0.2.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.86", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.82" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#enum_dispatch@0.3.13", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.86", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.82" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#equivalent@1.0.2" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#foldhash@0.2.0" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#form_urlencoded@1.2.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#funty@2.0.0" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.3.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.0", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.185", + "registry+https://github.com/rust-lang/crates.io-index#r-efi@5.2.0", + "registry+https://github.com/rust-lang/crates.io-index#wasi@0.14.2+wasi-0.2.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.16.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#allocator-api2@0.2.21", + "registry+https://github.com/rust-lang/crates.io-index#equivalent@1.0.2", + "registry+https://github.com/rust-lang/crates.io-index#foldhash@0.2.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#hex@0.4.3" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#icu_collections@1.5.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#displaydoc@0.2.5", + "registry+https://github.com/rust-lang/crates.io-index#yoke@0.7.4", + "registry+https://github.com/rust-lang/crates.io-index#zerofrom@0.1.4", + "registry+https://github.com/rust-lang/crates.io-index#zerovec@0.10.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#icu_locid@1.5.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#displaydoc@0.2.5", + "registry+https://github.com/rust-lang/crates.io-index#litemap@0.7.3", + "registry+https://github.com/rust-lang/crates.io-index#tinystr@0.7.6", + "registry+https://github.com/rust-lang/crates.io-index#writeable@0.5.5", + "registry+https://github.com/rust-lang/crates.io-index#zerovec@0.10.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#icu_locid_transform@1.5.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#displaydoc@0.2.5", + "registry+https://github.com/rust-lang/crates.io-index#icu_locid@1.5.0", + "registry+https://github.com/rust-lang/crates.io-index#icu_locid_transform_data@1.5.0", + "registry+https://github.com/rust-lang/crates.io-index#icu_provider@1.5.0", + "registry+https://github.com/rust-lang/crates.io-index#tinystr@0.7.6", + "registry+https://github.com/rust-lang/crates.io-index#zerovec@0.10.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#icu_locid_transform_data@1.5.0" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#icu_normalizer@1.5.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#displaydoc@0.2.5", + "registry+https://github.com/rust-lang/crates.io-index#icu_collections@1.5.0", + "registry+https://github.com/rust-lang/crates.io-index#icu_normalizer_data@1.5.0", + "registry+https://github.com/rust-lang/crates.io-index#icu_properties@1.5.1", + "registry+https://github.com/rust-lang/crates.io-index#icu_provider@1.5.0", + "registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1", + "registry+https://github.com/rust-lang/crates.io-index#utf16_iter@1.0.5", + "registry+https://github.com/rust-lang/crates.io-index#utf8_iter@1.0.4", + "registry+https://github.com/rust-lang/crates.io-index#write16@1.0.0", + "registry+https://github.com/rust-lang/crates.io-index#zerovec@0.10.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#icu_normalizer_data@1.5.0" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#icu_properties@1.5.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#displaydoc@0.2.5", + "registry+https://github.com/rust-lang/crates.io-index#icu_collections@1.5.0", + "registry+https://github.com/rust-lang/crates.io-index#icu_locid_transform@1.5.0", + "registry+https://github.com/rust-lang/crates.io-index#icu_properties_data@1.5.0", + "registry+https://github.com/rust-lang/crates.io-index#icu_provider@1.5.0", + "registry+https://github.com/rust-lang/crates.io-index#tinystr@0.7.6", + "registry+https://github.com/rust-lang/crates.io-index#zerovec@0.10.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#icu_properties_data@1.5.0" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#icu_provider@1.5.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#displaydoc@0.2.5", + "registry+https://github.com/rust-lang/crates.io-index#icu_locid@1.5.0", + "registry+https://github.com/rust-lang/crates.io-index#icu_provider_macros@1.5.0", + "registry+https://github.com/rust-lang/crates.io-index#stable_deref_trait@1.2.0", + "registry+https://github.com/rust-lang/crates.io-index#tinystr@0.7.6", + "registry+https://github.com/rust-lang/crates.io-index#writeable@0.5.5", + "registry+https://github.com/rust-lang/crates.io-index#yoke@0.7.4", + "registry+https://github.com/rust-lang/crates.io-index#zerofrom@0.1.4", + "registry+https://github.com/rust-lang/crates.io-index#zerovec@0.10.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#icu_provider_macros@1.5.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.86", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.82" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#idna@1.1.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#idna_adapter@1.2.0", + "registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1", + "registry+https://github.com/rust-lang/crates.io-index#utf8_iter@1.0.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#idna_adapter@1.2.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#icu_normalizer@1.5.0", + "registry+https://github.com/rust-lang/crates.io-index#icu_properties@1.5.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.11" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#jiter@0.14.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#ahash@0.8.12", + "registry+https://github.com/rust-lang/crates.io-index#bitvec@1.0.1", + "registry+https://github.com/rust-lang/crates.io-index#lexical-parse-float@1.0.5", + "registry+https://github.com/rust-lang/crates.io-index#num-bigint@0.4.6", + "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19", + "registry+https://github.com/rust-lang/crates.io-index#pyo3@0.28.3", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.28.3", + "registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#js-sys@0.3.77", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen@0.2.100" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#lexical-parse-float@1.0.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#lexical-parse-integer@1.0.5", + "registry+https://github.com/rust-lang/crates.io-index#lexical-util@1.0.6", + "registry+https://github.com/rust-lang/crates.io-index#static_assertions@1.1.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#lexical-parse-integer@1.0.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#lexical-util@1.0.6", + "registry+https://github.com/rust-lang/crates.io-index#static_assertions@1.1.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#lexical-util@1.0.6", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#static_assertions@1.1.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.185" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#litemap@0.7.3" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#log@0.4.27" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#lru@0.16.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.16.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#memchr@2.7.4" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#num-bigint@0.4.6", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#num-integer@0.1.46", + "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#num-integer@0.1.46", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#autocfg@1.3.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#portable-atomic@1.6.0" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.86", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.12" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.28.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#python3-dll-a@0.2.14", + "registry+https://github.com/rust-lang/crates.io-index#target-lexicon@0.13.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-ffi@0.28.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.185", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.28.3" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros-backend@0.28.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0", + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.86", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.28.3", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.82" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros@0.28.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.86", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros-backend@0.28.3", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.82" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3@0.28.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.185", + "registry+https://github.com/rust-lang/crates.io-index#num-bigint@0.4.6", + "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19", + "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "registry+https://github.com/rust-lang/crates.io-index#portable-atomic@1.6.0", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.28.3", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-ffi@0.28.3", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros@0.28.3", + "registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#python3-dll-a@0.2.14", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cc@1.0.101" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.86" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#r-efi@5.2.0" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#radium@0.7.0" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.13", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.3", + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.7.4", + "registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.5" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.5" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#regex@1.12.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.3", + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.7.4", + "registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.13", + "registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.5" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rustversion@1.0.17" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.228" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.228" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.228", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.86", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.82" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.11", + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.7.4", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#zmij@1.0.6" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#speedate@0.17.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#lexical-parse-float@1.0.5", + "registry+https://github.com/rust-lang/crates.io-index#strum@0.27.2", + "registry+https://github.com/rust-lang/crates.io-index#strum_macros@0.27.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#stable_deref_trait@1.2.0" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#static_assertions@1.1.0" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#strum@0.27.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#strum_macros@0.27.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#strum_macros@0.27.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0", + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.86", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.82" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.82", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.86", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.12" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#synstructure@0.13.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.86", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.82" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tap@1.0.1" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#target-lexicon@0.13.4" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tinystr@0.7.6", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#displaydoc@0.2.5", + "registry+https://github.com/rust-lang/crates.io-index#zerovec@0.10.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.12" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#url@2.5.8", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#form_urlencoded@1.2.2", + "registry+https://github.com/rust-lang/crates.io-index#idna@1.1.0", + "registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#utf16_iter@1.0.5" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#utf8_iter@1.0.4" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#uuid@1.23.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#js-sys@0.3.77", + "registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen@0.2.100" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#version_check@0.9.5" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#wasi@0.14.2+wasi-0.2.4", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#wit-bindgen-rt@0.39.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-backend@0.2.100", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#bumpalo@3.19.0", + "registry+https://github.com/rust-lang/crates.io-index#log@0.4.27", + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.86", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.82", + "registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-shared@0.2.100" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-macro-support@0.2.100", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.86", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.82", + "registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-backend@0.2.100", + "registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-shared@0.2.100" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-macro@0.2.100", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-macro-support@0.2.100" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-shared@0.2.100", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.12" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen@0.2.100", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.0", + "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "registry+https://github.com/rust-lang/crates.io-index#rustversion@1.0.17", + "registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-macro@0.2.100" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#wit-bindgen-rt@0.39.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#bitflags@2.9.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#write16@1.0.0" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#writeable@0.5.5" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#wyz@0.5.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#tap@1.0.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#yoke-derive@0.7.4", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.86", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.82", + "registry+https://github.com/rust-lang/crates.io-index#synstructure@0.13.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#yoke@0.7.4", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#stable_deref_trait@1.2.0", + "registry+https://github.com/rust-lang/crates.io-index#yoke-derive@0.7.4", + "registry+https://github.com/rust-lang/crates.io-index#zerofrom@0.1.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#zerocopy-derive@0.8.25", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.86", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.82" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.8.25", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#zerocopy-derive@0.8.25" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#zerofrom-derive@0.1.4", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.86", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.82", + "registry+https://github.com/rust-lang/crates.io-index#synstructure@0.13.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#zerofrom@0.1.4", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#zerofrom-derive@0.1.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#zerovec-derive@0.10.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.86", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.82" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#zerovec@0.10.4", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#yoke@0.7.4", + "registry+https://github.com/rust-lang/crates.io-index#zerofrom@0.1.4", + "registry+https://github.com/rust-lang/crates.io-index#zerovec-derive@0.10.3" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#zmij@1.0.6" + } + ] +} \ No newline at end of file diff --git a/source/target-vendors/cp312/pydantic_core/__init__.py b/source/target-vendors/cp312/pydantic_core/__init__.py new file mode 100644 index 0000000..d5facd1 --- /dev/null +++ b/source/target-vendors/cp312/pydantic_core/__init__.py @@ -0,0 +1,171 @@ +from __future__ import annotations + +import sys as _sys +from typing import Any as _Any + +from typing_extensions import Sentinel + +from ._pydantic_core import ( + ArgsKwargs, + MultiHostUrl, + PydanticCustomError, + PydanticKnownError, + PydanticOmit, + PydanticSerializationError, + PydanticSerializationUnexpectedValue, + PydanticUndefined, + PydanticUndefinedType, + PydanticUseDefault, + SchemaError, + SchemaSerializer, + SchemaValidator, + Some, + TzInfo, + Url, + ValidationError, + __version__, + from_json, + to_json, + to_jsonable_python, +) +from .core_schema import CoreConfig, CoreSchema, CoreSchemaType, ErrorType + +if _sys.version_info < (3, 11): + from typing_extensions import NotRequired as _NotRequired +else: + from typing import NotRequired as _NotRequired + +if _sys.version_info < (3, 12): + from typing_extensions import TypedDict as _TypedDict +else: + from typing import TypedDict as _TypedDict + +__all__ = [ + '__version__', + 'UNSET', + 'CoreConfig', + 'CoreSchema', + 'CoreSchemaType', + 'SchemaValidator', + 'SchemaSerializer', + 'Some', + 'Url', + 'MultiHostUrl', + 'ArgsKwargs', + 'PydanticUndefined', + 'PydanticUndefinedType', + 'SchemaError', + 'ErrorDetails', + 'InitErrorDetails', + 'ValidationError', + 'PydanticCustomError', + 'PydanticKnownError', + 'PydanticOmit', + 'PydanticUseDefault', + 'PydanticSerializationError', + 'PydanticSerializationUnexpectedValue', + 'TzInfo', + 'to_json', + 'from_json', + 'to_jsonable_python', +] + + +class ErrorDetails(_TypedDict): + type: str + """ + The type of error that occurred, this is an identifier designed for + programmatic use that will change rarely or never. + + `type` is unique for each error message, and can hence be used as an identifier to build custom error messages. + """ + loc: tuple[int | str, ...] + """Tuple of strings and ints identifying where in the schema the error occurred.""" + msg: str + """A human readable error message.""" + input: _Any + """The input data at this `loc` that caused the error.""" + ctx: _NotRequired[dict[str, _Any]] + """ + Values which are required to render the error message, and could hence be useful in rendering custom error messages. + Also useful for passing custom error data forward. + """ + url: _NotRequired[str] + """ + The documentation URL giving information about the error. No URL is available if + a [`PydanticCustomError`][pydantic_core.PydanticCustomError] is used. + """ + + +class InitErrorDetails(_TypedDict): + type: str | PydanticCustomError + """The type of error that occurred, this should be a "slug" identifier that changes rarely or never.""" + loc: _NotRequired[tuple[int | str, ...]] + """Tuple of strings and ints identifying where in the schema the error occurred.""" + input: _Any + """The input data at this `loc` that caused the error.""" + ctx: _NotRequired[dict[str, _Any]] + """ + Values which are required to render the error message, and could hence be useful in rendering custom error messages. + Also useful for passing custom error data forward. + """ + + +class ErrorTypeInfo(_TypedDict): + """ + Gives information about errors. + """ + + type: ErrorType + """The type of error that occurred, this should be a "slug" identifier that changes rarely or never.""" + message_template_python: str + """String template to render a human readable error message from using context, when the input is Python.""" + example_message_python: str + """Example of a human readable error message, when the input is Python.""" + message_template_json: _NotRequired[str] + """String template to render a human readable error message from using context, when the input is JSON data.""" + example_message_json: _NotRequired[str] + """Example of a human readable error message, when the input is JSON data.""" + example_context: dict[str, _Any] | None + """Example of context values.""" + + +class MultiHostHost(_TypedDict): + """ + A host part of a multi-host URL. + """ + + username: str | None + """The username part of this host, or `None`.""" + password: str | None + """The password part of this host, or `None`.""" + host: str | None + """The host part of this host, or `None`.""" + port: int | None + """The port part of this host, or `None`.""" + + +MISSING = Sentinel('MISSING') +"""A singleton indicating a field value was not provided during validation. + +This singleton can be used a default value, as an alternative to `None` when it has +an explicit meaning. During serialization, any field with `MISSING` as a value is excluded +from the output. + +Example: + ```python + from pydantic import BaseModel + + from pydantic_core import MISSING + + + class Configuration(BaseModel): + timeout: int | None | MISSING = MISSING + + + # configuration defaults, stored somewhere else: + defaults = {'timeout': 200} + + conf = Configuration.model_validate({...}) + timeout = conf.timeout if timeout.timeout is not MISSING else defaults['timeout'] +""" diff --git a/source/target-vendors/cp312/pydantic_core/_pydantic_core.cpython-312-x86_64-linux-gnu.so b/source/target-vendors/cp312/pydantic_core/_pydantic_core.cpython-312-x86_64-linux-gnu.so new file mode 100644 index 0000000..a2dcaf3 Binary files /dev/null and b/source/target-vendors/cp312/pydantic_core/_pydantic_core.cpython-312-x86_64-linux-gnu.so differ diff --git a/source/target-vendors/cp312/pydantic_core/_pydantic_core.pyi b/source/target-vendors/cp312/pydantic_core/_pydantic_core.pyi new file mode 100644 index 0000000..122c86f --- /dev/null +++ b/source/target-vendors/cp312/pydantic_core/_pydantic_core.pyi @@ -0,0 +1,1056 @@ +import datetime +from collections.abc import Mapping +from typing import Any, Callable, Generic, Literal, TypeVar, final + +from _typeshed import SupportsAllComparisons +from typing_extensions import LiteralString, Self, TypeAlias + +from pydantic_core import ErrorDetails, ErrorTypeInfo, InitErrorDetails, MultiHostHost +from pydantic_core.core_schema import CoreConfig, CoreSchema, ErrorType, ExtraBehavior + +__all__ = [ + '__version__', + 'build_profile', + 'build_info', + '_recursion_limit', + 'ArgsKwargs', + 'SchemaValidator', + 'SchemaSerializer', + 'Url', + 'MultiHostUrl', + 'SchemaError', + 'ValidationError', + 'PydanticCustomError', + 'PydanticKnownError', + 'PydanticOmit', + 'PydanticUseDefault', + 'PydanticSerializationError', + 'PydanticSerializationUnexpectedValue', + 'PydanticUndefined', + 'PydanticUndefinedType', + 'Some', + 'to_json', + 'from_json', + 'to_jsonable_python', + 'list_all_errors', + 'TzInfo', +] +__version__: str +build_profile: str +build_info: str +_recursion_limit: int + +_T = TypeVar('_T', default=Any, covariant=True) + +_StringInput: TypeAlias = 'dict[str, _StringInput]' + +@final +class Some(Generic[_T]): + """ + Similar to Rust's [`Option::Some`](https://doc.rust-lang.org/std/option/enum.Option.html) type, this + identifies a value as being present, and provides a way to access it. + + Generally used in a union with `None` to different between "some value which could be None" and no value. + """ + + __match_args__ = ('value',) + + @property + def value(self) -> _T: + """ + Returns the value wrapped by `Some`. + """ + @classmethod + def __class_getitem__(cls, item: Any, /) -> type[Self]: ... + +@final +class SchemaValidator: + """ + `SchemaValidator` is the Python wrapper for `pydantic-core`'s Rust validation logic, internally it owns one + `CombinedValidator` which may in turn own more `CombinedValidator`s which make up the full schema validator. + """ + + # note: pyo3 currently supports __new__, but not __init__, though we include __init__ stubs + # and docstrings here (and in the following classes) for documentation purposes + + def __init__(self, schema: CoreSchema, config: CoreConfig | None = None, _use_prebuilt: bool = True) -> None: + """Initializes the `SchemaValidator`. + + Arguments: + schema: The `CoreSchema` to use for validation. + config: Optionally a [`CoreConfig`][pydantic_core.core_schema.CoreConfig] to configure validation. + _use_prebuilt: Whether to use pre-built validators (False during rebuilds to avoid stale references). + """ + + def __new__(cls, schema: CoreSchema, config: CoreConfig | None = None, _use_prebuilt: bool = True) -> Self: ... + @property + def title(self) -> str: + """ + The title of the schema, as used in the heading of [`ValidationError.__str__()`][pydantic_core.ValidationError]. + """ + def validate_python( + self, + input: Any, + *, + strict: bool | None = None, + extra: ExtraBehavior | None = None, + from_attributes: bool | None = None, + context: Any | None = None, + self_instance: Any | None = None, + allow_partial: bool | Literal['off', 'on', 'trailing-strings'] = False, + by_alias: bool | None = None, + by_name: bool | None = None, + ) -> Any: + """ + Validate a Python object against the schema and return the validated object. + + Arguments: + input: The Python object to validate. + strict: Whether to validate the object in strict mode. + If `None`, the value of [`CoreConfig.strict`][pydantic_core.core_schema.CoreConfig] is used. + extra: Whether to ignore, allow, or forbid extra data during model validation. + If `None`, the value of [`CoreConfig.extra_fields_behavior`][pydantic_core.core_schema.CoreConfig] is used. + from_attributes: Whether to validate objects as inputs to models by extracting attributes. + If `None`, the value of [`CoreConfig.from_attributes`][pydantic_core.core_schema.CoreConfig] is used. + context: The context to use for validation, this is passed to functional validators as + [`info.context`][pydantic_core.core_schema.ValidationInfo.context]. + self_instance: An instance of a model set attributes on from validation, this is used when running + validation from the `__init__` method of a model. + allow_partial: Whether to allow partial validation; if `True` errors in the last element of sequences + and mappings are ignored. + `'trailing-strings'` means any final unfinished JSON string is included in the result. + by_alias: Whether to use the field's alias when validating against the provided input data. + by_name: Whether to use the field's name when validating against the provided input data. + + Raises: + ValidationError: If validation fails. + Exception: Other error types maybe raised if internal errors occur. + + Returns: + The validated object. + """ + def isinstance_python( + self, + input: Any, + *, + strict: bool | None = None, + extra: ExtraBehavior | None = None, + from_attributes: bool | None = None, + context: Any | None = None, + self_instance: Any | None = None, + by_alias: bool | None = None, + by_name: bool | None = None, + ) -> bool: + """ + Similar to [`validate_python()`][pydantic_core.SchemaValidator.validate_python] but returns a boolean. + + Arguments match `validate_python()`. This method will not raise `ValidationError`s but will raise internal + errors. + + Returns: + `True` if validation succeeds, `False` if validation fails. + """ + def validate_json( + self, + input: str | bytes | bytearray, + *, + strict: bool | None = None, + extra: ExtraBehavior | None = None, + context: Any | None = None, + self_instance: Any | None = None, + allow_partial: bool | Literal['off', 'on', 'trailing-strings'] = False, + by_alias: bool | None = None, + by_name: bool | None = None, + ) -> Any: + """ + Validate JSON data directly against the schema and return the validated Python object. + + This method should be significantly faster than `validate_python(json.loads(json_data))` as it avoids the + need to create intermediate Python objects + + It also handles constructing the correct Python type even in strict mode, where + `validate_python(json.loads(json_data))` would fail validation. + + Arguments: + input: The JSON data to validate. + strict: Whether to validate the object in strict mode. + If `None`, the value of [`CoreConfig.strict`][pydantic_core.core_schema.CoreConfig] is used. + extra: Whether to ignore, allow, or forbid extra data during model validation. + If `None`, the value of [`CoreConfig.extra_fields_behavior`][pydantic_core.core_schema.CoreConfig] is used. + context: The context to use for validation, this is passed to functional validators as + [`info.context`][pydantic_core.core_schema.ValidationInfo.context]. + self_instance: An instance of a model set attributes on from validation. + allow_partial: Whether to allow partial validation; if `True` incomplete JSON will be parsed successfully + and errors in the last element of sequences and mappings are ignored. + `'trailing-strings'` means any final unfinished JSON string is included in the result. + by_alias: Whether to use the field's alias when validating against the provided input data. + by_name: Whether to use the field's name when validating against the provided input data. + + Raises: + ValidationError: If validation fails or if the JSON data is invalid. + Exception: Other error types maybe raised if internal errors occur. + + Returns: + The validated Python object. + """ + def validate_strings( + self, + input: _StringInput, + *, + strict: bool | None = None, + extra: ExtraBehavior | None = None, + context: Any | None = None, + allow_partial: bool | Literal['off', 'on', 'trailing-strings'] = False, + by_alias: bool | None = None, + by_name: bool | None = None, + ) -> Any: + """ + Validate a string against the schema and return the validated Python object. + + This is similar to `validate_json` but applies to scenarios where the input will be a string but not + JSON data, e.g. URL fragments, query parameters, etc. + + Arguments: + input: The input as a string, or bytes/bytearray if `strict=False`. + strict: Whether to validate the object in strict mode. + If `None`, the value of [`CoreConfig.strict`][pydantic_core.core_schema.CoreConfig] is used. + extra: Whether to ignore, allow, or forbid extra data during model validation. + If `None`, the value of [`CoreConfig.extra_fields_behavior`][pydantic_core.core_schema.CoreConfig] is used. + context: The context to use for validation, this is passed to functional validators as + [`info.context`][pydantic_core.core_schema.ValidationInfo.context]. + allow_partial: Whether to allow partial validation; if `True` errors in the last element of sequences + and mappings are ignored. + `'trailing-strings'` means any final unfinished JSON string is included in the result. + by_alias: Whether to use the field's alias when validating against the provided input data. + by_name: Whether to use the field's name when validating against the provided input data. + + Raises: + ValidationError: If validation fails or if the JSON data is invalid. + Exception: Other error types maybe raised if internal errors occur. + + Returns: + The validated Python object. + """ + def validate_assignment( + self, + obj: Any, + field_name: str, + field_value: Any, + *, + strict: bool | None = None, + extra: ExtraBehavior | None = None, + from_attributes: bool | None = None, + context: Any | None = None, + by_alias: bool | None = None, + by_name: bool | None = None, + ) -> dict[str, Any] | tuple[dict[str, Any], dict[str, Any] | None, set[str]]: + """ + Validate an assignment to a field on a model. + + Arguments: + obj: The model instance being assigned to. + field_name: The name of the field to validate assignment for. + field_value: The value to assign to the field. + strict: Whether to validate the object in strict mode. + If `None`, the value of [`CoreConfig.strict`][pydantic_core.core_schema.CoreConfig] is used. + extra: Whether to ignore, allow, or forbid extra data during model validation. + If `None`, the value of [`CoreConfig.extra_fields_behavior`][pydantic_core.core_schema.CoreConfig] is used. + from_attributes: Whether to validate objects as inputs to models by extracting attributes. + If `None`, the value of [`CoreConfig.from_attributes`][pydantic_core.core_schema.CoreConfig] is used. + context: The context to use for validation, this is passed to functional validators as + [`info.context`][pydantic_core.core_schema.ValidationInfo.context]. + by_alias: Whether to use the field's alias when validating against the provided input data. + by_name: Whether to use the field's name when validating against the provided input data. + + Raises: + ValidationError: If validation fails. + Exception: Other error types maybe raised if internal errors occur. + + Returns: + Either the model dict or a tuple of `(model_data, model_extra, fields_set)` + """ + def get_default_value(self, *, strict: bool | None = None, context: Any = None) -> Some | None: + """ + Get the default value for the schema, including running default value validation. + + Arguments: + strict: Whether to validate the default value in strict mode. + If `None`, the value of [`CoreConfig.strict`][pydantic_core.core_schema.CoreConfig] is used. + context: The context to use for validation, this is passed to functional validators as + [`info.context`][pydantic_core.core_schema.ValidationInfo.context]. + + Raises: + ValidationError: If validation fails. + Exception: Other error types maybe raised if internal errors occur. + + Returns: + `None` if the schema has no default value, otherwise a [`Some`][pydantic_core.Some] containing the default. + """ + +# In reality, `bool` should be replaced by `Literal[True]` but mypy fails to correctly apply bidirectional type inference +# (e.g. when using `{'a': {'b': True}}`). +_IncEx: TypeAlias = set[int] | set[str] | Mapping[int, _IncEx | bool] | Mapping[str, _IncEx | bool] + +@final +class SchemaSerializer: + """ + `SchemaSerializer` is the Python wrapper for `pydantic-core`'s Rust serialization logic, internally it owns one + `CombinedSerializer` which may in turn own more `CombinedSerializer`s which make up the full schema serializer. + """ + + def __init__(self, schema: CoreSchema, config: CoreConfig | None = None, _use_prebuilt: bool = True) -> None: + """Initializes the `SchemaSerializer`. + + Arguments: + schema: The `CoreSchema` to use for serialization. + config: Optionally a [`CoreConfig`][pydantic_core.core_schema.CoreConfig] to to configure serialization. + _use_prebuilt: Whether to use pre-built validators (False during rebuilds to avoid stale references). + """ + + def __new__(cls, schema: CoreSchema, config: CoreConfig | None = None, _use_prebuilt: bool = True) -> Self: ... + def to_python( + self, + value: Any, + *, + mode: str | None = None, + include: _IncEx | None = None, + exclude: _IncEx | None = None, + by_alias: bool | None = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + exclude_computed_fields: bool = False, + round_trip: bool = False, + warnings: bool | Literal['none', 'warn', 'error'] = True, + fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, + polymorphic_serialization: bool | None = None, + context: Any | None = None, + ) -> Any: + """ + Serialize/marshal a Python object to a Python object including transforming and filtering data. + + Arguments: + value: The Python object to serialize. + mode: The serialization mode to use, either `'python'` or `'json'`, defaults to `'python'`. In JSON mode, + all values are converted to JSON compatible types, e.g. `None`, `int`, `float`, `str`, `list`, `dict`. + include: A set of fields to include, if `None` all fields are included. + exclude: A set of fields to exclude, if `None` no fields are excluded. + by_alias: Whether to use the alias names of fields. + exclude_unset: Whether to exclude fields that are not set, + e.g. are not included in `__pydantic_fields_set__`. + exclude_defaults: Whether to exclude fields that are equal to their default value. + exclude_none: Whether to exclude fields that have a value of `None`. + exclude_computed_fields: Whether to exclude computed fields. + round_trip: Whether to enable serialization and validation round-trip support. + warnings: How to handle invalid fields. False/"none" ignores them, True/"warn" logs errors, + "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError]. + fallback: A function to call when an unknown value is encountered, + if `None` a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised. + serialize_as_any: Whether to serialize fields with duck-typing serialization behavior. + polymorphic_serialization: Whether to use model and dataclass polymorphic serialization for this call. + context: The context to use for serialization, this is passed to functional serializers as + [`info.context`][pydantic_core.core_schema.SerializationInfo.context]. + + Raises: + PydanticSerializationError: If serialization fails and no `fallback` function is provided. + + Returns: + The serialized Python object. + """ + def to_json( + self, + value: Any, + *, + indent: int | None = None, + ensure_ascii: bool = False, + include: _IncEx | None = None, + exclude: _IncEx | None = None, + by_alias: bool | None = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + exclude_computed_fields: bool = False, + round_trip: bool = False, + warnings: bool | Literal['none', 'warn', 'error'] = True, + fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, + polymorphic_serialization: bool | None = None, + context: Any | None = None, + ) -> bytes: + """ + Serialize a Python object to JSON including transforming and filtering data. + + Arguments: + value: The Python object to serialize. + indent: If `None`, the JSON will be compact, otherwise it will be pretty-printed with the indent provided. + ensure_ascii: If `True`, the output is guaranteed to have all incoming non-ASCII characters escaped. + If `False` (the default), these characters will be output as-is. + include: A set of fields to include, if `None` all fields are included. + exclude: A set of fields to exclude, if `None` no fields are excluded. + by_alias: Whether to use the alias names of fields. + exclude_unset: Whether to exclude fields that are not set, + e.g. are not included in `__pydantic_fields_set__`. + exclude_defaults: Whether to exclude fields that are equal to their default value. + exclude_none: Whether to exclude fields that have a value of `None`. + exclude_computed_fields: Whether to exclude computed fields. + round_trip: Whether to enable serialization and validation round-trip support. + warnings: How to handle invalid fields. False/"none" ignores them, True/"warn" logs errors, + "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError]. + fallback: A function to call when an unknown value is encountered, + if `None` a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised. + serialize_as_any: Whether to serialize fields with duck-typing serialization behavior. + polymorphic_serialization: Whether to use model and dataclass polymorphic serialization for this call. + context: The context to use for serialization, this is passed to functional serializers as + [`info.context`][pydantic_core.core_schema.SerializationInfo.context]. + + Raises: + PydanticSerializationError: If serialization fails and no `fallback` function is provided. + + Returns: + JSON bytes. + """ + +def to_json( + value: Any, + *, + indent: int | None = None, + ensure_ascii: bool = False, + include: _IncEx | None = None, + exclude: _IncEx | None = None, + # Note: In Pydantic 2.11, the default value of `by_alias` on `SchemaSerializer` was changed from `True` to `None`, + # to be consistent with the Pydantic "dump" methods. However, the default of `True` was kept here for + # backwards compatibility. In Pydantic V3, `by_alias` is expected to default to `True` everywhere: + by_alias: bool = True, + exclude_none: bool = False, + round_trip: bool = False, + timedelta_mode: Literal['iso8601', 'float'] = 'iso8601', + temporal_mode: Literal['iso8601', 'seconds', 'milliseconds'] = 'iso8601', + bytes_mode: Literal['utf8', 'base64', 'hex'] = 'utf8', + inf_nan_mode: Literal['null', 'constants', 'strings'] = 'constants', + serialize_unknown: bool = False, + fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, + polymorphic_serialization: bool | None = None, + context: Any | None = None, +) -> bytes: + """ + Serialize a Python object to JSON including transforming and filtering data. + + This is effectively a standalone version of [`SchemaSerializer.to_json`][pydantic_core.SchemaSerializer.to_json]. + + Arguments: + value: The Python object to serialize. + indent: If `None`, the JSON will be compact, otherwise it will be pretty-printed with the indent provided. + ensure_ascii: If `True`, the output is guaranteed to have all incoming non-ASCII characters escaped. + If `False` (the default), these characters will be output as-is. + include: A set of fields to include, if `None` all fields are included. + exclude: A set of fields to exclude, if `None` no fields are excluded. + by_alias: Whether to use the alias names of fields. + exclude_none: Whether to exclude fields that have a value of `None`. + round_trip: Whether to enable serialization and validation round-trip support. + timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'` or `'float'`. + temporal_mode: How to serialize datetime-like objects (`datetime`, `date`, `time`), either `'iso8601'`, `'seconds'`, or `'milliseconds'`. + `iso8601` returns an ISO 8601 string; `seconds` returns the Unix timestamp in seconds as a float; `milliseconds` returns the Unix timestamp in milliseconds as a float. + + bytes_mode: How to serialize `bytes` objects, either `'utf8'`, `'base64'`, or `'hex'`. + inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`. + serialize_unknown: Attempt to serialize unknown types, `str(value)` will be used, if that fails + `""` will be used. + fallback: A function to call when an unknown value is encountered, + if `None` a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised. + serialize_as_any: Whether to serialize fields with duck-typing serialization behavior. + polymorphic_serialization: Whether to use model and dataclass polymorphic serialization for this call. + context: The context to use for serialization, this is passed to functional serializers as + [`info.context`][pydantic_core.core_schema.SerializationInfo.context]. + + Raises: + PydanticSerializationError: If serialization fails and no `fallback` function is provided. + + Returns: + JSON bytes. + """ + +def from_json( + data: str | bytes | bytearray, + *, + allow_inf_nan: bool = True, + cache_strings: bool | Literal['all', 'keys', 'none'] = True, + allow_partial: bool | Literal['off', 'on', 'trailing-strings'] = False, +) -> Any: + """ + Deserialize JSON data to a Python object. + + This is effectively a faster version of `json.loads()`, with some extra functionality. + + Arguments: + data: The JSON data to deserialize. + allow_inf_nan: Whether to allow `Infinity`, `-Infinity` and `NaN` values as `json.loads()` does by default. + cache_strings: Whether to cache strings to avoid constructing new Python objects, + this should have a significant impact on performance while increasing memory usage slightly, + `all/True` means cache all strings, `keys` means cache only dict keys, `none/False` means no caching. + allow_partial: Whether to allow partial deserialization, if `True` JSON data is returned if the end of the + input is reached before the full object is deserialized, e.g. `["aa", "bb", "c` would return `['aa', 'bb']`. + `'trailing-strings'` means any final unfinished JSON string is included in the result. + + Raises: + ValueError: If deserialization fails. + + Returns: + The deserialized Python object. + """ + +def to_jsonable_python( + value: Any, + *, + include: _IncEx | None = None, + exclude: _IncEx | None = None, + # Note: In Pydantic 2.11, the default value of `by_alias` on `SchemaSerializer` was changed from `True` to `None`, + # to be consistent with the Pydantic "dump" methods. However, the default of `True` was kept here for + # backwards compatibility. In Pydantic V3, `by_alias` is expected to default to `True` everywhere: + by_alias: bool = True, + exclude_none: bool = False, + round_trip: bool = False, + timedelta_mode: Literal['iso8601', 'float'] = 'iso8601', + temporal_mode: Literal['iso8601', 'seconds', 'milliseconds'] = 'iso8601', + bytes_mode: Literal['utf8', 'base64', 'hex'] = 'utf8', + inf_nan_mode: Literal['null', 'constants', 'strings'] = 'constants', + serialize_unknown: bool = False, + fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, + polymorphic_serialization: bool | None = None, + context: Any | None = None, +) -> Any: + """ + Serialize/marshal a Python object to a JSON-serializable Python object including transforming and filtering data. + + This is effectively a standalone version of + [`SchemaSerializer.to_python(mode='json')`][pydantic_core.SchemaSerializer.to_python]. + + Args: + value: The Python object to serialize. + include: A set of fields to include, if `None` all fields are included. + exclude: A set of fields to exclude, if `None` no fields are excluded. + by_alias: Whether to use the alias names of fields. + exclude_none: Whether to exclude fields that have a value of `None`. + round_trip: Whether to enable serialization and validation round-trip support. + timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'` or `'float'`. + temporal_mode: How to serialize datetime-like objects (`datetime`, `date`, `time`), either `'iso8601'`, `'seconds'`, or `'milliseconds'`. + `iso8601` returns an ISO 8601 string; `seconds` returns the Unix timestamp in seconds as a float; `milliseconds` returns the Unix timestamp in milliseconds as a float. + + bytes_mode: How to serialize `bytes` objects, either `'utf8'`, `'base64'`, or `'hex'`. + inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`. + serialize_unknown: Attempt to serialize unknown types, `str(value)` will be used, if that fails + `""` will be used. + fallback: A function to call when an unknown value is encountered, + if `None` a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised. + serialize_as_any: Whether to serialize fields with duck-typing serialization behavior. + polymorphic_serialization: Whether to use model and dataclass polymorphic serialization for this call. + context: The context to use for serialization, this is passed to functional serializers as + [`info.context`][pydantic_core.core_schema.SerializationInfo.context]. + + Raises: + PydanticSerializationError: If serialization fails and no `fallback` function is provided. + + Returns: + The serialized Python object. + """ + +class Url(SupportsAllComparisons): + """ + A URL type, internal logic uses the [url rust crate](https://docs.rs/url/latest/url/) originally developed + by Mozilla. + """ + + def __init__(self, url: str) -> None: ... + def __new__(cls, url: str) -> Self: ... + @property + def scheme(self) -> str: ... + @property + def username(self) -> str | None: ... + @property + def password(self) -> str | None: ... + @property + def host(self) -> str | None: ... + def unicode_host(self) -> str | None: ... + @property + def port(self) -> int | None: ... + @property + def path(self) -> str | None: ... + @property + def query(self) -> str | None: ... + def query_params(self) -> list[tuple[str, str]]: ... + @property + def fragment(self) -> str | None: ... + def unicode_string(self) -> str: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def __deepcopy__(self, memo: dict) -> str: ... + @classmethod + def build( + cls, + *, + scheme: str, + username: str | None = None, + password: str | None = None, + host: str, + port: int | None = None, + path: str | None = None, + query: str | None = None, + fragment: str | None = None, + ) -> Self: ... + +class MultiHostUrl(SupportsAllComparisons): + """ + A URL type with support for multiple hosts, as used by some databases for DSNs, e.g. `https://foo.com,bar.com/path`. + + Internal URL logic uses the [url rust crate](https://docs.rs/url/latest/url/) originally developed + by Mozilla. + """ + + def __init__(self, url: str) -> None: ... + def __new__(cls, url: str) -> Self: ... + @property + def scheme(self) -> str: ... + @property + def path(self) -> str | None: ... + @property + def query(self) -> str | None: ... + def query_params(self) -> list[tuple[str, str]]: ... + @property + def fragment(self) -> str | None: ... + def hosts(self) -> list[MultiHostHost]: ... + def unicode_string(self) -> str: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def __deepcopy__(self, memo: dict) -> Self: ... + @classmethod + def build( + cls, + *, + scheme: str, + hosts: list[MultiHostHost] | None = None, + username: str | None = None, + password: str | None = None, + host: str | None = None, + port: int | None = None, + path: str | None = None, + query: str | None = None, + fragment: str | None = None, + ) -> Self: ... + +@final +class SchemaError(Exception): + """ + Information about errors that occur while building a [`SchemaValidator`][pydantic_core.SchemaValidator] + or [`SchemaSerializer`][pydantic_core.SchemaSerializer]. + """ + + def error_count(self) -> int: + """ + Returns: + The number of errors in the schema. + """ + def errors(self) -> list[ErrorDetails]: + """ + Returns: + A list of [`ErrorDetails`][pydantic_core.ErrorDetails] for each error in the schema. + """ + +class ValidationError(ValueError): + """ + `ValidationError` is the exception raised by `pydantic-core` when validation fails, it contains a list of errors + which detail why validation failed. + """ + @classmethod + def from_exception_data( + cls, + title: str, + line_errors: list[InitErrorDetails], + input_type: Literal['python', 'json'] = 'python', + hide_input: bool = False, + ) -> Self: + """ + Python constructor for a Validation Error. + + Arguments: + title: The title of the error, as used in the heading of `str(validation_error)` + line_errors: A list of [`InitErrorDetails`][pydantic_core.InitErrorDetails] which contain information + about errors that occurred during validation. + input_type: Whether the error is for a Python object or JSON. + hide_input: Whether to hide the input value in the error message. + """ + @property + def title(self) -> str: + """ + The title of the error, as used in the heading of `str(validation_error)`. + """ + def error_count(self) -> int: + """ + Returns: + The number of errors in the validation error. + """ + def errors( + self, *, include_url: bool = True, include_context: bool = True, include_input: bool = True + ) -> list[ErrorDetails]: + """ + Details about each error in the validation error. + + Args: + include_url: Whether to include a URL to documentation on the error each error. + include_context: Whether to include the context of each error. + include_input: Whether to include the input value of each error. + + Returns: + A list of [`ErrorDetails`][pydantic_core.ErrorDetails] for each error in the validation error. + """ + def json( + self, + *, + indent: int | None = None, + include_url: bool = True, + include_context: bool = True, + include_input: bool = True, + ) -> str: + """ + Same as [`errors()`][pydantic_core.ValidationError.errors] but returns a JSON string. + + Args: + indent: The number of spaces to indent the JSON by, or `None` for no indentation - compact JSON. + include_url: Whether to include a URL to documentation on the error each error. + include_context: Whether to include the context of each error. + include_input: Whether to include the input value of each error. + + Returns: + a JSON string. + """ + + def __repr__(self) -> str: + """ + A string representation of the validation error. + + Whether or not documentation URLs are included in the repr is controlled by the + environment variable `PYDANTIC_ERRORS_INCLUDE_URL` being set to `1` or + `true`; by default, URLs are shown. + + Due to implementation details, this environment variable can only be set once, + before the first validation error is created. + """ + +class PydanticCustomError(ValueError): + """A custom exception providing flexible error handling for Pydantic validators. + + You can raise this error in custom validators when you'd like flexibility in regards to the error type, message, and context. + + Example: + ```py + from pydantic_core import PydanticCustomError + + def custom_validator(v) -> None: + if v <= 10: + raise PydanticCustomError('custom_value_error', 'Value must be greater than {value}', {'value': 10, 'extra_context': 'extra_data'}) + return v + ``` + + Arguments: + error_type: The error type. + message_template: The message template. + context: The data to inject into the message template. + """ + + def __init__( + self, error_type: LiteralString, message_template: LiteralString, context: dict[str, Any] | None = None, / + ) -> None: ... + @property + def context(self) -> dict[str, Any] | None: + """Values which are required to render the error message, and could hence be useful in passing error data forward.""" + + @property + def type(self) -> str: + """The error type associated with the error. For consistency with Pydantic, this is typically a snake_case string.""" + + @property + def message_template(self) -> str: + """The message template associated with the error. This is a string that can be formatted with context variables in `{curly_braces}`.""" + + def message(self) -> str: + """The formatted message associated with the error. This presents as the message template with context variables appropriately injected.""" + +@final +class PydanticKnownError(ValueError): + """A helper class for raising exceptions that mimic Pydantic's built-in exceptions, with more flexibility in regards to context. + + Unlike [`PydanticCustomError`][pydantic_core.PydanticCustomError], the `error_type` argument must be a known `ErrorType`. + + Example: + ```py + from pydantic_core import PydanticKnownError + + def custom_validator(v) -> None: + if v <= 10: + raise PydanticKnownError('greater_than', {'gt': 10}) + return v + ``` + + Arguments: + error_type: The error type. + context: The data to inject into the message template. + """ + + def __init__(self, error_type: ErrorType, context: dict[str, Any] | None = None, /) -> None: ... + @property + def context(self) -> dict[str, Any] | None: + """Values which are required to render the error message, and could hence be useful in passing error data forward.""" + + @property + def type(self) -> ErrorType: + """The type of the error.""" + + @property + def message_template(self) -> str: + """The message template associated with the provided error type. This is a string that can be formatted with context variables in `{curly_braces}`.""" + + def message(self) -> str: + """The formatted message associated with the error. This presents as the message template with context variables appropriately injected.""" + +@final +class PydanticOmit(Exception): + """An exception to signal that a field should be omitted from a generated result. + + This could span from omitting a field from a JSON Schema to omitting a field from a serialized result. + Upcoming: more robust support for using PydanticOmit in custom serializers is still in development. + Right now, this is primarily used in the JSON Schema generation process. + + Example: + ```py + from typing import Callable + + from pydantic_core import PydanticOmit + + from pydantic import BaseModel + from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue + + + class MyGenerateJsonSchema(GenerateJsonSchema): + def handle_invalid_for_json_schema(self, schema, error_info) -> JsonSchemaValue: + raise PydanticOmit + + + class Predicate(BaseModel): + name: str = 'no-op' + func: Callable = lambda x: x + + + instance_example = Predicate() + + validation_schema = instance_example.model_json_schema(schema_generator=MyGenerateJsonSchema, mode='validation') + print(validation_schema) + ''' + {'properties': {'name': {'default': 'no-op', 'title': 'Name', 'type': 'string'}}, 'title': 'Predicate', 'type': 'object'} + ''' + ``` + + For a more in depth example / explanation, see the [customizing JSON schema](../concepts/json_schema.md#customizing-the-json-schema-generation-process) docs. + """ + + def __new__(cls) -> Self: ... + +@final +class PydanticUseDefault(Exception): + """An exception to signal that standard validation either failed or should be skipped, and the default value should be used instead. + + This warning can be raised in custom validation functions to redirect the flow of validation. + + Example: + ```py + from pydantic_core import PydanticUseDefault + from datetime import datetime + from pydantic import BaseModel, field_validator + + + class Event(BaseModel): + name: str = 'meeting' + time: datetime + + @field_validator('name', mode='plain') + def name_must_be_present(cls, v) -> str: + if not v or not isinstance(v, str): + raise PydanticUseDefault() + return v + + + event1 = Event(name='party', time=datetime(2024, 1, 1, 12, 0, 0)) + print(repr(event1)) + # > Event(name='party', time=datetime.datetime(2024, 1, 1, 12, 0)) + event2 = Event(time=datetime(2024, 1, 1, 12, 0, 0)) + print(repr(event2)) + # > Event(name='meeting', time=datetime.datetime(2024, 1, 1, 12, 0)) + ``` + + For an additional example, see the [validating partial json data](../concepts/json.md#partial-json-parsing) section of the Pydantic documentation. + """ + + def __new__(cls) -> Self: ... + +@final +class PydanticSerializationError(ValueError): + """An error raised when an issue occurs during serialization. + + In custom serializers, this error can be used to indicate that serialization has failed. + + Arguments: + message: The message associated with the error. + """ + + def __init__(self, message: str, /) -> None: ... + +@final +class PydanticSerializationUnexpectedValue(ValueError): + """An error raised when an unexpected value is encountered during serialization. + + This error is often caught and coerced into a warning, as `pydantic-core` generally makes a best attempt + at serializing values, in contrast with validation where errors are eagerly raised. + + Example: + ```py + from pydantic import BaseModel, field_serializer + from pydantic_core import PydanticSerializationUnexpectedValue + + class BasicPoint(BaseModel): + x: int + y: int + + @field_serializer('*') + def serialize(self, v): + if not isinstance(v, int): + raise PydanticSerializationUnexpectedValue(f'Expected type `int`, got {type(v)} with value {v}') + return v + + point = BasicPoint(x=1, y=2) + # some sort of mutation + point.x = 'a' + + print(point.model_dump()) + ''' + UserWarning: Pydantic serializer warnings: + PydanticSerializationUnexpectedValue(Expected type `int`, got with value a) + return self.__pydantic_serializer__.to_python( + {'x': 'a', 'y': 2} + ''' + ``` + + This is often used internally in `pydantic-core` when unexpected types are encountered during serialization, + but it can also be used by users in custom serializers, as seen above. + + Arguments: + message: The message associated with the unexpected value. + """ + + def __init__(self, message: str, /) -> None: ... + +@final +class ArgsKwargs: + """A construct used to store arguments and keyword arguments for a function call. + + This data structure is generally used to store information for core schemas associated with functions (like in an arguments schema). + This data structure is also currently used for some validation against dataclasses. + + Example: + ```py + from pydantic.dataclasses import dataclass + from pydantic import model_validator + + + @dataclass + class Model: + a: int + b: int + + @model_validator(mode="before") + @classmethod + def no_op_validator(cls, values): + print(values) + return values + + Model(1, b=2) + #> ArgsKwargs((1,), {"b": 2}) + + Model(1, 2) + #> ArgsKwargs((1, 2), {}) + + Model(a=1, b=2) + #> ArgsKwargs((), {"a": 1, "b": 2}) + ``` + """ + + def __init__(self, args: tuple[Any, ...], kwargs: dict[str, Any] | None = None) -> None: + """Initializes the `ArgsKwargs`. + + Arguments: + args: The arguments (inherently ordered) for a function call. + kwargs: The keyword arguments for a function call + """ + + def __new__(cls, args: tuple[Any, ...], kwargs: dict[str, Any] | None = None) -> Self: ... + @property + def args(self) -> tuple[Any, ...]: + """The arguments (inherently ordered) for a function call.""" + + @property + def kwargs(self) -> dict[str, Any] | None: + """The keyword arguments for a function call.""" + +@final +class PydanticUndefinedType: + """A type used as a sentinel for undefined values.""" + + def __copy__(self) -> Self: ... + def __deepcopy__(self, memo: Any) -> Self: ... + +PydanticUndefined: PydanticUndefinedType + +def list_all_errors() -> list[ErrorTypeInfo]: + """ + Get information about all built-in errors. + + Returns: + A list of `ErrorTypeInfo` typed dicts. + """ +@final +class TzInfo(datetime.tzinfo): + """An `pydantic-core` implementation of the abstract [`datetime.tzinfo`][] class.""" + + def __init__(self, seconds: float = 0.0) -> None: + """Initializes the `TzInfo`. + + Arguments: + seconds: The offset from UTC in seconds. Defaults to 0.0 (UTC). + """ + + def __new__(cls, seconds: float = 0.0) -> Self: ... + + # Docstrings for attributes sourced from the abstract base class, [`datetime.tzinfo`](https://docs.python.org/3/library/datetime.html#datetime.tzinfo). + + def tzname(self, dt: datetime.datetime | None) -> str | None: + """Return the time zone name corresponding to the [`datetime`][datetime.datetime] object _dt_, as a string. + + For more info, see [`tzinfo.tzname`][datetime.tzinfo.tzname]. + """ + + def utcoffset(self, dt: datetime.datetime | None) -> datetime.timedelta | None: + """Return offset of local time from UTC, as a [`timedelta`][datetime.timedelta] object that is positive east of UTC. If local time is west of UTC, this should be negative. + + More info can be found at [`tzinfo.utcoffset`][datetime.tzinfo.utcoffset]. + """ + + def dst(self, dt: datetime.datetime | None) -> datetime.timedelta | None: + """Return the daylight saving time (DST) adjustment, as a [`timedelta`][datetime.timedelta] object or `None` if DST information isn’t known. + + More info can be found at[`tzinfo.dst`][datetime.tzinfo.dst].""" + + def fromutc(self, dt: datetime.datetime) -> datetime.datetime: + """Adjust the date and time data associated datetime object _dt_, returning an equivalent datetime in self’s local time. + + More info can be found at [`tzinfo.fromutc`][datetime.tzinfo.fromutc].""" + + def __deepcopy__(self, _memo: dict[Any, Any]) -> TzInfo: ... diff --git a/source/target-vendors/cp312/pydantic_core/core_schema.py b/source/target-vendors/cp312/pydantic_core/core_schema.py new file mode 100644 index 0000000..6ff202c --- /dev/null +++ b/source/target-vendors/cp312/pydantic_core/core_schema.py @@ -0,0 +1,4461 @@ +""" +This module contains definitions to build schemas which `pydantic_core` can +validate and serialize. +""" + +from __future__ import annotations as _annotations + +import sys +import warnings +from collections.abc import Generator, Hashable, Mapping +from datetime import date, datetime, time, timedelta +from decimal import Decimal +from re import Pattern +from typing import TYPE_CHECKING, Any, Callable, Literal, Union + +from typing_extensions import TypeVar, deprecated + +if sys.version_info < (3, 12): + from typing_extensions import TypedDict +else: + from typing import TypedDict + +if sys.version_info < (3, 11): + from typing_extensions import Protocol, Required, TypeAlias +else: + from typing import Protocol, Required, TypeAlias + +if TYPE_CHECKING: + from pydantic_core import PydanticUndefined +else: + # The initial build of pydantic_core requires PydanticUndefined to generate + # the core schema; so we need to conditionally skip it. mypy doesn't like + # this at all, hence the TYPE_CHECKING branch above. + try: + from pydantic_core import PydanticUndefined + except ImportError: + PydanticUndefined = object() + + +ExtraBehavior = Literal['allow', 'forbid', 'ignore'] + + +class CoreConfig(TypedDict, total=False): + """ + Base class for schema configuration options. + + Attributes: + title: The name of the configuration. + strict: Whether the configuration should strictly adhere to specified rules. + extra_fields_behavior: The behavior for handling extra fields. + typed_dict_total: Whether the TypedDict should be considered total. Default is `True`. + from_attributes: Whether to use attributes for models, dataclasses, and tagged union keys. + loc_by_alias: Whether to use the used alias (or first alias for "field required" errors) instead of + `field_names` to construct error `loc`s. Default is `True`. + revalidate_instances: Whether instances of models and dataclasses should re-validate. Default is 'never'. + validate_default: Whether to validate default values during validation. Default is `False`. + str_max_length: The maximum length for string fields. + str_min_length: The minimum length for string fields. + str_strip_whitespace: Whether to strip whitespace from string fields. + str_to_lower: Whether to convert string fields to lowercase. + str_to_upper: Whether to convert string fields to uppercase. + allow_inf_nan: Whether to allow infinity and NaN values for float fields. Default is `True`. + ser_json_timedelta: The serialization option for `timedelta` values. Default is 'iso8601'. + Note that if ser_json_temporal is set, then this param will be ignored. + ser_json_temporal: The serialization option for datetime like values. Default is 'iso8601'. + The types this covers are datetime, date, time and timedelta. + If this is set, it will take precedence over ser_json_timedelta + ser_json_bytes: The serialization option for `bytes` values. Default is 'utf8'. + ser_json_inf_nan: The serialization option for infinity and NaN values + in float fields. Default is 'null'. + val_json_bytes: The validation option for `bytes` values, complementing ser_json_bytes. Default is 'utf8'. + hide_input_in_errors: Whether to hide input data from `ValidationError` representation. + validation_error_cause: Whether to add user-python excs to the __cause__ of a ValidationError. + Requires exceptiongroup backport pre Python 3.11. + coerce_numbers_to_str: Whether to enable coercion of any `Number` type to `str` (not applicable in `strict` mode). + regex_engine: The regex engine to use for regex pattern validation. Default is 'rust-regex'. See `StringSchema`. + cache_strings: Whether to cache strings. Default is `True`, `True` or `'all'` is required to cache strings + during general validation since validators don't know if they're in a key or a value. + validate_by_alias: Whether to use the field's alias when validating against the provided input data. Default is `True`. + validate_by_name: Whether to use the field's name when validating against the provided input data. Default is `False`. Replacement for `populate_by_name`. + serialize_by_alias: Whether to serialize by alias. Default is `False`, expected to change to `True` in V3. + polymorphic_serialization: Whether to enable polymorphic serialization for models and dataclasses. Default is `False`. + url_preserve_empty_path: Whether to preserve empty URL paths when validating values for a URL type. Defaults to `False`. + """ + + title: str + strict: bool + # settings related to typed dicts, model fields, dataclass fields + extra_fields_behavior: ExtraBehavior + typed_dict_total: bool # default: True + # used for models, dataclasses, and tagged union keys + from_attributes: bool + # whether to use the used alias (or first alias for "field required" errors) instead of field_names + # to construct error `loc`s, default True + loc_by_alias: bool + # whether instances of models and dataclasses (including subclass instances) should re-validate, default 'never' + revalidate_instances: Literal['always', 'never', 'subclass-instances'] + # whether to validate default values during validation, default False + validate_default: bool + # used on typed-dicts and arguments + # fields related to string fields only + str_max_length: int + str_min_length: int + str_strip_whitespace: bool + str_to_lower: bool + str_to_upper: bool + # fields related to float fields only + allow_inf_nan: bool # default: True + # the config options are used to customise serialization to JSON + ser_json_timedelta: Literal['iso8601', 'float'] # default: 'iso8601' + ser_json_temporal: Literal['iso8601', 'seconds', 'milliseconds'] # default: 'iso8601' + ser_json_bytes: Literal['utf8', 'base64', 'hex'] # default: 'utf8' + ser_json_inf_nan: Literal['null', 'constants', 'strings'] # default: 'null' + val_json_bytes: Literal['utf8', 'base64', 'hex'] # default: 'utf8' + # used to hide input data from ValidationError repr + hide_input_in_errors: bool + validation_error_cause: bool # default: False + coerce_numbers_to_str: bool # default: False + regex_engine: Literal['rust-regex', 'python-re'] # default: 'rust-regex' + cache_strings: Union[bool, Literal['all', 'keys', 'none']] # default: 'True' + validate_by_alias: bool # default: True + validate_by_name: bool # default: False + serialize_by_alias: bool # default: False + polymorphic_serialization: bool # default: False + url_preserve_empty_path: bool # default: False + + +IncExCall: TypeAlias = 'set[int | str] | dict[int | str, IncExCall] | None' + +ContextT = TypeVar('ContextT', covariant=True, default='Any | None') + + +class SerializationInfo(Protocol[ContextT]): + """Extra data used during serialization.""" + + @property + def include(self) -> IncExCall: + """The `include` argument set during serialization.""" + ... + + @property + def exclude(self) -> IncExCall: + """The `exclude` argument set during serialization.""" + ... + + @property + def context(self) -> ContextT: + """The current serialization context.""" + ... + + @property + def mode(self) -> Literal['python', 'json'] | str: + """The serialization mode set during serialization.""" + ... + + @property + def by_alias(self) -> bool: + """The `by_alias` argument set during serialization.""" + ... + + @property + def exclude_unset(self) -> bool: + """The `exclude_unset` argument set during serialization.""" + ... + + @property + def exclude_defaults(self) -> bool: + """The `exclude_defaults` argument set during serialization.""" + ... + + @property + def exclude_none(self) -> bool: + """The `exclude_none` argument set during serialization.""" + ... + + @property + def exclude_computed_fields(self) -> bool: + """The `exclude_computed_fields` argument set during serialization.""" + ... + + @property + def serialize_as_any(self) -> bool: + """The `serialize_as_any` argument set during serialization.""" + ... + + @property + def polymorphic_serialization(self) -> bool | None: + """The `polymorphic_serialization` argument set during serialization, if any.""" + ... + + @property + def round_trip(self) -> bool: + """The `round_trip` argument set during serialization.""" + ... + + def mode_is_json(self) -> bool: ... + + def __str__(self) -> str: ... + + def __repr__(self) -> str: ... + + +class FieldSerializationInfo(SerializationInfo[ContextT], Protocol): + """Extra data used during field serialization.""" + + @property + def field_name(self) -> str: + """The name of the current field being serialized.""" + ... + + +class ValidationInfo(Protocol[ContextT]): + """Extra data used during validation.""" + + @property + def context(self) -> ContextT: + """The current validation context.""" + ... + + @property + def config(self) -> CoreConfig | None: + """The CoreConfig that applies to this validation.""" + ... + + @property + def mode(self) -> Literal['python', 'json']: + """The type of input data we are currently validating.""" + ... + + @property + def data(self) -> dict[str, Any]: + """The data being validated for this model.""" + ... + + @property + def field_name(self) -> str | None: + """ + The name of the current field being validated if this validator is + attached to a model field. + """ + ... + + +ExpectedSerializationTypes = Literal[ + 'none', + 'int', + 'bool', + 'float', + 'str', + 'bytes', + 'bytearray', + 'list', + 'tuple', + 'set', + 'frozenset', + 'generator', + 'dict', + 'datetime', + 'date', + 'time', + 'timedelta', + 'url', + 'multi-host-url', + 'json', + 'uuid', + 'any', +] + + +class SimpleSerSchema(TypedDict, total=False): + type: Required[ExpectedSerializationTypes] + + +def simple_ser_schema(type: ExpectedSerializationTypes) -> SimpleSerSchema: + """ + Returns a schema for serialization with a custom type. + + Args: + type: The type to use for serialization + """ + return SimpleSerSchema(type=type) + + +# (input_value: Any, /) -> Any +GeneralPlainNoInfoSerializerFunction = Callable[[Any], Any] +# (input_value: Any, info: FieldSerializationInfo, /) -> Any +GeneralPlainInfoSerializerFunction = Callable[[Any, SerializationInfo[Any]], Any] +# (model: Any, input_value: Any, /) -> Any +FieldPlainNoInfoSerializerFunction = Callable[[Any, Any], Any] +# (model: Any, input_value: Any, info: FieldSerializationInfo, /) -> Any +FieldPlainInfoSerializerFunction = Callable[[Any, Any, FieldSerializationInfo[Any]], Any] +SerializerFunction = Union[ + GeneralPlainNoInfoSerializerFunction, + GeneralPlainInfoSerializerFunction, + FieldPlainNoInfoSerializerFunction, + FieldPlainInfoSerializerFunction, +] + +WhenUsed = Literal['always', 'unless-none', 'json', 'json-unless-none'] +""" +Values have the following meanings: + +* `'always'` means always use +* `'unless-none'` means use unless the value is `None` +* `'json'` means use when serializing to JSON +* `'json-unless-none'` means use when serializing to JSON and the value is not `None` +""" + + +class PlainSerializerFunctionSerSchema(TypedDict, total=False): + type: Required[Literal['function-plain']] + function: Required[SerializerFunction] + is_field_serializer: bool # default False + info_arg: bool # default False + return_schema: CoreSchema # if omitted, AnySchema is used + when_used: WhenUsed # default: 'always' + + +def plain_serializer_function_ser_schema( + function: SerializerFunction, + *, + is_field_serializer: bool | None = None, + info_arg: bool | None = None, + return_schema: CoreSchema | None = None, + when_used: WhenUsed = 'always', +) -> PlainSerializerFunctionSerSchema: + """ + Returns a schema for serialization with a function, can be either a "general" or "field" function. + + Args: + function: The function to use for serialization + is_field_serializer: Whether the serializer is for a field, e.g. takes `model` as the first argument, + and `info` includes `field_name` + info_arg: Whether the function takes an `info` argument + return_schema: Schema to use for serializing return value + when_used: When the function should be called + """ + if when_used == 'always': + # just to avoid extra elements in schema, and to use the actual default defined in rust + when_used = None # type: ignore + return _dict_not_none( + type='function-plain', + function=function, + is_field_serializer=is_field_serializer, + info_arg=info_arg, + return_schema=return_schema, + when_used=when_used, + ) + + +class SerializerFunctionWrapHandler(Protocol): # pragma: no cover + def __call__(self, input_value: Any, index_key: int | str | None = None, /) -> Any: ... + + +# (input_value: Any, serializer: SerializerFunctionWrapHandler, /) -> Any +GeneralWrapNoInfoSerializerFunction = Callable[[Any, SerializerFunctionWrapHandler], Any] +# (input_value: Any, serializer: SerializerFunctionWrapHandler, info: SerializationInfo, /) -> Any +GeneralWrapInfoSerializerFunction = Callable[[Any, SerializerFunctionWrapHandler, SerializationInfo[Any]], Any] +# (model: Any, input_value: Any, serializer: SerializerFunctionWrapHandler, /) -> Any +FieldWrapNoInfoSerializerFunction = Callable[[Any, Any, SerializerFunctionWrapHandler], Any] +# (model: Any, input_value: Any, serializer: SerializerFunctionWrapHandler, info: FieldSerializationInfo, /) -> Any +FieldWrapInfoSerializerFunction = Callable[[Any, Any, SerializerFunctionWrapHandler, FieldSerializationInfo[Any]], Any] +WrapSerializerFunction = Union[ + GeneralWrapNoInfoSerializerFunction, + GeneralWrapInfoSerializerFunction, + FieldWrapNoInfoSerializerFunction, + FieldWrapInfoSerializerFunction, +] + + +class WrapSerializerFunctionSerSchema(TypedDict, total=False): + type: Required[Literal['function-wrap']] + function: Required[WrapSerializerFunction] + is_field_serializer: bool # default False + info_arg: bool # default False + schema: CoreSchema # if omitted, the schema on which this serializer is defined is used + return_schema: CoreSchema # if omitted, AnySchema is used + when_used: WhenUsed # default: 'always' + + +def wrap_serializer_function_ser_schema( + function: WrapSerializerFunction, + *, + is_field_serializer: bool | None = None, + info_arg: bool | None = None, + schema: CoreSchema | None = None, + return_schema: CoreSchema | None = None, + when_used: WhenUsed = 'always', +) -> WrapSerializerFunctionSerSchema: + """ + Returns a schema for serialization with a wrap function, can be either a "general" or "field" function. + + Args: + function: The function to use for serialization + is_field_serializer: Whether the serializer is for a field, e.g. takes `model` as the first argument, + and `info` includes `field_name` + info_arg: Whether the function takes an `info` argument + schema: The schema to use for the inner serialization + return_schema: Schema to use for serializing return value + when_used: When the function should be called + """ + if when_used == 'always': + # just to avoid extra elements in schema, and to use the actual default defined in rust + when_used = None # type: ignore + return _dict_not_none( + type='function-wrap', + function=function, + is_field_serializer=is_field_serializer, + info_arg=info_arg, + schema=schema, + return_schema=return_schema, + when_used=when_used, + ) + + +class FormatSerSchema(TypedDict, total=False): + type: Required[Literal['format']] + formatting_string: Required[str] + when_used: WhenUsed # default: 'json-unless-none' + + +def format_ser_schema(formatting_string: str, *, when_used: WhenUsed = 'json-unless-none') -> FormatSerSchema: + """ + Returns a schema for serialization using python's `format` method. + + Args: + formatting_string: String defining the format to use + when_used: Same meaning as for [general_function_plain_ser_schema], but with a different default + """ + if when_used == 'json-unless-none': + # just to avoid extra elements in schema, and to use the actual default defined in rust + when_used = None # type: ignore + return _dict_not_none(type='format', formatting_string=formatting_string, when_used=when_used) + + +class ToStringSerSchema(TypedDict, total=False): + type: Required[Literal['to-string']] + when_used: WhenUsed # default: 'json-unless-none' + + +def to_string_ser_schema(*, when_used: WhenUsed = 'json-unless-none') -> ToStringSerSchema: + """ + Returns a schema for serialization using python's `str()` / `__str__` method. + + Args: + when_used: Same meaning as for [general_function_plain_ser_schema], but with a different default + """ + s = dict(type='to-string') + if when_used != 'json-unless-none': + # just to avoid extra elements in schema, and to use the actual default defined in rust + s['when_used'] = when_used + return s # type: ignore + + +class ModelSerSchema(TypedDict, total=False): + type: Required[Literal['model']] + cls: Required[type[Any]] + schema: Required[CoreSchema] + + +def model_ser_schema(cls: type[Any], schema: CoreSchema) -> ModelSerSchema: + """ + Returns a schema for serialization using a model. + + Args: + cls: The expected class type, used to generate warnings if the wrong type is passed + schema: Internal schema to use to serialize the model dict + """ + return ModelSerSchema(type='model', cls=cls, schema=schema) + + +SerSchema = Union[ + SimpleSerSchema, + PlainSerializerFunctionSerSchema, + WrapSerializerFunctionSerSchema, + FormatSerSchema, + ToStringSerSchema, + ModelSerSchema, +] + + +class InvalidSchema(TypedDict, total=False): + type: Required[Literal['invalid']] + ref: str + metadata: dict[str, Any] + # note, we never plan to use this, but include it for type checking purposes to match + # all other CoreSchema union members + serialization: SerSchema + + +def invalid_schema(ref: str | None = None, metadata: dict[str, Any] | None = None) -> InvalidSchema: + """ + Returns an invalid schema, used to indicate that a schema is invalid. + + Args: + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + """ + + return _dict_not_none(type='invalid', ref=ref, metadata=metadata) + + +class ComputedField(TypedDict, total=False): + type: Required[Literal['computed-field']] + property_name: Required[str] + return_schema: Required[CoreSchema] + alias: str + serialization_exclude_if: Callable[[Any], bool] + metadata: dict[str, Any] + + +def computed_field( + property_name: str, + return_schema: CoreSchema, + *, + alias: str | None = None, + serialization_exclude_if: Callable[[Any], bool] | None = None, + metadata: dict[str, Any] | None = None, +) -> ComputedField: + """ + ComputedFields are properties of a model or dataclass that are included in serialization. + + Args: + property_name: The name of the property on the model or dataclass + return_schema: The schema used for the type returned by the computed field + alias: The name to use in the serialized output + metadata: Any other information you want to include with the schema, not used by pydantic-core + """ + return _dict_not_none( + type='computed-field', + property_name=property_name, + return_schema=return_schema, + alias=alias, + serialization_exclude_if=serialization_exclude_if, + metadata=metadata, + ) + + +class AnySchema(TypedDict, total=False): + type: Required[Literal['any']] + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def any_schema( + *, ref: str | None = None, metadata: dict[str, Any] | None = None, serialization: SerSchema | None = None +) -> AnySchema: + """ + Returns a schema that matches any value, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.any_schema() + v = SchemaValidator(schema) + assert v.validate_python(1) == 1 + ``` + + Args: + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none(type='any', ref=ref, metadata=metadata, serialization=serialization) + + +class NoneSchema(TypedDict, total=False): + type: Required[Literal['none']] + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def none_schema( + *, ref: str | None = None, metadata: dict[str, Any] | None = None, serialization: SerSchema | None = None +) -> NoneSchema: + """ + Returns a schema that matches a None value, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.none_schema() + v = SchemaValidator(schema) + assert v.validate_python(None) is None + ``` + + Args: + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none(type='none', ref=ref, metadata=metadata, serialization=serialization) + + +class BoolSchema(TypedDict, total=False): + type: Required[Literal['bool']] + strict: bool + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def bool_schema( + strict: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> BoolSchema: + """ + Returns a schema that matches a bool value, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.bool_schema() + v = SchemaValidator(schema) + assert v.validate_python('True') is True + ``` + + Args: + strict: Whether the value should be a bool or a value that can be converted to a bool + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none(type='bool', strict=strict, ref=ref, metadata=metadata, serialization=serialization) + + +class IntSchema(TypedDict, total=False): + type: Required[Literal['int']] + multiple_of: int + le: int + ge: int + lt: int + gt: int + strict: bool + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def int_schema( + *, + multiple_of: int | None = None, + le: int | None = None, + ge: int | None = None, + lt: int | None = None, + gt: int | None = None, + strict: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> IntSchema: + """ + Returns a schema that matches a int value, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.int_schema(multiple_of=2, le=6, ge=2) + v = SchemaValidator(schema) + assert v.validate_python('4') == 4 + ``` + + Args: + multiple_of: The value must be a multiple of this number + le: The value must be less than or equal to this number + ge: The value must be greater than or equal to this number + lt: The value must be strictly less than this number + gt: The value must be strictly greater than this number + strict: Whether the value should be a int or a value that can be converted to a int + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='int', + multiple_of=multiple_of, + le=le, + ge=ge, + lt=lt, + gt=gt, + strict=strict, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class FloatSchema(TypedDict, total=False): + type: Required[Literal['float']] + allow_inf_nan: bool # whether 'NaN', '+inf', '-inf' should be forbidden. default: True + multiple_of: float + le: float + ge: float + lt: float + gt: float + strict: bool + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def float_schema( + *, + allow_inf_nan: bool | None = None, + multiple_of: float | None = None, + le: float | None = None, + ge: float | None = None, + lt: float | None = None, + gt: float | None = None, + strict: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> FloatSchema: + """ + Returns a schema that matches a float value, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.float_schema(le=0.8, ge=0.2) + v = SchemaValidator(schema) + assert v.validate_python('0.5') == 0.5 + ``` + + Args: + allow_inf_nan: Whether to allow inf and nan values + multiple_of: The value must be a multiple of this number + le: The value must be less than or equal to this number + ge: The value must be greater than or equal to this number + lt: The value must be strictly less than this number + gt: The value must be strictly greater than this number + strict: Whether the value should be a float or a value that can be converted to a float + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='float', + allow_inf_nan=allow_inf_nan, + multiple_of=multiple_of, + le=le, + ge=ge, + lt=lt, + gt=gt, + strict=strict, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class DecimalSchema(TypedDict, total=False): + type: Required[Literal['decimal']] + allow_inf_nan: bool # whether 'NaN', '+inf', '-inf' should be forbidden. default: False + multiple_of: Decimal + le: Decimal + ge: Decimal + lt: Decimal + gt: Decimal + max_digits: int + decimal_places: int + strict: bool + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def decimal_schema( + *, + allow_inf_nan: bool | None = None, + multiple_of: Decimal | None = None, + le: Decimal | None = None, + ge: Decimal | None = None, + lt: Decimal | None = None, + gt: Decimal | None = None, + max_digits: int | None = None, + decimal_places: int | None = None, + strict: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> DecimalSchema: + """ + Returns a schema that matches a decimal value, e.g.: + + ```py + from decimal import Decimal + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.decimal_schema(le=0.8, ge=0.2) + v = SchemaValidator(schema) + assert v.validate_python('0.5') == Decimal('0.5') + ``` + + Args: + allow_inf_nan: Whether to allow inf and nan values + multiple_of: The value must be a multiple of this number + le: The value must be less than or equal to this number + ge: The value must be greater than or equal to this number + lt: The value must be strictly less than this number + gt: The value must be strictly greater than this number + max_digits: The maximum number of decimal digits allowed + decimal_places: The maximum number of decimal places allowed + strict: Whether the value should be a float or a value that can be converted to a float + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='decimal', + gt=gt, + ge=ge, + lt=lt, + le=le, + max_digits=max_digits, + decimal_places=decimal_places, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + strict=strict, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class ComplexSchema(TypedDict, total=False): + type: Required[Literal['complex']] + strict: bool + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def complex_schema( + *, + strict: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> ComplexSchema: + """ + Returns a schema that matches a complex value, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.complex_schema() + v = SchemaValidator(schema) + assert v.validate_python('1+2j') == complex(1, 2) + assert v.validate_python(complex(1, 2)) == complex(1, 2) + ``` + + Args: + strict: Whether the value should be a complex object instance or a value that can be converted to a complex object + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='complex', + strict=strict, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class StringSchema(TypedDict, total=False): + type: Required[Literal['str']] + pattern: Union[str, Pattern[str]] + max_length: int + min_length: int + strip_whitespace: bool + to_lower: bool + to_upper: bool + regex_engine: Literal['rust-regex', 'python-re'] # default: 'rust-regex' + strict: bool + coerce_numbers_to_str: bool + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def str_schema( + *, + pattern: str | Pattern[str] | None = None, + max_length: int | None = None, + min_length: int | None = None, + strip_whitespace: bool | None = None, + to_lower: bool | None = None, + to_upper: bool | None = None, + regex_engine: Literal['rust-regex', 'python-re'] | None = None, + strict: bool | None = None, + coerce_numbers_to_str: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> StringSchema: + """ + Returns a schema that matches a string value, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.str_schema(max_length=10, min_length=2) + v = SchemaValidator(schema) + assert v.validate_python('hello') == 'hello' + ``` + + Args: + pattern: A regex pattern that the value must match + max_length: The value must be at most this length + min_length: The value must be at least this length + strip_whitespace: Whether to strip whitespace from the value + to_lower: Whether to convert the value to lowercase + to_upper: Whether to convert the value to uppercase + regex_engine: The regex engine to use for pattern validation. Default is 'rust-regex'. + - `rust-regex` uses the [`regex`](https://docs.rs/regex) Rust + crate, which is non-backtracking and therefore more DDoS + resistant, but does not support all regex features. + - `python-re` use the [`re`](https://docs.python.org/3/library/re.html) module, + which supports all regex features, but may be slower. + strict: Whether the value should be a string or a value that can be converted to a string + coerce_numbers_to_str: Whether to enable coercion of any `Number` type to `str` (not applicable in `strict` mode). + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='str', + pattern=pattern, + max_length=max_length, + min_length=min_length, + strip_whitespace=strip_whitespace, + to_lower=to_lower, + to_upper=to_upper, + regex_engine=regex_engine, + strict=strict, + coerce_numbers_to_str=coerce_numbers_to_str, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class BytesSchema(TypedDict, total=False): + type: Required[Literal['bytes']] + max_length: int + min_length: int + strict: bool + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def bytes_schema( + *, + max_length: int | None = None, + min_length: int | None = None, + strict: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> BytesSchema: + """ + Returns a schema that matches a bytes value, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.bytes_schema(max_length=10, min_length=2) + v = SchemaValidator(schema) + assert v.validate_python(b'hello') == b'hello' + ``` + + Args: + max_length: The value must be at most this length + min_length: The value must be at least this length + strict: Whether the value should be a bytes or a value that can be converted to a bytes + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='bytes', + max_length=max_length, + min_length=min_length, + strict=strict, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class DateSchema(TypedDict, total=False): + type: Required[Literal['date']] + strict: bool + le: date + ge: date + lt: date + gt: date + now_op: Literal['past', 'future'] + # defaults to current local utc offset from `time.localtime().tm_gmtoff` + # value is restricted to -86_400 < offset < 86_400: + now_utc_offset: int + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def date_schema( + *, + strict: bool | None = None, + le: date | None = None, + ge: date | None = None, + lt: date | None = None, + gt: date | None = None, + now_op: Literal['past', 'future'] | None = None, + now_utc_offset: int | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> DateSchema: + """ + Returns a schema that matches a date value, e.g.: + + ```py + from datetime import date + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.date_schema(le=date(2020, 1, 1), ge=date(2019, 1, 1)) + v = SchemaValidator(schema) + assert v.validate_python(date(2019, 6, 1)) == date(2019, 6, 1) + ``` + + Args: + strict: Whether the value should be a date or a value that can be converted to a date + le: The value must be less than or equal to this date + ge: The value must be greater than or equal to this date + lt: The value must be strictly less than this date + gt: The value must be strictly greater than this date + now_op: The value must be in the past or future relative to the current date + now_utc_offset: The value must be in the past or future relative to the current date with this utc offset + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='date', + strict=strict, + le=le, + ge=ge, + lt=lt, + gt=gt, + now_op=now_op, + now_utc_offset=now_utc_offset, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class TimeSchema(TypedDict, total=False): + type: Required[Literal['time']] + strict: bool + le: time + ge: time + lt: time + gt: time + tz_constraint: Union[Literal['aware', 'naive'], int] + microseconds_precision: Literal['truncate', 'error'] + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def time_schema( + *, + strict: bool | None = None, + le: time | None = None, + ge: time | None = None, + lt: time | None = None, + gt: time | None = None, + tz_constraint: Literal['aware', 'naive'] | int | None = None, + microseconds_precision: Literal['truncate', 'error'] = 'truncate', + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> TimeSchema: + """ + Returns a schema that matches a time value, e.g.: + + ```py + from datetime import time + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.time_schema(le=time(12, 0, 0), ge=time(6, 0, 0)) + v = SchemaValidator(schema) + assert v.validate_python(time(9, 0, 0)) == time(9, 0, 0) + ``` + + Args: + strict: Whether the value should be a time or a value that can be converted to a time + le: The value must be less than or equal to this time + ge: The value must be greater than or equal to this time + lt: The value must be strictly less than this time + gt: The value must be strictly greater than this time + tz_constraint: The value must be timezone aware or naive, or an int to indicate required tz offset + microseconds_precision: The behavior when seconds have more than 6 digits or microseconds is too large + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='time', + strict=strict, + le=le, + ge=ge, + lt=lt, + gt=gt, + tz_constraint=tz_constraint, + microseconds_precision=microseconds_precision, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class DatetimeSchema(TypedDict, total=False): + type: Required[Literal['datetime']] + strict: bool + le: datetime + ge: datetime + lt: datetime + gt: datetime + now_op: Literal['past', 'future'] + tz_constraint: Union[Literal['aware', 'naive'], int] + # defaults to current local utc offset from `time.localtime().tm_gmtoff` + # value is restricted to -86_400 < offset < 86_400 by bounds in generate_self_schema.py + now_utc_offset: int + microseconds_precision: Literal['truncate', 'error'] # default: 'truncate' + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def datetime_schema( + *, + strict: bool | None = None, + le: datetime | None = None, + ge: datetime | None = None, + lt: datetime | None = None, + gt: datetime | None = None, + now_op: Literal['past', 'future'] | None = None, + tz_constraint: Literal['aware', 'naive'] | int | None = None, + now_utc_offset: int | None = None, + microseconds_precision: Literal['truncate', 'error'] = 'truncate', + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> DatetimeSchema: + """ + Returns a schema that matches a datetime value, e.g.: + + ```py + from datetime import datetime + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.datetime_schema() + v = SchemaValidator(schema) + now = datetime.now() + assert v.validate_python(str(now)) == now + ``` + + Args: + strict: Whether the value should be a datetime or a value that can be converted to a datetime + le: The value must be less than or equal to this datetime + ge: The value must be greater than or equal to this datetime + lt: The value must be strictly less than this datetime + gt: The value must be strictly greater than this datetime + now_op: The value must be in the past or future relative to the current datetime + tz_constraint: The value must be timezone aware or naive, or an int to indicate required tz offset + TODO: use of a tzinfo where offset changes based on the datetime is not yet supported + now_utc_offset: The value must be in the past or future relative to the current datetime with this utc offset + microseconds_precision: The behavior when seconds have more than 6 digits or microseconds is too large + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='datetime', + strict=strict, + le=le, + ge=ge, + lt=lt, + gt=gt, + now_op=now_op, + tz_constraint=tz_constraint, + now_utc_offset=now_utc_offset, + microseconds_precision=microseconds_precision, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class TimedeltaSchema(TypedDict, total=False): + type: Required[Literal['timedelta']] + strict: bool + le: timedelta + ge: timedelta + lt: timedelta + gt: timedelta + microseconds_precision: Literal['truncate', 'error'] + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def timedelta_schema( + *, + strict: bool | None = None, + le: timedelta | None = None, + ge: timedelta | None = None, + lt: timedelta | None = None, + gt: timedelta | None = None, + microseconds_precision: Literal['truncate', 'error'] = 'truncate', + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> TimedeltaSchema: + """ + Returns a schema that matches a timedelta value, e.g.: + + ```py + from datetime import timedelta + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.timedelta_schema(le=timedelta(days=1), ge=timedelta(days=0)) + v = SchemaValidator(schema) + assert v.validate_python(timedelta(hours=12)) == timedelta(hours=12) + ``` + + Args: + strict: Whether the value should be a timedelta or a value that can be converted to a timedelta + le: The value must be less than or equal to this timedelta + ge: The value must be greater than or equal to this timedelta + lt: The value must be strictly less than this timedelta + gt: The value must be strictly greater than this timedelta + microseconds_precision: The behavior when seconds have more than 6 digits or microseconds is too large + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='timedelta', + strict=strict, + le=le, + ge=ge, + lt=lt, + gt=gt, + microseconds_precision=microseconds_precision, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class LiteralSchema(TypedDict, total=False): + type: Required[Literal['literal']] + expected: Required[list[Any]] + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def literal_schema( + expected: list[Any], + *, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> LiteralSchema: + """ + Returns a schema that matches a literal value, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.literal_schema(['hello', 'world']) + v = SchemaValidator(schema) + assert v.validate_python('hello') == 'hello' + ``` + + Args: + expected: The value must be one of these values + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none(type='literal', expected=expected, ref=ref, metadata=metadata, serialization=serialization) + + +class EnumSchema(TypedDict, total=False): + type: Required[Literal['enum']] + cls: Required[Any] + members: Required[list[Any]] + sub_type: Literal['str', 'int', 'float'] + missing: Callable[[Any], Any] + strict: bool + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def enum_schema( + cls: Any, + members: list[Any], + *, + sub_type: Literal['str', 'int', 'float'] | None = None, + missing: Callable[[Any], Any] | None = None, + strict: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> EnumSchema: + """ + Returns a schema that matches an enum value, e.g.: + + ```py + from enum import Enum + from pydantic_core import SchemaValidator, core_schema + + class Color(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + schema = core_schema.enum_schema(Color, list(Color.__members__.values())) + v = SchemaValidator(schema) + assert v.validate_python(2) is Color.GREEN + ``` + + Args: + cls: The enum class + members: The members of the enum, generally `list(MyEnum.__members__.values())` + sub_type: The type of the enum, either 'str' or 'int' or None for plain enums + missing: A function to use when the value is not found in the enum, from `_missing_` + strict: Whether to use strict mode, defaults to False + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='enum', + cls=cls, + members=members, + sub_type=sub_type, + missing=missing, + strict=strict, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class MissingSentinelSchema(TypedDict, total=False): + type: Required[Literal['missing-sentinel']] + metadata: dict[str, Any] + serialization: SerSchema + + +def missing_sentinel_schema( + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> MissingSentinelSchema: + """Returns a schema for the `MISSING` sentinel.""" + + return _dict_not_none( + type='missing-sentinel', + metadata=metadata, + serialization=serialization, + ) + + +# must match input/parse_json.rs::JsonType::try_from +JsonType = Literal['null', 'bool', 'int', 'float', 'str', 'list', 'dict'] + + +class IsInstanceSchema(TypedDict, total=False): + type: Required[Literal['is-instance']] + cls: Required[Any] + cls_repr: str + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def is_instance_schema( + cls: Any, + *, + cls_repr: str | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> IsInstanceSchema: + """ + Returns a schema that checks if a value is an instance of a class, equivalent to python's `isinstance` method, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + class A: + pass + + schema = core_schema.is_instance_schema(cls=A) + v = SchemaValidator(schema) + v.validate_python(A()) + ``` + + Args: + cls: The value must be an instance of this class + cls_repr: If provided this string is used in the validator name instead of `repr(cls)` + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='is-instance', cls=cls, cls_repr=cls_repr, ref=ref, metadata=metadata, serialization=serialization + ) + + +class IsSubclassSchema(TypedDict, total=False): + type: Required[Literal['is-subclass']] + cls: Required[type[Any]] + cls_repr: str + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def is_subclass_schema( + cls: type[Any], + *, + cls_repr: str | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> IsInstanceSchema: + """ + Returns a schema that checks if a value is a subtype of a class, equivalent to python's `issubclass` method, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + class A: + pass + + class B(A): + pass + + schema = core_schema.is_subclass_schema(cls=A) + v = SchemaValidator(schema) + v.validate_python(B) + ``` + + Args: + cls: The value must be a subclass of this class + cls_repr: If provided this string is used in the validator name instead of `repr(cls)` + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='is-subclass', cls=cls, cls_repr=cls_repr, ref=ref, metadata=metadata, serialization=serialization + ) + + +class CallableSchema(TypedDict, total=False): + type: Required[Literal['callable']] + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def callable_schema( + *, ref: str | None = None, metadata: dict[str, Any] | None = None, serialization: SerSchema | None = None +) -> CallableSchema: + """ + Returns a schema that checks if a value is callable, equivalent to python's `callable` method, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.callable_schema() + v = SchemaValidator(schema) + v.validate_python(min) + ``` + + Args: + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none(type='callable', ref=ref, metadata=metadata, serialization=serialization) + + +class UuidSchema(TypedDict, total=False): + type: Required[Literal['uuid']] + version: Literal[1, 3, 4, 5, 7] + strict: bool + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def uuid_schema( + *, + version: Literal[1, 3, 4, 5, 6, 7, 8] | None = None, + strict: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> UuidSchema: + return _dict_not_none( + type='uuid', version=version, strict=strict, ref=ref, metadata=metadata, serialization=serialization + ) + + +class IncExSeqSerSchema(TypedDict, total=False): + type: Required[Literal['include-exclude-sequence']] + include: set[int] + exclude: set[int] + + +def filter_seq_schema(*, include: set[int] | None = None, exclude: set[int] | None = None) -> IncExSeqSerSchema: + return _dict_not_none(type='include-exclude-sequence', include=include, exclude=exclude) + + +IncExSeqOrElseSerSchema = Union[IncExSeqSerSchema, SerSchema] + + +class ListSchema(TypedDict, total=False): + type: Required[Literal['list']] + items_schema: CoreSchema + min_length: int + max_length: int + fail_fast: bool + strict: bool + ref: str + metadata: dict[str, Any] + serialization: IncExSeqOrElseSerSchema + + +def list_schema( + items_schema: CoreSchema | None = None, + *, + min_length: int | None = None, + max_length: int | None = None, + fail_fast: bool | None = None, + strict: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: IncExSeqOrElseSerSchema | None = None, +) -> ListSchema: + """ + Returns a schema that matches a list value, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.list_schema(core_schema.int_schema(), min_length=0, max_length=10) + v = SchemaValidator(schema) + assert v.validate_python(['4']) == [4] + ``` + + Args: + items_schema: The value must be a list of items that match this schema + min_length: The value must be a list with at least this many items + max_length: The value must be a list with at most this many items + fail_fast: Stop validation on the first error + strict: The value must be a list with exactly this many items + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='list', + items_schema=items_schema, + min_length=min_length, + max_length=max_length, + fail_fast=fail_fast, + strict=strict, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +# @deprecated('tuple_positional_schema is deprecated. Use pydantic_core.core_schema.tuple_schema instead.') +def tuple_positional_schema( + items_schema: list[CoreSchema], + *, + extras_schema: CoreSchema | None = None, + strict: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: IncExSeqOrElseSerSchema | None = None, +) -> TupleSchema: + """ + Returns a schema that matches a tuple of schemas, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.tuple_positional_schema( + [core_schema.int_schema(), core_schema.str_schema()] + ) + v = SchemaValidator(schema) + assert v.validate_python((1, 'hello')) == (1, 'hello') + ``` + + Args: + items_schema: The value must be a tuple with items that match these schemas + extras_schema: The value must be a tuple with items that match this schema + This was inspired by JSON schema's `prefixItems` and `items` fields. + In python's `typing.Tuple`, you can't specify a type for "extra" items -- they must all be the same type + if the length is variable. So this field won't be set from a `typing.Tuple` annotation on a pydantic model. + strict: The value must be a tuple with exactly this many items + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + if extras_schema is not None: + variadic_item_index = len(items_schema) + items_schema = items_schema + [extras_schema] + else: + variadic_item_index = None + return tuple_schema( + items_schema=items_schema, + variadic_item_index=variadic_item_index, + strict=strict, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +# @deprecated('tuple_variable_schema is deprecated. Use pydantic_core.core_schema.tuple_schema instead.') +def tuple_variable_schema( + items_schema: CoreSchema | None = None, + *, + min_length: int | None = None, + max_length: int | None = None, + strict: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: IncExSeqOrElseSerSchema | None = None, +) -> TupleSchema: + """ + Returns a schema that matches a tuple of a given schema, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.tuple_variable_schema( + items_schema=core_schema.int_schema(), min_length=0, max_length=10 + ) + v = SchemaValidator(schema) + assert v.validate_python(('1', 2, 3)) == (1, 2, 3) + ``` + + Args: + items_schema: The value must be a tuple with items that match this schema + min_length: The value must be a tuple with at least this many items + max_length: The value must be a tuple with at most this many items + strict: The value must be a tuple with exactly this many items + ref: Optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return tuple_schema( + items_schema=[items_schema or any_schema()], + variadic_item_index=0, + min_length=min_length, + max_length=max_length, + strict=strict, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class TupleSchema(TypedDict, total=False): + type: Required[Literal['tuple']] + items_schema: Required[list[CoreSchema]] + variadic_item_index: int + min_length: int + max_length: int + fail_fast: bool + strict: bool + ref: str + metadata: dict[str, Any] + serialization: IncExSeqOrElseSerSchema + + +def tuple_schema( + items_schema: list[CoreSchema], + *, + variadic_item_index: int | None = None, + min_length: int | None = None, + max_length: int | None = None, + fail_fast: bool | None = None, + strict: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: IncExSeqOrElseSerSchema | None = None, +) -> TupleSchema: + """ + Returns a schema that matches a tuple of schemas, with an optional variadic item, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.tuple_schema( + [core_schema.int_schema(), core_schema.str_schema(), core_schema.float_schema()], + variadic_item_index=1, + ) + v = SchemaValidator(schema) + assert v.validate_python((1, 'hello', 'world', 1.5)) == (1, 'hello', 'world', 1.5) + ``` + + Args: + items_schema: The value must be a tuple with items that match these schemas + variadic_item_index: The index of the schema in `items_schema` to be treated as variadic (following PEP 646) + min_length: The value must be a tuple with at least this many items + max_length: The value must be a tuple with at most this many items + fail_fast: Stop validation on the first error + strict: The value must be a tuple with exactly this many items + ref: Optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='tuple', + items_schema=items_schema, + variadic_item_index=variadic_item_index, + min_length=min_length, + max_length=max_length, + fail_fast=fail_fast, + strict=strict, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class SetSchema(TypedDict, total=False): + type: Required[Literal['set']] + items_schema: CoreSchema + min_length: int + max_length: int + fail_fast: bool + strict: bool + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def set_schema( + items_schema: CoreSchema | None = None, + *, + min_length: int | None = None, + max_length: int | None = None, + fail_fast: bool | None = None, + strict: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> SetSchema: + """ + Returns a schema that matches a set of a given schema, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.set_schema( + items_schema=core_schema.int_schema(), min_length=0, max_length=10 + ) + v = SchemaValidator(schema) + assert v.validate_python({1, '2', 3}) == {1, 2, 3} + ``` + + Args: + items_schema: The value must be a set with items that match this schema + min_length: The value must be a set with at least this many items + max_length: The value must be a set with at most this many items + fail_fast: Stop validation on the first error + strict: The value must be a set with exactly this many items + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='set', + items_schema=items_schema, + min_length=min_length, + max_length=max_length, + fail_fast=fail_fast, + strict=strict, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class FrozenSetSchema(TypedDict, total=False): + type: Required[Literal['frozenset']] + items_schema: CoreSchema + min_length: int + max_length: int + fail_fast: bool + strict: bool + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def frozenset_schema( + items_schema: CoreSchema | None = None, + *, + min_length: int | None = None, + max_length: int | None = None, + fail_fast: bool | None = None, + strict: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> FrozenSetSchema: + """ + Returns a schema that matches a frozenset of a given schema, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.frozenset_schema( + items_schema=core_schema.int_schema(), min_length=0, max_length=10 + ) + v = SchemaValidator(schema) + assert v.validate_python(frozenset(range(3))) == frozenset({0, 1, 2}) + ``` + + Args: + items_schema: The value must be a frozenset with items that match this schema + min_length: The value must be a frozenset with at least this many items + max_length: The value must be a frozenset with at most this many items + fail_fast: Stop validation on the first error + strict: The value must be a frozenset with exactly this many items + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='frozenset', + items_schema=items_schema, + min_length=min_length, + max_length=max_length, + fail_fast=fail_fast, + strict=strict, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class GeneratorSchema(TypedDict, total=False): + type: Required[Literal['generator']] + items_schema: CoreSchema + min_length: int + max_length: int + ref: str + metadata: dict[str, Any] + serialization: IncExSeqOrElseSerSchema + + +def generator_schema( + items_schema: CoreSchema | None = None, + *, + min_length: int | None = None, + max_length: int | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: IncExSeqOrElseSerSchema | None = None, +) -> GeneratorSchema: + """ + Returns a schema that matches a generator value, e.g.: + + ```py + from typing import Iterator + from pydantic_core import SchemaValidator, core_schema + + def gen() -> Iterator[int]: + yield 1 + + schema = core_schema.generator_schema(items_schema=core_schema.int_schema()) + v = SchemaValidator(schema) + v.validate_python(gen()) + ``` + + Unlike other types, validated generators do not raise ValidationErrors eagerly, + but instead will raise a ValidationError when a violating value is actually read from the generator. + This is to ensure that "validated" generators retain the benefit of lazy evaluation. + + Args: + items_schema: The value must be a generator with items that match this schema + min_length: The value must be a generator that yields at least this many items + max_length: The value must be a generator that yields at most this many items + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='generator', + items_schema=items_schema, + min_length=min_length, + max_length=max_length, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +IncExDict = set[Union[int, str]] + + +class IncExDictSerSchema(TypedDict, total=False): + type: Required[Literal['include-exclude-dict']] + include: IncExDict + exclude: IncExDict + + +def filter_dict_schema(*, include: IncExDict | None = None, exclude: IncExDict | None = None) -> IncExDictSerSchema: + return _dict_not_none(type='include-exclude-dict', include=include, exclude=exclude) + + +IncExDictOrElseSerSchema = Union[IncExDictSerSchema, SerSchema] + + +class DictSchema(TypedDict, total=False): + type: Required[Literal['dict']] + keys_schema: CoreSchema # default: AnySchema + values_schema: CoreSchema # default: AnySchema + min_length: int + max_length: int + fail_fast: bool + strict: bool + ref: str + metadata: dict[str, Any] + serialization: IncExDictOrElseSerSchema + + +def dict_schema( + keys_schema: CoreSchema | None = None, + values_schema: CoreSchema | None = None, + *, + min_length: int | None = None, + max_length: int | None = None, + fail_fast: bool | None = None, + strict: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> DictSchema: + """ + Returns a schema that matches a dict value, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.dict_schema( + keys_schema=core_schema.str_schema(), values_schema=core_schema.int_schema() + ) + v = SchemaValidator(schema) + assert v.validate_python({'a': '1', 'b': 2}) == {'a': 1, 'b': 2} + ``` + + Args: + keys_schema: The value must be a dict with keys that match this schema + values_schema: The value must be a dict with values that match this schema + min_length: The value must be a dict with at least this many items + max_length: The value must be a dict with at most this many items + fail_fast: Stop validation on the first error + strict: Whether the keys and values should be validated with strict mode + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='dict', + keys_schema=keys_schema, + values_schema=values_schema, + min_length=min_length, + max_length=max_length, + fail_fast=fail_fast, + strict=strict, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +# (input_value: Any, /) -> Any +NoInfoValidatorFunction = Callable[[Any], Any] + + +class NoInfoValidatorFunctionSchema(TypedDict): + type: Literal['no-info'] + function: NoInfoValidatorFunction + + +# (input_value: Any, info: ValidationInfo, /) -> Any +WithInfoValidatorFunction = Callable[[Any, ValidationInfo[Any]], Any] + + +class WithInfoValidatorFunctionSchema(TypedDict, total=False): + type: Required[Literal['with-info']] + function: Required[WithInfoValidatorFunction] + field_name: str # deprecated + + +ValidationFunction = Union[NoInfoValidatorFunctionSchema, WithInfoValidatorFunctionSchema] + + +class _ValidatorFunctionSchema(TypedDict, total=False): + function: Required[ValidationFunction] + schema: Required[CoreSchema] + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +class BeforeValidatorFunctionSchema(_ValidatorFunctionSchema, total=False): + type: Required[Literal['function-before']] + json_schema_input_schema: CoreSchema + + +def no_info_before_validator_function( + function: NoInfoValidatorFunction, + schema: CoreSchema, + *, + ref: str | None = None, + json_schema_input_schema: CoreSchema | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> BeforeValidatorFunctionSchema: + """ + Returns a schema that calls a validator function before validating, no `info` argument is provided, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + def fn(v: bytes) -> str: + return v.decode() + 'world' + + func_schema = core_schema.no_info_before_validator_function( + function=fn, schema=core_schema.str_schema() + ) + schema = core_schema.typed_dict_schema({'a': core_schema.typed_dict_field(func_schema)}) + + v = SchemaValidator(schema) + assert v.validate_python({'a': b'hello '}) == {'a': 'hello world'} + ``` + + Args: + function: The validator function to call + schema: The schema to validate the output of the validator function + ref: optional unique identifier of the schema, used to reference the schema in other places + json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='function-before', + function={'type': 'no-info', 'function': function}, + schema=schema, + ref=ref, + json_schema_input_schema=json_schema_input_schema, + metadata=metadata, + serialization=serialization, + ) + + +def with_info_before_validator_function( + function: WithInfoValidatorFunction, + schema: CoreSchema, + *, + field_name: str | None = None, + ref: str | None = None, + json_schema_input_schema: CoreSchema | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> BeforeValidatorFunctionSchema: + """ + Returns a schema that calls a validator function before validation, the function is called with + an `info` argument, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + def fn(v: bytes, info: core_schema.ValidationInfo) -> str: + assert info.data is not None + assert info.field_name is not None + return v.decode() + 'world' + + func_schema = core_schema.with_info_before_validator_function( + function=fn, schema=core_schema.str_schema() + ) + schema = core_schema.typed_dict_schema({'a': core_schema.typed_dict_field(func_schema)}) + + v = SchemaValidator(schema) + assert v.validate_python({'a': b'hello '}) == {'a': 'hello world'} + ``` + + Args: + function: The validator function to call + field_name: The name of the field this validator is applied to, if any (deprecated) + schema: The schema to validate the output of the validator function + ref: optional unique identifier of the schema, used to reference the schema in other places + json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + if field_name is not None: + warnings.warn( + 'The `field_name` argument on `with_info_before_validator_function` is deprecated, it will be passed to the function through `ValidationState` instead.', + DeprecationWarning, + stacklevel=2, + ) + + return _dict_not_none( + type='function-before', + function=_dict_not_none(type='with-info', function=function, field_name=field_name), + schema=schema, + ref=ref, + json_schema_input_schema=json_schema_input_schema, + metadata=metadata, + serialization=serialization, + ) + + +class AfterValidatorFunctionSchema(_ValidatorFunctionSchema, total=False): + type: Required[Literal['function-after']] + + +def no_info_after_validator_function( + function: NoInfoValidatorFunction, + schema: CoreSchema, + *, + ref: str | None = None, + json_schema_input_schema: CoreSchema | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> AfterValidatorFunctionSchema: + """ + Returns a schema that calls a validator function after validating, no `info` argument is provided, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + def fn(v: str) -> str: + return v + 'world' + + func_schema = core_schema.no_info_after_validator_function(fn, core_schema.str_schema()) + schema = core_schema.typed_dict_schema({'a': core_schema.typed_dict_field(func_schema)}) + + v = SchemaValidator(schema) + assert v.validate_python({'a': b'hello '}) == {'a': 'hello world'} + ``` + + Args: + function: The validator function to call after the schema is validated + schema: The schema to validate before the validator function + ref: optional unique identifier of the schema, used to reference the schema in other places + json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='function-after', + function={'type': 'no-info', 'function': function}, + schema=schema, + ref=ref, + json_schema_input_schema=json_schema_input_schema, + metadata=metadata, + serialization=serialization, + ) + + +def with_info_after_validator_function( + function: WithInfoValidatorFunction, + schema: CoreSchema, + *, + field_name: str | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> AfterValidatorFunctionSchema: + """ + Returns a schema that calls a validator function after validation, the function is called with + an `info` argument, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + def fn(v: str, info: core_schema.ValidationInfo) -> str: + assert info.data is not None + assert info.field_name is not None + return v + 'world' + + func_schema = core_schema.with_info_after_validator_function( + function=fn, schema=core_schema.str_schema() + ) + schema = core_schema.typed_dict_schema({'a': core_schema.typed_dict_field(func_schema)}) + + v = SchemaValidator(schema) + assert v.validate_python({'a': b'hello '}) == {'a': 'hello world'} + ``` + + Args: + function: The validator function to call after the schema is validated + schema: The schema to validate before the validator function + field_name: The name of the field this validator is applied to, if any (deprecated) + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + if field_name is not None: + warnings.warn( + 'The `field_name` argument on `with_info_after_validator_function` is deprecated, it will be passed to the function through `ValidationState` instead.', + DeprecationWarning, + stacklevel=2, + ) + + return _dict_not_none( + type='function-after', + function=_dict_not_none(type='with-info', function=function, field_name=field_name), + schema=schema, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class ValidatorFunctionWrapHandler(Protocol): + def __call__(self, input_value: Any, outer_location: str | int | None = None, /) -> Any: # pragma: no cover + ... + + +# (input_value: Any, validator: ValidatorFunctionWrapHandler, /) -> Any +NoInfoWrapValidatorFunction = Callable[[Any, ValidatorFunctionWrapHandler], Any] + + +class NoInfoWrapValidatorFunctionSchema(TypedDict): + type: Literal['no-info'] + function: NoInfoWrapValidatorFunction + + +# (input_value: Any, validator: ValidatorFunctionWrapHandler, info: ValidationInfo, /) -> Any +WithInfoWrapValidatorFunction = Callable[[Any, ValidatorFunctionWrapHandler, ValidationInfo[Any]], Any] + + +class WithInfoWrapValidatorFunctionSchema(TypedDict, total=False): + type: Required[Literal['with-info']] + function: Required[WithInfoWrapValidatorFunction] + field_name: str # deprecated + + +WrapValidatorFunction = Union[NoInfoWrapValidatorFunctionSchema, WithInfoWrapValidatorFunctionSchema] + + +class WrapValidatorFunctionSchema(TypedDict, total=False): + type: Required[Literal['function-wrap']] + function: Required[WrapValidatorFunction] + schema: Required[CoreSchema] + ref: str + json_schema_input_schema: CoreSchema + metadata: dict[str, Any] + serialization: SerSchema + + +def no_info_wrap_validator_function( + function: NoInfoWrapValidatorFunction, + schema: CoreSchema, + *, + ref: str | None = None, + json_schema_input_schema: CoreSchema | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> WrapValidatorFunctionSchema: + """ + Returns a schema which calls a function with a `validator` callable argument which can + optionally be used to call inner validation with the function logic, this is much like the + "onion" implementation of middleware in many popular web frameworks, no `info` argument is passed, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + def fn( + v: str, + validator: core_schema.ValidatorFunctionWrapHandler, + ) -> str: + return validator(input_value=v) + 'world' + + schema = core_schema.no_info_wrap_validator_function( + function=fn, schema=core_schema.str_schema() + ) + v = SchemaValidator(schema) + assert v.validate_python('hello ') == 'hello world' + ``` + + Args: + function: The validator function to call + schema: The schema to validate the output of the validator function + ref: optional unique identifier of the schema, used to reference the schema in other places + json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='function-wrap', + function={'type': 'no-info', 'function': function}, + schema=schema, + json_schema_input_schema=json_schema_input_schema, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +def with_info_wrap_validator_function( + function: WithInfoWrapValidatorFunction, + schema: CoreSchema, + *, + field_name: str | None = None, + json_schema_input_schema: CoreSchema | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> WrapValidatorFunctionSchema: + """ + Returns a schema which calls a function with a `validator` callable argument which can + optionally be used to call inner validation with the function logic, this is much like the + "onion" implementation of middleware in many popular web frameworks, an `info` argument is also passed, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + def fn( + v: str, + validator: core_schema.ValidatorFunctionWrapHandler, + info: core_schema.ValidationInfo, + ) -> str: + return validator(input_value=v) + 'world' + + schema = core_schema.with_info_wrap_validator_function( + function=fn, schema=core_schema.str_schema() + ) + v = SchemaValidator(schema) + assert v.validate_python('hello ') == 'hello world' + ``` + + Args: + function: The validator function to call + schema: The schema to validate the output of the validator function + field_name: The name of the field this validator is applied to, if any (deprecated) + json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + if field_name is not None: + warnings.warn( + 'The `field_name` argument on `with_info_wrap_validator_function` is deprecated, it will be passed to the function through `ValidationState` instead.', + DeprecationWarning, + stacklevel=2, + ) + + return _dict_not_none( + type='function-wrap', + function=_dict_not_none(type='with-info', function=function, field_name=field_name), + schema=schema, + json_schema_input_schema=json_schema_input_schema, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class PlainValidatorFunctionSchema(TypedDict, total=False): + type: Required[Literal['function-plain']] + function: Required[ValidationFunction] + ref: str + json_schema_input_schema: CoreSchema + metadata: dict[str, Any] + serialization: SerSchema + + +def no_info_plain_validator_function( + function: NoInfoValidatorFunction, + *, + ref: str | None = None, + json_schema_input_schema: CoreSchema | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> PlainValidatorFunctionSchema: + """ + Returns a schema that uses the provided function for validation, no `info` argument is passed, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + def fn(v: str) -> str: + assert 'hello' in v + return v + 'world' + + schema = core_schema.no_info_plain_validator_function(function=fn) + v = SchemaValidator(schema) + assert v.validate_python('hello ') == 'hello world' + ``` + + Args: + function: The validator function to call + ref: optional unique identifier of the schema, used to reference the schema in other places + json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='function-plain', + function={'type': 'no-info', 'function': function}, + ref=ref, + json_schema_input_schema=json_schema_input_schema, + metadata=metadata, + serialization=serialization, + ) + + +def with_info_plain_validator_function( + function: WithInfoValidatorFunction, + *, + field_name: str | None = None, + ref: str | None = None, + json_schema_input_schema: CoreSchema | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> PlainValidatorFunctionSchema: + """ + Returns a schema that uses the provided function for validation, an `info` argument is passed, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + def fn(v: str, info: core_schema.ValidationInfo) -> str: + assert 'hello' in v + return v + 'world' + + schema = core_schema.with_info_plain_validator_function(function=fn) + v = SchemaValidator(schema) + assert v.validate_python('hello ') == 'hello world' + ``` + + Args: + function: The validator function to call + field_name: The name of the field this validator is applied to, if any (deprecated) + ref: optional unique identifier of the schema, used to reference the schema in other places + json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + if field_name is not None: + warnings.warn( + 'The `field_name` argument on `with_info_plain_validator_function` is deprecated, it will be passed to the function through `ValidationState` instead.', + DeprecationWarning, + stacklevel=2, + ) + + return _dict_not_none( + type='function-plain', + function=_dict_not_none(type='with-info', function=function, field_name=field_name), + ref=ref, + json_schema_input_schema=json_schema_input_schema, + metadata=metadata, + serialization=serialization, + ) + + +class WithDefaultSchema(TypedDict, total=False): + type: Required[Literal['default']] + schema: Required[CoreSchema] + default: Any + default_factory: Union[Callable[[], Any], Callable[[dict[str, Any]], Any]] + default_factory_takes_data: bool + on_error: Literal['raise', 'omit', 'default'] # default: 'raise' + validate_default: bool # default: False + strict: bool + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def with_default_schema( + schema: CoreSchema, + *, + default: Any = PydanticUndefined, + default_factory: Union[Callable[[], Any], Callable[[dict[str, Any]], Any], None] = None, + default_factory_takes_data: bool | None = None, + on_error: Literal['raise', 'omit', 'default'] | None = None, + validate_default: bool | None = None, + strict: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> WithDefaultSchema: + """ + Returns a schema that adds a default value to the given schema, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.with_default_schema(core_schema.str_schema(), default='hello') + wrapper_schema = core_schema.typed_dict_schema( + {'a': core_schema.typed_dict_field(schema)} + ) + v = SchemaValidator(wrapper_schema) + assert v.validate_python({}) == v.validate_python({'a': 'hello'}) + ``` + + Args: + schema: The schema to add a default value to + default: The default value to use + default_factory: A callable that returns the default value to use + default_factory_takes_data: Whether the default factory takes a validated data argument + on_error: What to do if the schema validation fails. One of 'raise', 'omit', 'default' + validate_default: Whether the default value should be validated + strict: Whether the underlying schema should be validated with strict mode + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + s = _dict_not_none( + type='default', + schema=schema, + default_factory=default_factory, + default_factory_takes_data=default_factory_takes_data, + on_error=on_error, + validate_default=validate_default, + strict=strict, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + if default is not PydanticUndefined: + s['default'] = default + return s + + +class NullableSchema(TypedDict, total=False): + type: Required[Literal['nullable']] + schema: Required[CoreSchema] + strict: bool + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def nullable_schema( + schema: CoreSchema, + *, + strict: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> NullableSchema: + """ + Returns a schema that matches a nullable value, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.nullable_schema(core_schema.str_schema()) + v = SchemaValidator(schema) + assert v.validate_python(None) is None + ``` + + Args: + schema: The schema to wrap + strict: Whether the underlying schema should be validated with strict mode + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='nullable', schema=schema, strict=strict, ref=ref, metadata=metadata, serialization=serialization + ) + + +class UnionSchema(TypedDict, total=False): + type: Required[Literal['union']] + choices: Required[list[Union[CoreSchema, tuple[CoreSchema, str]]]] + # default true, whether to automatically collapse unions with one element to the inner validator + auto_collapse: bool + custom_error_type: str + custom_error_message: str + custom_error_context: dict[str, Union[str, int, float]] + mode: Literal['smart', 'left_to_right'] # default: 'smart' + strict: bool + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def union_schema( + choices: list[CoreSchema | tuple[CoreSchema, str]], + *, + auto_collapse: bool | None = None, + custom_error_type: str | None = None, + custom_error_message: str | None = None, + custom_error_context: dict[str, str | int] | None = None, + mode: Literal['smart', 'left_to_right'] | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> UnionSchema: + """ + Returns a schema that matches a union value, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.union_schema([core_schema.str_schema(), core_schema.int_schema()]) + v = SchemaValidator(schema) + assert v.validate_python('hello') == 'hello' + assert v.validate_python(1) == 1 + ``` + + Args: + choices: The schemas to match. If a tuple, the second item is used as the label for the case. + auto_collapse: whether to automatically collapse unions with one element to the inner validator, default true + custom_error_type: The custom error type to use if the validation fails + custom_error_message: The custom error message to use if the validation fails + custom_error_context: The custom error context to use if the validation fails + mode: How to select which choice to return + * `smart` (default) will try to return the choice which is the closest match to the input value + * `left_to_right` will return the first choice in `choices` which succeeds validation + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='union', + choices=choices, + auto_collapse=auto_collapse, + custom_error_type=custom_error_type, + custom_error_message=custom_error_message, + custom_error_context=custom_error_context, + mode=mode, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class TaggedUnionSchema(TypedDict, total=False): + type: Required[Literal['tagged-union']] + choices: Required[dict[Hashable, CoreSchema]] + discriminator: Required[Union[str, list[Union[str, int]], list[list[Union[str, int]]], Callable[[Any], Hashable]]] + custom_error_type: str + custom_error_message: str + custom_error_context: dict[str, Union[str, int, float]] + strict: bool + from_attributes: bool # default: True + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def tagged_union_schema( + choices: dict[Any, CoreSchema], + discriminator: str | list[str | int] | list[list[str | int]] | Callable[[Any], Any], + *, + custom_error_type: str | None = None, + custom_error_message: str | None = None, + custom_error_context: dict[str, int | str | float] | None = None, + strict: bool | None = None, + from_attributes: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> TaggedUnionSchema: + """ + Returns a schema that matches a tagged union value, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + apple_schema = core_schema.typed_dict_schema( + { + 'foo': core_schema.typed_dict_field(core_schema.str_schema()), + 'bar': core_schema.typed_dict_field(core_schema.int_schema()), + } + ) + banana_schema = core_schema.typed_dict_schema( + { + 'foo': core_schema.typed_dict_field(core_schema.str_schema()), + 'spam': core_schema.typed_dict_field( + core_schema.list_schema(items_schema=core_schema.int_schema()) + ), + } + ) + schema = core_schema.tagged_union_schema( + choices={ + 'apple': apple_schema, + 'banana': banana_schema, + }, + discriminator='foo', + ) + v = SchemaValidator(schema) + assert v.validate_python({'foo': 'apple', 'bar': '123'}) == {'foo': 'apple', 'bar': 123} + assert v.validate_python({'foo': 'banana', 'spam': [1, 2, 3]}) == { + 'foo': 'banana', + 'spam': [1, 2, 3], + } + ``` + + Args: + choices: The schemas to match + When retrieving a schema from `choices` using the discriminator value, if the value is a str, + it should be fed back into the `choices` map until a schema is obtained + (This approach is to prevent multiple ownership of a single schema in Rust) + discriminator: The discriminator to use to determine the schema to use + * If `discriminator` is a str, it is the name of the attribute to use as the discriminator + * If `discriminator` is a list of int/str, it should be used as a "path" to access the discriminator + * If `discriminator` is a list of lists, each inner list is a path, and the first path that exists is used + * If `discriminator` is a callable, it should return the discriminator when called on the value to validate; + the callable can return `None` to indicate that there is no matching discriminator present on the input + custom_error_type: The custom error type to use if the validation fails + custom_error_message: The custom error message to use if the validation fails + custom_error_context: The custom error context to use if the validation fails + strict: Whether the underlying schemas should be validated with strict mode + from_attributes: Whether to use the attributes of the object to retrieve the discriminator value + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='tagged-union', + choices=choices, + discriminator=discriminator, + custom_error_type=custom_error_type, + custom_error_message=custom_error_message, + custom_error_context=custom_error_context, + strict=strict, + from_attributes=from_attributes, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class ChainSchema(TypedDict, total=False): + type: Required[Literal['chain']] + steps: Required[list[CoreSchema]] + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def chain_schema( + steps: list[CoreSchema], + *, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> ChainSchema: + """ + Returns a schema that chains the provided validation schemas, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + def fn(v: str, info: core_schema.ValidationInfo) -> str: + assert 'hello' in v + return v + ' world' + + fn_schema = core_schema.with_info_plain_validator_function(function=fn) + schema = core_schema.chain_schema( + [fn_schema, fn_schema, fn_schema, core_schema.str_schema()] + ) + v = SchemaValidator(schema) + assert v.validate_python('hello') == 'hello world world world' + ``` + + Args: + steps: The schemas to chain + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none(type='chain', steps=steps, ref=ref, metadata=metadata, serialization=serialization) + + +class LaxOrStrictSchema(TypedDict, total=False): + type: Required[Literal['lax-or-strict']] + lax_schema: Required[CoreSchema] + strict_schema: Required[CoreSchema] + strict: bool + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def lax_or_strict_schema( + lax_schema: CoreSchema, + strict_schema: CoreSchema, + *, + strict: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> LaxOrStrictSchema: + """ + Returns a schema that uses the lax or strict schema, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + def fn(v: str, info: core_schema.ValidationInfo) -> str: + assert 'hello' in v + return v + ' world' + + lax_schema = core_schema.int_schema(strict=False) + strict_schema = core_schema.int_schema(strict=True) + + schema = core_schema.lax_or_strict_schema( + lax_schema=lax_schema, strict_schema=strict_schema, strict=True + ) + v = SchemaValidator(schema) + assert v.validate_python(123) == 123 + + schema = core_schema.lax_or_strict_schema( + lax_schema=lax_schema, strict_schema=strict_schema, strict=False + ) + v = SchemaValidator(schema) + assert v.validate_python('123') == 123 + ``` + + Args: + lax_schema: The lax schema to use + strict_schema: The strict schema to use + strict: Whether the strict schema should be used + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='lax-or-strict', + lax_schema=lax_schema, + strict_schema=strict_schema, + strict=strict, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class JsonOrPythonSchema(TypedDict, total=False): + type: Required[Literal['json-or-python']] + json_schema: Required[CoreSchema] + python_schema: Required[CoreSchema] + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def json_or_python_schema( + json_schema: CoreSchema, + python_schema: CoreSchema, + *, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> JsonOrPythonSchema: + """ + Returns a schema that uses the Json or Python schema depending on the input: + + ```py + from pydantic_core import SchemaValidator, ValidationError, core_schema + + v = SchemaValidator( + core_schema.json_or_python_schema( + json_schema=core_schema.int_schema(), + python_schema=core_schema.int_schema(strict=True), + ) + ) + + assert v.validate_json('"123"') == 123 + + try: + v.validate_python('123') + except ValidationError: + pass + else: + raise AssertionError('Validation should have failed') + ``` + + Args: + json_schema: The schema to use for Json inputs + python_schema: The schema to use for Python inputs + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='json-or-python', + json_schema=json_schema, + python_schema=python_schema, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class TypedDictField(TypedDict, total=False): + type: Required[Literal['typed-dict-field']] + schema: Required[CoreSchema] + required: bool + validation_alias: Union[str, list[Union[str, int]], list[list[Union[str, int]]]] + serialization_alias: str + serialization_exclude: bool # default: False + metadata: dict[str, Any] + serialization_exclude_if: Callable[[Any], bool] # default None + + +def typed_dict_field( + schema: CoreSchema, + *, + required: bool | None = None, + validation_alias: str | list[str | int] | list[list[str | int]] | None = None, + serialization_alias: str | None = None, + serialization_exclude: bool | None = None, + metadata: dict[str, Any] | None = None, + serialization_exclude_if: Callable[[Any], bool] | None = None, +) -> TypedDictField: + """ + Returns a schema that matches a typed dict field, e.g.: + + ```py + from pydantic_core import core_schema + + field = core_schema.typed_dict_field(schema=core_schema.int_schema(), required=True) + ``` + + Args: + schema: The schema to use for the field + required: Whether the field is required, otherwise uses the value from `total` on the typed dict + validation_alias: The alias(es) to use to find the field in the validation data + serialization_alias: The alias to use as a key when serializing + serialization_exclude: Whether to exclude the field when serializing + serialization_exclude_if: A callable that determines whether to exclude the field when serializing based on its value. + metadata: Any other information you want to include with the schema, not used by pydantic-core + """ + return _dict_not_none( + type='typed-dict-field', + schema=schema, + required=required, + validation_alias=validation_alias, + serialization_alias=serialization_alias, + serialization_exclude=serialization_exclude, + serialization_exclude_if=serialization_exclude_if, + metadata=metadata, + ) + + +class TypedDictSchema(TypedDict, total=False): + type: Required[Literal['typed-dict']] + fields: Required[dict[str, TypedDictField]] + cls: type[Any] + cls_name: str + computed_fields: list[ComputedField] + strict: bool + extras_schema: CoreSchema + # all these values can be set via config, equivalent fields have `typed_dict_` prefix + extra_behavior: ExtraBehavior + total: bool # default: True + ref: str + metadata: dict[str, Any] + serialization: SerSchema + config: CoreConfig + + +def typed_dict_schema( + fields: dict[str, TypedDictField], + *, + cls: type[Any] | None = None, + cls_name: str | None = None, + computed_fields: list[ComputedField] | None = None, + strict: bool | None = None, + extras_schema: CoreSchema | None = None, + extra_behavior: ExtraBehavior | None = None, + total: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, + config: CoreConfig | None = None, +) -> TypedDictSchema: + """ + Returns a schema that matches a typed dict, e.g.: + + ```py + from typing_extensions import TypedDict + + from pydantic_core import SchemaValidator, core_schema + + class MyTypedDict(TypedDict): + a: str + + wrapper_schema = core_schema.typed_dict_schema( + {'a': core_schema.typed_dict_field(core_schema.str_schema())}, cls=MyTypedDict + ) + v = SchemaValidator(wrapper_schema) + assert v.validate_python({'a': 'hello'}) == {'a': 'hello'} + ``` + + Args: + fields: The fields to use for the typed dict + cls: The class to use for the typed dict + cls_name: The name to use in error locations. Falls back to `cls.__name__`, or the validator name if no class + is provided. + computed_fields: Computed fields to use when serializing the model, only applies when directly inside a model + strict: Whether the typed dict is strict + extras_schema: The extra validator to use for the typed dict + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + extra_behavior: The extra behavior to use for the typed dict + total: Whether the typed dict is total, otherwise uses `typed_dict_total` from config + serialization: Custom serialization schema + """ + return _dict_not_none( + type='typed-dict', + fields=fields, + cls=cls, + cls_name=cls_name, + computed_fields=computed_fields, + strict=strict, + extras_schema=extras_schema, + extra_behavior=extra_behavior, + total=total, + ref=ref, + metadata=metadata, + serialization=serialization, + config=config, + ) + + +class ModelField(TypedDict, total=False): + type: Required[Literal['model-field']] + schema: Required[CoreSchema] + validation_alias: Union[str, list[Union[str, int]], list[list[Union[str, int]]]] + serialization_alias: str + serialization_exclude: bool # default: False + serialization_exclude_if: Callable[[Any], bool] # default: None + frozen: bool + metadata: dict[str, Any] + + +def model_field( + schema: CoreSchema, + *, + validation_alias: str | list[str | int] | list[list[str | int]] | None = None, + serialization_alias: str | None = None, + serialization_exclude: bool | None = None, + serialization_exclude_if: Callable[[Any], bool] | None = None, + frozen: bool | None = None, + metadata: dict[str, Any] | None = None, +) -> ModelField: + """ + Returns a schema for a model field, e.g.: + + ```py + from pydantic_core import core_schema + + field = core_schema.model_field(schema=core_schema.int_schema()) + ``` + + Args: + schema: The schema to use for the field + validation_alias: The alias(es) to use to find the field in the validation data + serialization_alias: The alias to use as a key when serializing + serialization_exclude: Whether to exclude the field when serializing + serialization_exclude_if: A Callable that determines whether to exclude a field during serialization based on its value. + frozen: Whether the field is frozen + metadata: Any other information you want to include with the schema, not used by pydantic-core + """ + return _dict_not_none( + type='model-field', + schema=schema, + validation_alias=validation_alias, + serialization_alias=serialization_alias, + serialization_exclude=serialization_exclude, + serialization_exclude_if=serialization_exclude_if, + frozen=frozen, + metadata=metadata, + ) + + +class ModelFieldsSchema(TypedDict, total=False): + type: Required[Literal['model-fields']] + fields: Required[dict[str, ModelField]] + model_name: str + computed_fields: list[ComputedField] + strict: bool + extras_schema: CoreSchema + extras_keys_schema: CoreSchema + extra_behavior: ExtraBehavior + from_attributes: bool + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def model_fields_schema( + fields: dict[str, ModelField], + *, + model_name: str | None = None, + computed_fields: list[ComputedField] | None = None, + strict: bool | None = None, + extras_schema: CoreSchema | None = None, + extras_keys_schema: CoreSchema | None = None, + extra_behavior: ExtraBehavior | None = None, + from_attributes: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> ModelFieldsSchema: + """ + Returns a schema that matches the fields of a Pydantic model, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + wrapper_schema = core_schema.model_fields_schema( + {'a': core_schema.model_field(core_schema.str_schema())} + ) + v = SchemaValidator(wrapper_schema) + print(v.validate_python({'a': 'hello'})) + #> ({'a': 'hello'}, None, {'a'}) + ``` + + Args: + fields: The fields of the model + model_name: The name of the model, used for error messages, defaults to "Model" + computed_fields: Computed fields to use when serializing the model, only applies when directly inside a model + strict: Whether the model is strict + extras_schema: The schema to use when validating extra input data + extras_keys_schema: The schema to use when validating the keys of extra input data + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + extra_behavior: The extra behavior to use for the model fields + from_attributes: Whether the model fields should be populated from attributes + serialization: Custom serialization schema + """ + return _dict_not_none( + type='model-fields', + fields=fields, + model_name=model_name, + computed_fields=computed_fields, + strict=strict, + extras_schema=extras_schema, + extras_keys_schema=extras_keys_schema, + extra_behavior=extra_behavior, + from_attributes=from_attributes, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class ModelSchema(TypedDict, total=False): + type: Required[Literal['model']] + cls: Required[type[Any]] + generic_origin: type[Any] + schema: Required[CoreSchema] + custom_init: bool + root_model: bool + post_init: str + revalidate_instances: Literal['always', 'never', 'subclass-instances'] # default: 'never' + strict: bool + frozen: bool + extra_behavior: ExtraBehavior + config: CoreConfig + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def model_schema( + cls: type[Any], + schema: CoreSchema, + *, + generic_origin: type[Any] | None = None, + custom_init: bool | None = None, + root_model: bool | None = None, + post_init: str | None = None, + revalidate_instances: Literal['always', 'never', 'subclass-instances'] | None = None, + strict: bool | None = None, + frozen: bool | None = None, + extra_behavior: ExtraBehavior | None = None, + config: CoreConfig | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> ModelSchema: + """ + A model schema generally contains a typed-dict schema. + It will run the typed dict validator, then create a new class + and set the dict and fields set returned from the typed dict validator + to `__dict__` and `__pydantic_fields_set__` respectively. + + Example: + + ```py + from pydantic_core import CoreConfig, SchemaValidator, core_schema + + class MyModel: + __slots__ = ( + '__dict__', + '__pydantic_fields_set__', + '__pydantic_extra__', + '__pydantic_private__', + ) + + schema = core_schema.model_schema( + cls=MyModel, + config=CoreConfig(str_max_length=5), + schema=core_schema.model_fields_schema( + fields={'a': core_schema.model_field(core_schema.str_schema())}, + ), + ) + v = SchemaValidator(schema) + assert v.isinstance_python({'a': 'hello'}) is True + assert v.isinstance_python({'a': 'too long'}) is False + ``` + + Args: + cls: The class to use for the model + schema: The schema to use for the model + generic_origin: The origin type used for this model, if it's a parametrized generic. Ex, + if this model schema represents `SomeModel[int]`, generic_origin is `SomeModel` + custom_init: Whether the model has a custom init method + root_model: Whether the model is a `RootModel` + post_init: The call after init to use for the model + revalidate_instances: whether instances of models and dataclasses (including subclass instances) + should re-validate defaults to config.revalidate_instances, else 'never' + strict: Whether the model is strict + frozen: Whether the model is frozen + extra_behavior: The extra behavior to use for the model, used in serialization + config: The config to use for the model + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='model', + cls=cls, + generic_origin=generic_origin, + schema=schema, + custom_init=custom_init, + root_model=root_model, + post_init=post_init, + revalidate_instances=revalidate_instances, + strict=strict, + frozen=frozen, + extra_behavior=extra_behavior, + config=config, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class DataclassField(TypedDict, total=False): + type: Required[Literal['dataclass-field']] + name: Required[str] + schema: Required[CoreSchema] + kw_only: bool # default: True + init: bool # default: True + init_only: bool # default: False + frozen: bool # default: False + validation_alias: Union[str, list[Union[str, int]], list[list[Union[str, int]]]] + serialization_alias: str + serialization_exclude: bool # default: False + metadata: dict[str, Any] + serialization_exclude_if: Callable[[Any], bool] # default: None + + +def dataclass_field( + name: str, + schema: CoreSchema, + *, + kw_only: bool | None = None, + init: bool | None = None, + init_only: bool | None = None, + validation_alias: str | list[str | int] | list[list[str | int]] | None = None, + serialization_alias: str | None = None, + serialization_exclude: bool | None = None, + metadata: dict[str, Any] | None = None, + serialization_exclude_if: Callable[[Any], bool] | None = None, + frozen: bool | None = None, +) -> DataclassField: + """ + Returns a schema for a dataclass field, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + field = core_schema.dataclass_field( + name='a', schema=core_schema.str_schema(), kw_only=False + ) + schema = core_schema.dataclass_args_schema('Foobar', [field]) + v = SchemaValidator(schema) + assert v.validate_python({'a': 'hello'}) == ({'a': 'hello'}, None) + ``` + + Args: + name: The name to use for the argument parameter + schema: The schema to use for the argument parameter + kw_only: Whether the field can be set with a positional argument as well as a keyword argument + init: Whether the field should be validated during initialization + init_only: Whether the field should be omitted from `__dict__` and passed to `__post_init__` + validation_alias: The alias(es) to use to find the field in the validation data + serialization_alias: The alias to use as a key when serializing + serialization_exclude: Whether to exclude the field when serializing + serialization_exclude_if: A callable that determines whether to exclude the field when serializing based on its value. + metadata: Any other information you want to include with the schema, not used by pydantic-core + frozen: Whether the field is frozen + """ + return _dict_not_none( + type='dataclass-field', + name=name, + schema=schema, + kw_only=kw_only, + init=init, + init_only=init_only, + validation_alias=validation_alias, + serialization_alias=serialization_alias, + serialization_exclude=serialization_exclude, + serialization_exclude_if=serialization_exclude_if, + metadata=metadata, + frozen=frozen, + ) + + +class DataclassArgsSchema(TypedDict, total=False): + type: Required[Literal['dataclass-args']] + dataclass_name: Required[str] + fields: Required[list[DataclassField]] + computed_fields: list[ComputedField] + collect_init_only: bool # default: False + ref: str + metadata: dict[str, Any] + serialization: SerSchema + extra_behavior: ExtraBehavior + + +def dataclass_args_schema( + dataclass_name: str, + fields: list[DataclassField], + *, + computed_fields: list[ComputedField] | None = None, + collect_init_only: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, + extra_behavior: ExtraBehavior | None = None, +) -> DataclassArgsSchema: + """ + Returns a schema for validating dataclass arguments, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + field_a = core_schema.dataclass_field( + name='a', schema=core_schema.str_schema(), kw_only=False + ) + field_b = core_schema.dataclass_field( + name='b', schema=core_schema.bool_schema(), kw_only=False + ) + schema = core_schema.dataclass_args_schema('Foobar', [field_a, field_b]) + v = SchemaValidator(schema) + assert v.validate_python({'a': 'hello', 'b': True}) == ({'a': 'hello', 'b': True}, None) + ``` + + Args: + dataclass_name: The name of the dataclass being validated + fields: The fields to use for the dataclass + computed_fields: Computed fields to use when serializing the dataclass + collect_init_only: Whether to collect init only fields into a dict to pass to `__post_init__` + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + extra_behavior: How to handle extra fields + """ + return _dict_not_none( + type='dataclass-args', + dataclass_name=dataclass_name, + fields=fields, + computed_fields=computed_fields, + collect_init_only=collect_init_only, + ref=ref, + metadata=metadata, + serialization=serialization, + extra_behavior=extra_behavior, + ) + + +class DataclassSchema(TypedDict, total=False): + type: Required[Literal['dataclass']] + cls: Required[type[Any]] + generic_origin: type[Any] + schema: Required[CoreSchema] + fields: Required[list[str]] + cls_name: str + post_init: bool # default: False + revalidate_instances: Literal['always', 'never', 'subclass-instances'] # default: 'never' + strict: bool # default: False + frozen: bool # default False + ref: str + metadata: dict[str, Any] + serialization: SerSchema + slots: bool + config: CoreConfig + + +def dataclass_schema( + cls: type[Any], + schema: CoreSchema, + fields: list[str], + *, + generic_origin: type[Any] | None = None, + cls_name: str | None = None, + post_init: bool | None = None, + revalidate_instances: Literal['always', 'never', 'subclass-instances'] | None = None, + strict: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, + frozen: bool | None = None, + slots: bool | None = None, + config: CoreConfig | None = None, +) -> DataclassSchema: + """ + Returns a schema for a dataclass. As with `ModelSchema`, this schema can only be used as a field within + another schema, not as the root type. + + Args: + cls: The dataclass type, used to perform subclass checks + schema: The schema to use for the dataclass fields + fields: Fields of the dataclass, this is used in serialization and in validation during re-validation + and while validating assignment + generic_origin: The origin type used for this dataclass, if it's a parametrized generic. Ex, + if this model schema represents `SomeDataclass[int]`, generic_origin is `SomeDataclass` + cls_name: The name to use in error locs, etc; this is useful for generics (default: `cls.__name__`) + post_init: Whether to call `__post_init__` after validation + revalidate_instances: whether instances of models and dataclasses (including subclass instances) + should re-validate defaults to config.revalidate_instances, else 'never' + strict: Whether to require an exact instance of `cls` + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + frozen: Whether the dataclass is frozen + slots: Whether `slots=True` on the dataclass, means each field is assigned independently, rather than + simply setting `__dict__`, default false + """ + return _dict_not_none( + type='dataclass', + cls=cls, + generic_origin=generic_origin, + fields=fields, + cls_name=cls_name, + schema=schema, + post_init=post_init, + revalidate_instances=revalidate_instances, + strict=strict, + ref=ref, + metadata=metadata, + serialization=serialization, + frozen=frozen, + slots=slots, + config=config, + ) + + +class ArgumentsParameter(TypedDict, total=False): + name: Required[str] + schema: Required[CoreSchema] + mode: Literal['positional_only', 'positional_or_keyword', 'keyword_only'] # default positional_or_keyword + alias: Union[str, list[Union[str, int]], list[list[Union[str, int]]]] + + +def arguments_parameter( + name: str, + schema: CoreSchema, + *, + mode: Literal['positional_only', 'positional_or_keyword', 'keyword_only'] | None = None, + alias: str | list[str | int] | list[list[str | int]] | None = None, +) -> ArgumentsParameter: + """ + Returns a schema that matches an argument parameter, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + param = core_schema.arguments_parameter( + name='a', schema=core_schema.str_schema(), mode='positional_only' + ) + schema = core_schema.arguments_schema([param]) + v = SchemaValidator(schema) + assert v.validate_python(('hello',)) == (('hello',), {}) + ``` + + Args: + name: The name to use for the argument parameter + schema: The schema to use for the argument parameter + mode: The mode to use for the argument parameter + alias: The alias to use for the argument parameter + """ + return _dict_not_none(name=name, schema=schema, mode=mode, alias=alias) + + +VarKwargsMode: TypeAlias = Literal['uniform', 'unpacked-typed-dict'] + + +class ArgumentsSchema(TypedDict, total=False): + type: Required[Literal['arguments']] + arguments_schema: Required[list[ArgumentsParameter]] + validate_by_name: bool + validate_by_alias: bool + var_args_schema: CoreSchema + var_kwargs_mode: VarKwargsMode + var_kwargs_schema: CoreSchema + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def arguments_schema( + arguments: list[ArgumentsParameter], + *, + validate_by_name: bool | None = None, + validate_by_alias: bool | None = None, + var_args_schema: CoreSchema | None = None, + var_kwargs_mode: VarKwargsMode | None = None, + var_kwargs_schema: CoreSchema | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> ArgumentsSchema: + """ + Returns a schema that matches an arguments schema, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + param_a = core_schema.arguments_parameter( + name='a', schema=core_schema.str_schema(), mode='positional_only' + ) + param_b = core_schema.arguments_parameter( + name='b', schema=core_schema.bool_schema(), mode='positional_only' + ) + schema = core_schema.arguments_schema([param_a, param_b]) + v = SchemaValidator(schema) + assert v.validate_python(('hello', True)) == (('hello', True), {}) + ``` + + Args: + arguments: The arguments to use for the arguments schema + validate_by_name: Whether to populate by the parameter names, defaults to `False`. + validate_by_alias: Whether to populate by the parameter aliases, defaults to `True`. + var_args_schema: The variable args schema to use for the arguments schema + var_kwargs_mode: The validation mode to use for variadic keyword arguments. If `'uniform'`, every value of the + keyword arguments will be validated against the `var_kwargs_schema` schema. If `'unpacked-typed-dict'`, + the `var_kwargs_schema` argument must be a [`typed_dict_schema`][pydantic_core.core_schema.typed_dict_schema] + var_kwargs_schema: The variable kwargs schema to use for the arguments schema + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='arguments', + arguments_schema=arguments, + validate_by_name=validate_by_name, + validate_by_alias=validate_by_alias, + var_args_schema=var_args_schema, + var_kwargs_mode=var_kwargs_mode, + var_kwargs_schema=var_kwargs_schema, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class ArgumentsV3Parameter(TypedDict, total=False): + name: Required[str] + schema: Required[CoreSchema] + mode: Literal[ + 'positional_only', + 'positional_or_keyword', + 'keyword_only', + 'var_args', + 'var_kwargs_uniform', + 'var_kwargs_unpacked_typed_dict', + ] # default positional_or_keyword + alias: Union[str, list[Union[str, int]], list[list[Union[str, int]]]] + + +def arguments_v3_parameter( + name: str, + schema: CoreSchema, + *, + mode: Literal[ + 'positional_only', + 'positional_or_keyword', + 'keyword_only', + 'var_args', + 'var_kwargs_uniform', + 'var_kwargs_unpacked_typed_dict', + ] + | None = None, + alias: str | list[str | int] | list[list[str | int]] | None = None, +) -> ArgumentsV3Parameter: + """ + Returns a schema that matches an argument parameter, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + param = core_schema.arguments_v3_parameter( + name='a', schema=core_schema.str_schema(), mode='positional_only' + ) + schema = core_schema.arguments_v3_schema([param]) + v = SchemaValidator(schema) + assert v.validate_python({'a': 'hello'}) == (('hello',), {}) + ``` + + Args: + name: The name to use for the argument parameter + schema: The schema to use for the argument parameter + mode: The mode to use for the argument parameter + alias: The alias to use for the argument parameter + """ + return _dict_not_none(name=name, schema=schema, mode=mode, alias=alias) + + +class ArgumentsV3Schema(TypedDict, total=False): + type: Required[Literal['arguments-v3']] + arguments_schema: Required[list[ArgumentsV3Parameter]] + validate_by_name: bool + validate_by_alias: bool + extra_behavior: Literal['forbid', 'ignore'] # 'allow' doesn't make sense here. + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def arguments_v3_schema( + arguments: list[ArgumentsV3Parameter], + *, + validate_by_name: bool | None = None, + validate_by_alias: bool | None = None, + extra_behavior: Literal['forbid', 'ignore'] | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> ArgumentsV3Schema: + """ + Returns a schema that matches an arguments schema, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + param_a = core_schema.arguments_v3_parameter( + name='a', schema=core_schema.str_schema(), mode='positional_only' + ) + param_b = core_schema.arguments_v3_parameter( + name='kwargs', schema=core_schema.bool_schema(), mode='var_kwargs_uniform' + ) + schema = core_schema.arguments_v3_schema([param_a, param_b]) + v = SchemaValidator(schema) + assert v.validate_python({'a': 'hi', 'kwargs': {'b': True}}) == (('hi',), {'b': True}) + ``` + + This schema is currently not used by other Pydantic components. In V3, it will most likely + become the default arguments schema for the `'call'` schema. + + Args: + arguments: The arguments to use for the arguments schema. + validate_by_name: Whether to populate by the parameter names, defaults to `False`. + validate_by_alias: Whether to populate by the parameter aliases, defaults to `True`. + extra_behavior: The extra behavior to use. + ref: optional unique identifier of the schema, used to reference the schema in other places. + metadata: Any other information you want to include with the schema, not used by pydantic-core. + serialization: Custom serialization schema. + """ + return _dict_not_none( + type='arguments-v3', + arguments_schema=arguments, + validate_by_name=validate_by_name, + validate_by_alias=validate_by_alias, + extra_behavior=extra_behavior, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class CallSchema(TypedDict, total=False): + type: Required[Literal['call']] + arguments_schema: Required[CoreSchema] + function: Required[Callable[..., Any]] + function_name: str # default function.__name__ + return_schema: CoreSchema + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def call_schema( + arguments: CoreSchema, + function: Callable[..., Any], + *, + function_name: str | None = None, + return_schema: CoreSchema | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> CallSchema: + """ + Returns a schema that matches an arguments schema, then calls a function, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + param_a = core_schema.arguments_parameter( + name='a', schema=core_schema.str_schema(), mode='positional_only' + ) + param_b = core_schema.arguments_parameter( + name='b', schema=core_schema.bool_schema(), mode='positional_only' + ) + args_schema = core_schema.arguments_schema([param_a, param_b]) + + schema = core_schema.call_schema( + arguments=args_schema, + function=lambda a, b: a + str(not b), + return_schema=core_schema.str_schema(), + ) + v = SchemaValidator(schema) + assert v.validate_python((('hello', True))) == 'helloFalse' + ``` + + Args: + arguments: The arguments to use for the arguments schema + function: The function to use for the call schema + function_name: The function name to use for the call schema, if not provided `function.__name__` is used + return_schema: The return schema to use for the call schema + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='call', + arguments_schema=arguments, + function=function, + function_name=function_name, + return_schema=return_schema, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class CustomErrorSchema(TypedDict, total=False): + type: Required[Literal['custom-error']] + schema: Required[CoreSchema] + custom_error_type: Required[str] + custom_error_message: str + custom_error_context: dict[str, Union[str, int, float]] + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def custom_error_schema( + schema: CoreSchema, + custom_error_type: str, + *, + custom_error_message: str | None = None, + custom_error_context: dict[str, Any] | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> CustomErrorSchema: + """ + Returns a schema that matches a custom error value, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.custom_error_schema( + schema=core_schema.int_schema(), + custom_error_type='MyError', + custom_error_message='Error msg', + ) + v = SchemaValidator(schema) + v.validate_python(1) + ``` + + Args: + schema: The schema to use for the custom error schema + custom_error_type: The custom error type to use for the custom error schema + custom_error_message: The custom error message to use for the custom error schema + custom_error_context: The custom error context to use for the custom error schema + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='custom-error', + schema=schema, + custom_error_type=custom_error_type, + custom_error_message=custom_error_message, + custom_error_context=custom_error_context, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class JsonSchema(TypedDict, total=False): + type: Required[Literal['json']] + schema: CoreSchema + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def json_schema( + schema: CoreSchema | None = None, + *, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> JsonSchema: + """ + Returns a schema that matches a JSON value, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + dict_schema = core_schema.model_fields_schema( + { + 'field_a': core_schema.model_field(core_schema.str_schema()), + 'field_b': core_schema.model_field(core_schema.bool_schema()), + }, + ) + + class MyModel: + __slots__ = ( + '__dict__', + '__pydantic_fields_set__', + '__pydantic_extra__', + '__pydantic_private__', + ) + field_a: str + field_b: bool + + json_schema = core_schema.json_schema(schema=dict_schema) + schema = core_schema.model_schema(cls=MyModel, schema=json_schema) + v = SchemaValidator(schema) + m = v.validate_python('{"field_a": "hello", "field_b": true}') + assert isinstance(m, MyModel) + ``` + + Args: + schema: The schema to use for the JSON schema + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none(type='json', schema=schema, ref=ref, metadata=metadata, serialization=serialization) + + +class UrlSchema(TypedDict, total=False): + type: Required[Literal['url']] + max_length: int + allowed_schemes: list[str] + host_required: bool # default False + default_host: str + default_port: int + default_path: str + strict: bool + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def url_schema( + *, + max_length: int | None = None, + allowed_schemes: list[str] | None = None, + host_required: bool | None = None, + default_host: str | None = None, + default_port: int | None = None, + default_path: str | None = None, + preserve_empty_path: bool | None = None, + strict: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> UrlSchema: + """ + Returns a schema that matches a URL value, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.url_schema() + v = SchemaValidator(schema) + print(v.validate_python('https://example.com')) + #> https://example.com/ + ``` + + Args: + max_length: The maximum length of the URL + allowed_schemes: The allowed URL schemes + host_required: Whether the URL must have a host + default_host: The default host to use if the URL does not have a host + default_port: The default port to use if the URL does not have a port + default_path: The default path to use if the URL does not have a path + preserve_empty_path: Whether to preserve an empty path or convert it to '/', default False + strict: Whether to use strict URL parsing + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='url', + max_length=max_length, + allowed_schemes=allowed_schemes, + host_required=host_required, + default_host=default_host, + default_port=default_port, + default_path=default_path, + preserve_empty_path=preserve_empty_path, + strict=strict, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class MultiHostUrlSchema(TypedDict, total=False): + type: Required[Literal['multi-host-url']] + max_length: int + allowed_schemes: list[str] + host_required: bool # default False + default_host: str + default_port: int + default_path: str + strict: bool + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def multi_host_url_schema( + *, + max_length: int | None = None, + allowed_schemes: list[str] | None = None, + host_required: bool | None = None, + default_host: str | None = None, + default_port: int | None = None, + default_path: str | None = None, + preserve_empty_path: bool | None = None, + strict: bool | None = None, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> MultiHostUrlSchema: + """ + Returns a schema that matches a URL value with possibly multiple hosts, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.multi_host_url_schema() + v = SchemaValidator(schema) + print(v.validate_python('redis://localhost,0.0.0.0,127.0.0.1')) + #> redis://localhost,0.0.0.0,127.0.0.1 + ``` + + Args: + max_length: The maximum length of the URL + allowed_schemes: The allowed URL schemes + host_required: Whether the URL must have a host + default_host: The default host to use if the URL does not have a host + default_port: The default port to use if the URL does not have a port + default_path: The default path to use if the URL does not have a path + preserve_empty_path: Whether to preserve an empty path or convert it to '/', default False + strict: Whether to use strict URL parsing + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='multi-host-url', + max_length=max_length, + allowed_schemes=allowed_schemes, + host_required=host_required, + default_host=default_host, + default_port=default_port, + default_path=default_path, + preserve_empty_path=preserve_empty_path, + strict=strict, + ref=ref, + metadata=metadata, + serialization=serialization, + ) + + +class DefinitionsSchema(TypedDict, total=False): + type: Required[Literal['definitions']] + schema: Required[CoreSchema] + definitions: Required[list[CoreSchema]] + metadata: dict[str, Any] + serialization: SerSchema + + +def definitions_schema(schema: CoreSchema, definitions: list[CoreSchema]) -> DefinitionsSchema: + """ + Build a schema that contains both an inner schema and a list of definitions which can be used + within the inner schema. + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.definitions_schema( + core_schema.list_schema(core_schema.definition_reference_schema('foobar')), + [core_schema.int_schema(ref='foobar')], + ) + v = SchemaValidator(schema) + assert v.validate_python([1, 2, '3']) == [1, 2, 3] + ``` + + Args: + schema: The inner schema + definitions: List of definitions which can be referenced within inner schema + """ + return DefinitionsSchema(type='definitions', schema=schema, definitions=definitions) + + +class DefinitionReferenceSchema(TypedDict, total=False): + type: Required[Literal['definition-ref']] + schema_ref: Required[str] + ref: str + metadata: dict[str, Any] + serialization: SerSchema + + +def definition_reference_schema( + schema_ref: str, + ref: str | None = None, + metadata: dict[str, Any] | None = None, + serialization: SerSchema | None = None, +) -> DefinitionReferenceSchema: + """ + Returns a schema that points to a schema stored in "definitions", this is useful for nested recursive + models and also when you want to define validators separately from the main schema, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema_definition = core_schema.definition_reference_schema('list-schema') + schema = core_schema.definitions_schema( + schema=schema_definition, + definitions=[ + core_schema.list_schema(items_schema=schema_definition, ref='list-schema'), + ], + ) + v = SchemaValidator(schema) + assert v.validate_python([()]) == [[]] + ``` + + Args: + schema_ref: The schema ref to use for the definition reference schema + metadata: Any other information you want to include with the schema, not used by pydantic-core + serialization: Custom serialization schema + """ + return _dict_not_none( + type='definition-ref', schema_ref=schema_ref, ref=ref, metadata=metadata, serialization=serialization + ) + + +MYPY = False +# See https://github.com/python/mypy/issues/14034 for details, in summary mypy is extremely slow to process this +# union which kills performance not just for pydantic, but even for code using pydantic +if not MYPY: + CoreSchema = Union[ + InvalidSchema, + AnySchema, + NoneSchema, + BoolSchema, + IntSchema, + FloatSchema, + DecimalSchema, + StringSchema, + BytesSchema, + DateSchema, + TimeSchema, + DatetimeSchema, + TimedeltaSchema, + LiteralSchema, + MissingSentinelSchema, + EnumSchema, + IsInstanceSchema, + IsSubclassSchema, + CallableSchema, + ListSchema, + TupleSchema, + SetSchema, + FrozenSetSchema, + GeneratorSchema, + DictSchema, + AfterValidatorFunctionSchema, + BeforeValidatorFunctionSchema, + WrapValidatorFunctionSchema, + PlainValidatorFunctionSchema, + WithDefaultSchema, + NullableSchema, + UnionSchema, + TaggedUnionSchema, + ChainSchema, + LaxOrStrictSchema, + JsonOrPythonSchema, + TypedDictSchema, + ModelFieldsSchema, + ModelSchema, + DataclassArgsSchema, + DataclassSchema, + ArgumentsSchema, + ArgumentsV3Schema, + CallSchema, + CustomErrorSchema, + JsonSchema, + UrlSchema, + MultiHostUrlSchema, + DefinitionsSchema, + DefinitionReferenceSchema, + UuidSchema, + ComplexSchema, + ] +elif False: + CoreSchema: TypeAlias = Mapping[str, Any] + + +# to update this, call `pytest -k test_core_schema_type_literal` and copy the output +CoreSchemaType = Literal[ + 'invalid', + 'any', + 'none', + 'bool', + 'int', + 'float', + 'decimal', + 'str', + 'bytes', + 'date', + 'time', + 'datetime', + 'timedelta', + 'literal', + 'missing-sentinel', + 'enum', + 'is-instance', + 'is-subclass', + 'callable', + 'list', + 'tuple', + 'set', + 'frozenset', + 'generator', + 'dict', + 'function-after', + 'function-before', + 'function-wrap', + 'function-plain', + 'default', + 'nullable', + 'union', + 'tagged-union', + 'chain', + 'lax-or-strict', + 'json-or-python', + 'typed-dict', + 'model-fields', + 'model', + 'dataclass-args', + 'dataclass', + 'arguments', + 'arguments-v3', + 'call', + 'custom-error', + 'json', + 'url', + 'multi-host-url', + 'definitions', + 'definition-ref', + 'uuid', + 'complex', +] + +CoreSchemaFieldType = Literal['model-field', 'dataclass-field', 'typed-dict-field', 'computed-field'] + + +# used in _pydantic_core.pyi::PydanticKnownError +# to update this, call `pytest -k test_all_errors` and copy the output +ErrorType = Literal[ + 'no_such_attribute', + 'json_invalid', + 'json_type', + 'needs_python_object', + 'recursion_loop', + 'missing', + 'frozen_field', + 'frozen_instance', + 'extra_forbidden', + 'invalid_key', + 'get_attribute_error', + 'model_type', + 'model_attributes_type', + 'dataclass_type', + 'dataclass_exact_type', + 'default_factory_not_called', + 'none_required', + 'greater_than', + 'greater_than_equal', + 'less_than', + 'less_than_equal', + 'multiple_of', + 'finite_number', + 'too_short', + 'too_long', + 'iterable_type', + 'iteration_error', + 'string_type', + 'string_sub_type', + 'string_unicode', + 'string_too_short', + 'string_too_long', + 'string_pattern_mismatch', + 'string_not_ascii', + 'enum', + 'dict_type', + 'mapping_type', + 'list_type', + 'tuple_type', + 'set_type', + 'set_item_not_hashable', + 'bool_type', + 'bool_parsing', + 'int_type', + 'int_parsing', + 'int_parsing_size', + 'int_from_float', + 'float_type', + 'float_parsing', + 'bytes_type', + 'bytes_too_short', + 'bytes_too_long', + 'bytes_invalid_encoding', + 'value_error', + 'assertion_error', + 'literal_error', + 'missing_sentinel_error', + 'date_type', + 'date_parsing', + 'date_from_datetime_parsing', + 'date_from_datetime_inexact', + 'date_past', + 'date_future', + 'time_type', + 'time_parsing', + 'datetime_type', + 'datetime_parsing', + 'datetime_object_invalid', + 'datetime_from_date_parsing', + 'datetime_past', + 'datetime_future', + 'timezone_naive', + 'timezone_aware', + 'timezone_offset', + 'time_delta_type', + 'time_delta_parsing', + 'frozen_set_type', + 'is_instance_of', + 'is_subclass_of', + 'callable_type', + 'union_tag_invalid', + 'union_tag_not_found', + 'arguments_type', + 'missing_argument', + 'unexpected_keyword_argument', + 'missing_keyword_only_argument', + 'unexpected_positional_argument', + 'missing_positional_only_argument', + 'multiple_argument_values', + 'url_type', + 'url_parsing', + 'url_syntax_violation', + 'url_too_long', + 'url_scheme', + 'uuid_type', + 'uuid_parsing', + 'uuid_version', + 'decimal_type', + 'decimal_parsing', + 'decimal_max_digits', + 'decimal_max_places', + 'decimal_whole_digits', + 'complex_type', + 'complex_str_parsing', +] + + +def _dict_not_none(**kwargs: Any) -> Any: + return {k: v for k, v in kwargs.items() if v is not None} + + +def iter_union_choices(union_schema: UnionSchema) -> Generator[CoreSchema]: + """Iterate over the choices of a `'union'` schema.""" + for choice in union_schema['choices']: + if isinstance(choice, tuple): + yield choice[0] + else: + yield choice + + +############################################################################### +# All this stuff is deprecated by #980 and will be removed eventually +# They're kept because some code external code will be using them + + +@deprecated('`field_before_validator_function` is deprecated, use `with_info_before_validator_function` instead.') +def field_before_validator_function(function: WithInfoValidatorFunction, field_name: str, schema: CoreSchema, **kwargs): + warnings.warn( + '`field_before_validator_function` is deprecated, use `with_info_before_validator_function` instead.', + DeprecationWarning, + ) + return with_info_before_validator_function(function, schema, field_name=field_name, **kwargs) + + +@deprecated('`general_before_validator_function` is deprecated, use `with_info_before_validator_function` instead.') +def general_before_validator_function(*args, **kwargs): + warnings.warn( + '`general_before_validator_function` is deprecated, use `with_info_before_validator_function` instead.', + DeprecationWarning, + ) + return with_info_before_validator_function(*args, **kwargs) + + +@deprecated('`field_after_validator_function` is deprecated, use `with_info_after_validator_function` instead.') +def field_after_validator_function(function: WithInfoValidatorFunction, field_name: str, schema: CoreSchema, **kwargs): + warnings.warn( + '`field_after_validator_function` is deprecated, use `with_info_after_validator_function` instead.', + DeprecationWarning, + ) + return with_info_after_validator_function(function, schema, field_name=field_name, **kwargs) + + +@deprecated('`general_after_validator_function` is deprecated, use `with_info_after_validator_function` instead.') +def general_after_validator_function(*args, **kwargs): + warnings.warn( + '`general_after_validator_function` is deprecated, use `with_info_after_validator_function` instead.', + DeprecationWarning, + ) + return with_info_after_validator_function(*args, **kwargs) + + +@deprecated('`field_wrap_validator_function` is deprecated, use `with_info_wrap_validator_function` instead.') +def field_wrap_validator_function( + function: WithInfoWrapValidatorFunction, field_name: str, schema: CoreSchema, **kwargs +): + warnings.warn( + '`field_wrap_validator_function` is deprecated, use `with_info_wrap_validator_function` instead.', + DeprecationWarning, + ) + return with_info_wrap_validator_function(function, schema, field_name=field_name, **kwargs) + + +@deprecated('`general_wrap_validator_function` is deprecated, use `with_info_wrap_validator_function` instead.') +def general_wrap_validator_function(*args, **kwargs): + warnings.warn( + '`general_wrap_validator_function` is deprecated, use `with_info_wrap_validator_function` instead.', + DeprecationWarning, + ) + return with_info_wrap_validator_function(*args, **kwargs) + + +@deprecated('`field_plain_validator_function` is deprecated, use `with_info_plain_validator_function` instead.') +def field_plain_validator_function(function: WithInfoValidatorFunction, field_name: str, **kwargs): + warnings.warn( + '`field_plain_validator_function` is deprecated, use `with_info_plain_validator_function` instead.', + DeprecationWarning, + ) + return with_info_plain_validator_function(function, field_name=field_name, **kwargs) + + +@deprecated('`general_plain_validator_function` is deprecated, use `with_info_plain_validator_function` instead.') +def general_plain_validator_function(*args, **kwargs): + warnings.warn( + '`general_plain_validator_function` is deprecated, use `with_info_plain_validator_function` instead.', + DeprecationWarning, + ) + return with_info_plain_validator_function(*args, **kwargs) + + +_deprecated_import_lookup = { + 'FieldValidationInfo': ValidationInfo, + 'FieldValidatorFunction': WithInfoValidatorFunction, + 'GeneralValidatorFunction': WithInfoValidatorFunction, + 'FieldWrapValidatorFunction': WithInfoWrapValidatorFunction, +} + +if TYPE_CHECKING: + FieldValidationInfo = ValidationInfo + + +def __getattr__(attr_name: str) -> object: + new_attr = _deprecated_import_lookup.get(attr_name) + if new_attr is None: + raise AttributeError(f"module 'pydantic_core' has no attribute '{attr_name}'") + else: + import warnings + + msg = f'`{attr_name}` is deprecated, use `{new_attr.__name__}` instead.' + warnings.warn(msg, DeprecationWarning, stacklevel=1) + return new_attr diff --git a/source/target-vendors/cp312/pydantic_core/py.typed b/source/target-vendors/cp312/pydantic_core/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/pydantic_settings-2.15.0.dist-info/METADATA b/source/target-vendors/cp312/pydantic_settings-2.15.0.dist-info/METADATA new file mode 100644 index 0000000..52926c2 --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings-2.15.0.dist-info/METADATA @@ -0,0 +1,70 @@ +Metadata-Version: 2.4 +Name: pydantic-settings +Version: 2.15.0 +Summary: Settings management using Pydantic +Project-URL: Homepage, https://github.com/pydantic/pydantic-settings +Project-URL: Funding, https://github.com/sponsors/samuelcolvin +Project-URL: Source, https://github.com/pydantic/pydantic-settings +Project-URL: Changelog, https://github.com/pydantic/pydantic-settings/releases +Project-URL: Documentation, https://docs.pydantic.dev/dev-v2/concepts/pydantic_settings/ +Author-email: Samuel Colvin , Eric Jolibois , Hasan Ramezani +License-Expression: MIT +License-File: LICENSE +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console +Classifier: Environment :: MacOS X +Classifier: Framework :: Pydantic +Classifier: Framework :: Pydantic :: 2 +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Information Technology +Classifier: Intended Audience :: System Administrators +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: POSIX :: Linux +Classifier: Operating System :: Unix +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Topic :: Internet +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires-Python: >=3.10 +Requires-Dist: pydantic>=2.7.0 +Requires-Dist: python-dotenv>=0.21.0 +Requires-Dist: typing-inspection>=0.4.0 +Provides-Extra: aws-secrets-manager +Requires-Dist: boto3>=1.35.0; extra == 'aws-secrets-manager' +Provides-Extra: azure-key-vault +Requires-Dist: azure-identity>=1.16.0; extra == 'azure-key-vault' +Requires-Dist: azure-keyvault-secrets>=4.8.0; extra == 'azure-key-vault' +Provides-Extra: gcp-secret-manager +Requires-Dist: google-cloud-secret-manager>=2.23.1; extra == 'gcp-secret-manager' +Provides-Extra: toml +Requires-Dist: tomli>=2.0.1; extra == 'toml' +Provides-Extra: yaml +Requires-Dist: pyyaml>=6.0.1; extra == 'yaml' +Description-Content-Type: text/markdown + +# pydantic-settings + +[![CI](https://github.com/pydantic/pydantic-settings/actions/workflows/ci.yml/badge.svg?event=push)](https://github.com/pydantic/pydantic-settings/actions/workflows/ci.yml?query=branch%3Amain) +[![Coverage](https://codecov.io/gh/pydantic/pydantic-settings/branch/main/graph/badge.svg)](https://codecov.io/gh/pydantic/pydantic-settings) +[![pypi](https://img.shields.io/pypi/v/pydantic-settings.svg)](https://pypi.python.org/pypi/pydantic-settings) +[![license](https://img.shields.io/github/license/pydantic/pydantic-settings.svg)](https://github.com/pydantic/pydantic-settings/blob/main/LICENSE) +[![downloads](https://static.pepy.tech/badge/pydantic-settings/month)](https://pepy.tech/project/pydantic-settings) +[![versions](https://img.shields.io/pypi/pyversions/pydantic-settings.svg)](https://github.com/pydantic/pydantic-settings) + +Settings management using Pydantic. + +See [documentation](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/) for more details. + +## Part of the Pydantic Stack + +The Pydantic Stack is everything you need to ship production-grade AI agents: + +- [Pydantic AI](https://pydantic.dev/pydantic-ai?utm_source=github&utm_medium=readme&utm_campaign=pydantic-settings) - Type-safe agent framework +- [Pydantic Logfire](https://pydantic.dev/logfire?utm_source=github&utm_medium=readme&utm_campaign=pydantic-settings) - AI-first, full-stack observability +- [Logfire AI Gateway](https://pydantic.dev/ai-gateway?utm_source=github&utm_medium=readme&utm_campaign=pydantic-settings) - Unified LLM proxy diff --git a/source/target-vendors/cp312/pydantic_settings-2.15.0.dist-info/RECORD b/source/target-vendors/cp312/pydantic_settings-2.15.0.dist-info/RECORD new file mode 100644 index 0000000..ebd1905 --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings-2.15.0.dist-info/RECORD @@ -0,0 +1,27 @@ +pydantic_settings/__init__.py,sha256=F24B0u2U_vPocmZwgqpN83JtwfFSAbsl3VePWAJCBbE,1781 +pydantic_settings/exceptions.py,sha256=o9WAppV9m_8334HP2LqxzbrVfM4WHV56Cqit57tgL4A,426 +pydantic_settings/main.py,sha256=HxKWdFB6duziUY4ezxTt5BS2Hlb-880pxsMO1H-GC_I,47390 +pydantic_settings/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pydantic_settings/utils.py,sha256=ZPXGoD5MU3QLl6TlT_P2VqP11kMqb9rqp0nc3nsCLRc,1703 +pydantic_settings/version.py,sha256=jSdrSRDnJmdDSOi5UEA39d_r3Ov1jn5kIrqsZnD1-Po,19 +pydantic_settings/sources/__init__.py,sha256=sOdQuzLbpBvJWKWGUyIK_ZeLvJnAvFiFNARE_PVHXJ4,2404 +pydantic_settings/sources/base.py,sha256=5X-IrXve090QH-u39NRVcVB5E3hUsNuxqTOOA3h-_4Q,26340 +pydantic_settings/sources/types.py,sha256=6XH_-XSRVb78LbDFSDIAcfA7GqP2237ozUlwZqUjTaQ,2617 +pydantic_settings/sources/utils.py,sha256=1xhDF5cG-JZxYjUf_xDTZmioM-LOoRGWhBuk7v3ci3Q,14570 +pydantic_settings/sources/providers/__init__.py,sha256=gE6XLz6hbKpc_sSidAQ-UM001uWUXg7xGU-IRHrC6Ug,1281 +pydantic_settings/sources/providers/aws.py,sha256=jielh-5n_kl8b8gGOd3kq3Mr1UkwW5JjUCqqtcOzw4s,2729 +pydantic_settings/sources/providers/azure.py,sha256=ljAhpbQ2CENyBBm8StZBXVhEhZN4rDqBcRP4Ji8kdYA,5689 +pydantic_settings/sources/providers/cli.py,sha256=e_LHvvyQyRXmp4s2pLJdVDCvMsR46LlayMrSJAzFGO4,79106 +pydantic_settings/sources/providers/dotenv.py,sha256=oul0VHKUS0UFvr5eFcR5gPDIP77QHMpH6963RFDzuak,8328 +pydantic_settings/sources/providers/env.py,sha256=vyV26JsO5LhLOhPX86k6pEK1ZQetSjHQH5kIrNTQYV4,16235 +pydantic_settings/sources/providers/gcp.py,sha256=ykP5sLd6l95eyAh3oHz7Me0tnlqNZnYm_YRuwyxPrpM,15238 +pydantic_settings/sources/providers/json.py,sha256=9rohQV_nK0z-Xn2n6dLU63-Kx3O7rxSkaykkMVUdgos,1673 +pydantic_settings/sources/providers/nested_secrets.py,sha256=k6pntQ_8GGfpRzZTMNAt3F7RaAN64Xjkv08b2iNRkOM,9260 +pydantic_settings/sources/providers/pyproject.py,sha256=-NYo0pHE0LuebcCHu5e2x5uYPYjT5Cch9W5nD23wwmo,2153 +pydantic_settings/sources/providers/secrets.py,sha256=8c3tV9FyW36P5TTjiWU4sF7XIYObkLMgxj7a8WL-pns,5188 +pydantic_settings/sources/providers/toml.py,sha256=FCLO4KuuvhDPm4cjEy18DGPghg6AQzq8sHzwmYudTio,3351 +pydantic_settings/sources/providers/yaml.py,sha256=rKkIPx2DF-3-prmoniU13GrxRYFjsEx84EvTbLjiZD4,5726 +pydantic_settings-2.15.0.dist-info/METADATA,sha256=WsEuQI32Lx1MdkMUbZ3wSs50nx_lzyizt5S-Cyg0ym0,3885 +pydantic_settings-2.15.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87 +pydantic_settings-2.15.0.dist-info/licenses/LICENSE,sha256=6zVadT4CA0bTPYO_l2kTW4n8YQVorFMaAcKVvO5_2Zg,1103 +pydantic_settings-2.15.0.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/pydantic_settings-2.15.0.dist-info/WHEEL b/source/target-vendors/cp312/pydantic_settings-2.15.0.dist-info/WHEEL new file mode 100644 index 0000000..7401812 --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings-2.15.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.31.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/source/target-vendors/cp312/pydantic_settings-2.15.0.dist-info/licenses/LICENSE b/source/target-vendors/cp312/pydantic_settings-2.15.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000..d90598f --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings-2.15.0.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2022 Samuel Colvin and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/source/target-vendors/cp312/pydantic_settings/__init__.py b/source/target-vendors/cp312/pydantic_settings/__init__.py new file mode 100644 index 0000000..8cdd40c --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/__init__.py @@ -0,0 +1,70 @@ +from .exceptions import IncompleteFieldDefinitionWarning, SettingsError +from .main import BaseSettings, CliApp, SettingsConfigDict +from .sources import ( + CLI_SUPPRESS, + AWSSecretsManagerSettingsSource, + AzureKeyVaultSettingsSource, + CliDualFlag, + CliExplicitFlag, + CliImplicitFlag, + CliMutuallyExclusiveGroup, + CliPositionalArg, + CliSettingsSource, + CliSubCommand, + CliSuppress, + CliToggleFlag, + CliUnknownArgs, + DotEnvSettingsSource, + EnvSettingsSource, + ForceDecode, + GoogleSecretManagerSettingsSource, + InitSettingsSource, + JsonConfigSettingsSource, + NestedSecretsSettingsSource, + NoDecode, + PydanticBaseSettingsSource, + PyprojectTomlConfigSettingsSource, + SecretsSettingsSource, + TomlConfigSettingsSource, + YamlConfigSettingsSource, + get_subcommand, +) +from .version import VERSION + +__all__ = ( + 'CLI_SUPPRESS', + 'AWSSecretsManagerSettingsSource', + 'AzureKeyVaultSettingsSource', + 'BaseSettings', + 'CliApp', + 'CliDualFlag', + 'CliExplicitFlag', + 'CliImplicitFlag', + 'CliMutuallyExclusiveGroup', + 'CliPositionalArg', + 'CliSettingsSource', + 'CliSubCommand', + 'CliSuppress', + 'CliToggleFlag', + 'CliUnknownArgs', + 'DotEnvSettingsSource', + 'EnvSettingsSource', + 'ForceDecode', + 'GoogleSecretManagerSettingsSource', + 'IncompleteFieldDefinitionWarning', + 'InitSettingsSource', + 'JsonConfigSettingsSource', + 'NestedSecretsSettingsSource', + 'NoDecode', + 'PydanticBaseSettingsSource', + 'PyprojectTomlConfigSettingsSource', + 'SecretsSettingsSource', + 'SettingsConfigDict', + 'SettingsError', + 'TomlConfigSettingsSource', + 'YamlConfigSettingsSource', + '__version__', + 'get_subcommand', +) + +__version__ = VERSION diff --git a/source/target-vendors/cp312/pydantic_settings/exceptions.py b/source/target-vendors/cp312/pydantic_settings/exceptions.py new file mode 100644 index 0000000..dab7c71 --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/exceptions.py @@ -0,0 +1,10 @@ +class SettingsError(ValueError): + """Base exception for settings-related errors.""" + + +class IncompleteFieldDefinitionWarning(UserWarning): + """Warning emitted when a field with an incomplete definition is used during settings resolution. + + A field definition is incomplete when its annotation contains unresolved forward references, + in which case settings sources may fail to correctly resolve its value. + """ diff --git a/source/target-vendors/cp312/pydantic_settings/main.py b/source/target-vendors/cp312/pydantic_settings/main.py new file mode 100644 index 0000000..3366241 --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/main.py @@ -0,0 +1,978 @@ +from __future__ import annotations as _annotations + +import asyncio +import inspect +import logging +import re +import threading +import warnings +from argparse import Namespace +from collections.abc import Mapping +from types import SimpleNamespace +from typing import Any, ClassVar, Literal, TextIO, TypeVar, cast + +from pydantic import ConfigDict +from pydantic._internal._config import config_keys +from pydantic._internal._signature import _field_name_for_signature +from pydantic._internal._utils import deep_update, is_model_class +from pydantic.dataclasses import is_pydantic_dataclass +from pydantic.main import BaseModel + +from .exceptions import SettingsError +from .sources import ( + ENV_FILE_SENTINEL, + CliSettingsSource, + ConfigFileSourceType, + DefaultSettingsSource, + DotenvFiltering, + DotEnvSettingsSource, + DotenvType, + EnvPrefixTarget, + EnvSettingsSource, + InitSettingsSource, + JsonConfigSettingsSource, + PathType, + PydanticBaseSettingsSource, + PydanticModel, + PyprojectTomlConfigSettingsSource, + SecretsSettingsSource, + TomlConfigSettingsSource, + YamlConfigSettingsSource, + get_subcommand, +) +from .sources.utils import InitState, _get_alias_names, _warn_if_field_info_incomplete +from .utils import _settings_debug_enabled, logger + +T = TypeVar('T') + + +class SettingsConfigDict(ConfigDict, total=False): + case_sensitive: bool + nested_model_default_partial_update: bool | None + env_prefix: str + env_prefix_target: EnvPrefixTarget + env_file: DotenvType | None + env_file_encoding: str | None + dotenv_filtering: DotenvFiltering | None + env_ignore_empty: bool + env_nested_delimiter: str | None + env_nested_max_split: int | None + env_parse_none_str: str | None + env_parse_enums: bool | None + cli_prog_name: str | None + cli_parse_args: bool | list[str] | tuple[str, ...] | None + cli_parse_none_str: str | None + cli_hide_none_type: bool + cli_avoid_json: bool + cli_enforce_required: bool + cli_use_class_docs_for_groups: bool + cli_show_env_vars: bool + cli_exit_on_error: bool + cli_prefix: str + cli_flag_prefix_char: str + cli_implicit_flags: bool | Literal['dual', 'toggle'] | None + cli_ignore_unknown_args: bool | None + cli_kebab_case: bool | Literal['all', 'no_enums'] | None + cli_shortcuts: Mapping[str, str | list[str]] | None + secrets_dir: PathType | None + json_file: ConfigFileSourceType | None + json_file_encoding: str | None + yaml_file: ConfigFileSourceType | None + yaml_file_encoding: str | None + yaml_config_section: str | None + """ + Specifies the section in a YAML file from which to load the settings. + Supports dot-notation for nested paths (e.g., 'config.app.settings'). + If provided, the settings will be loaded from the specified section. + This is useful when the YAML file contains multiple configuration sections + and you only want to load a specific subset into your settings model. + """ + + pyproject_toml_depth: int + """ + Number of levels **up** from the current working directory to attempt to find a pyproject.toml + file. + + This is only used when a pyproject.toml file is not found in the current working directory. + """ + + pyproject_toml_table_header: tuple[str, ...] + """ + Header of the TOML table within a pyproject.toml file to use when filling variables. + This is supplied as a `tuple[str, ...]` instead of a `str` to accommodate for headers + containing a `.`. + + For example, `toml_table_header = ("tool", "my.tool", "foo")` can be used to fill variable + values from a table with header `[tool."my.tool".foo]`. + + To use the root table, exclude this config setting or provide an empty tuple. + """ + + toml_file: ConfigFileSourceType | None + + toml_table_header: tuple[str, ...] + """ + Header of the TOML table in a TOML file to use when filling variables. + Similar to `pyproject_toml_table_header`, but for generic TOML file handling. + + Default is to use the root table, i.e. no table header. + """ + enable_decoding: bool + + +# Extend `config_keys` by pydantic settings config keys to +# support setting config through class kwargs. +# Pydantic uses `config_keys` in `pydantic._internal._config.ConfigWrapper.for_model` +# to extract config keys from model kwargs, So, by adding pydantic settings keys to +# `config_keys`, they will be considered as valid config keys and will be collected +# by Pydantic. +config_keys |= set(SettingsConfigDict.__annotations__.keys()) + + +class BaseSettings(BaseModel): + """ + Base class for settings, allowing values to be overridden by environment variables. + + This is useful in production for secrets you do not wish to save in code, it plays nicely with docker(-compose), + Heroku and any 12 factor app design. + + All the below attributes can be set via `model_config`. + + Args: + _case_sensitive: Whether environment and CLI variable names should be read with case-sensitivity. + Defaults to `None`. + _nested_model_default_partial_update: Whether to allow partial updates on nested model default object fields. + Defaults to `False`. + _env_prefix: Prefix for all environment variables. Defaults to `None`. + _env_prefix_target: Targets to which `_env_prefix` is applied. Default: `variable`. + _env_file: The env file(s) to load settings values from. Defaults to `Path('')`, which + means that the value from `model_config['env_file']` should be used. You can also pass + `None` to indicate that environment variables should not be loaded from an env file. + _env_file_encoding: The env file encoding, e.g. `'latin-1'`. Defaults to `None`. + _env_ignore_empty: Ignore environment variables where the value is an empty string. Default to `False`. + _env_nested_delimiter: The nested env values delimiter. Defaults to `None`. + _env_nested_max_split: The nested env values maximum nesting. Defaults to `None`, which means no limit. + _env_parse_none_str: The env string value that should be parsed (e.g. "null", "void", "None", etc.) + into `None` type(None). Defaults to `None` type(None), which means no parsing should occur. + _env_parse_enums: Parse enum field names to values. Defaults to `None.`, which means no parsing should occur. + _cli_prog_name: The CLI program name to display in help text. Defaults to `None` if _cli_parse_args is `None`. + Otherwise, defaults to sys.argv[0]. + _cli_parse_args: The list of CLI arguments to parse. Defaults to None. + If set to `True`, defaults to sys.argv[1:]. + _cli_settings_source: Override the default CLI settings source with a user defined instance. Defaults to None. + _cli_parse_none_str: The CLI string value that should be parsed (e.g. "null", "void", "None", etc.) into + `None` type(None). Defaults to _env_parse_none_str value if set. Otherwise, defaults to "null" if + _cli_avoid_json is `False`, and "None" if _cli_avoid_json is `True`. + _cli_hide_none_type: Hide `None` values in CLI help text. Defaults to `False`. + _cli_avoid_json: Avoid complex JSON objects in CLI help text. Defaults to `False`. + _cli_enforce_required: Enforce required fields at the CLI. Defaults to `False`. + _cli_use_class_docs_for_groups: Use class docstrings in CLI group help text instead of field descriptions. + Defaults to `False`. + _cli_show_env_vars: Show resolved environment variable names in CLI help text. Defaults to `False`. + _cli_exit_on_error: Determines whether or not the internal parser exits with error info when an error occurs. + Defaults to `True`. + _cli_prefix: The root parser command line arguments prefix. Defaults to "". + _cli_flag_prefix_char: The flag prefix character to use for CLI optional arguments. Defaults to '-'. + _cli_implicit_flags: Controls how `bool` fields are exposed as CLI flags. + + - False (default): no implicit flags are generated; booleans must be set explicitly (e.g. --flag=true). + - True / 'dual': optional boolean fields generate both positive and negative forms (--flag and --no-flag). + - 'toggle': required boolean fields remain in 'dual' mode, while optional boolean fields generate a single + flag aligned with the default value (if default=False, expose --flag; if default=True, expose --no-flag). + _cli_ignore_unknown_args: Whether to ignore unknown CLI args and parse only known ones. Defaults to `False`. + _cli_kebab_case: CLI args use kebab case. Defaults to `False`. + _cli_shortcuts: Mapping of target field name to alias names. Defaults to `None`. + _secrets_dir: The secret files directory or a sequence of directories. Defaults to `None`. + _build_sources: Pre-initialized sources and init kwargs to use for building instantiation values. + Defaults to `None`. + """ + + # Note: when adding new parameters, make sure to use `object` instead of `Any` to avoid issues with the Mypy plugin + # when used with `--disallow-any-explicit`. If `Any` needs to be used as a generic parameter for variance (e.g. in `_build_sources`), + # make sure to update the Pydantic Mypy plugin accordingly. + def __init__( + __pydantic_self__, + _case_sensitive: bool | None = None, + _nested_model_default_partial_update: bool | None = None, + _env_prefix: str | None = None, + _env_prefix_target: EnvPrefixTarget | None = None, + _env_file: DotenvType | None = ENV_FILE_SENTINEL, + _env_file_encoding: str | None = None, + _env_ignore_empty: bool | None = None, + _env_nested_delimiter: str | None = None, + _env_nested_max_split: int | None = None, + _env_parse_none_str: str | None = None, + _env_parse_enums: bool | None = None, + _cli_prog_name: str | None = None, + _cli_parse_args: bool | list[str] | tuple[str, ...] | None = None, + _cli_settings_source: CliSettingsSource[Any] | None = None, + _cli_parse_none_str: str | None = None, + _cli_hide_none_type: bool | None = None, + _cli_avoid_json: bool | None = None, + _cli_enforce_required: bool | None = None, + _cli_use_class_docs_for_groups: bool | None = None, + _cli_show_env_vars: bool | None = None, + _cli_exit_on_error: bool | None = None, + _cli_prefix: str | None = None, + _cli_flag_prefix_char: str | None = None, + _cli_implicit_flags: bool | Literal['dual', 'toggle'] | None = None, + _cli_ignore_unknown_args: bool | None = None, + _cli_kebab_case: bool | Literal['all', 'no_enums'] | None = None, + _cli_shortcuts: Mapping[str, str | list[str]] | None = None, + _secrets_dir: PathType | None = None, + _build_sources: tuple[tuple[PydanticBaseSettingsSource, ...], dict[str, Any]] | None = None, + **values: Any, + ) -> None: + sources, init_kwargs = ( + _build_sources + if _build_sources is not None + else __pydantic_self__.__class__._settings_init_sources( + _case_sensitive=_case_sensitive, + _nested_model_default_partial_update=_nested_model_default_partial_update, + _env_prefix=_env_prefix, + _env_prefix_target=_env_prefix_target, + _env_file=_env_file, + _env_file_encoding=_env_file_encoding, + _env_ignore_empty=_env_ignore_empty, + _env_nested_delimiter=_env_nested_delimiter, + _env_nested_max_split=_env_nested_max_split, + _env_parse_none_str=_env_parse_none_str, + _env_parse_enums=_env_parse_enums, + _cli_prog_name=_cli_prog_name, + _cli_parse_args=_cli_parse_args, + _cli_settings_source=_cli_settings_source, + _cli_parse_none_str=_cli_parse_none_str, + _cli_hide_none_type=_cli_hide_none_type, + _cli_avoid_json=_cli_avoid_json, + _cli_enforce_required=_cli_enforce_required, + _cli_use_class_docs_for_groups=_cli_use_class_docs_for_groups, + _cli_show_env_vars=_cli_show_env_vars, + _cli_exit_on_error=_cli_exit_on_error, + _cli_prefix=_cli_prefix, + _cli_flag_prefix_char=_cli_flag_prefix_char, + _cli_implicit_flags=_cli_implicit_flags, + _cli_ignore_unknown_args=_cli_ignore_unknown_args, + _cli_kebab_case=_cli_kebab_case, + _cli_shortcuts=_cli_shortcuts, + _secrets_dir=_secrets_dir, + _init_kwargs=values, + ) + ) + + super().__init__(**__pydantic_self__.__class__._settings_build_values(sources, init_kwargs)) + + @classmethod + def settings_customise_sources( + cls, + settings_cls: type[BaseSettings], + init_settings: PydanticBaseSettingsSource, + env_settings: PydanticBaseSettingsSource, + dotenv_settings: PydanticBaseSettingsSource, + file_secret_settings: PydanticBaseSettingsSource, + ) -> tuple[PydanticBaseSettingsSource, ...]: + """ + Define the sources and their order for loading the settings values. + + Args: + settings_cls: The Settings class. + init_settings: The `InitSettingsSource` instance. + env_settings: The `EnvSettingsSource` instance. + dotenv_settings: The `DotEnvSettingsSource` instance. + file_secret_settings: The `SecretsSettingsSource` instance. + + Returns: + A tuple containing the sources and their order for loading the settings values. + """ + return init_settings, env_settings, dotenv_settings, file_secret_settings + + @classmethod + def _settings_init_sources( + cls, + _case_sensitive: bool | None = None, + _nested_model_default_partial_update: bool | None = None, + _env_prefix: str | None = None, + _env_prefix_target: EnvPrefixTarget | None = None, + _env_file: DotenvType | None = ENV_FILE_SENTINEL, + _env_file_encoding: str | None = None, + _env_ignore_empty: bool | None = None, + _env_nested_delimiter: str | None = None, + _env_nested_max_split: int | None = None, + _env_parse_none_str: str | None = None, + _env_parse_enums: bool | None = None, + _cli_prog_name: str | None = None, + _cli_parse_args: bool | list[str] | tuple[str, ...] | None = None, + _cli_settings_source: CliSettingsSource[Any] | None = None, + _cli_parse_none_str: str | None = None, + _cli_hide_none_type: bool | None = None, + _cli_avoid_json: bool | None = None, + _cli_enforce_required: bool | None = None, + _cli_use_class_docs_for_groups: bool | None = None, + _cli_show_env_vars: bool | None = None, + _cli_exit_on_error: bool | None = None, + _cli_prefix: str | None = None, + _cli_flag_prefix_char: str | None = None, + _cli_implicit_flags: bool | Literal['dual', 'toggle'] | None = None, + _cli_ignore_unknown_args: bool | None = None, + _cli_kebab_case: bool | Literal['all', 'no_enums'] | None = None, + _cli_shortcuts: Mapping[str, str | list[str]] | None = None, + _secrets_dir: PathType | None = None, + _init_kwargs: dict[str, Any] | None = None, + ) -> tuple[tuple[PydanticBaseSettingsSource, ...], dict[str, Any]]: + # Determine settings config values + case_sensitive = _case_sensitive if _case_sensitive is not None else cls.model_config.get('case_sensitive') + env_prefix = _env_prefix if _env_prefix is not None else cls.model_config.get('env_prefix') + env_prefix_target = ( + _env_prefix_target if _env_prefix_target is not None else cls.model_config.get('env_prefix_target') + ) + nested_model_default_partial_update = ( + _nested_model_default_partial_update + if _nested_model_default_partial_update is not None + else cls.model_config.get('nested_model_default_partial_update') + ) + env_file = _env_file if _env_file != ENV_FILE_SENTINEL else cls.model_config.get('env_file') + env_file_encoding = ( + _env_file_encoding if _env_file_encoding is not None else cls.model_config.get('env_file_encoding') + ) + env_ignore_empty = ( + _env_ignore_empty if _env_ignore_empty is not None else cls.model_config.get('env_ignore_empty') + ) + env_nested_delimiter = ( + _env_nested_delimiter if _env_nested_delimiter is not None else cls.model_config.get('env_nested_delimiter') + ) + env_nested_max_split = ( + _env_nested_max_split if _env_nested_max_split is not None else cls.model_config.get('env_nested_max_split') + ) + env_parse_none_str = ( + _env_parse_none_str if _env_parse_none_str is not None else cls.model_config.get('env_parse_none_str') + ) + env_parse_enums = _env_parse_enums if _env_parse_enums is not None else cls.model_config.get('env_parse_enums') + + cli_prog_name = _cli_prog_name if _cli_prog_name is not None else cls.model_config.get('cli_prog_name') + cli_parse_args = _cli_parse_args if _cli_parse_args is not None else cls.model_config.get('cli_parse_args') + cli_settings_source = ( + _cli_settings_source if _cli_settings_source is not None else cls.model_config.get('cli_settings_source') + ) + cli_parse_none_str = ( + _cli_parse_none_str if _cli_parse_none_str is not None else cls.model_config.get('cli_parse_none_str') + ) + cli_parse_none_str = cli_parse_none_str if not env_parse_none_str else env_parse_none_str + cli_hide_none_type = ( + _cli_hide_none_type if _cli_hide_none_type is not None else cls.model_config.get('cli_hide_none_type') + ) + cli_avoid_json = _cli_avoid_json if _cli_avoid_json is not None else cls.model_config.get('cli_avoid_json') + cli_enforce_required = ( + _cli_enforce_required if _cli_enforce_required is not None else cls.model_config.get('cli_enforce_required') + ) + cli_use_class_docs_for_groups = ( + _cli_use_class_docs_for_groups + if _cli_use_class_docs_for_groups is not None + else cls.model_config.get('cli_use_class_docs_for_groups') + ) + cli_show_env_vars = ( + _cli_show_env_vars if _cli_show_env_vars is not None else cls.model_config.get('cli_show_env_vars') + ) + cli_exit_on_error = ( + _cli_exit_on_error if _cli_exit_on_error is not None else cls.model_config.get('cli_exit_on_error') + ) + cli_prefix = _cli_prefix if _cli_prefix is not None else cls.model_config.get('cli_prefix') + cli_flag_prefix_char = ( + _cli_flag_prefix_char if _cli_flag_prefix_char is not None else cls.model_config.get('cli_flag_prefix_char') + ) + cli_implicit_flags = ( + _cli_implicit_flags if _cli_implicit_flags is not None else cls.model_config.get('cli_implicit_flags') + ) + cli_ignore_unknown_args = ( + _cli_ignore_unknown_args + if _cli_ignore_unknown_args is not None + else cls.model_config.get('cli_ignore_unknown_args') + ) + cli_kebab_case = _cli_kebab_case if _cli_kebab_case is not None else cls.model_config.get('cli_kebab_case') + cli_shortcuts = _cli_shortcuts if _cli_shortcuts is not None else cls.model_config.get('cli_shortcuts') + + secrets_dir = _secrets_dir if _secrets_dir is not None else cls.model_config.get('secrets_dir') + + # Configure built-in sources, sharing the same init state so that incomplete + # fields are only warned about once per settings resolution. + init_state: InitState = {'field_info_ids': set()} + default_settings = DefaultSettingsSource( + cls, nested_model_default_partial_update=nested_model_default_partial_update, _init_state=init_state + ) + init_settings = InitSettingsSource( + cls, + init_kwargs=_init_kwargs if _init_kwargs is not None else {}, + nested_model_default_partial_update=nested_model_default_partial_update, + _init_state=default_settings._init_state, + ) + env_settings = EnvSettingsSource( + cls, + case_sensitive=case_sensitive, + env_prefix=env_prefix, + env_prefix_target=env_prefix_target, + env_nested_delimiter=env_nested_delimiter, + env_nested_max_split=env_nested_max_split, + env_ignore_empty=env_ignore_empty, + env_parse_none_str=env_parse_none_str, + env_parse_enums=env_parse_enums, + _init_state=init_settings._init_state, + ) + dotenv_settings = DotEnvSettingsSource( + cls, + env_file=env_file, + env_file_encoding=env_file_encoding, + case_sensitive=case_sensitive, + env_prefix=env_prefix, + env_prefix_target=env_prefix_target, + env_nested_delimiter=env_nested_delimiter, + env_nested_max_split=env_nested_max_split, + env_ignore_empty=env_ignore_empty, + env_parse_none_str=env_parse_none_str, + env_parse_enums=env_parse_enums, + _init_state=env_settings._init_state, + ) + + file_secret_settings = SecretsSettingsSource( + cls, + secrets_dir=secrets_dir, + case_sensitive=case_sensitive, + env_prefix=env_prefix, + env_prefix_target=env_prefix_target, + _init_state=dotenv_settings._init_state, + ) + # Provide a hook to set built-in sources priority and add / remove sources + sources = cls.settings_customise_sources( + cls, + init_settings=init_settings, + env_settings=env_settings, + dotenv_settings=dotenv_settings, + file_secret_settings=file_secret_settings, + ) + (default_settings,) + custom_cli_sources = [source for source in sources if isinstance(source, CliSettingsSource)] + if not any(custom_cli_sources): + if isinstance(cli_settings_source, CliSettingsSource): + sources = (cli_settings_source,) + sources + elif cli_parse_args is not None: + cli_settings = CliSettingsSource[Any]( + cls, + _init_state=file_secret_settings._init_state, + cli_prog_name=cli_prog_name, + cli_parse_args=cli_parse_args, + cli_parse_none_str=cli_parse_none_str, + cli_hide_none_type=cli_hide_none_type, + cli_avoid_json=cli_avoid_json, + cli_enforce_required=cli_enforce_required, + cli_use_class_docs_for_groups=cli_use_class_docs_for_groups, + cli_show_env_vars=cli_show_env_vars, + cli_exit_on_error=cli_exit_on_error, + cli_prefix=cli_prefix, + cli_flag_prefix_char=cli_flag_prefix_char, + cli_implicit_flags=cli_implicit_flags, + cli_ignore_unknown_args=cli_ignore_unknown_args, + cli_kebab_case=cli_kebab_case, + cli_shortcuts=cli_shortcuts, + case_sensitive=case_sensitive, + _env_settings_source=env_settings, + ) + sources = (cli_settings,) + sources + # We ensure that if command line arguments haven't been parsed yet, we do so. + elif cli_parse_args not in (None, False) and not custom_cli_sources[0].env_vars: + custom_cli_sources[0](args=cli_parse_args) + + cls._settings_warn_unused_config_keys(sources, cls.model_config) + + return sources, _init_kwargs if _init_kwargs is not None else {} + + @classmethod + def _settings_build_values( + cls, sources: tuple[PydanticBaseSettingsSource, ...], init_kwargs: dict[str, Any] + ) -> dict[str, Any]: + if sources: + state: dict[str, Any] = {} + defaults: dict[str, Any] = {} + states: dict[str, dict[str, Any]] = {} + for source in sources: + if isinstance(source, PydanticBaseSettingsSource): + source._set_current_state(state) + source._set_settings_sources_data(states) + + source_name = source.__name__ if hasattr(source, '__name__') else type(source).__name__ + source_state = source() + + if isinstance(source, DefaultSettingsSource): + defaults = source_state + + states[source_name] = source_state + state = deep_update(source_state, state) + + # Strip any default values not explicitly set before returning final state + state = {key: val for key, val in state.items() if key not in defaults or defaults[key] != val} + + if _settings_debug_enabled() and logger.isEnabledFor(logging.DEBUG): + cls._settings_log_debug(states) + # The last source is the `DefaultSettingsSource` instance created in `_settings_init_sources()`, + # holding the init state shared by all built-in sources: + last_source = sources[-1] + init_state = last_source._init_state if isinstance(last_source, PydanticBaseSettingsSource) else InitState() + cls._settings_restore_init_kwarg_names(cls, init_kwargs, state, init_state) + + return state + else: + # no one should mean to do this, but I think returning an empty dict is marginally preferable + # to an informative error and much better than a confusing error + return {} + + @classmethod + def _settings_log_debug(cls, states: dict[str, dict[str, Any]]) -> None: + """Log the data collected by each settings source in priority order. + + Enabled by setting the ``PYDANTIC_SETTINGS_DEBUG`` environment variable to a truthy value. + This is emitted at ``DEBUG`` level on the ``pydantic_settings`` logger. + + Warning: the output may contain sensitive values loaded from the environment, dotenv + files, or secret files. Only enable it in a trusted debugging context. + """ + lines = [f'Resolving settings for {cls.__name__!r} (sources in priority order, highest first):'] + for name, source_state in states.items(): + lines.append(f' {name}: {source_state!r}') + logger.debug('\n'.join(lines)) + + @staticmethod + def _settings_restore_init_kwarg_names( + settings_cls: type[BaseSettings], + init_kwargs: dict[str, Any], + state: dict[str, Any], + init_state: InitState, + ) -> None: + """ + Restore the init_kwarg key names to the final merged state dictionary. + + This function renames keys in state to match the original init_kwargs key names, + preserving the merged values from the source priority order. + """ + if init_kwargs and state: + state_kwarg_names = set(state.keys()) + init_kwarg_names = set(init_kwargs.keys()) + for field_name, field_info in settings_cls.model_fields.items(): + _warn_if_field_info_incomplete(field_info, field_name, init_state) + alias_names, *_ = _get_alias_names(field_name, field_info) + matchable_names = set(alias_names) + include_name = settings_cls.model_config.get( + 'populate_by_name', False + ) or settings_cls.model_config.get('validate_by_name', False) + if include_name: + matchable_names.add(field_name) + init_kwarg_name = init_kwarg_names & matchable_names + state_kwarg_name = state_kwarg_names & matchable_names + if init_kwarg_name and state_kwarg_name: + # Use deterministic selection for both keys. + # Target key: the key from init_kwargs that should be used in the final state. + target_key = next(iter(init_kwarg_name)) + # Source key: prefer the alias (first in alias_names) if present in state, + # as InitSettingsSource normalizes to the preferred alias. + # This ensures we get the highest-priority value for this field. + source_key = None + for alias in alias_names: + if alias in state_kwarg_name: + source_key = alias + break + if source_key is None: + # Fall back to field_name if no alias found in state + source_key = field_name if field_name in state_kwarg_name else next(iter(state_kwarg_name)) + # Get the value from the source key and remove all matching keys + value = state.pop(source_key) + for key in state_kwarg_name - {source_key}: + state.pop(key, None) + state[target_key] = value + + @staticmethod + def _settings_warn_unused_config_keys(sources: tuple[object, ...], model_config: SettingsConfigDict) -> None: + """ + Warns if any values in model_config were set but the corresponding settings source has not been initialised. + + The list alternative sources and their config keys can be found here: + https://docs.pydantic.dev/latest/concepts/pydantic_settings/#other-settings-source + + Args: + sources: The tuple of configured sources + model_config: The model config to check for unused config keys + """ + + def warn_if_not_used(source_type: type[PydanticBaseSettingsSource], keys: tuple[str, ...]) -> None: + if not any(isinstance(source, source_type) for source in sources): + for key in keys: + if model_config.get(key) is not None: + warnings.warn( + f'Config key `{key}` is set in model_config but will be ignored because no ' + f'{source_type.__name__} source is configured. To use this config key, add a ' + f'{source_type.__name__} source to the settings sources via the ' + 'settings_customise_sources hook.', + UserWarning, + stacklevel=3, + ) + + warn_if_not_used(JsonConfigSettingsSource, ('json_file', 'json_file_encoding')) + warn_if_not_used(PyprojectTomlConfigSettingsSource, ('pyproject_toml_depth', 'pyproject_toml_table_header')) + warn_if_not_used(TomlConfigSettingsSource, ('toml_file', 'toml_table_header')) + warn_if_not_used(YamlConfigSettingsSource, ('yaml_file', 'yaml_file_encoding', 'yaml_config_section')) + + model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict( + extra='forbid', + arbitrary_types_allowed=True, + validate_default=True, + case_sensitive=False, + env_prefix='', + env_prefix_target='variable', + nested_model_default_partial_update=False, + env_file=None, + env_file_encoding=None, + env_ignore_empty=False, + env_nested_delimiter=None, + env_nested_max_split=None, + env_parse_none_str=None, + env_parse_enums=None, + cli_prog_name=None, + cli_parse_args=None, + cli_parse_none_str=None, + cli_hide_none_type=False, + cli_avoid_json=False, + cli_enforce_required=False, + cli_use_class_docs_for_groups=False, + cli_show_env_vars=False, + cli_exit_on_error=True, + cli_prefix='', + cli_flag_prefix_char='-', + cli_implicit_flags=False, + cli_ignore_unknown_args=False, + cli_kebab_case=False, + cli_shortcuts=None, + json_file=None, + json_file_encoding=None, + yaml_file=None, + yaml_file_encoding=None, + yaml_config_section=None, + toml_file=None, + secrets_dir=None, + protected_namespaces=('model_validate', 'model_dump', 'settings_customise_sources'), + enable_decoding=True, + ) + + +class CliApp: + """ + A utility class for running Pydantic `BaseSettings`, `BaseModel`, or `pydantic.dataclasses.dataclass` as + CLI applications. + """ + + _subcommand_stack: ClassVar[dict[int, tuple[CliSettingsSource[Any], Any, str]]] = {} + _ansi_color: ClassVar[re.Pattern[str]] = re.compile(r'\x1b\[[0-9;]*m') + + @staticmethod + def _get_base_settings_cls(model_cls: type[Any]) -> type[BaseSettings]: + if issubclass(model_cls, BaseSettings): + return model_cls + + class CliAppBaseSettings(BaseSettings, model_cls): # type: ignore + __doc__ = model_cls.__doc__ + model_config = SettingsConfigDict( + nested_model_default_partial_update=True, + case_sensitive=True, + cli_hide_none_type=True, + cli_avoid_json=True, + cli_enforce_required=True, + cli_implicit_flags=True, + cli_kebab_case=True, + ) + + return CliAppBaseSettings + + @staticmethod + def _run_cli_cmd(model: Any, cli_cmd_method_name: str, is_required: bool) -> Any: + command = getattr(type(model), cli_cmd_method_name, None) + if command is None: + if is_required: + raise SettingsError(f'Error: {type(model).__name__} class is missing {cli_cmd_method_name} entrypoint') + return model + + # If the method is asynchronous, we handle its execution based on the current event loop status. + if inspect.iscoroutinefunction(command): + # For asynchronous methods, we have two execution scenarios: + # 1. If no event loop is running in the current thread, run the coroutine directly with asyncio.run(). + # 2. If an event loop is already running in the current thread, run the coroutine in a separate thread to avoid conflicts. + try: + # Check if an event loop is currently running in this thread. + loop = asyncio.get_running_loop() + except RuntimeError: + loop = None + + if loop and loop.is_running(): + # We're in a context with an active event loop (e.g., Jupyter Notebook). + # Running asyncio.run() here would cause conflicts, so we use a separate thread. + exception_container = [] + + def run_coro() -> None: + try: + # Execute the coroutine in a new event loop in this separate thread. + asyncio.run(command(model)) + except Exception as e: + exception_container.append(e) + + thread = threading.Thread(target=run_coro) + thread.start() + thread.join() + if exception_container: + # Propagate exceptions from the separate thread. + raise exception_container[0] + else: + # No event loop is running; safe to run the coroutine directly. + asyncio.run(command(model)) + else: + # For synchronous methods, call them directly. + command(model) + + return model + + @staticmethod + def run( + model_cls: type[T], + cli_args: list[str] | Namespace | SimpleNamespace | dict[str, Any] | None = None, + cli_settings_source: CliSettingsSource[Any] | None = None, + cli_exit_on_error: bool | None = None, + cli_show_env_vars: bool | None = None, + cli_cmd_method_name: str = 'cli_cmd', + **model_init_data: Any, + ) -> T: + """ + Runs a Pydantic `BaseSettings`, `BaseModel`, or `pydantic.dataclasses.dataclass` as a CLI application. + Running a model as a CLI application requires the `cli_cmd` method to be defined in the model class. + + Args: + model_cls: The model class to run as a CLI application. + cli_args: The list of CLI arguments to parse. If `cli_settings_source` is specified, this may + also be a namespace or dictionary of pre-parsed CLI arguments. Defaults to `sys.argv[1:]`. + cli_settings_source: Override the default CLI settings source with a user defined instance. + Defaults to `None`. + cli_exit_on_error: Determines whether this function exits on error. If model is subclass of + `BaseSettings`, defaults to BaseSettings `cli_exit_on_error` value. Otherwise, defaults to + `True`. + cli_show_env_vars: Show resolved environment variable names in CLI help text. Defaults to `False`. + cli_cmd_method_name: The CLI command method name to run. Defaults to "cli_cmd". + model_init_data: The model init data. + + Returns: + The ran instance of model. + + Raises: + SettingsError: If model_cls is not subclass of `BaseModel` or `pydantic.dataclasses.dataclass`. + SettingsError: If model_cls does not have a `cli_cmd` entrypoint defined. + """ + + if not (is_pydantic_dataclass(model_cls) or is_model_class(model_cls)): + raise SettingsError( + f'Error: {model_cls.__name__} is not subclass of BaseModel or pydantic.dataclasses.dataclass' + ) + + cli_settings = None + cli_parse_args = True if cli_args is None else cli_args + if cli_settings_source is not None: + if isinstance(cli_parse_args, (Namespace, SimpleNamespace, dict)): + cli_settings = cli_settings_source(parsed_args=cli_parse_args) + else: + cli_settings = cli_settings_source(args=cli_parse_args) + elif isinstance(cli_parse_args, (Namespace, SimpleNamespace, dict)): + raise SettingsError('Error: `cli_args` must be list[str] or None when `cli_settings_source` is not used') + + if not issubclass(model_cls, BaseSettings): + base_settings_cls = CliApp._get_base_settings_cls(model_cls) + sources, init_kwargs = base_settings_cls._settings_init_sources( + _cli_parse_args=cli_parse_args, # type: ignore[arg-type] + _cli_exit_on_error=cli_exit_on_error, + _cli_show_env_vars=cli_show_env_vars, + _cli_settings_source=cli_settings, + _init_kwargs=model_init_data, + ) + model = base_settings_cls(**base_settings_cls._settings_build_values(sources, init_kwargs)) + model_init_data = {} + for field_name, field_info in base_settings_cls.model_fields.items(): + model_init_data[_field_name_for_signature(field_name, field_info)] = getattr(model, field_name) + command = model_cls(**model_init_data) + else: + sources, init_kwargs = model_cls._settings_init_sources( + _cli_parse_args=cli_parse_args, # type: ignore[arg-type] + _cli_exit_on_error=cli_exit_on_error, + _cli_show_env_vars=cli_show_env_vars, + _cli_settings_source=cli_settings, + _init_kwargs=model_init_data, + ) + command = model_cls(_build_sources=(sources, init_kwargs)) + + subcommand_dest = ':subcommand' + cli_settings_source = next(source for source in sources if isinstance(source, CliSettingsSource)) + CliApp._subcommand_stack[id(command)] = (cli_settings_source, cli_settings_source.root_parser, subcommand_dest) + try: + data_model = CliApp._run_cli_cmd(command, cli_cmd_method_name, is_required=False) + finally: + del CliApp._subcommand_stack[id(command)] + return data_model + + @staticmethod + def run_subcommand( + model: PydanticModel, + cli_exit_on_error: bool | None = None, + cli_show_env_vars: bool | None = None, + cli_cmd_method_name: str = 'cli_cmd', + ) -> PydanticModel: + """ + Runs the model subcommand. Running a model subcommand requires the `cli_cmd` method to be defined in + the nested model subcommand class. + + Args: + model: The model to run the subcommand from. + cli_exit_on_error: Determines whether this function exits with error if no subcommand is found. + Defaults to model_config `cli_exit_on_error` value if set. Otherwise, defaults to `True`. + cli_show_env_vars: Show resolved environment variable names in CLI help text. Defaults to `False`. + cli_cmd_method_name: The CLI command method name to run. Defaults to "cli_cmd". + + Returns: + The ran subcommand model. + + Raises: + SystemExit: When no subcommand is found and cli_exit_on_error=`True` (the default). + SettingsError: When no subcommand is found and cli_exit_on_error=`False`. + """ + + if id(model) in CliApp._subcommand_stack: + cli_settings_source, parser, subcommand_dest = CliApp._subcommand_stack[id(model)] + else: + cli_settings_source = CliSettingsSource[Any]( + CliApp._get_base_settings_cls(type(model)), + cli_show_env_vars=cli_show_env_vars, + ) + parser = cli_settings_source.root_parser + subcommand_dest = ':subcommand' + + cli_exit_on_error = cli_settings_source.cli_exit_on_error if cli_exit_on_error is None else cli_exit_on_error + + errors: list[SettingsError | SystemExit] = [] + subcommand = get_subcommand( + model, is_required=True, cli_exit_on_error=cli_exit_on_error, _suppress_errors=errors + ) + if errors: + err = errors[0] + if err.__context__ is None and err.__cause__ is None and cli_settings_source._format_help is not None: + error_message = f'{err}\n{cli_settings_source._format_help(parser)}' + raise type(err)(error_message) from None + else: + raise err + + subcommand_cls = cast(type[BaseModel], type(subcommand)) + subcommand_arg = cli_settings_source._parser_map[subcommand_dest][subcommand_cls] + subcommand_dest = f'{subcommand_arg.dest}.:subcommand' + subcommand_parser = subcommand_arg.parser + CliApp._subcommand_stack[id(subcommand)] = (cli_settings_source, subcommand_parser, subcommand_dest) + try: + data_model = CliApp._run_cli_cmd(subcommand, cli_cmd_method_name, is_required=True) + finally: + del CliApp._subcommand_stack[id(subcommand)] + return data_model + + @staticmethod + def serialize( + model: PydanticModel, + list_style: Literal['json', 'argparse', 'lazy'] = 'json', + dict_style: Literal['json', 'env'] = 'json', + positionals_first: bool = False, + ) -> list[str]: + """ + Serializes the CLI arguments for a Pydantic data model. + + Args: + model: The data model to serialize. + list_style: + Controls how list-valued fields are serialized on the command line. + - 'json' (default): + Lists are encoded as a single JSON array. + Example: `--tags '["a","b","c"]'` + - 'argparse': + Each list element becomes its own repeated flag, following + typical `argparse` conventions. + Example: `--tags a --tags b --tags c` + - 'lazy': + Lists are emitted as a single comma-separated string without JSON + quoting or escaping. + Example: `--tags a,b,c` + dict_style: + Controls how dictionary-valued fields are serialized. + - 'json' (default): + The entire dictionary is emitted as a single JSON object. + Example: `--config '{"host": "localhost", "port": 5432}'` + - 'env': + The dictionary is flattened into multiple CLI flags using + environment-variable-style assignment. + Example: `--config host=localhost --config port=5432` + positionals_first: Controls whether positional arguments should be serialized + first compared to optional arguments. Defaults to `False`. + + Returns: + The serialized CLI arguments for the data model. + """ + + base_settings_cls = CliApp._get_base_settings_cls(type(model)) + serialized_args = CliSettingsSource[Any](base_settings_cls)._serialized_args( + model, + list_style=list_style, + dict_style=dict_style, + positionals_first=positionals_first, + ) + return CliSettingsSource._flatten_serialized_args(serialized_args, positionals_first) + + @staticmethod + def format_help( + model: PydanticModel | type[T], + cli_settings_source: CliSettingsSource[Any] | None = None, + strip_ansi_color: bool = False, + ) -> str: + """ + Return a string containing a help message for a Pydantic model. + + Args: + model: The model or model class. + cli_settings_source: Override the default CLI settings source with a user defined instance. + Defaults to `None`. + strip_ansi_color: Strips ANSI color codes from the help message when set to `True`. + + Returns: + The help message string for the model. + """ + model_cls = model if isinstance(model, type) else type(model) + if cli_settings_source is None: + if not isinstance(model, type) and id(model) in CliApp._subcommand_stack: + cli_settings_source, *_ = CliApp._subcommand_stack[id(model)] + else: + cli_settings_source = CliSettingsSource(CliApp._get_base_settings_cls(model_cls)) + help_message = cli_settings_source._format_help(cli_settings_source.root_parser) + return help_message if not strip_ansi_color else CliApp._ansi_color.sub('', help_message) + + @staticmethod + def print_help( + model: PydanticModel | type[T], + cli_settings_source: CliSettingsSource[Any] | None = None, + file: TextIO | None = None, + strip_ansi_color: bool = False, + ) -> None: + """ + Print a help message for a Pydantic model. + + Args: + model: The model or model class. + cli_settings_source: Override the default CLI settings source with a user defined instance. + Defaults to `None`. + file: A text stream to which the help message is written. If `None`, the output is sent to sys.stdout. + strip_ansi_color: Strips ANSI color codes from the help message when set to `True`. + """ + print( + CliApp.format_help( + model, + cli_settings_source=cli_settings_source, + strip_ansi_color=strip_ansi_color, + ), + file=file, + ) diff --git a/source/target-vendors/cp312/pydantic_settings/py.typed b/source/target-vendors/cp312/pydantic_settings/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/pydantic_settings/sources/__init__.py b/source/target-vendors/cp312/pydantic_settings/sources/__init__.py new file mode 100644 index 0000000..a9afe6e --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/sources/__init__.py @@ -0,0 +1,88 @@ +"""Package for handling configuration sources in pydantic-settings.""" + +from .base import ( + ConfigFileSourceMixin, + DefaultSettingsSource, + InitSettingsSource, + PydanticBaseEnvSettingsSource, + PydanticBaseSettingsSource, + get_subcommand, +) +from .providers.aws import AWSSecretsManagerSettingsSource +from .providers.azure import AzureKeyVaultSettingsSource +from .providers.cli import ( + CLI_SUPPRESS, + CliDualFlag, + CliExplicitFlag, + CliImplicitFlag, + CliMutuallyExclusiveGroup, + CliPositionalArg, + CliSettingsSource, + CliSubCommand, + CliSuppress, + CliToggleFlag, + CliUnknownArgs, +) +from .providers.dotenv import DotEnvSettingsSource, read_env_file +from .providers.env import EnvSettingsSource +from .providers.gcp import GoogleSecretManagerSettingsSource +from .providers.json import JsonConfigSettingsSource +from .providers.nested_secrets import NestedSecretsSettingsSource +from .providers.pyproject import PyprojectTomlConfigSettingsSource +from .providers.secrets import SecretsSettingsSource +from .providers.toml import TomlConfigSettingsSource +from .providers.yaml import YamlConfigSettingsSource +from .types import ( + DEFAULT_PATH, + ENV_FILE_SENTINEL, + ConfigFileSourceType, + DotenvFiltering, + DotenvType, + EnvPrefixTarget, + ForceDecode, + NoDecode, + PathType, + PydanticModel, +) + +__all__ = [ + 'CLI_SUPPRESS', + 'DEFAULT_PATH', + 'ENV_FILE_SENTINEL', + 'AWSSecretsManagerSettingsSource', + 'AzureKeyVaultSettingsSource', + 'CliDualFlag', + 'CliExplicitFlag', + 'CliImplicitFlag', + 'CliMutuallyExclusiveGroup', + 'CliPositionalArg', + 'CliSettingsSource', + 'CliSubCommand', + 'CliSuppress', + 'CliToggleFlag', + 'CliUnknownArgs', + 'ConfigFileSourceMixin', + 'ConfigFileSourceType', + 'DefaultSettingsSource', + 'DotEnvSettingsSource', + 'DotenvFiltering', + 'DotenvType', + 'EnvPrefixTarget', + 'EnvSettingsSource', + 'ForceDecode', + 'GoogleSecretManagerSettingsSource', + 'InitSettingsSource', + 'JsonConfigSettingsSource', + 'NestedSecretsSettingsSource', + 'NoDecode', + 'PathType', + 'PydanticBaseEnvSettingsSource', + 'PydanticBaseSettingsSource', + 'PydanticModel', + 'PyprojectTomlConfigSettingsSource', + 'SecretsSettingsSource', + 'TomlConfigSettingsSource', + 'YamlConfigSettingsSource', + 'get_subcommand', + 'read_env_file', +] diff --git a/source/target-vendors/cp312/pydantic_settings/sources/base.py b/source/target-vendors/cp312/pydantic_settings/sources/base.py new file mode 100644 index 0000000..9d73d14 --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/sources/base.py @@ -0,0 +1,639 @@ +"""Base classes and core functionality for pydantic-settings sources.""" + +from __future__ import annotations as _annotations + +import json +from abc import ABC, abstractmethod +from collections.abc import Sequence +from dataclasses import asdict, is_dataclass +from pathlib import Path +from typing import TYPE_CHECKING, Any, cast, get_args + +from pydantic import AliasChoices, AliasPath, BaseModel, TypeAdapter +from pydantic._internal._typing_extra import ( # type: ignore[attr-defined] + get_origin, +) +from pydantic._internal._utils import deep_update, is_model_class +from pydantic.fields import FieldInfo +from typing_inspection.introspection import is_union_origin + +from ..exceptions import SettingsError +from ..utils import _lenient_issubclass +from .types import ( + ConfigFileSourceType, + EnvNoneType, + EnvPrefixTarget, + ForceDecode, + NoDecode, + PydanticModel, + _CliSubCommand, +) +from .utils import ( + InitState, + _annotation_is_complex, + _get_alias_names, + _get_field_metadata, + _get_model_fields, + _resolve_type_alias, + _strip_annotated, + _union_is_complex, + _warn_if_field_info_incomplete, +) + +if TYPE_CHECKING: + from pydantic_settings.main import BaseSettings + + from .types import Traversable + + +def get_subcommand( + model: PydanticModel, + is_required: bool = True, + cli_exit_on_error: bool | None = None, + _suppress_errors: list[SettingsError | SystemExit] | None = None, +) -> PydanticModel | None: + """ + Get the subcommand from a model. + + Args: + model: The model to get the subcommand from. + is_required: Determines whether a model must have subcommand set and raises error if not + found. Defaults to `True`. + cli_exit_on_error: Determines whether this function exits with error if no subcommand is found. + Defaults to model_config `cli_exit_on_error` value if set. Otherwise, defaults to `True`. + + Returns: + The subcommand model if found, otherwise `None`. + + Raises: + SystemExit: When no subcommand is found and is_required=`True` and cli_exit_on_error=`True` + (the default). + SettingsError: When no subcommand is found and is_required=`True` and + cli_exit_on_error=`False`. + """ + + model_cls = type(model) + if cli_exit_on_error is None and is_model_class(model_cls): + model_default = model_cls.model_config.get('cli_exit_on_error') + if isinstance(model_default, bool): + cli_exit_on_error = model_default + if cli_exit_on_error is None: + cli_exit_on_error = True + + subcommands: list[str] = [] + for field_name, field_info in _get_model_fields(model_cls).items(): + if _CliSubCommand in field_info.metadata: + if getattr(model, field_name) is not None: + return getattr(model, field_name) + subcommands.append(field_name) + # TODO: we could warn if no sub command was found and one of `field_info`s + # isn't complete. + + if is_required: + error_message = ( + f'Error: CLI subcommand is required {{{", ".join(subcommands)}}}' + if subcommands + else 'Error: CLI subcommand is required but no subcommands were found.' + ) + err = SystemExit(error_message) if cli_exit_on_error else SettingsError(error_message) + if _suppress_errors is None: + raise err + _suppress_errors.append(err) + + return None + + +class PydanticBaseSettingsSource(ABC): + """ + Abstract base class for settings sources, every settings source classes should inherit from it. + """ + + def __init__(self, settings_cls: type[BaseSettings], _init_state: InitState | None = None): + self._init_state: InitState = {} if _init_state is None else _init_state + self.settings_cls = settings_cls + self.config = settings_cls.model_config + self._current_state: dict[str, Any] = {} + self._settings_sources_data: dict[str, dict[str, Any]] = {} + + def _set_current_state(self, state: dict[str, Any]) -> None: + """ + Record the state of settings from the previous settings sources. This should + be called right before __call__. + """ + self._current_state = state + + def _set_settings_sources_data(self, states: dict[str, dict[str, Any]]) -> None: + """ + Record the state of settings from all previous settings sources. This should + be called right before __call__. + """ + self._settings_sources_data = states + + @property + def current_state(self) -> dict[str, Any]: + """ + The current state of the settings, populated by the previous settings sources. + """ + return self._current_state + + @property + def settings_sources_data(self) -> dict[str, dict[str, Any]]: + """ + The state of all previous settings sources. + """ + return self._settings_sources_data + + @abstractmethod + def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: + """ + Gets the value, the key for model creation, and a flag to determine whether value is complex. + + This is an abstract method that should be overridden in every settings source classes. + + Args: + field: The field. + field_name: The field name. + + Returns: + A tuple that contains the value, key and a flag to determine whether value is complex. + """ + + def field_is_complex(self, field: FieldInfo) -> bool: + """ + Checks whether a field is complex, in which case it will attempt to be parsed as JSON. + + Args: + field: The field. + + Returns: + Whether the field is complex. + """ + return _annotation_is_complex(field.annotation, field.metadata, self._init_state) + + def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any: + """ + Prepares the value of a field. + + Args: + field_name: The field name. + field: The field. + value: The value of the field that has to be prepared. + value_is_complex: A flag to determine whether value is complex. + + Returns: + The prepared value. + """ + if value is not None and (self.field_is_complex(field) or value_is_complex): + return self.decode_complex_value(field_name, field, value) + return value + + def decode_complex_value(self, field_name: str, field: FieldInfo, value: Any) -> Any: + """ + Decode the value for a complex field + + Args: + field_name: The field name. + field: The field. + value: The value of the field that has to be prepared. + + Returns: + The decoded value for further preparation + """ + if field and ( + NoDecode in _get_field_metadata(field) + or (self.config.get('enable_decoding') is False and ForceDecode not in field.metadata) + ): + return value + + return json.loads(value) + + @abstractmethod + def __call__(self) -> dict[str, Any]: + pass + + +class ConfigFileSourceMixin(ABC): + def _read_files(self, files: ConfigFileSourceType | None, deep_merge: bool = False) -> dict[str, Any]: + if files is None: + return {} + if not isinstance(files, Sequence) or isinstance(files, str): + files = [files] + vars: dict[str, Any] = {} + for file in files: + file_path: Path | Traversable = Path(file) if isinstance(file, str) else file + if isinstance(file_path, Path): + file_path = file_path.expanduser() + + if not file_path.is_file(): + continue + + updating_vars = self._read_file(file_path) + if deep_merge: + vars = deep_update(vars, updating_vars) + else: + vars.update(updating_vars) + return vars + + @abstractmethod + def _read_file(self, path: Path | Traversable) -> dict[str, Any]: + pass + + +def _unwrap_optional_annotation(annotation: Any) -> Any: + """Return the inner type of an ``Optional[T]`` annotation, otherwise the annotation unchanged.""" + if is_union_origin(get_origin(annotation)): + args = get_args(annotation) + if len(args) == 2 and type(None) in args: + for arg in args: + if arg is not type(None): + return arg + return annotation + + +def _has_discriminator(field_info: FieldInfo) -> bool: + """Check if a field uses a discriminated union (via Annotated or Field(discriminator=...)).""" + if field_info.discriminator is not None: + return True + from pydantic import Discriminator + + return any(isinstance(m, Discriminator) for m in field_info.metadata) + + +class DefaultSettingsSource(PydanticBaseSettingsSource): + """ + Source class for loading default object values. + + Args: + settings_cls: The Settings class. + nested_model_default_partial_update: Whether to allow partial updates on nested model default object fields. + Defaults to `False`. + """ + + def __init__( + self, + settings_cls: type[BaseSettings], + nested_model_default_partial_update: bool | None = None, + _init_state: InitState | None = None, + ): + super().__init__(settings_cls, _init_state) + self.defaults: dict[str, Any] = {} + self.nested_model_default_partial_update = ( + nested_model_default_partial_update + if nested_model_default_partial_update is not None + else self.config.get('nested_model_default_partial_update', False) + ) + if self.nested_model_default_partial_update: + for field_name, field_info in settings_cls.model_fields.items(): + _warn_if_field_info_incomplete(field_info, field_name, self._init_state) + if _has_discriminator(field_info): + continue + alias_names, *_ = _get_alias_names(field_name, field_info) + preferred_alias = alias_names[0] + if is_dataclass(type(field_info.default)): + self.defaults[preferred_alias] = asdict(field_info.default) + elif is_model_class(type(field_info.default)): + self.defaults[preferred_alias] = field_info.default.model_dump() + + def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: + # Nothing to do here. Only implement the return statement to make mypy happy + return None, '', False + + def __call__(self) -> dict[str, Any]: + return self.defaults + + def __repr__(self) -> str: + return ( + f'{self.__class__.__name__}(nested_model_default_partial_update={self.nested_model_default_partial_update})' + ) + + +class InitSettingsSource(PydanticBaseSettingsSource): + """ + Source class for loading values provided during settings class initialization. + """ + + def __init__( + self, + settings_cls: type[BaseSettings], + init_kwargs: dict[str, Any], + nested_model_default_partial_update: bool | None = None, + _init_state: InitState | None = None, + ): + super().__init__(settings_cls, _init_state) + case_sensitive = self.config.get('case_sensitive', False) + include_name = self.config.get('populate_by_name', False) or self.config.get('validate_by_name', False) + + def normalize(name: str) -> str: + # When case_sensitive is False, matching is done on lower-cased names. + return name if case_sensitive else name.lower() + + self.init_kwargs = {} + init_kwarg_names = set(init_kwargs.keys()) + # Map each normalized name to all provided keys sharing it, so field aliases can + # be matched without losing the original key used to look up the value. A 1-to-many + # mapping is required because case_sensitive=False can collapse distinct keys + # (e.g. ``TeSt`` and ``TEST``) onto the same normalized name. + normalized_provided: dict[str, list[str]] = {} + for key in init_kwargs: + normalized_provided.setdefault(normalize(key), []).append(key) + for field_name, field_info in settings_cls.model_fields.items(): + _warn_if_field_info_incomplete(field_info, field_name, self._init_state) + # Canonical (case-preserving) alias names determine the output key, so the value + # still matches pydantic's validation aliases, which are always case-sensitive. + canonical_alias_names, *_ = _get_alias_names(field_name, field_info) + # Match alias names respect case_sensitive when matching against provided keys. + match_alias_names, *_ = _get_alias_names(field_name, field_info, case_sensitive=case_sensitive) + # When populate_by_name is True, allow using the field name as an input key, + # but normalize to the preferred alias to keep keys consistent across sources. + matchable_names = list(match_alias_names) + if include_name: + name = normalize(field_name) + if name not in matchable_names: + matchable_names.append(name) + # All provided keys matching this field, in matchable_names preference order + # (aliases first, then the field name). Only keys not yet consumed by an + # earlier field are considered. + matched_keys = [ + original + for name in matchable_names + if name in normalized_provided + for original in normalized_provided[name] + if original in init_kwarg_names + ] + if matched_keys: + preferred_alias = canonical_alias_names[0] if canonical_alias_names else field_name + # Prefer the first match in preference order; drop the remaining matches + # so they are not re-added as extras. + init_kwarg_names.difference_update(matched_keys) + self.init_kwargs[preferred_alias] = init_kwargs[matched_keys[0]] + # Include any remaining init kwargs (e.g., extras) unchanged + # Note: If populate_by_name is True and the provided key is the field name, but + # no alias exists, we keep it as-is so it can be processed as extra if allowed. + self.init_kwargs.update({key: val for key, val in init_kwargs.items() if key in init_kwarg_names}) + + self.nested_model_default_partial_update = ( + nested_model_default_partial_update + if nested_model_default_partial_update is not None + else self.config.get('nested_model_default_partial_update', False) + ) + + def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: + # Nothing to do here. Only implement the return statement to make mypy happy + return None, '', False + + def __call__(self) -> dict[str, Any]: + return ( + TypeAdapter(dict[str, Any]).dump_python(self.init_kwargs) + if self.nested_model_default_partial_update + else self.init_kwargs + ) + + def __repr__(self) -> str: + return f'{self.__class__.__name__}(init_kwargs={self.init_kwargs!r})' + + +class PydanticBaseEnvSettingsSource(PydanticBaseSettingsSource): + def __init__( + self, + settings_cls: type[BaseSettings], + case_sensitive: bool | None = None, + env_prefix: str | None = None, + env_prefix_target: EnvPrefixTarget | None = None, + env_ignore_empty: bool | None = None, + env_parse_none_str: str | None = None, + env_parse_enums: bool | None = None, + _init_state: InitState | None = None, + ) -> None: + super().__init__(settings_cls, _init_state) + self.case_sensitive = case_sensitive if case_sensitive is not None else self.config.get('case_sensitive', False) + self.env_prefix = env_prefix if env_prefix is not None else self.config.get('env_prefix', '') + self.env_prefix_target = ( + env_prefix_target if env_prefix_target is not None else self.config.get('env_prefix_target', 'variable') + ) + self.env_ignore_empty = ( + env_ignore_empty if env_ignore_empty is not None else self.config.get('env_ignore_empty', False) + ) + self.env_parse_none_str = ( + env_parse_none_str if env_parse_none_str is not None else self.config.get('env_parse_none_str') + ) + self.env_parse_enums = env_parse_enums if env_parse_enums is not None else self.config.get('env_parse_enums') + + def _apply_case_sensitive(self, value: str) -> str: + return value.lower() if not self.case_sensitive else value + + def _extract_field_info(self, field: FieldInfo, field_name: str) -> list[tuple[str, str, bool]]: + """ + Extracts field info. This info is used to get the value of field from environment variables. + + It returns a list of tuples, each tuple contains: + * field_key: The key of field that has to be used in model creation. + * env_name: The environment variable name of the field. + * value_is_complex: A flag to determine whether the value from environment variable + is complex and has to be parsed. + + Args: + field (FieldInfo): The field. + field_name (str): The field name. + + Returns: + list[tuple[str, str, bool]]: List of tuples, each tuple contains field_key, env_name, and value_is_complex. + """ + _warn_if_field_info_incomplete(field, field_name, self._init_state) + field_info: list[tuple[str, str, bool]] = [] + if isinstance(field.validation_alias, (AliasChoices, AliasPath)): + v_alias: str | list[str | int] | list[list[str | int]] | None = field.validation_alias.convert_to_aliases() + else: + v_alias = field.validation_alias + + if v_alias: + env_prefix = self.env_prefix if self.env_prefix_target in ('alias', 'all') else '' + if isinstance(v_alias, list): # AliasChoices, AliasPath + for alias in v_alias: + if isinstance(alias, str): # AliasPath + field_info.append((alias, self._apply_case_sensitive(env_prefix + alias), len(alias) > 1)) + elif isinstance(alias, list): # AliasChoices + first_arg = cast(str, alias[0]) # first item of an AliasChoices must be a str + field_info.append( + ( + first_arg, + self._apply_case_sensitive(env_prefix + first_arg), + len(alias) > 1, + ) + ) + else: # string validation alias + field_info.append((v_alias, self._apply_case_sensitive(env_prefix + v_alias), False)) + + if not v_alias or self.config.get('populate_by_name', False) or self.config.get('validate_by_name', False): + annotation = _strip_annotated(_resolve_type_alias(field.annotation)) + env_prefix = self.env_prefix if self.env_prefix_target in ('variable', 'all') else '' + if is_union_origin(get_origin(annotation)) and _union_is_complex( + annotation, field.metadata, self._init_state + ): + field_info.append((field_name, self._apply_case_sensitive(env_prefix + field_name), True)) + else: + field_info.append((field_name, self._apply_case_sensitive(env_prefix + field_name), False)) + + return field_info + + def _replace_field_names_case_insensitively(self, field: FieldInfo, field_values: dict[str, Any]) -> dict[str, Any]: + """ + Replace field names in values dict by looking in models fields insensitively. + + By having the following models: + + ```py + class SubSubSub(BaseModel): + VaL3: str + + class SubSub(BaseModel): + Val2: str + SUB_sub_SuB: SubSubSub + + class Sub(BaseModel): + VAL1: str + SUB_sub: SubSub + + class Settings(BaseSettings): + nested: Sub + + model_config = SettingsConfigDict(env_nested_delimiter='__') + ``` + + Then: + _replace_field_names_case_insensitively( + field, + {"val1": "v1", "sub_SUB": {"VAL2": "v2", "sub_SUB_sUb": {"vAl3": "v3"}}} + ) + Returns {'VAL1': 'v1', 'SUB_sub': {'Val2': 'v2', 'SUB_sub_SuB': {'VaL3': 'v3'}}} + """ + values: dict[str, Any] = {} + + for name, value in field_values.items(): + sub_model_field: FieldInfo | None = None + + annotation = _unwrap_optional_annotation(field.annotation) + + # This is here to make mypy happy + # Item "None" of "Optional[Type[Any]]" has no attribute "model_fields" + if not annotation or not hasattr(annotation, 'model_fields'): + values[name] = value + continue + else: + model_fields: dict[str, FieldInfo] = annotation.model_fields + + # Find field in sub model by looking in fields case insensitively + field_key: str | None = None + for sub_model_field_name, sub_model_field in model_fields.items(): + _warn_if_field_info_incomplete(sub_model_field, sub_model_field_name, self._init_state) + aliases, _ = _get_alias_names(sub_model_field_name, sub_model_field) + _search = (alias for alias in aliases if alias.lower() == name.lower()) + if field_key := next(_search, None): + break + + if not field_key: + values[name] = value + continue + + if ( + sub_model_field is not None + and _lenient_issubclass(_unwrap_optional_annotation(sub_model_field.annotation), BaseModel) + and isinstance(value, dict) + ): + values[field_key] = self._replace_field_names_case_insensitively(sub_model_field, value) + else: + values[field_key] = value + + return values + + def _replace_env_none_type_values(self, field_value: dict[str, Any]) -> dict[str, Any]: + """ + Recursively parse values that are of "None" type(EnvNoneType) to `None` type(None). + """ + values: dict[str, Any] = {} + + for key, value in field_value.items(): + if not isinstance(value, EnvNoneType): + values[key] = value if not isinstance(value, dict) else self._replace_env_none_type_values(value) + else: + values[key] = None + + return values + + def _get_resolved_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: + """ + Gets the value, the preferred alias key for model creation, and a flag to determine whether value + is complex. + + Note: + In V3, this method should either be made public, or, this method should be removed and the + abstract method get_field_value should be updated to include a "use_preferred_alias" flag. + + Args: + field: The field. + field_name: The field name. + + Returns: + A tuple that contains the value, preferred key and a flag to determine whether value is complex. + """ + field_value, field_key, value_is_complex = self.get_field_value(field, field_name) + if not ( + value_is_complex + or ( + (self.config.get('populate_by_name', False) or self.config.get('validate_by_name', False)) + and (field_key == field_name) + ) + ): + field_infos = self._extract_field_info(field, field_name) + preferred_key, _, preferred_is_complex = field_infos[0] + # Only normalize to preferred_key when it's a simple string alias. + # When the preferred key comes from an AliasPath (complex entry), skip normalization + # to avoid using the AliasPath's first element as the key (see #766). + if not preferred_is_complex: + return field_value, preferred_key, value_is_complex + return field_value, field_key, value_is_complex + + def __call__(self) -> dict[str, Any]: + data: dict[str, Any] = {} + + for field_name, field in self.settings_cls.model_fields.items(): + _warn_if_field_info_incomplete(field, field_name, self._init_state) + try: + field_value, field_key, value_is_complex = self._get_resolved_field_value(field, field_name) + except Exception as e: + raise SettingsError( + f'error getting value for field "{field_name}" from source "{self.__class__.__name__}"' + ) from e + + try: + field_value = self.prepare_field_value(field_name, field, field_value, value_is_complex) + except ValueError as e: + raise SettingsError( + f'error parsing value for field "{field_name}" from source "{self.__class__.__name__}"' + ) from e + + if field_value is not None: + if self.env_parse_none_str is not None: + if isinstance(field_value, dict): + field_value = self._replace_env_none_type_values(field_value) + elif isinstance(field_value, EnvNoneType): + field_value = None + if ( + not self.case_sensitive + # and _lenient_issubclass(field.annotation, BaseModel) + and isinstance(field_value, dict) + ): + data[field_key] = self._replace_field_names_case_insensitively(field, field_value) + else: + data[field_key] = field_value + + return data + + +__all__ = [ + 'ConfigFileSourceMixin', + 'DefaultSettingsSource', + 'InitSettingsSource', + 'PydanticBaseEnvSettingsSource', + 'PydanticBaseSettingsSource', + 'SettingsError', +] diff --git a/source/target-vendors/cp312/pydantic_settings/sources/providers/__init__.py b/source/target-vendors/cp312/pydantic_settings/sources/providers/__init__.py new file mode 100644 index 0000000..b55a2ed --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/sources/providers/__init__.py @@ -0,0 +1,45 @@ +"""Package containing individual source implementations.""" + +from .aws import AWSSecretsManagerSettingsSource +from .azure import AzureKeyVaultSettingsSource +from .cli import ( + CliDualFlag, + CliExplicitFlag, + CliImplicitFlag, + CliMutuallyExclusiveGroup, + CliPositionalArg, + CliSettingsSource, + CliSubCommand, + CliSuppress, + CliToggleFlag, +) +from .dotenv import DotEnvSettingsSource +from .env import EnvSettingsSource +from .gcp import GoogleSecretManagerSettingsSource +from .json import JsonConfigSettingsSource +from .pyproject import PyprojectTomlConfigSettingsSource +from .secrets import SecretsSettingsSource +from .toml import TomlConfigSettingsSource +from .yaml import YamlConfigSettingsSource + +__all__ = [ + 'AWSSecretsManagerSettingsSource', + 'AzureKeyVaultSettingsSource', + 'CliDualFlag', + 'CliExplicitFlag', + 'CliImplicitFlag', + 'CliMutuallyExclusiveGroup', + 'CliPositionalArg', + 'CliSettingsSource', + 'CliSubCommand', + 'CliSuppress', + 'CliToggleFlag', + 'DotEnvSettingsSource', + 'EnvSettingsSource', + 'GoogleSecretManagerSettingsSource', + 'JsonConfigSettingsSource', + 'PyprojectTomlConfigSettingsSource', + 'SecretsSettingsSource', + 'TomlConfigSettingsSource', + 'YamlConfigSettingsSource', +] diff --git a/source/target-vendors/cp312/pydantic_settings/sources/providers/aws.py b/source/target-vendors/cp312/pydantic_settings/sources/providers/aws.py new file mode 100644 index 0000000..848c2d0 --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/sources/providers/aws.py @@ -0,0 +1,87 @@ +from __future__ import annotations as _annotations # important for BaseSettings import to work + +import json +from collections.abc import Mapping +from typing import TYPE_CHECKING + +from ..utils import InitState, parse_env_vars +from .env import EnvSettingsSource + +if TYPE_CHECKING: + from types_boto3_secretsmanager.client import SecretsManagerClient + + from pydantic_settings.main import BaseSettings + + +boto3_client = None + + +def import_aws_secrets_manager() -> None: + global boto3_client + + try: + from boto3 import client as boto3_client + except ImportError as e: # pragma: no cover + raise ImportError( + 'AWS Secrets Manager dependencies are not installed, run `pip install pydantic-settings[aws-secrets-manager]`' + ) from e + + +class AWSSecretsManagerSettingsSource(EnvSettingsSource): + _secret_id: str + _secretsmanager_client: SecretsManagerClient + + def __init__( + self, + settings_cls: type[BaseSettings], + secret_id: str, + region_name: str | None = None, + endpoint_url: str | None = None, + case_sensitive: bool | None = True, + env_prefix: str | None = None, + env_nested_delimiter: str | None = '--', + env_parse_none_str: str | None = None, + env_parse_enums: bool | None = None, + version_id: str | None = None, + _init_state: InitState | None = None, + ) -> None: + import_aws_secrets_manager() + self._secretsmanager_client = boto3_client('secretsmanager', region_name=region_name, endpoint_url=endpoint_url) # type: ignore + self._secret_id = secret_id + self._version_id = version_id + super().__init__( + settings_cls, + case_sensitive=case_sensitive, + env_prefix=env_prefix, + env_nested_delimiter=env_nested_delimiter, + env_ignore_empty=False, + env_parse_none_str=env_parse_none_str, + env_parse_enums=env_parse_enums, + _init_state=_init_state, + ) + + def _load_env_vars(self) -> Mapping[str, str | None]: + request = {'SecretId': self._secret_id} + + if self._version_id: + request['VersionId'] = self._version_id + + response = self._secretsmanager_client.get_secret_value(**request) + + return parse_env_vars( + json.loads(response['SecretString']), + self.case_sensitive, + self.env_ignore_empty, + self.env_parse_none_str, + ) + + def __repr__(self) -> str: + return ( + f'{self.__class__.__name__}(secret_id={self._secret_id!r}, ' + f'env_nested_delimiter={self.env_nested_delimiter!r})' + ) + + +__all__ = [ + 'AWSSecretsManagerSettingsSource', +] diff --git a/source/target-vendors/cp312/pydantic_settings/sources/providers/azure.py b/source/target-vendors/cp312/pydantic_settings/sources/providers/azure.py new file mode 100644 index 0000000..7bda681 --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/sources/providers/azure.py @@ -0,0 +1,162 @@ +"""Azure Key Vault settings source.""" + +from __future__ import annotations as _annotations + +from collections.abc import Iterator, Mapping +from typing import TYPE_CHECKING + +from pydantic.alias_generators import to_snake +from pydantic.fields import FieldInfo + +from ..utils import InitState +from .env import EnvSettingsSource + +if TYPE_CHECKING: + from azure.core.credentials import TokenCredential + from azure.core.exceptions import ResourceNotFoundError + from azure.keyvault.secrets import SecretClient + + from pydantic_settings.main import BaseSettings +else: + TokenCredential = None + ResourceNotFoundError = None + SecretClient = None + + +def import_azure_key_vault() -> None: + global TokenCredential + global SecretClient + global ResourceNotFoundError + + try: + from azure.core.credentials import TokenCredential + from azure.core.exceptions import ResourceNotFoundError + from azure.keyvault.secrets import SecretClient + except ImportError as e: # pragma: no cover + raise ImportError( + 'Azure Key Vault dependencies are not installed, run `pip install pydantic-settings[azure-key-vault]`' + ) from e + + +class AzureKeyVaultMapping(Mapping[str, str | None]): + _loaded_secrets: dict[str, str | None] + _secret_client: SecretClient + _secret_names: list[str] + + def __init__( + self, + secret_client: SecretClient, + case_sensitive: bool, + snake_case_conversion: bool, + env_prefix: str | None, + ) -> None: + self._loaded_secrets = {} + self._secret_client = secret_client + self._case_sensitive = case_sensitive + self._snake_case_conversion = snake_case_conversion + self._env_prefix = env_prefix if env_prefix else '' + self._secret_map: dict[str, str] = self._load_remote() + + def _load_remote(self) -> dict[str, str]: + secret_names: Iterator[str] = ( + secret.name for secret in self._secret_client.list_properties_of_secrets() if secret.name and secret.enabled + ) + + if self._snake_case_conversion: + name_map: dict[str, str] = {} + for name in secret_names: + if name.startswith(self._env_prefix): + name_map[f'{self._env_prefix}{to_snake(name[len(self._env_prefix) :])}'] = name + else: + name_map[to_snake(name)] = name + return name_map + + if self._case_sensitive: + return {name: name for name in secret_names} + + return {name.lower(): name for name in secret_names} + + def __getitem__(self, key: str) -> str | None: + new_key = key + + if self._snake_case_conversion: + if key.startswith(self._env_prefix): + new_key = f'{self._env_prefix}{to_snake(key[len(self._env_prefix) :])}' + else: + new_key = to_snake(key) + + elif not self._case_sensitive: + new_key = key.lower() + + if new_key not in self._loaded_secrets: + if new_key in self._secret_map: + self._loaded_secrets[new_key] = self._secret_client.get_secret(self._secret_map[new_key]).value + else: + raise KeyError(key) + + return self._loaded_secrets[new_key] + + def __len__(self) -> int: + return len(self._secret_map) + + def __iter__(self) -> Iterator[str]: + return iter(self._secret_map.keys()) + + +class AzureKeyVaultSettingsSource(EnvSettingsSource): + _url: str + _credential: TokenCredential + + def __init__( + self, + settings_cls: type[BaseSettings], + url: str, + credential: TokenCredential, + dash_to_underscore: bool = False, + case_sensitive: bool | None = None, + snake_case_conversion: bool = False, + env_prefix: str | None = None, + env_parse_none_str: str | None = None, + env_parse_enums: bool | None = None, + _init_state: InitState | None = None, + ) -> None: + import_azure_key_vault() + self._url = url + self._credential = credential + self._dash_to_underscore = dash_to_underscore + self._snake_case_conversion = snake_case_conversion + super().__init__( + settings_cls, + case_sensitive=True if snake_case_conversion else case_sensitive, + env_prefix=env_prefix, + env_nested_delimiter='__' if snake_case_conversion else '--', + env_ignore_empty=False, + env_parse_none_str=env_parse_none_str, + env_parse_enums=env_parse_enums, + _init_state=_init_state, + ) + + def _load_env_vars(self) -> Mapping[str, str | None]: + secret_client = SecretClient(vault_url=self._url, credential=self._credential) + return AzureKeyVaultMapping( + secret_client=secret_client, + case_sensitive=self.case_sensitive, + snake_case_conversion=self._snake_case_conversion, + env_prefix=self.env_prefix, + ) + + def _extract_field_info(self, field: FieldInfo, field_name: str) -> list[tuple[str, str, bool]]: + if self._snake_case_conversion: + field_info = [(x[0], x[1], x[2]) for x in super()._extract_field_info(field, field_name)] + return field_info + + if self._dash_to_underscore: + return [(x[0], x[1].replace('_', '-'), x[2]) for x in super()._extract_field_info(field, field_name)] + + return super()._extract_field_info(field, field_name) + + def __repr__(self) -> str: + return f'{self.__class__.__name__}(url={self._url!r}, env_nested_delimiter={self.env_nested_delimiter!r})' + + +__all__ = ['AzureKeyVaultMapping', 'AzureKeyVaultSettingsSource'] diff --git a/source/target-vendors/cp312/pydantic_settings/sources/providers/cli.py b/source/target-vendors/cp312/pydantic_settings/sources/providers/cli.py new file mode 100644 index 0000000..1064759 --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/sources/providers/cli.py @@ -0,0 +1,1700 @@ +"""Command-line interface settings source.""" + +from __future__ import annotations as _annotations + +import copy +import json +import re +import shlex +import sys +import typing +from argparse import ( + SUPPRESS, + ArgumentParser, + BooleanOptionalAction, + Namespace, + RawDescriptionHelpFormatter, + _SubParsersAction, +) +from collections import defaultdict +from collections.abc import Callable, Mapping, Sequence +from enum import Enum +from functools import cached_property +from itertools import chain +from textwrap import dedent +from types import SimpleNamespace +from typing import ( + TYPE_CHECKING, + Annotated, + Any, + Generic, + Literal, + NoReturn, + TypeVar, + cast, + get_args, + get_origin, + overload, +) + +from pydantic import AliasChoices, AliasPath, BaseModel, Field, PrivateAttr, TypeAdapter +from pydantic._internal._repr import Representation +from pydantic._internal._utils import is_model_class +from pydantic.dataclasses import is_pydantic_dataclass +from pydantic.fields import FieldInfo +from pydantic_core import PydanticUndefined +from typing_inspection import typing_objects +from typing_inspection.introspection import is_union_origin + +from ...exceptions import SettingsError +from ...utils import _lenient_issubclass, _typing_base, _WithArgsTypes +from ..types import ( + ForceDecode, + NoDecode, + PydanticModel, + _CliDualFlag, + _CliExplicitFlag, + _CliImplicitFlag, + _CliPositionalArg, + _CliSubCommand, + _CliToggleFlag, + _CliUnknownArgs, +) +from ..utils import ( + InitState, + _annotation_contains_types, + _annotation_enum_val_to_name, + _get_alias_names, + _get_model_fields, + _is_function, + _strip_annotated, + parse_env_vars, +) +from .env import EnvSettingsSource + +if TYPE_CHECKING: + from pydantic_settings.main import BaseSettings + + +class _CliInternalArgParser(ArgumentParser): + def __init__(self, cli_exit_on_error: bool = True, **kwargs: Any) -> None: + super().__init__(**kwargs) + self._cli_exit_on_error = cli_exit_on_error + + def error(self, message: str) -> NoReturn: + if not self._cli_exit_on_error: + raise SettingsError(f'error parsing CLI: {message}') + super().error(message) + + +class CliMutuallyExclusiveGroup(BaseModel): + pass + + +def _get_model_description(model_cls: type[Any]) -> str | None: + """Get model description from json_schema_extra or __doc__ fallback. + + ``json_schema_extra.description`` takes precedence over ``__doc__`` to + match pydantic's own behaviour. When neither is available (e.g. under + ``python -OO`` where docstrings are stripped), returns ``None``. + """ + config: Any = {} + if is_model_class(model_cls): + config = model_cls.model_config + elif is_pydantic_dataclass(model_cls): + config = getattr(model_cls, '__pydantic_config__', {}) + json_schema_extra = config.get('json_schema_extra') + if isinstance(json_schema_extra, dict): + desc = json_schema_extra.get('description') + if desc is not None: + return desc + elif callable(json_schema_extra): + try: + desc = None + if is_model_class(model_cls): + desc = model_cls.model_json_schema().get('description') + elif is_pydantic_dataclass(model_cls): + desc = TypeAdapter(model_cls).json_schema().get('description') + if desc is not None: + return desc + except Exception: + pass + if model_cls.__doc__ is not None: + return dedent(model_cls.__doc__) + return None + + +def _collect_sub_models(type_: Any, sub_models: list[type[BaseModel]]) -> None: + """Recursively collect BaseModel subclasses from possibly nested union types.""" + stripped = _strip_annotated(type_) + if is_model_class(stripped) or is_pydantic_dataclass(stripped): + sub_models.append(stripped) # type: ignore[arg-type] + elif is_union_origin(get_origin(stripped)): + for arg in get_args(stripped): + _collect_sub_models(arg, sub_models) + + +class _CliArg(BaseModel): + model: Any + parser: Any + field_name: str + arg_prefix: str + case_sensitive: bool + populate_by_name: bool + hide_none_type: bool + kebab_case: bool | Literal['all', 'no_enums'] | None + enable_decoding: bool | None + env_prefix_len: int + args: list[str] = [] + kwargs: dict[str, Any] = {} + + _alias_names: tuple[str, ...] = PrivateAttr(()) + _alias_paths: dict[str, int | None] = PrivateAttr({}) + _is_alias_path_only: bool = PrivateAttr(False) + _field_info: FieldInfo = PrivateAttr() + + def __init__( + self, + field_info: FieldInfo, + parser_map: defaultdict[str | FieldInfo, dict[int | None | str | type[BaseModel], _CliArg]], + **values: Any, + ) -> None: + super().__init__(**values) + self._field_info = field_info + self._alias_names, self._is_alias_path_only = _get_alias_names( + self.field_name, + self.field_info, + alias_path_args=self._alias_paths, + case_sensitive=self.case_sensitive, + populate_by_name=self.populate_by_name, + ) + + alias_path_dests = {f'{self.arg_prefix}{name}': index for name, index in self._alias_paths.items()} + if self.subcommand_dest: + for sub_model in self.sub_models: + subcommand_alias = self.subcommand_alias(sub_model) + parser_map[self.subcommand_dest][subcommand_alias] = self.model_copy(update={'args': [], 'kwargs': {}}) + parser_map[self.subcommand_dest][sub_model] = parser_map[self.subcommand_dest][subcommand_alias] + parser_map[self.field_info][subcommand_alias] = parser_map[self.subcommand_dest][subcommand_alias] + elif self.dest not in alias_path_dests: + parser_map[self.dest][None] = self + parser_map[self.field_info][None] = parser_map[self.dest][None] + for alias_path_dest, index in alias_path_dests.items(): + parser_map[alias_path_dest][index] = self.model_copy(update={'args': [], 'kwargs': {}}) + parser_map[self.field_info][index] = parser_map[alias_path_dest][index] + + @classmethod + def get_kebab_case(cls, name: str, kebab_case: bool | Literal['all', 'no_enums'] | None) -> str: + return name.replace('_', '-') if kebab_case not in (None, False) else name + + @classmethod + def get_enum_names( + cls, annotation: type[Any], kebab_case: bool | Literal['all', 'no_enums'] | None + ) -> tuple[str, ...]: + enum_names: tuple[str, ...] = () + annotation = _strip_annotated(annotation) + for type_ in get_args(annotation): + enum_names += cls.get_enum_names(type_, kebab_case) + if annotation and _lenient_issubclass(annotation, Enum): + enum_names += tuple(cls.get_kebab_case(name, kebab_case == 'all') for name in annotation.__members__) + return enum_names + + def subcommand_alias(self, sub_model: type[BaseModel]) -> str: + return self.get_kebab_case( + sub_model.__name__ if len(self.sub_models) > 1 else self.preferred_alias, self.kebab_case + ) + + @cached_property + def field_info(self) -> FieldInfo: + return self._field_info + + @cached_property + def subcommand_dest(self) -> str | None: + return f'{self.arg_prefix}:subcommand' if _CliSubCommand in self.field_info.metadata else None + + @cached_property + def dest(self) -> str: + if ( + not self.subcommand_dest + and self.arg_prefix + and self.field_info.validation_alias is not None + and not self.is_parser_submodel + ): + # Strip prefix if validation alias is set and value is not complex. + # Related https://github.com/pydantic/pydantic-settings/pull/25 + return f'{self.arg_prefix}{self.preferred_alias}'[self.env_prefix_len :] + return f'{self.arg_prefix}{self.preferred_alias}' + + @cached_property + def preferred_arg_name(self) -> str: + return self.args[0].replace('_', '-') if self.kebab_case else self.args[0] + + @cached_property + def sub_models(self) -> list[type[BaseModel]]: + field_types: tuple[Any, ...] = ( + (self.field_info.annotation,) + if not get_args(self.field_info.annotation) + else get_args(self.field_info.annotation) + ) + if self.hide_none_type: + field_types = tuple([type_ for type_ in field_types if type_ is not type(None)]) + + sub_models: list[type[BaseModel]] = [] + for type_ in field_types: + if _annotation_contains_types(type_, (_CliSubCommand,), is_include_origin=False): + raise SettingsError( + f'CliSubCommand is not outermost annotation for {self.model.__name__}.{self.field_name}' + ) + elif _annotation_contains_types(type_, (_CliPositionalArg,), is_include_origin=False): + raise SettingsError( + f'CliPositionalArg is not outermost annotation for {self.model.__name__}.{self.field_name}' + ) + _collect_sub_models(type_, sub_models) + return sub_models + + @cached_property + def alias_names(self) -> tuple[str, ...]: + return self._alias_names + + @cached_property + def alias_paths(self) -> dict[str, int | None]: + return self._alias_paths + + @cached_property + def preferred_alias(self) -> str: + return self._alias_names[0] + + @cached_property + def is_alias_path_only(self) -> bool: + return self._is_alias_path_only + + @cached_property + def is_append_action(self) -> bool: + return not self.subcommand_dest and _annotation_contains_types( + self.field_info.annotation, (list, set, dict, Sequence, Mapping), is_strip_annotated=True + ) + + @cached_property + def is_parser_submodel(self) -> bool: + return not self.subcommand_dest and bool(self.sub_models) and not self.is_append_action + + @cached_property + def is_no_decode(self) -> bool: + return self.field_info is not None and ( + NoDecode in self.field_info.metadata + or (self.enable_decoding is False and ForceDecode not in self.field_info.metadata) + ) + + +T = TypeVar('T') +CliSubCommand = Annotated[T | None, _CliSubCommand] +CliPositionalArg = Annotated[T, _CliPositionalArg] +_CliBoolFlag = TypeVar('_CliBoolFlag', bound=bool) +CliImplicitFlag = Annotated[_CliBoolFlag, _CliImplicitFlag] +CliExplicitFlag = Annotated[_CliBoolFlag, _CliExplicitFlag] +CliToggleFlag = Annotated[_CliBoolFlag, _CliToggleFlag] +CliDualFlag = Annotated[_CliBoolFlag, _CliDualFlag] +CLI_SUPPRESS = SUPPRESS +CliSuppress = Annotated[T, CLI_SUPPRESS] +CliUnknownArgs = Annotated[list[str], Field(default=[]), _CliUnknownArgs, NoDecode] + + +class CliSettingsSource(EnvSettingsSource, Generic[T]): + """ + Source class for loading settings values from CLI. + + Note: + A `CliSettingsSource` connects with a `root_parser` object by using the parser methods to add + `settings_cls` fields as command line arguments. The `CliSettingsSource` internal parser representation + is based upon the `argparse` parsing library, and therefore, requires the parser methods to support + the same attributes as their `argparse` library counterparts. + + Args: + cli_prog_name: The CLI program name to display in help text. Defaults to `None` if cli_parse_args is `None`. + Otherwise, defaults to sys.argv[0]. + cli_parse_args: The list of CLI arguments to parse. Defaults to None. + If set to `True`, defaults to sys.argv[1:]. + cli_parse_none_str: The CLI string value that should be parsed (e.g. "null", "void", "None", etc.) into `None` + type(None). Defaults to "null" if cli_avoid_json is `False`, and "None" if cli_avoid_json is `True`. + cli_hide_none_type: Hide `None` values in CLI help text. Defaults to `False`. + cli_avoid_json: Avoid complex JSON objects in CLI help text. Defaults to `False`. + cli_enforce_required: Enforce required fields at the CLI. Defaults to `False`. + cli_use_class_docs_for_groups: Use class docstrings in CLI group help text instead of field descriptions. + Defaults to `False`. + cli_show_env_vars: Show resolved environment variable names in CLI help text. Defaults to `False`. + cli_exit_on_error: Determines whether or not the internal parser exits with error info when an error occurs. + Defaults to `True`. + cli_prefix: Prefix for command line arguments added under the root parser. Defaults to "". + cli_flag_prefix_char: The flag prefix character to use for CLI optional arguments. Defaults to '-'. + cli_implicit_flags: Controls how `bool` fields are exposed as CLI flags. + + - False (default): no implicit flags are generated; booleans must be set explicitly (e.g. --flag=true). + - True / 'dual': optional boolean fields generate both positive and negative forms (--flag and --no-flag). + - 'toggle': required boolean fields remain in 'dual' mode, while optional boolean fields generate a single + flag aligned with the default value (if default=False, expose --flag; if default=True, expose --no-flag). + cli_ignore_unknown_args: Whether to ignore unknown CLI args and parse only known ones. Defaults to `False`. + cli_kebab_case: CLI args use kebab case. Defaults to `False`. + cli_shortcuts: Mapping of target field name to alias names. Defaults to `None`. + case_sensitive: Whether CLI "--arg" names should be read with case-sensitivity. Defaults to `True`. + Note: Case-insensitive matching is only supported on the internal root parser and does not apply to CLI + subcommands. + root_parser: The root parser object. + parse_args_method: The root parser parse args method. Defaults to `argparse.ArgumentParser.parse_args`. + add_argument_method: The root parser add argument method. Defaults to `argparse.ArgumentParser.add_argument`. + add_argument_group_method: The root parser add argument group method. + Defaults to `argparse.ArgumentParser.add_argument_group`. + add_parser_method: The root parser add new parser (sub-command) method. + Defaults to `argparse._SubParsersAction.add_parser`. + add_subparsers_method: The root parser add subparsers (sub-commands) method. + Defaults to `argparse.ArgumentParser.add_subparsers`. + format_help_method: The root parser format help method. Defaults to `argparse.ArgumentParser.format_help`. + formatter_class: A class for customizing the root parser help text. Defaults to `argparse.RawDescriptionHelpFormatter`. + """ + + def __init__( + self, + settings_cls: type[BaseSettings], + cli_prog_name: str | None = None, + cli_parse_args: bool | list[str] | tuple[str, ...] | None = None, + cli_parse_none_str: str | None = None, + cli_hide_none_type: bool | None = None, + cli_avoid_json: bool | None = None, + cli_enforce_required: bool | None = None, + cli_use_class_docs_for_groups: bool | None = None, + cli_show_env_vars: bool | None = None, + cli_exit_on_error: bool | None = None, + cli_prefix: str | None = None, + cli_flag_prefix_char: str | None = None, + cli_implicit_flags: bool | Literal['dual', 'toggle'] | None = None, + cli_ignore_unknown_args: bool | None = None, + cli_kebab_case: bool | Literal['all', 'no_enums'] | None = None, + cli_shortcuts: Mapping[str, str | list[str]] | None = None, + case_sensitive: bool | None = True, + root_parser: Any = None, + parse_args_method: Callable[..., Any] | None = None, + add_argument_method: Callable[..., Any] | None = ArgumentParser.add_argument, + add_argument_group_method: Callable[..., Any] | None = ArgumentParser.add_argument_group, + add_parser_method: Callable[..., Any] | None = _SubParsersAction.add_parser, + add_subparsers_method: Callable[..., Any] | None = ArgumentParser.add_subparsers, + format_help_method: Callable[..., Any] | None = ArgumentParser.format_help, + formatter_class: Any = RawDescriptionHelpFormatter, + _env_settings_source: EnvSettingsSource | None = None, + _init_state: InitState | None = None, + ) -> None: + self.cli_prog_name = ( + cli_prog_name if cli_prog_name is not None else settings_cls.model_config.get('cli_prog_name', sys.argv[0]) + ) + self.cli_hide_none_type = ( + cli_hide_none_type + if cli_hide_none_type is not None + else settings_cls.model_config.get('cli_hide_none_type', False) + ) + self.cli_avoid_json = ( + cli_avoid_json if cli_avoid_json is not None else settings_cls.model_config.get('cli_avoid_json', False) + ) + if not cli_parse_none_str: + cli_parse_none_str = 'None' if self.cli_avoid_json is True else 'null' + self.cli_parse_none_str = cli_parse_none_str + self.cli_enforce_required = ( + cli_enforce_required + if cli_enforce_required is not None + else settings_cls.model_config.get('cli_enforce_required', False) + ) + self.cli_use_class_docs_for_groups = ( + cli_use_class_docs_for_groups + if cli_use_class_docs_for_groups is not None + else settings_cls.model_config.get('cli_use_class_docs_for_groups', False) + ) + self.cli_show_env_vars = ( + cli_show_env_vars + if cli_show_env_vars is not None + else settings_cls.model_config.get('cli_show_env_vars', False) + ) + self.cli_exit_on_error = ( + cli_exit_on_error + if cli_exit_on_error is not None + else settings_cls.model_config.get('cli_exit_on_error', True) + ) + self.cli_prefix = cli_prefix if cli_prefix is not None else settings_cls.model_config.get('cli_prefix', '') + self.cli_flag_prefix_char = ( + cli_flag_prefix_char + if cli_flag_prefix_char is not None + else settings_cls.model_config.get('cli_flag_prefix_char', '-') + ) + self._cli_flag_prefix = self.cli_flag_prefix_char * 2 + if self.cli_prefix: + if cli_prefix.startswith('.') or cli_prefix.endswith('.') or not cli_prefix.replace('.', '').isidentifier(): # type: ignore + raise SettingsError(f'CLI settings source prefix is invalid: {cli_prefix}') + self.cli_prefix += '.' + self.cli_implicit_flags = ( + cli_implicit_flags + if cli_implicit_flags is not None + else settings_cls.model_config.get('cli_implicit_flags', False) + ) + self.cli_ignore_unknown_args = ( + cli_ignore_unknown_args + if cli_ignore_unknown_args is not None + else settings_cls.model_config.get('cli_ignore_unknown_args', False) + ) + self.cli_kebab_case = ( + cli_kebab_case if cli_kebab_case is not None else settings_cls.model_config.get('cli_kebab_case', False) + ) + self.cli_shortcuts = ( + cli_shortcuts if cli_shortcuts is not None else settings_cls.model_config.get('cli_shortcuts', None) + ) + self._env_settings_source: EnvSettingsSource | None = None + if self.cli_show_env_vars: + self._env_settings_source = _env_settings_source or EnvSettingsSource(settings_cls) + self._env_var_names: dict[str, tuple[str, ...]] = {} + + case_sensitive = case_sensitive if case_sensitive is not None else True + if not case_sensitive and root_parser is not None: + raise SettingsError('Case-insensitive matching is only supported on the internal root parser') + + super().__init__( + settings_cls, + env_nested_delimiter='.', + env_parse_none_str=self.cli_parse_none_str, + env_parse_enums=True, + env_prefix=self.cli_prefix, + case_sensitive=case_sensitive, + env_nested_max_split=0, + _init_state=_init_state, + ) + + root_parser = ( + _CliInternalArgParser( + cli_exit_on_error=self.cli_exit_on_error, + prog=self.cli_prog_name, + description=_get_model_description(settings_cls), + formatter_class=formatter_class, + prefix_chars=self.cli_flag_prefix_char, + allow_abbrev=False, + add_help=False, + ) + if root_parser is None + else root_parser + ) + self._connect_root_parser( + root_parser=root_parser, + parse_args_method=parse_args_method, + add_argument_method=add_argument_method, + add_argument_group_method=add_argument_group_method, + add_parser_method=add_parser_method, + add_subparsers_method=add_subparsers_method, + format_help_method=format_help_method, + formatter_class=formatter_class, + ) + + if cli_parse_args not in (None, False): + if cli_parse_args is True: + cli_parse_args = sys.argv[1:] + elif not isinstance(cli_parse_args, (list, tuple)): + raise SettingsError( + f'cli_parse_args must be a list or tuple of strings, received {type(cli_parse_args)}' + ) + self._load_env_vars(parsed_args=self._parse_args(self.root_parser, cli_parse_args)) + + @property + def env_var_names(self) -> Mapping[str, tuple[str, ...]]: + """Resolved environment variable names for CLI arguments.""" + return self._env_var_names.copy() + + @overload + def __call__(self) -> dict[str, Any]: ... + + @overload + def __call__(self, *, args: list[str] | tuple[str, ...] | bool) -> CliSettingsSource[T]: + """ + Parse and load the command line arguments list into the CLI settings source. + + Args: + args: + The command line arguments to parse and load. Defaults to `None`, which means do not parse + command line arguments. If set to `True`, defaults to sys.argv[1:]. If set to `False`, does + not parse command line arguments. + + Returns: + CliSettingsSource: The object instance itself. + """ + + @overload + def __call__(self, *, parsed_args: Namespace | SimpleNamespace | dict[str, Any]) -> CliSettingsSource[T]: + """ + Loads parsed command line arguments into the CLI settings source. + + Note: + The parsed args must be in `argparse.Namespace`, `SimpleNamespace`, or vars dictionary + (e.g., vars(argparse.Namespace)) format. + + Args: + parsed_args: The parsed args to load. + + Returns: + CliSettingsSource: The object instance itself. + """ + + def __call__( + self, + *, + args: list[str] | tuple[str, ...] | bool | None = None, + parsed_args: Namespace | SimpleNamespace | dict[str, list[str] | str] | None = None, + ) -> dict[str, Any] | CliSettingsSource[T]: + if args is not None and parsed_args is not None: + raise SettingsError('`args` and `parsed_args` are mutually exclusive') + elif args is not None: + if args is False: + return self._load_env_vars(parsed_args={}) + if args is True: + args = sys.argv[1:] + return self._load_env_vars(parsed_args=self._parse_args(self.root_parser, args)) + elif parsed_args is not None: + return self._load_env_vars(parsed_args=copy.copy(parsed_args)) + else: + return super().__call__() + + @overload + def _load_env_vars(self) -> Mapping[str, str | None]: ... + + @overload + def _load_env_vars(self, *, parsed_args: Namespace | SimpleNamespace | dict[str, Any]) -> CliSettingsSource[T]: + """ + Loads the parsed command line arguments into the CLI environment settings variables. + + Note: + The parsed args must be in `argparse.Namespace`, `SimpleNamespace`, or vars dictionary + (e.g., vars(argparse.Namespace)) format. + + Args: + parsed_args: The parsed args to load. + + Returns: + CliSettingsSource: The object instance itself. + """ + + def _load_env_vars( + self, *, parsed_args: Namespace | SimpleNamespace | dict[str, list[str] | str] | None = None + ) -> Mapping[str, str | None] | CliSettingsSource[T]: + if parsed_args is None: + return {} + + if isinstance(parsed_args, (Namespace, SimpleNamespace)): + parsed_args = vars(parsed_args) + + selected_subcommands = self._resolve_parsed_args(parsed_args) + for arg_dest, arg_map in self._parser_map.items(): + if isinstance(arg_dest, str) and arg_dest.endswith(':subcommand'): + for subcommand_dest in [arg.dest for arg in arg_map.values()]: + if subcommand_dest not in selected_subcommands: + parsed_args[subcommand_dest] = self.cli_parse_none_str + + parsed_args = { + key: val + for key, val in parsed_args.items() + if not key.endswith(':subcommand') and val is not PydanticUndefined + } + if selected_subcommands: + last_selected_subcommand = max(selected_subcommands, key=len) + if not any(field_name for field_name in parsed_args if f'{last_selected_subcommand}.' in field_name): + parsed_args[last_selected_subcommand] = '{}' + else: + last_selected_subcommand = '' + + # When using parse_known_args due to a subcommand's CliUnknownArgs, reject + # unknown args if the selected subcommand does not accept them. + if not self.cli_ignore_unknown_args and self._cli_unknown_args: + has_unknown = any(args for args in self._cli_unknown_args.values()) + if has_unknown: + selected_accepts_unknown = any( + dest.rsplit('.', 1)[0] in last_selected_subcommand for dest in self._cli_unknown_args + ) + if not selected_accepts_unknown: + unknown = next(args for args in self._cli_unknown_args.values() if args) + if isinstance(self.root_parser, ArgumentParser): + self.root_parser.error(f'unrecognized arguments: {" ".join(unknown)}') + raise SystemExit(2) + + parsed_args.update(self._cli_unknown_args) + + self.env_vars = parse_env_vars( + cast(Mapping[str, str], parsed_args), + self.case_sensitive, + self.env_ignore_empty, + self.cli_parse_none_str, + ) + + return self + + def _resolve_parsed_args(self, parsed_args: dict[str, list[str] | str]) -> list[str]: + selected_subcommands: list[str] = [] + for field_name, val in list(parsed_args.items()): + if isinstance(val, list): + if self._is_nested_alias_path_only_workaround(parsed_args, field_name, val): + # Workaround for nested alias path environment variables not being handled. + # See https://github.com/pydantic/pydantic-settings/issues/670 + continue + + cli_arg = self._parser_map.get(field_name, {}).get(None) + if cli_arg and cli_arg.is_no_decode: + parsed_args[field_name] = ','.join(val) + continue + + parsed_args[field_name] = self._merge_parsed_list(val, field_name) + elif field_name.endswith(':subcommand') and val is not None: + selected_subcommands.append(self._parser_map[field_name][val].dest) + elif self.cli_kebab_case == 'all' and isinstance(val, str): + snake_val = val.replace('-', '_') + cli_arg = self._parser_map.get(field_name, {}).get(None) + if ( + cli_arg + and cli_arg.field_info.annotation + and (snake_val in cli_arg.get_enum_names(cli_arg.field_info.annotation, False)) + ): + if '_' in val: + raise ValueError(f'Input should be kebab-case "{val.replace("_", "-")}", not "{val}"') + parsed_args[field_name] = snake_val + + return selected_subcommands + + def _is_nested_alias_path_only_workaround( + self, parsed_args: dict[str, list[str] | str], field_name: str, val: list[str] + ) -> bool: + """ + Workaround for nested alias path environment variables not being handled. + See https://github.com/pydantic/pydantic-settings/issues/670 + """ + known_arg = self._parser_map.get(field_name, {}).values() + if not known_arg: + return False + arg = next(iter(known_arg)) + if arg.is_alias_path_only and arg.arg_prefix.endswith('.'): + del parsed_args[field_name] + nested_dest = arg.arg_prefix[:-1] + nested_val = f'"{arg.preferred_alias}": {self._merge_parsed_list(val, field_name)}' + parsed_args[nested_dest] = ( + f'{{{nested_val}}}' + if nested_dest not in parsed_args + else f'{parsed_args[nested_dest][:-1]}, {nested_val}}}' + ) + return True + return False + + def _get_merge_parsed_list_types(self, parsed_list: list[str], field_name: str) -> tuple[type | None, type | None]: + merge_type = self._cli_dict_args.get(field_name, list) + if ( + merge_type is list + or not is_union_origin(get_origin(merge_type)) + or not any( + type_ + for type_ in get_args(merge_type) + if type_ is not type(None) and get_origin(type_) not in (dict, Mapping) + ) + ): + inferred_type = merge_type + else: + inferred_type = list if parsed_list and (len(parsed_list) > 1 or parsed_list[0].startswith('[')) else str + + return merge_type, inferred_type + + def _merged_list_to_str(self, merged_list: list[str], field_name: str) -> str: + decode_list: list[str] = [] + is_use_decode: bool | None = None + cli_arg_map = self._parser_map.get(field_name, {}) + try: + list_adapter: Any = TypeAdapter(next(iter(cli_arg_map.values())).field_info.annotation) + is_num_type_str = type(next(iter(list_adapter.validate_python(['1'])))) is str + except Exception: + is_num_type_str = None + for index, item in enumerate(merged_list): + cli_arg = cli_arg_map.get(index) + is_decode = cli_arg is None or not cli_arg.is_no_decode + if is_use_decode is None: + is_use_decode = is_decode + elif is_use_decode != is_decode: + raise SettingsError('Mixing Decode and NoDecode across different AliasPath fields is not allowed') + if is_use_decode: + item = item.replace('\\', '\\\\') + try: + unquoted_item = item[1:-1] if item.startswith('"') and item.endswith('"') else item + float(unquoted_item) + item = f'"{unquoted_item}"' if is_num_type_str else unquoted_item + except ValueError: + pass + elif item.startswith('"') and item.endswith('"'): + item = item[1:-1] + decode_list.append(item) + merged_list_str = ','.join(decode_list) + return f'[{merged_list_str}]' if is_use_decode else merged_list_str + + def _merge_parsed_list(self, parsed_list: list[str], field_name: str) -> str: + try: + merged_list: list[str] = [] + is_last_consumed_a_value = False + merge_type, inferred_type = self._get_merge_parsed_list_types(parsed_list, field_name) + for val in parsed_list: + if not isinstance(val, str): + # If val is not a string, it's from an external parser and we can ignore parsing the rest of the + # list. + break + val = val.strip() + if val.startswith('[') and val.endswith(']'): + val = val[1:-1].strip() + while val: + val = val.strip() + if val.startswith(','): + val = self._consume_comma(val, merged_list, is_last_consumed_a_value) + is_last_consumed_a_value = False + else: + if val.startswith(('{', '[')): + val = self._consume_object_or_array(val, merged_list) + else: + try: + val = self._consume_string_or_number(val, merged_list, merge_type) + except ValueError: + if merge_type is inferred_type: + raise + merge_type = inferred_type + val = self._consume_string_or_number(val, merged_list, merge_type) + is_last_consumed_a_value = True + if not is_last_consumed_a_value: + val = self._consume_comma(val, merged_list, is_last_consumed_a_value) + + if merge_type is str: + return merged_list[0] + elif merge_type is list: + return self._merged_list_to_str(merged_list, field_name) + else: + merged_dict: dict[str, str] = {} + for item in merged_list: + merged_dict.update(json.loads(item)) + return json.dumps(merged_dict) + except Exception as e: + raise SettingsError(f'Parsing error encountered for {field_name}: {e}') + + def _consume_comma(self, item: str, merged_list: list[str], is_last_consumed_a_value: bool) -> str: + if not is_last_consumed_a_value: + merged_list.append('""') + return item[1:] + + def _consume_object_or_array(self, item: str, merged_list: list[str]) -> str: + count = 1 + close_delim = '}' if item.startswith('{') else ']' + in_str = False + for consumed in range(1, len(item)): + if item[consumed] == '"' and item[consumed - 1] != '\\': + in_str = not in_str + elif in_str: + continue + elif item[consumed] in ('{', '['): + count += 1 + elif item[consumed] in ('}', ']'): + count -= 1 + if item[consumed] == close_delim and count == 0: + merged_list.append(item[: consumed + 1]) + return item[consumed + 1 :] + raise SettingsError(f'Missing end delimiter "{close_delim}"') + + def _consume_string_or_number(self, item: str, merged_list: list[str], merge_type: type[Any] | None) -> str: + consumed = 0 if merge_type is not str else len(item) + is_find_end_quote = False + while consumed < len(item): + if item[consumed] == '"' and (consumed == 0 or item[consumed - 1] != '\\'): + is_find_end_quote = not is_find_end_quote + if not is_find_end_quote and item[consumed] == ',': + break + consumed += 1 + if is_find_end_quote: + raise SettingsError('Mismatched quotes') + val_string = item[:consumed].strip() + if merge_type in (list, str): + try: + float(val_string) + except ValueError: + if val_string == self.cli_parse_none_str: + val_string = 'null' + if val_string not in ('true', 'false', 'null') and not val_string.startswith('"'): + val_string = f'"{val_string}"' + merged_list.append(val_string) + else: + key, val = (kv for kv in val_string.split('=', 1)) + if key.startswith('"') and not key.endswith('"') and not val.startswith('"') and val.endswith('"'): + raise ValueError(f'Dictionary key=val parameter is a quoted string: {val_string}') + key, val = key.strip('"'), val.strip('"') + merged_list.append(json.dumps({key: val})) + return item[consumed:] + + def _verify_cli_flag_annotations(self, model: type[BaseModel], field_name: str, field_info: FieldInfo) -> None: + if _CliImplicitFlag in field_info.metadata: + cli_flag_name = 'CliImplicitFlag' + elif _CliExplicitFlag in field_info.metadata: + cli_flag_name = 'CliExplicitFlag' + elif _CliToggleFlag in field_info.metadata: + cli_flag_name = 'CliToggleFlag' + if not isinstance(field_info.default, bool): + raise SettingsError( + f'{cli_flag_name} argument {model.__name__}.{field_name} must have a default bool value' + ) + elif _CliDualFlag in field_info.metadata: + cli_flag_name = 'CliDualFlag' + else: + return + + if field_info.annotation is not bool: + raise SettingsError(f'{cli_flag_name} argument {model.__name__}.{field_name} is not of type bool') + + def _sort_arg_fields(self, model: type[BaseModel]) -> list[tuple[str, FieldInfo]]: + positional_variadic_arg: list[tuple[str, FieldInfo]] = [] + positional_args, subcommand_args, optional_args = [], [], [] + for field_name, field_info in _get_model_fields(model).items(): + if _CliSubCommand in field_info.metadata: + if not field_info.is_required(): + raise SettingsError(f'subcommand argument {model.__name__}.{field_name} has a default value') + else: + alias_names, *_ = _get_alias_names(field_name, field_info) + if len(alias_names) > 1: + raise SettingsError(f'subcommand argument {model.__name__}.{field_name} has multiple aliases') + field_types = [type_ for type_ in get_args(field_info.annotation) if type_ is not type(None)] + for field_type in field_types: + if not (is_model_class(field_type) or is_pydantic_dataclass(field_type)): + raise SettingsError( + f'subcommand argument {model.__name__}.{field_name} has type not derived from BaseModel' + ) + subcommand_args.append((field_name, field_info)) + elif _CliPositionalArg in field_info.metadata: + alias_names, *_ = _get_alias_names(field_name, field_info) + if len(alias_names) > 1: + raise SettingsError(f'positional argument {model.__name__}.{field_name} has multiple aliases') + is_append_action = _annotation_contains_types( + field_info.annotation, (list, set, dict, Sequence, Mapping), is_strip_annotated=True + ) + if not is_append_action: + positional_args.append((field_name, field_info)) + else: + positional_variadic_arg.append((field_name, field_info)) + else: + self._verify_cli_flag_annotations(model, field_name, field_info) + optional_args.append((field_name, field_info)) + + if positional_variadic_arg: + if len(positional_variadic_arg) > 1: + field_names = ', '.join([name for name, info in positional_variadic_arg]) + raise SettingsError(f'{model.__name__} has multiple variadic positional arguments: {field_names}') + elif subcommand_args: + field_names = ', '.join([name for name, info in positional_variadic_arg + subcommand_args]) + raise SettingsError( + f'{model.__name__} has variadic positional arguments and subcommand arguments: {field_names}' + ) + + return positional_args + positional_variadic_arg + subcommand_args + optional_args + + @property + def root_parser(self) -> T: + """The connected root parser instance.""" + return self._root_parser + + def _connect_parser_method( + self, parser_method: Callable[..., Any] | None, method_name: str, *args: Any, **kwargs: Any + ) -> Callable[..., Any]: + if ( + parser_method is not None + and self.case_sensitive is False + and method_name == 'parse_args_method' + and isinstance(self._root_parser, _CliInternalArgParser) + ): + + def parse_args_insensitive_method( + root_parser: _CliInternalArgParser, + args: list[str] | tuple[str, ...] | None = None, + namespace: Namespace | None = None, + ) -> Any: + insensitive_args = [] + for arg in shlex.split(shlex.join(args)) if args else []: + flag_prefix = rf'\{self.cli_flag_prefix_char}{{1,2}}' + matched = re.match(rf'^({flag_prefix}[^\s=]+)(.*)', arg) + if matched: + arg = matched.group(1).lower() + matched.group(2) + insensitive_args.append(arg) + return parser_method(root_parser, insensitive_args, namespace) + + return parse_args_insensitive_method + + elif parser_method is None: + + def none_parser_method(*args: Any, **kwargs: Any) -> Any: + raise SettingsError( + f'cannot connect CLI settings source root parser: {method_name} is set to `None` but is needed for connecting' + ) + + return none_parser_method + + else: + return parser_method + + def _connect_group_method(self, add_argument_group_method: Callable[..., Any] | None) -> Callable[..., Any]: + add_argument_group = self._connect_parser_method(add_argument_group_method, 'add_argument_group_method') + + def add_group_method(parser: Any, **kwargs: Any) -> Any: + if not kwargs.pop('_is_cli_mutually_exclusive_group'): + kwargs.pop('required') + return add_argument_group(parser, **kwargs) + else: + main_group_kwargs = {arg: kwargs.pop(arg) for arg in ['title', 'description'] if arg in kwargs} + main_group_kwargs['title'] += ' (mutually exclusive)' + group = add_argument_group(parser, **main_group_kwargs) + if not hasattr(group, 'add_mutually_exclusive_group'): + raise SettingsError( + 'cannot connect CLI settings source root parser: ' + 'group object is missing add_mutually_exclusive_group but is needed for connecting' + ) + return group.add_mutually_exclusive_group(**kwargs) + + return add_group_method + + def _connect_root_parser( + self, + root_parser: T, + parse_args_method: Callable[..., Any] | None, + add_argument_method: Callable[..., Any] | None = ArgumentParser.add_argument, + add_argument_group_method: Callable[..., Any] | None = ArgumentParser.add_argument_group, + add_parser_method: Callable[..., Any] | None = _SubParsersAction.add_parser, + add_subparsers_method: Callable[..., Any] | None = ArgumentParser.add_subparsers, + format_help_method: Callable[..., Any] | None = ArgumentParser.format_help, + formatter_class: Any = RawDescriptionHelpFormatter, + ) -> None: + self._cli_unknown_args: dict[str, list[str]] = {} + + def _parse_known_args(*args: Any, **kwargs: Any) -> Namespace: + args, unknown_args = ArgumentParser.parse_known_args(*args, **kwargs) + for dest in self._cli_unknown_args: + self._cli_unknown_args[dest] = unknown_args + return cast(Namespace, args) + + self._root_parser = root_parser + _is_default_parse_args = parse_args_method is None + if parse_args_method is None: + parse_args_method = _parse_known_args if self.cli_ignore_unknown_args else ArgumentParser.parse_args + self._parse_args = self._connect_parser_method(parse_args_method, 'parse_args_method') + self._add_argument = self._connect_parser_method(add_argument_method, 'add_argument_method') + self._add_group = self._connect_group_method(add_argument_group_method) + self._add_parser = self._connect_parser_method(add_parser_method, 'add_parser_method') + self._add_subparsers = self._connect_parser_method(add_subparsers_method, 'add_subparsers_method') + self._format_help = self._connect_parser_method(format_help_method, 'format_help_method') + self._formatter_class = formatter_class + self._cli_dict_args: dict[str, type[Any] | None] = {} + self._parser_map: defaultdict[str | FieldInfo, dict[int | None | str | type[BaseModel], _CliArg]] = defaultdict( + dict + ) + self._add_default_help() + self._add_parser_args( + parser=self.root_parser, + model=self.settings_cls, + added_args=[], + arg_prefix=self.env_prefix, + subcommand_prefix=self.env_prefix, + group=None, + alias_prefixes=[], + model_default=PydanticUndefined, + model_path=set(), + env_prefixes=self._root_env_prefixes(), + ) + + # If subcommands registered CliUnknownArgs fields but root does not have + # cli_ignore_unknown_args=True, upgrade to parse_known_args so that argparse + # does not error on unknown arguments destined for a subcommand. + if self._cli_unknown_args and not self.cli_ignore_unknown_args and _is_default_parse_args: + self._parse_args = self._connect_parser_method(_parse_known_args, 'parse_args_method') + + def _add_default_help(self) -> None: + if isinstance(self._root_parser, _CliInternalArgParser): + if not self.cli_prefix: + for field_name, field_info in _get_model_fields(self.settings_cls).items(): + alias_names, *_ = _get_alias_names(field_name, field_info, case_sensitive=self.case_sensitive) + if 'help' in alias_names: + return + + self._add_argument( + self.root_parser, + f'{self._cli_flag_prefix[:1]}h', + f'{self._cli_flag_prefix[:2]}help', + action='help', + default=SUPPRESS, + help='show this help message and exit', + ) + + def _add_parser_args( # noqa: C901 + self, + parser: Any, + model: type[BaseModel], + added_args: list[str], + arg_prefix: str, + subcommand_prefix: str, + group: Any, + alias_prefixes: list[str], + model_default: Any, + is_model_suppressed: bool = False, + discriminator_vals: dict[str, set[Any]] | None = None, + is_last_discriminator: bool = True, + model_path: set[type[BaseModel]] | None = None, + env_prefixes: tuple[tuple[str, bool], ...] = (), + ) -> ArgumentParser: + if discriminator_vals is None: + discriminator_vals = {} + if model_path is None: + model_path = set() + model_path = model_path | {model} + subparsers: Any = None + alias_path_args: dict[str, int | None] = {} + # Ignore model default if the default is a model and not a subclass of the current model. + model_default = ( + None + if ( + (is_model_class(type(model_default)) or is_pydantic_dataclass(type(model_default))) + and not issubclass(type(model_default), model) + ) + else model_default + ) + for field_name, field_info in self._sort_arg_fields(model): + arg = _CliArg( + parser=parser, + field_info=field_info, + parser_map=self._parser_map, + model=model, + field_name=field_name, + arg_prefix=arg_prefix, + case_sensitive=self.case_sensitive, + populate_by_name=self.config.get('populate_by_name', False) + or self.config.get('validate_by_name', False), + hide_none_type=self.cli_hide_none_type, + kebab_case=self.cli_kebab_case, + enable_decoding=self.config.get('enable_decoding'), + env_prefix_len=self.env_prefix_len, + ) + alias_path_args.update(arg.alias_paths) + + if arg.subcommand_dest: + for sub_model in arg.sub_models: + subcommand_alias = arg.subcommand_alias(sub_model) + subcommand_arg = self._parser_map[arg.subcommand_dest][subcommand_alias] + subcommand_arg.args = [subcommand_alias] + subcommand_arg.kwargs['allow_abbrev'] = False + subcommand_arg.kwargs['formatter_class'] = self._formatter_class + subcommand_arg.kwargs['description'] = _get_model_description(sub_model) + subcommand_arg.kwargs['help'] = None if len(arg.sub_models) > 1 else field_info.description + if self.cli_use_class_docs_for_groups: + subcommand_arg.kwargs['help'] = _get_model_description(sub_model) + + subparsers = ( + self._add_subparsers( + parser, + title='subcommands', + dest=f'{arg_prefix}:subcommand', + description=field_info.description if len(arg.sub_models) > 1 else None, + ) + if subparsers is None + else subparsers + ) + + if hasattr(subparsers, 'metavar'): + subparsers.metavar = ( + f'{subparsers.metavar[:-1]},{subcommand_alias}}}' + if subparsers.metavar + else f'{{{subcommand_alias}}}' + ) + + subcommand_arg.parser = self._add_parser(subparsers, *subcommand_arg.args, **subcommand_arg.kwargs) + self._add_parser_args( + parser=subcommand_arg.parser, + model=sub_model, + added_args=[], + arg_prefix=f'{arg.dest}.', + subcommand_prefix=f'{subcommand_prefix}{arg.preferred_alias}.', + group=None, + alias_prefixes=[], + model_default=PydanticUndefined, + model_path=model_path, + env_prefixes=self._nested_env_prefixes( + self._get_field_env_var_names(field_name, field_info, env_prefixes) + ), + ) + else: + flag_prefix: str = self._cli_flag_prefix + env_var_names = self._get_field_env_var_names(field_name, field_info, env_prefixes) + arg.kwargs['dest'] = arg.dest + arg.kwargs['default'] = CLI_SUPPRESS + arg.kwargs['help'] = self._help_format( + field_name, + field_info, + model_default, + is_model_suppressed, + env_var_names, + ) + arg.kwargs['metavar'] = self._metavar_format(field_info.annotation) + arg.kwargs['required'] = ( + self.cli_enforce_required and field_info.is_required() and model_default is PydanticUndefined + ) + + arg_names = self._get_arg_names( + arg, + subcommand_prefix, + alias_prefixes, + added_args, + discriminator_vals, + is_last_discriminator, + ) + if not arg_names or (arg.kwargs['dest'] in added_args): + continue + + self._convert_append_action(arg.kwargs, field_info, arg.is_append_action) + + if _CliPositionalArg in field_info.metadata: + arg_names, flag_prefix = self._convert_positional_arg( + arg.kwargs, field_info, arg.preferred_alias, model_default + ) + + self._convert_bool_flag(arg.kwargs, field_info, model_default) + + non_recursive_sub_models = [m for m in arg.sub_models if m not in model_path] + if ( + arg.is_parser_submodel + and not getattr(field_info.annotation, '__pydantic_root_model__', False) + and non_recursive_sub_models + ): + self._add_parser_submodels( + parser, + model, + non_recursive_sub_models, + added_args, + arg_prefix, + subcommand_prefix, + flag_prefix, + arg_names, + arg.kwargs, + field_name, + field_info, + arg.alias_names, + model_default=model_default, + is_model_suppressed=is_model_suppressed, + model_path=model_path, + env_var_names=env_var_names, + ) + elif _CliUnknownArgs in field_info.metadata: + self._cli_unknown_args[arg.kwargs['dest']] = [] + elif not arg.is_alias_path_only: + if isinstance(group, dict): + group = self._add_group(parser, **group) + context = parser if group is None else group + if arg.kwargs.get('action') == 'store_false': + flag_prefix += 'no-' + arg.args = [f'{flag_prefix[: 1 if len(name) == 1 else None]}{name}' for name in arg_names] + self._add_env_var_names(arg.dest, arg.kwargs['help'], env_var_names) + self._add_argument(context, *arg.args, **arg.kwargs) + added_args += list(arg_names) + + self._add_parser_alias_paths(parser, alias_path_args, added_args, arg_prefix, subcommand_prefix, group) + return parser + + def _convert_append_action(self, kwargs: dict[str, Any], field_info: FieldInfo, is_append_action: bool) -> None: + if is_append_action: + kwargs['action'] = 'append' + if _annotation_contains_types(field_info.annotation, (dict, Mapping), is_strip_annotated=True): + self._cli_dict_args[kwargs['dest']] = field_info.annotation + + def _convert_bool_flag(self, kwargs: dict[str, Any], field_info: FieldInfo, model_default: Any) -> None: + if kwargs['metavar'] == 'bool': + meta_bool_flags = [ + meta + for meta in field_info.metadata + if isinstance(meta, type) and issubclass(meta, _CliImplicitFlag | _CliExplicitFlag) + ] + if not meta_bool_flags and self.cli_implicit_flags: + meta_bool_flags = [_CliImplicitFlag] + if meta_bool_flags: + bool_flag = meta_bool_flags.pop() + if bool_flag is _CliImplicitFlag: + bool_flag = ( + _CliToggleFlag + if self.cli_implicit_flags == 'toggle' and isinstance(field_info.default, bool) + else _CliDualFlag + ) + if bool_flag is _CliDualFlag: + del kwargs['metavar'] + kwargs['action'] = BooleanOptionalAction + elif bool_flag is _CliToggleFlag: + del kwargs['metavar'] + kwargs['action'] = 'store_false' if field_info.default else 'store_true' + + def _convert_positional_arg( + self, kwargs: dict[str, Any], field_info: FieldInfo, preferred_alias: str, model_default: Any + ) -> tuple[list[str], str]: + flag_prefix = '' + arg_names = [kwargs['dest']] + kwargs['default'] = PydanticUndefined + kwargs['metavar'] = _CliArg.get_kebab_case(preferred_alias.upper(), self.cli_kebab_case) + + # Note: CLI positional args are always strictly required at the CLI. Therefore, use field_info.is_required in + # conjunction with model_default instead of the derived kwargs['required']. + is_required = field_info.is_required() and model_default is PydanticUndefined + if kwargs.get('action') == 'append': + del kwargs['action'] + kwargs['nargs'] = '+' if is_required else '*' + elif not is_required: + kwargs['nargs'] = '?' + + del kwargs['dest'] + del kwargs['required'] + return arg_names, flag_prefix + + def _get_arg_names( + self, + arg: _CliArg, + subcommand_prefix: str, + alias_prefixes: list[str], + added_args: list[str], + discriminator_vals: dict[str, set[Any]], + is_last_discriminator: bool, + ) -> list[str]: + arg_names: list[str] = [] + for prefix in [arg.arg_prefix] + alias_prefixes: + for name in arg.alias_names: + arg_name = _CliArg.get_kebab_case( + f'{prefix}{name}' + if subcommand_prefix == self.env_prefix + else f'{prefix.replace(subcommand_prefix, "", 1)}{name}', + self.cli_kebab_case, + ) + if arg_name not in added_args: + arg_names.append(arg_name) + + if self.cli_shortcuts: + for target, aliases in self.cli_shortcuts.items(): + if target in arg_names: + alias_list = [aliases] if isinstance(aliases, str) else aliases + arg_names.extend(alias for alias in alias_list if alias not in added_args) + + tags: set[Any] = set() + discriminators = discriminator_vals.get(arg.dest) + if discriminators is not None: + _annotation_contains_types( + arg.field_info.annotation, + (Literal,), + is_include_origin=True, + collect=tags, + ) + discriminators.update(chain.from_iterable(get_args(tag) for tag in tags)) + if not is_last_discriminator: + return [] + arg.kwargs['metavar'] = self._metavar_format(Literal[tuple(sorted(discriminators))]) + + return arg_names + + def _add_parser_submodels( + self, + parser: Any, + model: type[BaseModel], + sub_models: list[type[BaseModel]], + added_args: list[str], + arg_prefix: str, + subcommand_prefix: str, + flag_prefix: str, + arg_names: list[str], + kwargs: dict[str, Any], + field_name: str, + field_info: FieldInfo, + alias_names: tuple[str, ...], + model_default: Any, + is_model_suppressed: bool, + model_path: set[type[BaseModel]] | None = None, + env_var_names: tuple[str, ...] = (), + ) -> None: + if issubclass(model, CliMutuallyExclusiveGroup): + # Argparse has deprecated "calling add_argument_group() or add_mutually_exclusive_group() on a + # mutually exclusive group" (https://docs.python.org/3/library/argparse.html#mutual-exclusion). + # Since nested models result in a group add, raise an exception for nested models in a mutually + # exclusive group. + raise SettingsError('cannot have nested models in a CliMutuallyExclusiveGroup') + + model_group_kwargs: dict[str, Any] = {} + model_group_kwargs['title'] = f'{arg_names[0]} options' + model_group_kwargs['description'] = field_info.description + model_group_kwargs['required'] = kwargs['required'] + model_group_kwargs['_is_cli_mutually_exclusive_group'] = any( + issubclass(model, CliMutuallyExclusiveGroup) for model in sub_models + ) + if model_group_kwargs['_is_cli_mutually_exclusive_group'] and len(sub_models) > 1: + raise SettingsError('cannot use union with CliMutuallyExclusiveGroup') + if self.cli_use_class_docs_for_groups and len(sub_models) == 1: + model_group_kwargs['description'] = _get_model_description(sub_models[0]) + + if model_default is not PydanticUndefined: + if is_model_class(type(model_default)) or is_pydantic_dataclass(type(model_default)): + model_default = getattr(model_default, field_name) + else: + if field_info.default is not PydanticUndefined: + model_default = field_info.default + elif field_info.default_factory is not None: + model_default = field_info.default_factory + if model_default is None: + desc_header = f'default: {self.cli_parse_none_str} (undefined)' + if model_group_kwargs['description'] is not None: + model_group_kwargs['description'] = dedent(f'{desc_header}\n{model_group_kwargs["description"]}') + else: + model_group_kwargs['description'] = desc_header + + preferred_alias = alias_names[0] + is_model_suppressed = self._is_field_suppressed(field_info) or is_model_suppressed + if is_model_suppressed: + model_group_kwargs['description'] = CLI_SUPPRESS + added_args.append(arg_names[0]) + kwargs['required'] = False + kwargs['nargs'] = '?' + kwargs['const'] = '{}' + kwargs['help'] = ( + CLI_SUPPRESS + if is_model_suppressed or self.cli_avoid_json + else self._help_format_env_vars(f'set {arg_names[0]} from JSON string (default: {{}})', env_var_names) + ) + self._add_env_var_names(kwargs['dest'], kwargs['help'], env_var_names) + model_group = self._add_group(parser, **model_group_kwargs) + self._add_argument(model_group, *(f'{flag_prefix}{name}' for name in arg_names), **kwargs) + discriminator_vals: dict[str, set[Any]] = ( + {f'{arg_prefix}{preferred_alias}.{field_info.discriminator}': set()} if field_info.discriminator else {} + ) + for sub_model in sub_models: + self._add_parser_args( + parser=parser, + model=sub_model, + added_args=added_args, + arg_prefix=f'{arg_prefix}{preferred_alias}.', + subcommand_prefix=subcommand_prefix, + group=model_group, + alias_prefixes=[f'{arg_prefix}{name}.' for name in alias_names[1:]], + model_default=model_default, + is_model_suppressed=is_model_suppressed, + discriminator_vals=discriminator_vals, + is_last_discriminator=sub_model is sub_models[-1], + model_path=model_path, + env_prefixes=self._nested_env_prefixes(env_var_names), + ) + + def _add_parser_alias_paths( + self, + parser: Any, + alias_path_args: dict[str, int | None], + added_args: list[str], + arg_prefix: str, + subcommand_prefix: str, + group: Any, + ) -> None: + if alias_path_args: + context = parser + if group is not None: + context = self._add_group(parser, **group) if isinstance(group, dict) else group + for name, index in alias_path_args.items(): + arg_name = ( + f'{arg_prefix}{name}' + if subcommand_prefix == self.env_prefix + else f'{arg_prefix.replace(subcommand_prefix, "", 1)}{name}' + ) + kwargs: dict[str, Any] = {} + kwargs['default'] = CLI_SUPPRESS + kwargs['help'] = 'pydantic alias path' + kwargs['action'] = 'append' + kwargs['metavar'] = 'list' + if index is None: + kwargs['metavar'] = 'dict' + self._cli_dict_args[arg_name] = dict + args = [f'{self._cli_flag_prefix}{arg_name}'] + for arg in self._parser_map[arg_name].values(): + arg.args, arg.kwargs = args, kwargs + self._add_argument(context, *args, **kwargs) + added_args.append(arg_name) + + def _get_modified_args(self, obj: Any) -> tuple[str, ...]: + if not self.cli_hide_none_type: + return get_args(obj) + else: + return tuple([type_ for type_ in get_args(obj) if type_ is not type(None)]) + + def _metavar_format_choices(self, args: list[str], obj_qualname: str | None = None) -> str: + if 'JSON' in args: + args = args[: args.index('JSON') + 1] + [arg for arg in args[args.index('JSON') + 1 :] if arg != 'JSON'] + metavar = ','.join(args) + if obj_qualname: + return f'{obj_qualname}[{metavar}]' + else: + return metavar if len(args) == 1 else f'{{{metavar}}}' + + def _metavar_format_recurse(self, obj: Any) -> str: + """Pretty metavar representation of a type. Adapts logic from `pydantic._repr.display_as_type`.""" + obj = _strip_annotated(obj) + if _is_function(obj): + # If function is locally defined use __name__ instead of __qualname__ + return obj.__name__ if '' in obj.__qualname__ else obj.__qualname__ + elif obj is ...: + return '...' + elif isinstance(obj, Representation): + return repr(obj) + elif isinstance(obj, typing.ForwardRef) or typing_objects.is_typealiastype(obj): + return str(obj) + + if not isinstance(obj, (_typing_base, _WithArgsTypes, type)): + obj = obj.__class__ + + origin = get_origin(obj) + if is_union_origin(origin): + return self._metavar_format_choices(list(map(self._metavar_format_recurse, self._get_modified_args(obj)))) + elif typing_objects.is_literal(origin): + return self._metavar_format_choices(list(map(str, self._get_modified_args(obj)))) + elif _lenient_issubclass(obj, Enum): + return self._metavar_format_choices( + [_CliArg.get_kebab_case(name, self.cli_kebab_case == 'all') for name in obj.__members__] + ) + elif isinstance(obj, _WithArgsTypes): + return self._metavar_format_choices( + list(map(self._metavar_format_recurse, self._get_modified_args(obj))), + obj_qualname=obj.__qualname__ if hasattr(obj, '__qualname__') else str(obj), + ) + elif obj is type(None): + return self.cli_parse_none_str + elif is_model_class(obj) or is_pydantic_dataclass(obj): + return ( + self._metavar_format_recurse(_get_model_fields(obj)['root'].annotation) + if getattr(obj, '__pydantic_root_model__', False) + else 'JSON' + ) + elif isinstance(obj, type): + return obj.__qualname__ + else: + return repr(obj).replace('typing.', '').replace('typing_extensions.', '') + + def _metavar_format(self, obj: Any) -> str: + return self._metavar_format_recurse(obj).replace(', ', ',') + + def _root_env_prefixes(self) -> tuple[tuple[str, bool], ...]: + if self._env_settings_source is None: + return () + return ((self._env_settings_source.env_prefix, False),) + + def _nested_env_prefixes(self, env_var_names: tuple[str, ...]) -> tuple[tuple[str, bool], ...]: + if self._env_settings_source is None or not self._env_settings_source.env_nested_delimiter: + return () + return tuple( + (f'{env_name}{self._env_settings_source.env_nested_delimiter}', True) for env_name in env_var_names + ) + + def _display_env_var_name(self, env_name: str) -> str: + if self._env_settings_source is not None and not self._env_settings_source.case_sensitive: + return env_name.upper() + return env_name + + def _get_field_env_var_names( + self, + field_name: str, + field_info: FieldInfo, + env_prefixes: tuple[tuple[str, bool], ...], + ) -> tuple[str, ...]: + if ( + self._env_settings_source is None + or self._is_field_suppressed(field_info) + or _CliPositionalArg in field_info.metadata + or _CliUnknownArgs in field_info.metadata + ): + return () + + env_var_names: list[str] = [] + env_settings_source = copy.copy(self._env_settings_source) + for env_prefix, force_env_prefix in env_prefixes: + env_settings_source.env_prefix = env_prefix + normalized_env_prefix = env_settings_source._apply_case_sensitive(env_prefix) + for _, env_name, _ in env_settings_source._extract_field_info(field_info, field_name): + if force_env_prefix and normalized_env_prefix and not env_name.startswith(normalized_env_prefix): + env_name = f'{normalized_env_prefix}{env_name}' + display_env_name = self._display_env_var_name(env_name) + if display_env_name and display_env_name not in env_var_names: + env_var_names.append(display_env_name) + + return tuple(env_var_names) + + def _help_format_env_vars(self, help_text: str, env_var_names: tuple[str, ...]) -> str: + if not env_var_names: + return help_text + env_help = f'[env: {" | ".join(env_var_names)}]' + return f'{help_text} {env_help}' if help_text else env_help + + def _add_env_var_names(self, dest: str, help_text: str, env_var_names: tuple[str, ...]) -> None: + if env_var_names and help_text != CLI_SUPPRESS: + self._env_var_names[dest] = env_var_names + + def _help_format( + self, + field_name: str, + field_info: FieldInfo, + model_default: Any, + is_model_suppressed: bool, + env_var_names: tuple[str, ...] = (), + ) -> str: + _help = field_info.description if field_info.description else '' + if is_model_suppressed or self._is_field_suppressed(field_info): + return CLI_SUPPRESS + + if field_info.is_required() and model_default in (PydanticUndefined, None): + if _CliPositionalArg not in field_info.metadata: + ifdef = 'ifdef: ' if model_default is None else '' + _help += f' ({ifdef}required)' if _help else f'({ifdef}required)' + else: + default = f'(default: {self.cli_parse_none_str})' + if is_model_class(type(model_default)) or is_pydantic_dataclass(type(model_default)): + default = f'(default: {getattr(model_default, field_name)})' + elif model_default not in (PydanticUndefined, None) and _is_function(model_default): + default = f'(default factory: {self._metavar_format(model_default)})' + elif field_info.default not in (PydanticUndefined, None): + enum_name = _annotation_enum_val_to_name(field_info.annotation, field_info.default) + default = f'(default: {field_info.default if enum_name is None else enum_name})' + elif field_info.default_factory is not None: + default = f'(default factory: {self._metavar_format(field_info.default_factory)})' + + if _CliToggleFlag not in field_info.metadata: + _help += f' {default}' if _help else default + _help = self._help_format_env_vars(_help, env_var_names) + return _help.replace('%', '%%') if issubclass(type(self._root_parser), ArgumentParser) else _help + + def _is_field_suppressed(self, field_info: FieldInfo) -> bool: + _help = field_info.description if field_info.description else '' + return _help == CLI_SUPPRESS or CLI_SUPPRESS in field_info.metadata + + def _update_alias_path_only_default( + self, arg_name: str, value: Any, field_info: FieldInfo, alias_path_only_defaults: dict[str, Any] + ) -> list[Any] | dict[str, Any]: + alias_path: AliasPath = next( + alias if isinstance(alias, AliasPath) else cast(AliasPath, alias.choices[0]) + for alias in (field_info.alias, field_info.validation_alias) + if isinstance(alias, (AliasPath, AliasChoices)) + ) + + alias_nested_paths: list[str] = alias_path.path[1:-1] # type: ignore + if not alias_nested_paths: + alias_path_only_defaults.setdefault(arg_name, []) + alias_default = alias_path_only_defaults[arg_name] + else: + alias_path_only_defaults.setdefault(arg_name, {}) + current_path = alias_path_only_defaults[arg_name] + + for nested_path in alias_nested_paths[:-1]: + current_path.setdefault(nested_path, {}) + current_path = current_path[nested_path] + current_path.setdefault(alias_nested_paths[-1], []) + alias_default = current_path[alias_nested_paths[-1]] + + alias_path_index = cast(int, alias_path.path[-1]) + alias_default.extend([''] * max(alias_path_index + 1 - len(alias_default), 0)) + alias_default[alias_path_index] = value + return alias_path_only_defaults[arg_name] + + def _coerce_value_styles( + self, + model_default: Any, + value: str | list[Any] | dict[str, Any], + list_style: Literal['json', 'argparse', 'lazy'] = 'json', + dict_style: Literal['json', 'env'] = 'json', + ) -> list[str | list[Any] | dict[str, Any]]: + values = [value] + if isinstance(value, str): + if isinstance(model_default, list): + if list_style == 'lazy': + values = [','.join(f'{v}' for v in json.loads(value))] + elif list_style == 'argparse': + values = [f'{v}' for v in json.loads(value)] + elif isinstance(model_default, dict) and dict_style == 'env': + values = [f'{k}={v}' for k, v in json.loads(value).items()] + return values + + @staticmethod + def _flatten_serialized_args( + serialized_args: dict[str, list[str]], + positionals_first: bool, + ) -> list[str]: + return ( + serialized_args['optional'] + serialized_args['positional'] + if not positionals_first + else serialized_args['positional'] + serialized_args['optional'] + ) + serialized_args['subcommand'] + + def _serialized_args( + self, + model: PydanticModel, + list_style: Literal['json', 'argparse', 'lazy'] = 'json', + dict_style: Literal['json', 'env'] = 'json', + positionals_first: bool = False, + _is_submodel: bool = False, + ) -> dict[str, list[str]]: + alias_path_only_defaults: dict[str, Any] = {} + optional_args: list[str | list[Any] | dict[str, Any]] = [] + positional_args: list[str | list[Any] | dict[str, Any]] = [] + subcommand_args: list[str] = [] + for field_name, field_info in _get_model_fields(type(model) if _is_submodel else self.settings_cls).items(): + model_default = getattr(model, field_name) + if field_info.default == model_default: + continue + if _CliSubCommand in field_info.metadata and model_default is None: + continue + arg = next(iter(self._parser_map[field_info].values())) + if arg.subcommand_dest: + subcommand_args.append(arg.subcommand_alias(type(model_default))) + sub_args = self._serialized_args( + model_default, + list_style=list_style, + dict_style=dict_style, + positionals_first=positionals_first, + _is_submodel=True, + ) + subcommand_args += self._flatten_serialized_args(sub_args, positionals_first) + continue + if is_model_class(type(model_default)) or is_pydantic_dataclass(type(model_default)): + sub_args = self._serialized_args( + model_default, + list_style=list_style, + dict_style=dict_style, + positionals_first=positionals_first, + _is_submodel=True, + ) + optional_args += sub_args['optional'] + positional_args += sub_args['positional'] + subcommand_args += sub_args['subcommand'] + continue + + matched = re.match(r'(-*)(.+)', arg.preferred_arg_name) + flag_chars, arg_name = matched.groups() if matched else ('', '') + value: str | list[Any] | dict[str, Any] = ( + json.dumps(model_default) if isinstance(model_default, (dict, list, set)) else str(model_default) + ) + + if arg.is_alias_path_only: + # For alias path only, we won't know the complete value until we've finished parsing the entire class. In + # this case, insert value as a non-string reference pointing to the relevant alias_path_only_defaults + # entry and convert into completed string value later. + value = self._update_alias_path_only_default(arg_name, value, field_info, alias_path_only_defaults) + + if _CliPositionalArg in field_info.metadata: + for value in model_default if isinstance(model_default, list) else [model_default]: + value = json.dumps(value) if isinstance(value, (dict, list, set)) else str(value) + positional_args.append(value) + continue + + # Note: prepend 'no-' for boolean optional action flag if model_default value is False and flag is not a short option + if arg.kwargs.get('action') == BooleanOptionalAction and model_default is False and flag_chars == '--': + flag_chars += 'no-' + + for coerced_value in self._coerce_value_styles( + model_default, value, list_style=list_style, dict_style=dict_style + ): + optional_args.append(f'{flag_chars}{arg_name}') + + # If implicit bool flag, do not add a value + if arg.kwargs.get('action') not in (BooleanOptionalAction, 'store_true', 'store_false'): + optional_args.append(coerced_value) + + return { + 'optional': [json.dumps(value) if not isinstance(value, str) else value for value in optional_args], + 'positional': [json.dumps(value) if not isinstance(value, str) else value for value in positional_args], + 'subcommand': subcommand_args, + } diff --git a/source/target-vendors/cp312/pydantic_settings/sources/providers/dotenv.py b/source/target-vendors/cp312/pydantic_settings/sources/providers/dotenv.py new file mode 100644 index 0000000..d4b7848 --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/sources/providers/dotenv.py @@ -0,0 +1,210 @@ +"""Dotenv file settings source.""" + +from __future__ import annotations as _annotations + +import logging +import os +import warnings +from collections.abc import Mapping +from pathlib import Path +from typing import TYPE_CHECKING, Any + +from dotenv import dotenv_values +from pydantic._internal._typing_extra import ( # type: ignore[attr-defined] + get_origin, +) +from typing_inspection.introspection import is_union_origin + +from ...utils import _settings_debug_enabled, logger +from ..types import ENV_FILE_SENTINEL, DotenvFiltering, DotenvType, EnvPrefixTarget +from ..utils import ( + InitState, + _annotation_is_complex, + _union_is_complex, + parse_env_vars, +) +from .env import EnvSettingsSource + +if TYPE_CHECKING: + from pydantic_settings.main import BaseSettings + + +class DotEnvSettingsSource(EnvSettingsSource): + """ + Source class for loading settings values from env files. + """ + + def __init__( + self, + settings_cls: type[BaseSettings], + env_file: DotenvType | None = ENV_FILE_SENTINEL, + env_file_encoding: str | None = None, + dotenv_filtering: DotenvFiltering | None = None, + case_sensitive: bool | None = None, + env_prefix: str | None = None, + env_prefix_target: EnvPrefixTarget | None = None, + env_nested_delimiter: str | None = None, + env_nested_max_split: int | None = None, + env_ignore_empty: bool | None = None, + env_parse_none_str: str | None = None, + env_parse_enums: bool | None = None, + _init_state: InitState | None = None, + ) -> None: + self.env_file = env_file if env_file != ENV_FILE_SENTINEL else settings_cls.model_config.get('env_file') + self.env_file_encoding = ( + env_file_encoding if env_file_encoding is not None else settings_cls.model_config.get('env_file_encoding') + ) + self.dotenv_filtering = ( + dotenv_filtering if dotenv_filtering is not None else settings_cls.model_config.get('dotenv_filtering') + ) + super().__init__( + settings_cls, + case_sensitive, + env_prefix, + env_prefix_target, + env_nested_delimiter, + env_nested_max_split, + env_ignore_empty, + env_parse_none_str, + env_parse_enums, + _init_state, + ) + + def _load_env_vars(self) -> Mapping[str, str | None]: + return self._read_env_files() + + @staticmethod + def _static_read_env_file( + file_path: Path, + *, + encoding: str | None = None, + case_sensitive: bool = False, + ignore_empty: bool = False, + parse_none_str: str | None = None, + ) -> Mapping[str, str | None]: + file_vars: dict[str, str | None] = dotenv_values(file_path, encoding=encoding or 'utf8') + return parse_env_vars(file_vars, case_sensitive, ignore_empty, parse_none_str) + + def _read_env_file( + self, + file_path: Path, + ) -> Mapping[str, str | None]: + return self._static_read_env_file( + file_path, + encoding=self.env_file_encoding, + case_sensitive=self.case_sensitive, + ignore_empty=self.env_ignore_empty, + parse_none_str=self.env_parse_none_str, + ) + + def _read_env_files(self) -> Mapping[str, str | None]: + env_files = self.env_file + if env_files is None: + return {} + + if isinstance(env_files, (str, os.PathLike)): + env_files = [env_files] + + debug = _settings_debug_enabled() and logger.isEnabledFor(logging.DEBUG) + + dotenv_vars: dict[str, str | None] = {} + for env_file in env_files: + env_path = Path(env_file).expanduser() + if env_path.is_file() or env_path.is_fifo(): + if debug: + logger.debug('Loading env file: %s', env_path.resolve()) + dotenv_vars.update(self._read_env_file(env_path)) + elif debug: + logger.debug('Env file not found, skipping: %s', env_path.resolve()) + + return dotenv_vars + + def __call__(self) -> dict[str, Any]: # noqa: C901 + data: dict[str, Any] = super().__call__() + if self.dotenv_filtering == 'only_existing': + # This case behaves like the EnvSettingsSource, only return existing fields + return data + if self.dotenv_filtering == 'match_prefix': + # In this case add all env vars that match the prefix, stripping the prefix. + prefix = self._apply_case_sensitive(self.env_prefix) + for env_name, env_value in self.env_vars.items(): + if env_name.startswith(prefix): + normalized_env_name = env_name[len(self.env_prefix) :] + if ( + self.env_nested_delimiter + and self.env_nested_delimiter in normalized_env_name + and normalized_env_name.partition(self.env_nested_delimiter)[0] in data + ): + continue + if normalized_env_name not in data: + data[normalized_env_name] = env_value + return data + + is_extra_allowed = self.config.get('extra') != 'forbid' + + # As `extra` config is allowed in dotenv settings source, We have to + # update data with extra env variables from dotenv file. + for env_name, env_value in self.env_vars.items(): + if not env_value or env_name in data or (self.env_prefix and env_name in self.settings_cls.model_fields): + continue + env_used = False + for field_name, field in self.settings_cls.model_fields.items(): + for _, field_env_name, _ in self._extract_field_info(field, field_name): + if env_name == field_env_name or ( + ( + _annotation_is_complex(field.annotation, field.metadata, self._init_state) + or ( + is_union_origin(get_origin(field.annotation)) + and _union_is_complex(field.annotation, field.metadata, self._init_state) + ) + ) + # A var only belongs to a complex field when it sits at the nested + # delimiter boundary (`db...`, matching what explode_env_vars + # consumes). A bare name-prefix match (e.g. `dbx_token` vs field `db`) + # must not claim the var, otherwise it is silently dropped instead of + # being kept as an extra. + and self.env_nested_delimiter + and env_name.startswith(f'{field_env_name}{self.env_nested_delimiter}') + ): + env_used = True + break + if env_used: + break + if not env_used: + if is_extra_allowed and env_name.startswith(self.env_prefix): + # env_prefix should be respected and removed from the env_name + normalized_env_name = env_name[len(self.env_prefix) :] + data[normalized_env_name] = env_value + else: + data[env_name] = env_value + return data + + def __repr__(self) -> str: + return ( + f'{self.__class__.__name__}(env_file={self.env_file!r}, env_file_encoding={self.env_file_encoding!r}, ' + f'env_nested_delimiter={self.env_nested_delimiter!r}, env_prefix_len={self.env_prefix_len!r})' + ) + + +def read_env_file( + file_path: Path, + *, + encoding: str | None = None, + case_sensitive: bool = False, + ignore_empty: bool = False, + parse_none_str: str | None = None, +) -> Mapping[str, str | None]: + warnings.warn( + 'read_env_file will be removed in the next version, use DotEnvSettingsSource._static_read_env_file if you must', + DeprecationWarning, + ) + return DotEnvSettingsSource._static_read_env_file( + file_path, + encoding=encoding, + case_sensitive=case_sensitive, + ignore_empty=ignore_empty, + parse_none_str=parse_none_str, + ) + + +__all__ = ['DotEnvSettingsSource', 'read_env_file'] diff --git a/source/target-vendors/cp312/pydantic_settings/sources/providers/env.py b/source/target-vendors/cp312/pydantic_settings/sources/providers/env.py new file mode 100644 index 0000000..e6c3993 --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/sources/providers/env.py @@ -0,0 +1,382 @@ +from __future__ import annotations as _annotations + +import json +import os +from collections.abc import Mapping +from typing import ( + TYPE_CHECKING, + Any, + get_args, + get_origin, +) + +from pydantic import AliasChoices, AliasPath, Json, TypeAdapter, ValidationError +from pydantic._internal._utils import deep_update, is_model_class +from pydantic.dataclasses import is_pydantic_dataclass +from pydantic.fields import FieldInfo +from typing_inspection.introspection import is_union_origin + +from ...utils import _lenient_issubclass +from ..base import PydanticBaseEnvSettingsSource +from ..types import EnvNoneType, EnvPrefixTarget +from ..utils import ( + InitState, + _annotation_contains_types, + _annotation_enum_name_to_val, + _annotation_is_complex, + _get_model_fields, + _literal_has_numeric_enum, + _union_has_strict_types, + _union_is_complex, + parse_env_vars, +) + +if TYPE_CHECKING: + from pydantic_settings.main import BaseSettings + + +def _environ_is_case_insensitive() -> bool: + """Whether ``os.environ`` is the OS-backed, case-insensitive mapping (i.e. on Windows). + + When ``os.environ`` has been replaced (e.g. patched to a plain ``dict`` in tests), the + requested case sensitivity is honored as-is. + """ + return os.name == 'nt' and isinstance(os.environ, os._Environ) + + +class EnvSettingsSource(PydanticBaseEnvSettingsSource): + """ + Source class for loading settings values from environment variables. + """ + + def __init__( + self, + settings_cls: type[BaseSettings], + case_sensitive: bool | None = None, + env_prefix: str | None = None, + env_prefix_target: EnvPrefixTarget | None = None, + env_nested_delimiter: str | None = None, + env_nested_max_split: int | None = None, + env_ignore_empty: bool | None = None, + env_parse_none_str: str | None = None, + env_parse_enums: bool | None = None, + _init_state: InitState | None = None, + ) -> None: + super().__init__( + settings_cls, + case_sensitive, + env_prefix, + env_prefix_target, + env_ignore_empty, + env_parse_none_str, + env_parse_enums, + _init_state, + ) + self.env_nested_delimiter = ( + env_nested_delimiter if env_nested_delimiter is not None else self.config.get('env_nested_delimiter') + ) + self.env_nested_max_split = ( + env_nested_max_split if env_nested_max_split is not None else self.config.get('env_nested_max_split') + ) + self.maxsplit = (self.env_nested_max_split or 0) - 1 + self.env_prefix_len = len(self.env_prefix) + + self.env_vars = self._load_env_vars() + + def _load_env_vars(self) -> Mapping[str, str | None]: + # On Windows, ``os.environ`` is case-insensitive: variable names are normalized to + # upper-case and looked up regardless of case. Matching environment variables + # case-sensitively is therefore impossible, so we fall back to case-insensitive + # matching to avoid spurious "field required" errors (see #295). ``.env`` files do + # preserve case and are handled by ``DotEnvSettingsSource``, which overrides this method. + if self.case_sensitive and _environ_is_case_insensitive(): + self.case_sensitive = False + return parse_env_vars(os.environ, self.case_sensitive, self.env_ignore_empty, self.env_parse_none_str) + + def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: + """ + Gets the value for field from environment variables and a flag to determine whether value is complex. + + Args: + field: The field. + field_name: The field name. + + Returns: + A tuple that contains the value (`None` if not found), key, and + a flag to determine whether value is complex. + """ + + env_val: str | None = None + for field_key, env_name, value_is_complex in self._extract_field_info(field, field_name): + env_val = self.env_vars.get(env_name) + if env_val is not None: + break + + return env_val, field_key, value_is_complex + + def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any: + """ + Prepare value for the field. + + * Extract value for nested field. + * Deserialize value to python object for complex field. + + Args: + field: The field. + field_name: The field name. + + Returns: + A tuple contains prepared value for the field. + + Raises: + ValuesError: When There is an error in deserializing value for complex field. + """ + is_complex, allow_parse_failure = self._field_is_complex(field) + if self.env_parse_enums: + enum_val = _annotation_enum_name_to_val(field.annotation, value) + value = value if enum_val is None else enum_val + + if is_complex or value_is_complex: + if isinstance(value, EnvNoneType): + return value + elif value is None: + # field is complex but no value found so far, try explode_env_vars + env_val_built = self.explode_env_vars(field_name, field, self.env_vars) + if env_val_built: + return env_val_built + else: + # field is complex and there's a value, decode that as JSON, then add explode_env_vars + try: + value = self.decode_complex_value(field_name, field, value) + except ValueError: + if not allow_parse_failure: + raise + + if isinstance(value, dict): + return deep_update(value, self.explode_env_vars(field_name, field, self.env_vars)) + else: + return value + elif value is not None: + # simplest case, field is not complex, we only need to add the value if it was found + return self._coerce_env_val_strict(field, value) + + def _matches_alias_path_head(self, field: FieldInfo | Any | None, key: str) -> bool: + """ + Whether ``key`` matches the first element of a multi-part ``AliasPath`` on ``field``. + + When a nested field is aliased via e.g. ``AliasPath('path', 0)``, the env value found + under ``path`` is a container that must be JSON-decoded before pydantic can navigate + into it (see #670). This mirrors the complexity that ``_extract_field_info`` already + reports for top-level ``AliasPath`` fields. + + Args: + field: The field (or raw type) matched for ``key``. + key: The (case-normalized) env name segment matched for the field. + + Returns: + Whether ``key`` is the head of a multi-element ``AliasPath``. + """ + if not isinstance(field, FieldInfo): + return False + v_alias = field.validation_alias + paths: list[tuple[str | int, ...]] = [] + if isinstance(v_alias, AliasPath): + paths.append(tuple(v_alias.path)) + elif isinstance(v_alias, AliasChoices): + paths.extend(tuple(choice.path) for choice in v_alias.choices if isinstance(choice, AliasPath)) + for path in paths: + head = path[0] + if len(path) > 1 and isinstance(head, str) and self._apply_case_sensitive(head) == key: + return True + return False + + def _field_is_complex(self, field: FieldInfo) -> tuple[bool, bool]: + """ + Find out if a field is complex, and if so whether JSON errors should be ignored + """ + if self.field_is_complex(field): + allow_parse_failure = False + elif is_union_origin(get_origin(field.annotation)) and _union_is_complex( + field.annotation, field.metadata, self._init_state + ): + allow_parse_failure = True + else: + return False, False + + return True, allow_parse_failure + + # Default value of `case_sensitive` is `None`, because we don't want to break existing behavior. + # We have to change the method to a non-static method and use + # `self.case_sensitive` instead in V3. + def next_field( + self, field: FieldInfo | Any | None, key: str, case_sensitive: bool | None = None + ) -> FieldInfo | None: + """ + Find the field in a sub model by key(env name) + + By having the following models: + + ```py + class SubSubModel(BaseSettings): + dvals: Dict + + class SubModel(BaseSettings): + vals: list[str] + sub_sub_model: SubSubModel + + class Cfg(BaseSettings): + sub_model: SubModel + ``` + + Then: + next_field(sub_model, 'vals') Returns the `vals` field of `SubModel` class + next_field(sub_model, 'sub_sub_model') Returns `sub_sub_model` field of `SubModel` class + + Args: + field: The field. + key: The key (env name). + case_sensitive: Whether to search for key case sensitively. + + Returns: + Field if it finds the next field otherwise `None`. + """ + if not field: + return None + + annotation = field.annotation if isinstance(field, FieldInfo) else field + for type_ in get_args(annotation): + type_has_key = self.next_field(type_, key, case_sensitive) + if type_has_key: + return type_has_key + if _lenient_issubclass(get_origin(annotation), dict): + # get value type if it's a dict + return get_args(annotation)[-1] + elif is_model_class(annotation) or is_pydantic_dataclass(annotation): # type: ignore[arg-type] + fields = _get_model_fields(annotation) + # `case_sensitive is None` is here to be compatible with the old behavior. + # Has to be removed in V3. + for field_name, f in fields.items(): + for _, env_name, _ in self._extract_field_info(f, field_name): + if case_sensitive is None or case_sensitive: + if field_name == key or env_name == key: + return f + elif field_name.lower() == key.lower() or env_name.lower() == key.lower(): + return f + return None + + def explode_env_vars(self, field_name: str, field: FieldInfo, env_vars: Mapping[str, str | None]) -> dict[str, Any]: # noqa: C901 + """ + Process env_vars and extract the values of keys containing env_nested_delimiter into nested dictionaries. + + This is applied to a single field, hence filtering by env_var prefix. + + Args: + field_name: The field name. + field: The field. + env_vars: Environment variables. + + Returns: + A dictionary contains extracted values from nested env values. + """ + if not self.env_nested_delimiter: + return {} + + ann = field.annotation + is_dict = ann is dict or _lenient_issubclass(get_origin(ann), dict) + + prefixes = [ + f'{env_name}{self.env_nested_delimiter}' for _, env_name, _ in self._extract_field_info(field, field_name) + ] + result: dict[str, Any] = {} + for env_name, env_val in env_vars.items(): + try: + prefix = next(prefix for prefix in prefixes if env_name.startswith(prefix)) + except StopIteration: + continue + # we remove the prefix before splitting in case the prefix has characters in common with the delimiter + env_name_without_prefix = env_name[len(prefix) :] + *keys, last_key = env_name_without_prefix.split(self.env_nested_delimiter, self.maxsplit) + env_var = result + target_field: FieldInfo | None = field + for key in keys: + target_field = self.next_field(target_field, key, self.case_sensitive) + if isinstance(env_var, dict): + env_var = env_var.setdefault(key, {}) + + # get proper field with last_key + target_field = self.next_field(target_field, last_key, self.case_sensitive) + + # check if env_val maps to a complex field and if so, parse the env_val + if (target_field or is_dict) and env_val: + if isinstance(target_field, FieldInfo): + is_complex, allow_json_failure = self._field_is_complex(target_field) + if not is_complex and self._matches_alias_path_head(target_field, last_key): + # The env value sits under an AliasPath head (e.g. AliasPath('path', 0)); + # decode it so pydantic can navigate into the container (see #670). + is_complex, allow_json_failure = True, True + if self.env_parse_enums: + enum_val = _annotation_enum_name_to_val(target_field.annotation, env_val) + env_val = env_val if enum_val is None else enum_val + elif target_field: + # target_field is a raw type (e.g. from dict value type annotation) + is_complex = _annotation_is_complex(target_field, [], self._init_state) + allow_json_failure = True + else: + # nested field type is dict + is_complex, allow_json_failure = True, True + if is_complex: + try: + field_info = target_field if isinstance(target_field, FieldInfo) else None + env_val = self.decode_complex_value(last_key, field_info, env_val) # type: ignore + except ValueError: + if not allow_json_failure: + raise + if isinstance(env_var, dict) and ( + last_key not in env_var or not isinstance(env_val, EnvNoneType) or env_var[last_key] == {} + ): + env_var[last_key] = self._coerce_env_val_strict(target_field, env_val) + return result + + def _coerce_env_val_strict(self, field: FieldInfo | None, value: Any) -> Any: + """ + Coerce environment string values based on field annotation if model config is `strict=True` + or if the field annotation contains strict-annotated types (e.g. Optional[StrictBool]). + + Args: + field: The field. + value: The value to coerce. + + Returns: + The coerced value if successful, otherwise the original value. + """ + try: + should_coerce = self.config.get('strict') + if not should_coerce and isinstance(field, FieldInfo): + should_coerce = ( + is_union_origin(get_origin(field.annotation)) and _union_has_strict_types(field.annotation) + ) or _literal_has_numeric_enum(field.annotation) + if should_coerce and isinstance(value, str) and isinstance(field, FieldInfo): + if value == self.env_parse_none_str: + return value + if not _annotation_contains_types(field.annotation, (Json,), is_instance=True): + try: + return TypeAdapter(field.annotation).validate_python(value) + except ValidationError: + # Try JSON decoding as fallback (e.g. 'true' -> True for StrictBool) + decoded = json.loads(value) + if not isinstance(decoded, str): + return TypeAdapter(field.annotation).validate_python(decoded) + raise + except (ValidationError, json.JSONDecodeError): + # Allow validation error to be raised at time of instantiation + pass + return value + + def __repr__(self) -> str: + return ( + f'{self.__class__.__name__}(env_nested_delimiter={self.env_nested_delimiter!r}, ' + f'env_prefix_len={self.env_prefix_len!r})' + ) + + +__all__ = ['EnvSettingsSource'] diff --git a/source/target-vendors/cp312/pydantic_settings/sources/providers/gcp.py b/source/target-vendors/cp312/pydantic_settings/sources/providers/gcp.py new file mode 100644 index 0000000..f3a9261 --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/sources/providers/gcp.py @@ -0,0 +1,349 @@ +from __future__ import annotations as _annotations + +import warnings +from collections.abc import Iterator, Mapping +from functools import cached_property +from typing import TYPE_CHECKING, Any + +from pydantic.fields import FieldInfo + +from ...exceptions import SettingsError +from ..types import SecretVersion +from ..utils import InitState +from .env import EnvSettingsSource + +if TYPE_CHECKING: + from google.auth import default as google_auth_default + from google.auth.credentials import Credentials + from google.cloud.secretmanager import SecretManagerServiceClient + + from pydantic_settings.main import BaseSettings +else: + Credentials = None + SecretManagerServiceClient = None + google_auth_default = None + + +def import_gcp_secret_manager() -> None: + global Credentials + global SecretManagerServiceClient + global google_auth_default + + try: + from google.auth import default as google_auth_default + from google.auth.credentials import Credentials + + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', category=FutureWarning) + from google.cloud.secretmanager import SecretManagerServiceClient + except ImportError as e: # pragma: no cover + raise ImportError( + 'GCP Secret Manager dependencies are not installed, run `pip install pydantic-settings[gcp-secret-manager]`' + ) from e + + +def _is_not_found_error(exc: Exception) -> bool: + try: + from google.api_core.exceptions import NotFound + + return isinstance(exc, NotFound) + except ImportError: + return False + + +class GoogleSecretManagerMapping(Mapping[str, str | None]): + _loaded_secrets: dict[str, str | None] + _secret_client: SecretManagerServiceClient + + def __init__(self, secret_client: SecretManagerServiceClient, project_id: str, case_sensitive: bool) -> None: + self._loaded_secrets = {} + self._secret_client = secret_client + self._project_id = project_id + self._case_sensitive = case_sensitive + + @property + def _gcp_project_path(self) -> str: + return self._secret_client.common_project_path(self._project_id) + + def _select_case_insensitive_secret(self, lower_name: str, candidates: list[str]) -> str: + if len(candidates) == 1: + return candidates[0] + + # Sort to ensure deterministic selection (prefer lowercase / ASCII last) + candidates.sort() + winner = candidates[-1] + warnings.warn( + f"Secret collision: Found multiple secrets {candidates} normalizing to '{lower_name}'. " + f"Using '{winner}' for case-insensitive lookup.", + UserWarning, + stacklevel=2, + ) + return winner + + @cached_property + def _secret_name_map(self) -> dict[str, str]: + mapping: dict[str, str] = {} + # Group secrets by normalized name to detect collisions + normalized_groups: dict[str, list[str]] = {} + + secrets = self._secret_client.list_secrets(parent=self._gcp_project_path) + for secret in secrets: + name = self._secret_client.parse_secret_path(secret.name).get('secret', '') + mapping[name] = name + + if not self._case_sensitive: + lower_name = name.lower() + if lower_name not in normalized_groups: + normalized_groups[lower_name] = [] + normalized_groups[lower_name].append(name) + + if not self._case_sensitive: + for lower_name, candidates in normalized_groups.items(): + mapping[lower_name] = self._select_case_insensitive_secret(lower_name, candidates) + + return mapping + + @property + def _secret_names(self) -> list[str]: + return list(self._secret_name_map.keys()) + + def _secret_version_path(self, key: str, version: str = 'latest') -> str: + return self._secret_client.secret_version_path(self._project_id, key, version) + + def _get_secret_value(self, gcp_secret_name: str, version: str = 'latest') -> str | None: + try: + return self._secret_client.access_secret_version( + name=self._secret_version_path(gcp_secret_name, version) + ).payload.data.decode('UTF-8') + except Exception: + return None + + def _get_secret_value_or_raise(self, gcp_secret_name: str) -> str | None: + try: + return self._secret_client.access_secret_version( + name=self._secret_version_path(gcp_secret_name) + ).payload.data.decode('UTF-8') + except Exception as e: + if _is_not_found_error(e): + raise KeyError(gcp_secret_name) from e + return None + + def __getitem__(self, key: str) -> str | None: + if key in self._loaded_secrets: + return self._loaded_secrets[key] + + if self._case_sensitive: + self._loaded_secrets[key] = self._get_secret_value_or_raise(key) + return self._loaded_secrets[key] + + gcp_secret_name = self._secret_name_map.get(key) + if gcp_secret_name is None: + gcp_secret_name = self._secret_name_map.get(key.lower()) + + if gcp_secret_name: + self._loaded_secrets[key] = self._get_secret_value(gcp_secret_name) + else: + raise KeyError(key) + + return self._loaded_secrets[key] + + def __len__(self) -> int: + return len(self._secret_names) + + def __iter__(self) -> Iterator[str]: + return iter(self._secret_names) + + +class GoogleSecretManagerSettingsSource(EnvSettingsSource): + _credentials: Credentials | None + _secret_client: SecretManagerServiceClient | None + _project_id: str | None + _explicit_project_id: str | None + + def __init__( + self, + settings_cls: type[BaseSettings], + credentials: Credentials | None = None, + project_id: str | None = None, + env_prefix: str | None = None, + env_parse_none_str: str | None = None, + env_parse_enums: bool | None = None, + secret_client: SecretManagerServiceClient | None = None, + case_sensitive: bool | None = True, + project_id_field: str = 'project_id', + _init_state: InitState | None = None, + ) -> None: + """Settings source that reads secrets from Google Cloud Secret Manager. + + Args: + project_id: The GCP project to read secrets from. If not provided, it is + resolved lazily (see below). + project_id_field: The key, populated by a previous (higher priority) + settings source, to use as the ``project_id`` when one is not passed + explicitly. This must match the key used in ``current_state`` by the + previous source (typically the field name or its preferred alias). + For example, with ``some_field: str = Field(alias='GCP_PROJECT')``, + previous env sources will expose the value under ``'GCP_PROJECT'``. + Defaults to ``'project_id'``. + + The ``project_id`` is resolved lazily in :meth:`__call__` rather than at + construction time so that it can be sourced from settings resolved by previous + sources (only available via ``current_state`` once the source is called). + Resolution order is: + + 1. the explicit ``project_id`` argument + 2. the ``project_id_field`` value from previous settings sources + 3. ``google.auth.default()`` + """ + # Import Google Packages if they haven't already been imported + if SecretManagerServiceClient is None or Credentials is None or google_auth_default is None: + import_gcp_secret_manager() + + # Resolution is deferred to __call__ (see _resolve_gcp_project / _load_env_vars). + # _explicit_project_id is the immutable user input; _project_id is the resolved + # value and is written exclusively by _resolve_gcp_project. + self._explicit_project_id = project_id + self._project_id = None + self._credentials = credentials + self._secret_client = secret_client + self._project_id_field = project_id_field + self._env_vars_loaded = False + + super().__init__( + settings_cls, + case_sensitive=case_sensitive, + env_prefix=env_prefix, + env_ignore_empty=False, + env_parse_none_str=env_parse_none_str, + env_parse_enums=env_parse_enums, + _init_state=_init_state, + ) + # __init__ is past; subsequent _load_env_vars() calls may now resolve the project. + self._env_vars_loaded = True + + def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: + """Override get_field_value to get the secret value from GCP Secret Manager. + Look for a SecretVersion metadata field to specify a particular SecretVersion. + + Args: + field: The field to get the value for + field_name: The declared name of the field + + Returns: + A tuple of (value, key, value_is_complex), where `key` is the identifier used + to populate the model (either the field name or an alias, depending on + configuration). + """ + + secret_version = next((m.version for m in field.metadata if isinstance(m, SecretVersion)), None) + + # If a secret version is specified, try to get that specific version of the secret from + # GCP Secret Manager via the GoogleSecretManagerMapping. This allows different versions + # of the same secret name to be retrieved independently and cached in the GoogleSecretManagerMapping + if secret_version and isinstance(self.env_vars, GoogleSecretManagerMapping): + for field_key, env_name, value_is_complex in self._extract_field_info(field, field_name): + if self.case_sensitive: + gcp_secret_name: str | None = env_name + else: + gcp_secret_name = self.env_vars._secret_name_map.get(env_name) + if gcp_secret_name is None: + gcp_secret_name = self.env_vars._secret_name_map.get(env_name.lower()) + + if gcp_secret_name: + env_val = self.env_vars._get_secret_value(gcp_secret_name, secret_version) + if env_val is not None: + # If populate_by_name is enabled, return field_name to allow multiple fields + # with the same alias but different versions to be distinguished + if self.settings_cls.model_config.get('populate_by_name'): + return env_val, field_name, value_is_complex + return env_val, field_key, value_is_complex + + # If a secret version is specified but not found, we should not fall back to "latest" (default behavior) + # as that would be incorrect. We return None to indicate the value was not found. + return None, field_name, False + + val, key, is_complex = super().get_field_value(field, field_name) + + # If populate_by_name is enabled, we need to return the field_name as the key + # without this being enabled, you cannot load two secrets with the same name but different versions + if self.settings_cls.model_config.get('populate_by_name') and val is not None: + return val, field_name, is_complex + return val, key, is_complex + + def _resolve_gcp_project(self) -> None: + """Resolve the credentials, project_id and Secret Manager client. + + ``project_id`` is resolved, in order of precedence, from: the explicit + ``project_id`` argument, the ``project_id_field`` value from previous settings + sources (``current_state``), and finally ``google.auth.default()``. + """ + if self._project_id is not None: + return + + project_id = self._explicit_project_id + credentials = self._credentials + + # Fall back to a value resolved by a previous (higher priority) settings source. + if project_id is None: + state_project_id = self.current_state.get(self._project_id_field) + if isinstance(state_project_id, str): + project_id = state_project_id + + # Fall back to google.auth.default for whatever is still missing. + # Credentials are only needed if we have to build a client ourselves — if the + # caller supplied a pre-built secret_client we can skip the auth call entirely + # when project_id is also known, avoiding an unnecessary RPC / file read. + need_credentials = self._secret_client is None and credentials is None + need_project = project_id is None + if need_credentials or need_project: + _creds, _project_id = google_auth_default() + if need_credentials: + credentials = _creds + if need_project and isinstance(_project_id, str): + project_id = _project_id + + if project_id is None: + raise AttributeError( + 'project_id is required to be specified either as an argument, via a previous ' + 'settings source, or from google.auth.default. See ' + 'https://google-auth.readthedocs.io/en/master/reference/google.auth.html#google.auth.default' + ) + + self._credentials = credentials + self._project_id = project_id + if self._secret_client is None: + self._secret_client = SecretManagerServiceClient(credentials=self._credentials) + + def _load_env_vars(self) -> Mapping[str, str | None]: + # During __init__ the previous sources have not run yet, so defer building the + # mapping until __call__, where current_state (and thus project_id) is available. + if not self._env_vars_loaded: + return {} + + self._resolve_gcp_project() + if self._project_id is None or self._secret_client is None: + raise SettingsError( + 'GoogleSecretManagerSettingsSource: could not determine GCP project_id or initialize the Secret Manager client. ' + 'Pass project_id explicitly or ensure it is available via application default credentials or a previous settings source.' + ) + return GoogleSecretManagerMapping( + self._secret_client, project_id=self._project_id, case_sensitive=self.case_sensitive + ) + + def __call__(self) -> dict[str, Any]: + # current_state is populated by previous sources right before __call__; (re)build + # the mapping now so project_id can be sourced from them. + self._env_vars_loaded = True + self.env_vars = self._load_env_vars() + return super().__call__() + + def __repr__(self) -> str: + # Prefer the resolved project_id, falling back to the explicit constructor + # arg so the repr is informative before the source has been called. + project_id = self._project_id if self._project_id is not None else self._explicit_project_id + return ( + f'{self.__class__.__name__}(project_id={project_id!r}, env_nested_delimiter={self.env_nested_delimiter!r})' + ) + + +__all__ = ['GoogleSecretManagerMapping', 'GoogleSecretManagerSettingsSource'] diff --git a/source/target-vendors/cp312/pydantic_settings/sources/providers/json.py b/source/target-vendors/cp312/pydantic_settings/sources/providers/json.py new file mode 100644 index 0000000..872917c --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/sources/providers/json.py @@ -0,0 +1,53 @@ +"""JSON file settings source.""" + +from __future__ import annotations as _annotations + +import json +from typing import ( + TYPE_CHECKING, + Any, +) + +from ..base import ConfigFileSourceMixin, InitSettingsSource +from ..types import DEFAULT_PATH, ConfigFileSourceType +from ..utils import InitState + +if TYPE_CHECKING: + from pathlib import Path + + from pydantic_settings.main import BaseSettings + + from ..types import Traversable + + +class JsonConfigSettingsSource(InitSettingsSource, ConfigFileSourceMixin): + """ + A source class that loads variables from a JSON file + """ + + def __init__( + self, + settings_cls: type[BaseSettings], + json_file: ConfigFileSourceType | None = DEFAULT_PATH, + json_file_encoding: str | None = None, + deep_merge: bool = False, + _init_state: InitState | None = None, + ): + self.json_file_path = json_file if json_file != DEFAULT_PATH else settings_cls.model_config.get('json_file') + self.json_file_encoding = ( + json_file_encoding + if json_file_encoding is not None + else settings_cls.model_config.get('json_file_encoding') + ) + self.json_data = self._read_files(self.json_file_path, deep_merge=deep_merge) + super().__init__(settings_cls, self.json_data, _init_state=_init_state) + + def _read_file(self, file_path: Path | Traversable) -> dict[str, Any]: + with file_path.open(encoding=self.json_file_encoding) as json_file: + return json.load(json_file) + + def __repr__(self) -> str: + return f'{self.__class__.__name__}(json_file={self.json_file_path})' + + +__all__ = ['JsonConfigSettingsSource'] diff --git a/source/target-vendors/cp312/pydantic_settings/sources/providers/nested_secrets.py b/source/target-vendors/cp312/pydantic_settings/sources/providers/nested_secrets.py new file mode 100644 index 0000000..e25df06 --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/sources/providers/nested_secrets.py @@ -0,0 +1,214 @@ +import os +import warnings +from collections.abc import Iterator +from functools import reduce +from pathlib import Path +from typing import TYPE_CHECKING, Any, Literal, Optional + +from ...exceptions import SettingsError +from ...utils import path_type_label +from ..base import PydanticBaseSettingsSource +from ..utils import InitState, parse_env_vars +from .env import EnvSettingsSource +from .secrets import SecretsSettingsSource + +if TYPE_CHECKING: + from ...main import BaseSettings + from ...sources import PathType + + +SECRETS_DIR_MAX_SIZE = 16 * 2**20 # 16 MiB seems to be a reasonable default + + +class NestedSecretsSettingsSource(EnvSettingsSource): + def __init__( + self, + file_secret_settings: PydanticBaseSettingsSource | SecretsSettingsSource, + secrets_dir: Optional['PathType'] = None, + secrets_dir_missing: Literal['ok', 'warn', 'error'] | None = None, + secrets_dir_max_size: int | None = None, + secrets_case_sensitive: bool | None = None, + secrets_prefix: str | None = None, + secrets_nested_delimiter: str | None = None, + secrets_nested_subdir: bool | None = None, + # args for compatibility with SecretsSettingsSource, don't use directly + case_sensitive: bool | None = None, + env_prefix: str | None = None, + _init_state: InitState | None = None, + ) -> None: + # We allow the first argument to be settings_cls like original + # SecretsSettingsSource. However, it is recommended to pass + # SecretsSettingsSource instance instead (as it is shown in usage examples), + # otherwise `_secrets_dir` arg passed to Settings() constructor will be ignored. + settings_cls: type[BaseSettings] = getattr( + file_secret_settings, + 'settings_cls', + file_secret_settings, # type: ignore[arg-type] + ) + # config options + conf = settings_cls.model_config + self.secrets_dir: PathType | None = first_not_none( + getattr(file_secret_settings, 'secrets_dir', None), + secrets_dir, + conf.get('secrets_dir'), + ) + self.secrets_dir_missing: Literal['ok', 'warn', 'error'] = first_not_none( + secrets_dir_missing, + conf.get('secrets_dir_missing'), + 'warn', + ) + if self.secrets_dir_missing not in ('ok', 'warn', 'error'): + raise SettingsError(f'invalid secrets_dir_missing value: {self.secrets_dir_missing}') + self.secrets_dir_max_size: int = first_not_none( + secrets_dir_max_size, + conf.get('secrets_dir_max_size'), + SECRETS_DIR_MAX_SIZE, + ) + self.case_sensitive: bool = first_not_none( + secrets_case_sensitive, + conf.get('secrets_case_sensitive'), + case_sensitive, + conf.get('case_sensitive'), + False, + ) + self.secrets_prefix: str = first_not_none( + secrets_prefix, + conf.get('secrets_prefix'), + env_prefix, + conf.get('env_prefix'), + '', + ) + + # nested options + self.secrets_nested_delimiter: str | None = first_not_none( + secrets_nested_delimiter, + conf.get('secrets_nested_delimiter'), + conf.get('env_nested_delimiter'), + ) + self.secrets_nested_subdir: bool = first_not_none( + secrets_nested_subdir, + conf.get('secrets_nested_subdir'), + False, + ) + if self.secrets_nested_subdir: + if secrets_nested_delimiter or conf.get('secrets_nested_delimiter'): + raise SettingsError('Options secrets_nested_delimiter and secrets_nested_subdir are mutually exclusive') + else: + self.secrets_nested_delimiter = os.sep + + # ensure valid secrets_path + if self.secrets_dir is None: + paths = [] + elif isinstance(self.secrets_dir, (Path, str)): + paths = [self.secrets_dir] + else: + paths = list(self.secrets_dir) + self.secrets_paths: list[Path] = [Path(p).expanduser().resolve() for p in paths] + for path in self.secrets_paths: + self.validate_secrets_path(path) + + # construct parent, inheriting the init state of the wrapped source if not provided + super().__init__( + settings_cls, + case_sensitive=self.case_sensitive, + env_prefix=self.secrets_prefix, + env_nested_delimiter=self.secrets_nested_delimiter, + env_ignore_empty=False, # match SecretsSettingsSource behaviour + env_parse_enums=True, # we can pass everything here, it will still behave as "True" + env_parse_none_str=None, # match SecretsSettingsSource behaviour + _init_state=_init_state if _init_state is not None else getattr(file_secret_settings, '_init_state', None), + ) + self.env_parse_none_str = None # update manually because of None + + # update parent members + if not len(self.secrets_paths): + self.env_vars = {} + else: + secrets = reduce( + lambda d1, d2: dict((*d1.items(), *d2.items())), + (self.load_secrets(p) for p in self.secrets_paths), + ) + self.env_vars = parse_env_vars( + secrets, + self.case_sensitive, + self.env_ignore_empty, + self.env_parse_none_str, + ) + + def validate_secrets_path(self, path: Path) -> None: + if not path.exists(): + if self.secrets_dir_missing == 'ok': + pass + elif self.secrets_dir_missing == 'warn': + warnings.warn(f'directory "{path}" does not exist', stacklevel=2) + elif self.secrets_dir_missing == 'error': + raise SettingsError(f'directory "{path}" does not exist') + else: + raise ValueError # unreachable, checked before + else: + if not path.is_dir(): + raise SettingsError(f'secrets_dir must reference a directory, not a {path_type_label(path)}') + secrets_dir_size = sum(f.stat().st_size for f in self._iter_secret_files(path)) + if secrets_dir_size > self.secrets_dir_max_size: + raise SettingsError(f'secrets_dir size is above {self.secrets_dir_max_size} bytes') + + @staticmethod + def _iter_secret_files(path: Path) -> Iterator[Path]: + """Yield the secret files contained in ``path``. + + ``path`` is expected to already be resolved. The directory tree is walked + explicitly so that symbolic links are handled safely: + + * a file is only yielded if its real location stays within ``path``; entries + that resolve outside of it (e.g. through a symbolic link) are skipped, so + they neither contribute to the ``secrets_dir_max_size`` accounting nor get + loaded; + * each real directory is visited at most once, so cyclic or repeated + symlinks cannot make the walk loop and inflate the size accounting or the + number of loaded secrets. + + Because the size check and the loader share this iterator, they always see + the same set of files. + """ + seen_dirs: set[Path] = set() + + def walk(directory: Path) -> Iterator[Path]: + # Guard against symlink loops / a directory reachable through multiple + # links being traversed more than once. + resolved_dir = directory.resolve() + if resolved_dir in seen_dirs: + return + seen_dirs.add(resolved_dir) + try: + entries = sorted(directory.iterdir()) + except OSError: + return + for entry in entries: + resolved = entry.resolve() + if resolved.is_dir(): + # Only descend into directories that stay within secrets_dir. + # A symlinked directory pointing outside of ``path`` is not + # followed at all, so we never walk (potentially large) external + # trees and never read files from outside secrets_dir. + if resolved == path or path in resolved.parents: + yield from walk(entry) + elif resolved.is_file() and path in resolved.parents: + # Defense in depth: a file whose real location escapes + # secrets_dir (e.g. a symlink pointing outside of ``path``) is + # skipped from both the size accounting and the load. + yield entry + + yield from walk(path) + + @classmethod + def load_secrets(cls, path: Path) -> dict[str, str]: + # Explicit encoding, matching SecretsSettingsSource: the locale default would + # corrupt UTF-8 secrets on a non-UTF-8 Windows code page. + return {str(p.relative_to(path)): p.read_text(encoding='utf-8').strip() for p in cls._iter_secret_files(path)} + + def __repr__(self) -> str: + return f'NestedSecretsSettingsSource(secrets_dir={self.secrets_dir!r})' + + +def first_not_none(*objs: Any) -> Any: + return next(filter(lambda o: o is not None, objs), None) diff --git a/source/target-vendors/cp312/pydantic_settings/sources/providers/pyproject.py b/source/target-vendors/cp312/pydantic_settings/sources/providers/pyproject.py new file mode 100644 index 0000000..309b6b6 --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/sources/providers/pyproject.py @@ -0,0 +1,64 @@ +"""Pyproject TOML file settings source.""" + +from __future__ import annotations as _annotations + +from pathlib import Path +from typing import ( + TYPE_CHECKING, +) + +from ..utils import InitState +from .toml import TomlConfigSettingsSource + +if TYPE_CHECKING: + from pydantic_settings.main import BaseSettings + + +class PyprojectTomlConfigSettingsSource(TomlConfigSettingsSource): + """ + A source class that loads variables from a `pyproject.toml` file. + """ + + def __init__( + self, + settings_cls: type[BaseSettings], + toml_file: Path | None = None, + _init_state: InitState | None = None, + ) -> None: + self.toml_file_path = self._pick_pyproject_toml_file( + toml_file, settings_cls.model_config.get('pyproject_toml_depth', 0) + ) + self.toml_table_header: tuple[str, ...] = settings_cls.model_config.get( + 'pyproject_toml_table_header', ('tool', 'pydantic-settings') + ) + self.toml_data = self._read_files(self.toml_file_path) + for key in self.toml_table_header: + self.toml_data = self.toml_data.get(key, {}) + super(TomlConfigSettingsSource, self).__init__(settings_cls, self.toml_data, _init_state=_init_state) + + @staticmethod + def _pick_pyproject_toml_file(provided: Path | None, depth: int) -> Path: + """Pick a `pyproject.toml` file path to use. + + Args: + provided: Explicit path provided when instantiating this class. + depth: Number of directories up the tree to check of a pyproject.toml. + + """ + if provided: + return provided.resolve() + rv = Path.cwd() / 'pyproject.toml' + count = 0 + if not rv.is_file(): + child = rv.parent.parent / 'pyproject.toml' + while count < depth: + if child.is_file(): + return child + if str(child.parent) == rv.root: + break # end discovery after checking system root once + child = child.parent.parent / 'pyproject.toml' + count += 1 + return rv + + +__all__ = ['PyprojectTomlConfigSettingsSource'] diff --git a/source/target-vendors/cp312/pydantic_settings/sources/providers/secrets.py b/source/target-vendors/cp312/pydantic_settings/sources/providers/secrets.py new file mode 100644 index 0000000..df22acf --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/sources/providers/secrets.py @@ -0,0 +1,143 @@ +"""Secrets file settings source.""" + +from __future__ import annotations as _annotations + +import logging +import os +import warnings +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, +) + +from pydantic.fields import FieldInfo + +from pydantic_settings.utils import _settings_debug_enabled, logger, path_type_label + +from ...exceptions import SettingsError +from ..base import PydanticBaseEnvSettingsSource +from ..types import EnvPrefixTarget, PathType +from ..utils import InitState + +if TYPE_CHECKING: + from pydantic_settings.main import BaseSettings + + +class SecretsSettingsSource(PydanticBaseEnvSettingsSource): + """ + Source class for loading settings values from secret files. + """ + + def __init__( + self, + settings_cls: type[BaseSettings], + secrets_dir: PathType | None = None, + case_sensitive: bool | None = None, + env_prefix: str | None = None, + env_prefix_target: EnvPrefixTarget | None = None, + env_ignore_empty: bool | None = None, + env_parse_none_str: str | None = None, + env_parse_enums: bool | None = None, + _init_state: InitState | None = None, + ) -> None: + super().__init__( + settings_cls, + case_sensitive, + env_prefix, + env_prefix_target, + env_ignore_empty, + env_parse_none_str, + env_parse_enums, + _init_state, + ) + self.secrets_dir = secrets_dir if secrets_dir is not None else self.config.get('secrets_dir') + + def __call__(self) -> dict[str, Any]: + """ + Build fields from "secrets" files. + """ + secrets: dict[str, str | None] = {} + + if self.secrets_dir is None: + return secrets + + secrets_dirs = [self.secrets_dir] if isinstance(self.secrets_dir, (str, os.PathLike)) else self.secrets_dir + secrets_paths = [Path(p).expanduser() for p in secrets_dirs] + self.secrets_paths = [] + + for path in secrets_paths: + if not path.exists(): + warnings.warn(f'directory "{path}" does not exist') + else: + self.secrets_paths.append(path) + + if not len(self.secrets_paths): + return secrets + + for path in self.secrets_paths: + if not path.is_dir(): + raise SettingsError(f'secrets_dir must reference a directory, not a {path_type_label(path)}') + + return super().__call__() + + @classmethod + def find_case_path(cls, dir_path: Path, file_name: str, case_sensitive: bool) -> Path | None: + """ + Find a file within path's directory matching filename, optionally ignoring case. + + Args: + dir_path: Directory path. + file_name: File name. + case_sensitive: Whether to search for file name case sensitively. + + Returns: + Whether file path or `None` if file does not exist in directory. + """ + for f in dir_path.iterdir(): + if f.name == file_name or (not case_sensitive and f.name.lower() == file_name.lower()): + return f + return None + + def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: + """ + Gets the value for field from secret file and a flag to determine whether value is complex. + + Args: + field: The field. + field_name: The field name. + + Returns: + A tuple that contains the value (`None` if the file does not exist), key, and + a flag to determine whether value is complex. + """ + + debug = _settings_debug_enabled() and logger.isEnabledFor(logging.DEBUG) + + for field_key, env_name, value_is_complex in self._extract_field_info(field, field_name): + # paths reversed to match the last-wins behaviour of `env_file` + for secrets_path in reversed(self.secrets_paths): + path = self.find_case_path(secrets_path, env_name, self.case_sensitive) + if not path: + # path does not exist, we currently don't return a warning for this + if debug: + logger.debug('Secret file not found, skipping: %s', (secrets_path / env_name).resolve()) + continue + + if path.is_file(): + if debug: + logger.debug('Loading secret file: %s', path.resolve()) + # Explicit encoding: `read_text()` would otherwise decode with the + # locale encoding, so a UTF-8 secret is corrupted on a non-UTF-8 + # Windows code page (silently, when every byte happens to map). + return path.read_text(encoding='utf-8').strip(), field_key, value_is_complex + else: + warnings.warn( + f'attempted to load secret file "{path}" but found a {path_type_label(path)} instead.', + stacklevel=4, + ) + + return None, field_key, value_is_complex + + def __repr__(self) -> str: + return f'{self.__class__.__name__}(secrets_dir={self.secrets_dir!r})' diff --git a/source/target-vendors/cp312/pydantic_settings/sources/providers/toml.py b/source/target-vendors/cp312/pydantic_settings/sources/providers/toml.py new file mode 100644 index 0000000..9147adc --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/sources/providers/toml.py @@ -0,0 +1,100 @@ +"""TOML file settings source.""" + +from __future__ import annotations as _annotations + +import sys +from collections.abc import Sequence +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, +) + +from ..base import ConfigFileSourceMixin, InitSettingsSource +from ..types import DEFAULT_PATH, ConfigFileSourceType +from ..utils import InitState + +if TYPE_CHECKING: + from pydantic_settings.main import BaseSettings + + from ..types import Traversable + + if sys.version_info >= (3, 11): + import tomllib + else: + tomllib = None + import tomli +else: + tomllib = None + tomli = None + + +def import_toml() -> None: + global tomli + global tomllib + if sys.version_info < (3, 11): + if tomli is not None: + return + try: + import tomli + except ImportError as e: # pragma: no cover + raise ImportError('tomli is not installed, run `pip install pydantic-settings[toml]`') from e + else: + if tomllib is not None: + return + import tomllib + + +class TomlConfigSettingsSource(InitSettingsSource, ConfigFileSourceMixin): + """ + A source class that loads variables from a TOML file + """ + + def __init__( + self, + settings_cls: type[BaseSettings], + toml_file: ConfigFileSourceType | None = DEFAULT_PATH, + toml_table_header: tuple[str, ...] = (), + deep_merge: bool = False, + _init_state: InitState | None = None, + ): + self.toml_file_path = toml_file if toml_file != DEFAULT_PATH else settings_cls.model_config.get('toml_file') + self.toml_table_header = ( + toml_table_header if toml_table_header else settings_cls.model_config.get('toml_table_header', ()) + ) + self.toml_data = self._read_files(self.toml_file_path, deep_merge=deep_merge) + + if self._any_file_exists(self.toml_file_path): + for key in self.toml_table_header: + if key not in self.toml_data: + raise KeyError(f'toml_table_header key "{key}" not found in {self.toml_file_path}') + self.toml_data = self.toml_data[key] + + super().__init__(settings_cls, self.toml_data, _init_state=_init_state) + + def _read_file(self, file_path: Path | Traversable) -> dict[str, Any]: + import_toml() + with file_path.open(mode='rb') as toml_file: + if sys.version_info < (3, 11): + return tomli.load(toml_file) + return tomllib.load(toml_file) + + @staticmethod + def _any_file_exists(paths: ConfigFileSourceType | None) -> bool: + """Check if any of the given file paths exist.""" + if paths is None: + return False + if isinstance(paths, str) or not isinstance(paths, Sequence): + paths = [paths] + + def _exists(path: Path | str | Traversable) -> bool: + if isinstance(path, (str, Path)): + return Path(path).exists() + # Non-`Path` `Traversable` (e.g. a resource inside a zip/wheel) is not + # `os.PathLike`, so it can't be wrapped in `Path`; query it directly. + return path.is_file() + + return any(_exists(path) for path in paths) + + def __repr__(self) -> str: + return f'{self.__class__.__name__}(toml_file={self.toml_file_path}, toml_table_header={self.toml_table_header})' diff --git a/source/target-vendors/cp312/pydantic_settings/sources/providers/yaml.py b/source/target-vendors/cp312/pydantic_settings/sources/providers/yaml.py new file mode 100644 index 0000000..b3aa64e --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/sources/providers/yaml.py @@ -0,0 +1,147 @@ +"""YAML file settings source.""" + +from __future__ import annotations as _annotations + +from typing import ( + TYPE_CHECKING, + Any, +) + +from ..base import ConfigFileSourceMixin, InitSettingsSource +from ..types import DEFAULT_PATH, ConfigFileSourceType +from ..utils import InitState + +if TYPE_CHECKING: + from pathlib import Path + + import yaml + + from pydantic_settings.main import BaseSettings + + from ..types import Traversable +else: + yaml = None + + +def import_yaml() -> None: + global yaml + if yaml is not None: + return + try: + import yaml + except ImportError as e: + raise ImportError('PyYAML is not installed, run `pip install pydantic-settings[yaml]`') from e + + +class YamlConfigSettingsSource(InitSettingsSource, ConfigFileSourceMixin): + """ + A source class that loads variables from a yaml file + """ + + def __init__( + self, + settings_cls: type[BaseSettings], + yaml_file: ConfigFileSourceType | None = DEFAULT_PATH, + yaml_file_encoding: str | None = None, + yaml_config_section: str | None = None, + deep_merge: bool = False, + _init_state: InitState | None = None, + ): + self.yaml_file_path = yaml_file if yaml_file != DEFAULT_PATH else settings_cls.model_config.get('yaml_file') + self.yaml_file_encoding = ( + yaml_file_encoding + if yaml_file_encoding is not None + else settings_cls.model_config.get('yaml_file_encoding') + ) + self.yaml_config_section = ( + yaml_config_section + if yaml_config_section is not None + else settings_cls.model_config.get('yaml_config_section') + ) + self.yaml_data = self._read_files(self.yaml_file_path, deep_merge=deep_merge) + + if self.yaml_config_section is not None: + section_data = self._traverse_nested_section( + self.yaml_data, self.yaml_config_section, self.yaml_config_section + ) + # An empty section (`section:` with no children) parses to `None`; treat it as an + # empty mapping so it falls back to defaults, mirroring `_read_file`'s `or {}` for + # an empty whole file. A non-mapping section (e.g. a scalar) is a user error, so + # raise a clear message instead of letting `InitSettingsSource` choke on `.keys()`. + if section_data is None: + section_data = {} + elif not isinstance(section_data, dict): + raise TypeError( + f'yaml_config_section "{self.yaml_config_section}" in {self.yaml_file_path} ' + f'must be a mapping, got {type(section_data).__name__}.' + ) + self.yaml_data = section_data + super().__init__(settings_cls, self.yaml_data, _init_state=_init_state) + + def _read_file(self, file_path: Path | Traversable) -> dict[str, Any]: + import_yaml() + with file_path.open(encoding=self.yaml_file_encoding) as yaml_file: + return yaml.safe_load(yaml_file) or {} + + def _traverse_nested_section( + self, data: dict[str, Any], section_path: str, original_path: str | None = None + ) -> dict[str, Any]: + """ + Traverse nested YAML sections using dot-notation path. + + This method tries to match the longest possible key first before splitting on dots, + allowing access to YAML keys that contain literal dot characters. + + For example, with section_path="a.b.c", it will try: + 1. "a.b.c" as a literal key + 2. "a.b" as a key, then traverse to "c" + 3. "a" as a key, then traverse to "b.c" + 4. "a" as a key, then "b" as a key, then "c" as a key + """ + # Track the original path for error messages + if original_path is None: + original_path = section_path + + # Only reject truly empty paths + if not section_path: + raise ValueError('yaml_config_section cannot be empty') + + # Try the full path as a literal key first (even with leading/trailing/consecutive dots) + try: + return data[section_path] + except KeyError: + pass # Not a literal key, try splitting + except TypeError: + raise TypeError( + f'yaml_config_section path "{original_path}" cannot be traversed in {self.yaml_file_path}. ' + f'An intermediate value is not a dictionary.' + ) + + # If path contains no dots, we already tried it as a literal key above + if '.' not in section_path: + raise KeyError(f'yaml_config_section key "{original_path}" not found in {self.yaml_file_path}') + + # Try progressively shorter prefixes (greedy left-to-right approach) + parts = section_path.split('.') + for i in range(len(parts) - 1, 0, -1): + prefix = '.'.join(parts[:i]) + suffix = '.'.join(parts[i:]) + + if prefix in data: + # Found the prefix as a literal key, now recursively traverse the suffix + try: + return self._traverse_nested_section(data[prefix], suffix, original_path) + except TypeError: + raise TypeError( + f'yaml_config_section path "{original_path}" cannot be traversed in {self.yaml_file_path}. ' + f'An intermediate value is not a dictionary.' + ) + + # If we get here, no match was found + raise KeyError(f'yaml_config_section key "{original_path}" not found in {self.yaml_file_path}') + + def __repr__(self) -> str: + return f'{self.__class__.__name__}(yaml_file={self.yaml_file_path})' + + +__all__ = ['YamlConfigSettingsSource'] diff --git a/source/target-vendors/cp312/pydantic_settings/sources/types.py b/source/target-vendors/cp312/pydantic_settings/sources/types.py new file mode 100644 index 0000000..29f449f --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/sources/types.py @@ -0,0 +1,108 @@ +"""Type definitions for pydantic-settings sources.""" + +from __future__ import annotations as _annotations + +import sys +from collections.abc import Sequence +from pathlib import Path +from typing import TYPE_CHECKING, Any, Literal + +if sys.version_info >= (3, 11): + from importlib.resources.abc import Traversable as Traversable # noqa: PLC0414 (explicit re-export) +else: + from importlib.abc import Traversable as Traversable # noqa: PLC0414 (explicit re-export) + +if TYPE_CHECKING: + from pydantic._internal._dataclasses import PydanticDataclass + from pydantic.main import BaseModel + + PydanticModel = PydanticDataclass | BaseModel +else: + PydanticModel = Any + + +class EnvNoneType(str): + pass + + +class NoDecode: + """Annotation to prevent decoding of a field value.""" + + +class ForceDecode: + """Annotation to force decoding of a field value.""" + + +EnvPrefixTarget = Literal['variable', 'alias', 'all'] +DotenvType = Path | str | Sequence[Path | str] +PathType = Path | str | Sequence[Path | str] +# Config file sources (json/toml/yaml) additionally accept packaged resources exposed as a +# `Traversable` (e.g. `importlib.resources.files(...).joinpath(...)`), including non-filesystem +# ones such as a file inside a zip/wheel. +# See https://github.com/pydantic/pydantic-settings/issues/299 +ConfigFileSourceType = Path | str | Traversable | Sequence[Path | str | Traversable] +DotenvFiltering = Literal['match_prefix', 'only_existing'] +DEFAULT_PATH: PathType = Path('') + +# This is used as default value for `_env_file` in the `BaseSettings` class and +# `env_file` in `DotEnvSettingsSource` so the default can be distinguished from `None`. +# See the docstring of `BaseSettings` for more details. +ENV_FILE_SENTINEL: DotenvType = Path('') + + +class _CliSubCommand: + pass + + +class _CliPositionalArg: + pass + + +class _CliImplicitFlag: + pass + + +class _CliToggleFlag(_CliImplicitFlag): + pass + + +class _CliDualFlag(_CliImplicitFlag): + pass + + +class _CliExplicitFlag: + pass + + +class _CliUnknownArgs: + pass + + +class SecretVersion: + def __init__(self, version: str) -> None: + self.version = version + + def __repr__(self) -> str: + return f'{self.__class__.__name__}({self.version!r})' + + +__all__ = [ + 'DEFAULT_PATH', + 'ENV_FILE_SENTINEL', + 'ConfigFileSourceType', + 'DotenvType', + 'EnvNoneType', + 'EnvPrefixTarget', + 'ForceDecode', + 'NoDecode', + 'PathType', + 'PydanticModel', + 'SecretVersion', + '_CliDualFlag', + '_CliExplicitFlag', + '_CliImplicitFlag', + '_CliPositionalArg', + '_CliSubCommand', + '_CliToggleFlag', + '_CliUnknownArgs', +] diff --git a/source/target-vendors/cp312/pydantic_settings/sources/utils.py b/source/target-vendors/cp312/pydantic_settings/sources/utils.py new file mode 100644 index 0000000..d4bec22 --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/sources/utils.py @@ -0,0 +1,376 @@ +"""Utility functions for pydantic-settings sources.""" + +from __future__ import annotations as _annotations + +import warnings +from collections import deque +from collections.abc import Mapping, Sequence +from dataclasses import is_dataclass +from enum import Enum +from typing import Any, TypedDict, TypeVar, cast, get_args, get_origin + +from pydantic import BaseModel, Json, RootModel, Secret +from pydantic._internal._utils import is_model_class +from pydantic.dataclasses import is_pydantic_dataclass +from pydantic.fields import FieldInfo +from pydantic.types import Strict +from typing_inspection import typing_objects +from typing_inspection.introspection import is_union_origin + +from ..exceptions import IncompleteFieldDefinitionWarning, SettingsError +from ..utils import _lenient_issubclass +from .types import EnvNoneType + + +class InitState(TypedDict, total=False): + """State shared between settings sources during a single settings resolution.""" + + field_info_ids: set[int] + """The `id()`s of the incomplete `FieldInfo` instances that were already warned about.""" + + +def _warn_if_field_info_incomplete(field_info: FieldInfo, field_name: str, init_state: InitState) -> None: + """Warn if the field is incomplete, i.e. its annotation contains unresolved forward references. + + An incomplete `FieldInfo` instance is unsafe to inspect — any of its attributes (annotation, + aliases, metadata, default) may rely on the unresolved annotation, so settings sources may + silently fail to resolve the field's value. Each instance is only warned about once per + `init_state`, so that a field accessed by multiple sources during a single settings + resolution doesn't emit duplicate warnings. + """ + if getattr(field_info, '_complete', True): + return + warned_ids = init_state.setdefault('field_info_ids', set()) + if id(field_info) in warned_ids: + return + warned_ids.add(id(field_info)) + warnings.warn( + f'Field {field_name!r} has an incomplete definition: its annotation contains an unresolved ' + 'forward reference, so settings sources may fail to correctly resolve its value. ' + 'Call `model_rebuild()` on the model where the field is defined, once all the referenced ' + 'types are defined.', + IncompleteFieldDefinitionWarning, + ) + + +def _get_env_var_key(key: str, case_sensitive: bool = False) -> str: + return key if case_sensitive else key.lower() + + +def _parse_env_none_str(value: str | None, parse_none_str: str | None = None) -> str | None | EnvNoneType: + return value if not (value == parse_none_str and parse_none_str is not None) else EnvNoneType(value) + + +def parse_env_vars( + env_vars: Mapping[str, str | None], + case_sensitive: bool = False, + ignore_empty: bool = False, + parse_none_str: str | None = None, +) -> Mapping[str, str | None]: + return { + _get_env_var_key(k, case_sensitive): _parse_env_none_str(v, parse_none_str) + for k, v in env_vars.items() + if not (ignore_empty and v == '') + } + + +def _substitute_typevars(tp: Any, param_map: dict[Any, Any]) -> Any: + """Substitute TypeVars in a type annotation with concrete types from param_map.""" + if isinstance(tp, TypeVar) and tp in param_map: + return param_map[tp] + args = get_args(tp) + if not args: + return tp + new_args = tuple(_substitute_typevars(arg, param_map) for arg in args) + if new_args == args: + return tp + origin = get_origin(tp) + if origin is not None: + try: + return origin[new_args] + except TypeError: + # types.UnionType and similar are not directly subscriptable, + # reconstruct using | operator + import functools + import operator + + return functools.reduce(operator.or_, new_args) + return tp + + +def _resolve_type_alias(annotation: Any) -> Any: + """Resolve a TypeAliasType to its underlying value, substituting type params if parameterized.""" + if typing_objects.is_typealiastype(annotation): + return annotation.__value__ + origin = get_origin(annotation) + if typing_objects.is_typealiastype(origin): + type_params = getattr(origin, '__type_params__', ()) + type_args = get_args(annotation) + value = origin.__value__ + if type_params and type_args: + return _substitute_typevars(value, dict(zip(type_params, type_args))) + return value + return annotation + + +def _annotation_is_complex(annotation: Any, metadata: list[Any], init_state: InitState | None = None) -> bool: + # If the model is a root model, the root annotation should be used to + # evaluate the complexity. + annotation = _resolve_type_alias(annotation) + if annotation is not None and _lenient_issubclass(annotation, RootModel) and annotation is not RootModel: + annotation = cast('type[RootModel[Any]]', annotation) + root_field = annotation.model_fields['root'] + if init_state is not None: + _warn_if_field_info_incomplete(root_field, f'{annotation.__name__}.root', init_state) + root_annotation = root_field.annotation + if root_annotation is not None: # pragma: no branch + annotation = root_annotation + + if any(isinstance(md, Json) for md in metadata): # type: ignore[misc] + return False + + origin = get_origin(annotation) + + # Check if annotation is of the form Annotated[type, metadata]. + if typing_objects.is_annotated(origin): + # Return result of recursive call on inner type. + inner, *meta = get_args(annotation) + return _annotation_is_complex(inner, meta, init_state) + + if _lenient_issubclass(origin, Secret): + return False + + return ( + _annotation_is_complex_inner(annotation) + or _annotation_is_complex_inner(origin) + or hasattr(origin, '__pydantic_core_schema__') + or hasattr(origin, '__get_pydantic_core_schema__') + ) + + +def _get_field_metadata(field: FieldInfo) -> list[Any]: + annotation = _resolve_type_alias(field.annotation) + metadata = field.metadata + origin = get_origin(annotation) + if typing_objects.is_annotated(origin): + _, *meta = get_args(annotation) + metadata += meta + return metadata + + +def _annotation_is_complex_inner(annotation: type[Any] | None) -> bool: + if _lenient_issubclass(annotation, (str, bytes)): + return False + + return _lenient_issubclass( + annotation, (BaseModel, Mapping, Sequence, tuple, set, frozenset, deque) + ) or is_dataclass(annotation) + + +def _union_is_complex(annotation: type[Any] | None, metadata: list[Any], init_state: InitState | None = None) -> bool: + """Check if a union type contains any complex types.""" + for arg in get_args(annotation): + if _annotation_is_complex(arg, metadata, init_state): + return True + # _annotation_is_complex doesn't handle bare Union types, so when an arg + # is Annotated[Union[X, Y], ...], stripping Annotated yields a bare Union + # that _annotation_is_complex can't evaluate. Recurse into it, but only + # if the Annotated metadata doesn't suppress complexity (e.g. Json). + inner = _strip_annotated(arg) + if inner is not arg: + _, *inner_meta = get_args(arg) + if any(isinstance(md, Json) for md in inner_meta): # type: ignore[misc] + continue + if is_union_origin(get_origin(inner)) and _union_is_complex(inner, metadata, init_state): + return True + return False + + +def _union_has_strict_types(annotation: type[Any] | None) -> bool: + """Check if a union type contains any strict-annotated types.""" + for arg in get_args(annotation): + if typing_objects.is_annotated(get_origin(arg)): + _, *meta = get_args(arg) + if any(isinstance(m, Strict) for m in meta): + return True + return False + + +def _annotation_contains_types( + annotation: type[Any] | None, + types: tuple[Any, ...], + is_include_origin: bool = True, + is_strip_annotated: bool = False, + is_instance: bool = False, + collect: set[Any] | None = None, +) -> bool: + """Check if a type annotation contains any of the specified types.""" + if is_strip_annotated: + annotation = _strip_annotated(annotation) + if is_include_origin is True: + origin = get_origin(annotation) + if origin in types: + if collect is None: + return True + collect.add(annotation) + if is_instance and any(isinstance(origin, type_) for type_ in types): + if collect is None: + return True + collect.add(annotation) + for type_ in get_args(annotation): + if ( + _annotation_contains_types( + type_, + types, + is_include_origin=True, + is_strip_annotated=is_strip_annotated, + is_instance=is_instance, + collect=collect, + ) + and collect is None + ): + return True + if is_instance and any(isinstance(annotation, type_) for type_ in types): + if collect is None: + return True + collect.add(annotation) + if annotation in types: + if collect is not None: + collect.add(annotation) + return True + return False + + +def _strip_annotated(annotation: Any) -> Any: + if typing_objects.is_annotated(get_origin(annotation)): + return annotation.__origin__ + else: + return annotation + + +def _annotation_enum_val_to_name(annotation: type[Any] | None, value: Any) -> str | None: + for type_ in (annotation, get_origin(annotation)): + if _lenient_issubclass(type_, Enum): + enum_type = cast('type[Enum]', type_) + if value in enum_type.__members__.values(): + return enum_type(value).name + for arg in get_args(annotation): + enum_name = _annotation_enum_val_to_name(arg, value) + if enum_name is not None: + return enum_name + return None + + +def _annotation_enum_name_to_val(annotation: type[Any] | None, name: Any) -> Any: + for type_ in (annotation, get_origin(annotation)): + if _lenient_issubclass(type_, Enum): + enum_type = cast('type[Enum]', type_) + if name in enum_type.__members__: + return enum_type[name] + for arg in get_args(annotation): + enum_val = _annotation_enum_name_to_val(arg, name) + if enum_val is not None: + return enum_val + return None + + +def _literal_has_numeric_enum(annotation: type[Any] | None) -> bool: + """Check if annotation is a Literal type containing numeric Enum members (IntEnum, (int, Enum), (float, Enum)).""" + if typing_objects.is_literal(get_origin(annotation)): + return any(isinstance(arg, (int, float)) and isinstance(arg, Enum) for arg in get_args(annotation)) + # Handle Annotated wrapping, e.g. Annotated[Literal[IntEnum.member], Field(...)] + if typing_objects.is_annotated(get_origin(annotation)): + inner = get_args(annotation)[0] + return _literal_has_numeric_enum(inner) + # Handle Union/Optional wrapping, e.g. Optional[Literal[IntEnum.member]] + if is_union_origin(get_origin(annotation)): + return any(_literal_has_numeric_enum(arg) for arg in get_args(annotation)) + return False + + +def _get_model_fields(model_cls: type[Any]) -> dict[str, FieldInfo]: + """Get fields from a pydantic model or dataclass.""" + + if is_pydantic_dataclass(model_cls) and hasattr(model_cls, '__pydantic_fields__'): + return model_cls.__pydantic_fields__ + if is_model_class(model_cls): + return model_cls.model_fields + raise SettingsError(f'Error: {model_cls.__name__} is not subclass of BaseModel or pydantic.dataclasses.dataclass') + + +def _get_alias_names( + field_name: str, + field_info: FieldInfo, + alias_path_args: dict[str, int | None] | None = None, + case_sensitive: bool = True, + populate_by_name: bool = False, +) -> tuple[tuple[str, ...], bool]: + """Get alias names for a field, handling alias paths and case sensitivity.""" + from pydantic import AliasChoices, AliasPath + + alias_names: list[str] = [] + is_alias_path_only: bool = True + if not any((field_info.alias, field_info.validation_alias)): + alias_names += [field_name] + is_alias_path_only = False + else: + new_alias_paths: list[AliasPath] = [] + for alias in (field_info.alias, field_info.validation_alias): + if alias is None: + continue + elif isinstance(alias, str): + alias_names.append(alias) + is_alias_path_only = False + elif isinstance(alias, AliasChoices): + for name in alias.choices: + if isinstance(name, str): + alias_names.append(name) + is_alias_path_only = False + else: + new_alias_paths.append(name) + else: + new_alias_paths.append(alias) + for alias_path in new_alias_paths: + name = cast(str, alias_path.path[0]) + name = name.lower() if not case_sensitive else name + if alias_path_args is not None: + alias_path_args[name] = ( + alias_path.path[1] if len(alias_path.path) > 1 and isinstance(alias_path.path[1], int) else None + ) + if not alias_names and is_alias_path_only: + alias_names.append(name) + if populate_by_name and field_name not in alias_names: + alias_names.append(field_name) + is_alias_path_only = False + if not case_sensitive: + alias_names = [alias_name.lower() for alias_name in alias_names] + return tuple(dict.fromkeys(alias_names)), is_alias_path_only + + +def _is_function(obj: Any) -> bool: + """Check if an object is a function.""" + from types import BuiltinFunctionType, FunctionType + + return isinstance(obj, (FunctionType, BuiltinFunctionType)) + + +__all__ = [ + 'InitState', + '_annotation_contains_types', + '_annotation_enum_name_to_val', + '_annotation_enum_val_to_name', + '_annotation_is_complex', + '_annotation_is_complex_inner', + '_get_alias_names', + '_get_env_var_key', + '_get_model_fields', + '_is_function', + '_literal_has_numeric_enum', + '_parse_env_none_str', + '_resolve_type_alias', + '_strip_annotated', + '_union_has_strict_types', + '_union_is_complex', + '_warn_if_field_info_incomplete', + 'parse_env_vars', +] diff --git a/source/target-vendors/cp312/pydantic_settings/utils.py b/source/target-vendors/cp312/pydantic_settings/utils.py new file mode 100644 index 0000000..7bd6fe8 --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/utils.py @@ -0,0 +1,55 @@ +import logging +import os +import types +from pathlib import Path +from typing import Any, _Final, _GenericAlias, get_origin # type: ignore [attr-defined] + +logger = logging.getLogger('pydantic_settings') + +_DEBUG_ENV_VAR = 'PYDANTIC_SETTINGS_DEBUG' + + +def _settings_debug_enabled() -> bool: + """Whether settings source debugging is enabled via the `PYDANTIC_SETTINGS_DEBUG` env var.""" + return os.environ.get(_DEBUG_ENV_VAR, '').strip().lower() in ('1', 'true', 'yes', 'on') + + +_PATH_TYPE_LABELS = { + Path.is_dir: 'directory', + Path.is_file: 'file', + Path.is_mount: 'mount point', + Path.is_symlink: 'symlink', + Path.is_block_device: 'block device', + Path.is_char_device: 'char device', + Path.is_fifo: 'FIFO', + Path.is_socket: 'socket', +} + + +def path_type_label(p: Path) -> str: + """ + Find out what sort of thing a path is. + """ + assert p.exists(), 'path does not exist' + for method, name in _PATH_TYPE_LABELS.items(): + if method(p): + return name + + return 'unknown' # pragma: no cover + + +# TODO remove and replace usage by `isinstance(cls, type) and issubclass(cls, class_or_tuple)` +# once we drop support for Python 3.10. +def _lenient_issubclass(cls: Any, class_or_tuple: Any) -> bool: # pragma: no cover + try: + return isinstance(cls, type) and issubclass(cls, class_or_tuple) + except TypeError: + if get_origin(cls) is not None: + # Up until Python 3.10, isinstance(, type) is True + # (e.g. list[int]) + return False + raise + + +_WithArgsTypes = (_GenericAlias, types.GenericAlias, types.UnionType) +_typing_base: Any = _Final # pyright: ignore[reportAttributeAccessIssue] diff --git a/source/target-vendors/cp312/pydantic_settings/version.py b/source/target-vendors/cp312/pydantic_settings/version.py new file mode 100644 index 0000000..b4e42de --- /dev/null +++ b/source/target-vendors/cp312/pydantic_settings/version.py @@ -0,0 +1 @@ +VERSION = '2.15.0' diff --git a/source/target-vendors/cp312/pyjwt-2.14.0.dist-info/METADATA b/source/target-vendors/cp312/pyjwt-2.14.0.dist-info/METADATA new file mode 100644 index 0000000..0db0043 --- /dev/null +++ b/source/target-vendors/cp312/pyjwt-2.14.0.dist-info/METADATA @@ -0,0 +1,93 @@ +Metadata-Version: 2.4 +Name: PyJWT +Version: 2.14.0 +Summary: JSON Web Token implementation in Python +Author-email: Jose Padilla +License-Expression: MIT +Project-URL: Homepage, https://github.com/jpadilla/pyjwt +Keywords: json,jwt,security,signing,token,web +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Natural Language :: English +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Topic :: Utilities +Requires-Python: >=3.9 +Description-Content-Type: text/x-rst +License-File: LICENSE +License-File: AUTHORS.rst +Requires-Dist: typing_extensions>=4.0; python_version < "3.11" +Provides-Extra: crypto +Requires-Dist: cryptography>=3.4.0; extra == "crypto" +Dynamic: license-file + +PyJWT +===== + +.. image:: https://github.com/jpadilla/pyjwt/workflows/CI/badge.svg + :target: https://github.com/jpadilla/pyjwt/actions?query=workflow%3ACI + +.. image:: https://img.shields.io/pypi/v/pyjwt.svg + :target: https://pypi.python.org/pypi/pyjwt + +.. image:: https://codecov.io/gh/jpadilla/pyjwt/branch/master/graph/badge.svg + :target: https://codecov.io/gh/jpadilla/pyjwt + +.. image:: https://readthedocs.org/projects/pyjwt/badge/?version=stable + :target: https://pyjwt.readthedocs.io/en/stable/ + +A Python implementation of `RFC 7519 `_. Original implementation was written by `@progrium `_. + +Sponsor +------- + +.. |auth0-logo| image:: https://github.com/user-attachments/assets/ee98379e-ee76-4bcb-943a-e25c4ea6d174 + :width: 160px + ++--------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| |auth0-logo| | If you want to quickly add secure token-based authentication to Python projects, feel free to check Auth0's Python SDK and free plan at `auth0.com/signup `_. | ++--------------+-----------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + +Installing +---------- + +Install with **pip**: + +.. code-block:: console + + $ pip install PyJWT + + +Usage +----- + +.. code-block:: pycon + + >>> import jwt + >>> encoded = jwt.encode({"some": "payload"}, "secret", algorithm="HS256") + >>> print(encoded) + eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg + >>> jwt.decode(encoded, "secret", algorithms=["HS256"]) + {'some': 'payload'} + +Documentation +------------- + +View the full docs online at https://pyjwt.readthedocs.io/en/stable/ + + +Tests +----- + +You can run tests from the project root after cloning with: + +.. code-block:: console + + $ tox diff --git a/source/target-vendors/cp312/pyjwt-2.14.0.dist-info/RECORD b/source/target-vendors/cp312/pyjwt-2.14.0.dist-info/RECORD new file mode 100644 index 0000000..478b91f --- /dev/null +++ b/source/target-vendors/cp312/pyjwt-2.14.0.dist-info/RECORD @@ -0,0 +1,19 @@ +jwt/__init__.py,sha256=BWR5W0NaQShI2w1ngQD9E4qjCWB16MzjZHUcfkJ9YP0,1805 +jwt/algorithms.py,sha256=vsUpnHXbHUvXpfnxw4LIX8ZIAlcVt-UisyDhYdOutVo,40244 +jwt/api_jwk.py,sha256=XfmRO9SEb-fSV7nADDn4vGEcdfgydMmK5QM4p8sgxj8,6328 +jwt/api_jws.py,sha256=oMV41OCcgjr-AZ7T4LBvsYTGZf80z-QExoIprjJyJp0,17421 +jwt/api_jwt.py,sha256=iP4AZ7WXxyarU6SZYyB4IqGo79vBOGAxRr1DJGAM2hg,22244 +jwt/exceptions.py,sha256=NUD0h2A0MiHV6U-TPt_Ar0prnAHxyJJ0i6LHIWzd8VQ,2375 +jwt/help.py,sha256=HB3ZsrJVclR0XY-sDH-Lvl5K7ae5QidGiU6WyTAnYhs,1784 +jwt/jwk_set_cache.py,sha256=A395RCkJUDxKLj_M6RZUkS405AeDepEwztU4k4xDSoU,961 +jwt/jwks_client.py,sha256=ALpi3iPIyA2Q-x9zU5qPvzLEepWdjQ-69R5GgAxBmrc,11764 +jwt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +jwt/types.py,sha256=R46Bkno1V08fquxnVr71AvW6uEPilHJlqkgX6ObZFnw,3174 +jwt/utils.py,sha256=5OPU-vCxXEegSZQAiOJWQ0tlZpIOxxY8FyzkdoTCmsA,4067 +jwt/warnings.py,sha256=iQTvP-NPDQA-tuhPhjifEkYRqMUgte99iOPMJc_DLJk,330 +pyjwt-2.14.0.dist-info/licenses/AUTHORS.rst,sha256=klzkNGECnu2_VY7At89_xLBF3vUSDruXk3xwgUBxzwc,322 +pyjwt-2.14.0.dist-info/licenses/LICENSE,sha256=eXp6ICMdTEM-nxkR2xcx0GtYKLmPSZgZoDT3wPVvXOU,1085 +pyjwt-2.14.0.dist-info/METADATA,sha256=uFTrRhqu74BGlhIhB4asH79tEGTx1Rr17iYZt7vdsbo,3449 +pyjwt-2.14.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91 +pyjwt-2.14.0.dist-info/top_level.txt,sha256=RP5DHNyJbMq2ka0FmfTgoSaQzh7e3r5XuCWCO8a00k8,4 +pyjwt-2.14.0.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/pyjwt-2.14.0.dist-info/WHEEL b/source/target-vendors/cp312/pyjwt-2.14.0.dist-info/WHEEL new file mode 100644 index 0000000..13013d7 --- /dev/null +++ b/source/target-vendors/cp312/pyjwt-2.14.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (84.0.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/source/target-vendors/cp312/pyjwt-2.14.0.dist-info/licenses/AUTHORS.rst b/source/target-vendors/cp312/pyjwt-2.14.0.dist-info/licenses/AUTHORS.rst new file mode 100644 index 0000000..88e2b6a --- /dev/null +++ b/source/target-vendors/cp312/pyjwt-2.14.0.dist-info/licenses/AUTHORS.rst @@ -0,0 +1,7 @@ +Authors +======= + +``pyjwt`` is currently written and maintained by `Jose Padilla `_. +Originally written and maintained by `Jeff Lindsay `_. + +A full list of contributors can be found on GitHub’s `overview `_. diff --git a/source/target-vendors/cp312/pyjwt-2.14.0.dist-info/licenses/LICENSE b/source/target-vendors/cp312/pyjwt-2.14.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000..fd0ecbc --- /dev/null +++ b/source/target-vendors/cp312/pyjwt-2.14.0.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-2022 José Padilla + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/source/target-vendors/cp312/pyjwt-2.14.0.dist-info/top_level.txt b/source/target-vendors/cp312/pyjwt-2.14.0.dist-info/top_level.txt new file mode 100644 index 0000000..27ccc9b --- /dev/null +++ b/source/target-vendors/cp312/pyjwt-2.14.0.dist-info/top_level.txt @@ -0,0 +1 @@ +jwt diff --git a/source/target-vendors/cp312/python_dotenv-1.2.3.dist-info/METADATA b/source/target-vendors/cp312/python_dotenv-1.2.3.dist-info/METADATA new file mode 100644 index 0000000..9bc77db --- /dev/null +++ b/source/target-vendors/cp312/python_dotenv-1.2.3.dist-info/METADATA @@ -0,0 +1,820 @@ +Metadata-Version: 2.4 +Name: python-dotenv +Version: 1.2.3 +Summary: Read key-value pairs from a .env file and set them as environment variables +Author-email: Saurabh Kumar +License: BSD-3-Clause +Project-URL: Source, https://github.com/theskumar/python-dotenv +Keywords: environment variables,deployments,settings,env,dotenv,configurations,python +Classifier: Development Status :: 5 - Production/Stable +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: System Administrators +Classifier: Operating System :: OS Independent +Classifier: Topic :: System :: Systems Administration +Classifier: Topic :: Utilities +Classifier: Environment :: Web Environment +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +License-File: LICENSE +Provides-Extra: cli +Requires-Dist: click>=5.0; extra == "cli" +Dynamic: license-file + +# python-dotenv + +[![Build Status][build_status_badge]][build_status_link] +[![PyPI version][pypi_badge]][pypi_link] + +python-dotenv reads key-value pairs from a `.env` file and can set them as +environment variables. It helps in the development of applications following the +[12-factor](https://12factor.net/) principles. + +- [Getting Started](#getting-started) +- [Other Use Cases](#other-use-cases) + - [Load configuration without altering the environment](#load-configuration-without-altering-the-environment) + - [Parse configuration as a stream](#parse-configuration-as-a-stream) + - [Load .env files in IPython](#load-env-files-in-ipython) +- [Command-line Interface](#command-line-interface) +- [File format](#file-format) + - [Multiline values](#multiline-values) + - [Variable expansion](#variable-expansion) +- [Related Projects](#related-projects) +- [Acknowledgements](#acknowledgements) + +## Getting Started + +```shell +pip install python-dotenv +``` + +If your application takes its configuration from environment variables, like a +12-factor application, launching it in development is not very practical because +you have to set those environment variables yourself. + +To help you with that, you can add python-dotenv to your application to make it +load the configuration from a `.env` file when it is present (e.g. in +development) while remaining configurable via the environment: + +```python +from dotenv import load_dotenv + +load_dotenv() # reads variables from a .env file and sets them in os.environ + +# Code of your application, which uses environment variables (e.g. from `os.environ` or +# `os.getenv`) as if they came from the actual environment. +``` + +By default, `load_dotenv()` will: + +- Look for a `.env` file in the same directory as the Python script (or higher up the directory tree). +- Read each key-value pair and add it to `os.environ`. +- **Not override** existing environment variables (`override=False`). Pass `override=True` to override existing variables. + +To configure the development environment, add a `.env` in the root directory of +your project: + +``` +. +├── .env +└── foo.py +``` + +The syntax of `.env` files supported by python-dotenv is similar to that of +Bash: + +```bash +# Development settings +DOMAIN=example.org +ADMIN_EMAIL=admin@${DOMAIN} +ROOT_URL=${DOMAIN}/app +``` + +If you use variables in values, ensure they are surrounded with `{` and `}`, +like `${DOMAIN}`, as bare variables such as `$DOMAIN` are not expanded. + +You will probably want to add `.env` to your `.gitignore`, especially if it +contains secrets like a password. + +See the section "[File format](#file-format)" below for more information about what you can write in a `.env` file. + +## Other Use Cases + +### Load configuration without altering the environment + +The function `dotenv_values` works more or less the same way as `load_dotenv`, +except it doesn't touch the environment, it just returns a `dict` with the +values parsed from the `.env` file. + +```python +from dotenv import dotenv_values + +config = dotenv_values(".env") # config = {"USER": "foo", "EMAIL": "foo@example.org"} +``` + +This notably enables advanced configuration management: + +```python +import os +from dotenv import dotenv_values + +config = { + **dotenv_values(".env.shared"), # load shared development variables + **dotenv_values(".env.secret"), # load sensitive variables + **os.environ, # override loaded values with environment variables +} +``` + +### Parse configuration as a stream + +`load_dotenv` and `dotenv_values` accept [streams][python_streams] via their +`stream` argument. It is thus possible to load the variables from sources other +than the filesystem (e.g. the network). + +```python +from io import StringIO + +from dotenv import load_dotenv + +config = StringIO("USER=foo\nEMAIL=foo@example.org") +load_dotenv(stream=config) +``` + +### Load .env files in IPython + +You can use dotenv in IPython. By default, it will use `find_dotenv` to search for a +`.env` file: + +```python +%load_ext dotenv +%dotenv +``` + +You can also specify a path: + +```python +%dotenv relative/or/absolute/path/to/.env +``` + +Optional flags: + +- `-o` to override existing variables. +- `-v` for increased verbosity. + +### Disable load_dotenv + +Set `PYTHON_DOTENV_DISABLED=1` to disable `load_dotenv()` from loading .env +files or streams. Useful when you can't modify third-party package calls or in +production. + +## Command-line Interface + +A CLI interface `dotenv` is also included, which helps you manipulate the `.env` +file without manually opening it. + +```shell +$ pip install "python-dotenv[cli]" +$ dotenv set USER foo +$ dotenv set EMAIL foo@example.org +$ dotenv list +USER=foo +EMAIL=foo@example.org +$ dotenv list --format=json +{ + "USER": "foo", + "EMAIL": "foo@example.org" +} +$ dotenv run -- python foo.py +``` + +Run `dotenv --help` for more information about the options and subcommands. + +## File format + +The format is not formally specified and still improves over time. That being +said, `.env` files should mostly look like Bash files. Reading from FIFOs (named +pipes) on Unix systems is also supported. + +Keys can be unquoted or single-quoted. Values can be unquoted, single- or +double-quoted. Spaces before and after keys, equal signs, and values are +ignored. Values can be followed by a comment. Lines can start with the `export` +directive, which does not affect their interpretation. + +Allowed escape sequences: + +- in single-quoted values: `\\`, `\'` +- in double-quoted values: `\\`, `\'`, `\"`, `\a`, `\b`, `\f`, `\n`, `\r`, `\t`, `\v` + +### Multiline values + +It is possible for single- or double-quoted values to span multiple lines. The +following examples are equivalent: + +```bash +FOO="first line +second line" +``` + +```bash +FOO="first line\nsecond line" +``` + +### Variable without a value + +A variable can have no value: + +```bash +FOO +``` + +It results in `dotenv_values` associating that variable name with the value +`None` (e.g. `{"FOO": None}`. `load_dotenv`, on the other hand, simply ignores +such variables. + +This shouldn't be confused with `FOO=`, in which case the variable is associated +with the empty string. + +### Variable expansion + +python-dotenv can interpolate variables using POSIX variable expansion. + +With `load_dotenv(override=True)` or `dotenv_values()`, the value of a variable +is the first of the values defined in the following list: + +- Value of that variable in the `.env` file. +- Value of that variable in the environment. +- Default value, if provided. +- Empty string. + +With `load_dotenv(override=False)`, the value of a variable is the first of the +values defined in the following list: + +- Value of that variable in the environment. +- Value of that variable in the `.env` file. +- Default value, if provided. +- Empty string. + +## Related Projects + +- [environs](https://github.com/sloria/environs) +- [Honcho](https://github.com/nickstenning/honcho) +- [dump-env](https://github.com/sobolevn/dump-env) +- [dynaconf](https://github.com/dynaconf/dynaconf) +- [parse_it](https://github.com/naorlivne/parse_it) +- [django-dotenv](https://github.com/jpadilla/django-dotenv) +- [django-environ](https://github.com/joke2k/django-environ) +- [python-decouple](https://github.com/HBNetwork/python-decouple) +- [django-configuration](https://github.com/jezdez/django-configurations) + +## Acknowledgements + +This project is currently maintained by [Saurabh Kumar][saurabh-homepage] and +[Bertrand Bonnefoy-Claudet][gh-bbc2] and would not have been possible without +the support of these [awesome people][contributors]. + +[gh-bbc2]: https://github.com/bbc2 +[saurabh-homepage]: https://saurabh-kumar.com +[pypi_link]: https://badge.fury.io/py/python-dotenv +[pypi_badge]: https://badge.fury.io/py/python-dotenv.svg +[python_streams]: https://docs.python.org/3/library/io.html +[contributors]: https://github.com/theskumar/python-dotenv/graphs/contributors +[build_status_link]: https://github.com/theskumar/python-dotenv/actions/workflows/test.yml +[build_status_badge]: https://github.com/theskumar/python-dotenv/actions/workflows/test.yml/badge.svg + +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this +project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.2.3] - 2026-08-16 + +### Fixed + +- Strip a leading UTF-8 BOM from `.env` file contents so the first variable is no longer silently lost when the file is saved with BOM (e.g. by some JetBrains IDEs on Windows) by [@h1whelan] in [#640] +- `set_key` now escapes backslashes, so values containing them (Windows paths, regular expressions) survive a write/read round-trip. Quoted values ending in an escaped backslash are no longer mis-parsed as an escaped quote, which used to swallow the following lines by [@dchaudhari7177] in [#680] +- `dotenv run` now prints a friendly error instead of a traceback when no command is given by [@bbc2] in [#606] +- Cache the parsed result for empty `.env` files so repeated `dotenv_values`/`load_dotenv` calls no longer re-read the file by [@ReinerBRO] in [#638] + +## [1.2.2] - 2026-03-01 + +### Added + +- Support for Python 3.14, including the free-threaded (3.14t) build. (#588) + +### Changed + +- The `dotenv run` command now forwards flags directly to the specified command by [@bbc2] in [#607] +- Improved documentation clarity regarding override behavior and the reference page. +- Updated PyPy support to version 3.11. +- Documentation for FIFO file support. +- Dropped Support for Python 3.9. + +### Fixed + +- Improved `set_key` and `unset_key` behavior when interacting with symlinks by [@bbc2] in [790c5c0] +- Corrected the license specifier and added missing Python 3.14 classifiers in package metadata by [@JYOuyang] in [#590] + +### Breaking Changes + +- `dotenv.set_key` and `dotenv.unset_key` used to follow symlinks in some + situations. This is no longer the case. For that behavior to be restored in + all cases, `follow_symlinks=True` should be used. + +- In the CLI, `set` and `unset` used to follow symlinks in some situations. This + is no longer the case. + +- `dotenv.set_key`, `dotenv.unset_key` and the CLI commands `set` and `unset` + used to reset the file mode of the modified .env file to `0o600` in some + situations. This is no longer the case: The original mode of the file is now + preserved. Is the file needed to be created or wasn't a regular file, mode + `0o600` is used. + +## [1.2.1] - 2025-10-26 + +- Move more config to `pyproject.toml`, removed `setup.cfg` +- Add support for reading `.env` from FIFOs (Unix) by [@sidharth-sudhir] in [#586] + +## [1.2.0] - 2025-10-26 + +- Upgrade build system to use PEP 517 & PEP 518 to use `build` and `pyproject.toml` by [@EpicWink] in [#583] +- Add support for Python 3.14 by [@23f3001135] in [#579] +- Add support for disabling of `load_dotenv()` using `PYTHON_DOTENV_DISABLED` env var. by [@matthewfranglen] in [#569] + +## [1.1.1] - 2025-06-24 + +### Fixed + +- CLI: Ensure `find_dotenv` work reliably on python 3.13 by [@theskumar] in [#563] +- CLI: revert the use of execvpe on Windows by [@wrongontheinternet] in [#566] + +## [1.1.0] - 2025-03-25 + +### Added + +- Add support for python 3.13 +- Enhance `dotenv run`, switch to `execvpe` for better resource management and signal handling ([#523]) by [@eekstunt] + +### Fixed + +- `find_dotenv` and `load_dotenv` now correctly looks up at the current directory when running in debugger or pdb ([#553] by [@randomseed42]) + +### Misc + +- Drop support for Python 3.8 + +## [1.0.1] - 2024-01-23 + +### Fixed + +- Gracefully handle code which has been imported from a zipfile ([#456] by [@samwyma]) +- Allow modules using `load_dotenv` to be reloaded when launched in a separate thread ([#497] by [@freddyaboulton]) +- Fix file not closed after deletion, handle error in the rewrite function ([#469] by [@Qwerty-133]) + +### Misc + +- Use pathlib.Path in tests ([#466] by [@eumiro]) +- Fix year in release date in changelog.md ([#454] by [@jankislinger]) +- Use https in README links ([#474] by [@Nicals]) + +## [1.0.0] - 2023-02-24 + +### Fixed + +- Drop support for python 3.7, add python 3.12-dev (#449 by [@theskumar]) +- Handle situations where the cwd does not exist. (#446 by [@jctanner]) + +## [0.21.1] - 2023-01-21 + +### Added + +- Use Python 3.11 non-beta in CI (#438 by [@bbc2]) +- Modernize variables code (#434 by [@Nougat-Waffle]) +- Modernize main.py and parser.py code (#435 by [@Nougat-Waffle]) +- Improve conciseness of cli.py and **init**.py (#439 by [@Nougat-Waffle]) +- Improve error message for `get` and `list` commands when env file can't be opened (#441 by [@bbc2]) +- Updated License to align with BSD OSI template (#433 by [@lsmith77]) + +### Fixed + +- Fix Out-of-scope error when "dest" variable is undefined (#413 by [@theGOTOguy]) +- Fix IPython test warning about deprecated `magic` (#440 by [@bbc2]) +- Fix type hint for dotenv_path var, add StrPath alias (#432 by [@eaf]) + +## [0.21.0] - 2022-09-03 + +### Added + +- CLI: add support for invocations via 'python -m'. (#395 by [@theskumar]) +- `load_dotenv` function now returns `False`. (#388 by [@larsks]) +- CLI: add --format= option to list command. (#407 by [@sammck]) + +### Fixed + +- Drop Python 3.5 and 3.6 and upgrade GA (#393 by [@eggplants]) +- Use `open` instead of `io.open`. (#389 by [@rabinadk1]) +- Improve documentation for variables without a value (#390 by [@bbc2]) +- Add `parse_it` to Related Projects (#410 by [@naorlivne]) +- Update README.md (#415 by [@harveer07]) +- Improve documentation with direct use of MkDocs (#398 by [@bbc2]) + +## [0.20.0] - 2022-03-24 + +### Added + +- Add `encoding` (`Optional[str]`) parameter to `get_key`, `set_key` and `unset_key`. + (#379 by [@bbc2]) + +### Fixed + +- Use dict to specify the `entry_points` parameter of `setuptools.setup` (#376 by + [@mgorny]). +- Don't build universal wheels (#387 by [@bbc2]). + +## [0.19.2] - 2021-11-11 + +### Fixed + +- In `set_key`, add missing newline character before new entry if necessary. (#361 by + [@bbc2]) + +## [0.19.1] - 2021-08-09 + +### Added + +- Add support for Python 3.10. (#359 by [@theskumar]) + +## [0.19.0] - 2021-07-24 + +### Changed + +- Require Python 3.5 or a later version. Python 2 and 3.4 are no longer supported. (#341 + by [@bbc2]). + +### Added + +- The `dotenv_path` argument of `set_key` and `unset_key` now has a type of `Union[str, +os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]). +- The `stream` argument of `load_dotenv` and `dotenv_values` can now be a text stream + (`IO[str]`), which includes values like `io.StringIO("foo")` and `open("file.env", +"r")` (#348 by [@bbc2]). + +## [0.18.0] - 2021-06-20 + +### Changed + +- Raise `ValueError` if `quote_mode` isn't one of `always`, `auto` or `never` in + `set_key` (#330 by [@bbc2]). +- When writing a value to a .env file with `set_key` or `dotenv set ` (#330 + by [@bbc2]): + - Use single quotes instead of double quotes. + - Don't strip surrounding quotes. + - In `auto` mode, don't add quotes if the value is only made of alphanumeric characters + (as determined by `string.isalnum`). + +## [0.17.1] - 2021-04-29 + +### Fixed + +- Fixed tests for build environments relying on `PYTHONPATH` (#318 by [@befeleme]). + +## [0.17.0] - 2021-04-02 + +### Changed + +- Make `dotenv get ` only show the value, not `key=value` (#313 by [@bbc2]). + +### Added + +- Add `--override`/`--no-override` option to `dotenv run` (#312 by [@zueve] and [@bbc2]). + +## [0.16.0] - 2021-03-27 + +### Changed + +- The default value of the `encoding` parameter for `load_dotenv` and `dotenv_values` is + now `"utf-8"` instead of `None` (#306 by [@bbc2]). +- Fix resolution order in variable expansion with `override=False` (#287 by [@bbc2]). + +## [0.15.0] - 2020-10-28 + +### Added + +- Add `--export` option to `set` to make it prepend the binding with `export` (#270 by + [@jadutter]). + +### Changed + +- Make `set` command create the `.env` file in the current directory if no `.env` file was + found (#270 by [@jadutter]). + +### Fixed + +- Fix potentially empty expanded value for duplicate key (#260 by [@bbc2]). +- Fix import error on Python 3.5.0 and 3.5.1 (#267 by [@gongqingkui]). +- Fix parsing of unquoted values containing several adjacent space or tab characters + (#277 by [@bbc2], review by [@x-yuri]). + +## [0.14.0] - 2020-07-03 + +### Changed + +- Privilege definition in file over the environment in variable expansion (#256 by + [@elbehery95]). + +### Fixed + +- Improve error message for when file isn't found (#245 by [@snobu]). +- Use HTTPS URL in package meta data (#251 by [@ekohl]). + +## [0.13.0] - 2020-04-16 + +### Added + +- Add support for a Bash-like default value in variable expansion (#248 by [@bbc2]). + +## [0.12.0] - 2020-02-28 + +### Changed + +- Use current working directory to find `.env` when bundled by PyInstaller (#213 by + [@gergelyk]). + +### Fixed + +- Fix escaping of quoted values written by `set_key` (#236 by [@bbc2]). +- Fix `dotenv run` crashing on environment variables without values (#237 by [@yannham]). +- Remove warning when last line is empty (#238 by [@bbc2]). + +## [0.11.0] - 2020-02-07 + +### Added + +- Add `interpolate` argument to `load_dotenv` and `dotenv_values` to disable interpolation + (#232 by [@ulyssessouza]). + +### Changed + +- Use logging instead of warnings (#231 by [@bbc2]). + +### Fixed + +- Fix installation in non-UTF-8 environments (#225 by [@altendky]). +- Fix PyPI classifiers (#228 by [@bbc2]). + +## [0.10.5] - 2020-01-19 + +### Fixed + +- Fix handling of malformed lines and lines without a value (#222 by [@bbc2]): + - Don't print warning when key has no value. + - Reject more malformed lines (e.g. "A: B", "a='b',c"). +- Fix handling of lines with just a comment (#224 by [@bbc2]). + +## [0.10.4] - 2020-01-17 + +### Added + +- Make typing optional (#179 by [@techalchemy]). +- Print a warning on malformed line (#211 by [@bbc2]). +- Support keys without a value (#220 by [@ulyssessouza]). + +## 0.10.3 + +- Improve interactive mode detection ([@andrewsmith])([#183]). +- Refactor parser to fix parsing inconsistencies ([@bbc2])([#170]). + - Interpret escapes as control characters only in double-quoted strings. + - Interpret `#` as start of comment only if preceded by whitespace. + +## 0.10.2 + +- Add type hints and expose them to users ([@qnighy])([#172]) +- `load_dotenv` and `dotenv_values` now accept an `encoding` parameter, defaults to `None` + ([@theskumar])([@earlbread])([#161]) +- Fix `str`/`unicode` inconsistency in Python 2: values are always `str` now. ([@bbc2])([#121]) +- Fix Unicode error in Python 2, introduced in 0.10.0. ([@bbc2])([#176]) + +## 0.10.1 + +- Fix parsing of variable without a value ([@asyncee])([@bbc2])([#158]) + +## 0.10.0 + +- Add support for UTF-8 in unquoted values ([@bbc2])([#148]) +- Add support for trailing comments ([@bbc2])([#148]) +- Add backslashes support in values ([@bbc2])([#148]) +- Add support for newlines in values ([@bbc2])([#148]) +- Force environment variables to str with Python2 on Windows ([@greyli]) +- Drop Python 3.3 support ([@greyli]) +- Fix stderr/-out/-in redirection ([@venthur]) + +## 0.9.0 + +- Add `--version` parameter to cli ([@venthur]) +- Enable loading from current directory ([@cjauvin]) +- Add 'dotenv run' command for calling arbitrary shell script with .env ([@venthur]) + +## 0.8.1 + +- Add tests for docs ([@Flimm]) +- Make 'cli' support optional. Use `pip install python-dotenv[cli]`. ([@theskumar]) + +## 0.8.0 + +- `set_key` and `unset_key` only modified the affected file instead of + parsing and re-writing file, this causes comments and other file + entact as it is. +- Add support for `export` prefix in the line. +- Internal refractoring ([@theskumar]) +- Allow `load_dotenv` and `dotenv_values` to work with `StringIO())` ([@alanjds])([@theskumar])([#78]) + +## 0.7.1 + +- Remove hard dependency on iPython ([@theskumar]) + +## 0.7.0 + +- Add support to override system environment variable via .env. + ([@milonimrod](https://github.com/milonimrod)) + ([\#63](https://github.com/theskumar/python-dotenv/issues/63)) +- Disable ".env not found" warning by default + ([@maxkoryukov](https://github.com/maxkoryukov)) + ([\#57](https://github.com/theskumar/python-dotenv/issues/57)) + +## 0.6.5 + +- Add support for special characters `\`. + ([@pjona](https://github.com/pjona)) + ([\#60](https://github.com/theskumar/python-dotenv/issues/60)) + +## 0.6.4 + +- Fix issue with single quotes ([@Flimm]) + ([\#52](https://github.com/theskumar/python-dotenv/issues/52)) + +## 0.6.3 + +- Handle unicode exception in setup.py + ([\#46](https://github.com/theskumar/python-dotenv/issues/46)) + +## 0.6.2 + +- Fix dotenv list command ([@ticosax](https://github.com/ticosax)) +- Add iPython Support + ([@tillahoffmann](https://github.com/tillahoffmann)) + +## 0.6.0 + +- Drop support for Python 2.6 +- Handle escaped characters and newlines in quoted values. (Thanks + [@iameugenejo](https://github.com/iameugenejo)) +- Remove any spaces around unquoted key/value. (Thanks + [@paulochf](https://github.com/paulochf)) +- Added POSIX variable expansion. (Thanks + [@hugochinchilla](https://github.com/hugochinchilla)) + +## 0.5.1 + +- Fix `find_dotenv` - it now start search from the file where this + function is called from. + +## 0.5.0 + +- Add `find_dotenv` method that will try to find a `.env` file. + (Thanks [@isms](https://github.com/isms)) + +## 0.4.0 + +- cli: Added `-q/--quote` option to control the behaviour of quotes + around values in `.env`. (Thanks + [@hugochinchilla](https://github.com/hugochinchilla)). +- Improved test coverage. + + + +[#78]: https://github.com/theskumar/python-dotenv/issues/78 +[#121]: https://github.com/theskumar/python-dotenv/issues/121 +[#148]: https://github.com/theskumar/python-dotenv/issues/148 +[#158]: https://github.com/theskumar/python-dotenv/issues/158 +[#170]: https://github.com/theskumar/python-dotenv/issues/170 +[#172]: https://github.com/theskumar/python-dotenv/issues/172 +[#176]: https://github.com/theskumar/python-dotenv/issues/176 +[#183]: https://github.com/theskumar/python-dotenv/issues/183 +[#359]: https://github.com/theskumar/python-dotenv/issues/359 +[#469]: https://github.com/theskumar/python-dotenv/issues/469 +[#456]: https://github.com/theskumar/python-dotenv/issues/456 +[#466]: https://github.com/theskumar/python-dotenv/issues/466 +[#454]: https://github.com/theskumar/python-dotenv/issues/454 +[#474]: https://github.com/theskumar/python-dotenv/issues/474 +[#523]: https://github.com/theskumar/python-dotenv/issues/523 +[#553]: https://github.com/theskumar/python-dotenv/issues/553 +[#569]: https://github.com/theskumar/python-dotenv/issues/569 +[#583]: https://github.com/theskumar/python-dotenv/issues/583 +[#586]: https://github.com/theskumar/python-dotenv/issues/586 +[#590]: https://github.com/theskumar/python-dotenv/issues/590 +[#607]: https://github.com/theskumar/python-dotenv/issues/607 +[#588]: https://github.com/theskumar/python-dotenv/issues/588 +[#579]: https://github.com/theskumar/python-dotenv/pull/579 +[#566]: https://github.com/theskumar/python-dotenv/pull/566 +[#563]: https://github.com/theskumar/python-dotenv/pull/563 +[#497]: https://github.com/theskumar/python-dotenv/pull/497 +[#161]: https://github.com/theskumar/python-dotenv/issues/161 +[#606]: https://github.com/theskumar/python-dotenv/pull/606 +[#638]: https://github.com/theskumar/python-dotenv/pull/638 +[#640]: https://github.com/theskumar/python-dotenv/pull/640 +[#680]: https://github.com/theskumar/python-dotenv/pull/680 +[790c5c0]: https://github.com/theskumar/python-dotenv/commit/790c5c02991100aa1bf41ee5330aca75edc51311 + + + +[@23f3001135]: https://github.com/23f3001135 +[@EpicWink]: https://github.com/EpicWink +[@Flimm]: https://github.com/Flimm +[@Nicals]: https://github.com/Nicals +[@Nougat-Waffle]: https://github.com/Nougat-Waffle +[@Qwerty-133]: https://github.com/Qwerty-133 +[@ReinerBRO]: https://github.com/ReinerBRO +[@alanjds]: https://github.com/alanjds +[@altendky]: https://github.com/altendky +[@andrewsmith]: https://github.com/andrewsmith +[@asyncee]: https://github.com/asyncee +[@bbc2]: https://github.com/bbc2 +[@befeleme]: https://github.com/befeleme +[@cjauvin]: https://github.com/cjauvin +[@dchaudhari7177]: https://github.com/dchaudhari7177 +[@eaf]: https://github.com/eaf +[@earlbread]: https://github.com/earlbread +[@eekstunt]: https://github.com/eekstunt +[@eggplants]: https://github.com/eggplants +[@ekohl]: https://github.com/ekohl +[@elbehery95]: https://github.com/elbehery95 +[@eumiro]: https://github.com/eumiro +[@freddyaboulton]: https://github.com/freddyaboulton +[@gergelyk]: https://github.com/gergelyk +[@gongqingkui]: https://github.com/gongqingkui +[@greyli]: https://github.com/greyli +[@h1whelan]: https://github.com/h1whelan +[@harveer07]: https://github.com/harveer07 +[@jadutter]: https://github.com/jadutter +[@jankislinger]: https://github.com/jankislinger +[@jctanner]: https://github.com/jctanner +[@larsks]: https://github.com/larsks +[@lsmith77]: https://github.com/lsmith77 +[@matthewfranglen]: https://github.com/matthewfranglen +[@mgorny]: https://github.com/mgorny +[@naorlivne]: https://github.com/naorlivne +[@qnighy]: https://github.com/qnighy +[@rabinadk1]: https://github.com/rabinadk1 +[@randomseed42]: https://github.com/randomseed42 +[@sammck]: https://github.com/sammck +[@samwyma]: https://github.com/samwyma +[@sidharth-sudhir]: https://github.com/sidharth-sudhir +[@snobu]: https://github.com/snobu +[@techalchemy]: https://github.com/techalchemy +[@theGOTOguy]: https://github.com/theGOTOguy +[@theskumar]: https://github.com/theskumar +[@ulyssessouza]: https://github.com/ulyssessouza +[@venthur]: https://github.com/venthur +[@wrongontheinternet]: https://github.com/wrongontheinternet +[@x-yuri]: https://github.com/x-yuri +[@yannham]: https://github.com/yannham +[@zueve]: https://github.com/zueve +[@JYOuyang]: https://github.com/JYOuyang +[@burnout-projects]: https://github.com/burnout-projects +[@cpackham-atlnz]: https://github.com/cpackham-atlnz +[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v1.2.3...HEAD +[1.2.3]: https://github.com/theskumar/python-dotenv/compare/v1.2.2...v1.2.3 +[1.2.2]: https://github.com/theskumar/python-dotenv/compare/v1.2.1...v1.2.2 +[1.2.1]: https://github.com/theskumar/python-dotenv/compare/v1.2.0...v1.2.1 +[1.2.0]: https://github.com/theskumar/python-dotenv/compare/v1.1.1...v1.2.0 +[1.1.1]: https://github.com/theskumar/python-dotenv/compare/v1.1.0...v1.1.1 +[1.1.0]: https://github.com/theskumar/python-dotenv/compare/v1.0.1...v1.1.0 +[1.0.1]: https://github.com/theskumar/python-dotenv/compare/v1.0.0...v1.0.1 +[1.0.0]: https://github.com/theskumar/python-dotenv/compare/v0.21.0...v1.0.0 +[0.21.1]: https://github.com/theskumar/python-dotenv/compare/v0.21.0...v0.21.1 +[0.21.0]: https://github.com/theskumar/python-dotenv/compare/v0.20.0...v0.21.0 +[0.20.0]: https://github.com/theskumar/python-dotenv/compare/v0.19.2...v0.20.0 +[0.19.2]: https://github.com/theskumar/python-dotenv/compare/v0.19.1...v0.19.2 +[0.19.1]: https://github.com/theskumar/python-dotenv/compare/v0.19.0...v0.19.1 +[0.19.0]: https://github.com/theskumar/python-dotenv/compare/v0.18.0...v0.19.0 +[0.18.0]: https://github.com/theskumar/python-dotenv/compare/v0.17.1...v0.18.0 +[0.17.1]: https://github.com/theskumar/python-dotenv/compare/v0.17.0...v0.17.1 +[0.17.0]: https://github.com/theskumar/python-dotenv/compare/v0.16.0...v0.17.0 +[0.16.0]: https://github.com/theskumar/python-dotenv/compare/v0.15.0...v0.16.0 +[0.15.0]: https://github.com/theskumar/python-dotenv/compare/v0.14.0...v0.15.0 +[0.14.0]: https://github.com/theskumar/python-dotenv/compare/v0.13.0...v0.14.0 +[0.13.0]: https://github.com/theskumar/python-dotenv/compare/v0.12.0...v0.13.0 +[0.12.0]: https://github.com/theskumar/python-dotenv/compare/v0.11.0...v0.12.0 +[0.11.0]: https://github.com/theskumar/python-dotenv/compare/v0.10.5...v0.11.0 +[0.10.5]: https://github.com/theskumar/python-dotenv/compare/v0.10.4...v0.10.5 +[0.10.4]: https://github.com/theskumar/python-dotenv/compare/v0.10.3...v0.10.4 diff --git a/source/target-vendors/cp312/python_dotenv-1.2.3.dist-info/RECORD b/source/target-vendors/cp312/python_dotenv-1.2.3.dist-info/RECORD new file mode 100644 index 0000000..da675af --- /dev/null +++ b/source/target-vendors/cp312/python_dotenv-1.2.3.dist-info/RECORD @@ -0,0 +1,15 @@ +dotenv/__init__.py,sha256=bhY-iK6wwHZamWII6eSCuCAWXUNdn2TQFmEOuXSU7SM,1230 +dotenv/__main__.py,sha256=N0RhLG7nHIqtlJHwwepIo-zbJPNx9sewCCRGY528h_4,129 +dotenv/cli.py,sha256=jFR-2VtVH0kthcycgcVeR2quvtADksFKT_3aO4AHy7s,6865 +dotenv/ipython.py,sha256=dHQBd9PcdCUphGb67Xwy3GkUqMSdf9XUUiNYxTZ3tYU,1326 +dotenv/main.py,sha256=aX6zPu5114tz22Hq-Wv5plR1SMGBHJNQG2fgVuVHDeo,15179 +dotenv/parser.py,sha256=YFJTOmzU4K1QUv6I5ZNoUwWcKO0_7A8JjNKafLN23KQ,5394 +dotenv/py.typed,sha256=8PjyZ1aVoQpRVvt71muvuq5qE-jTFZkK-GLHkhdebmc,26 +dotenv/variables.py,sha256=CD0qXOvvpB3q5RpBQMD9qX6vHX7SyW-SuiwGMFSlt08,2348 +dotenv/version.py,sha256=C-D_WWrVkBDmQmApLcm0sWNh2CgIrwWfc8_sB5vvU-Q,22 +python_dotenv-1.2.3.dist-info/licenses/LICENSE,sha256=gGGbcEnwjIFoOtDgHwjyV6hAZS3XHugxRtNmWMfSwrk,1556 +python_dotenv-1.2.3.dist-info/METADATA,sha256=I4DqlwB5a3OY_YFW4i-QKK_l7r-RjrhKa9XrsKj907U,29008 +python_dotenv-1.2.3.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91 +python_dotenv-1.2.3.dist-info/entry_points.txt,sha256=yRl1rCbswb1nQTQ_gZRlCw5QfabztUGnfGWLhlXFNdI,47 +python_dotenv-1.2.3.dist-info/top_level.txt,sha256=eyqUH4SHJNr6ahOYlxIunTr4XinE8Z5ajWLdrK3r0D8,7 +python_dotenv-1.2.3.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/python_dotenv-1.2.3.dist-info/WHEEL b/source/target-vendors/cp312/python_dotenv-1.2.3.dist-info/WHEEL new file mode 100644 index 0000000..13013d7 --- /dev/null +++ b/source/target-vendors/cp312/python_dotenv-1.2.3.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (84.0.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/source/target-vendors/cp312/python_dotenv-1.2.3.dist-info/entry_points.txt b/source/target-vendors/cp312/python_dotenv-1.2.3.dist-info/entry_points.txt new file mode 100644 index 0000000..0a86823 --- /dev/null +++ b/source/target-vendors/cp312/python_dotenv-1.2.3.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +dotenv = dotenv.__main__:cli diff --git a/source/target-vendors/cp312/python_dotenv-1.2.3.dist-info/licenses/LICENSE b/source/target-vendors/cp312/python_dotenv-1.2.3.dist-info/licenses/LICENSE new file mode 100644 index 0000000..3a97119 --- /dev/null +++ b/source/target-vendors/cp312/python_dotenv-1.2.3.dist-info/licenses/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2014, Saurabh Kumar (python-dotenv), 2013, Ted Tieken (django-dotenv-rw), 2013, Jacob Kaplan-Moss (django-dotenv) + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +- Neither the name of django-dotenv nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/source/target-vendors/cp312/python_dotenv-1.2.3.dist-info/top_level.txt b/source/target-vendors/cp312/python_dotenv-1.2.3.dist-info/top_level.txt new file mode 100644 index 0000000..fe7c01a --- /dev/null +++ b/source/target-vendors/cp312/python_dotenv-1.2.3.dist-info/top_level.txt @@ -0,0 +1 @@ +dotenv diff --git a/source/target-vendors/cp312/python_multipart-0.0.32.dist-info/METADATA b/source/target-vendors/cp312/python_multipart-0.0.32.dist-info/METADATA new file mode 100644 index 0000000..d00853e --- /dev/null +++ b/source/target-vendors/cp312/python_multipart-0.0.32.dist-info/METADATA @@ -0,0 +1,43 @@ +Metadata-Version: 2.4 +Name: python-multipart +Version: 0.0.32 +Summary: A streaming multipart parser for Python +Project-URL: Homepage, https://github.com/Kludex/python-multipart +Project-URL: Documentation, https://kludex.github.io/python-multipart/ +Project-URL: Changelog, https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md +Project-URL: Source, https://github.com/Kludex/python-multipart +Author-email: Andrew Dunham +Maintainer-email: Marcelo Trylesinski +License-Expression: Apache-2.0 +License-File: LICENSE.txt +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires-Python: >=3.10 +Description-Content-Type: text/markdown + +# [Python-Multipart](https://kludex.github.io/python-multipart/) + +[![Build Status](https://github.com/Kludex/python-multipart/workflows/CI/badge.svg)](https://github.com/Kludex/python-multipart/actions) +[![Package version](https://badge.fury.io/py/python-multipart.svg)](https://pypi.python.org/pypi/python-multipart) +[![Supported Python Version](https://img.shields.io/pypi/pyversions/python-multipart.svg?color=%2334D058)](https://pypi.org/project/python-multipart) +[![Discord](https://img.shields.io/discord/1051468649518616576?logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/RxKUF5JuHs) + +--- + +`python-multipart` is an Apache2-licensed streaming multipart parser for Python. +Test coverage is currently 100%. + +## Why? + +Because streaming uploads are awesome for large files. diff --git a/source/target-vendors/cp312/python_multipart-0.0.32.dist-info/RECORD b/source/target-vendors/cp312/python_multipart-0.0.32.dist-info/RECORD new file mode 100644 index 0000000..2e2bab8 --- /dev/null +++ b/source/target-vendors/cp312/python_multipart-0.0.32.dist-info/RECORD @@ -0,0 +1,13 @@ +multipart/__init__.py,sha256=gbY7W9NUdsFZavp-iKQroN4FxonfZqzokkLW5B3jnZ0,1474 +multipart/decoders.py,sha256=XvkAwTU9UFPiXkc0hkvovHf0W6H3vK-2ieWlhav02hQ,40 +multipart/exceptions.py,sha256=6D_X-seiOmMAlIeiGlPGUs8-vpcvIGJeQycFMDb1f7A,42 +multipart/multipart.py,sha256=8fDH14j_VMbrch_58wlzi63XNARGv80kOZAyN72aG7A,41 +python_multipart/__init__.py,sha256=qr5CNo-H4S3lUCU61_h2_FczSuUdjzK_1E4aa06kFPo,937 +python_multipart/decoders.py,sha256=dJmwSRxowd5Xp3s86aIpU8f_Y55Kr0H4f5wgvScidEM,6676 +python_multipart/exceptions.py,sha256=U0lNBjhR4dQI97zV02aQpULQbDjjLqyWk90h5NrCxWA,954 +python_multipart/multipart.py,sha256=Ozy3ZJqDzJa6f4CgsiU4xO9DOb5PPVyUSrT72Rhfn6I,79407 +python_multipart/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +python_multipart-0.0.32.dist-info/METADATA,sha256=6dOZ196l7d8OTSS1wkaHqC5-UL8sKzD4-KBqZNiH3YY,2131 +python_multipart-0.0.32.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87 +python_multipart-0.0.32.dist-info/licenses/LICENSE.txt,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358 +python_multipart-0.0.32.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/python_multipart-0.0.32.dist-info/WHEEL b/source/target-vendors/cp312/python_multipart-0.0.32.dist-info/WHEEL new file mode 100644 index 0000000..bd84226 --- /dev/null +++ b/source/target-vendors/cp312/python_multipart-0.0.32.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.30.1 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/source/target-vendors/cp312/python_multipart-0.0.32.dist-info/licenses/LICENSE.txt b/source/target-vendors/cp312/python_multipart-0.0.32.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/source/target-vendors/cp312/python_multipart-0.0.32.dist-info/licenses/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/source/target-vendors/cp312/python_multipart/__init__.py b/source/target-vendors/cp312/python_multipart/__init__.py new file mode 100644 index 0000000..7b60cac --- /dev/null +++ b/source/target-vendors/cp312/python_multipart/__init__.py @@ -0,0 +1,35 @@ +# Copyright 2012, Andrew Dunham +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.0.32" + +from .multipart import ( + BaseParser, + FormParser, + MultipartParser, + OctetStreamParser, + QuerystringParser, + create_form_parser, + parse_form, +) + +__all__ = ( + "BaseParser", + "FormParser", + "MultipartParser", + "OctetStreamParser", + "QuerystringParser", + "create_form_parser", + "parse_form", +) diff --git a/source/target-vendors/cp312/python_multipart/decoders.py b/source/target-vendors/cp312/python_multipart/decoders.py new file mode 100644 index 0000000..c4cdfb6 --- /dev/null +++ b/source/target-vendors/cp312/python_multipart/decoders.py @@ -0,0 +1,185 @@ +import base64 +import binascii +from typing import TYPE_CHECKING + +from .exceptions import DecodeError + +if TYPE_CHECKING: # pragma: no cover + from typing import Protocol, TypeVar + + _T_contra = TypeVar("_T_contra", contravariant=True) + + class SupportsWrite(Protocol[_T_contra]): + def write(self, __b: _T_contra) -> object: ... + + # No way to specify optional methods. See + # https://github.com/python/typing/issues/601 + # close() [Optional] + # finalize() [Optional] + + +class Base64Decoder: + """This object provides an interface to decode a stream of Base64 data. It + is instantiated with an "underlying object", and whenever a write() + operation is performed, it will decode the incoming data as Base64, and + call write() on the underlying object. This is primarily used for decoding + form data encoded as Base64, but can be used for other purposes:: + + from python_multipart.decoders import Base64Decoder + fd = open("notb64.txt", "wb") + decoder = Base64Decoder(fd) + try: + decoder.write("Zm9vYmFy") # "foobar" in Base64 + decoder.finalize() + finally: + decoder.close() + + # The contents of "notb64.txt" should be "foobar". + + This object will also pass all finalize() and close() calls to the + underlying object, if the underlying object supports them. + + Note that this class maintains a cache of base64 chunks, so that a write of + arbitrary size can be performed. You must call :meth:`finalize` on this + object after all writes are completed to ensure that all data is flushed + to the underlying object. + + :param underlying: the underlying object to pass writes to + """ + + def __init__(self, underlying: "SupportsWrite[bytes]") -> None: + self.cache = bytearray() + self.underlying = underlying + + def write(self, data: bytes) -> int: + """Takes any input data provided, decodes it as base64, and passes it + on to the underlying object. If the data provided is invalid base64 + data, then this method will raise + a :class:`python_multipart.exceptions.DecodeError` + + :param data: base64 data to decode + """ + + # Prepend any cache info to our data. + if len(self.cache) > 0: + data = bytes(self.cache) + data + + # Slice off a string that's a multiple of 4. + decode_len = (len(data) // 4) * 4 + val = data[:decode_len] + + # Decode and write, if we have any. + if len(val) > 0: + try: + decoded = base64.b64decode(val) + except binascii.Error: + raise DecodeError("There was an error raised while decoding base64-encoded data.") + + self.underlying.write(decoded) + + # Get the remaining bytes and save in our cache. + remaining_len = len(data) % 4 + if remaining_len > 0: + self.cache[:] = data[-remaining_len:] + else: + self.cache[:] = b"" + + # Return the length of the data to indicate no error. + return len(data) + + def close(self) -> None: + """Close this decoder. If the underlying object has a `close()` + method, this function will call it. + """ + if hasattr(self.underlying, "close"): + self.underlying.close() + + def finalize(self) -> None: + """Finalize this object. This should be called when no more data + should be written to the stream. This function can raise a + :class:`python_multipart.exceptions.DecodeError` if there is some remaining + data in the cache. + + If the underlying object has a `finalize()` method, this function will + call it. + """ + if len(self.cache) > 0: + raise DecodeError( + "There are %d bytes remaining in the Base64Decoder cache when finalize() is called" % len(self.cache) + ) + + if hasattr(self.underlying, "finalize"): + self.underlying.finalize() + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(underlying={self.underlying!r})" + + +class QuotedPrintableDecoder: + """This object provides an interface to decode a stream of quoted-printable + data. It is instantiated with an "underlying object", in the same manner + as the :class:`python_multipart.decoders.Base64Decoder` class. This class behaves + in exactly the same way, including maintaining a cache of quoted-printable + chunks. + + :param underlying: the underlying object to pass writes to + """ + + def __init__(self, underlying: "SupportsWrite[bytes]") -> None: + self.cache = b"" + self.underlying = underlying + + def write(self, data: bytes) -> int: + """Takes any input data provided, decodes it as quoted-printable, and + passes it on to the underlying object. + + :param data: quoted-printable data to decode + """ + # Prepend any cache info to our data. + if len(self.cache) > 0: + data = self.cache + data + + # If the last 2 characters have an '=' sign in it, then we won't be + # able to decode the encoded value and we'll need to save it for the + # next decoding step. + if data[-2:].find(b"=") != -1: + enc, rest = data[:-2], data[-2:] + else: + enc = data + rest = b"" + + # Encode and write, if we have data. + if len(enc) > 0: + self.underlying.write(binascii.a2b_qp(enc)) + + # Save remaining in cache. + self.cache = rest + return len(data) + + def close(self) -> None: + """Close this decoder. If the underlying object has a `close()` + method, this function will call it. + """ + if hasattr(self.underlying, "close"): + self.underlying.close() + + def finalize(self) -> None: + """Finalize this object. This should be called when no more data + should be written to the stream. This function will not raise any + exceptions, but it may write more data to the underlying object if + there is data remaining in the cache. + + If the underlying object has a `finalize()` method, this function will + call it. + """ + # If we have a cache, write and then remove it. + if len(self.cache) > 0: # pragma: no cover + self.underlying.write(binascii.a2b_qp(self.cache)) + self.cache = b"" + + # Finalize our underlying stream. + if hasattr(self.underlying, "finalize"): + self.underlying.finalize() + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(underlying={self.underlying!r})" diff --git a/source/target-vendors/cp312/python_multipart/exceptions.py b/source/target-vendors/cp312/python_multipart/exceptions.py new file mode 100644 index 0000000..2e7df21 --- /dev/null +++ b/source/target-vendors/cp312/python_multipart/exceptions.py @@ -0,0 +1,34 @@ +class FormParserError(ValueError): + """Base error class for our form parser.""" + + +class ParseError(FormParserError): + """This exception (or a subclass) is raised when there is an error while + parsing something. + """ + + def __init__(self, message: str, *, offset: int = -1) -> None: + super().__init__(message) + self.offset = offset + + +class MultipartParseError(ParseError): + """This is a specific error that is raised when the MultipartParser detects + an error while parsing. + """ + + +class QuerystringParseError(ParseError): + """This is a specific error that is raised when the QuerystringParser + detects an error while parsing. + """ + + +class DecodeError(ParseError): + """This exception is raised when there is a decoding error - for example + with the Base64Decoder or QuotedPrintableDecoder. + """ + + +class FileError(FormParserError, OSError): + """Exception class for problems with the File class.""" diff --git a/source/target-vendors/cp312/python_multipart/multipart.py b/source/target-vendors/cp312/python_multipart/multipart.py new file mode 100644 index 0000000..887cf60 --- /dev/null +++ b/source/target-vendors/cp312/python_multipart/multipart.py @@ -0,0 +1,1925 @@ +from __future__ import annotations + +import logging +import os +import shutil +import sys +import tempfile +from enum import IntEnum +from io import BufferedRandom, BytesIO +from numbers import Number +from typing import TYPE_CHECKING, cast + +from .decoders import Base64Decoder, QuotedPrintableDecoder +from .exceptions import FileError, FormParserError, MultipartParseError, QuerystringParseError + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Literal, Protocol, TypeAlias, TypedDict + + class SupportsRead(Protocol): + def read(self, __n: int) -> bytes: ... + + class QuerystringCallbacks(TypedDict, total=False): + on_field_start: Callable[[], None] + on_field_name: Callable[[bytes, int, int], None] + on_field_data: Callable[[bytes, int, int], None] + on_field_end: Callable[[], None] + on_end: Callable[[], None] + + class OctetStreamCallbacks(TypedDict, total=False): + on_start: Callable[[], None] + on_data: Callable[[bytes, int, int], None] + on_end: Callable[[], None] + + class MultipartCallbacks(TypedDict, total=False): + on_part_begin: Callable[[], None] + on_part_data: Callable[[bytes, int, int], None] + on_part_end: Callable[[], None] + on_header_begin: Callable[[], None] + on_header_field: Callable[[bytes, int, int], None] + on_header_value: Callable[[bytes, int, int], None] + on_header_end: Callable[[], None] + on_headers_finished: Callable[[], None] + on_end: Callable[[], None] + + class FileConfig(TypedDict, total=False): + UPLOAD_DIR: str | bytes | None + UPLOAD_DELETE_TMP: bool + UPLOAD_KEEP_FILENAME: bool + UPLOAD_KEEP_EXTENSIONS: bool + MAX_MEMORY_FILE_SIZE: int + + class FormParserConfig(FileConfig): + UPLOAD_ERROR_ON_BAD_CTE: bool + MAX_BODY_SIZE: float + MAX_HEADER_COUNT: int + MAX_HEADER_SIZE: int + + CallbackName: TypeAlias = Literal[ + "start", + "data", + "end", + "field_start", + "field_name", + "field_data", + "field_end", + "part_begin", + "part_data", + "part_end", + "header_begin", + "header_field", + "header_value", + "header_end", + "headers_finished", + ] + +# Unique missing object. +_missing = object() + + +class QuerystringState(IntEnum): + """Querystring parser states. + + These are used to keep track of the state of the parser, and are used to determine + what to do when new data is encountered. + """ + + BEFORE_FIELD = 0 + FIELD_NAME = 1 + FIELD_DATA = 2 + + +class MultipartState(IntEnum): + """Multipart parser states. + + These are used to keep track of the state of the parser, and are used to determine + what to do when new data is encountered. + """ + + START = 0 + START_BOUNDARY = 1 + HEADER_FIELD_START = 2 + HEADER_FIELD = 3 + HEADER_VALUE_START = 4 + HEADER_VALUE = 5 + HEADER_VALUE_ALMOST_DONE = 6 + HEADERS_ALMOST_DONE = 7 + PART_DATA_START = 8 + PART_DATA = 9 + PART_DATA_END = 10 + END_BOUNDARY = 11 + END = 12 + + +# Flags for the multipart parser. +FLAG_PART_BOUNDARY = 1 +FLAG_LAST_BOUNDARY = 2 + +# Get constants. Since iterating over a str on Python 2 gives you a 1-length +# string, but iterating over a bytes object on Python 3 gives you an integer, +# we need to save these constants. +CR = b"\r"[0] +LF = b"\n"[0] +COLON = b":"[0] +SPACE = b" "[0] +HYPHEN = b"-"[0] +AMPERSAND = b"&"[0] +LOWER_A = b"a"[0] +LOWER_Z = b"z"[0] +NULL = b"\x00"[0] + +# fmt: off +# Mask for ASCII characters that can be http tokens. +# Per RFC7230 - 3.2.6, this is all alpha-numeric characters +# and these: !#$%&'*+-.^_`|~ +TOKEN_CHARS = ( + b"ABCDEFGHIJKLMNOPQRSTUVWXYZ" + b"abcdefghijklmnopqrstuvwxyz" + b"0123456789" + b"!#$%&'*+-.^_`|~") +TOKEN_CHARS_SET = frozenset(TOKEN_CHARS) +# fmt: on + +DEFAULT_MAX_HEADER_COUNT = 8 +"""Default maximum number of headers allowed per multipart part.""" + +DEFAULT_MAX_HEADER_SIZE = 4096 + 128 +"""Default maximum size of a single multipart header line, including syntax overhead.""" + +MAX_BOUNDARY_LENGTH = 256 +"""Maximum allowed length of a multipart boundary. + +[RFC 2046 §5.1.1](https://datatracker.ietf.org/doc/html/rfc2046#section-5.1.1) +recommends boundaries be at most 70 bytes. 256 bytes is generous headroom over +every HTTP client. +""" + + +def _parseparam(s: str) -> list[str]: + # Vendored from the standard library's + # [`email.message._parseparam`](https://github.com/python/cpython/blob/v3.14.2/Lib/email/message.py#L73-L96) + # to split a header into its `;`-separated parts without treating a `;` inside a double-quoted string as a + # separator - and without the RFC 2231 decoding that `email.message.Message.get_params` would apply on top. + s = ";" + s + plist: list[str] = [] + start = 0 + while s.find(";", start) == start: + start += 1 + end = s.find(";", start) + ind, diff = start, 0 + while end > 0: + diff += s.count('"', ind, end) - s.count('\\"', ind, end) + if diff % 2 == 0: + break + end, ind = ind, s.find(";", end + 1) + if end < 0: + end = len(s) + i = s.find("=", start, end) + if i == -1: + f = s[start:end] + else: + f = s[start:i].rstrip().lower() + "=" + s[i + 1 : end].lstrip() + plist.append(f.strip()) + start = end + return plist + + +def parse_options_header(value: str | bytes | None) -> tuple[bytes, dict[bytes, bytes]]: + """Parses a Content-Type header into a value in the following format: (content_type, {parameters}).""" + if not value: + return (b"", {}) + + # If we are passed bytes, we assume that it conforms to WSGI, encoding in latin-1. + if isinstance(value, bytes): # pragma: no cover + value = value.decode("latin-1") + + # For types + assert isinstance(value, str), "Value should be a string by now" + + # If we have no options, return the string as-is. + if ";" not in value: + return (value.lower().strip().encode("latin-1"), {}) + + ctype, *segments = _parseparam(value) + options: dict[bytes, bytes] = {} + for segment in segments: + key, _, val = segment.partition("=") + # [RFC 7578 §4.2](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) + # forbids the RFC 5987/2231 extended syntax (`key*=`, `key*0`, ...) in + # multipart/form-data, so we ignore those parameters and keep the plain + # `key` authoritative. + if "*" in key: + continue + if len(val) >= 2 and val[0] == '"' and val[-1] == '"': + val = val[1:-1].replace("\\\\", "\\").replace('\\"', '"') + # Work around an IE6 bug where the full file path is sent instead of + # just the filename. + if key == "filename" and (val[1:3] == ":\\" or val[:2] == "\\\\"): + val = val.split("\\")[-1] + options[key.encode("latin-1")] = val.encode("latin-1") + return ctype.encode("latin-1"), options + + +class Field: + """A Field object represents a (parsed) form field. It represents a single + field with a corresponding name and value. + + The name that a :class:`Field` will be instantiated with is the same name + that would be found in the following HTML:: + + + + This class defines two methods, :meth:`on_data` and :meth:`on_end`, that + will be called when data is written to the Field, and when the Field is + finalized, respectively. + + Args: + name: The name of the form field. + content_type: The value of the Content-Type header for this field. + """ + + def __init__(self, name: bytes | None, *, content_type: str | None = None) -> None: + self._name = name + self._value: list[bytes] = [] + self._content_type = content_type + + # We cache the joined version of _value for speed. + self._cache = _missing + + @classmethod + def from_value(cls, name: bytes, value: bytes | None) -> Field: + """Create an instance of a :class:`Field`, and set the corresponding + value - either None or an actual value. This method will also + finalize the Field itself. + + Args: + name: the name of the form field. + value: the value of the form field - either a bytestring or None. + + Returns: + A new instance of a [`Field`][python_multipart.Field]. + """ + + f = cls(name) + if value is None: + f.set_none() + else: + f.write(value) + f.finalize() + return f + + def write(self, data: bytes) -> int: + """Write some data into the form field. + + Args: + data: The data to write to the field. + + Returns: + The number of bytes written. + """ + return self.on_data(data) + + def on_data(self, data: bytes) -> int: + """This method is a callback that will be called whenever data is + written to the Field. + + Args: + data: The data to write to the field. + + Returns: + The number of bytes written. + """ + self._value.append(data) + self._cache = _missing + return len(data) + + def on_end(self) -> None: + """This method is called whenever the Field is finalized.""" + if self._cache is _missing: + self._cache = b"".join(self._value) + + def finalize(self) -> None: + """Finalize the form field.""" + self.on_end() + + def close(self) -> None: + """Close the Field object. This will free any underlying cache.""" + # Free our value array. + if self._cache is _missing: + self._cache = b"".join(self._value) + + del self._value + + def set_none(self) -> None: + """Some fields in a querystring can possibly have a value of None - for + example, the string "foo&bar=&baz=asdf" will have a field with the + name "foo" and value None, one with name "bar" and value "", and one + with name "baz" and value "asdf". Since the write() interface doesn't + support writing None, this function will set the field value to None. + """ + self._cache = None + + @property + def field_name(self) -> bytes | None: + """This property returns the name of the field.""" + return self._name + + @property + def value(self) -> bytes | None: + """This property returns the value of the form field.""" + if self._cache is _missing: + self._cache = b"".join(self._value) + + assert isinstance(self._cache, bytes) or self._cache is None + return self._cache + + @property + def content_type(self) -> str | None: + """This property returns the content_type value of the field.""" + return self._content_type + + def __eq__(self, other: object) -> bool: + if isinstance(other, Field): + return self.field_name == other.field_name and self.value == other.value + else: + return NotImplemented + + def __repr__(self) -> str: + if self.value is not None and len(self.value) > 97: + # We get the repr, and then insert three dots before the final + # quote. + v = repr(self.value[:97])[:-1] + "...'" + else: + v = repr(self.value) + + return f"{self.__class__.__name__}(field_name={self.field_name!r}, value={v})" + + +class File: + """This class represents an uploaded file. It handles writing file data to + either an in-memory file or a temporary file on-disk, if the optional + threshold is passed. + + There are some options that can be passed to the File to change behavior + of the class. Valid options are as follows: + + | Name | Type | Default | Description | + |-----------------------|-------|---------|-------------| + | UPLOAD_DIR | `str` | None | The directory to store uploaded files in. If this is None, a temporary file will be created in the system's standard location. | + | UPLOAD_DELETE_TMP | `bool`| True | Delete automatically created TMP file | + | UPLOAD_KEEP_FILENAME | `bool`| False | Whether or not to keep the filename of the uploaded file. If True, then the filename will be converted to a safe representation (e.g. by removing any invalid path segments), and then saved with the same name). Otherwise, a temporary name will be used. | + | UPLOAD_KEEP_EXTENSIONS| `bool`| False | Whether or not to keep the uploaded file's extension. If False, the file will be saved with the default temporary extension (usually ".tmp"). Otherwise, the file's extension will be maintained. Note that this will properly combine with the UPLOAD_KEEP_FILENAME setting. | + | MAX_MEMORY_FILE_SIZE | `int` | 1 MiB | The maximum number of bytes of a File to keep in memory. By default, the contents of a File are kept into memory until a certain limit is reached, after which the contents of the File are written to a temporary file. This behavior can be disabled by setting this value to an appropriately large value (or, for example, infinity, such as `float('inf')`. | + + Args: + file_name: The name of the file that this [`File`][python_multipart.File] represents. + field_name: The name of the form field that this file was uploaded with. This can be None, if, for example, + the file was uploaded with Content-Type application/octet-stream. + config: The configuration for this File. See above for valid configuration keys and their corresponding values. + content_type: The value of the Content-Type header. + """ # noqa: E501 + + def __init__( + self, + file_name: bytes | None, + field_name: bytes | None = None, + config: FileConfig = {}, + *, + content_type: str | None = None, + ) -> None: + # Save configuration, set other variables default. + self.logger = logging.getLogger(__name__) + self._config = config + self._in_memory = True + self._bytes_written = 0 + self._fileobj: BytesIO | BufferedRandom = BytesIO() + + # Save the provided field/file name and content type. + self._field_name = field_name + self._file_name = file_name + self._content_type = content_type + + # Our actual file name is None by default, since, depending on our + # config, we may not actually use the provided name. + self._actual_file_name: bytes | None = None + + # Split the extension from the filename. + if file_name is not None: + # Extract just the basename to avoid directory traversal + basename = os.path.basename(file_name) + base, ext = os.path.splitext(basename) + self._file_base = base + self._ext = ext + + @property + def field_name(self) -> bytes | None: + """The form field associated with this file. May be None if there isn't + one, for example when we have an application/octet-stream upload. + """ + return self._field_name + + @property + def file_name(self) -> bytes | None: + """The file name given in the upload request.""" + return self._file_name + + @property + def actual_file_name(self) -> bytes | None: + """The file name that this file is saved as. Will be None if it's not + currently saved on disk. + """ + return self._actual_file_name + + @property + def file_object(self) -> BytesIO | BufferedRandom: + """The file object that we're currently writing to. Note that this + will either be an instance of a :class:`io.BytesIO`, or a regular file + object. + """ + return self._fileobj + + @property + def size(self) -> int: + """The total size of this file, counted as the number of bytes that + currently have been written to the file. + """ + return self._bytes_written + + @property + def in_memory(self) -> bool: + """A boolean representing whether or not this file object is currently + stored in-memory or on-disk. + """ + return self._in_memory + + @property + def content_type(self) -> str | None: + """The Content-Type value for this part, if it was set.""" + return self._content_type + + def flush_to_disk(self) -> None: + """If the file is already on-disk, do nothing. Otherwise, copy from + the in-memory buffer to a disk file, and then reassign our internal + file object to this new disk file. + + Note that if you attempt to flush a file that is already on-disk, a + warning will be logged to this module's logger. + """ + if not self._in_memory: + self.logger.warning("Trying to flush to disk when we're not in memory") + return + + # Go back to the start of our file. + self._fileobj.seek(0) + + # Open a new file. + new_file = self._get_disk_file() + + # Copy the file objects. + shutil.copyfileobj(self._fileobj, new_file) + + # Seek to the new position in our new file. + new_file.seek(self._bytes_written) + + # Reassign the fileobject. + old_fileobj = self._fileobj + self._fileobj = new_file + + # We're no longer in memory. + self._in_memory = False + + # Close the old file object. + old_fileobj.close() + + def _get_disk_file(self) -> BufferedRandom: + """This function is responsible for getting a file object on-disk for us.""" + self.logger.info("Opening a file on disk") + + file_dir = self._config.get("UPLOAD_DIR") + keep_filename = self._config.get("UPLOAD_KEEP_FILENAME", False) + keep_extensions = self._config.get("UPLOAD_KEEP_EXTENSIONS", False) + delete_tmp = self._config.get("UPLOAD_DELETE_TMP", True) + tmp_file: None | BufferedRandom = None + + # If we have a directory and are to keep the filename... + if file_dir is not None and keep_filename: + self.logger.info("Saving with filename in: %r", file_dir) + + # Build our filename. + # TODO: what happens if we don't have a filename? + fname = self._file_base + self._ext if keep_extensions else self._file_base + + path = os.path.join(file_dir, fname) # type: ignore[arg-type] + try: + self.logger.info("Opening file: %r", path) + tmp_file = open(path, "w+b") + except OSError: + tmp_file = None + + self.logger.exception("Error opening temporary file") + raise FileError("Error opening temporary file: %r" % path) + else: + # Build options array. + # Note that on Python 3, tempfile doesn't support byte names. We + # encode our paths using the default filesystem encoding. + suffix = self._ext.decode(sys.getfilesystemencoding()) if keep_extensions else None + + if file_dir is None: + dir = None + elif isinstance(file_dir, bytes): + dir = file_dir.decode(sys.getfilesystemencoding()) + else: + dir = file_dir # pragma: no cover + + # Create a temporary (named) file with the appropriate settings. + self.logger.info( + "Creating a temporary file with options: %r", {"suffix": suffix, "delete": delete_tmp, "dir": dir} + ) + try: + tmp_file = cast(BufferedRandom, tempfile.NamedTemporaryFile(suffix=suffix, delete=delete_tmp, dir=dir)) + except OSError: + self.logger.exception("Error creating named temporary file") + raise FileError("Error creating named temporary file") + + assert tmp_file is not None + # Encode filename as bytes. + if isinstance(tmp_file.name, str): + fname = tmp_file.name.encode(sys.getfilesystemencoding()) + else: + fname = cast(bytes, tmp_file.name) # pragma: no cover + + self._actual_file_name = fname + return tmp_file + + def write(self, data: bytes) -> int: + """Write some data to the File. + + :param data: a bytestring + """ + return self.on_data(data) + + def on_data(self, data: bytes) -> int: + """This method is a callback that will be called whenever data is + written to the File. + + Args: + data: The data to write to the file. + + Returns: + The number of bytes written. + """ + bwritten = self._fileobj.write(data) + + # If the bytes written isn't the same as the length, just return. + if bwritten != len(data): + self.logger.warning("bwritten != len(data) (%d != %d)", bwritten, len(data)) + return bwritten + + # Keep track of how many bytes we've written. + self._bytes_written += bwritten + + # If we're in-memory and are over our limit, we create a file. + max_memory_file_size = self._config.get("MAX_MEMORY_FILE_SIZE") + if self._in_memory and max_memory_file_size is not None and (self._bytes_written > max_memory_file_size): + self.logger.info("Flushing to disk") + self.flush_to_disk() + + # Return the number of bytes written. + return bwritten + + def on_end(self) -> None: + """This method is called whenever the Field is finalized.""" + # Flush the underlying file object + self._fileobj.flush() + + def finalize(self) -> None: + """Finalize the form file. This will not close the underlying file, + but simply signal that we are finished writing to the File. + """ + self.on_end() + + def close(self) -> None: + """Close the File object. This will actually close the underlying + file object (whether it's a :class:`io.BytesIO` or an actual file + object). + """ + self._fileobj.close() + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(file_name={self.file_name!r}, field_name={self.field_name!r})" + + +class BaseParser: + """This class is the base class for all parsers. It contains the logic for + calling and adding callbacks. + + A callback can be one of two different forms. "Notification callbacks" are + callbacks that are called when something happens - for example, when a new + part of a multipart message is encountered by the parser. "Data callbacks" + are called when we get some sort of data - for example, part of the body of + a multipart chunk. Notification callbacks are called with no parameters, + whereas data callbacks are called with three, as follows:: + + data_callback(data, start, end) + + The "data" parameter is a bytestring (i.e. "foo" on Python 2, or b"foo" on + Python 3). "start" and "end" are integer indexes into the "data" string + that represent the data of interest. Thus, in a data callback, the slice + `data[start:end]` represents the data that the callback is "interested in". + The callback is not passed a copy of the data, since copying severely hurts + performance. + """ + + def __init__(self) -> None: + self.logger = logging.getLogger(__name__) + self.callbacks: QuerystringCallbacks | OctetStreamCallbacks | MultipartCallbacks = {} + + def callback( + self, name: CallbackName, data: bytes | None = None, start: int | None = None, end: int | None = None + ) -> None: + """This function calls a provided callback with some data. If the + callback is not set, will do nothing. + + Args: + name: The name of the callback to call (as a string). + data: Data to pass to the callback. If None, then it is assumed that the callback is a notification + callback, and no parameters are given. + end: An integer that is passed to the data callback. + start: An integer that is passed to the data callback. + """ + func = self.callbacks.get("on_" + name) + if func is None: + return + func = cast("Callable[..., Any]", func) + # Depending on whether we're given a buffer... + if data is not None: + # Don't do anything if we have start == end. + if start is not None and start == end: + return + func(data, start, end) + else: + func() + + def set_callback(self, name: CallbackName, new_func: Callable[..., Any] | None) -> None: + """Update the function for a callback. Removes from the callbacks dict + if new_func is None. + + :param name: The name of the callback to call (as a string). + + :param new_func: The new function for the callback. If None, then the + callback will be removed (with no error if it does not + exist). + """ + if new_func is None: + self.callbacks.pop("on_" + name, None) # type: ignore[misc] + else: + self.callbacks["on_" + name] = new_func # type: ignore[literal-required] + + def close(self) -> None: + pass # pragma: no cover + + def finalize(self) -> None: + pass # pragma: no cover + + def __repr__(self) -> str: + return "%s()" % self.__class__.__name__ + + +class OctetStreamParser(BaseParser): + """This parser parses an octet-stream request body and calls callbacks when + incoming data is received. Callbacks are as follows: + + | Callback Name | Parameters | Description | + |----------------|-----------------|-----------------------------------------------------| + | on_start | None | Called when the first data is parsed. | + | on_data | data, start, end| Called for each data chunk that is parsed. | + | on_end | None | Called when the parser is finished parsing all data.| + + Args: + callbacks: A dictionary of callbacks. See the documentation for [`BaseParser`][python_multipart.BaseParser]. + max_size: The maximum size of body to parse. Defaults to infinity - i.e. unbounded. + """ + + def __init__(self, callbacks: OctetStreamCallbacks = {}, max_size: float = float("inf")): + super().__init__() + self.callbacks = callbacks + self._started = False + + if not isinstance(max_size, Number) or max_size < 1: + raise ValueError("max_size must be a positive number, not %r" % max_size) + self.max_size: int | float = max_size + self._current_size = 0 + + def write(self, data: bytes) -> int: + """Write some data to the parser, which will perform size verification, + and then pass the data to the underlying callback. + + Args: + data: The data to write to the parser. + + Returns: + The number of bytes written. + """ + if not self._started: + self.callback("start") + self._started = True + + # Truncate data length. + data_len = len(data) + if (self._current_size + data_len) > self.max_size: + # We truncate the length of data that we are to process. + new_size = int(self.max_size - self._current_size) + self.logger.warning( + "Current size is %d (max %d), so truncating data length from %d to %d", + self._current_size, + self.max_size, + data_len, + new_size, + ) + data_len = new_size + + # Increment size, then callback, in case there's an exception. + self._current_size += data_len + self.callback("data", data, 0, data_len) + return data_len + + def finalize(self) -> None: + """Finalize this parser, which signals to that we are finished parsing, + and sends the on_end callback. + """ + self.callback("end") + + def __repr__(self) -> str: + return "%s()" % self.__class__.__name__ + + +class QuerystringParser(BaseParser): + """This is a streaming querystring parser. It will consume data, and call + the callbacks given when it has data. + + | Callback Name | Parameters | Description | + |----------------|-----------------|-----------------------------------------------------| + | on_field_start | None | Called when a new field is encountered. | + | on_field_name | data, start, end| Called when a portion of a field's name is encountered. | + | on_field_data | data, start, end| Called when a portion of a field's data is encountered. | + | on_field_end | None | Called when the end of a field is encountered. | + | on_end | None | Called when the parser is finished parsing all data.| + + Args: + callbacks: A dictionary of callbacks. See the documentation for [`BaseParser`][python_multipart.BaseParser]. + strict_parsing: Whether or not to parse the body strictly. Defaults to False. If this is set to True, then the + behavior of the parser changes as the following: if a field has a value with an equal sign + (e.g. "foo=bar", or "foo="), it is always included. If a field has no equals sign (e.g. "...&name&..."), + it will be treated as an error if 'strict_parsing' is True, otherwise included. If an error is encountered, + then a [`QuerystringParseError`][python_multipart.exceptions.QuerystringParseError] will be raised. + max_size: The maximum size of body to parse. Defaults to infinity - i.e. unbounded. + """ # noqa: E501 + + state: QuerystringState + + def __init__( + self, callbacks: QuerystringCallbacks = {}, strict_parsing: bool = False, max_size: float = float("inf") + ) -> None: + super().__init__() + self.state = QuerystringState.BEFORE_FIELD + self._found_sep = False + + self.callbacks = callbacks + + # Max-size stuff + if not isinstance(max_size, Number) or max_size < 1: + raise ValueError("max_size must be a positive number, not %r" % max_size) + self.max_size: int | float = max_size + self._current_size = 0 + + # Should parsing be strict? + self.strict_parsing = strict_parsing + + def write(self, data: bytes) -> int: + """Write some data to the parser, which will perform size verification, + parse into either a field name or value, and then pass the + corresponding data to the underlying callback. If an error is + encountered while parsing, a QuerystringParseError will be raised. The + "offset" attribute of the raised exception will be set to the offset in + the input data chunk (NOT the overall stream) that caused the error. + + Args: + data: The data to write to the parser. + + Returns: + The number of bytes written. + """ + # Handle sizing. + data_len = len(data) + if (self._current_size + data_len) > self.max_size: + # We truncate the length of data that we are to process. + new_size = int(self.max_size - self._current_size) + self.logger.warning( + "Current size is %d (max %d), so truncating data length from %d to %d", + self._current_size, + self.max_size, + data_len, + new_size, + ) + data_len = new_size + + l = 0 + try: + l = self._internal_write(data, data_len) + finally: + self._current_size += l + + return l + + def _internal_write(self, data: bytes, length: int) -> int: + state = self.state + strict_parsing = self.strict_parsing + found_sep = self._found_sep + + i = 0 + while i < length: + ch = data[i] + + # Depending on our state... + if state == QuerystringState.BEFORE_FIELD: + # If the 'found_sep' flag is set, we've already encountered + # and skipped a single separator. If so, we check our strict + # parsing flag and decide what to do. Otherwise, we haven't + # yet reached a separator, and thus, if we do, we need to skip + # it as it will be the boundary between fields that's supposed + # to be there. + if ch == AMPERSAND: + if found_sep: + # If we're parsing strictly, we disallow blank chunks. + if strict_parsing: + raise QuerystringParseError("Skipping duplicate ampersand at %d" % i, offset=i) + else: + self.logger.debug("Skipping duplicate ampersand at %d", i) + else: + # This case is when we're skipping the (first) + # separator between fields, so we just set our flag + # and continue on. + found_sep = True + else: + # Emit a field-start event, and go to that state. Also, + # reset the "found_sep" flag, for the next time we get to + # this state. + self.callback("field_start") + i -= 1 + state = QuerystringState.FIELD_NAME + found_sep = False + + elif state == QuerystringState.FIELD_NAME: + # Try and find a separator - we ensure that, if we do, we only + # look for the equal sign before it. + sep_pos = data.find(b"&", i, length) + + # See if we can find an equals sign in the remaining data. If + # so, we can immediately emit the field name and jump to the + # data state. + if sep_pos != -1: + equals_pos = data.find(b"=", i, sep_pos) + else: + equals_pos = data.find(b"=", i, length) + + if equals_pos != -1: + # Emit this name. + self.callback("field_name", data, i, equals_pos) + + # Jump i to this position. Note that it will then have 1 + # added to it below, which means the next iteration of this + # loop will inspect the character after the equals sign. + i = equals_pos + state = QuerystringState.FIELD_DATA + else: + # No equals sign found. + if not strict_parsing: + # See also comments in the QuerystringState.FIELD_DATA case below. + # If we found the separator, we emit the name and just + # end - there's no data callback at all (not even with + # a blank value). + if sep_pos != -1: + self.callback("field_name", data, i, sep_pos) + self.callback("field_end") + + i = sep_pos - 1 + state = QuerystringState.BEFORE_FIELD + else: + # Otherwise, no separator in this block, so the + # rest of this chunk must be a name. + self.callback("field_name", data, i, length) + i = length + + else: + # We're parsing strictly. If we find a separator, + # this is an error - we require an equals sign. + if sep_pos != -1: + raise QuerystringParseError( + "When strict_parsing is True, we require an " + "equals sign in all field chunks. Did not " + "find one in the chunk that starts at %d" % (i,), + offset=i, + ) + + # No separator in the rest of this chunk, so it's just + # a field name. + self.callback("field_name", data, i, length) + i = length + + elif state == QuerystringState.FIELD_DATA: + # Try finding an ampersand after this position. + sep_pos = data.find(b"&", i, length) + + # If we found it, callback this bit as data and then go back + # to expecting to find a field. + if sep_pos != -1: + self.callback("field_data", data, i, sep_pos) + self.callback("field_end") + + # Note that we go to the separator, which brings us to the + # "before field" state. This allows us to properly emit + # "field_start" events only when we actually have data for + # a field of some sort. + i = sep_pos - 1 + state = QuerystringState.BEFORE_FIELD + + # Otherwise, emit the rest as data and finish. + else: + self.callback("field_data", data, i, length) + i = length + + else: # pragma: no cover (error case) + msg = "Reached an unknown state %d at %d" % (state, i) + self.logger.warning(msg) + raise QuerystringParseError(msg, offset=i) + + i += 1 + + self.state = state + self._found_sep = found_sep + return length + + def finalize(self) -> None: + """Finalize this parser, which signals to that we are finished parsing, + if we're still in the middle of a field, an on_field_end callback, and + then the on_end callback. + """ + # If we're currently in the middle of a field, we finish it. + if self.state in (QuerystringState.FIELD_DATA, QuerystringState.FIELD_NAME): + self.callback("field_end") + self.callback("end") + + def __repr__(self) -> str: + return "{}(strict_parsing={!r}, max_size={!r})".format( + self.__class__.__name__, self.strict_parsing, self.max_size + ) + + +class MultipartParser(BaseParser): + """This class is a streaming multipart/form-data parser. + + | Callback Name | Parameters | Description | + |--------------------|-----------------|-------------| + | on_part_begin | None | Called when a new part of the multipart message is encountered. | + | on_part_data | data, start, end| Called when a portion of a part's data is encountered. | + | on_part_end | None | Called when the end of a part is reached. | + | on_header_begin | None | Called when we've found a new header in a part of a multipart message | + | on_header_field | data, start, end| Called each time an additional portion of a header is read (i.e. the part of the header that is before the colon; the "Foo" in "Foo: Bar"). | + | on_header_value | data, start, end| Called when we get data for a header. | + | on_header_end | None | Called when the current header is finished - i.e. we've reached the newline at the end of the header. | + | on_headers_finished| None | Called when all headers are finished, and before the part data starts. | + | on_end | None | Called when the parser is finished parsing all data. | + + Args: + boundary: The multipart boundary. This is required, and must match what is given in the HTTP request - usually in the Content-Type header. + callbacks: A dictionary of callbacks. See the documentation for [`BaseParser`][python_multipart.BaseParser]. + max_size: The maximum size of body to parse. Defaults to infinity - i.e. unbounded. + max_header_count: The maximum number of headers allowed per part. + max_header_size: The maximum size of a single header line (excluding the trailing CRLF). + """ # noqa: E501 + + def __init__( + self, + boundary: bytes | str, + callbacks: MultipartCallbacks = {}, + max_size: float = float("inf"), + *, + max_header_count: int = DEFAULT_MAX_HEADER_COUNT, + max_header_size: int = DEFAULT_MAX_HEADER_SIZE, + ) -> None: + # Initialize parser state. + super().__init__() + self.state = MultipartState.START + self.index = self.flags = 0 + + self.callbacks = callbacks + + if not isinstance(max_size, Number) or max_size < 1: + raise ValueError("max_size must be a positive number, not %r" % max_size) + self.max_size = max_size + self._current_size = 0 + + self.max_header_count = max_header_count + self._current_header_count = 0 + + self.max_header_size = max_header_size + self._current_header_size = 0 + + # Setup marks. These are used to track the state of data received. + self.marks: dict[str, int] = {} + + # Save our boundary. + if isinstance(boundary, str): # pragma: no cover + boundary = boundary.encode("latin-1") + if len(boundary) > MAX_BOUNDARY_LENGTH: + raise FormParserError(f"Boundary length {len(boundary)} exceeds maximum of {MAX_BOUNDARY_LENGTH}") + self.boundary = b"\r\n--" + boundary + + def write(self, data: bytes) -> int: + """Write some data to the parser, which will perform size verification, + and then parse the data into the appropriate location (e.g. header, + data, etc.), and pass this on to the underlying callback. If an error + is encountered, a MultipartParseError will be raised. The "offset" + attribute on the raised exception will be set to the offset of the byte + in the input chunk that caused the error. + + Args: + data: The data to write to the parser. + + Returns: + The number of bytes written. + """ + # Handle sizing. + data_len = len(data) + if (self._current_size + data_len) > self.max_size: + # We truncate the length of data that we are to process. + new_size = int(self.max_size - self._current_size) + self.logger.warning( + "Current size is %d (max %d), so truncating data length from %d to %d", + self._current_size, + self.max_size, + data_len, + new_size, + ) + data_len = new_size + + l = 0 + try: + l = self._internal_write(data, data_len) + finally: + self._current_size += l + + return l + + def _internal_write(self, data: bytes, length: int) -> int: + # Get values from locals. + boundary = self.boundary + boundary_length = len(boundary) + + # Get our state, flags and index. These are persisted between calls to + # this function. + state = self.state + index = self.index + flags = self.flags + current_header_count = self._current_header_count + current_header_size = self._current_header_size + + # Our index defaults to 0. + i = 0 + + def advance_header_size(amount: int = 1) -> None: + nonlocal current_header_size + current_header_size += amount + if current_header_size > self.max_header_size: + raise MultipartParseError("Maximum header size exceeded", offset=i) + + # Set a mark. + def set_mark(name: str) -> None: + self.marks[name] = i + + # Remove a mark. + def delete_mark(name: str, reset: bool = False) -> None: + self.marks.pop(name, None) + + # Helper function that makes calling a callback with data easier. The + # 'remaining' parameter will callback from the marked value until the + # end of the buffer, and reset the mark, instead of deleting it. This + # is used at the end of the function to call our callbacks with any + # remaining data in this chunk. + def data_callback(name: CallbackName, end_i: int, remaining: bool = False) -> None: + marked_index = self.marks.get(name) + if marked_index is None: + return + + # Otherwise, we call it from the mark to the current byte we're + # processing. + if end_i <= marked_index: + # There is no additional data to send. + pass + elif marked_index >= 0: + # We are emitting data from the local buffer. + self.callback(name, data, marked_index, end_i) + else: + # Some of the data comes from a partial boundary match. + # and requires look-behind. + # We need to use self.flags (and not flags) because we care about + # the state when we entered the loop. + lookbehind_len = -marked_index + if lookbehind_len <= boundary_length: + self.callback(name, boundary, 0, lookbehind_len) + elif self.flags & FLAG_PART_BOUNDARY: + lookback = boundary + b"\r\n" + self.callback(name, lookback, 0, lookbehind_len) + elif self.flags & FLAG_LAST_BOUNDARY: + lookback = boundary + b"--\r\n" + self.callback(name, lookback, 0, lookbehind_len) + else: # pragma: no cover (error case) + self.logger.warning("Look-back buffer error") + + if end_i > 0: + self.callback(name, data, 0, end_i) + # If we're getting remaining data, we have got all the data we + # can be certain is not a boundary, leaving only a partial boundary match. + if remaining: + self.marks[name] = end_i - length + else: + self.marks.pop(name, None) + + # For each byte... + while i < length: + c = data[i] + + if state == MultipartState.START: + # Skip leading newlines + if c == CR or c == LF: + i = data.find(b"-", i) + if i == -1: + # No boundary candidate in this chunk, so ignore the content after the leading CR/LF. + i = length + break + continue + + # index is used as in index into our boundary. Set to 0. + index = 0 + + # Move to the next state, but decrement i so that we re-process + # this character. + state = MultipartState.START_BOUNDARY + i -= 1 + + elif state == MultipartState.START_BOUNDARY: + # Check to ensure that the last 2 characters in our boundary + # are CRLF. + if index == boundary_length - 2: + if c == HYPHEN: + # Potential empty message. + state = MultipartState.END_BOUNDARY + elif c != CR: + # Error! + msg = "Did not find CR at end of boundary (%d)" % (i,) + self.logger.warning(msg) + raise MultipartParseError(msg, offset=i) + + index += 1 + + elif index == boundary_length - 1: + if c != LF: + msg = "Did not find LF at end of boundary (%d)" % (i,) + self.logger.warning(msg) + raise MultipartParseError(msg, offset=i) + + # The index is now used for indexing into our boundary. + index = 0 + + # Callback for the start of a part. + self.callback("part_begin") + current_header_count = 0 + current_header_size = 0 + + # Move to the next character and state. + state = MultipartState.HEADER_FIELD_START + + else: + # Check to ensure our boundary matches + if c != boundary[index + 2]: + msg = "Expected boundary character %r, got %r at index %d" % (boundary[index + 2], c, index + 2) + self.logger.warning(msg) + raise MultipartParseError(msg, offset=i) + + # Increment index into boundary and continue. + index += 1 + + elif state == MultipartState.HEADER_FIELD_START: + # Mark the start of a header field here, reset the index, and + # continue parsing our header field. + index = 0 + + if c != CR: + current_header_count += 1 + if current_header_count > self.max_header_count: + raise MultipartParseError("Maximum header count exceeded", offset=i) + current_header_size = 0 + + # Set a mark of our header field. + set_mark("header_field") + + # Notify that we're starting a header if the next character is + # not a CR; a CR at the beginning of the header will cause us + # to stop parsing headers in the MultipartState.HEADER_FIELD state, + # below. + if c != CR: + self.callback("header_begin") + + # Move to parsing header fields. + state = MultipartState.HEADER_FIELD + i -= 1 + + elif state == MultipartState.HEADER_FIELD: + # If we've reached a CR at the beginning of a header, it means + # that we've reached the second of 2 newlines, and so there are + # no more headers to parse. + if c == CR and index == 0: + delete_mark("header_field") + state = MultipartState.HEADERS_ALMOST_DONE + i += 1 + continue + + # The field name runs until the colon; jump straight to it and + # validate the whole span at once instead of byte by byte. + colon = data.find(b":", i, length) + end = colon if colon != -1 else length + + # Enforce the size limit before slicing and validating, so an oversized header + # name fails fast instead of copying and scanning a potentially huge span. + advance_header_size(end - i if colon == -1 else end - i + 1) + + field = data[i:end] + if field.translate(None, TOKEN_CHARS): + bad = next(b for b in field if b not in TOKEN_CHARS_SET) + bad_i = i + field.index(bad) + msg = "Found invalid character %r in header at %d" % (bad, bad_i) + self.logger.warning(msg) + raise MultipartParseError(msg, offset=bad_i) + + index += end - i + if colon == -1: + # Field name continues into the next chunk. + i = length + else: + # A 0-length header is an error. + if index == 0: + msg = "Found 0-length header at %d" % (i,) + self.logger.warning(msg) + raise MultipartParseError(msg, offset=i) + + # Call our callback with the header field. + i = colon + data_callback("header_field", i) + + # Move to parsing the header value. + state = MultipartState.HEADER_VALUE_START + + elif state == MultipartState.HEADER_VALUE_START: + # Skip leading spaces. + if c == SPACE: + advance_header_size() + i += 1 + continue + + # Mark the start of the header value. + set_mark("header_value") + + # Move to the header-value state, reprocessing this character. + state = MultipartState.HEADER_VALUE + i -= 1 + + elif state == MultipartState.HEADER_VALUE: + # The value runs until the terminating CR; jump straight to it + # instead of inspecting every byte. + cr = data.find(b"\r", i, length) + end = cr if cr != -1 else length + advance_header_size(end - i) + if cr != -1: + i = cr + data_callback("header_value", i) + self.callback("header_end") + current_header_size = 0 + state = MultipartState.HEADER_VALUE_ALMOST_DONE + else: + i = length + + elif state == MultipartState.HEADER_VALUE_ALMOST_DONE: + # The last character should be a LF. If not, it's an error. + if c != LF: + msg = f"Did not find LF character at end of header (found {c!r})" + self.logger.warning(msg) + raise MultipartParseError(msg, offset=i) + + # Move back to the start of another header. Note that if that + # state detects ANOTHER newline, it'll trigger the end of our + # headers. + state = MultipartState.HEADER_FIELD_START + + elif state == MultipartState.HEADERS_ALMOST_DONE: + # We're almost done our headers. This is reached when we parse + # a CR at the beginning of a header, so our next character + # should be a LF, or it's an error. + if c != LF: + msg = f"Did not find LF at end of headers (found {c!r})" + self.logger.warning(msg) + raise MultipartParseError(msg, offset=i) + + self.callback("headers_finished") + state = MultipartState.PART_DATA_START + + elif state == MultipartState.PART_DATA_START: + # Mark the start of our part data. + set_mark("part_data") + + # Start processing part data, including this character. + state = MultipartState.PART_DATA + i -= 1 + + elif state == MultipartState.PART_DATA: + # We're processing our part data right now. During this, we + # need to efficiently search for our boundary, since any data + # on any number of lines can be a part of the current data. + + # Save the current value of our index. We use this in case we + # find part of a boundary, but it doesn't match fully. + prev_index = index + + # If our index is 0, we're starting a new part, so start our + # search. + if index == 0: + # The most common case is likely to be that the whole + # boundary is present in the buffer. + # Calling `find` is much faster than iterating here. + i0 = data.find(boundary, i, length) + if i0 >= 0: + # We matched the whole boundary string. + index = boundary_length - 1 + i = i0 + boundary_length - 1 + c = data[i] + else: + # No whole boundary, but the tail may hold a partial one + # that completes in the next chunk. Boundary starts with + # CR, which an RFC boundary contains nowhere else, so the + # last CR in the tail is the only candidate prefix start. + k = data.rfind(boundary[:1], max(i, length - boundary_length + 1), length) + if k != -1 and boundary.startswith(data[k:length]): + index = length - k + # Carry the partial via index; the end-of-chunk flush + # emits the data before it and re-marks the lookbehind. + i = length + continue + + # Now, we have a couple of cases here. If our index is before + # the end of the boundary... + if index < boundary_length: + # If the character matches... + if boundary[index] == c: + # The current character matches, so continue! + index += 1 + else: + index = 0 + + # Our index is equal to the length of our boundary! + elif index == boundary_length: + # First we increment it. + index += 1 + + # Now, if we've reached a newline, we need to set this as + # the potential end of our boundary. + if c == CR: + flags |= FLAG_PART_BOUNDARY + + # Otherwise, if this is a hyphen, we might be at the last + # of all boundaries. + elif c == HYPHEN: + flags |= FLAG_LAST_BOUNDARY + + # Otherwise, we reset our index, since this isn't either a + # newline or a hyphen. + else: + index = 0 + + # Our index is right after the part boundary, which should be + # a LF. + elif index == boundary_length + 1: + # If we're at a part boundary (i.e. we've seen a CR + # character already)... + if flags & FLAG_PART_BOUNDARY: + # We need a LF character next. + if c == LF: + # Unset the part boundary flag. + flags &= ~FLAG_PART_BOUNDARY + + # We have identified a boundary, callback for any data before it. + data_callback("part_data", i - index) + # Callback indicating that we've reached the end of + # a part, and are starting a new one. + self.callback("part_end") + self.callback("part_begin") + current_header_count = 0 + current_header_size = 0 + + # Move to parsing new headers. + index = 0 + state = MultipartState.HEADER_FIELD_START + i += 1 + continue + + # We didn't find an LF character, so no match. Reset + # our index and clear our flag. + index = 0 + flags &= ~FLAG_PART_BOUNDARY + + # Otherwise, if we're at the last boundary (i.e. we've + # seen a hyphen already)... + elif flags & FLAG_LAST_BOUNDARY: + # We need a second hyphen here. + if c == HYPHEN: + # We have identified a boundary, callback for any data before it. + data_callback("part_data", i - index) + # Callback to end the current part, and then the + # message. + self.callback("part_end") + self.callback("end") + state = MultipartState.END + else: + # No match, so reset index. + index = 0 + + # Otherwise, our index is 0. If the previous index is not, it + # means we reset something, and we need to take the data we + # thought was part of our boundary and send it along as actual + # data. + if index == 0 and prev_index > 0: + # Overwrite our previous index. + prev_index = 0 + + # Re-consider the current character, since this could be + # the start of the boundary itself. + i -= 1 + + elif state == MultipartState.END_BOUNDARY: + if index == boundary_length - 1: + if c != HYPHEN: + msg = "Did not find - at end of boundary (%d)" % (i,) + self.logger.warning(msg) + raise MultipartParseError(msg, offset=i) + index += 1 + self.callback("end") + state = MultipartState.END + + elif state == MultipartState.END: + # Silently discard any epilogue data (RFC 2046 section 5.1.1 allows a CRLF and optional + # epilogue after the closing boundary). Django and Werkzeug do the same. + i = length + break + + else: # pragma: no cover (error case) + # We got into a strange state somehow! Just stop processing. + msg = "Reached an unknown state %d at %d" % (state, i) + self.logger.warning(msg) + raise MultipartParseError(msg, offset=i) + + # Move to the next byte. + i += 1 + + # We call our callbacks with any remaining data. Note that we pass + # the 'remaining' flag, which sets the mark back to 0 instead of + # deleting it, if it's found. This is because, if the mark is found + # at this point, we assume that there's data for one of these things + # that has been parsed, but not yet emitted. And, as such, it implies + # that we haven't yet reached the end of this 'thing'. So, by setting + # the mark to 0, we cause any data callbacks that take place in future + # calls to this function to start from the beginning of that buffer. + data_callback("header_field", length, True) + data_callback("header_value", length, True) + data_callback("part_data", length - index, True) + + # Save values to locals. + self.state = state + self.index = index + self.flags = flags + self._current_header_count = current_header_count + self._current_header_size = current_header_size + + # Return our data length to indicate no errors, and that we processed + # all of it. + return length + + def finalize(self) -> None: + """Finalize this parser, which signals to that we are finished parsing. + + Note: It does not currently, but in the future, it will verify that we + are in the final state of the parser (i.e. the end of the multipart + message is well-formed), and, if not, throw an error. + """ + # TODO: verify that we're in the state MultipartState.END, otherwise throw an + # error or otherwise state that we're not finished parsing. + pass + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(boundary={self.boundary!r})" + + +class FormParser: + """This class is the all-in-one form parser. Given all the information + necessary to parse a form, it will instantiate the correct parser, create + the proper :class:`Field` and :class:`File` classes to store the data that + is parsed, and call the two given callbacks with each field and file as + they become available. + + Args: + content_type: The Content-Type of the incoming request. This is used to select the appropriate parser. + on_field: The callback to call when a field has been parsed and is ready for usage. See above for parameters. + on_file: The callback to call when a file has been parsed and is ready for usage. See above for parameters. + on_end: An optional callback to call when all fields and files in a request has been parsed. Can be None. + boundary: If the request is a multipart/form-data request, this should be the boundary of the request, as given + in the Content-Type header, as a bytestring. + file_name: If the request is of type application/octet-stream, then the body of the request will not contain any + information about the uploaded file. In such cases, you can provide the file name of the uploaded file + manually. + config: Configuration to use for this FormParser. The default values are taken from the DEFAULT_CONFIG value, + and then any keys present in this dictionary will overwrite the default values. + """ + + #: This is the default configuration for our form parser. + #: Note: all file sizes should be in bytes. + DEFAULT_CONFIG: FormParserConfig = { + "MAX_BODY_SIZE": float("inf"), + "MAX_HEADER_COUNT": DEFAULT_MAX_HEADER_COUNT, + "MAX_HEADER_SIZE": DEFAULT_MAX_HEADER_SIZE, + "MAX_MEMORY_FILE_SIZE": 1 * 1024 * 1024, + "UPLOAD_DIR": None, + "UPLOAD_DELETE_TMP": True, + "UPLOAD_KEEP_FILENAME": False, + "UPLOAD_KEEP_EXTENSIONS": False, + # Error on invalid Content-Transfer-Encoding? + "UPLOAD_ERROR_ON_BAD_CTE": False, + } + + def __init__( + self, + content_type: str, + on_field: Callable[[Field], None] | None, + on_file: Callable[[File], None] | None, + on_end: Callable[[], None] | None = None, + boundary: bytes | str | None = None, + file_name: bytes | None = None, + config: dict[Any, Any] = {}, + ) -> None: + self.logger = logging.getLogger(__name__) + + # Save variables. + self.content_type = content_type + self.boundary = boundary + self.bytes_received = 0 + self.parser = None + + # Save callbacks. + self.on_field = on_field + self.on_file = on_file + self.on_end = on_end + + # Set configuration options. + self.config: FormParserConfig = self.DEFAULT_CONFIG.copy() + self.config.update(config) # type: ignore[typeddict-item] + + parser: OctetStreamParser | MultipartParser | QuerystringParser | None = None + + # Depending on the Content-Type, we instantiate the correct parser. + if content_type == "application/octet-stream": + file: File | None = None + + def on_start() -> None: + nonlocal file + file = File(file_name, None, config=self.config) + + def on_data(data: bytes, start: int, end: int) -> None: + nonlocal file + assert file is not None + file.write(data[start:end]) + + def _on_end() -> None: + nonlocal file + assert file is not None + # Finalize the file itself. + file.finalize() + + # Call our callback. + if on_file: + on_file(file) + + # Call the on-end callback. + if self.on_end is not None: + self.on_end() + + # Instantiate an octet-stream parser + parser = OctetStreamParser( + callbacks={"on_start": on_start, "on_data": on_data, "on_end": _on_end}, + max_size=self.config["MAX_BODY_SIZE"], + ) + + elif content_type == "application/x-www-form-urlencoded" or content_type == "application/x-url-encoded": + name_buffer: list[bytes] = [] + + f: Field | None = None + + def on_field_start() -> None: + pass + + def on_field_name(data: bytes, start: int, end: int) -> None: + name_buffer.append(data[start:end]) + + def on_field_data(data: bytes, start: int, end: int) -> None: + nonlocal f + if f is None: + f = Field(b"".join(name_buffer)) + del name_buffer[:] + f.write(data[start:end]) + + def on_field_end() -> None: + nonlocal f + # Finalize and call callback. + if f is None: + # If we get here, it's because there was no field data. + # We create a field, set it to None, and then continue. + f = Field(b"".join(name_buffer)) + del name_buffer[:] + f.set_none() + + f.finalize() + if on_field: + on_field(f) + f = None + + def _on_end() -> None: + if self.on_end is not None: + self.on_end() + + # Instantiate parser. + parser = QuerystringParser( + callbacks={ + "on_field_start": on_field_start, + "on_field_name": on_field_name, + "on_field_data": on_field_data, + "on_field_end": on_field_end, + "on_end": _on_end, + }, + max_size=self.config["MAX_BODY_SIZE"], + ) + + elif content_type == "multipart/form-data": + if boundary is None: + self.logger.error("No boundary given") + raise FormParserError("No boundary given") + + header_name: list[bytes] = [] + header_value: list[bytes] = [] + headers: dict[bytes, bytes] = {} + + f_multi: File | Field | None = None + writer: File | Field | Base64Decoder | QuotedPrintableDecoder | None = None + is_file = False + + def on_part_begin() -> None: + # Reset headers in case this isn't the first part. + nonlocal headers + headers = {} + + def on_part_data(data: bytes, start: int, end: int) -> None: + nonlocal writer + assert writer is not None + writer.write(data[start:end]) + # TODO: check for error here. + + def on_part_end() -> None: + nonlocal f_multi, is_file + assert f_multi is not None + f_multi.finalize() + if is_file: + if on_file: + assert isinstance(f_multi, File) + on_file(f_multi) + else: + if on_field: + assert isinstance(f_multi, Field) + on_field(f_multi) + + def on_header_field(data: bytes, start: int, end: int) -> None: + header_name.append(data[start:end]) + + def on_header_value(data: bytes, start: int, end: int) -> None: + header_value.append(data[start:end]) + + def on_header_end() -> None: + headers[b"".join(header_name).lower()] = b"".join(header_value) + del header_name[:] + del header_value[:] + + def on_headers_finished() -> None: + nonlocal is_file, f_multi, writer + # Reset the 'is file' flag. + is_file = False + + # Parse the content-disposition header. + content_disp = headers.get(b"content-disposition") + disp, options = parse_options_header(content_disp) + + # Get the field and filename. + field_name = options.get(b"name") + file_name = options.get(b"filename") + # RFC 7578 §4.2: each part MUST have a Content-Disposition header with a "name" parameter. + if field_name is None: + raise FormParserError(f'Field name not found in Content-Disposition: "{content_disp!r}"') + + # Create the proper class. + content_type_b = headers.get(b"content-type") + content_type = content_type_b.decode("latin-1") if content_type_b is not None else None + if file_name is None: + f_multi = Field(field_name, content_type=content_type) + else: + f_multi = File(file_name, field_name, config=self.config, content_type=content_type) + is_file = True + + # Parse the given Content-Transfer-Encoding to determine what + # we need to do with the incoming data. + # TODO: check that we properly handle 8bit / 7bit encoding. + # RFC 2045 section 6.1: Content-Transfer-Encoding values are case-insensitive. + # https://www.rfc-editor.org/rfc/rfc2045#section-6.1 + transfer_encoding = headers.get(b"content-transfer-encoding", b"7bit").lower() + + if transfer_encoding in (b"binary", b"8bit", b"7bit"): + writer = f_multi + + elif transfer_encoding == b"base64": + writer = Base64Decoder(f_multi) + + elif transfer_encoding == b"quoted-printable": + writer = QuotedPrintableDecoder(f_multi) + + else: + self.logger.warning("Unknown Content-Transfer-Encoding: %r", transfer_encoding) + if self.config["UPLOAD_ERROR_ON_BAD_CTE"]: + raise FormParserError(f'Unknown Content-Transfer-Encoding "{transfer_encoding!r}"') + else: + # If we aren't erroring, then we just treat this as an + # unencoded Content-Transfer-Encoding. + writer = f_multi + + def _on_end() -> None: + nonlocal writer + if writer is not None: + writer.finalize() + if self.on_end is not None: + self.on_end() + + # Instantiate a multipart parser. + parser = MultipartParser( + boundary, + callbacks={ + "on_part_begin": on_part_begin, + "on_part_data": on_part_data, + "on_part_end": on_part_end, + "on_header_field": on_header_field, + "on_header_value": on_header_value, + "on_header_end": on_header_end, + "on_headers_finished": on_headers_finished, + "on_end": _on_end, + }, + max_size=self.config["MAX_BODY_SIZE"], + max_header_count=self.config["MAX_HEADER_COUNT"], + max_header_size=self.config["MAX_HEADER_SIZE"], + ) + + else: + self.logger.warning("Unknown Content-Type: %r", content_type) + raise FormParserError(f"Unknown Content-Type: {content_type}") + + self.parser = parser + + def write(self, data: bytes) -> int: + """Write some data. The parser will forward this to the appropriate + underlying parser. + + Args: + data: The data to write. + + Returns: + The number of bytes processed. + """ + self.bytes_received += len(data) + # TODO: check the parser's return value for errors? + assert self.parser is not None + return self.parser.write(data) + + def finalize(self) -> None: + """Finalize the parser.""" + if self.parser is not None and hasattr(self.parser, "finalize"): + self.parser.finalize() + + def close(self) -> None: + """Close the parser.""" + if self.parser is not None and hasattr(self.parser, "close"): + self.parser.close() + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(content_type={self.content_type!r}, parser={self.parser!r})" + + +def create_form_parser( + headers: dict[str, bytes], + on_field: Callable[[Field], None] | None, + on_file: Callable[[File], None] | None, + config: dict[Any, Any] = {}, +) -> FormParser: + """This function is a helper function to aid in creating a FormParser + instances. Given a dictionary-like headers object, it will determine + the correct information needed, instantiate a FormParser with the + appropriate values and given callbacks, and then return the corresponding + parser. + + Args: + headers: A dictionary-like object of HTTP headers. The only required header is Content-Type. + on_field: Callback to call with each parsed field. + on_file: Callback to call with each parsed file. + config: Configuration variables to pass to the FormParser. + """ + content_type: str | bytes | None = headers.get("Content-Type") + if content_type is None: + logging.getLogger(__name__).warning("No Content-Type header given") + raise ValueError("No Content-Type header given!") + + # Boundaries are optional (the FormParser will raise if one is needed + # but not given). + content_type, params = parse_options_header(content_type) + boundary = params.get(b"boundary") + + # We need content_type to be a string, not a bytes object. + content_type = content_type.decode("latin-1") + + # Instantiate a form parser. + form_parser = FormParser(content_type, on_field, on_file, boundary=boundary, config=config) + + # Return our parser. + return form_parser + + +def parse_form( + headers: dict[str, bytes], + input_stream: SupportsRead, + on_field: Callable[[Field], None] | None, + on_file: Callable[[File], None] | None, + chunk_size: int = 1048576, +) -> None: + """This function is useful if you just want to parse a request body, + without too much work. Pass it a dictionary-like object of the request's + headers, and a file-like object for the input stream, along with two + callbacks that will get called whenever a field or file is parsed. + + Args: + headers: A dictionary-like object of HTTP headers. The only required header is Content-Type. + input_stream: A file-like object that represents the request body. The read() method must return bytestrings. + on_field: Callback to call with each parsed field. + on_file: Callback to call with each parsed file. + chunk_size: The maximum size to read from the input stream and write to the parser at one time. + Defaults to 1 MiB. + """ + if chunk_size < 1: + raise ValueError(f"chunk_size must be a positive number, not {chunk_size!r}") + + # Create our form parser. + parser = create_form_parser(headers, on_field, on_file) + + # Read chunks of 1MiB and write to the parser, but never read more than + # the given Content-Length, if any. + content_length: int | float | bytes | None = headers.get("Content-Length") + if content_length is not None: + content_length = int(content_length) + if content_length < 0: + raise ValueError("Content-Length must be non-negative") + else: + content_length = float("inf") + bytes_read = 0 + + while True: + # Read only up to the Content-Length given. + max_readable = int(min(content_length - bytes_read, chunk_size)) + buff = input_stream.read(max_readable) + + # Write to the parser and update our length. + parser.write(buff) + bytes_read += len(buff) + + # If we get a buffer that's smaller than the size requested, or if we + # have read up to our content length, we're done. + if len(buff) != max_readable or bytes_read == content_length: + break + + # Tell our parser that we're done writing data. + parser.finalize() diff --git a/source/target-vendors/cp312/python_multipart/py.typed b/source/target-vendors/cp312/python_multipart/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/referencing-0.37.0.dist-info/METADATA b/source/target-vendors/cp312/referencing-0.37.0.dist-info/METADATA new file mode 100644 index 0000000..9a6abbc --- /dev/null +++ b/source/target-vendors/cp312/referencing-0.37.0.dist-info/METADATA @@ -0,0 +1,64 @@ +Metadata-Version: 2.4 +Name: referencing +Version: 0.37.0 +Summary: JSON Referencing + Python +Project-URL: Documentation, https://referencing.readthedocs.io/ +Project-URL: Homepage, https://github.com/python-jsonschema/referencing +Project-URL: Issues, https://github.com/python-jsonschema/referencing/issues/ +Project-URL: Funding, https://github.com/sponsors/Julian +Project-URL: Tidelift, https://tidelift.com/subscription/pkg/pypi-referencing?utm_source=pypi-referencing&utm_medium=referral&utm_campaign=pypi-link +Project-URL: Changelog, https://referencing.readthedocs.io/en/stable/changes/ +Project-URL: Source, https://github.com/python-jsonschema/referencing +Author-email: Julian Berman +License-Expression: MIT +License-File: COPYING +Keywords: asyncapi,json,jsonschema,openapi,referencing +Classifier: Development Status :: 3 - Alpha +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: File Formats :: JSON +Classifier: Topic :: File Formats :: JSON :: JSON Schema +Requires-Python: >=3.10 +Requires-Dist: attrs>=22.2.0 +Requires-Dist: rpds-py>=0.7.0 +Requires-Dist: typing-extensions>=4.4.0; python_version < '3.13' +Description-Content-Type: text/x-rst + +=============== +``referencing`` +=============== + +|PyPI| |Pythons| |CI| |ReadTheDocs| |pre-commit| + +.. |PyPI| image:: https://img.shields.io/pypi/v/referencing.svg + :alt: PyPI version + :target: https://pypi.org/project/referencing/ + +.. |Pythons| image:: https://img.shields.io/pypi/pyversions/referencing.svg + :alt: Supported Python versions + :target: https://pypi.org/project/referencing/ + +.. |CI| image:: https://github.com/python-jsonschema/referencing/workflows/CI/badge.svg + :alt: Build status + :target: https://github.com/python-jsonschema/referencing/actions?query=workflow%3ACI + +.. |ReadTheDocs| image:: https://readthedocs.org/projects/referencing/badge/?version=stable&style=flat + :alt: ReadTheDocs status + :target: https://referencing.readthedocs.io/en/stable/ + +.. |pre-commit| image:: https://results.pre-commit.ci/badge/github/python-jsonschema/referencing/main.svg + :alt: pre-commit.ci status + :target: https://results.pre-commit.ci/latest/github/python-jsonschema/referencing/main + + +An implementation-agnostic implementation of JSON reference resolution. + +See `the documentation `_ for more details. diff --git a/source/target-vendors/cp312/referencing-0.37.0.dist-info/RECORD b/source/target-vendors/cp312/referencing-0.37.0.dist-info/RECORD new file mode 100644 index 0000000..bb50514 --- /dev/null +++ b/source/target-vendors/cp312/referencing-0.37.0.dist-info/RECORD @@ -0,0 +1,19 @@ +referencing/__init__.py,sha256=5IZKXaAH_FWyCJRkaTn1XcptLfg9cveLb9u5nYUxJKs,207 +referencing/_attrs.py,sha256=bgT-KMhDVLeGtWxM_SGKYeLaZBFzT2kUVFdAkOcXi8g,791 +referencing/_attrs.pyi,sha256=g2wX-aLEqQAvWU-s0qIN2OMGAqjOA0R72R_uSffO8_M,573 +referencing/_core.py,sha256=2SPGfaTvzA3sRqbj_4R2wC--fiXolCUVpLFLJJ6zdZ8,24732 +referencing/exceptions.py,sha256=zFgaEg6WiKeT58MQuKNsgGDnHszp26c4oReC6sF9gHM,4176 +referencing/jsonschema.py,sha256=jXZ6t6x9oTvTHMIC0tRyaFjW5r2qeHObYuu7UhSfK3Q,18615 +referencing/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +referencing/retrieval.py,sha256=cn9EeAdaaTbnMDdC4JRIhiEpXDjflRdHCOLFrw1ZhLA,2729 +referencing/typing.py,sha256=WjUbnZ6jPAd31cnCFAaeWIVENzyHtHdJyOlelv1GY70,1445 +referencing/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +referencing/tests/test_core.py,sha256=eap0CAaI23vjMIbVyEj92qLddp3iHH3AxC55CKUN4LU,37854 +referencing/tests/test_exceptions.py,sha256=7eOdHyobXMt7-h5AnnH7u8iw2uHPaH7U4Bs9JhLgjWo,934 +referencing/tests/test_jsonschema.py,sha256=4QnjUWOAMAn5yeA8ZtldJkhI54vwKWJWB0LDzNdx5xc,11687 +referencing/tests/test_referencing_suite.py,sha256=wD6veMfLsUu0s4MLjm7pS8cg4cIfL7FMBENngk73zCI,2335 +referencing/tests/test_retrieval.py,sha256=vcbnfA4TqVeqUzW073wO-nLeqVIv0rQZWNWv0z9km48,3719 +referencing-0.37.0.dist-info/METADATA,sha256=yTsA5qsVJP1ghIN2YEwYmhFDdHdkdcJaMdV75Hsp3mo,2845 +referencing-0.37.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87 +referencing-0.37.0.dist-info/licenses/COPYING,sha256=QtzWNJX4e063x3V6-jebtVpT-Ur9el9lfZrfVyNuUVw,1057 +referencing-0.37.0.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/referencing-0.37.0.dist-info/WHEEL b/source/target-vendors/cp312/referencing-0.37.0.dist-info/WHEEL new file mode 100644 index 0000000..12228d4 --- /dev/null +++ b/source/target-vendors/cp312/referencing-0.37.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.27.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/source/target-vendors/cp312/referencing-0.37.0.dist-info/licenses/COPYING b/source/target-vendors/cp312/referencing-0.37.0.dist-info/licenses/COPYING new file mode 100644 index 0000000..a9f853e --- /dev/null +++ b/source/target-vendors/cp312/referencing-0.37.0.dist-info/licenses/COPYING @@ -0,0 +1,19 @@ +Copyright (c) 2022 Julian Berman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/source/target-vendors/cp312/referencing/__init__.py b/source/target-vendors/cp312/referencing/__init__.py new file mode 100644 index 0000000..e09207d --- /dev/null +++ b/source/target-vendors/cp312/referencing/__init__.py @@ -0,0 +1,7 @@ +""" +Cross-specification, implementation-agnostic JSON referencing. +""" + +from referencing._core import Anchor, Registry, Resource, Specification + +__all__ = ["Anchor", "Registry", "Resource", "Specification"] diff --git a/source/target-vendors/cp312/referencing/_attrs.py b/source/target-vendors/cp312/referencing/_attrs.py new file mode 100644 index 0000000..ae85b86 --- /dev/null +++ b/source/target-vendors/cp312/referencing/_attrs.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from typing import NoReturn, TypeVar + +from attrs import define as _define, frozen as _frozen + +_T = TypeVar("_T") + + +def define(cls: type[_T]) -> type[_T]: # pragma: no cover + cls.__init_subclass__ = _do_not_subclass + return _define(cls) + + +def frozen(cls: type[_T]) -> type[_T]: + cls.__init_subclass__ = _do_not_subclass + return _frozen(cls) + + +class UnsupportedSubclassing(Exception): + def __str__(self): + return ( + "Subclassing is not part of referencing's public API. " + "If no other suitable API exists for what you're trying to do, " + "feel free to file an issue asking for one." + ) + + +@staticmethod +def _do_not_subclass() -> NoReturn: # pragma: no cover + raise UnsupportedSubclassing() diff --git a/source/target-vendors/cp312/referencing/_attrs.pyi b/source/target-vendors/cp312/referencing/_attrs.pyi new file mode 100644 index 0000000..eca6db2 --- /dev/null +++ b/source/target-vendors/cp312/referencing/_attrs.pyi @@ -0,0 +1,21 @@ +from collections.abc import Callable +from typing import Any, TypeVar + +from attr import attrib, field + +class UnsupportedSubclassing(Exception): ... + +_T = TypeVar("_T") + +def __dataclass_transform__( + *, + frozen_default: bool = False, + field_descriptors: tuple[type | Callable[..., Any], ...] = ..., +) -> Callable[[_T], _T]: ... +@__dataclass_transform__(field_descriptors=(attrib, field)) +def define(cls: type[_T]) -> type[_T]: ... +@__dataclass_transform__( + frozen_default=True, + field_descriptors=(attrib, field), +) +def frozen(cls: type[_T]) -> type[_T]: ... diff --git a/source/target-vendors/cp312/referencing/_core.py b/source/target-vendors/cp312/referencing/_core.py new file mode 100644 index 0000000..4206845 --- /dev/null +++ b/source/target-vendors/cp312/referencing/_core.py @@ -0,0 +1,739 @@ +from __future__ import annotations + +from collections.abc import Callable, Iterable, Iterator, Sequence +from enum import Enum +from typing import Any, ClassVar, Generic, Protocol +from urllib.parse import unquote, urldefrag, urljoin + +from attrs import evolve, field +from rpds import HashTrieMap, HashTrieSet, List + +try: + from typing_extensions import TypeVar +except ImportError: # pragma: no cover + from typing import TypeVar + +from referencing import exceptions +from referencing._attrs import frozen +from referencing.typing import URI, Anchor as AnchorType, D, Mapping, Retrieve + +EMPTY_UNCRAWLED: HashTrieSet[URI] = HashTrieSet() +EMPTY_PREVIOUS_RESOLVERS: List[URI] = List() + + +class _Unset(Enum): + """ + What sillyness... + """ + + SENTINEL = 1 + + +_UNSET = _Unset.SENTINEL + + +class _MaybeInSubresource(Protocol[D]): + def __call__( + self, + segments: Sequence[int | str], + resolver: Resolver[D], + subresource: Resource[D], + ) -> Resolver[D]: ... + + +def _detect_or_error(contents: D) -> Specification[D]: + if not isinstance(contents, Mapping): + raise exceptions.CannotDetermineSpecification(contents) + + jsonschema_dialect_id = contents.get("$schema") # type: ignore[reportUnknownMemberType] + if not isinstance(jsonschema_dialect_id, str): + raise exceptions.CannotDetermineSpecification(contents) + + from referencing.jsonschema import specification_with + + return specification_with(jsonschema_dialect_id) + + +def _detect_or_default( + default: Specification[D], +) -> Callable[[D], Specification[D]]: + def _detect(contents: D) -> Specification[D]: + if not isinstance(contents, Mapping): + return default + + jsonschema_dialect_id = contents.get("$schema") # type: ignore[reportUnknownMemberType] + if jsonschema_dialect_id is None: + return default + + from referencing.jsonschema import specification_with + + return specification_with( + jsonschema_dialect_id, # type: ignore[reportUnknownArgumentType] + default=default, + ) + + return _detect + + +class _SpecificationDetector: + def __get__( + self, + instance: Specification[D] | None, + cls: type[Specification[D]], + ) -> Callable[[D], Specification[D]]: + if instance is None: + return _detect_or_error + else: + return _detect_or_default(instance) + + +@frozen +class Specification(Generic[D]): + """ + A specification which defines referencing behavior. + + The various methods of a `Specification` allow for varying referencing + behavior across JSON Schema specification versions, etc. + """ + + #: A short human-readable name for the specification, used for debugging. + name: str + + #: Find the ID of a given document. + id_of: Callable[[D], URI | None] + + #: Retrieve the subresources of the given document (without traversing into + #: the subresources themselves). + subresources_of: Callable[[D], Iterable[D]] + + #: While resolving a JSON pointer, conditionally enter a subresource + #: (if e.g. we have just entered a keyword whose value is a subresource) + maybe_in_subresource: _MaybeInSubresource[D] + + #: Retrieve the anchors contained in the given document. + _anchors_in: Callable[ + [Specification[D], D], + Iterable[AnchorType[D]], + ] = field(alias="anchors_in") + + #: An opaque specification where resources have no subresources + #: nor internal identifiers. + OPAQUE: ClassVar[Specification[Any]] + + #: Attempt to discern which specification applies to the given contents. + #: + #: May be called either as an instance method or as a class method, with + #: slightly different behavior in the following case: + #: + #: Recall that not all contents contains enough internal information about + #: which specification it is written for -- the JSON Schema ``{}``, + #: for instance, is valid under many different dialects and may be + #: interpreted as any one of them. + #: + #: When this method is used as an instance method (i.e. called on a + #: specific specification), that specification is used as the default + #: if the given contents are unidentifiable. + #: + #: On the other hand when called as a class method, an error is raised. + #: + #: To reiterate, ``DRAFT202012.detect({})`` will return ``DRAFT202012`` + #: whereas the class method ``Specification.detect({})`` will raise an + #: error. + #: + #: (Note that of course ``DRAFT202012.detect(...)`` may return some other + #: specification when given a schema which *does* identify as being for + #: another version). + #: + #: Raises: + #: + #: `CannotDetermineSpecification` + #: + #: if the given contents don't have any discernible + #: information which could be used to guess which + #: specification they identify as + detect = _SpecificationDetector() + + def __repr__(self) -> str: + return f"" + + def anchors_in(self, contents: D): + """ + Retrieve the anchors contained in the given document. + """ + return self._anchors_in(self, contents) + + def create_resource(self, contents: D) -> Resource[D]: + """ + Create a resource which is interpreted using this specification. + """ + return Resource(contents=contents, specification=self) + + +Specification.OPAQUE = Specification( + name="opaque", + id_of=lambda contents: None, + subresources_of=lambda contents: [], + anchors_in=lambda specification, contents: [], + maybe_in_subresource=lambda segments, resolver, subresource: resolver, +) + + +@frozen +class Resource(Generic[D]): + r""" + A document (deserialized JSON) with a concrete interpretation under a spec. + + In other words, a Python object, along with an instance of `Specification` + which describes how the document interacts with referencing -- both + internally (how it refers to other `Resource`\ s) and externally (how it + should be identified such that it is referenceable by other documents). + """ + + contents: D + _specification: Specification[D] = field(alias="specification") + + @classmethod + def from_contents( + cls, + contents: D, + default_specification: ( + type[Specification[D]] | Specification[D] + ) = Specification, + ) -> Resource[D]: + """ + Create a resource guessing which specification applies to the contents. + + Raises: + + `CannotDetermineSpecification` + + if the given contents don't have any discernible + information which could be used to guess which + specification they identify as + + """ + specification = default_specification.detect(contents) + return specification.create_resource(contents=contents) + + @classmethod + def opaque(cls, contents: D) -> Resource[D]: + """ + Create an opaque `Resource` -- i.e. one with opaque specification. + + See `Specification.OPAQUE` for details. + """ + return Specification.OPAQUE.create_resource(contents=contents) + + def id(self) -> URI | None: + """ + Retrieve this resource's (specification-specific) identifier. + """ + id = self._specification.id_of(self.contents) + if id is None: + return + return id.rstrip("#") + + def subresources(self) -> Iterable[Resource[D]]: + """ + Retrieve this resource's subresources. + """ + return ( + Resource.from_contents( + each, + default_specification=self._specification, + ) + for each in self._specification.subresources_of(self.contents) + ) + + def anchors(self) -> Iterable[AnchorType[D]]: + """ + Retrieve this resource's (specification-specific) identifier. + """ + return self._specification.anchors_in(self.contents) + + def pointer(self, pointer: str, resolver: Resolver[D]) -> Resolved[D]: + """ + Resolve the given JSON pointer. + + Raises: + + `exceptions.PointerToNowhere` + + if the pointer points to a location not present in the document + + """ + if not pointer: + return Resolved(contents=self.contents, resolver=resolver) + + contents = self.contents + segments: list[int | str] = [] + for segment in unquote(pointer[1:]).split("/"): + if isinstance(contents, Sequence): + segment = int(segment) + else: + segment = segment.replace("~1", "/").replace("~0", "~") + try: + contents = contents[segment] # type: ignore[reportUnknownArgumentType] + except LookupError as lookup_error: + error = exceptions.PointerToNowhere(ref=pointer, resource=self) + raise error from lookup_error + + segments.append(segment) + last = resolver + resolver = self._specification.maybe_in_subresource( + segments=segments, + resolver=resolver, + subresource=self._specification.create_resource(contents), + ) + if resolver is not last: + segments = [] + return Resolved(contents=contents, resolver=resolver) # type: ignore[reportUnknownArgumentType] + + +def _fail_to_retrieve(uri: URI): + raise exceptions.NoSuchResource(ref=uri) + + +@frozen +class Registry(Mapping[URI, Resource[D]]): + r""" + A registry of `Resource`\ s, each identified by their canonical URIs. + + Registries store a collection of in-memory resources, and optionally + enable additional resources which may be stored elsewhere (e.g. in a + database, a separate set of files, over the network, etc.). + + They also lazily walk their known resources, looking for subresources + within them. In other words, subresources contained within any added + resources will be retrievable via their own IDs (though this discovery of + subresources will be delayed until necessary). + + Registries are immutable, and their methods return new instances of the + registry with the additional resources added to them. + + The ``retrieve`` argument can be used to configure retrieval of resources + dynamically, either over the network, from a database, or the like. + Pass it a callable which will be called if any URI not present in the + registry is accessed. It must either return a `Resource` or else raise a + `NoSuchResource` exception indicating that the resource does not exist + even according to the retrieval logic. + """ + + _resources: HashTrieMap[URI, Resource[D]] = field( + default=HashTrieMap(), + converter=HashTrieMap.convert, # type: ignore[reportGeneralTypeIssues] + alias="resources", + ) + _anchors: HashTrieMap[tuple[URI, str], AnchorType[D]] = HashTrieMap() + _uncrawled: HashTrieSet[URI] = EMPTY_UNCRAWLED + _retrieve: Retrieve[D] = field(default=_fail_to_retrieve, alias="retrieve") + + def __getitem__(self, uri: URI) -> Resource[D]: + """ + Return the (already crawled) `Resource` identified by the given URI. + """ + try: + return self._resources[uri.rstrip("#")] + except KeyError: + raise exceptions.NoSuchResource(ref=uri) from None + + def __iter__(self) -> Iterator[URI]: + """ + Iterate over all crawled URIs in the registry. + """ + return iter(self._resources) + + def __len__(self) -> int: + """ + Count the total number of fully crawled resources in this registry. + """ + return len(self._resources) + + def __rmatmul__( + self, + new: Resource[D] | Iterable[Resource[D]], + ) -> Registry[D]: + """ + Create a new registry with resource(s) added using their internal IDs. + + Resources must have a internal IDs (e.g. the :kw:`$id` keyword in + modern JSON Schema versions), otherwise an error will be raised. + + Both a single resource as well as an iterable of resources works, i.e.: + + * ``resource @ registry`` or + + * ``[iterable, of, multiple, resources] @ registry`` + + which -- again, assuming the resources have internal IDs -- is + equivalent to calling `Registry.with_resources` as such: + + .. code:: python + + registry.with_resources( + (resource.id(), resource) for resource in new_resources + ) + + Raises: + + `NoInternalID` + + if the resource(s) in fact do not have IDs + + """ + if isinstance(new, Resource): + new = (new,) + + resources = self._resources + uncrawled = self._uncrawled + for resource in new: + id = resource.id() + if id is None: + raise exceptions.NoInternalID(resource=resource) + uncrawled = uncrawled.insert(id) + resources = resources.insert(id, resource) + return evolve(self, resources=resources, uncrawled=uncrawled) + + def __repr__(self) -> str: + size = len(self) + pluralized = "resource" if size == 1 else "resources" + if self._uncrawled: + uncrawled = len(self._uncrawled) + if uncrawled == size: + summary = f"uncrawled {pluralized}" + else: + summary = f"{pluralized}, {uncrawled} uncrawled" + else: + summary = f"{pluralized}" + return f"" + + def get_or_retrieve(self, uri: URI) -> Retrieved[D, Resource[D]]: + """ + Get a resource from the registry, crawling or retrieving if necessary. + + May involve crawling to find the given URI if it is not already known, + so the returned object is a `Retrieved` object which contains both the + resource value as well as the registry which ultimately contained it. + """ + resource = self._resources.get(uri) + if resource is not None: + return Retrieved(registry=self, value=resource) + + registry = self.crawl() + resource = registry._resources.get(uri) + if resource is not None: + return Retrieved(registry=registry, value=resource) + + try: + resource = registry._retrieve(uri) + except ( + exceptions.CannotDetermineSpecification, + exceptions.NoSuchResource, + ): + raise + except Exception as error: + raise exceptions.Unretrievable(ref=uri) from error + else: + registry = registry.with_resource(uri, resource) + return Retrieved(registry=registry, value=resource) + + def remove(self, uri: URI): + """ + Return a registry with the resource identified by a given URI removed. + """ + if uri not in self._resources: + raise exceptions.NoSuchResource(ref=uri) + + return evolve( + self, + resources=self._resources.remove(uri), + uncrawled=self._uncrawled.discard(uri), + anchors=HashTrieMap( + (k, v) for k, v in self._anchors.items() if k[0] != uri + ), + ) + + def anchor(self, uri: URI, name: str): + """ + Retrieve a given anchor from a resource which must already be crawled. + """ + value = self._anchors.get((uri, name)) + if value is not None: + return Retrieved(value=value, registry=self) + + registry = self.crawl() + value = registry._anchors.get((uri, name)) + if value is not None: + return Retrieved(value=value, registry=registry) + + resource = self[uri] + canonical_uri = resource.id() + if canonical_uri is not None: + value = registry._anchors.get((canonical_uri, name)) + if value is not None: + return Retrieved(value=value, registry=registry) + + if "/" in name: + raise exceptions.InvalidAnchor( + ref=uri, + resource=resource, + anchor=name, + ) + raise exceptions.NoSuchAnchor(ref=uri, resource=resource, anchor=name) + + def contents(self, uri: URI) -> D: + """ + Retrieve the (already crawled) contents identified by the given URI. + """ + return self[uri].contents + + def crawl(self) -> Registry[D]: + """ + Crawl all added resources, discovering subresources. + """ + resources = self._resources + anchors = self._anchors + uncrawled = [(uri, resources[uri]) for uri in self._uncrawled] + while uncrawled: + uri, resource = uncrawled.pop() + + id = resource.id() + if id is not None: + uri = urljoin(uri, id) + resources = resources.insert(uri, resource) + for each in resource.anchors(): + anchors = anchors.insert((uri, each.name), each) + uncrawled.extend((uri, each) for each in resource.subresources()) + return evolve( + self, + resources=resources, + anchors=anchors, + uncrawled=EMPTY_UNCRAWLED, + ) + + def with_resource(self, uri: URI, resource: Resource[D]): + """ + Add the given `Resource` to the registry, without crawling it. + """ + return self.with_resources([(uri, resource)]) + + def with_resources( + self, + pairs: Iterable[tuple[URI, Resource[D]]], + ) -> Registry[D]: + r""" + Add the given `Resource`\ s to the registry, without crawling them. + """ + resources = self._resources + uncrawled = self._uncrawled + for uri, resource in pairs: + # Empty fragment URIs are equivalent to URIs without the fragment. + # TODO: Is this true for non JSON Schema resources? Probably not. + uri = uri.rstrip("#") + uncrawled = uncrawled.insert(uri) + resources = resources.insert(uri, resource) + return evolve(self, resources=resources, uncrawled=uncrawled) + + def with_contents( + self, + pairs: Iterable[tuple[URI, D]], + **kwargs: Any, + ) -> Registry[D]: + r""" + Add the given contents to the registry, autodetecting when necessary. + """ + return self.with_resources( + (uri, Resource.from_contents(each, **kwargs)) + for uri, each in pairs + ) + + def combine(self, *registries: Registry[D]) -> Registry[D]: + """ + Combine together one or more other registries, producing a unified one. + """ + if registries == (self,): + return self + resources = self._resources + anchors = self._anchors + uncrawled = self._uncrawled + retrieve = self._retrieve + for registry in registries: + resources = resources.update(registry._resources) + anchors = anchors.update(registry._anchors) + uncrawled = uncrawled.update(registry._uncrawled) + + if registry._retrieve is not _fail_to_retrieve: + if registry._retrieve is not retrieve is not _fail_to_retrieve: + raise ValueError( # noqa: TRY003 + "Cannot combine registries with conflicting retrieval " + "functions.", + ) + retrieve = registry._retrieve + return evolve( + self, + anchors=anchors, + resources=resources, + uncrawled=uncrawled, + retrieve=retrieve, + ) + + def resolver(self, base_uri: URI = "") -> Resolver[D]: + """ + Return a `Resolver` which resolves references against this registry. + """ + return Resolver(base_uri=base_uri, registry=self) + + def resolver_with_root(self, resource: Resource[D]) -> Resolver[D]: + """ + Return a `Resolver` with a specific root resource. + """ + uri = resource.id() or "" + return Resolver( + base_uri=uri, + registry=self.with_resource(uri, resource), + ) + + +#: An anchor or resource. +AnchorOrResource = TypeVar( + "AnchorOrResource", + AnchorType[Any], + Resource[Any], + default=Resource[Any], +) + + +@frozen +class Retrieved(Generic[D, AnchorOrResource]): + """ + A value retrieved from a `Registry`. + """ + + value: AnchorOrResource + registry: Registry[D] + + +@frozen +class Resolved(Generic[D]): + """ + A reference resolved to its contents by a `Resolver`. + """ + + contents: D + resolver: Resolver[D] + + +@frozen +class Resolver(Generic[D]): + """ + A reference resolver. + + Resolvers help resolve references (including relative ones) by + pairing a fixed base URI with a `Registry`. + + This object, under normal circumstances, is expected to be used by + *implementers of libraries* built on top of `referencing` (e.g. JSON Schema + implementations or other libraries resolving JSON references), + not directly by end-users populating registries or while writing + schemas or other resources. + + References are resolved against the base URI, and the combined URI + is then looked up within the registry. + + The process of resolving a reference may itself involve calculating + a *new* base URI for future reference resolution (e.g. if an + intermediate resource sets a new base URI), or may involve encountering + additional subresources and adding them to a new registry. + """ + + _base_uri: URI = field(alias="base_uri") + _registry: Registry[D] = field(alias="registry") + _previous: List[URI] = field(default=List(), repr=False, alias="previous") + + def lookup(self, ref: URI) -> Resolved[D]: + """ + Resolve the given reference to the resource it points to. + + Raises: + + `exceptions.Unresolvable` + + or a subclass thereof (see below) if the reference isn't + resolvable + + `exceptions.NoSuchAnchor` + + if the reference is to a URI where a resource exists but + contains a plain name fragment which does not exist within + the resource + + `exceptions.PointerToNowhere` + + if the reference is to a URI where a resource exists but + contains a JSON pointer to a location within the resource + that does not exist + + """ + if ref.startswith("#"): + uri, fragment = self._base_uri, ref[1:] + else: + uri, fragment = urldefrag(urljoin(self._base_uri, ref)) + try: + retrieved = self._registry.get_or_retrieve(uri) + except exceptions.NoSuchResource: + raise exceptions.Unresolvable(ref=ref) from None + except exceptions.Unretrievable as error: + raise exceptions.Unresolvable(ref=ref) from error + + if fragment.startswith("/"): + resolver = self._evolve(registry=retrieved.registry, base_uri=uri) + return retrieved.value.pointer(pointer=fragment, resolver=resolver) + + if fragment: + retrieved = retrieved.registry.anchor(uri, fragment) + resolver = self._evolve(registry=retrieved.registry, base_uri=uri) + return retrieved.value.resolve(resolver=resolver) + + resolver = self._evolve(registry=retrieved.registry, base_uri=uri) + return Resolved(contents=retrieved.value.contents, resolver=resolver) + + def in_subresource(self, subresource: Resource[D]) -> Resolver[D]: + """ + Create a resolver for a subresource (which may have a new base URI). + """ + id = subresource.id() + if id is None: + return self + return evolve(self, base_uri=urljoin(self._base_uri, id)) + + def dynamic_scope(self) -> Iterable[tuple[URI, Registry[D]]]: + """ + In specs with such a notion, return the URIs in the dynamic scope. + """ + for uri in self._previous: + yield uri, self._registry + + def _evolve(self, base_uri: URI, **kwargs: Any): + """ + Evolve, appending to the dynamic scope. + """ + previous = self._previous + if self._base_uri and (not previous or base_uri != self._base_uri): + previous = previous.push_front(self._base_uri) + return evolve(self, base_uri=base_uri, previous=previous, **kwargs) + + +@frozen +class Anchor(Generic[D]): + """ + A simple anchor in a `Resource`. + """ + + name: str + resource: Resource[D] + + def resolve(self, resolver: Resolver[D]): + """ + Return the resource for this anchor. + """ + return Resolved(contents=self.resource.contents, resolver=resolver) diff --git a/source/target-vendors/cp312/referencing/exceptions.py b/source/target-vendors/cp312/referencing/exceptions.py new file mode 100644 index 0000000..3267fc7 --- /dev/null +++ b/source/target-vendors/cp312/referencing/exceptions.py @@ -0,0 +1,165 @@ +""" +Errors, oh no! +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import attrs + +from referencing._attrs import frozen + +if TYPE_CHECKING: + from referencing import Resource + from referencing.typing import URI + + +@frozen +class NoSuchResource(KeyError): + """ + The given URI is not present in a registry. + + Unlike most exceptions, this class *is* intended to be publicly + instantiable and *is* part of the public API of the package. + """ + + ref: URI + + def __eq__(self, other: object) -> bool: + if self.__class__ is not other.__class__: + return NotImplemented + return attrs.astuple(self) == attrs.astuple(other) + + def __hash__(self) -> int: + return hash(attrs.astuple(self)) + + +@frozen +class NoInternalID(Exception): + """ + A resource has no internal ID, but one is needed. + + E.g. in modern JSON Schema drafts, this is the :kw:`$id` keyword. + + One might be needed if a resource was to-be added to a registry but no + other URI is available, and the resource doesn't declare its canonical URI. + """ + + resource: Resource[Any] + + def __eq__(self, other: object) -> bool: + if self.__class__ is not other.__class__: + return NotImplemented + return attrs.astuple(self) == attrs.astuple(other) + + def __hash__(self) -> int: + return hash(attrs.astuple(self)) + + +@frozen +class Unretrievable(KeyError): + """ + The given URI is not present in a registry, and retrieving it failed. + """ + + ref: URI + + def __eq__(self, other: object) -> bool: + if self.__class__ is not other.__class__: + return NotImplemented + return attrs.astuple(self) == attrs.astuple(other) + + def __hash__(self) -> int: + return hash(attrs.astuple(self)) + + +@frozen +class CannotDetermineSpecification(Exception): + """ + Attempting to detect the appropriate `Specification` failed. + + This happens if no discernible information is found in the contents of the + new resource which would help identify it. + """ + + contents: Any + + def __eq__(self, other: object) -> bool: + if self.__class__ is not other.__class__: + return NotImplemented + return attrs.astuple(self) == attrs.astuple(other) + + def __hash__(self) -> int: + return hash(attrs.astuple(self)) + + +@attrs.frozen # Because here we allow subclassing below. +class Unresolvable(Exception): + """ + A reference was unresolvable. + """ + + ref: URI + + def __eq__(self, other: object) -> bool: + if self.__class__ is not other.__class__: + return NotImplemented + return attrs.astuple(self) == attrs.astuple(other) + + def __hash__(self) -> int: + return hash(attrs.astuple(self)) + + +@frozen +class PointerToNowhere(Unresolvable): + """ + A JSON Pointer leads to a part of a document that does not exist. + """ + + resource: Resource[Any] + + def __str__(self) -> str: + msg = f"{self.ref!r} does not exist within {self.resource.contents!r}" + if self.ref == "/": + msg += ( + ". The pointer '/' is a valid JSON Pointer but it points to " + "an empty string property ''. If you intended to point " + "to the entire resource, you should use '#'." + ) + return msg + + +@frozen +class NoSuchAnchor(Unresolvable): + """ + An anchor does not exist within a particular resource. + """ + + resource: Resource[Any] + anchor: str + + def __str__(self) -> str: + return ( + f"{self.anchor!r} does not exist within {self.resource.contents!r}" + ) + + +@frozen +class InvalidAnchor(Unresolvable): + """ + An anchor which could never exist in a resource was dereferenced. + + It is somehow syntactically invalid. + """ + + resource: Resource[Any] + anchor: str + + def __str__(self) -> str: + return ( + f"'#{self.anchor}' is not a valid anchor, neither as a " + "plain name anchor nor as a JSON Pointer. You may have intended " + f"to use '#/{self.anchor}', as the slash is required *before each " + "segment* of a JSON pointer." + ) diff --git a/source/target-vendors/cp312/referencing/jsonschema.py b/source/target-vendors/cp312/referencing/jsonschema.py new file mode 100644 index 0000000..93e77a7 --- /dev/null +++ b/source/target-vendors/cp312/referencing/jsonschema.py @@ -0,0 +1,642 @@ +""" +Referencing implementations for JSON Schema specs (historic & current). +""" + +from __future__ import annotations + +from collections.abc import Iterable, Sequence, Set +from typing import Any + +from referencing import Anchor, Registry, Resource, Specification, exceptions +from referencing._attrs import frozen +from referencing._core import ( + _UNSET, # type: ignore[reportPrivateUsage] + Resolved as _Resolved, + Resolver as _Resolver, + _Unset, # type: ignore[reportPrivateUsage] +) +from referencing.typing import URI, Anchor as AnchorType, Mapping + +#: A JSON Schema which is a JSON object +ObjectSchema = Mapping[str, Any] + +#: A JSON Schema of any kind +Schema = bool | ObjectSchema + +#: A Resource whose contents are JSON Schemas +SchemaResource = Resource[Schema] + +#: A JSON Schema Registry +SchemaRegistry = Registry[Schema] + +#: The empty JSON Schema Registry +EMPTY_REGISTRY: SchemaRegistry = Registry() + + +@frozen +class UnknownDialect(Exception): + """ + A dialect identifier was found for a dialect unknown by this library. + + If it's a custom ("unofficial") dialect, be sure you've registered it. + """ + + uri: URI + + +def _dollar_id(contents: Schema) -> URI | None: + if isinstance(contents, bool): + return + return contents.get("$id") + + +def _legacy_dollar_id(contents: Schema) -> URI | None: + if isinstance(contents, bool) or "$ref" in contents: + return + id = contents.get("$id") + if id is not None and not id.startswith("#"): + return id + + +def _legacy_id(contents: ObjectSchema) -> URI | None: + if "$ref" in contents: + return + id = contents.get("id") + if id is not None and not id.startswith("#"): + return id + + +def _anchor( + specification: Specification[Schema], + contents: Schema, +) -> Iterable[AnchorType[Schema]]: + if isinstance(contents, bool): + return + anchor = contents.get("$anchor") + if anchor is not None: + yield Anchor( + name=anchor, + resource=specification.create_resource(contents), + ) + + dynamic_anchor = contents.get("$dynamicAnchor") + if dynamic_anchor is not None: + yield DynamicAnchor( + name=dynamic_anchor, + resource=specification.create_resource(contents), + ) + + +def _anchor_2019( + specification: Specification[Schema], + contents: Schema, +) -> Iterable[Anchor[Schema]]: + if isinstance(contents, bool): + return [] + anchor = contents.get("$anchor") + if anchor is None: + return [] + return [ + Anchor( + name=anchor, + resource=specification.create_resource(contents), + ), + ] + + +def _legacy_anchor_in_dollar_id( + specification: Specification[Schema], + contents: Schema, +) -> Iterable[Anchor[Schema]]: + if isinstance(contents, bool): + return [] + id = contents.get("$id", "") + if not id.startswith("#"): + return [] + return [ + Anchor( + name=id[1:], + resource=specification.create_resource(contents), + ), + ] + + +def _legacy_anchor_in_id( + specification: Specification[ObjectSchema], + contents: ObjectSchema, +) -> Iterable[Anchor[ObjectSchema]]: + id = contents.get("id", "") + if not id.startswith("#"): + return [] + return [ + Anchor( + name=id[1:], + resource=specification.create_resource(contents), + ), + ] + + +def _subresources_of( + in_value: Set[str] = frozenset(), + in_subvalues: Set[str] = frozenset(), + in_subarray: Set[str] = frozenset(), +): + """ + Create a callable returning JSON Schema specification-style subschemas. + + Relies on specifying the set of keywords containing subschemas in their + values, in a subobject's values, or in a subarray. + """ + + def subresources_of(contents: Schema) -> Iterable[ObjectSchema]: + if isinstance(contents, bool): + return + for each in in_value: + if each in contents: + yield contents[each] + for each in in_subarray: + if each in contents: + yield from contents[each] + for each in in_subvalues: + if each in contents: + yield from contents[each].values() + + return subresources_of + + +def _subresources_of_with_crazy_items( + in_value: Set[str] = frozenset(), + in_subvalues: Set[str] = frozenset(), + in_subarray: Set[str] = frozenset(), +): + """ + Specifically handle older drafts where there are some funky keywords. + """ + + def subresources_of(contents: Schema) -> Iterable[ObjectSchema]: + if isinstance(contents, bool): + return + for each in in_value: + if each in contents: + yield contents[each] + for each in in_subarray: + if each in contents: + yield from contents[each] + for each in in_subvalues: + if each in contents: + yield from contents[each].values() + + items = contents.get("items") + if items is not None: + if isinstance(items, Sequence): + yield from items + else: + yield items + + return subresources_of + + +def _subresources_of_with_crazy_items_dependencies( + in_value: Set[str] = frozenset(), + in_subvalues: Set[str] = frozenset(), + in_subarray: Set[str] = frozenset(), +): + """ + Specifically handle older drafts where there are some funky keywords. + """ + + def subresources_of(contents: Schema) -> Iterable[ObjectSchema]: + if isinstance(contents, bool): + return + for each in in_value: + if each in contents: + yield contents[each] + for each in in_subarray: + if each in contents: + yield from contents[each] + for each in in_subvalues: + if each in contents: + yield from contents[each].values() + + items = contents.get("items") + if items is not None: + if isinstance(items, Sequence): + yield from items + else: + yield items + dependencies = contents.get("dependencies") + if dependencies is not None: + values = iter(dependencies.values()) + value = next(values, None) + if isinstance(value, Mapping): + yield value + yield from values + + return subresources_of + + +def _subresources_of_with_crazy_aP_items_dependencies( + in_value: Set[str] = frozenset(), + in_subvalues: Set[str] = frozenset(), + in_subarray: Set[str] = frozenset(), +): + """ + Specifically handle even older drafts where there are some funky keywords. + """ + + def subresources_of(contents: ObjectSchema) -> Iterable[ObjectSchema]: + for each in in_value: + if each in contents: + yield contents[each] + for each in in_subarray: + if each in contents: + yield from contents[each] + for each in in_subvalues: + if each in contents: + yield from contents[each].values() + + items = contents.get("items") + if items is not None: + if isinstance(items, Sequence): + yield from items + else: + yield items + dependencies = contents.get("dependencies") + if dependencies is not None: + values = iter(dependencies.values()) + value = next(values, None) + if isinstance(value, Mapping): + yield value + yield from values + + for each in "additionalItems", "additionalProperties": + value = contents.get(each) + if isinstance(value, Mapping): + yield value + + return subresources_of + + +def _maybe_in_subresource( + in_value: Set[str] = frozenset(), + in_subvalues: Set[str] = frozenset(), + in_subarray: Set[str] = frozenset(), +): + in_child = in_subvalues | in_subarray + + def maybe_in_subresource( + segments: Sequence[int | str], + resolver: _Resolver[Any], + subresource: Resource[Any], + ) -> _Resolver[Any]: + _segments = iter(segments) + for segment in _segments: + if segment not in in_value and ( + segment not in in_child or next(_segments, None) is None + ): + return resolver + return resolver.in_subresource(subresource) + + return maybe_in_subresource + + +def _maybe_in_subresource_crazy_items( + in_value: Set[str] = frozenset(), + in_subvalues: Set[str] = frozenset(), + in_subarray: Set[str] = frozenset(), +): + in_child = in_subvalues | in_subarray + + def maybe_in_subresource( + segments: Sequence[int | str], + resolver: _Resolver[Any], + subresource: Resource[Any], + ) -> _Resolver[Any]: + _segments = iter(segments) + for segment in _segments: + if segment == "items" and isinstance( + subresource.contents, + Mapping, + ): + return resolver.in_subresource(subresource) + if segment not in in_value and ( + segment not in in_child or next(_segments, None) is None + ): + return resolver + return resolver.in_subresource(subresource) + + return maybe_in_subresource + + +def _maybe_in_subresource_crazy_items_dependencies( + in_value: Set[str] = frozenset(), + in_subvalues: Set[str] = frozenset(), + in_subarray: Set[str] = frozenset(), +): + in_child = in_subvalues | in_subarray + + def maybe_in_subresource( + segments: Sequence[int | str], + resolver: _Resolver[Any], + subresource: Resource[Any], + ) -> _Resolver[Any]: + _segments = iter(segments) + for segment in _segments: + if segment in {"items", "dependencies"} and isinstance( + subresource.contents, + Mapping, + ): + return resolver.in_subresource(subresource) + if segment not in in_value and ( + segment not in in_child or next(_segments, None) is None + ): + return resolver + return resolver.in_subresource(subresource) + + return maybe_in_subresource + + +#: JSON Schema draft 2020-12 +DRAFT202012 = Specification( + name="draft2020-12", + id_of=_dollar_id, + subresources_of=_subresources_of( + in_value={ + "additionalProperties", + "contains", + "contentSchema", + "else", + "if", + "items", + "not", + "propertyNames", + "then", + "unevaluatedItems", + "unevaluatedProperties", + }, + in_subarray={"allOf", "anyOf", "oneOf", "prefixItems"}, + in_subvalues={ + "$defs", + "definitions", + "dependentSchemas", + "patternProperties", + "properties", + }, + ), + anchors_in=_anchor, + maybe_in_subresource=_maybe_in_subresource( + in_value={ + "additionalProperties", + "contains", + "contentSchema", + "else", + "if", + "items", + "not", + "propertyNames", + "then", + "unevaluatedItems", + "unevaluatedProperties", + }, + in_subarray={"allOf", "anyOf", "oneOf", "prefixItems"}, + in_subvalues={ + "$defs", + "definitions", + "dependentSchemas", + "patternProperties", + "properties", + }, + ), +) +#: JSON Schema draft 2019-09 +DRAFT201909 = Specification( + name="draft2019-09", + id_of=_dollar_id, + subresources_of=_subresources_of_with_crazy_items( + in_value={ + "additionalItems", + "additionalProperties", + "contains", + "contentSchema", + "else", + "if", + "not", + "propertyNames", + "then", + "unevaluatedItems", + "unevaluatedProperties", + }, + in_subarray={"allOf", "anyOf", "oneOf"}, + in_subvalues={ + "$defs", + "definitions", + "dependentSchemas", + "patternProperties", + "properties", + }, + ), + anchors_in=_anchor_2019, + maybe_in_subresource=_maybe_in_subresource_crazy_items( + in_value={ + "additionalItems", + "additionalProperties", + "contains", + "contentSchema", + "else", + "if", + "not", + "propertyNames", + "then", + "unevaluatedItems", + "unevaluatedProperties", + }, + in_subarray={"allOf", "anyOf", "oneOf"}, + in_subvalues={ + "$defs", + "definitions", + "dependentSchemas", + "patternProperties", + "properties", + }, + ), +) +#: JSON Schema draft 7 +DRAFT7 = Specification( + name="draft-07", + id_of=_legacy_dollar_id, + subresources_of=_subresources_of_with_crazy_items_dependencies( + in_value={ + "additionalItems", + "additionalProperties", + "contains", + "else", + "if", + "not", + "propertyNames", + "then", + }, + in_subarray={"allOf", "anyOf", "oneOf"}, + in_subvalues={"definitions", "patternProperties", "properties"}, + ), + anchors_in=_legacy_anchor_in_dollar_id, + maybe_in_subresource=_maybe_in_subresource_crazy_items_dependencies( + in_value={ + "additionalItems", + "additionalProperties", + "contains", + "else", + "if", + "not", + "propertyNames", + "then", + }, + in_subarray={"allOf", "anyOf", "oneOf"}, + in_subvalues={"definitions", "patternProperties", "properties"}, + ), +) +#: JSON Schema draft 6 +DRAFT6 = Specification( + name="draft-06", + id_of=_legacy_dollar_id, + subresources_of=_subresources_of_with_crazy_items_dependencies( + in_value={ + "additionalItems", + "additionalProperties", + "contains", + "not", + "propertyNames", + }, + in_subarray={"allOf", "anyOf", "oneOf"}, + in_subvalues={"definitions", "patternProperties", "properties"}, + ), + anchors_in=_legacy_anchor_in_dollar_id, + maybe_in_subresource=_maybe_in_subresource_crazy_items_dependencies( + in_value={ + "additionalItems", + "additionalProperties", + "contains", + "not", + "propertyNames", + }, + in_subarray={"allOf", "anyOf", "oneOf"}, + in_subvalues={"definitions", "patternProperties", "properties"}, + ), +) +#: JSON Schema draft 4 +DRAFT4 = Specification( + name="draft-04", + id_of=_legacy_id, + subresources_of=_subresources_of_with_crazy_aP_items_dependencies( + in_value={"not"}, + in_subarray={"allOf", "anyOf", "oneOf"}, + in_subvalues={"definitions", "patternProperties", "properties"}, + ), + anchors_in=_legacy_anchor_in_id, + maybe_in_subresource=_maybe_in_subresource_crazy_items_dependencies( + in_value={"additionalItems", "additionalProperties", "not"}, + in_subarray={"allOf", "anyOf", "oneOf"}, + in_subvalues={"definitions", "patternProperties", "properties"}, + ), +) +#: JSON Schema draft 3 +DRAFT3 = Specification( + name="draft-03", + id_of=_legacy_id, + subresources_of=_subresources_of_with_crazy_aP_items_dependencies( + in_subarray={"extends"}, + in_subvalues={"definitions", "patternProperties", "properties"}, + ), + anchors_in=_legacy_anchor_in_id, + maybe_in_subresource=_maybe_in_subresource_crazy_items_dependencies( + in_value={"additionalItems", "additionalProperties"}, + in_subarray={"extends"}, + in_subvalues={"definitions", "patternProperties", "properties"}, + ), +) + + +_SPECIFICATIONS: Registry[Specification[Schema]] = Registry( + { + dialect_id: Resource.opaque(specification) + for dialect_id, specification in [ + ("https://json-schema.org/draft/2020-12/schema", DRAFT202012), + ("https://json-schema.org/draft/2019-09/schema", DRAFT201909), + ("http://json-schema.org/draft-07/schema", DRAFT7), + ("http://json-schema.org/draft-06/schema", DRAFT6), + ("http://json-schema.org/draft-04/schema", DRAFT4), + ("http://json-schema.org/draft-03/schema", DRAFT3), + ] + }, +) + + +def specification_with( + dialect_id: URI, + default: Specification[Any] | _Unset = _UNSET, +) -> Specification[Any]: + """ + Retrieve the `Specification` with the given dialect identifier. + + Raises: + + `UnknownDialect` + + if the given ``dialect_id`` isn't known + + """ + resource = _SPECIFICATIONS.get(dialect_id.rstrip("#")) + if resource is not None: + return resource.contents + if default is _UNSET: + raise UnknownDialect(dialect_id) + return default + + +@frozen +class DynamicAnchor: + """ + Dynamic anchors, introduced in draft 2020. + """ + + name: str + resource: SchemaResource + + def resolve(self, resolver: _Resolver[Schema]) -> _Resolved[Schema]: + """ + Resolve this anchor dynamically. + """ + last = self.resource + for uri, registry in resolver.dynamic_scope(): + try: + anchor = registry.anchor(uri, self.name).value + except exceptions.NoSuchAnchor: + continue + if isinstance(anchor, DynamicAnchor): + last = anchor.resource + return _Resolved( + contents=last.contents, + resolver=resolver.in_subresource(last), + ) + + +def lookup_recursive_ref(resolver: _Resolver[Schema]) -> _Resolved[Schema]: + """ + Recursive references (via recursive anchors), present only in draft 2019. + + As per the 2019 specification (§ 8.2.4.2.1), only the ``#`` recursive + reference is supported (and is therefore assumed to be the relevant + reference). + """ + resolved = resolver.lookup("#") + if isinstance(resolved.contents, Mapping) and resolved.contents.get( + "$recursiveAnchor", + ): + for uri, _ in resolver.dynamic_scope(): + next_resolved = resolver.lookup(uri) + if not isinstance( + next_resolved.contents, + Mapping, + ) or not next_resolved.contents.get("$recursiveAnchor"): + break + resolved = next_resolved + return resolved diff --git a/source/target-vendors/cp312/referencing/py.typed b/source/target-vendors/cp312/referencing/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/referencing/retrieval.py b/source/target-vendors/cp312/referencing/retrieval.py new file mode 100644 index 0000000..ac5a65b --- /dev/null +++ b/source/target-vendors/cp312/referencing/retrieval.py @@ -0,0 +1,94 @@ +""" +Helpers related to (dynamic) resource retrieval. +""" + +from __future__ import annotations + +from functools import lru_cache +from typing import TYPE_CHECKING +import json + +try: + from typing_extensions import TypeVar +except ImportError: # pragma: no cover + from typing import TypeVar + +from referencing import Resource + +if TYPE_CHECKING: + from collections.abc import Callable + + from referencing.typing import URI, D, Retrieve + +#: A serialized document (e.g. a JSON string) +_T = TypeVar("_T", default=str) + + +def to_cached_resource( + cache: Callable[[Retrieve[D]], Retrieve[D]] | None = None, + loads: Callable[[_T], D] = json.loads, + from_contents: Callable[[D], Resource[D]] = Resource.from_contents, +) -> Callable[[Callable[[URI], _T]], Retrieve[D]]: + """ + Create a retriever which caches its return values from a simpler callable. + + Takes a function which returns things like serialized JSON (strings) and + returns something suitable for passing to `Registry` as a retrieve + function. + + This decorator both reduces a small bit of boilerplate for a common case + (deserializing JSON from strings and creating `Resource` objects from the + result) as well as makes the probable need for caching a bit easier. + Retrievers which otherwise do expensive operations (like hitting the + network) might otherwise be called repeatedly. + + Examples + -------- + + .. testcode:: + + from referencing import Registry + from referencing.typing import URI + import referencing.retrieval + + + @referencing.retrieval.to_cached_resource() + def retrieve(uri: URI): + print(f"Retrieved {uri}") + + # Normally, go get some expensive JSON from the network, a file ... + return ''' + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "foo": "bar" + } + ''' + + one = Registry(retrieve=retrieve).get_or_retrieve("urn:example:foo") + print(one.value.contents["foo"]) + + # Retrieving the same URI again reuses the same value (and thus doesn't + # print another retrieval message here) + two = Registry(retrieve=retrieve).get_or_retrieve("urn:example:foo") + print(two.value.contents["foo"]) + + .. testoutput:: + + Retrieved urn:example:foo + bar + bar + + """ + if cache is None: + cache = lru_cache(maxsize=None) + + def decorator(retrieve: Callable[[URI], _T]): + @cache + def cached_retrieve(uri: URI): + response = retrieve(uri) + contents = loads(response) + return from_contents(contents) + + return cached_retrieve + + return decorator diff --git a/source/target-vendors/cp312/referencing/tests/__init__.py b/source/target-vendors/cp312/referencing/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/referencing/tests/test_core.py b/source/target-vendors/cp312/referencing/tests/test_core.py new file mode 100644 index 0000000..3edddbc --- /dev/null +++ b/source/target-vendors/cp312/referencing/tests/test_core.py @@ -0,0 +1,1057 @@ +from rpds import HashTrieMap +import pytest + +from referencing import Anchor, Registry, Resource, Specification, exceptions +from referencing.jsonschema import DRAFT202012 + +ID_AND_CHILDREN = Specification( + name="id-and-children", + id_of=lambda contents: contents.get("ID"), + subresources_of=lambda contents: contents.get("children", []), + anchors_in=lambda specification, contents: [ + Anchor( + name=name, + resource=specification.create_resource(contents=each), + ) + for name, each in contents.get("anchors", {}).items() + ], + maybe_in_subresource=lambda segments, resolver, subresource: ( + resolver.in_subresource(subresource) + if not len(segments) % 2 + and all(each == "children" for each in segments[::2]) + else resolver + ), +) + + +def blow_up(uri): # pragma: no cover + """ + A retriever suitable for use in tests which expect it never to be used. + """ + raise RuntimeError("This retrieve function expects to never be called!") + + +class TestRegistry: + def test_with_resource(self): + """ + Adding a resource to the registry then allows re-retrieving it. + """ + + resource = Resource.opaque(contents={"foo": "bar"}) + uri = "urn:example" + registry = Registry().with_resource(uri=uri, resource=resource) + assert registry[uri] is resource + + def test_with_resources(self): + """ + Adding multiple resources to the registry is like adding each one. + """ + + one = Resource.opaque(contents={}) + two = Resource(contents={"foo": "bar"}, specification=ID_AND_CHILDREN) + registry = Registry().with_resources( + [ + ("http://example.com/1", one), + ("http://example.com/foo/bar", two), + ], + ) + assert registry == Registry().with_resource( + uri="http://example.com/1", + resource=one, + ).with_resource( + uri="http://example.com/foo/bar", + resource=two, + ) + + def test_matmul_resource(self): + uri = "urn:example:resource" + resource = ID_AND_CHILDREN.create_resource({"ID": uri, "foo": 12}) + registry = resource @ Registry() + assert registry == Registry().with_resource(uri, resource) + + def test_matmul_many_resources(self): + one_uri = "urn:example:one" + one = ID_AND_CHILDREN.create_resource({"ID": one_uri, "foo": 12}) + + two_uri = "urn:example:two" + two = ID_AND_CHILDREN.create_resource({"ID": two_uri, "foo": 12}) + + registry = [one, two] @ Registry() + assert registry == Registry().with_resources( + [(one_uri, one), (two_uri, two)], + ) + + def test_matmul_resource_without_id(self): + resource = Resource.opaque(contents={"foo": "bar"}) + with pytest.raises(exceptions.NoInternalID) as e: + resource @ Registry() + assert e.value == exceptions.NoInternalID(resource=resource) + + def test_with_contents_from_json_schema(self): + uri = "urn:example" + schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"} + registry = Registry().with_contents([(uri, schema)]) + + expected = Resource(contents=schema, specification=DRAFT202012) + assert registry[uri] == expected + + def test_with_contents_and_default_specification(self): + uri = "urn:example" + registry = Registry().with_contents( + [(uri, {"foo": "bar"})], + default_specification=Specification.OPAQUE, + ) + assert registry[uri] == Resource.opaque({"foo": "bar"}) + + def test_len(self): + total = 5 + registry = Registry().with_contents( + [(str(i), {"foo": "bar"}) for i in range(total)], + default_specification=Specification.OPAQUE, + ) + assert len(registry) == total + + def test_bool_empty(self): + assert not Registry() + + def test_bool_not_empty(self): + registry = Registry().with_contents( + [(str(i), {"foo": "bar"}) for i in range(3)], + default_specification=Specification.OPAQUE, + ) + assert registry + + def test_iter(self): + registry = Registry().with_contents( + [(str(i), {"foo": "bar"}) for i in range(8)], + default_specification=Specification.OPAQUE, + ) + assert set(registry) == {str(i) for i in range(8)} + + def test_crawl_still_has_top_level_resource(self): + resource = Resource.opaque({"foo": "bar"}) + uri = "urn:example" + registry = Registry({uri: resource}).crawl() + assert registry[uri] is resource + + def test_crawl_finds_a_subresource(self): + child_id = "urn:child" + root = ID_AND_CHILDREN.create_resource( + {"ID": "urn:root", "children": [{"ID": child_id, "foo": 12}]}, + ) + registry = root @ Registry() + with pytest.raises(LookupError): + registry[child_id] + + expected = ID_AND_CHILDREN.create_resource({"ID": child_id, "foo": 12}) + assert registry.crawl()[child_id] == expected + + def test_crawl_finds_anchors_with_id(self): + resource = ID_AND_CHILDREN.create_resource( + {"ID": "urn:bar", "anchors": {"foo": 12}}, + ) + registry = resource @ Registry() + + assert registry.crawl().anchor(resource.id(), "foo").value == Anchor( + name="foo", + resource=ID_AND_CHILDREN.create_resource(12), + ) + + def test_crawl_finds_anchors_no_id(self): + resource = ID_AND_CHILDREN.create_resource({"anchors": {"foo": 12}}) + registry = Registry().with_resource("urn:root", resource) + + assert registry.crawl().anchor("urn:root", "foo").value == Anchor( + name="foo", + resource=ID_AND_CHILDREN.create_resource(12), + ) + + def test_contents(self): + resource = Resource.opaque({"foo": "bar"}) + uri = "urn:example" + registry = Registry().with_resource(uri, resource) + assert registry.contents(uri) == {"foo": "bar"} + + def test_getitem_strips_empty_fragments(self): + uri = "http://example.com/" + resource = ID_AND_CHILDREN.create_resource({"ID": uri + "#"}) + registry = resource @ Registry() + assert registry[uri] == registry[uri + "#"] == resource + + def test_contents_strips_empty_fragments(self): + uri = "http://example.com/" + resource = ID_AND_CHILDREN.create_resource({"ID": uri + "#"}) + registry = resource @ Registry() + assert ( + registry.contents(uri) + == registry.contents(uri + "#") + == {"ID": uri + "#"} + ) + + def test_contents_nonexistent_resource(self): + registry = Registry() + with pytest.raises(exceptions.NoSuchResource) as e: + registry.contents("urn:example") + assert e.value == exceptions.NoSuchResource(ref="urn:example") + + def test_crawled_anchor(self): + resource = ID_AND_CHILDREN.create_resource({"anchors": {"foo": "bar"}}) + registry = Registry().with_resource("urn:example", resource) + retrieved = registry.anchor("urn:example", "foo") + assert retrieved.value == Anchor( + name="foo", + resource=ID_AND_CHILDREN.create_resource("bar"), + ) + assert retrieved.registry == registry.crawl() + + def test_anchor_in_nonexistent_resource(self): + registry = Registry() + with pytest.raises(exceptions.NoSuchResource) as e: + registry.anchor("urn:example", "foo") + assert e.value == exceptions.NoSuchResource(ref="urn:example") + + def test_init(self): + one = Resource.opaque(contents={}) + two = ID_AND_CHILDREN.create_resource({"foo": "bar"}) + registry = Registry( + { + "http://example.com/1": one, + "http://example.com/foo/bar": two, + }, + ) + assert ( + registry + == Registry() + .with_resources( + [ + ("http://example.com/1", one), + ("http://example.com/foo/bar", two), + ], + ) + .crawl() + ) + + def test_dict_conversion(self): + """ + Passing a `dict` to `Registry` gets converted to a `HashTrieMap`. + + So continuing to use the registry works. + """ + + one = Resource.opaque(contents={}) + two = ID_AND_CHILDREN.create_resource({"foo": "bar"}) + registry = Registry( + {"http://example.com/1": one}, + ).with_resource("http://example.com/foo/bar", two) + assert ( + registry.crawl() + == Registry() + .with_resources( + [ + ("http://example.com/1", one), + ("http://example.com/foo/bar", two), + ], + ) + .crawl() + ) + + def test_no_such_resource(self): + registry = Registry() + with pytest.raises(exceptions.NoSuchResource) as e: + registry["urn:bigboom"] + assert e.value == exceptions.NoSuchResource(ref="urn:bigboom") + + def test_combine(self): + one = Resource.opaque(contents={}) + two = ID_AND_CHILDREN.create_resource({"foo": "bar"}) + three = ID_AND_CHILDREN.create_resource({"baz": "quux"}) + four = ID_AND_CHILDREN.create_resource({"anchors": {"foo": 12}}) + + first = Registry({"http://example.com/1": one}) + second = Registry().with_resource("http://example.com/foo/bar", two) + third = Registry( + { + "http://example.com/1": one, + "http://example.com/baz": three, + }, + ) + fourth = ( + Registry() + .with_resource( + "http://example.com/foo/quux", + four, + ) + .crawl() + ) + assert first.combine(second, third, fourth) == Registry( + [ + ("http://example.com/1", one), + ("http://example.com/baz", three), + ("http://example.com/foo/quux", four), + ], + anchors=HashTrieMap( + { + ("http://example.com/foo/quux", "foo"): Anchor( + name="foo", + resource=ID_AND_CHILDREN.create_resource(12), + ), + }, + ), + ).with_resource("http://example.com/foo/bar", two) + + def test_combine_self(self): + """ + Combining a registry with itself short-circuits. + + This is a performance optimization -- otherwise we do lots more work + (in jsonschema this seems to correspond to making the test suite take + *3x* longer). + """ + + registry = Registry({"urn:foo": "bar"}) + assert registry.combine(registry) is registry + + def test_combine_with_uncrawled_resources(self): + one = Resource.opaque(contents={}) + two = ID_AND_CHILDREN.create_resource({"foo": "bar"}) + three = ID_AND_CHILDREN.create_resource({"baz": "quux"}) + + first = Registry().with_resource("http://example.com/1", one) + second = Registry().with_resource("http://example.com/foo/bar", two) + third = Registry( + { + "http://example.com/1": one, + "http://example.com/baz": three, + }, + ) + expected = Registry( + [ + ("http://example.com/1", one), + ("http://example.com/foo/bar", two), + ("http://example.com/baz", three), + ], + ) + combined = first.combine(second, third) + assert combined != expected + assert combined.crawl() == expected + + def test_combine_with_single_retrieve(self): + one = Resource.opaque(contents={}) + two = ID_AND_CHILDREN.create_resource({"foo": "bar"}) + three = ID_AND_CHILDREN.create_resource({"baz": "quux"}) + + def retrieve(uri): # pragma: no cover + pass + + first = Registry().with_resource("http://example.com/1", one) + second = Registry( + retrieve=retrieve, + ).with_resource("http://example.com/2", two) + third = Registry().with_resource("http://example.com/3", three) + + assert first.combine(second, third) == Registry( + retrieve=retrieve, + ).with_resources( + [ + ("http://example.com/1", one), + ("http://example.com/2", two), + ("http://example.com/3", three), + ], + ) + assert second.combine(first, third) == Registry( + retrieve=retrieve, + ).with_resources( + [ + ("http://example.com/1", one), + ("http://example.com/2", two), + ("http://example.com/3", three), + ], + ) + + def test_combine_with_common_retrieve(self): + one = Resource.opaque(contents={}) + two = ID_AND_CHILDREN.create_resource({"foo": "bar"}) + three = ID_AND_CHILDREN.create_resource({"baz": "quux"}) + + def retrieve(uri): # pragma: no cover + pass + + first = Registry(retrieve=retrieve).with_resource( + "http://example.com/1", + one, + ) + second = Registry( + retrieve=retrieve, + ).with_resource("http://example.com/2", two) + third = Registry(retrieve=retrieve).with_resource( + "http://example.com/3", + three, + ) + + assert first.combine(second, third) == Registry( + retrieve=retrieve, + ).with_resources( + [ + ("http://example.com/1", one), + ("http://example.com/2", two), + ("http://example.com/3", three), + ], + ) + assert second.combine(first, third) == Registry( + retrieve=retrieve, + ).with_resources( + [ + ("http://example.com/1", one), + ("http://example.com/2", two), + ("http://example.com/3", three), + ], + ) + + def test_combine_conflicting_retrieve(self): + one = Resource.opaque(contents={}) + two = ID_AND_CHILDREN.create_resource({"foo": "bar"}) + three = ID_AND_CHILDREN.create_resource({"baz": "quux"}) + + def foo_retrieve(uri): # pragma: no cover + pass + + def bar_retrieve(uri): # pragma: no cover + pass + + first = Registry(retrieve=foo_retrieve).with_resource( + "http://example.com/1", + one, + ) + second = Registry().with_resource("http://example.com/2", two) + third = Registry(retrieve=bar_retrieve).with_resource( + "http://example.com/3", + three, + ) + + with pytest.raises(Exception, match="conflict.*retriev"): + first.combine(second, third) + + def test_remove(self): + one = Resource.opaque(contents={}) + two = ID_AND_CHILDREN.create_resource({"foo": "bar"}) + registry = Registry({"urn:foo": one, "urn:bar": two}) + assert registry.remove("urn:foo") == Registry({"urn:bar": two}) + + def test_remove_uncrawled(self): + one = Resource.opaque(contents={}) + two = ID_AND_CHILDREN.create_resource({"foo": "bar"}) + registry = Registry().with_resources( + [("urn:foo", one), ("urn:bar", two)], + ) + assert registry.remove("urn:foo") == Registry().with_resource( + "urn:bar", + two, + ) + + def test_remove_with_anchors(self): + one = Resource.opaque(contents={}) + two = ID_AND_CHILDREN.create_resource({"anchors": {"foo": "bar"}}) + registry = ( + Registry() + .with_resources( + [("urn:foo", one), ("urn:bar", two)], + ) + .crawl() + ) + assert ( + registry.remove("urn:bar") + == Registry() + .with_resource( + "urn:foo", + one, + ) + .crawl() + ) + + def test_remove_nonexistent_uri(self): + with pytest.raises(exceptions.NoSuchResource) as e: + Registry().remove("urn:doesNotExist") + assert e.value == exceptions.NoSuchResource(ref="urn:doesNotExist") + + def test_retrieve(self): + foo = Resource.opaque({"foo": "bar"}) + registry = Registry(retrieve=lambda uri: foo) + assert registry.get_or_retrieve("urn:example").value == foo + + def test_retrieve_arbitrary_exception(self): + foo = Resource.opaque({"foo": "bar"}) + + def retrieve(uri): + if uri == "urn:succeed": + return foo + raise Exception("Oh no!") + + registry = Registry(retrieve=retrieve) + assert registry.get_or_retrieve("urn:succeed").value == foo + with pytest.raises(exceptions.Unretrievable): + registry.get_or_retrieve("urn:uhoh") + + def test_retrieve_no_such_resource(self): + foo = Resource.opaque({"foo": "bar"}) + + def retrieve(uri): + if uri == "urn:succeed": + return foo + raise exceptions.NoSuchResource(ref=uri) + + registry = Registry(retrieve=retrieve) + assert registry.get_or_retrieve("urn:succeed").value == foo + with pytest.raises(exceptions.NoSuchResource): + registry.get_or_retrieve("urn:uhoh") + + def test_retrieve_cannot_determine_specification(self): + def retrieve(uri): + return Resource.from_contents({}) + + registry = Registry(retrieve=retrieve) + with pytest.raises(exceptions.CannotDetermineSpecification): + registry.get_or_retrieve("urn:uhoh") + + def test_retrieve_already_available_resource(self): + foo = Resource.opaque({"foo": "bar"}) + registry = Registry({"urn:example": foo}, retrieve=blow_up) + assert registry["urn:example"] == foo + assert registry.get_or_retrieve("urn:example").value == foo + + def test_retrieve_first_checks_crawlable_resource(self): + child = ID_AND_CHILDREN.create_resource({"ID": "urn:child", "foo": 12}) + root = ID_AND_CHILDREN.create_resource({"children": [child.contents]}) + registry = Registry(retrieve=blow_up).with_resource("urn:root", root) + assert registry.crawl()["urn:child"] == child + + def test_resolver(self): + one = Resource.opaque(contents={}) + registry = Registry({"http://example.com": one}) + resolver = registry.resolver(base_uri="http://example.com") + assert resolver.lookup("#").contents == {} + + def test_resolver_with_root_identified(self): + root = ID_AND_CHILDREN.create_resource({"ID": "http://example.com"}) + resolver = Registry().resolver_with_root(root) + assert resolver.lookup("http://example.com").contents == root.contents + assert resolver.lookup("#").contents == root.contents + + def test_resolver_with_root_unidentified(self): + root = Resource.opaque(contents={}) + resolver = Registry().resolver_with_root(root) + assert resolver.lookup("#").contents == root.contents + + def test_repr(self): + one = Resource.opaque(contents={}) + two = ID_AND_CHILDREN.create_resource({"foo": "bar"}) + registry = Registry().with_resources( + [ + ("http://example.com/1", one), + ("http://example.com/foo/bar", two), + ], + ) + assert repr(registry) == "" + assert repr(registry.crawl()) == "" + + def test_repr_mixed_crawled(self): + one = Resource.opaque(contents={}) + two = ID_AND_CHILDREN.create_resource({"foo": "bar"}) + registry = ( + Registry( + {"http://example.com/1": one}, + ) + .crawl() + .with_resource(uri="http://example.com/foo/bar", resource=two) + ) + assert repr(registry) == "" + + def test_repr_one_resource(self): + registry = Registry().with_resource( + uri="http://example.com/1", + resource=Resource.opaque(contents={}), + ) + assert repr(registry) == "" + + def test_repr_empty(self): + assert repr(Registry()) == "" + + +class TestResource: + def test_from_contents_from_json_schema(self): + schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"} + resource = Resource.from_contents(schema) + assert resource == Resource(contents=schema, specification=DRAFT202012) + + def test_from_contents_with_no_discernible_information(self): + """ + Creating a resource with no discernible way to see what + specification it belongs to (e.g. no ``$schema`` keyword for JSON + Schema) raises an error. + """ + + with pytest.raises(exceptions.CannotDetermineSpecification): + Resource.from_contents({"foo": "bar"}) + + def test_from_contents_with_no_discernible_information_and_default(self): + resource = Resource.from_contents( + {"foo": "bar"}, + default_specification=Specification.OPAQUE, + ) + assert resource == Resource.opaque(contents={"foo": "bar"}) + + def test_from_contents_unneeded_default(self): + schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"} + resource = Resource.from_contents( + schema, + default_specification=Specification.OPAQUE, + ) + assert resource == Resource( + contents=schema, + specification=DRAFT202012, + ) + + def test_non_mapping_from_contents(self): + resource = Resource.from_contents( + True, + default_specification=ID_AND_CHILDREN, + ) + assert resource == Resource( + contents=True, + specification=ID_AND_CHILDREN, + ) + + def test_from_contents_with_fallback(self): + resource = Resource.from_contents( + {"foo": "bar"}, + default_specification=Specification.OPAQUE, + ) + assert resource == Resource.opaque(contents={"foo": "bar"}) + + def test_id_delegates_to_specification(self): + specification = Specification( + name="", + id_of=lambda contents: "urn:fixedID", + subresources_of=lambda contents: [], + anchors_in=lambda specification, contents: [], + maybe_in_subresource=( + lambda segments, resolver, subresource: resolver + ), + ) + resource = Resource( + contents={"foo": "baz"}, + specification=specification, + ) + assert resource.id() == "urn:fixedID" + + def test_id_strips_empty_fragment(self): + uri = "http://example.com/" + root = ID_AND_CHILDREN.create_resource({"ID": uri + "#"}) + assert root.id() == uri + + def test_subresources_delegates_to_specification(self): + resource = ID_AND_CHILDREN.create_resource({"children": [{}, 12]}) + assert list(resource.subresources()) == [ + ID_AND_CHILDREN.create_resource(each) for each in [{}, 12] + ] + + def test_subresource_with_different_specification(self): + schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"} + resource = ID_AND_CHILDREN.create_resource({"children": [schema]}) + assert list(resource.subresources()) == [ + DRAFT202012.create_resource(schema), + ] + + def test_anchors_delegates_to_specification(self): + resource = ID_AND_CHILDREN.create_resource( + {"anchors": {"foo": {}, "bar": 1, "baz": ""}}, + ) + assert list(resource.anchors()) == [ + Anchor(name="foo", resource=ID_AND_CHILDREN.create_resource({})), + Anchor(name="bar", resource=ID_AND_CHILDREN.create_resource(1)), + Anchor(name="baz", resource=ID_AND_CHILDREN.create_resource("")), + ] + + def test_pointer_to_mapping(self): + resource = Resource.opaque(contents={"foo": "baz"}) + resolver = Registry().resolver() + assert resource.pointer("/foo", resolver=resolver).contents == "baz" + + def test_pointer_to_array(self): + resource = Resource.opaque(contents={"foo": {"bar": [3]}}) + resolver = Registry().resolver() + assert resource.pointer("/foo/bar/0", resolver=resolver).contents == 3 + + def test_root_pointer(self): + contents = {"foo": "baz"} + resource = Resource.opaque(contents=contents) + resolver = Registry().resolver() + assert resource.pointer("", resolver=resolver).contents == contents + + def test_opaque(self): + contents = {"foo": "bar"} + assert Resource.opaque(contents) == Resource( + contents=contents, + specification=Specification.OPAQUE, + ) + + +class TestResolver: + def test_lookup_exact_uri(self): + resource = Resource.opaque(contents={"foo": "baz"}) + resolver = Registry({"http://example.com/1": resource}).resolver() + resolved = resolver.lookup("http://example.com/1") + assert resolved.contents == resource.contents + + def test_lookup_subresource(self): + root = ID_AND_CHILDREN.create_resource( + { + "ID": "http://example.com/", + "children": [ + {"ID": "http://example.com/a", "foo": 12}, + ], + }, + ) + registry = root @ Registry() + resolved = registry.resolver().lookup("http://example.com/a") + assert resolved.contents == {"ID": "http://example.com/a", "foo": 12} + + def test_lookup_anchor_with_id(self): + root = ID_AND_CHILDREN.create_resource( + { + "ID": "http://example.com/", + "anchors": {"foo": 12}, + }, + ) + registry = root @ Registry() + resolved = registry.resolver().lookup("http://example.com/#foo") + assert resolved.contents == 12 + + def test_lookup_anchor_without_id(self): + root = ID_AND_CHILDREN.create_resource({"anchors": {"foo": 12}}) + resolver = Registry().with_resource("urn:example", root).resolver() + resolved = resolver.lookup("urn:example#foo") + assert resolved.contents == 12 + + def test_lookup_unknown_reference(self): + resolver = Registry().resolver() + ref = "http://example.com/does/not/exist" + with pytest.raises(exceptions.Unresolvable) as e: + resolver.lookup(ref) + assert e.value == exceptions.Unresolvable(ref=ref) + + def test_lookup_non_existent_pointer(self): + resource = Resource.opaque({"foo": {}}) + resolver = Registry({"http://example.com/1": resource}).resolver() + ref = "http://example.com/1#/foo/bar" + with pytest.raises(exceptions.Unresolvable) as e: + resolver.lookup(ref) + assert e.value == exceptions.PointerToNowhere( + ref="/foo/bar", + resource=resource, + ) + assert str(e.value) == "'/foo/bar' does not exist within {'foo': {}}" + + def test_lookup_non_existent_pointer_to_array_index(self): + resource = Resource.opaque([1, 2, 4, 8]) + resolver = Registry({"http://example.com/1": resource}).resolver() + ref = "http://example.com/1#/10" + with pytest.raises(exceptions.Unresolvable) as e: + resolver.lookup(ref) + assert e.value == exceptions.PointerToNowhere( + ref="/10", + resource=resource, + ) + + def test_lookup_pointer_to_empty_string(self): + resolver = Registry().resolver_with_root(Resource.opaque({"": {}})) + assert resolver.lookup("#/").contents == {} + + def test_lookup_non_existent_pointer_to_empty_string(self): + resource = Resource.opaque({"foo": {}}) + resolver = Registry().resolver_with_root(resource) + with pytest.raises( + exceptions.Unresolvable, + match="^'/' does not exist within {'foo': {}}.*'#'", + ) as e: + resolver.lookup("#/") + assert e.value == exceptions.PointerToNowhere( + ref="/", + resource=resource, + ) + + def test_lookup_non_existent_anchor(self): + root = ID_AND_CHILDREN.create_resource({"anchors": {}}) + resolver = Registry().with_resource("urn:example", root).resolver() + resolved = resolver.lookup("urn:example") + assert resolved.contents == root.contents + + ref = "urn:example#noSuchAnchor" + with pytest.raises(exceptions.Unresolvable) as e: + resolver.lookup(ref) + assert "'noSuchAnchor' does not exist" in str(e.value) + assert e.value == exceptions.NoSuchAnchor( + ref="urn:example", + resource=root, + anchor="noSuchAnchor", + ) + + def test_lookup_invalid_JSON_pointerish_anchor(self): + resolver = Registry().resolver_with_root( + ID_AND_CHILDREN.create_resource( + { + "ID": "http://example.com/", + "foo": {"bar": 12}, + }, + ), + ) + + valid = resolver.lookup("#/foo/bar") + assert valid.contents == 12 + + with pytest.raises(exceptions.InvalidAnchor) as e: + resolver.lookup("#foo/bar") + assert " '#/foo/bar'" in str(e.value) + + def test_lookup_retrieved_resource(self): + resource = Resource.opaque(contents={"foo": "baz"}) + resolver = Registry(retrieve=lambda uri: resource).resolver() + resolved = resolver.lookup("http://example.com/") + assert resolved.contents == resource.contents + + def test_lookup_failed_retrieved_resource(self): + """ + Unretrievable exceptions are also wrapped in Unresolvable. + """ + + uri = "http://example.com/" + + registry = Registry(retrieve=blow_up) + with pytest.raises(exceptions.Unretrievable): + registry.get_or_retrieve(uri) + + resolver = registry.resolver() + with pytest.raises(exceptions.Unresolvable): + resolver.lookup(uri) + + def test_repeated_lookup_from_retrieved_resource(self): + """ + A (custom-)retrieved resource is added to the registry returned by + looking it up. + """ + resource = Resource.opaque(contents={"foo": "baz"}) + once = [resource] + + def retrieve(uri): + return once.pop() + + resolver = Registry(retrieve=retrieve).resolver() + resolved = resolver.lookup("http://example.com/") + assert resolved.contents == resource.contents + + resolved = resolved.resolver.lookup("http://example.com/") + assert resolved.contents == resource.contents + + def test_repeated_anchor_lookup_from_retrieved_resource(self): + resource = Resource.opaque(contents={"foo": "baz"}) + once = [resource] + + def retrieve(uri): + return once.pop() + + resolver = Registry(retrieve=retrieve).resolver() + resolved = resolver.lookup("http://example.com/") + assert resolved.contents == resource.contents + + resolved = resolved.resolver.lookup("#") + assert resolved.contents == resource.contents + + # FIXME: The tests below aren't really representable in the current + # suite, though we should probably think of ways to do so. + + def test_in_subresource(self): + root = ID_AND_CHILDREN.create_resource( + { + "ID": "http://example.com/", + "children": [ + { + "ID": "child/", + "children": [{"ID": "grandchild"}], + }, + ], + }, + ) + registry = root @ Registry() + + resolver = registry.resolver() + first = resolver.lookup("http://example.com/") + assert first.contents == root.contents + + with pytest.raises(exceptions.Unresolvable): + first.resolver.lookup("grandchild") + + sub = first.resolver.in_subresource( + ID_AND_CHILDREN.create_resource(first.contents["children"][0]), + ) + second = sub.lookup("grandchild") + assert second.contents == {"ID": "grandchild"} + + def test_in_pointer_subresource(self): + root = ID_AND_CHILDREN.create_resource( + { + "ID": "http://example.com/", + "children": [ + { + "ID": "child/", + "children": [{"ID": "grandchild"}], + }, + ], + }, + ) + registry = root @ Registry() + + resolver = registry.resolver() + first = resolver.lookup("http://example.com/") + assert first.contents == root.contents + + with pytest.raises(exceptions.Unresolvable): + first.resolver.lookup("grandchild") + + second = first.resolver.lookup("#/children/0") + third = second.resolver.lookup("grandchild") + assert third.contents == {"ID": "grandchild"} + + def test_dynamic_scope(self): + one = ID_AND_CHILDREN.create_resource( + { + "ID": "http://example.com/", + "children": [ + { + "ID": "child/", + "children": [{"ID": "grandchild"}], + }, + ], + }, + ) + two = ID_AND_CHILDREN.create_resource( + { + "ID": "http://example.com/two", + "children": [{"ID": "two-child/"}], + }, + ) + registry = [one, two] @ Registry() + + resolver = registry.resolver() + first = resolver.lookup("http://example.com/") + second = first.resolver.lookup("#/children/0") + third = second.resolver.lookup("grandchild") + fourth = third.resolver.lookup("http://example.com/two") + assert list(fourth.resolver.dynamic_scope()) == [ + ("http://example.com/child/grandchild", fourth.resolver._registry), + ("http://example.com/child/", fourth.resolver._registry), + ("http://example.com/", fourth.resolver._registry), + ] + assert list(third.resolver.dynamic_scope()) == [ + ("http://example.com/child/", third.resolver._registry), + ("http://example.com/", third.resolver._registry), + ] + assert list(second.resolver.dynamic_scope()) == [ + ("http://example.com/", second.resolver._registry), + ] + assert list(first.resolver.dynamic_scope()) == [] + + +class TestSpecification: + def test_create_resource(self): + specification = Specification( + name="", + id_of=lambda contents: "urn:fixedID", + subresources_of=lambda contents: [], + anchors_in=lambda specification, contents: [], + maybe_in_subresource=( + lambda segments, resolver, subresource: resolver + ), + ) + resource = specification.create_resource(contents={"foo": "baz"}) + assert resource == Resource( + contents={"foo": "baz"}, + specification=specification, + ) + assert resource.id() == "urn:fixedID" + + def test_detect_from_json_schema(self): + schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"} + specification = Specification.detect(schema) + assert specification == DRAFT202012 + + def test_detect_with_no_discernible_information(self): + with pytest.raises(exceptions.CannotDetermineSpecification): + Specification.detect({"foo": "bar"}) + + def test_detect_with_non_URI_schema(self): + with pytest.raises(exceptions.CannotDetermineSpecification): + Specification.detect({"$schema": 37}) + + def test_detect_with_no_discernible_information_and_default(self): + specification = Specification.OPAQUE.detect({"foo": "bar"}) + assert specification is Specification.OPAQUE + + def test_detect_unneeded_default(self): + schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"} + specification = Specification.OPAQUE.detect(schema) + assert specification == DRAFT202012 + + def test_non_mapping_detect(self): + with pytest.raises(exceptions.CannotDetermineSpecification): + Specification.detect(True) + + def test_non_mapping_detect_with_default(self): + specification = ID_AND_CHILDREN.detect(True) + assert specification is ID_AND_CHILDREN + + def test_detect_with_fallback(self): + specification = Specification.OPAQUE.detect({"foo": "bar"}) + assert specification is Specification.OPAQUE + + def test_repr(self): + assert ( + repr(ID_AND_CHILDREN) == "" + ) + + +class TestOpaqueSpecification: + THINGS = [{"foo": "bar"}, True, 37, "foo", object()] + + @pytest.mark.parametrize("thing", THINGS) + def test_no_id(self, thing): + """ + An arbitrary thing has no ID. + """ + + assert Specification.OPAQUE.id_of(thing) is None + + @pytest.mark.parametrize("thing", THINGS) + def test_no_subresources(self, thing): + """ + An arbitrary thing has no subresources. + """ + + assert list(Specification.OPAQUE.subresources_of(thing)) == [] + + @pytest.mark.parametrize("thing", THINGS) + def test_no_anchors(self, thing): + """ + An arbitrary thing has no anchors. + """ + + assert list(Specification.OPAQUE.anchors_in(thing)) == [] + + +@pytest.mark.parametrize( + "cls", + [Anchor, Registry, Resource, Specification, exceptions.PointerToNowhere], +) +def test_nonsubclassable(cls): + with pytest.raises(Exception, match="(?i)subclassing"): + + class Boom(cls): # pragma: no cover + pass diff --git a/source/target-vendors/cp312/referencing/tests/test_exceptions.py b/source/target-vendors/cp312/referencing/tests/test_exceptions.py new file mode 100644 index 0000000..85cf99e --- /dev/null +++ b/source/target-vendors/cp312/referencing/tests/test_exceptions.py @@ -0,0 +1,34 @@ +import itertools + +import pytest + +from referencing import Resource, exceptions + + +def pairs(choices): + return itertools.combinations(choices, 2) + + +TRUE = Resource.opaque(True) + + +thunks = ( + lambda: exceptions.CannotDetermineSpecification(TRUE), + lambda: exceptions.NoSuchResource("urn:example:foo"), + lambda: exceptions.NoInternalID(TRUE), + lambda: exceptions.InvalidAnchor(resource=TRUE, anchor="foo", ref="a#b"), + lambda: exceptions.NoSuchAnchor(resource=TRUE, anchor="foo", ref="a#b"), + lambda: exceptions.PointerToNowhere(resource=TRUE, ref="urn:example:foo"), + lambda: exceptions.Unresolvable("urn:example:foo"), + lambda: exceptions.Unretrievable("urn:example:foo"), +) + + +@pytest.mark.parametrize("one, two", pairs(each() for each in thunks)) +def test_eq_incompatible_types(one, two): + assert one != two + + +@pytest.mark.parametrize("thunk", thunks) +def test_hash(thunk): + assert thunk() in {thunk()} diff --git a/source/target-vendors/cp312/referencing/tests/test_jsonschema.py b/source/target-vendors/cp312/referencing/tests/test_jsonschema.py new file mode 100644 index 0000000..c80714d --- /dev/null +++ b/source/target-vendors/cp312/referencing/tests/test_jsonschema.py @@ -0,0 +1,382 @@ +import pytest + +from referencing import Registry, Resource, Specification +import referencing.jsonschema + + +@pytest.mark.parametrize( + "uri, expected", + [ + ( + "https://json-schema.org/draft/2020-12/schema", + referencing.jsonschema.DRAFT202012, + ), + ( + "https://json-schema.org/draft/2019-09/schema", + referencing.jsonschema.DRAFT201909, + ), + ( + "http://json-schema.org/draft-07/schema#", + referencing.jsonschema.DRAFT7, + ), + ( + "http://json-schema.org/draft-06/schema#", + referencing.jsonschema.DRAFT6, + ), + ( + "http://json-schema.org/draft-04/schema#", + referencing.jsonschema.DRAFT4, + ), + ( + "http://json-schema.org/draft-03/schema#", + referencing.jsonschema.DRAFT3, + ), + ], +) +def test_schemas_with_explicit_schema_keywords_are_detected(uri, expected): + """ + The $schema keyword in JSON Schema is a dialect identifier. + """ + contents = {"$schema": uri} + resource = Resource.from_contents(contents) + assert resource == Resource(contents=contents, specification=expected) + + +def test_unknown_dialect(): + dialect_id = "http://example.com/unknown-json-schema-dialect-id" + with pytest.raises(referencing.jsonschema.UnknownDialect) as excinfo: + Resource.from_contents({"$schema": dialect_id}) + assert excinfo.value.uri == dialect_id + + +@pytest.mark.parametrize( + "id, specification", + [ + ("$id", referencing.jsonschema.DRAFT202012), + ("$id", referencing.jsonschema.DRAFT201909), + ("$id", referencing.jsonschema.DRAFT7), + ("$id", referencing.jsonschema.DRAFT6), + ("id", referencing.jsonschema.DRAFT4), + ("id", referencing.jsonschema.DRAFT3), + ], +) +def test_id_of_mapping(id, specification): + uri = "http://example.com/some-schema" + assert specification.id_of({id: uri}) == uri + + +@pytest.mark.parametrize( + "specification", + [ + referencing.jsonschema.DRAFT202012, + referencing.jsonschema.DRAFT201909, + referencing.jsonschema.DRAFT7, + referencing.jsonschema.DRAFT6, + ], +) +@pytest.mark.parametrize("value", [True, False]) +def test_id_of_bool(specification, value): + assert specification.id_of(value) is None + + +@pytest.mark.parametrize( + "specification", + [ + referencing.jsonschema.DRAFT202012, + referencing.jsonschema.DRAFT201909, + referencing.jsonschema.DRAFT7, + referencing.jsonschema.DRAFT6, + ], +) +@pytest.mark.parametrize("value", [True, False]) +def test_anchors_in_bool(specification, value): + assert list(specification.anchors_in(value)) == [] + + +@pytest.mark.parametrize( + "specification", + [ + referencing.jsonschema.DRAFT202012, + referencing.jsonschema.DRAFT201909, + referencing.jsonschema.DRAFT7, + referencing.jsonschema.DRAFT6, + ], +) +@pytest.mark.parametrize("value", [True, False]) +def test_subresources_of_bool(specification, value): + assert list(specification.subresources_of(value)) == [] + + +@pytest.mark.parametrize( + "uri, expected", + [ + ( + "https://json-schema.org/draft/2020-12/schema", + referencing.jsonschema.DRAFT202012, + ), + ( + "https://json-schema.org/draft/2019-09/schema", + referencing.jsonschema.DRAFT201909, + ), + ( + "http://json-schema.org/draft-07/schema#", + referencing.jsonschema.DRAFT7, + ), + ( + "http://json-schema.org/draft-06/schema#", + referencing.jsonschema.DRAFT6, + ), + ( + "http://json-schema.org/draft-04/schema#", + referencing.jsonschema.DRAFT4, + ), + ( + "http://json-schema.org/draft-03/schema#", + referencing.jsonschema.DRAFT3, + ), + ], +) +def test_specification_with(uri, expected): + assert referencing.jsonschema.specification_with(uri) == expected + + +@pytest.mark.parametrize( + "uri, expected", + [ + ( + "http://json-schema.org/draft-07/schema", + referencing.jsonschema.DRAFT7, + ), + ( + "http://json-schema.org/draft-06/schema", + referencing.jsonschema.DRAFT6, + ), + ( + "http://json-schema.org/draft-04/schema", + referencing.jsonschema.DRAFT4, + ), + ( + "http://json-schema.org/draft-03/schema", + referencing.jsonschema.DRAFT3, + ), + ], +) +def test_specification_with_no_empty_fragment(uri, expected): + assert referencing.jsonschema.specification_with(uri) == expected + + +def test_specification_with_unknown_dialect(): + dialect_id = "http://example.com/unknown-json-schema-dialect-id" + with pytest.raises(referencing.jsonschema.UnknownDialect) as excinfo: + referencing.jsonschema.specification_with(dialect_id) + assert excinfo.value.uri == dialect_id + + +def test_specification_with_default(): + dialect_id = "http://example.com/unknown-json-schema-dialect-id" + specification = referencing.jsonschema.specification_with( + dialect_id, + default=Specification.OPAQUE, + ) + assert specification is Specification.OPAQUE + + +# FIXME: The tests below should move to the referencing suite but I haven't yet +# figured out how to represent dynamic (& recursive) ref lookups in it. +def test_lookup_trivial_dynamic_ref(): + one = referencing.jsonschema.DRAFT202012.create_resource( + {"$dynamicAnchor": "foo"}, + ) + resolver = Registry().with_resource("http://example.com", one).resolver() + resolved = resolver.lookup("http://example.com#foo") + assert resolved.contents == one.contents + + +def test_multiple_lookup_trivial_dynamic_ref(): + TRUE = referencing.jsonschema.DRAFT202012.create_resource(True) + root = referencing.jsonschema.DRAFT202012.create_resource( + { + "$id": "http://example.com", + "$dynamicAnchor": "fooAnchor", + "$defs": { + "foo": { + "$id": "foo", + "$dynamicAnchor": "fooAnchor", + "$defs": { + "bar": True, + "baz": { + "$dynamicAnchor": "fooAnchor", + }, + }, + }, + }, + }, + ) + resolver = ( + Registry() + .with_resources( + [ + ("http://example.com", root), + ("http://example.com/foo/", TRUE), + ("http://example.com/foo/bar", root), + ], + ) + .resolver() + ) + + first = resolver.lookup("http://example.com") + second = first.resolver.lookup("foo/") + resolver = second.resolver.lookup("bar").resolver + fourth = resolver.lookup("#fooAnchor") + assert fourth.contents == root.contents + + +def test_multiple_lookup_dynamic_ref_to_nondynamic_ref(): + one = referencing.jsonschema.DRAFT202012.create_resource( + {"$anchor": "fooAnchor"}, + ) + two = referencing.jsonschema.DRAFT202012.create_resource( + { + "$id": "http://example.com", + "$dynamicAnchor": "fooAnchor", + "$defs": { + "foo": { + "$id": "foo", + "$dynamicAnchor": "fooAnchor", + "$defs": { + "bar": True, + "baz": { + "$dynamicAnchor": "fooAnchor", + }, + }, + }, + }, + }, + ) + resolver = ( + Registry() + .with_resources( + [ + ("http://example.com", two), + ("http://example.com/foo/", one), + ("http://example.com/foo/bar", two), + ], + ) + .resolver() + ) + + first = resolver.lookup("http://example.com") + second = first.resolver.lookup("foo/") + resolver = second.resolver.lookup("bar").resolver + fourth = resolver.lookup("#fooAnchor") + assert fourth.contents == two.contents + + +def test_lookup_trivial_recursive_ref(): + one = referencing.jsonschema.DRAFT201909.create_resource( + {"$recursiveAnchor": True}, + ) + resolver = Registry().with_resource("http://example.com", one).resolver() + first = resolver.lookup("http://example.com") + resolved = referencing.jsonschema.lookup_recursive_ref( + resolver=first.resolver, + ) + assert resolved.contents == one.contents + + +def test_lookup_recursive_ref_to_bool(): + TRUE = referencing.jsonschema.DRAFT201909.create_resource(True) + registry = Registry({"http://example.com": TRUE}) + resolved = referencing.jsonschema.lookup_recursive_ref( + resolver=registry.resolver(base_uri="http://example.com"), + ) + assert resolved.contents == TRUE.contents + + +def test_multiple_lookup_recursive_ref_to_bool(): + TRUE = referencing.jsonschema.DRAFT201909.create_resource(True) + root = referencing.jsonschema.DRAFT201909.create_resource( + { + "$id": "http://example.com", + "$recursiveAnchor": True, + "$defs": { + "foo": { + "$id": "foo", + "$recursiveAnchor": True, + "$defs": { + "bar": True, + "baz": { + "$recursiveAnchor": True, + "$anchor": "fooAnchor", + }, + }, + }, + }, + }, + ) + resolver = ( + Registry() + .with_resources( + [ + ("http://example.com", root), + ("http://example.com/foo/", TRUE), + ("http://example.com/foo/bar", root), + ], + ) + .resolver() + ) + + first = resolver.lookup("http://example.com") + second = first.resolver.lookup("foo/") + resolver = second.resolver.lookup("bar").resolver + fourth = referencing.jsonschema.lookup_recursive_ref(resolver=resolver) + assert fourth.contents == root.contents + + +def test_multiple_lookup_recursive_ref_with_nonrecursive_ref(): + one = referencing.jsonschema.DRAFT201909.create_resource( + {"$recursiveAnchor": True}, + ) + two = referencing.jsonschema.DRAFT201909.create_resource( + { + "$id": "http://example.com", + "$recursiveAnchor": True, + "$defs": { + "foo": { + "$id": "foo", + "$recursiveAnchor": True, + "$defs": { + "bar": True, + "baz": { + "$recursiveAnchor": True, + "$anchor": "fooAnchor", + }, + }, + }, + }, + }, + ) + three = referencing.jsonschema.DRAFT201909.create_resource( + {"$recursiveAnchor": False}, + ) + resolver = ( + Registry() + .with_resources( + [ + ("http://example.com", three), + ("http://example.com/foo/", two), + ("http://example.com/foo/bar", one), + ], + ) + .resolver() + ) + + first = resolver.lookup("http://example.com") + second = first.resolver.lookup("foo/") + resolver = second.resolver.lookup("bar").resolver + fourth = referencing.jsonschema.lookup_recursive_ref(resolver=resolver) + assert fourth.contents == two.contents + + +def test_empty_registry(): + assert referencing.jsonschema.EMPTY_REGISTRY == Registry() diff --git a/source/target-vendors/cp312/referencing/tests/test_referencing_suite.py b/source/target-vendors/cp312/referencing/tests/test_referencing_suite.py new file mode 100644 index 0000000..4b8ae91 --- /dev/null +++ b/source/target-vendors/cp312/referencing/tests/test_referencing_suite.py @@ -0,0 +1,66 @@ +from pathlib import Path +import json +import os + +import pytest + +from referencing import Registry +from referencing.exceptions import Unresolvable +import referencing.jsonschema + + +class SuiteNotFound(Exception): + def __str__(self): # pragma: no cover + return ( + "Cannot find the referencing suite. " + "Set the REFERENCING_SUITE environment variable to the path to " + "the suite, or run the test suite from alongside a full checkout " + "of the git repository." + ) + + +if "REFERENCING_SUITE" in os.environ: # pragma: no cover + SUITE = Path(os.environ["REFERENCING_SUITE"]) / "tests" +else: + SUITE = Path(__file__).parent.parent.parent / "suite/tests" +if not SUITE.is_dir(): # pragma: no cover + raise SuiteNotFound() +DIALECT_IDS = json.loads(SUITE.joinpath("specifications.json").read_text()) + + +@pytest.mark.parametrize( + "test_path", + [ + pytest.param(each, id=f"{each.parent.name}-{each.stem}") + for each in SUITE.glob("*/**/*.json") + ], +) +def test_referencing_suite(test_path, subtests): + dialect_id = DIALECT_IDS[test_path.relative_to(SUITE).parts[0]] + specification = referencing.jsonschema.specification_with(dialect_id) + loaded = json.loads(test_path.read_text()) + registry = loaded["registry"] + registry = Registry().with_resources( + (uri, specification.create_resource(contents)) + for uri, contents in loaded["registry"].items() + ) + for test in loaded["tests"]: + with subtests.test(test=test): + if "normalization" in test_path.stem: + pytest.xfail("APIs need to change for proper URL support.") + + resolver = registry.resolver(base_uri=test.get("base_uri", "")) + + if test.get("error"): + with pytest.raises(Unresolvable): + resolver.lookup(test["ref"]) + else: + resolved = resolver.lookup(test["ref"]) + assert resolved.contents == test["target"] + + then = test.get("then") + while then: # pragma: no cover + with subtests.test(test=test, then=then): + resolved = resolved.resolver.lookup(then["ref"]) + assert resolved.contents == then["target"] + then = then.get("then") diff --git a/source/target-vendors/cp312/referencing/tests/test_retrieval.py b/source/target-vendors/cp312/referencing/tests/test_retrieval.py new file mode 100644 index 0000000..d0a8f8a --- /dev/null +++ b/source/target-vendors/cp312/referencing/tests/test_retrieval.py @@ -0,0 +1,106 @@ +from functools import lru_cache +import json + +import pytest + +from referencing import Registry, Resource, exceptions +from referencing.jsonschema import DRAFT202012 +from referencing.retrieval import to_cached_resource + + +class TestToCachedResource: + def test_it_caches_retrieved_resources(self): + contents = {"$schema": "https://json-schema.org/draft/2020-12/schema"} + stack = [json.dumps(contents)] + + @to_cached_resource() + def retrieve(uri): + return stack.pop() + + registry = Registry(retrieve=retrieve) + + expected = Resource.from_contents(contents) + + got = registry.get_or_retrieve("urn:example:schema") + assert got.value == expected + + # And a second time we get the same value. + again = registry.get_or_retrieve("urn:example:schema") + assert again.value is got.value + + def test_custom_loader(self): + contents = {"$schema": "https://json-schema.org/draft/2020-12/schema"} + stack = [json.dumps(contents)[::-1]] + + @to_cached_resource(loads=lambda s: json.loads(s[::-1])) + def retrieve(uri): + return stack.pop() + + registry = Registry(retrieve=retrieve) + + expected = Resource.from_contents(contents) + + got = registry.get_or_retrieve("urn:example:schema") + assert got.value == expected + + # And a second time we get the same value. + again = registry.get_or_retrieve("urn:example:schema") + assert again.value is got.value + + def test_custom_from_contents(self): + contents = {} + stack = [json.dumps(contents)] + + @to_cached_resource(from_contents=DRAFT202012.create_resource) + def retrieve(uri): + return stack.pop() + + registry = Registry(retrieve=retrieve) + + expected = DRAFT202012.create_resource(contents) + + got = registry.get_or_retrieve("urn:example:schema") + assert got.value == expected + + # And a second time we get the same value. + again = registry.get_or_retrieve("urn:example:schema") + assert again.value is got.value + + def test_custom_cache(self): + schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"} + mapping = { + "urn:example:1": dict(schema, foo=1), + "urn:example:2": dict(schema, foo=2), + "urn:example:3": dict(schema, foo=3), + } + + resources = { + uri: Resource.from_contents(contents) + for uri, contents in mapping.items() + } + + @to_cached_resource(cache=lru_cache(maxsize=2)) + def retrieve(uri): + return json.dumps(mapping.pop(uri)) + + registry = Registry(retrieve=retrieve) + + got = registry.get_or_retrieve("urn:example:1") + assert got.value == resources["urn:example:1"] + assert registry.get_or_retrieve("urn:example:1").value is got.value + assert registry.get_or_retrieve("urn:example:1").value is got.value + + got = registry.get_or_retrieve("urn:example:2") + assert got.value == resources["urn:example:2"] + assert registry.get_or_retrieve("urn:example:2").value is got.value + assert registry.get_or_retrieve("urn:example:2").value is got.value + + # This still succeeds, but evicts the first URI + got = registry.get_or_retrieve("urn:example:3") + assert got.value == resources["urn:example:3"] + assert registry.get_or_retrieve("urn:example:3").value is got.value + assert registry.get_or_retrieve("urn:example:3").value is got.value + + # And now this fails (as we popped the value out of `mapping`) + with pytest.raises(exceptions.Unretrievable): + registry.get_or_retrieve("urn:example:1") diff --git a/source/target-vendors/cp312/referencing/typing.py b/source/target-vendors/cp312/referencing/typing.py new file mode 100644 index 0000000..a614464 --- /dev/null +++ b/source/target-vendors/cp312/referencing/typing.py @@ -0,0 +1,61 @@ +""" +Type-annotation related support for the referencing library. +""" + +from __future__ import annotations + +from collections.abc import Mapping as Mapping +from typing import TYPE_CHECKING, Any, Protocol + +try: + from typing_extensions import TypeVar +except ImportError: # pragma: no cover + from typing import TypeVar + +if TYPE_CHECKING: + from referencing._core import Resolved, Resolver, Resource + +#: A URI which identifies a `Resource`. +URI = str + +#: The type of documents within a registry. +D = TypeVar("D", default=Any) + + +class Retrieve(Protocol[D]): + """ + A retrieval callable, usable within a `Registry` for resource retrieval. + + Does not make assumptions about where the resource might be coming from. + """ + + def __call__(self, uri: URI) -> Resource[D]: + """ + Retrieve the resource with the given URI. + + Raise `referencing.exceptions.NoSuchResource` if you wish to indicate + the retriever cannot lookup the given URI. + """ + ... + + +class Anchor(Protocol[D]): + """ + An anchor within a `Resource`. + + Beyond "simple" anchors, some specifications like JSON Schema's 2020 + version have dynamic anchors. + """ + + @property + def name(self) -> str: + """ + Return the name of this anchor. + """ + ... + + def resolve(self, resolver: Resolver[D]) -> Resolved[D]: + """ + Return the resource for this anchor. + """ + ... diff --git a/source/target-vendors/cp312/rpds/__init__.py b/source/target-vendors/cp312/rpds/__init__.py new file mode 100644 index 0000000..257da6a --- /dev/null +++ b/source/target-vendors/cp312/rpds/__init__.py @@ -0,0 +1,5 @@ +from .rpds import * + +__doc__ = rpds.__doc__ +if hasattr(rpds, "__all__"): + __all__ = rpds.__all__ \ No newline at end of file diff --git a/source/target-vendors/cp312/rpds/__init__.pyi b/source/target-vendors/cp312/rpds/__init__.pyi new file mode 100644 index 0000000..51f03b0 --- /dev/null +++ b/source/target-vendors/cp312/rpds/__init__.pyi @@ -0,0 +1,79 @@ +from collections.abc import ( + ItemsView, + Iterable, + Iterator, + KeysView, + Mapping, + ValuesView, +) +from typing import ( + TypeVar, +) + +_T = TypeVar("_T") +_KT_co = TypeVar("_KT_co", covariant=True) +_VT_co = TypeVar("_VT_co", covariant=True) +_KU_co = TypeVar("_KU_co", covariant=True) +_VU_co = TypeVar("_VU_co", covariant=True) + +class HashTrieMap(Mapping[_KT_co, _VT_co]): + def __init__( + self, + value: Mapping[_KT_co, _VT_co] | Iterable[tuple[_KT_co, _VT_co]] = {}, + **kwds: Mapping[_KT_co, _VT_co], + ): ... + def __getitem__(self, key: _KT_co) -> _VT_co: ... + def __iter__(self) -> Iterator[_KT_co]: ... + def __len__(self) -> int: ... + def discard(self, key: _KT_co) -> HashTrieMap[_KT_co, _VT_co]: ... + def items(self) -> ItemsView[_KT_co, _VT_co]: ... + def keys(self) -> KeysView[_KT_co]: ... + def values(self) -> ValuesView[_VT_co]: ... + def remove(self, key: _KT_co) -> HashTrieMap[_KT_co, _VT_co]: ... + def insert( + self, + key: _KT_co, + val: _VT_co, + ) -> HashTrieMap[_KT_co, _VT_co]: ... + def update( + self, + *args: Mapping[_KU_co, _VU_co] | Iterable[tuple[_KU_co, _VU_co]], + ) -> HashTrieMap[_KT_co | _KU_co, _VT_co | _VU_co]: ... + @classmethod + def convert( + cls, + value: Mapping[_KT_co, _VT_co] | Iterable[tuple[_KT_co, _VT_co]], + ) -> HashTrieMap[_KT_co, _VT_co]: ... + @classmethod + def fromkeys( + cls, + keys: Iterable[_KT_co], + value: _VT_co = None, + ) -> HashTrieMap[_KT_co, _VT_co]: ... + +class HashTrieSet(frozenset[_T]): + def __init__(self, value: Iterable[_T] = ()): ... + def __iter__(self) -> Iterator[_T]: ... + def __len__(self) -> int: ... + def discard(self, value: _T) -> HashTrieSet[_T]: ... + def remove(self, value: _T) -> HashTrieSet[_T]: ... + def insert(self, value: _T) -> HashTrieSet[_T]: ... + def update(self, *args: Iterable[_T]) -> HashTrieSet[_T]: ... + +class List(Iterable[_T]): + def __init__(self, value: Iterable[_T] = (), *more: _T): ... + def __iter__(self) -> Iterator[_T]: ... + def __len__(self) -> int: ... + def push_front(self, value: _T) -> List[_T]: ... + def drop_first(self) -> List[_T]: ... + +class Queue(Iterable[_T]): + def __init__(self, value: Iterable[_T] = (), *more: _T): ... + def __iter__(self) -> Iterator[_T]: ... + def __len__(self) -> int: ... + def enqueue(self, value: _T) -> Queue[_T]: ... + def dequeue(self, value: _T) -> Queue[_T]: ... + @property + def is_empty(self) -> _T: ... + @property + def peek(self) -> _T: ... diff --git a/source/target-vendors/cp312/rpds/py.typed b/source/target-vendors/cp312/rpds/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/rpds/rpds.cpython-312-x86_64-linux-gnu.so b/source/target-vendors/cp312/rpds/rpds.cpython-312-x86_64-linux-gnu.so new file mode 100644 index 0000000..6d5f769 Binary files /dev/null and b/source/target-vendors/cp312/rpds/rpds.cpython-312-x86_64-linux-gnu.so differ diff --git a/source/target-vendors/cp312/rpds_py-2026.6.3.dist-info/METADATA b/source/target-vendors/cp312/rpds_py-2026.6.3.dist-info/METADATA new file mode 100644 index 0000000..bcf64a4 --- /dev/null +++ b/source/target-vendors/cp312/rpds_py-2026.6.3.dist-info/METADATA @@ -0,0 +1,99 @@ +Metadata-Version: 2.4 +Name: rpds-py +Version: 2026.6.3 +Classifier: Development Status :: 3 - Alpha +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Rust +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: 3.15 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +License-File: LICENSE +Summary: Python bindings to Rust's persistent data structures (rpds) +Keywords: data structures,rust,persistent +Author-email: Julian Berman +License-Expression: MIT +Requires-Python: >=3.11 +Description-Content-Type: text/x-rst; charset=UTF-8 +Project-URL: Documentation, https://rpds.readthedocs.io/ +Project-URL: Funding, https://github.com/sponsors/Julian +Project-URL: Homepage, https://github.com/crate-py/rpds +Project-URL: Issues, https://github.com/crate-py/rpds/issues/ +Project-URL: Source, https://github.com/crate-py/rpds +Project-URL: Tidelift, https://tidelift.com/subscription/pkg/pypi-rpds-py?utm_source=pypi-rpds-py&utm_medium=referral&utm_campaign=pypi-link +Project-URL: Upstream, https://github.com/orium/rpds + +=========== +``rpds.py`` +=========== + +|PyPI| |Pythons| |CI| + +.. |PyPI| image:: https://img.shields.io/pypi/v/rpds-py.svg + :alt: PyPI version + :target: https://pypi.org/project/rpds-py/ + +.. |Pythons| image:: https://img.shields.io/pypi/pyversions/rpds-py.svg + :alt: Supported Python versions + :target: https://pypi.org/project/rpds-py/ + +.. |CI| image:: https://github.com/crate-py/rpds/workflows/CI/badge.svg + :alt: Build status + :target: https://github.com/crate-py/rpds/actions?query=workflow%3ACI + +.. |ReadTheDocs| image:: https://readthedocs.org/projects/referencing/badge/?version=stable&style=flat + :alt: ReadTheDocs status + :target: https://referencing.readthedocs.io/en/stable/ + + +Python bindings to the `Rust rpds crate `_ for persistent data structures. + +What's here is quite minimal (in transparency, it was written initially to support replacing ``pyrsistent`` in the `referencing library `_). +If you see something missing (which is very likely), a PR is definitely welcome to add it. + +Installation +------------ + +The distribution on PyPI is named ``rpds.py`` (equivalently ``rpds-py``), and thus can be installed via e.g.: + +.. code:: sh + + $ pip install rpds-py + +Note that if you install ``rpds-py`` from source, you will need a Rust toolchain installed, as it is a build-time dependency. +An example of how to do so in a ``Dockerfile`` can be found `here `_. + +If you believe you are on a common platform which should have wheels built (i.e. and not need to compile from source), feel free to file an issue or pull request modifying the GitHub action used here to build wheels via ``maturin``. + +Usage +----- + +Methods in general are named similarly to their ``rpds`` counterparts (rather than ``pyrsistent``\ 's conventions, though probably a full drop-in ``pyrsistent``\ -compatible wrapper module is a good addition at some point). + +.. code:: python + + >>> from rpds import HashTrieMap, HashTrieSet, List + + >>> m = HashTrieMap({"foo": "bar", "baz": "quux"}) + >>> m.insert("spam", 37) == HashTrieMap({"foo": "bar", "baz": "quux", "spam": 37}) + True + >>> m.remove("foo") == HashTrieMap({"baz": "quux"}) + True + + >>> s = HashTrieSet({"foo", "bar", "baz", "quux"}) + >>> s.insert("spam") == HashTrieSet({"foo", "bar", "baz", "quux", "spam"}) + True + >>> s.remove("foo") == HashTrieSet({"bar", "baz", "quux"}) + True + + >>> L = List([1, 3, 5]) + >>> L.push_front(-1) == List([-1, 1, 3, 5]) + True + >>> L.rest == List([3, 5]) + True + diff --git a/source/target-vendors/cp312/rpds_py-2026.6.3.dist-info/RECORD b/source/target-vendors/cp312/rpds_py-2026.6.3.dist-info/RECORD new file mode 100644 index 0000000..40f3721 --- /dev/null +++ b/source/target-vendors/cp312/rpds_py-2026.6.3.dist-info/RECORD @@ -0,0 +1,9 @@ +rpds/__init__.py,sha256=w3MgXW7lpTCICw0KXbw20QX573_kbsEnWIeMsCAugvM,99 +rpds/__init__.pyi,sha256=am7x6oMa_pu_kv1NlolqJbPr6_UvCvoyxGKrDGSMKEk,2602 +rpds/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +rpds/rpds.cpython-312-x86_64-linux-gnu.so,sha256=gCKJWrgLJvnkD9UWAiPP37QdQh1v7LyT70kZnHUKRas,966896 +rpds_py-2026.6.3.dist-info/METADATA,sha256=-L57F9o5gLLEhZaUFBGkPHCsQbHi1TCqUx8sFeqy6xw,4147 +rpds_py-2026.6.3.dist-info/WHEEL,sha256=xePaIXZtDnKvRf8NMrO2enfKl7aJHT-f4ydLBaOLZ8E,147 +rpds_py-2026.6.3.dist-info/licenses/LICENSE,sha256=MU5Okb47qpPA-0vMyeTpfNZD64ObBlr5IXgsIXX-mQk,1057 +rpds_py-2026.6.3.dist-info/sboms/rpds-py.cyclonedx.json,sha256=gKO_uuFgflGyVpVmpGm4ptmphNSSPAgkCGQYpsij52M,20594 +rpds_py-2026.6.3.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/rpds_py-2026.6.3.dist-info/WHEEL b/source/target-vendors/cp312/rpds_py-2026.6.3.dist-info/WHEEL new file mode 100644 index 0000000..4898fa8 --- /dev/null +++ b/source/target-vendors/cp312/rpds_py-2026.6.3.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: maturin (1.14.1) +Root-Is-Purelib: false +Tag: cp312-cp312-manylinux_2_17_x86_64 +Tag: cp312-cp312-manylinux2014_x86_64 diff --git a/source/target-vendors/cp312/rpds_py-2026.6.3.dist-info/licenses/LICENSE b/source/target-vendors/cp312/rpds_py-2026.6.3.dist-info/licenses/LICENSE new file mode 100644 index 0000000..119a1f2 --- /dev/null +++ b/source/target-vendors/cp312/rpds_py-2026.6.3.dist-info/licenses/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 Julian Berman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/source/target-vendors/cp312/rpds_py-2026.6.3.dist-info/sboms/rpds-py.cyclonedx.json b/source/target-vendors/cp312/rpds_py-2026.6.3.dist-info/sboms/rpds-py.cyclonedx.json new file mode 100644 index 0000000..87f246b --- /dev/null +++ b/source/target-vendors/cp312/rpds_py-2026.6.3.dist-info/sboms/rpds-py.cyclonedx.json @@ -0,0 +1,680 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "urn:uuid:db798bf9-1726-4cb1-82cb-2a13771d0822", + "metadata": { + "timestamp": "2026-06-30T07:08:42.115208144Z", + "tools": [ + { + "vendor": "CycloneDX", + "name": "cargo-cyclonedx", + "version": "0.5.9" + } + ], + "component": { + "type": "library", + "bom-ref": "path+file:///home/runner/work/rpds/rpds#rpds-py@2026.6.3", + "name": "rpds-py", + "version": "2026.6.3", + "scope": "required", + "purl": "pkg:cargo/rpds-py@2026.6.3?download_url=file://.", + "components": [ + { + "type": "library", + "bom-ref": "path+file:///home/runner/work/rpds/rpds#rpds-py@2026.6.3 bin-target-0", + "name": "rpds", + "version": "2026.6.3", + "purl": "pkg:cargo/rpds-py@2026.6.3?download_url=file://.#src/lib.rs" + } + ] + }, + "properties": [ + { + "name": "cdx:rustc:sbom:target:all_targets", + "value": "true" + } + ] + }, + "components": [ + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#archery@1.2.2", + "author": "Diogo Sousa ", + "name": "archery", + "version": "1.2.2", + "description": "Abstract over the atomicity of reference-counting pointers", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "70e0a5f99dfebb87bb342d0f53bb92c81842e100bbb915223e38349580e5441d" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/archery@1.2.2", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/archery" + }, + { + "type": "website", + "url": "https://github.com/orium/archery" + }, + { + "type": "vcs", + "url": "https://github.com/orium/archery" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0", + "name": "heck", + "version": "0.5.0", + "description": "heck is a case conversion library.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/heck@0.5.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/withoutboats/heck" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.177", + "author": "The Rust Project Developers", + "name": "libc", + "version": "0.2.177", + "description": "Raw FFI bindings to platform libraries like libc.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/libc@0.2.177", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-lang/libc" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "author": "Aleksey Kladov ", + "name": "once_cell", + "version": "1.21.3", + "description": "Single assignment cells and lazy values.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/once_cell@1.21.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/once_cell" + }, + { + "type": "vcs", + "url": "https://github.com/matklad/once_cell" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.103", + "author": "David Tolnay , Alex Crichton ", + "name": "proc-macro2", + "version": "1.0.103", + "description": "A substitute implementation of the compiler's `proc_macro` API to decouple token-based libraries from the procedural macro use case.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/proc-macro2@1.0.103", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/proc-macro2" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/proc-macro2" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.29.0", + "author": "PyO3 Project and Contributors ", + "name": "pyo3-build-config", + "version": "0.29.0", + "description": "Build configuration for the PyO3 ecosystem", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pyo3-build-config@0.29.0", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/pyo3/pyo3" + }, + { + "type": "vcs", + "url": "https://github.com/pyo3/pyo3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-ffi@0.29.0", + "author": "PyO3 Project and Contributors ", + "name": "pyo3-ffi", + "version": "0.29.0", + "description": "Python-API bindings for the PyO3 ecosystem", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pyo3-ffi@0.29.0", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/pyo3/pyo3" + }, + { + "type": "other", + "url": "python" + }, + { + "type": "vcs", + "url": "https://github.com/pyo3/pyo3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros-backend@0.29.0", + "author": "PyO3 Project and Contributors ", + "name": "pyo3-macros-backend", + "version": "0.29.0", + "description": "Code generation for PyO3 package", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pyo3-macros-backend@0.29.0", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/pyo3/pyo3" + }, + { + "type": "vcs", + "url": "https://github.com/pyo3/pyo3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros@0.29.0", + "author": "PyO3 Project and Contributors ", + "name": "pyo3-macros", + "version": "0.29.0", + "description": "Proc macros for PyO3 package", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pyo3-macros@0.29.0", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/pyo3/pyo3" + }, + { + "type": "vcs", + "url": "https://github.com/pyo3/pyo3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3@0.29.0", + "author": "PyO3 Project and Contributors ", + "name": "pyo3", + "version": "0.29.0", + "description": "Bindings to Python interpreter", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pyo3@0.29.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/crate/pyo3/" + }, + { + "type": "website", + "url": "https://github.com/pyo3/pyo3" + }, + { + "type": "other", + "url": "pyo3-python" + }, + { + "type": "vcs", + "url": "https://github.com/pyo3/pyo3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.42", + "author": "David Tolnay ", + "name": "quote", + "version": "1.0.42", + "description": "Quasi-quoting macro quote!(...)", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/quote@1.0.42", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/quote/" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/quote" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rpds@1.2.1", + "author": "Diogo Sousa ", + "name": "rpds", + "version": "1.2.1", + "description": "Persistent data structures with structural sharing", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "e025feb26210bc196b908e72deb063b1b4000754304341cbc168a1e72c857ebc" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/rpds@1.2.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/rpds" + }, + { + "type": "website", + "url": "https://github.com/orium/rpds" + }, + { + "type": "vcs", + "url": "https://github.com/orium/rpds" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1", + "author": "The Servo Project Developers", + "name": "smallvec", + "version": "1.15.1", + "description": "'Small vector' optimization: store up to a small number of items on the stack", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/smallvec@1.15.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/smallvec/" + }, + { + "type": "vcs", + "url": "https://github.com/servo/rust-smallvec" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.111", + "author": "David Tolnay ", + "name": "syn", + "version": "2.0.111", + "description": "Parser for Rust source code", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/syn@2.0.111", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/syn" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/syn" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#target-lexicon@0.13.3", + "author": "Dan Gohman ", + "name": "target-lexicon", + "version": "0.13.3", + "description": "LLVM target triple types", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 WITH LLVM-exception" + } + ], + "purl": "pkg:cargo/target-lexicon@0.13.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/target-lexicon/" + }, + { + "type": "vcs", + "url": "https://github.com/bytecodealliance/target-lexicon" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#triomphe@0.1.15", + "author": "Manish Goregaokar , The Servo Project Developers", + "name": "triomphe", + "version": "0.1.15", + "description": "A fork of std::sync::Arc with some extra functionality and without weak references (originally servo_arc)", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/triomphe@0.1.15", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/Manishearth/triomphe" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.22", + "author": "David Tolnay ", + "name": "unicode-ident", + "version": "1.0.22", + "description": "Determine whether characters have the XID_Start or XID_Continue properties according to Unicode Standard Annex #31", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + } + ], + "licenses": [ + { + "expression": "(MIT OR Apache-2.0) AND Unicode-3.0" + } + ], + "purl": "pkg:cargo/unicode-ident@1.0.22", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/unicode-ident" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/unicode-ident" + } + ] + } + ], + "dependencies": [ + { + "ref": "path+file:///home/runner/work/rpds/rpds#rpds-py@2026.6.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#archery@1.2.2", + "registry+https://github.com/rust-lang/crates.io-index#pyo3@0.29.0", + "registry+https://github.com/rust-lang/crates.io-index#rpds@1.2.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#archery@1.2.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#triomphe@0.1.15" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.177" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.103", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.22" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.29.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#target-lexicon@0.13.3" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-ffi@0.29.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.177", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.29.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros-backend@0.29.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0", + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.103", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.42", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.111" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros@0.29.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.103", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros-backend@0.29.0", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.42", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.111" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3@0.29.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.177", + "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.29.0", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-ffi@0.29.0", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros@0.29.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.42", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.103" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rpds@1.2.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#archery@1.2.2", + "registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.111", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.103", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.42", + "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.22" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#target-lexicon@0.13.3" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#triomphe@0.1.15" + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.22" + } + ] +} \ No newline at end of file diff --git a/source/target-vendors/cp312/sse_starlette-3.4.11.dist-info/METADATA b/source/target-vendors/cp312/sse_starlette-3.4.11.dist-info/METADATA new file mode 100644 index 0000000..f461f39 --- /dev/null +++ b/source/target-vendors/cp312/sse_starlette-3.4.11.dist-info/METADATA @@ -0,0 +1,493 @@ +Metadata-Version: 2.4 +Name: sse-starlette +Version: 3.4.11 +Summary: SSE plugin for Starlette +Author-email: sysid +License-Expression: BSD-3-Clause +Project-URL: Homepage, https://github.com/sysid/sse-starlette +Project-URL: Repository, https://github.com/sysid/sse-starlette +Project-URL: Issues, https://github.com/sysid/sse-starlette/issues +Project-URL: Source, https://github.com/sysid/sse-starlette +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Topic :: Internet :: WWW/HTTP +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +License-File: LICENSE +License-File: AUTHORS +Requires-Dist: starlette>=0.49.1 +Requires-Dist: anyio>=4.7.0 +Provides-Extra: examples +Requires-Dist: uvicorn>=0.34.0; extra == "examples" +Requires-Dist: fastapi>=0.115.12; extra == "examples" +Requires-Dist: pydantic>=2; extra == "examples" +Provides-Extra: examples-db +Requires-Dist: sqlalchemy[asyncio]>=2.0.41; extra == "examples-db" +Requires-Dist: aiosqlite>=0.21.0; extra == "examples-db" +Provides-Extra: uvicorn +Requires-Dist: uvicorn>=0.34.0; extra == "uvicorn" +Provides-Extra: granian +Requires-Dist: granian>=2.3.1; extra == "granian" +Provides-Extra: daphne +Requires-Dist: daphne>=4.2.0; extra == "daphne" +Dynamic: license-file + +# Server-Sent Events for [Starlette](https://github.com/encode/starlette) and [FastAPI](https://fastapi.tiangolo.com/) + +[![Downloads](https://static.pepy.tech/badge/sse-starlette/week)](https://pepy.tech/project/sse-starlette) +[![PyPI Version][pypi-image]][pypi-url] +[![Build Status][build-image]][build-url] + +> Background: https://sysid.github.io/server-sent-events/ + +Production ready Server-Sent Events implementation for Starlette and FastAPI following the [W3C SSE specification](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events). + +## Installation + +```bash +pip install sse-starlette +uv add sse-starlette + +# To run the examples (fastapi, uvicorn, pydantic) +uv add sse-starlette[examples] + +# Example 03 also needs the DB extras (sqlalchemy, aiosqlite) +uv add sse-starlette[examples,examples-db] + +# Recommended ASGI server +uv add sse-starlette[uvicorn,granian,daphne] +``` + +## Quick Start + +```python +import asyncio +from starlette.applications import Starlette +from starlette.routing import Route +from sse_starlette import EventSourceResponse + +async def generate_events(): + for i in range(10): + yield {"data": f"Event {i}"} + await asyncio.sleep(1) + +async def sse_endpoint(request): + return EventSourceResponse(generate_events()) + +app = Starlette(routes=[Route("/events", sse_endpoint)]) +``` + +## Core Features + +- **Standards Compliant**: Full SSE specification implementation +- **Framework Integration**: Native Starlette and FastAPI support +- **Async/Await**: Built on modern Python async patterns +- **Connection Management**: Automatic client disconnect detection +- **Graceful Shutdown**: Proper cleanup on server termination with cooperative shutdown support +- **Thread Safety**: Context-local event management for multi-threaded applications +- **Multi-Loop Support**: Works correctly with multiple asyncio event loops + +For a detailed look at the internal task coordination, shutdown detection, and cancellation +flows, see [ARCHITECTURE.md](ARCHITECTURE.md). + +## Key Components + +### EventSourceResponse + +The main response class that handles SSE streaming: + +```python +from sse_starlette import EventSourceResponse + +# Basic usage +async def stream_data(): + for item in data: + yield {"data": item, "event": "update", "id": str(item.id)} + +return EventSourceResponse(stream_data()) +``` + +### ServerSentEvent + +For structured event creation: + +```python +from sse_starlette import ServerSentEvent + +event = ServerSentEvent( + data="Custom message", + event="notification", + id="msg-123", + retry=5000 +) +``` + +### JSONServerSentEvent + +For an easy way to send json data as SSE events: + +```python +from sse_starlette import JSONServerSentEvent + +event = JSONServerSentEvent( + data={"field":"value"}, # Anything serializable with json.dumps +) +``` + +## Advanced Usage + +### Custom Ping Configuration + +```python +from sse_starlette import ServerSentEvent + +def custom_ping(): + return ServerSentEvent(comment="Custom ping message") + +return EventSourceResponse( + generate_events(), + ping=10, # Ping every 10 seconds + ping_message_factory=custom_ping +) +``` + +### Multi-Threaded Usage + +sse-starlette now supports usage in multi-threaded applications and with multiple asyncio event loops: + +```python +import threading +import asyncio +from sse_starlette import EventSourceResponse + +def run_sse_in_thread(): + """SSE streaming works correctly in separate threads""" + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + async def thread_events(): + for i in range(5): + yield {"data": f"Thread event {i}"} + await asyncio.sleep(1) + + # This works without "Event bound to different loop" errors + response = EventSourceResponse(thread_events()) + loop.close() + +# Start SSE in multiple threads +for i in range(3): + thread = threading.Thread(target=run_sse_in_thread) + thread.start() +``` + +### Database Streaming (Thread-Safe) + +```python +async def stream_database_results(request): + # CORRECT: Create session within generator context + async with AsyncSession() as session: + results = await session.execute(select(User)) + for row in results: + if await request.is_disconnected(): + break + yield {"data": row.name, "id": str(row.id)} + +return EventSourceResponse(stream_database_results(request)) +``` + +### Error Handling and Timeouts + +```python +async def robust_stream(request): + try: + for i in range(100): + if await request.is_disconnected(): + break + yield {"data": f"Item {i}"} + await asyncio.sleep(0.5) + except asyncio.CancelledError: + # Client disconnected - perform cleanup + raise + +return EventSourceResponse( + robust_stream(request), + send_timeout=30, # Timeout hanging sends + headers={"Cache-Control": "no-cache"} +) +``` + +### Memory Channels Alternative + +For complex data flows, use memory channels instead of generators: + +```python +import anyio +from functools import partial + +async def data_producer(send_channel): + async with send_channel: + for i in range(10): + await send_channel.send({"data": f"Item {i}"}) + await anyio.sleep(1) + +async def channel_endpoint(request): + send_channel, receive_channel = anyio.create_memory_object_stream(10) + + return EventSourceResponse( + receive_channel, + data_sender_callable=partial(data_producer, send_channel) + ) +``` + +### Cooperative Shutdown + +By default, generators receive `CancelledError` immediately when the server shuts down. With +cooperative shutdown, generators can detect the shutdown signal, send farewell events to clients, +and exit gracefully within a configurable grace period. + +```python +import anyio +from sse_starlette import EventSourceResponse + +async def graceful_stream(request): + shutdown_event = anyio.Event() + + async def generate(): + try: + while not shutdown_event.is_set(): + yield {"data": "tick"} + # Check for shutdown between iterations + with anyio.move_on_after(1.0): + await shutdown_event.wait() + # Shutdown detected — send farewell event + yield {"event": "shutdown", "data": "Server is shutting down"} + except anyio.get_cancelled_exc_class(): + # Grace period expired — generator force-cancelled + raise + + return EventSourceResponse( + generate(), + shutdown_event=shutdown_event, # Library sets this on shutdown + shutdown_grace_period=5.0, # Seconds to wait before force-cancel + ) +``` + +**How it works:** + +1. On server shutdown, the library sets your `shutdown_event` +2. Your generator sees the event and can yield final events +3. If the generator exits within `shutdown_grace_period`, shutdown is clean (no `CancelledError`) +4. If the generator doesn't exit in time, it is force-cancelled as before + +**Important:** `shutdown_grace_period` should be less than your ASGI server's graceful shutdown +timeout (e.g. uvicorn's `--timeout-graceful-shutdown`), otherwise the process is killed before the +grace period expires. + +Without `shutdown_event` (the default), behavior is identical to previous versions: immediate +cancellation on server shutdown. + +## Configuration Options + +### EventSourceResponse Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `content` | `ContentStream` | Required | Async generator or iterable | +| `ping` | `int` | 15 | Ping interval in seconds (`0` disables keep-alive pings) | +| `sep` | `str` | `"\r\n"` | Line separator (`\r\n`, `\r`, `\n`) | +| `send_timeout` | `float` | `None` | Send operation timeout in seconds | +| `headers` | `dict` | `None` | Additional HTTP headers | +| `ping_message_factory` | `Callable` | `None` | Custom ping message creator | +| `shutdown_event` | `anyio.Event` | `None` | Event set by library on server shutdown | +| `shutdown_grace_period` | `float` | `0` | Seconds to wait after setting `shutdown_event` before force-cancel | + +### Client Disconnection + +```python +async def monitored_stream(request): + events_sent = 0 + try: + while events_sent < 100: + if await request.is_disconnected(): + print(f"Client disconnected after {events_sent} events") + break + + yield {"data": f"Event {events_sent}"} + events_sent += 1 + await asyncio.sleep(1) + + except asyncio.CancelledError: + print("Stream cancelled") + raise +``` + +## Testing + +sse-starlette includes now comprehensive test isolation without manual setup. The library automatically handles event loop contexts, eliminating the need for manual state resets: + +```python +# this is deprecated and not needed since version 3.0.0 +import pytest +from sse_starlette import EventSourceResponse + +@pytest.fixture +def reset_sse_app_status(): + AppStatus.should_exit_event = None + yield + AppStatus.should_exit_event = None +``` + +## Production Considerations + +### Performance Limits + +- **Memory**: Each connection maintains a buffer. Monitor memory usage. +- **Connections**: Limited by system file descriptors and application design. +- **Network**: High-frequency events can saturate bandwidth. + +### Error Recovery + +Implement client-side reconnection logic: + +```javascript +function createEventSource(url) { + const eventSource = new EventSource(url); + + eventSource.onerror = function() { + setTimeout(() => { + createEventSource(url); // Reconnect after delay + }, 5000); + }; + + return eventSource; +} +``` + +## Learning Resources + +### Examples Directory + +The `examples/` directory contains production-ready patterns: + +- **`01_basic_sse.py`**: Fundamental SSE concepts +- **`02_message_broadcasting.py`**: Multi-client message distribution +- **`03_database_streaming.py`**: Thread-safe database integration +- **`04_advanced_features.py`**: Custom protocols and error handling + +### Demonstrations Directory + +The `examples/demonstrations/` directory provides educational scenarios: + +**Basic Patterns** (`basic_patterns/`): +- Client disconnect detection and cleanup +- Graceful server shutdown behavior + +**Production Scenarios** (`production_scenarios/`): +- Load testing with concurrent clients +- Network interruption handling + +**Advanced Patterns** (`advanced_patterns/`): +- Memory channels vs generators +- Error recovery and circuit breakers +- Custom protocol development + +Run any demonstration: +```bash +python examples/demonstrations/basic_patterns/client_disconnect.py +python examples/demonstrations/production_scenarios/load_simulations.py +python examples/demonstrations/advanced_patterns/error_recovery.py +``` + +## Troubleshooting + +### Common Issues + +**Database session errors with async generators** +- Create database sessions inside generators, not as dependencies + +**Hanging connections after client disconnect** +- Always check `await request.is_disconnected()` in loops +- Use `send_timeout` parameter to detect dead connections + +If you are using Postman, please see: https://github.com/sysid/sse-starlette/issues/47#issuecomment-1445953826 + +### Performance Optimization + +```python +# Connection limits +class ConnectionLimiter: + def __init__(self, max_connections=100): + self.semaphore = asyncio.Semaphore(max_connections) + + async def limited_endpoint(self, request): + async with self.semaphore: + return EventSourceResponse(generate_events()) +``` + +## Network-Level Gotchas + +Network infrastructure components can buffer SSE streams, breaking real-time delivery. Here are the most common issues and solutions: + +### Reverse Proxy Buffering (Nginx/Apache) + +**Problem**: Nginx buffers responses by default, delaying SSE events until ~16KB accumulates. + +**Solution**: Add the `X-Accel-Buffering: no` header. + +**Nginx Configuration** (if you can't modify app headers): +```nginx +location /events { + proxy_pass http://localhost:8000; + proxy_http_version 1.1; + proxy_set_header Connection ''; + proxy_buffering off; # Disable for this location + chunked_transfer_encoding off; +} +``` + +### CDN Issues + +**Cloudflare**: Buffers ~100KB before flushing to clients, breaking real-time delivery. +**Akamai**: Edge servers buffer by default. + +### Load Balancer Problems + +**HAProxy**: Timeout settings must exceed heartbeat frequency. +```haproxy +# Ensure timeouts > ping interval +timeout client 60s # If ping every 45s +timeout server 60s +``` + +**F5 Load Balancers**: Buffer responses by default. + +## Contributing + +See examples and demonstrations for implementation patterns. Run tests with: + +```bash +make test-unit # Unit tests only (default dev install) +make test # Unit + docker integration tests +make test-experimentation # Optional experimentation tests (multi-consumer load tests) + # Requires the `experimentation` dependency group: + # uv sync --group experimentation +``` + +The `experimentation` group is opt-in (kept out of the default `dev` install) to keep the +contributor footprint small. See `tests/experimentation/` for the tests it enables. + + +[pypi-image]: https://badge.fury.io/py/sse-starlette.svg +[pypi-url]: https://pypi.org/project/sse-starlette/ +[build-image]: https://github.com/sysid/sse-starlette/actions/workflows/build.yml/badge.svg +[build-url]: https://github.com/sysid/sse-starlette/actions/workflows/build.yml +[coverage-image]: https://codecov.io/gh/sysid/sse-starlette/branch/master/graph/badge.svg + + diff --git a/source/target-vendors/cp312/sse_starlette-3.4.11.dist-info/RECORD b/source/target-vendors/cp312/sse_starlette-3.4.11.dist-info/RECORD new file mode 100644 index 0000000..7892a92 --- /dev/null +++ b/source/target-vendors/cp312/sse_starlette-3.4.11.dist-info/RECORD @@ -0,0 +1,11 @@ +sse_starlette/__init__.py,sha256=9ixg0ov-gHr8LSSotykb6kDhX4WqNrT_0-TD1rLkgF0,219 +sse_starlette/_utils.py,sha256=c1971mx2Wlr_MN7ekij6JYN-e3i9IQbY3l4MqDQ_VEA,1439 +sse_starlette/event.py,sha256=wePjktO-n0jsguXGmlu3hQooQWfJYNrUa8iD_RDe2K0,2801 +sse_starlette/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sse_starlette/sse.py,sha256=M7qzZzruHeJQLoLoCTQ4f7oJXu5_XaA4RTawayI4bV0,21365 +sse_starlette-3.4.11.dist-info/licenses/AUTHORS,sha256=wv9j0uzxKLvairGyAWgwpdoSQgL7uXFoaFu5lGrtPxo,395 +sse_starlette-3.4.11.dist-info/licenses/LICENSE,sha256=gK9r_Mvr0sFNSquWaVyrmVDhdxkMyA9e-BnpmZaIPo4,1511 +sse_starlette-3.4.11.dist-info/METADATA,sha256=DSt-l025V8pKogBTkYUWpvKKhDPv9AFyhIPLsjXkg6Q,15343 +sse_starlette-3.4.11.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91 +sse_starlette-3.4.11.dist-info/top_level.txt,sha256=-WvV5NMWkq_J766sZPusKqv8pESM-Dxugi96Lp9oer4,14 +sse_starlette-3.4.11.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/sse_starlette-3.4.11.dist-info/WHEEL b/source/target-vendors/cp312/sse_starlette-3.4.11.dist-info/WHEEL new file mode 100644 index 0000000..13013d7 --- /dev/null +++ b/source/target-vendors/cp312/sse_starlette-3.4.11.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (84.0.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/source/target-vendors/cp312/sse_starlette-3.4.11.dist-info/licenses/AUTHORS b/source/target-vendors/cp312/sse_starlette-3.4.11.dist-info/licenses/AUTHORS new file mode 100644 index 0000000..26fddfc --- /dev/null +++ b/source/target-vendors/cp312/sse_starlette-3.4.11.dist-info/licenses/AUTHORS @@ -0,0 +1,10 @@ +sysid +synodriver +Skyler Lewis +Margret `Pax` Williams +nekonoshiri +justindujardin +Håvard Thom <34199185+havardthom@users.noreply.github.com> +Vaibhav Mule +issmirnov +Cyprien diff --git a/source/target-vendors/cp312/sse_starlette-3.4.11.dist-info/licenses/LICENSE b/source/target-vendors/cp312/sse_starlette-3.4.11.dist-info/licenses/LICENSE new file mode 100644 index 0000000..3d12971 --- /dev/null +++ b/source/target-vendors/cp312/sse_starlette-3.4.11.dist-info/licenses/LICENSE @@ -0,0 +1,27 @@ +Copyright © 2020, [sysid](https://sysid.github.io/). +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/source/target-vendors/cp312/sse_starlette-3.4.11.dist-info/top_level.txt b/source/target-vendors/cp312/sse_starlette-3.4.11.dist-info/top_level.txt new file mode 100644 index 0000000..a0dde6f --- /dev/null +++ b/source/target-vendors/cp312/sse_starlette-3.4.11.dist-info/top_level.txt @@ -0,0 +1 @@ +sse_starlette diff --git a/source/target-vendors/cp312/sse_starlette/__init__.py b/source/target-vendors/cp312/sse_starlette/__init__.py new file mode 100644 index 0000000..207a327 --- /dev/null +++ b/source/target-vendors/cp312/sse_starlette/__init__.py @@ -0,0 +1,5 @@ +from sse_starlette.event import ServerSentEvent, JSONServerSentEvent +from sse_starlette.sse import EventSourceResponse + +__all__ = ["EventSourceResponse", "ServerSentEvent", "JSONServerSentEvent"] +__version__ = "3.4.11" diff --git a/source/target-vendors/cp312/sse_starlette/_utils.py b/source/target-vendors/cp312/sse_starlette/_utils.py new file mode 100644 index 0000000..c7e03d4 --- /dev/null +++ b/source/target-vendors/cp312/sse_starlette/_utils.py @@ -0,0 +1,37 @@ +import sys +from contextlib import contextmanager +from typing import Generator + +# Divergence #3 (see sse_starlette/sse.py module docstring): vendored copy of +# starlette._utils.collapse_excgroups. We do not import the upstream version +# because it lives in a private (underscore-prefixed) Starlette module, and +# the pinned floor (starlette>=0.41.3) makes that import surface unstable. +# +# Behaviour: AnyIO v4 wraps task-group failures in an ExceptionGroup; this +# helper unwraps single-exception groups so user middleware sees the bare +# exception (matching pre-v4 anyio and Starlette's StreamingResponse). +# Solution per https://anyio.readthedocs.io/en/stable/migration.html + +has_exceptiongroups = True +if sys.version_info < (3, 11): + try: + from exceptiongroup import BaseExceptionGroup # noqa: F401 + except ImportError: + has_exceptiongroups = False + + +@contextmanager +def collapse_excgroups() -> Generator[None, None, None]: + try: + yield + except BaseException as exc: + if has_exceptiongroups: + # `ty` does not narrow BaseExceptionGroup.exceptions; the runtime + # contract is identical to starlette._utils.collapse_excgroups. + while isinstance(exc, BaseExceptionGroup) and len(exc.exceptions) == 1: # ty: ignore[unresolved-attribute] + exc = exc.exceptions[0] # ty: ignore[unresolved-attribute] + + raise exc + + +__all__ = ["collapse_excgroups"] diff --git a/source/target-vendors/cp312/sse_starlette/event.py b/source/target-vendors/cp312/sse_starlette/event.py new file mode 100644 index 0000000..d72cfd5 --- /dev/null +++ b/source/target-vendors/cp312/sse_starlette/event.py @@ -0,0 +1,96 @@ +import io +import re +import json +from typing import Optional, Any, Union + + +class ServerSentEvent: + """ + Helper class to format data for Server-Sent Events (SSE). + """ + + _LINE_SEP_EXPR = re.compile(r"\r\n|\r|\n") + DEFAULT_SEPARATOR = "\r\n" + + def __init__( + self, + data: Optional[Any] = None, + *, + event: Optional[str] = None, + id: Optional[str] = None, + retry: Optional[int] = None, + comment: Optional[str] = None, + sep: Optional[str] = None, + ) -> None: + self.data = data + self.event = event + self.id = id + self.retry = retry + self.comment = comment + self._sep = sep if sep is not None else self.DEFAULT_SEPARATOR + + def encode(self) -> bytes: + buffer = io.StringIO() + if self.comment is not None: + for chunk in self._LINE_SEP_EXPR.split(str(self.comment)): + buffer.write(f": {chunk}{self._sep}") + + if self.id is not None: + # Clean newlines in the event id + buffer.write("id: " + self._LINE_SEP_EXPR.sub("", self.id) + self._sep) + + if self.event is not None: + # Clean newlines in the event name + buffer.write( + "event: " + self._LINE_SEP_EXPR.sub("", self.event) + self._sep + ) + + if self.data is not None: + # Break multi-line data into multiple data: lines + for chunk in self._LINE_SEP_EXPR.split(str(self.data)): + buffer.write(f"data: {chunk}{self._sep}") + + if self.retry is not None: + if not isinstance(self.retry, int): + raise TypeError("retry argument must be int") + buffer.write(f"retry: {self.retry}{self._sep}") + + buffer.write(self._sep) + return buffer.getvalue().encode("utf-8") + + +class JSONServerSentEvent(ServerSentEvent): + """ + Helper class to format JSON data for Server-Sent Events (SSE). + """ + + def __init__( + self, + data: Optional[Any] = None, + *args, + **kwargs, + ) -> None: + super().__init__( + json.dumps( + data, + ensure_ascii=False, + allow_nan=False, + indent=None, + separators=(",", ":"), + ) + if data is not None + else None, + *args, + **kwargs, + ) + + +def ensure_bytes(data: Union[bytes, dict, ServerSentEvent, Any], sep: str) -> bytes: + if isinstance(data, (bytes, memoryview)): + return bytes(data) + if isinstance(data, ServerSentEvent): + return data.encode() + if isinstance(data, dict): + data["sep"] = sep + return ServerSentEvent(**data).encode() + return ServerSentEvent(str(data), sep=sep).encode() diff --git a/source/target-vendors/cp312/sse_starlette/py.typed b/source/target-vendors/cp312/sse_starlette/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/sse_starlette/sse.py b/source/target-vendors/cp312/sse_starlette/sse.py new file mode 100644 index 0000000..c096b66 --- /dev/null +++ b/source/target-vendors/cp312/sse_starlette/sse.py @@ -0,0 +1,531 @@ +"""Server-Sent Events response for Starlette / FastAPI. + +Intentional divergence from ``starlette.responses.StreamingResponse`` +-------------------------------------------------------------------- + +``EventSourceResponse`` is modelled on Starlette's ``StreamingResponse`` and +re-syncs most of its behaviour (WebSocket denial, ``collapse_excgroups()`` +around the task group, ``memoryview`` chunk handling). The following points +are deliberate divergences — DO NOT "fix" them without reading the rationale: + +1. ASGI ``spec_version >= 2.4`` fast path is NOT adopted. + Upstream short-circuits to ``await stream_response(send)`` and converts + ``OSError`` into ``ClientDisconnect``, skipping ``listen_for_disconnect``. + We keep ``_listen_for_disconnect`` running because it + (a) invokes ``client_close_handler_callable`` on disconnect, + (b) flips ``self.active = False`` so ``_ping`` and the cooperative + shutdown grace loop exit promptly. + Adopting the upstream fast path would regress both features. + +2. ``_wrap_websocket_denial_send`` is inlined in this module rather than + inherited from ``starlette.responses.Response``. The helper landed on + Starlette ``main`` after our minimum pin (``starlette>=0.41.3``); inline + until the floor moves past the release that contains it. + +3. ``collapse_excgroups()`` is vendored in ``sse_starlette._utils`` rather + than imported from ``starlette._utils`` (private module). +""" + +import asyncio +import logging +import signal +import threading +from dataclasses import dataclass, field +from datetime import datetime, timezone +from typing import ( + Any, + AsyncIterable, + Awaitable, + Callable, + Coroutine, + Iterator, + Mapping, + Optional, + Set, + Union, +) + +import anyio +from starlette.background import BackgroundTask +from starlette.concurrency import iterate_in_threadpool +from starlette.datastructures import MutableHeaders +from starlette.responses import Response +from starlette.types import Receive, Scope, Send, Message + +from sse_starlette._utils import collapse_excgroups +from sse_starlette.event import ServerSentEvent, ensure_bytes + + +logger = logging.getLogger(__name__) + + +@dataclass +class _ShutdownState: + """Per-thread state for shutdown coordination. + + Issue #152 fix: Uses threading.local() instead of ContextVar to ensure + one watcher per thread rather than one per async context. + """ + + events: Set[anyio.Event] = field(default_factory=set) + watcher_started: bool = False + + +# Each thread gets its own shutdown state (one event loop per thread typically) +_thread_state = threading.local() + + +def _get_shutdown_state() -> _ShutdownState: + """Get or create shutdown state for the current thread.""" + state = getattr(_thread_state, "shutdown_state", None) + if state is None: + state = _ShutdownState() + _thread_state.shutdown_state = state + return state + + +def _get_uvicorn_server(): + """ + Try to get uvicorn Server instance via signal handler introspection. + + When uvicorn registers signal handlers, they're bound methods on the Server instance. + We can retrieve the Server from the handler's __self__ attribute. + + Returns None if: + - Not running under uvicorn + - Signal handler isn't a bound method + - Any introspection fails + """ + try: + handler = signal.getsignal(signal.SIGTERM) + if hasattr(handler, "__self__"): + server = handler.__self__ + if hasattr(server, "should_exit"): + return server + except Exception: + pass + return None + + +async def _shutdown_watcher() -> None: + """ + Poll for shutdown and broadcast to all events in this context. + + One watcher runs per thread (event loop). Checks two shutdown sources: + 1. AppStatus.should_exit - set when our monkey-patch works + 2. uvicorn Server.should_exit - via signal handler introspection (Issue #132 fix) + + When either becomes True, signals all registered events. + """ + state = _get_shutdown_state() + uvicorn_server = _get_uvicorn_server() + + try: + while True: + # Check our flag (monkey-patch worked or manually set) + if AppStatus.should_exit: + break + # Check uvicorn's flag directly (monkey-patch failed - Issue #132) + if ( + AppStatus.enable_automatic_graceful_drain + and uvicorn_server is not None + and uvicorn_server.should_exit + ): + AppStatus.should_exit = True # Sync state for consistency + break + await anyio.sleep(0.5) + + # Shutdown detected - broadcast to all waiting events + for event in list(state.events): + event.set() + finally: + # Allow watcher to be restarted if loop is reused + state.watcher_started = False + + +def _ensure_watcher_started_on_this_loop() -> None: + """Ensure the shutdown watcher is running for this thread (event loop).""" + state = _get_shutdown_state() + if not state.watcher_started: + state.watcher_started = True + try: + loop = asyncio.get_running_loop() + loop.create_task(_shutdown_watcher()) + except RuntimeError: + # No running loop - shouldn't happen in normal use + state.watcher_started = False + + +def _wrap_websocket_denial_send(send: Send) -> Send: + """Mirror of ``starlette.responses.Response._wrap_websocket_denial_send``. + + Divergence #2 (see module docstring): inlined because the helper landed + on Starlette ``main`` (commit 9ee9519) after our minimum pin + ``starlette>=0.41.3``. Drop this once the floor moves past the release + that contains it. + """ + + async def wrapped(message: Message) -> None: + message_type = message["type"] + if message_type in {"http.response.start", "http.response.body"}: + message = {**message, "type": "websocket." + message_type} + await send(message) + + return wrapped + + +class SendTimeoutError(TimeoutError): + pass + + +class AppStatus: + """Helper to capture a shutdown signal from Uvicorn so we can gracefully terminate SSE streams.""" + + should_exit = False + enable_automatic_graceful_drain = True + original_handler: Optional[Callable] = None + + @staticmethod + def disable_automatic_graceful_drain(): + """ + Prevent automatic SSE stream termination on server shutdown. + + WARNING: When disabled, you MUST set AppStatus.should_exit = True + at some point during shutdown, or streams will never close and the + server will hang indefinitely (or until uvicorn's graceful shutdown + timeout expires). + """ + AppStatus.enable_automatic_graceful_drain = False + + @staticmethod + def enable_automatic_graceful_drain_mode(): + """ + Re-enable automatic SSE stream termination on server shutdown. + + This restores the default behavior where SIGTERM triggers immediate + stream draining. Call this to undo a previous call to + disable_automatic_graceful_drain(). + """ + AppStatus.enable_automatic_graceful_drain = True + + @staticmethod + def handle_exit(*args, **kwargs): + if AppStatus.enable_automatic_graceful_drain: + AppStatus.should_exit = True + if AppStatus.original_handler is not None: + AppStatus.original_handler(*args, **kwargs) + + +try: + from uvicorn.main import Server + + AppStatus.original_handler = Server.handle_exit + Server.handle_exit = AppStatus.handle_exit # type: ignore +except ImportError: + logger.debug( + "Uvicorn not installed. Graceful shutdown on server termination disabled." + ) + +Content = Union[str, bytes, dict, ServerSentEvent, Any] +SyncContentStream = Iterator[Content] +AsyncContentStream = AsyncIterable[Content] +ContentStream = Union[AsyncContentStream, SyncContentStream] + + +class EventSourceResponse(Response): + """Streaming response implementing the SSE (Server-Sent Events) specification. + + Args: + content: Async iterable or sync iterator yielding SSE event data. + status_code: HTTP status code. Default: 200. + headers: Additional HTTP headers. + media_type: Response media type. Default: "text/event-stream". + background: Background task to run after response completes. + ping: Ping interval in seconds. Default: 15. Set to 0 to disable + keep-alive pings entirely (no ping task is started). + sep: Line separator for SSE messages ("\\r\\n", "\\r", or "\\n"). + ping_message_factory: Callable returning custom ping ServerSentEvent. + data_sender_callable: Async callable for push-based data sending. + send_timeout: Timeout in seconds for individual data or heartbeat sends. + client_close_handler_callable: Async callback on client disconnect. + shutdown_event: Optional ``anyio.Event`` set by the library when server + shutdown is detected. Generators can watch this event to send farewell + messages and exit cooperatively instead of receiving CancelledError. + shutdown_grace_period: Seconds to wait after setting ``shutdown_event`` + before force-cancelling the generator. Must be >= 0. Should be less + than your ASGI server's graceful shutdown timeout. Default: 0 + (immediate cancel, identical to pre-v3.3.0 behavior). + """ + + DEFAULT_PING_INTERVAL = 15 + DEFAULT_SEPARATOR = "\r\n" + + def __init__( + self, + content: ContentStream, + status_code: int = 200, + headers: Optional[Mapping[str, str]] = None, + media_type: str = "text/event-stream", + background: Optional[BackgroundTask] = None, + ping: Optional[Union[int, float]] = None, + sep: Optional[str] = None, + ping_message_factory: Optional[Callable[[], ServerSentEvent]] = None, + data_sender_callable: Optional[ + Callable[[], Coroutine[None, None, None]] + ] = None, + send_timeout: Optional[float] = None, + client_close_handler_callable: Optional[ + Callable[[Message], Awaitable[None]] + ] = None, + shutdown_event: Optional[anyio.Event] = None, + shutdown_grace_period: float = 0, + ) -> None: + # Validate separator + if sep not in (None, "\r\n", "\r", "\n"): + raise ValueError(f"sep must be one of: \\r\\n, \\r, \\n, got: {sep}") + self.sep = sep or self.DEFAULT_SEPARATOR + + # If content is sync, wrap it for async iteration + if isinstance(content, AsyncIterable): + self.body_iterator = content + else: + self.body_iterator = iterate_in_threadpool(content) + + self.status_code = status_code + self.media_type = self.media_type if media_type is None else media_type + self.background = background + self.data_sender_callable = data_sender_callable + self.send_timeout = send_timeout + + # Build SSE-specific headers. + _headers = MutableHeaders() + if headers is not None: # pragma: no cover + _headers.update(headers) + + # "The no-store response directive indicates that any caches of any kind (private or shared) + # should not store this response." + # -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control + # allow cache control header to be set by user to support fan out proxies + # https://www.fastly.com/blog/server-sent-events-fastly + + _headers.setdefault("Cache-Control", "no-store") + # mandatory for servers-sent events headers + _headers["Connection"] = "keep-alive" + _headers["X-Accel-Buffering"] = "no" + self.init_headers(_headers) + + self.ping_interval = self.DEFAULT_PING_INTERVAL if ping is None else ping + self.ping_message_factory = ping_message_factory + + self.client_close_handler_callable = client_close_handler_callable + + # Cooperative shutdown (Issue #167): Allow generators to send farewell + # events before force-cancellation. The grace period should be less than + # your ASGI server's graceful shutdown timeout (e.g. uvicorn's + # --timeout-graceful-shutdown), otherwise the process is killed before + # the grace period expires. + if shutdown_grace_period < 0: + raise ValueError("shutdown_grace_period must be >= 0") + self._shutdown_event = shutdown_event + self._shutdown_grace_period = shutdown_grace_period + + self.active = True + # https://github.com/sysid/sse-starlette/pull/55#issuecomment-1732374113 + self._send_lock = anyio.Lock() + + @property + def ping_interval(self) -> Union[int, float]: + return self._ping_interval + + @ping_interval.setter + def ping_interval(self, value: Union[int, float]) -> None: + if not isinstance(value, (int, float)): + raise TypeError("ping interval must be int or float") + if value < 0: + raise ValueError("ping interval must be >= 0 (0 disables ping)") + self._ping_interval = value + + def enable_compression(self, force: bool = False) -> None: + raise NotImplementedError("Compression is not supported for SSE streams.") + + async def _send_with_timeout(self, send: Send, message: Message) -> None: + """Send one ASGI message, applying the response's per-send timeout.""" + with anyio.move_on_after(self.send_timeout) as cancel_scope: + await send(message) + + if cancel_scope and cancel_scope.cancel_called: + raise SendTimeoutError() + + async def _stream_response(self, send: Send) -> None: + """Send out SSE data to the client as it becomes available in the iterator.""" + await send( + { + "type": "http.response.start", + "status": self.status_code, + "headers": self.raw_headers, + } + ) + + async for data in self.body_iterator: + chunk = ensure_bytes(data, self.sep) + logger.debug("chunk: %s", chunk) + try: + await self._send_with_timeout( + send, + {"type": "http.response.body", "body": chunk, "more_body": True}, + ) + except SendTimeoutError: + aclose = getattr(self.body_iterator, "aclose", None) + if aclose is not None: + await aclose() + raise + + async with self._send_lock: + self.active = False + await send({"type": "http.response.body", "body": b"", "more_body": False}) + + async def _listen_for_disconnect(self, receive: Receive) -> None: + """Watch for a disconnect message from the client. + + Divergence #1 (see module docstring): kept unconditionally instead of + adopting Starlette's ASGI 2.4 ``OSError → ClientDisconnect`` fast path, + because this loop drives ``client_close_handler_callable`` and flips + ``self.active = False`` for ``_ping`` and the shutdown grace loop. + """ + while self.active: + message = await receive() + if message["type"] == "http.disconnect": + self.active = False + logger.debug("Got event: http.disconnect. Stop streaming.") + if self.client_close_handler_callable: + await self.client_close_handler_callable(message) + break + + @staticmethod + async def _listen_for_exit_signal() -> None: + """Wait for shutdown signal via the shared watcher.""" + if AppStatus.should_exit: + return + + _ensure_watcher_started_on_this_loop() + + state = _get_shutdown_state() + event = anyio.Event() + state.events.add(event) + + try: + # Double-check after registration + if AppStatus.should_exit: + return + await event.wait() + finally: + state.events.discard(event) + + async def _listen_for_exit_signal_with_grace(self) -> None: + """Wait for shutdown signal, then optionally give generator a grace period. + + Issue #167: When a shutdown_event is provided, the library sets it before + returning, giving the generator a chance to send farewell events and exit + cooperatively. The shutdown_grace_period controls how long to wait before + force-cancelling via task group cancellation. + """ + await self._listen_for_exit_signal() + + # Signal the user's generator that shutdown is happening + if self._shutdown_event: + self._shutdown_event.set() + + # Grace period: let generator finish naturally before force-cancel + if self._shutdown_grace_period > 0: + with anyio.move_on_after(self._shutdown_grace_period): + while self.active: + await anyio.sleep(0.1) + + async def _ping(self, send: Send) -> None: + """Periodically send ping messages to keep the connection alive on proxies. + - frequency ca every 15 seconds. + - Alternatively one can send periodically a comment line (one starting with a ':' character) + + Invariant (Issue #206): must NOT be started when ``ping_interval == 0`` + (ping disabled), because ``anyio.sleep(0)`` is a bare checkpoint and the + loop would busy-spin, flooding the client with pings and contending for + ``_send_lock`` with the data stream. ``__call__`` skips the task instead + of returning early here, since ``_ping`` runs under ``cancel_on_finish`` + and any return would tear down the whole response. + """ + while self.active: + await anyio.sleep(self._ping_interval) + sse_ping = ( + self.ping_message_factory() + if self.ping_message_factory + else ServerSentEvent( + comment=f"ping - {datetime.now(timezone.utc)}", sep=self.sep + ) + ) + ping_bytes = ensure_bytes(sse_ping, self.sep) + logger.debug("ping: %s", ping_bytes) + + async with self._send_lock: + if self.active: + # A SendTimeoutError raised here deliberately does NOT + # aclose() the body iterator, unlike _stream_response. When a + # heartbeat times out, _stream_response is normally suspended + # inside body_iterator.__anext__(), where ag_running_async is + # set, so aclose() would raise "RuntimeError: aclose(): + # asynchronous generator is already running" (verified). + # Propagating instead lets the task group cancel + # _stream_response, which throws into the suspended + # __anext__ and runs the generator's finally block -- see + # test_ping_whenSendTimesOut_thenTearsDownResponseAndRunsCleanup. + await self._send_with_timeout( + send, + { + "type": "http.response.body", + "body": ping_bytes, + "more_body": True, + }, + ) + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + """Entrypoint for Starlette's ASGI contract. We spin up tasks: + - _stream_response to push events + - _ping to keep the connection alive + - _listen_for_exit_signal to respond to server shutdown + - _listen_for_disconnect to respond to client disconnect + """ + # WebSocket denial parity with Starlette's StreamingResponse: a + # streaming response on a websocket scope must wrap send so message + # types become ``websocket.http.response.*``. + if scope["type"] == "websocket": + send = _wrap_websocket_denial_send(send) + + # collapse_excgroups parity with Starlette's StreamingResponse: anyio + # v4 wraps task-group failures in ExceptionGroup; user middleware + # expects the bare exception. + with collapse_excgroups(): + async with anyio.create_task_group() as task_group: + # https://trio.readthedocs.io/en/latest/reference-core.html#custom-supervisors + async def cancel_on_finish(coro: Callable[[], Awaitable[None]]): + await coro() + task_group.cancel_scope.cancel() + + task_group.start_soon( + cancel_on_finish, lambda: self._stream_response(send) + ) + # ping_interval == 0 disables keep-alive pings entirely (#206) + if self._ping_interval > 0: + task_group.start_soon(cancel_on_finish, lambda: self._ping(send)) + task_group.start_soon( + cancel_on_finish, self._listen_for_exit_signal_with_grace + ) + + if self.data_sender_callable: + task_group.start_soon(self.data_sender_callable) + + # Wait for the client to disconnect last + task_group.start_soon( + cancel_on_finish, lambda: self._listen_for_disconnect(receive) + ) + + if self.background is not None: + await self.background() diff --git a/source/target-vendors/cp312/starlette-1.7.0.dist-info/METADATA b/source/target-vendors/cp312/starlette-1.7.0.dist-info/METADATA new file mode 100644 index 0000000..714e2cf --- /dev/null +++ b/source/target-vendors/cp312/starlette-1.7.0.dist-info/METADATA @@ -0,0 +1,182 @@ +Metadata-Version: 2.5 +Name: starlette +Version: 1.7.0 +Summary: The little ASGI library that shines. +Project-URL: Homepage, https://github.com/Kludex/starlette +Project-URL: Documentation, https://starlette.dev/ +Project-URL: Changelog, https://starlette.dev/release-notes/ +Project-URL: Funding, https://github.com/sponsors/Kludex +Project-URL: Source, https://github.com/Kludex/starlette +Author-email: Tom Christie +Maintainer-email: Marcelo Trylesinski +License-Expression: BSD-3-Clause +License-File: LICENSE.md +Classifier: Development Status :: 3 - Alpha +Classifier: Environment :: Web Environment +Classifier: Framework :: AnyIO +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: 3.15 +Classifier: Topic :: Internet :: WWW/HTTP +Requires-Python: >=3.10 +Requires-Dist: anyio<5,>=4.0.0 +Requires-Dist: typing-extensions>=4.10.0; python_version < '3.13' +Provides-Extra: full +Requires-Dist: httpx2>=2.0.0; extra == 'full' +Requires-Dist: httpx<0.29.0,>=0.27.0; extra == 'full' +Requires-Dist: itsdangerous; extra == 'full' +Requires-Dist: jinja2; extra == 'full' +Requires-Dist: opentelemetry-api; extra == 'full' +Requires-Dist: python-multipart>=0.0.18; extra == 'full' +Requires-Dist: pyyaml; extra == 'full' +Description-Content-Type: text/markdown + +

+ + + + starlette-logo + +

+ +

+ ✨ The little ASGI framework that shines. ✨ +

+ +--- + +[![Build Status](https://github.com/Kludex/starlette/workflows/Test%20Suite/badge.svg)](https://github.com/Kludex/starlette/actions) +[![Package version](https://badge.fury.io/py/starlette.svg)](https://pypi.python.org/pypi/starlette) +[![Supported Python Version](https://img.shields.io/pypi/pyversions/starlette.svg?color=%2334D058)](https://pypi.org/project/starlette) +[![Discord](https://img.shields.io/discord/1051468649518616576?logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/RxKUF5JuHs) + +--- + +**Documentation**:
https://starlette.dev + +**Source Code**: https://github.com/Kludex/starlette + +--- + +# Starlette + +Starlette is a lightweight [ASGI][asgi] framework/toolkit, +which is ideal for building async web services in Python. + +It is production-ready, and gives you the following: + +* A lightweight, low-complexity HTTP web framework. +* WebSocket support. +* In-process background tasks. +* Startup and shutdown events. +* Test client built on `httpx2`. +* CORS, GZip, Static Files, Streaming responses. +* Session and Cookie support. +* 100% test coverage. +* 100% type annotated codebase. +* Few hard dependencies. +* Compatible with `asyncio` and `trio` backends. +* Great overall performance [against independent benchmarks][techempower]. + +## Installation + +```shell +$ pip install starlette +``` + +You'll also want to install an ASGI server, such as [uvicorn](https://uvicorn.dev) or any of the [other ASGI server implementations](https://asgi.readthedocs.io/en/latest/implementations.html#servers). + +```shell +$ pip install uvicorn +``` + +## Example + +```python title="main.py" +from starlette.applications import Starlette +from starlette.responses import JSONResponse +from starlette.routing import Route + + +async def homepage(request): + return JSONResponse({'hello': 'world'}) + +routes = [ + Route("/", endpoint=homepage) +] + +app = Starlette(debug=True, routes=routes) +``` + +Then run the application using Uvicorn: + +```shell +$ uvicorn main:app +``` + +## Dependencies + +Starlette only requires `anyio`, and the following are optional: + +* [`httpx2`][httpx2] - Required if you want to use the `TestClient`. +* [`jinja2`][jinja2] - Required if you want to use `Jinja2Templates`. +* [`opentelemetry-api`][opentelemetry-api] - Required for `OpenTelemetryMiddleware`. +* [`python-multipart`][python-multipart] - Required if you want to support form parsing, with `request.form()`. +* [`itsdangerous`][itsdangerous] - Required for `SessionMiddleware` support. +* [`pyyaml`][pyyaml] - Required for `SchemaGenerator` support. + +You can install all of these with `pip install starlette[full]`. + +## Framework or Toolkit + +Starlette is designed to be used either as a complete framework, or as +an ASGI toolkit. You can use any of its components independently. + +```python +from starlette.responses import PlainTextResponse + + +async def app(scope, receive, send): + assert scope['type'] == 'http' + response = PlainTextResponse('Hello, world!') + await response(scope, receive, send) +``` + +Run the `app` application in `example.py`: + +```shell +$ uvicorn example:app +INFO: Started server process [11509] +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +Run uvicorn with `--reload` to enable auto-reloading on code changes. + +## Modularity + +The modularity that Starlette is designed on promotes building reusable +components that can be shared between any ASGI framework. This should enable +an ecosystem of shared middleware and mountable applications. + +The clean API separation also means it's easier to understand each component +in isolation. + +--- + +

Starlette is BSD licensed code.
Designed & crafted with care.

— ⭐️ —

+ +[asgi]: https://asgi.readthedocs.io/en/latest/ +[httpx2]: https://pypi.org/project/httpx2/ +[jinja2]: https://jinja.palletsprojects.com/ +[opentelemetry-api]: https://opentelemetry.io/docs/languages/python/api/ +[python-multipart]: https://multipart.fastapiexpert.com/ +[itsdangerous]: https://itsdangerous.palletsprojects.com/ +[sqlalchemy]: https://www.sqlalchemy.org +[pyyaml]: https://pyyaml.org/wiki/PyYAMLDocumentation +[techempower]: https://github.com/TechEmpower/FrameworkBenchmarks diff --git a/source/target-vendors/cp312/starlette-1.7.0.dist-info/RECORD b/source/target-vendors/cp312/starlette-1.7.0.dist-info/RECORD new file mode 100644 index 0000000..06c5375 --- /dev/null +++ b/source/target-vendors/cp312/starlette-1.7.0.dist-info/RECORD @@ -0,0 +1,41 @@ +starlette/__init__.py,sha256=lEEx5lwinUby-YNl7w-VFGUcjcibsJ9EuzgJ2W7ocrU,22 +starlette/_exception_handler.py,sha256=8Vm915fWYe9xLo_za5iQ5WyrAvFBu2kEEJc7XreTyOk,2257 +starlette/_utils.py,sha256=NTNRqxu24V4GwVq5gnhQ0oFBlRnrjbywbKahzeiwrw0,4211 +starlette/applications.py,sha256=gYh-C6992lgRukt6Z-Scu6t66XsyG6cuR1x5f5iWkOk,5763 +starlette/authentication.py,sha256=rJx0wvk1xsVifLsyiHV6P_b3oAherBRXW6QkXKw3APM,5341 +starlette/background.py,sha256=D-XpUpFRgUBnZeA1tHgzMOgube6ICVZFlOA4ML0q1T8,1122 +starlette/concurrency.py,sha256=Pp4kXQYO85cWxY7XVNt5lcYvhYHUquyLguCec7hKKoM,1701 +starlette/config.py,sha256=GVwko0pGSusxuQHO35tL_Nt_ApL0fmVYL3IiHHYpLW4,4426 +starlette/convertors.py,sha256=F1rse3AacN9rsfJnTeuDnjbN51r_ouHc3WLyYkjkX_o,2304 +starlette/datastructures.py,sha256=NdxfT1u9a-NIoFGoz257evM14p1sq__46iMp9Xo_-60,23200 +starlette/endpoints.py,sha256=KI-tMTdlqi4rU1eR9hnOptdyGFxc_T1adwOKIgeOBm0,5304 +starlette/exceptions.py,sha256=-0sNmNNLa46g9V6xpsqOqqPwDRJMoe4vEqDAbCVQJTU,1492 +starlette/formparsers.py,sha256=rdPatCDCwT8yj1mDeFOAHknV7O4x3wU0cCZs4GbVaOI,12374 +starlette/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +starlette/requests.py,sha256=hpGwGuSez6CF_mQZNDZ49zt3yS_GOkERBqd9jpvwrEQ,12708 +starlette/responses.py,sha256=5Ypb6Ud-MLloC7Jw4PGw_G4lKuG6ZXZecNOUZzLTJ_c,22090 +starlette/routing.py,sha256=FSFciPQYfJjPSY51exiMDgb76A2V94t2MMTlH5JhGvQ,30472 +starlette/schemas.py,sha256=e9KnCHN1KBV9F8Ml54wC55ItHE5LES5WWXLVEYUgot0,5494 +starlette/staticfiles.py,sha256=UP3kCtjzAs1ZsdluuP7NLEq55ER_FI65LMfKVbrsMBg,9071 +starlette/status.py,sha256=XYcVn-aU522TYJ8BJxl0WUnf6J_3yBTQz7Tqgye8KO4,6430 +starlette/templating.py,sha256=y-ATlQrNDoLMZs5hrHzQBTapECBDr9wAl-bs8Blk5jw,5447 +starlette/testclient.py,sha256=noWneuuRPAkTsqnel3rmF1VOzQimReyJvKLggJ__dhs,21597 +starlette/types.py,sha256=87wTuwF1036QxRse3mL5qOuS9i4a9xaIZMqTjY-lvvo,1041 +starlette/websockets.py,sha256=mflI9LUwlf93_uujI8w-tY-noS-zdREbzaUmeOVDP9c,8518 +starlette/middleware/__init__.py,sha256=BORnOj0n0mL1OWUUs1PLTu1SASmvT8kEEDA6cbuhuPo,1349 +starlette/middleware/authentication.py,sha256=-RSwwJ0pIkrM0v-2Ip8HXVB24xtSp7ObzR3OfTC8wSA,1800 +starlette/middleware/base.py,sha256=5Od_lmyCvfI0jsEDWiQTiHy9DEbAb3tKAPedJ24tils,10500 +starlette/middleware/body_limit.py,sha256=L12EvFC-9ryO_A_2ngdDpt6WaVkwhAtqiuJPhyhqyA8,4609 +starlette/middleware/cors.py,sha256=_OnWebUt2WcPOCFtqs3KsVnGmVzbUlbuUWLcWNJaXE8,7688 +starlette/middleware/errors.py,sha256=E14AZhjybcfRH72ByiaQ7Mdt0AwnYZqiv6zGtC0F0xM,8126 +starlette/middleware/exceptions.py,sha256=7OgSUiBgwHS4VMmpaWlw21uDKNDmOMzLVZrWDLDUqWo,2784 +starlette/middleware/gzip.py,sha256=7ijfBszT4h6syblkcd89ple43WqYBgH-hCycsKuWtms,8805 +starlette/middleware/httpsredirect.py,sha256=fogRKypMx3gKzoHgJpKPIITGdKxFdUCEAqskRHndZBQ,1023 +starlette/middleware/opentelemetry.py,sha256=lAP3BYvOjunvnPBK9GcQaZR7MF6wVmYLbpc75-54hwI,5719 +starlette/middleware/sessions.py,sha256=8YpoNTO02-KWaUvtJdNe7lKExKxYuZD-hIRC3TdtBf4,5175 +starlette/middleware/trustedhost.py,sha256=WkeCKP_loVg5IBpjjMyoWXEnS0URQmon8Iee-dTz46U,2456 +starlette/middleware/wsgi.py,sha256=8_xjdUNjA-DcWY0owaqyiZlGcvml4UMIB18rGxtdNvg,5536 +starlette-1.7.0.dist-info/METADATA,sha256=94cPwZq3wuNkiHBXNAuftH6b2nTSSwbFjKOZSUq_4Bk,6610 +starlette-1.7.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87 +starlette-1.7.0.dist-info/licenses/LICENSE.md,sha256=3LlWd6AiQCQxh-lk-UGEfRmxeCHPmeWvrmhPqzKMGb8,1518 +starlette-1.7.0.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/starlette-1.7.0.dist-info/WHEEL b/source/target-vendors/cp312/starlette-1.7.0.dist-info/WHEEL new file mode 100644 index 0000000..51abe1f --- /dev/null +++ b/source/target-vendors/cp312/starlette-1.7.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.32.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/source/target-vendors/cp312/starlette-1.7.0.dist-info/licenses/LICENSE.md b/source/target-vendors/cp312/starlette-1.7.0.dist-info/licenses/LICENSE.md new file mode 100644 index 0000000..d16a60e --- /dev/null +++ b/source/target-vendors/cp312/starlette-1.7.0.dist-info/licenses/LICENSE.md @@ -0,0 +1,27 @@ +Copyright © 2018, [Encode OSS Ltd](https://www.encode.io/). +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/source/target-vendors/cp312/starlette/__init__.py b/source/target-vendors/cp312/starlette/__init__.py new file mode 100644 index 0000000..14d9d2f --- /dev/null +++ b/source/target-vendors/cp312/starlette/__init__.py @@ -0,0 +1 @@ +__version__ = "1.7.0" diff --git a/source/target-vendors/cp312/starlette/_exception_handler.py b/source/target-vendors/cp312/starlette/_exception_handler.py new file mode 100644 index 0000000..9491f8b --- /dev/null +++ b/source/target-vendors/cp312/starlette/_exception_handler.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from typing import Any + +from starlette._utils import is_async_callable +from starlette.concurrency import run_in_threadpool +from starlette.exceptions import HTTPException +from starlette.requests import Request +from starlette.types import ASGIApp, ExceptionHandler, Message, Receive, Scope, Send +from starlette.websockets import WebSocket + +ExceptionHandlers = dict[Any, ExceptionHandler] +StatusHandlers = dict[int, ExceptionHandler] + + +def _lookup_exception_handler(exc_handlers: ExceptionHandlers, exc: Exception) -> ExceptionHandler | None: + for cls in type(exc).__mro__: + if cls in exc_handlers: + return exc_handlers[cls] + return None + + +def wrap_app_handling_exceptions(app: ASGIApp, conn: Request | WebSocket) -> ASGIApp: + exception_handlers: ExceptionHandlers + status_handlers: StatusHandlers + try: + exception_handlers, status_handlers = conn.scope["starlette.exception_handlers"] + except KeyError: + exception_handlers, status_handlers = {}, {} + + async def wrapped_app(scope: Scope, receive: Receive, send: Send) -> None: + response_started = False + + async def sender(message: Message) -> None: + nonlocal response_started + + if message["type"] == "http.response.start": + response_started = True + await send(message) + + try: + await app(scope, receive, sender) + except Exception as exc: + handler = None + + if isinstance(exc, HTTPException): + handler = status_handlers.get(exc.status_code) + + if handler is None: + handler = _lookup_exception_handler(exception_handlers, exc) + + if handler is None: + raise exc + + if response_started: + raise RuntimeError("Caught handled exception, but response already started.") from exc + + if is_async_callable(handler): + response = await handler(conn, exc) # type: ignore[arg-type] + else: + response = await run_in_threadpool(handler, conn, exc) # type: ignore[arg-type] + if response is not None: + await response(scope, receive, sender) + + return wrapped_app diff --git a/source/target-vendors/cp312/starlette/_utils.py b/source/target-vendors/cp312/starlette/_utils.py new file mode 100644 index 0000000..21bc95a --- /dev/null +++ b/source/target-vendors/cp312/starlette/_utils.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +import functools +import re +import sys +from collections.abc import AsyncGenerator, Awaitable, Callable, Generator +from contextlib import AbstractAsyncContextManager, asynccontextmanager +from dataclasses import dataclass +from ipaddress import AddressValueError, IPv6Address +from typing import Any, Generic, Protocol, TypeVar, overload + +import anyio.abc + +from starlette.types import Scope + +if sys.version_info >= (3, 13): # pragma: no cover + from inspect import iscoroutinefunction + from typing import TypeIs +else: # pragma: no cover + from asyncio import iscoroutinefunction + + from typing_extensions import TypeIs + +if sys.version_info < (3, 11): # pragma: no cover + from exceptiongroup import BaseExceptionGroup + + +T = TypeVar("T") +AwaitableCallable = Callable[..., Awaitable[T]] + +# Reject characters that could make a Host header change the URL path or authority. +_HOST_RE = re.compile( + r"^(?P[a-z0-9._~%!$&'()*+,;=-]+|\[(?:(?P[a-f0-9]*:[a-f0-9.:]+)|" + r"(?-i:v)[a-f0-9]+\.[a-z0-9._~!$&'()*+,;=:-]+)\])(?::(?P[0-9]+))?$", + re.IGNORECASE, +) + + +@dataclass(frozen=True, slots=True) +class ParsedHost: + host: str + port: str | None + + @property + def authority(self) -> str: + return self.host if self.port is None else f"{self.host}:{self.port}" + + @property + def is_valid_port(self) -> bool: + if self.port is None: + return True + port = self.port.lstrip("0") + return len(port) <= 5 and int(port or "0") <= 65535 + + +@overload +def is_async_callable(obj: AwaitableCallable[T]) -> TypeIs[AwaitableCallable[T]]: ... + + +@overload +def is_async_callable(obj: Any) -> TypeIs[AwaitableCallable[Any]]: ... + + +def is_async_callable(obj: Any) -> Any: + while isinstance(obj, functools.partial): + obj = obj.func + + return iscoroutinefunction(obj) or (callable(obj) and iscoroutinefunction(obj.__call__)) + + +T_co = TypeVar("T_co", covariant=True) + + +class AwaitableOrContextManager( + Awaitable[T_co], AbstractAsyncContextManager[T_co], Protocol[T_co] +): ... # pragma: no branch + + +class SupportsAsyncClose(Protocol): + async def close(self) -> None: ... # pragma: no cover + + +SupportsAsyncCloseType = TypeVar("SupportsAsyncCloseType", bound=SupportsAsyncClose, covariant=False) + + +class AwaitableOrContextManagerWrapper(Generic[SupportsAsyncCloseType]): + __slots__ = ("aw", "entered") + + def __init__(self, aw: Awaitable[SupportsAsyncCloseType]) -> None: + self.aw = aw + + def __await__(self) -> Generator[Any, None, SupportsAsyncCloseType]: + return self.aw.__await__() + + async def __aenter__(self) -> SupportsAsyncCloseType: + self.entered = await self.aw + return self.entered + + async def __aexit__(self, *args: Any) -> None | bool: + await self.entered.close() + return None + + +@asynccontextmanager +async def create_collapsing_task_group() -> AsyncGenerator[anyio.abc.TaskGroup, None]: + try: + async with anyio.create_task_group() as tg: + yield tg + except BaseExceptionGroup as excs: + if len(excs.exceptions) != 1: + raise + + exc = excs.exceptions[0] + context = None if exc.__suppress_context__ else exc.__context__ + raise exc from exc.__cause__ or context + + +def parse_host_header(host_header: str | None) -> ParsedHost | None: + """Parse `host_header` into its host and port components. + + The host preserves brackets around IP literals. Invalid headers produce `None`. + """ + if host_header is None: + return None + + match = _HOST_RE.fullmatch(host_header) + if match is None: + return None + + ipv6 = match["ipv6"] + if ipv6 is not None: + try: + IPv6Address(ipv6) + except AddressValueError: + return None + + return ParsedHost(match["host"], match["port"]) + + +def get_route_path(scope: Scope) -> str: + path: str = scope["path"] + root_path = scope.get("root_path", "") + if not root_path: + return path + + if not path.startswith(root_path): + return path + + if path == root_path: + return "" + + if path[len(root_path)] == "/": + return path[len(root_path) :] + + return path diff --git a/source/target-vendors/cp312/starlette/applications.py b/source/target-vendors/cp312/starlette/applications.py new file mode 100644 index 0000000..a28cde4 --- /dev/null +++ b/source/target-vendors/cp312/starlette/applications.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +from collections.abc import Awaitable, Callable, Mapping, Sequence +from typing import Any, ParamSpec, TypeVar + +from starlette.datastructures import State, URLPath +from starlette.middleware import Middleware, _MiddlewareFactory +from starlette.middleware.body_limit import RequestBodyLimitMiddleware +from starlette.middleware.errors import ServerErrorMiddleware +from starlette.middleware.exceptions import ExceptionMiddleware +from starlette.requests import Request +from starlette.responses import Response +from starlette.routing import BaseRoute, Router +from starlette.types import ASGIApp, ExceptionHandler, Lifespan, Receive, Scope, Send + +AppType = TypeVar("AppType", bound="Starlette") +P = ParamSpec("P") + + +class Starlette: + """Creates a Starlette application.""" + + def __init__( + self: AppType, + debug: bool = False, + routes: Sequence[BaseRoute] | None = None, + middleware: Sequence[Middleware] | None = None, + exception_handlers: Mapping[Any, ExceptionHandler] | None = None, + lifespan: Lifespan[AppType] | None = None, + *, + max_body_size: int | None = None, + ) -> None: + """Initializes the application. + + Parameters: + debug: Boolean indicating if debug tracebacks should be returned on errors. + routes: A list of routes to serve incoming HTTP and WebSocket requests. + middleware: A list of middleware to run for every request. A starlette + application will always automatically include two middleware classes. + `ServerErrorMiddleware` is added as the very outermost middleware, to handle + any uncaught errors occurring anywhere in the entire stack. + `ExceptionMiddleware` is added as the very innermost middleware, to deal + with handled exception cases occurring in the routing or endpoints. + exception_handlers: A mapping of either integer status codes, + or exception class types onto callables which handle the exceptions. + Exception handler callables should be of the form + `handler(request, exc) -> response` and may be either standard functions, or + async functions. + lifespan: A lifespan context function, which can be used to perform + startup and shutdown tasks. This is a newer style that replaces the + `on_startup` and `on_shutdown` handlers. Use one or the other, not both. + max_body_size: Non-negative maximum total size in bytes of an HTTP request + body. The default, `None`, does not limit request body size. + """ + self.debug = debug + self.state = State() + self.router = Router(routes, lifespan=lifespan) + self.max_body_size = max_body_size + self.exception_handlers = {} if exception_handlers is None else dict(exception_handlers) + self.user_middleware = [] if middleware is None else list(middleware) + self.middleware_stack: ASGIApp | None = None + + def build_middleware_stack(self) -> ASGIApp: + debug = self.debug + error_handler = None + exception_handlers: dict[Any, ExceptionHandler] = {} + + for key, value in self.exception_handlers.items(): + if key in (500, Exception): + error_handler = value + else: + exception_handlers[key] = value + + middleware = [Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)] + if self.max_body_size is not None: + middleware.append(Middleware(RequestBodyLimitMiddleware, max_body_size=self.max_body_size)) + middleware += self.user_middleware + middleware.append(Middleware(ExceptionMiddleware, handlers=exception_handlers, debug=debug)) + + app = self.router + for cls, args, kwargs in reversed(middleware): + app = cls(app, *args, **kwargs) + return app + + @property + def routes(self) -> list[BaseRoute]: + return self.router.routes + + def url_path_for(self, name: str, /, **path_params: Any) -> URLPath: + return self.router.url_path_for(name, **path_params) + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + scope["app"] = self + if self.middleware_stack is None: + self.middleware_stack = self.build_middleware_stack() + await self.middleware_stack(scope, receive, send) + + def mount(self, path: str, app: ASGIApp, name: str | None = None) -> None: + self.router.mount(path, app=app, name=name) # pragma: no cover + + def host(self, host: str, app: ASGIApp, name: str | None = None) -> None: + self.router.host(host, app=app, name=name) # pragma: no cover + + def add_middleware(self, middleware_class: _MiddlewareFactory[P], *args: P.args, **kwargs: P.kwargs) -> None: + if self.middleware_stack is not None: # pragma: no cover + raise RuntimeError("Cannot add middleware after an application has started") + self.user_middleware.insert(0, Middleware(middleware_class, *args, **kwargs)) + + def add_exception_handler( + self, + exc_class_or_status_code: int | type[Exception], + handler: ExceptionHandler, + ) -> None: # pragma: no cover + self.exception_handlers[exc_class_or_status_code] = handler + + def add_route( + self, + path: str, + route: Callable[[Request], Awaitable[Response] | Response], + methods: list[str] | None = None, + name: str | None = None, + include_in_schema: bool = True, + ) -> None: # pragma: no cover + self.router.add_route(path, route, methods=methods, name=name, include_in_schema=include_in_schema) diff --git a/source/target-vendors/cp312/starlette/authentication.py b/source/target-vendors/cp312/starlette/authentication.py new file mode 100644 index 0000000..317ef01 --- /dev/null +++ b/source/target-vendors/cp312/starlette/authentication.py @@ -0,0 +1,157 @@ +from __future__ import annotations + +import functools +import inspect +from collections.abc import Callable, Sequence +from typing import Any, ParamSpec +from urllib.parse import urlencode + +from starlette._utils import is_async_callable +from starlette.exceptions import HTTPException +from starlette.requests import HTTPConnection, Request +from starlette.responses import RedirectResponse +from starlette.websockets import WebSocket + +_P = ParamSpec("_P") + + +def has_required_scope(conn: HTTPConnection, scopes: Sequence[str]) -> bool: + for scope in scopes: + if scope not in conn.auth.scopes: + return False + return True + + +def requires( + scopes: str | Sequence[str], + status_code: int = 403, + redirect: str | None = None, +) -> Callable[[Callable[_P, Any]], Callable[_P, Any]]: + scopes_list = [scopes] if isinstance(scopes, str) else list(scopes) + + def decorator( + func: Callable[_P, Any], + ) -> Callable[_P, Any]: + sig = inspect.signature(func) + for idx, parameter in enumerate(sig.parameters.values()): + if parameter.name == "request" or parameter.name == "websocket": + type_ = parameter.name + break + else: + raise Exception(f'No "request" or "websocket" argument on function "{func}"') + + if type_ == "websocket": + # Handle websocket functions. (Always async) + @functools.wraps(func) + async def websocket_wrapper(*args: _P.args, **kwargs: _P.kwargs) -> None: + websocket = kwargs.get("websocket", args[idx] if idx < len(args) else None) + assert isinstance(websocket, WebSocket), ( + "Parameter with name 'websocket' is required to be of type 'WebSocket'" + f" not '{type(websocket).__name__}'" + ) + + if not has_required_scope(websocket, scopes_list): + await websocket.close() + else: + await func(*args, **kwargs) + + return websocket_wrapper + + elif is_async_callable(func): + # Handle async request/response functions. + @functools.wraps(func) + async def async_wrapper(*args: _P.args, **kwargs: _P.kwargs) -> Any: + request = kwargs.get("request", args[idx] if idx < len(args) else None) + assert isinstance(request, Request), ( + f"Parameter with name 'request' is required to be of type 'Request' not '{type(request).__name__}'" + ) + + if not has_required_scope(request, scopes_list): + if redirect is not None: + orig_request_qparam = urlencode({"next": str(request.url)}) + next_url = f"{request.url_for(redirect)}?{orig_request_qparam}" + return RedirectResponse(url=next_url, status_code=303) + raise HTTPException(status_code=status_code) + return await func(*args, **kwargs) + + return async_wrapper + + else: + # Handle sync request/response functions. + @functools.wraps(func) + def sync_wrapper(*args: _P.args, **kwargs: _P.kwargs) -> Any: + request = kwargs.get("request", args[idx] if idx < len(args) else None) + assert isinstance(request, Request), ( + f"Parameter with name 'request' is required to be of type 'Request' not '{type(request).__name__}'" + ) + + if not has_required_scope(request, scopes_list): + if redirect is not None: + orig_request_qparam = urlencode({"next": str(request.url)}) + next_url = f"{request.url_for(redirect)}?{orig_request_qparam}" + return RedirectResponse(url=next_url, status_code=303) + raise HTTPException(status_code=status_code) + return func(*args, **kwargs) + + return sync_wrapper + + return decorator + + +class AuthenticationError(Exception): + pass + + +class AuthenticationBackend: + async def authenticate(self, conn: HTTPConnection) -> tuple[AuthCredentials, BaseUser] | None: + raise NotImplementedError() # pragma: no cover + + +class AuthCredentials: + def __init__(self, scopes: Sequence[str] | None = None): + self.scopes = [] if scopes is None else list(scopes) + + +class BaseUser: + @property + def is_authenticated(self) -> bool: + raise NotImplementedError() # pragma: no cover + + @property + def display_name(self) -> str: + raise NotImplementedError() # pragma: no cover + + @property + def identity(self) -> str: + raise NotImplementedError() # pragma: no cover + + +class SimpleUser(BaseUser): + def __init__(self, username: str) -> None: + self.username = username + + @property + def is_authenticated(self) -> bool: + return True + + @property + def display_name(self) -> str: + return self.username + + @property + def identity(self) -> str: + return self.username + + +class UnauthenticatedUser(BaseUser): + @property + def is_authenticated(self) -> bool: + return False + + @property + def display_name(self) -> str: + return "" + + @property + def identity(self) -> str: + return "" diff --git a/source/target-vendors/cp312/starlette/background.py b/source/target-vendors/cp312/starlette/background.py new file mode 100644 index 0000000..55135ee --- /dev/null +++ b/source/target-vendors/cp312/starlette/background.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from collections.abc import Callable, Sequence +from typing import Any, ParamSpec + +from starlette._utils import is_async_callable +from starlette.concurrency import run_in_threadpool + +P = ParamSpec("P") + + +class BackgroundTask: + def __init__(self, func: Callable[P, Any], *args: P.args, **kwargs: P.kwargs) -> None: + self.func = func + self.args = args + self.kwargs = kwargs + self.is_async = is_async_callable(func) + + async def __call__(self) -> None: + if self.is_async: + await self.func(*self.args, **self.kwargs) + else: + await run_in_threadpool(self.func, *self.args, **self.kwargs) + + +class BackgroundTasks(BackgroundTask): + def __init__(self, tasks: Sequence[BackgroundTask] | None = None): + self.tasks = list(tasks) if tasks else [] + + def add_task(self, func: Callable[P, Any], *args: P.args, **kwargs: P.kwargs) -> None: + task = BackgroundTask(func, *args, **kwargs) + self.tasks.append(task) + + async def __call__(self) -> None: + for task in self.tasks: + await task() diff --git a/source/target-vendors/cp312/starlette/concurrency.py b/source/target-vendors/cp312/starlette/concurrency.py new file mode 100644 index 0000000..a2c55be --- /dev/null +++ b/source/target-vendors/cp312/starlette/concurrency.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import functools +import warnings +from collections.abc import AsyncIterator, Callable, Coroutine, Iterable, Iterator +from typing import ParamSpec, TypeVar + +import anyio.to_thread + +from starlette.exceptions import StarletteDeprecationWarning + +P = ParamSpec("P") +T = TypeVar("T") + + +async def run_until_first_complete(*args: tuple[Callable, dict]) -> None: # type: ignore[type-arg] + warnings.warn( + "run_until_first_complete is deprecated and will be removed in a future version.", + StarletteDeprecationWarning, + ) + + async with anyio.create_task_group() as task_group: + + async def run(func: Callable[[], Coroutine]) -> None: # type: ignore[type-arg] + await func() + task_group.cancel_scope.cancel() + + for func, kwargs in args: + task_group.start_soon(run, functools.partial(func, **kwargs)) + + +async def run_in_threadpool(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: + func = functools.partial(func, *args, **kwargs) + return await anyio.to_thread.run_sync(func) + + +class _StopIteration(Exception): + pass + + +def _next(iterator: Iterator[T]) -> T: + # We can't raise `StopIteration` from within the threadpool iterator + # and catch it outside that context, so we coerce them into a different + # exception type. + try: + return next(iterator) + except StopIteration: + raise _StopIteration + + +async def iterate_in_threadpool( + iterator: Iterable[T], +) -> AsyncIterator[T]: + as_iterator = iter(iterator) + while True: + try: + yield await anyio.to_thread.run_sync(_next, as_iterator) + except _StopIteration: + break diff --git a/source/target-vendors/cp312/starlette/config.py b/source/target-vendors/cp312/starlette/config.py new file mode 100644 index 0000000..56c25ca --- /dev/null +++ b/source/target-vendors/cp312/starlette/config.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +import os +import warnings +from collections.abc import Callable, Iterator, Mapping, MutableMapping +from pathlib import Path +from typing import Any, TypeVar, overload + + +class undefined: + pass + + +class EnvironError(Exception): + pass + + +class Environ(MutableMapping[str, str]): + def __init__(self, environ: MutableMapping[str, str] = os.environ): + self._environ = environ + self._has_been_read: set[str] = set() + + def __getitem__(self, key: str) -> str: + self._has_been_read.add(key) + return self._environ.__getitem__(key) + + def __setitem__(self, key: str, value: str) -> None: + if key in self._has_been_read: + raise EnvironError(f"Attempting to set environ['{key}'], but the value has already been read.") + self._environ.__setitem__(key, value) + + def __delitem__(self, key: str) -> None: + if key in self._has_been_read: + raise EnvironError(f"Attempting to delete environ['{key}'], but the value has already been read.") + self._environ.__delitem__(key) + + def __iter__(self) -> Iterator[str]: + return iter(self._environ) + + def __len__(self) -> int: + return len(self._environ) + + +environ = Environ() + +T = TypeVar("T") + + +class Config: + def __init__( + self, + env_file: str | Path | None = None, + environ: Mapping[str, str] = environ, + env_prefix: str = "", + encoding: str = "utf-8", + ) -> None: + self.environ = environ + self.env_prefix = env_prefix + self.file_values: dict[str, str] = {} + if env_file is not None: + if not os.path.isfile(env_file): + warnings.warn(f"Config file '{env_file}' not found.") + else: + self.file_values = self._read_file(env_file, encoding) + + @overload + def __call__(self, key: str, *, default: None) -> str | None: ... + + @overload + def __call__(self, key: str, cast: type[T], default: T = ...) -> T: ... + + @overload + def __call__(self, key: str, cast: type[str] = ..., default: str = ...) -> str: ... + + @overload + def __call__( + self, + key: str, + cast: Callable[[Any], T] = ..., + default: Any = ..., + ) -> T: ... + + @overload + def __call__(self, key: str, cast: type[str] = ..., default: T = ...) -> T | str: ... + + def __call__( + self, + key: str, + cast: Callable[[Any], Any] | None = None, + default: Any = undefined, + ) -> Any: + return self.get(key, cast, default) + + def get( + self, + key: str, + cast: Callable[[Any], Any] | None = None, + default: Any = undefined, + ) -> Any: + key = self.env_prefix + key + if key in self.environ: + value = self.environ[key] + return self._perform_cast(key, value, cast) + if key in self.file_values: + value = self.file_values[key] + return self._perform_cast(key, value, cast) + if default is not undefined: + return self._perform_cast(key, default, cast) + raise KeyError(f"Config '{key}' is missing, and has no default.") + + def _read_file(self, file_name: str | Path, encoding: str) -> dict[str, str]: + file_values: dict[str, str] = {} + with open(file_name, encoding=encoding) as input_file: + for line in input_file.readlines(): + line = line.strip() + if "=" in line and not line.startswith("#"): + key, value = line.split("=", 1) + key = key.strip() + value = value.strip().strip("\"'") + file_values[key] = value + return file_values + + def _perform_cast( + self, + key: str, + value: Any, + cast: Callable[[Any], Any] | None = None, + ) -> Any: + if cast is None or value is None: + return value + elif cast is bool and isinstance(value, str): + mapping = {"true": True, "1": True, "false": False, "0": False} + value = value.lower() + if value not in mapping: + raise ValueError(f"Config '{key}' has value '{value}'. Not a valid bool.") + return mapping[value] + try: + return cast(value) + except (TypeError, ValueError): + raise ValueError(f"Config '{key}' has value '{value}'. Not a valid {cast.__name__}.") diff --git a/source/target-vendors/cp312/starlette/convertors.py b/source/target-vendors/cp312/starlette/convertors.py new file mode 100644 index 0000000..72b1cf9 --- /dev/null +++ b/source/target-vendors/cp312/starlette/convertors.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +import math +import uuid +from typing import Any, ClassVar, Generic, TypeVar + +T = TypeVar("T") + + +class Convertor(Generic[T]): + regex: ClassVar[str] = "" + + def convert(self, value: str) -> T: + raise NotImplementedError() # pragma: no cover + + def to_string(self, value: T) -> str: + raise NotImplementedError() # pragma: no cover + + +class StringConvertor(Convertor[str]): + regex = "[^/]+" + + def convert(self, value: str) -> str: + return value + + def to_string(self, value: str) -> str: + value = str(value) + assert "/" not in value, "May not contain path separators" + assert value, "Must not be empty" + return value + + +class PathConvertor(Convertor[str]): + regex = ".*" + + def convert(self, value: str) -> str: + return str(value) + + def to_string(self, value: str) -> str: + return str(value) + + +class IntegerConvertor(Convertor[int]): + regex = "[0-9]+" + + def convert(self, value: str) -> int: + return int(value) + + def to_string(self, value: int) -> str: + value = int(value) + assert value >= 0, "Negative integers are not supported" + return str(value) + + +class FloatConvertor(Convertor[float]): + regex = r"[0-9]+(\.[0-9]+)?" + + def convert(self, value: str) -> float: + return float(value) + + def to_string(self, value: float) -> str: + value = float(value) + assert value >= 0.0, "Negative floats are not supported" + assert not math.isnan(value), "NaN values are not supported" + assert not math.isinf(value), "Infinite values are not supported" + return ("%0.20f" % value).rstrip("0").rstrip(".") + + +class UUIDConvertor(Convertor[uuid.UUID]): + regex = "[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}" + + def convert(self, value: str) -> uuid.UUID: + return uuid.UUID(value) + + def to_string(self, value: uuid.UUID) -> str: + return str(value) + + +CONVERTOR_TYPES: dict[str, Convertor[Any]] = { + "str": StringConvertor(), + "path": PathConvertor(), + "int": IntegerConvertor(), + "float": FloatConvertor(), + "uuid": UUIDConvertor(), +} + + +def register_url_convertor(key: str, convertor: Convertor[Any]) -> None: + CONVERTOR_TYPES[key] = convertor diff --git a/source/target-vendors/cp312/starlette/datastructures.py b/source/target-vendors/cp312/starlette/datastructures.py new file mode 100644 index 0000000..d4ec95e --- /dev/null +++ b/source/target-vendors/cp312/starlette/datastructures.py @@ -0,0 +1,704 @@ +from __future__ import annotations + +from collections.abc import ItemsView, Iterable, Iterator, KeysView, Mapping, MutableMapping, Sequence, ValuesView +from shlex import shlex +from typing import Any, BinaryIO, Literal, NamedTuple, TypeVar, cast +from urllib.parse import SplitResult, parse_qsl, urlencode, urlsplit + +from starlette._utils import parse_host_header +from starlette.concurrency import run_in_threadpool +from starlette.types import Scope + + +class Address(NamedTuple): + host: str + port: int + + +_KeyType = TypeVar("_KeyType") +# Mapping keys are invariant but their values are covariant since +# you can only read them +# that is, you can't do `Mapping[str, Animal]()["fido"] = Dog()` +_CovariantValueType = TypeVar("_CovariantValueType", covariant=True) + + +class URL: + def __init__( + self, + url: str = "", + scope: Scope | None = None, + **components: Any, + ) -> None: + if scope is not None: + assert not url, 'Cannot set both "url" and "scope".' + assert not components, 'Cannot set both "scope" and "**components".' + scheme = scope.get("scheme", "http") + server = scope.get("server", None) + path = scope["path"] + query_string = scope.get("query_string", b"") + + host_header = None + for key, value in scope["headers"]: + if key == b"host": + host_header = value.decode("latin-1") + break + + parsed_host = parse_host_header(host_header) + if parsed_host is not None and parsed_host.is_valid_port: + netloc = parsed_host.authority + elif server is not None: + host, port = server + if ":" in host and not host.startswith("["): + host = f"[{host}]" + default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}[scheme] + netloc = host if port == default_port else f"{host}:{port}" + else: + netloc = None + + query = query_string.decode() + if netloc is not None: + url = SplitResult(scheme=scheme, netloc=netloc, path=path, query=query, fragment="").geturl() + else: + url = f"{path}?{query}" if query else path + elif components: + assert not url, 'Cannot set both "url" and "**components".' + url = URL("").replace(**components).components.geturl() + + self._url = url + + @property + def components(self) -> SplitResult: + if not hasattr(self, "_components"): + self._components = urlsplit(self._url) + return self._components + + @property + def scheme(self) -> str: + return self.components.scheme + + @property + def netloc(self) -> str: + return self.components.netloc + + @property + def path(self) -> str: + return self.components.path + + @property + def query(self) -> str: + return self.components.query + + @property + def fragment(self) -> str: + return self.components.fragment + + @property + def username(self) -> None | str: + return self.components.username + + @property + def password(self) -> None | str: + return self.components.password + + @property + def hostname(self) -> None | str: + return self.components.hostname + + @property + def port(self) -> int | None: + return self.components.port + + @property + def is_secure(self) -> bool: + return self.scheme in ("https", "wss") + + def replace(self, **kwargs: Any) -> URL: + if "username" in kwargs or "password" in kwargs or "hostname" in kwargs or "port" in kwargs: + hostname = kwargs.pop("hostname", None) + port = kwargs.pop("port", self.port) + username = kwargs.pop("username", self.username) + password = kwargs.pop("password", self.password) + + if hostname is None: + netloc = self.netloc + _, _, hostname = netloc.rpartition("@") + + if hostname and hostname[-1] != "]": + hostname = hostname.rsplit(":", 1)[0] + + netloc = hostname + if port is not None: + netloc += f":{port}" + if username is not None: + userpass = username + if password is not None: + userpass += f":{password}" + netloc = f"{userpass}@{netloc}" + + kwargs["netloc"] = netloc + + components = self.components._replace(**kwargs) + return self.__class__(components.geturl()) + + def include_query_params(self, **kwargs: Any) -> URL: + params = MultiDict(parse_qsl(self.query, keep_blank_values=True)) + params.update({str(key): str(value) for key, value in kwargs.items()}) + query = urlencode(params.multi_items()) + return self.replace(query=query) + + def replace_query_params(self, **kwargs: Any) -> URL: + query = urlencode([(str(key), str(value)) for key, value in kwargs.items()]) + return self.replace(query=query) + + def remove_query_params(self, keys: str | Sequence[str]) -> URL: + if isinstance(keys, str): + keys = [keys] + params = MultiDict(parse_qsl(self.query, keep_blank_values=True)) + for key in keys: + params.pop(key, None) + query = urlencode(params.multi_items()) + return self.replace(query=query) + + def __eq__(self, other: Any) -> bool: + return str(self) == str(other) + + def __str__(self) -> str: + return self._url + + def __repr__(self) -> str: + url = str(self) + if self.password: + url = str(self.replace(password="********")) + return f"{self.__class__.__name__}({repr(url)})" + + +class URLPath(str): + """ + A URL path string that may also hold an associated protocol and/or host. + Used by the routing to return `url_path_for` matches. + """ + + def __new__(cls, path: str, protocol: Literal["http", "websocket", ""] = "", host: str = "") -> URLPath: + assert protocol in ("http", "websocket", "") + return str.__new__(cls, path) + + def __init__(self, path: str, protocol: Literal["http", "websocket", ""] = "", host: str = "") -> None: + self.protocol = protocol + self.host = host + + def make_absolute_url(self, base_url: str | URL) -> URL: + if isinstance(base_url, str): + base_url = URL(base_url) + if self.protocol: + scheme = { + "http": {True: "https", False: "http"}, + "websocket": {True: "wss", False: "ws"}, + }[self.protocol][base_url.is_secure] + else: + scheme = base_url.scheme + + netloc = self.host or base_url.netloc + path = base_url.path.rstrip("/") + str(self) + return URL(scheme=scheme, netloc=netloc, path=path) + + +class Secret: + """ + Holds a string value that should not be revealed in tracebacks etc. + You should cast the value to `str` at the point it is required. + """ + + def __init__(self, value: str): + self._value = value + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + return f"{class_name}('**********')" + + def __str__(self) -> str: + return self._value + + def __bool__(self) -> bool: + return bool(self._value) + + +class CommaSeparatedStrings(Sequence[str]): + def __init__(self, value: str | Sequence[str]): + if isinstance(value, str): + splitter = shlex(value, posix=True) + splitter.whitespace = "," + splitter.whitespace_split = True + self._items = [item.strip() for item in splitter] + else: + self._items = list(value) + + def __len__(self) -> int: + return len(self._items) + + def __getitem__(self, index: int | slice) -> Any: + return self._items[index] + + def __iter__(self) -> Iterator[str]: + return iter(self._items) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + items = [item for item in self] + return f"{class_name}({items!r})" + + def __str__(self) -> str: + return ", ".join(repr(item) for item in self) + + +class ImmutableMultiDict(Mapping[_KeyType, _CovariantValueType]): + _dict: dict[_KeyType, _CovariantValueType] + + def __init__( + self, + *args: ImmutableMultiDict[_KeyType, _CovariantValueType] + | Mapping[_KeyType, _CovariantValueType] + | Iterable[tuple[_KeyType, _CovariantValueType]], + **kwargs: Any, + ) -> None: + assert len(args) < 2, "Too many arguments." + + value: Any = args[0] if args else [] + if kwargs: + value = ImmutableMultiDict(value).multi_items() + ImmutableMultiDict(kwargs).multi_items() + + if not value: + _items: list[tuple[Any, Any]] = [] + elif hasattr(value, "multi_items"): + value = cast(ImmutableMultiDict[_KeyType, _CovariantValueType], value) + _items = list(value.multi_items()) + elif hasattr(value, "items"): + value = cast(Mapping[_KeyType, _CovariantValueType], value) + _items = list(value.items()) + else: + value = cast("list[tuple[Any, Any]]", value) + _items = list(value) + + self._dict = {k: v for k, v in _items} + self._list = _items + + def getlist(self, key: Any) -> list[_CovariantValueType]: + return [item_value for item_key, item_value in self._list if item_key == key] + + def keys(self) -> KeysView[_KeyType]: + return self._dict.keys() + + def values(self) -> ValuesView[_CovariantValueType]: + return self._dict.values() + + def items(self) -> ItemsView[_KeyType, _CovariantValueType]: + return self._dict.items() + + def multi_items(self) -> list[tuple[_KeyType, _CovariantValueType]]: + return list(self._list) + + def __getitem__(self, key: _KeyType) -> _CovariantValueType: + return self._dict[key] + + def __contains__(self, key: Any) -> bool: + return key in self._dict + + def __iter__(self) -> Iterator[_KeyType]: + return iter(self.keys()) + + def __len__(self) -> int: + return len(self._dict) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, self.__class__): + return False + return sorted(self._list) == sorted(other._list) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + items = self.multi_items() + return f"{class_name}({items!r})" + + +class MultiDict(ImmutableMultiDict[Any, Any]): + def __setitem__(self, key: Any, value: Any) -> None: + self.setlist(key, [value]) + + def __delitem__(self, key: Any) -> None: + self._list = [(k, v) for k, v in self._list if k != key] + del self._dict[key] + + def pop(self, key: Any, default: Any = None) -> Any: + self._list = [(k, v) for k, v in self._list if k != key] + return self._dict.pop(key, default) + + def popitem(self) -> tuple[Any, Any]: + key, value = self._dict.popitem() + self._list = [(k, v) for k, v in self._list if k != key] + return key, value + + def poplist(self, key: Any) -> list[Any]: + values = [v for k, v in self._list if k == key] + self.pop(key) + return values + + def clear(self) -> None: + self._dict.clear() + self._list.clear() + + def setdefault(self, key: Any, default: Any = None) -> Any: + if key not in self: + self._dict[key] = default + self._list.append((key, default)) + + return self[key] + + def setlist(self, key: Any, values: list[Any]) -> None: + if not values: + self.pop(key, None) + else: + existing_items = [(k, v) for (k, v) in self._list if k != key] + self._list = existing_items + [(key, value) for value in values] + self._dict[key] = values[-1] + + def append(self, key: Any, value: Any) -> None: + self._list.append((key, value)) + self._dict[key] = value + + def update( + self, + *args: MultiDict | Mapping[Any, Any] | list[tuple[Any, Any]], + **kwargs: Any, + ) -> None: + value = MultiDict(*args, **kwargs) + existing_items = [(k, v) for (k, v) in self._list if k not in value.keys()] + self._list = existing_items + value.multi_items() + self._dict.update(value) + + +class QueryParams(ImmutableMultiDict[str, str]): + """ + An immutable multidict. + """ + + def __init__( + self, + *args: ImmutableMultiDict[Any, Any] | Mapping[Any, Any] | list[tuple[Any, Any]] | str | bytes, + **kwargs: Any, + ) -> None: + assert len(args) < 2, "Too many arguments." + + value = args[0] if args else [] + + if isinstance(value, str): + super().__init__(parse_qsl(value, keep_blank_values=True), **kwargs) + elif isinstance(value, bytes): + super().__init__(parse_qsl(value.decode("latin-1"), keep_blank_values=True), **kwargs) + else: + super().__init__(*args, **kwargs) # type: ignore[arg-type] + self._list = [(str(k), str(v)) for k, v in self._list] + self._dict = {str(k): str(v) for k, v in self._dict.items()} + + def __str__(self) -> str: + return urlencode(self._list) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + query_string = str(self) + return f"{class_name}({query_string!r})" + + +class UploadFile: + """ + An uploaded file included as part of the request data. + """ + + def __init__( + self, + file: BinaryIO, + *, + size: int | None = None, + filename: str | None = None, + headers: Headers | None = None, + ) -> None: + self.filename = filename + self.file = file + self.size = size + self.headers = headers or Headers() + + # Capture max size from SpooledTemporaryFile if one is provided. This slightly speeds up future checks. + # Note 0 means unlimited mirroring SpooledTemporaryFile's __init__ + self._max_mem_size = getattr(self.file, "_max_size", 0) + + @property + def content_type(self) -> str | None: + return self.headers.get("content-type", None) + + @property + def _in_memory(self) -> bool: + # check for SpooledTemporaryFile._rolled + rolled_to_disk = getattr(self.file, "_rolled", True) + return not rolled_to_disk + + def _will_roll(self, size_to_add: int) -> bool: + # If we're not in_memory then we will always roll + if not self._in_memory: + return True + + # Check for SpooledTemporaryFile._max_size + future_size = self.file.tell() + size_to_add + return bool(future_size > self._max_mem_size) if self._max_mem_size else False + + async def write(self, data: bytes) -> None: + new_data_len = len(data) + if self.size is not None: + self.size += new_data_len + + if self._will_roll(new_data_len): + await run_in_threadpool(self.file.write, data) + else: + self.file.write(data) + + async def read(self, size: int = -1) -> bytes: + if self._in_memory: + return self.file.read(size) + return await run_in_threadpool(self.file.read, size) + + async def seek(self, offset: int) -> None: + if self._in_memory: + self.file.seek(offset) + else: + await run_in_threadpool(self.file.seek, offset) + + async def close(self) -> None: + if self._in_memory: + self.file.close() + else: + await run_in_threadpool(self.file.close) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(filename={self.filename!r}, size={self.size!r}, headers={self.headers!r})" + + +class FormData(ImmutableMultiDict[str, UploadFile | str]): + """ + An immutable multidict, containing both file uploads and text input. + """ + + def __init__( + self, + *args: FormData | Mapping[str, str | UploadFile] | list[tuple[str, str | UploadFile]], + **kwargs: str | UploadFile, + ) -> None: + super().__init__(*args, **kwargs) + + async def close(self) -> None: + for key, value in self.multi_items(): + if isinstance(value, UploadFile): + await value.close() + + +class Headers(Mapping[str, str]): + """ + An immutable, case-insensitive multidict. + """ + + def __init__( + self, + headers: Mapping[str, str] | None = None, + raw: list[tuple[bytes, bytes]] | None = None, + scope: MutableMapping[str, Any] | None = None, + ) -> None: + self._list: list[tuple[bytes, bytes]] = [] + if headers is not None: + assert raw is None, 'Cannot set both "headers" and "raw".' + assert scope is None, 'Cannot set both "headers" and "scope".' + self._list = [(key.lower().encode("latin-1"), value.encode("latin-1")) for key, value in headers.items()] + elif raw is not None: + assert scope is None, 'Cannot set both "raw" and "scope".' + self._list = raw + elif scope is not None: + # scope["headers"] isn't necessarily a list + # it might be a tuple or other iterable + self._list = scope["headers"] = list(scope["headers"]) + + @property + def raw(self) -> list[tuple[bytes, bytes]]: + return list(self._list) + + def keys(self) -> list[str]: # type: ignore[override] + return [key.decode("latin-1") for key, value in self._list] + + def values(self) -> list[str]: # type: ignore[override] + return [value.decode("latin-1") for key, value in self._list] + + def items(self) -> list[tuple[str, str]]: # type: ignore[override] + return [(key.decode("latin-1"), value.decode("latin-1")) for key, value in self._list] + + def getlist(self, key: str) -> list[str]: + get_header_key = key.lower().encode("latin-1") + return [item_value.decode("latin-1") for item_key, item_value in self._list if item_key == get_header_key] + + def mutablecopy(self) -> MutableHeaders: + return MutableHeaders(raw=self._list[:]) + + def __getitem__(self, key: str) -> str: + get_header_key = key.lower().encode("latin-1") + for header_key, header_value in self._list: + if header_key == get_header_key: + return header_value.decode("latin-1") + raise KeyError(key) + + def __contains__(self, key: Any) -> bool: + get_header_key = key.lower().encode("latin-1") + for header_key, header_value in self._list: + if header_key == get_header_key: + return True + return False + + def __iter__(self) -> Iterator[Any]: + return iter(self.keys()) + + def __len__(self) -> int: + return len(self._list) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, Headers): + return False + return sorted(self._list) == sorted(other._list) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + as_dict = dict(self.items()) + if len(as_dict) == len(self): + return f"{class_name}({as_dict!r})" + return f"{class_name}(raw={self.raw!r})" + + +class MutableHeaders(Headers): + def __setitem__(self, key: str, value: str) -> None: + """ + Set the header `key` to `value`, removing any duplicate entries. + Retains insertion order. + """ + set_key = key.lower().encode("latin-1") + set_value = value.encode("latin-1") + + found_indexes: list[int] = [] + for idx, (item_key, item_value) in enumerate(self._list): + if item_key == set_key: + found_indexes.append(idx) + + for idx in reversed(found_indexes[1:]): + del self._list[idx] + + if found_indexes: + idx = found_indexes[0] + self._list[idx] = (set_key, set_value) + else: + self._list.append((set_key, set_value)) + + def __delitem__(self, key: str) -> None: + """ + Remove the header `key`. + """ + del_key = key.lower().encode("latin-1") + + pop_indexes: list[int] = [] + for idx, (item_key, item_value) in enumerate(self._list): + if item_key == del_key: + pop_indexes.append(idx) + + for idx in reversed(pop_indexes): + del self._list[idx] + + def __ior__(self, other: Mapping[str, str]) -> MutableHeaders: + if not isinstance(other, Mapping): + raise TypeError(f"Expected a mapping but got {other.__class__.__name__}") + self.update(other) + return self + + def __or__(self, other: Mapping[str, str]) -> MutableHeaders: + if not isinstance(other, Mapping): + raise TypeError(f"Expected a mapping but got {other.__class__.__name__}") + new = self.mutablecopy() + new.update(other) + return new + + @property + def raw(self) -> list[tuple[bytes, bytes]]: + return self._list + + def setdefault(self, key: str, value: str) -> str: + """ + If the header `key` does not exist, then set it to `value`. + Returns the header value. + """ + set_key = key.lower().encode("latin-1") + set_value = value.encode("latin-1") + + for idx, (item_key, item_value) in enumerate(self._list): + if item_key == set_key: + return item_value.decode("latin-1") + self._list.append((set_key, set_value)) + return value + + def update(self, other: Mapping[str, str]) -> None: + for key, val in other.items(): + self[key] = val + + def append(self, key: str, value: str) -> None: + """ + Append a header, preserving any duplicate entries. + """ + append_key = key.lower().encode("latin-1") + append_value = value.encode("latin-1") + self._list.append((append_key, append_value)) + + def add_vary_header(self, vary: str) -> None: + existing = self.get("vary") + if existing is not None: + vary = ", ".join([existing, vary]) + self["vary"] = vary + + +class State: + """ + An object that can be used to store arbitrary state. + + Used for `request.state` and `app.state`. + """ + + _state: dict[str, Any] + + def __init__(self, state: dict[str, Any] | None = None): + if state is None: + state = {} + super().__setattr__("_state", state) + + def __setattr__(self, key: Any, value: Any) -> None: + self._state[key] = value + + def __getattr__(self, key: Any) -> Any: + try: + return self._state[key] + except KeyError: + message = "'{}' object has no attribute '{}'" + raise AttributeError(message.format(self.__class__.__name__, key)) + + def __delattr__(self, key: Any) -> None: + del self._state[key] + + def __getitem__(self, key: str) -> Any: + return self._state[key] + + def __setitem__(self, key: str, value: Any) -> None: + self._state[key] = value + + def __delitem__(self, key: str) -> None: + del self._state[key] + + def __iter__(self) -> Iterator[str]: + return iter(self._state) + + def __len__(self) -> int: + return len(self._state) diff --git a/source/target-vendors/cp312/starlette/endpoints.py b/source/target-vendors/cp312/starlette/endpoints.py new file mode 100644 index 0000000..9b2032c --- /dev/null +++ b/source/target-vendors/cp312/starlette/endpoints.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import json +from collections.abc import Callable, Generator +from typing import Any, Literal + +from starlette import status +from starlette._utils import is_async_callable +from starlette.concurrency import run_in_threadpool +from starlette.exceptions import HTTPException +from starlette.requests import Request +from starlette.responses import PlainTextResponse, Response +from starlette.types import Message, Receive, Scope, Send +from starlette.websockets import WebSocket + + +class HTTPEndpoint: + def __init__(self, scope: Scope, receive: Receive, send: Send) -> None: + assert scope["type"] == "http" + self.scope = scope + self.receive = receive + self.send = send + self._allowed_methods = [ + method + for method in ("GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "QUERY") + if getattr(self, method.lower(), None) is not None + ] + + def __await__(self) -> Generator[Any, None, None]: + return self.dispatch().__await__() + + async def dispatch(self) -> None: + request = Request(self.scope, receive=self.receive) + handler_name = "get" if request.method == "HEAD" and not hasattr(self, "head") else request.method.lower() + + handler: Callable[[Request], Any] + if request.method in self._allowed_methods or (request.method == "HEAD" and "GET" in self._allowed_methods): + handler = getattr(self, handler_name) + else: + handler = self.method_not_allowed + is_async = is_async_callable(handler) + if is_async: + response = await handler(request) + else: + response = await run_in_threadpool(handler, request) + await response(self.scope, self.receive, self.send) + + async def method_not_allowed(self, request: Request) -> Response: + # If we're running inside a starlette application then raise an + # exception, so that the configurable exception handler can deal with + # returning the response. For plain ASGI apps, just return the response. + headers = {"Allow": ", ".join(self._allowed_methods)} + if "app" in self.scope: + raise HTTPException(status_code=405, headers=headers) + return PlainTextResponse("Method Not Allowed", status_code=405, headers=headers) + + +class WebSocketEndpoint: + encoding: Literal["text", "bytes", "json"] | None = None + + def __init__(self, scope: Scope, receive: Receive, send: Send) -> None: + assert scope["type"] == "websocket" + self.scope = scope + self.receive = receive + self.send = send + + def __await__(self) -> Generator[Any, None, None]: + return self.dispatch().__await__() + + async def dispatch(self) -> None: + websocket = WebSocket(self.scope, receive=self.receive, send=self.send) + await self.on_connect(websocket) + + close_code = status.WS_1000_NORMAL_CLOSURE + + try: + while True: + message = await websocket.receive() + if message["type"] == "websocket.receive": + data = await self.decode(websocket, message) + await self.on_receive(websocket, data) + elif message["type"] == "websocket.disconnect": # pragma: no branch + close_code = int(message.get("code") or status.WS_1000_NORMAL_CLOSURE) + break + except Exception as exc: + close_code = status.WS_1011_INTERNAL_ERROR + raise exc + finally: + await self.on_disconnect(websocket, close_code) + + async def decode(self, websocket: WebSocket, message: Message) -> Any: + if self.encoding == "text": + if message.get("text") is None: + await websocket.close(code=status.WS_1003_UNSUPPORTED_DATA) + raise RuntimeError("Expected text websocket messages, but got bytes") + return message["text"] + + elif self.encoding == "bytes": + if message.get("bytes") is None: + await websocket.close(code=status.WS_1003_UNSUPPORTED_DATA) + raise RuntimeError("Expected bytes websocket messages, but got text") + return message["bytes"] + + elif self.encoding == "json": + if message.get("text") is not None: + text = message["text"] + else: + text = message["bytes"].decode("utf-8") + + try: + return json.loads(text) + except json.decoder.JSONDecodeError: + await websocket.close(code=status.WS_1003_UNSUPPORTED_DATA) + raise RuntimeError("Malformed JSON data received.") + + assert self.encoding is None, f"Unsupported 'encoding' attribute {self.encoding}" + return message["text"] if message.get("text") is not None else message["bytes"] + + async def on_connect(self, websocket: WebSocket) -> None: + """Override to handle an incoming websocket connection""" + await websocket.accept() + + async def on_receive(self, websocket: WebSocket, data: Any) -> None: + """Override to handle an incoming websocket message""" + + async def on_disconnect(self, websocket: WebSocket, close_code: int) -> None: + """Override to handle a disconnecting websocket""" diff --git a/source/target-vendors/cp312/starlette/exceptions.py b/source/target-vendors/cp312/starlette/exceptions.py new file mode 100644 index 0000000..37bdae1 --- /dev/null +++ b/source/target-vendors/cp312/starlette/exceptions.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import http.client +from collections.abc import Mapping + + +class HTTPException(Exception): + def __init__(self, status_code: int, detail: str | None = None, headers: Mapping[str, str] | None = None) -> None: + if detail is None: + detail = http.client.responses.get(status_code, "") + self.status_code = status_code + self.detail = detail + self.headers = headers + + def __str__(self) -> str: + return f"{self.status_code}: {self.detail}" + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + return f"{class_name}(status_code={self.status_code!r}, detail={self.detail!r})" + + +class WebSocketException(Exception): + def __init__(self, code: int, reason: str | None = None) -> None: + self.code = code + self.reason = reason or "" + + def __str__(self) -> str: + return f"{self.code}: {self.reason}" + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + return f"{class_name}(code={self.code!r}, reason={self.reason!r})" + + +class StarletteDeprecationWarning(UserWarning): + """A custom deprecation warning for Starlette. + + Unlike the built-in DeprecationWarning, this inherits from UserWarning to ensure it is visible by default, helping + users discover deprecated features without needing to enable warnings explicitly. + + Reference: https://sethmlarson.dev/deprecations-via-warnings-dont-work-for-python-libraries + """ diff --git a/source/target-vendors/cp312/starlette/formparsers.py b/source/target-vendors/cp312/starlette/formparsers.py new file mode 100644 index 0000000..e3cdc85 --- /dev/null +++ b/source/target-vendors/cp312/starlette/formparsers.py @@ -0,0 +1,301 @@ +from __future__ import annotations + +from collections.abc import AsyncGenerator +from dataclasses import dataclass, field +from enum import Enum +from tempfile import SpooledTemporaryFile +from typing import TYPE_CHECKING +from urllib.parse import unquote_plus + +from starlette.datastructures import FormData, Headers, UploadFile + +if TYPE_CHECKING: + import python_multipart as multipart + from python_multipart.exceptions import FormParserError + from python_multipart.multipart import MultipartCallbacks, QuerystringCallbacks, parse_options_header +else: + try: + try: + import python_multipart as multipart + from python_multipart.exceptions import FormParserError + from python_multipart.multipart import parse_options_header + except ModuleNotFoundError: # pragma: no cover + import multipart + from multipart.exceptions import FormParserError + from multipart.multipart import parse_options_header + except ModuleNotFoundError: # pragma: no cover + multipart = None + parse_options_header = None + + +class FormMessage(Enum): + FIELD_START = 1 + FIELD_NAME = 2 + FIELD_DATA = 3 + FIELD_END = 4 + END = 5 + + +@dataclass +class MultipartPart: + content_disposition: bytes | None = None + field_name: str = "" + data: bytearray = field(default_factory=bytearray) + file: UploadFile | None = None + item_headers: list[tuple[bytes, bytes]] = field(default_factory=list) + + +def _user_safe_decode(src: bytes | bytearray, codec: str) -> str: + try: + return src.decode(codec) + except (UnicodeDecodeError, LookupError): + return src.decode("latin-1") + + +class MultiPartException(Exception): + def __init__(self, message: str) -> None: + self.message = message + + +class FormParser: + def __init__( + self, + headers: Headers, + stream: AsyncGenerator[bytes, None], + *, + max_fields: int | float = 1000, + max_part_size: int = 1024 * 1024, # 1MB + ) -> None: + assert multipart is not None, "The `python-multipart` library must be installed to use form parsing." + self.headers = headers + self.stream = stream + self.max_fields = max_fields + self.max_part_size = max_part_size + self.messages: list[tuple[FormMessage, bytes]] = [] + self._current_field_size = 0 + self._current_fields = 0 + + def on_field_start(self) -> None: + self._current_field_size = 0 + message = (FormMessage.FIELD_START, b"") + self.messages.append(message) + + def on_field_name(self, data: bytes, start: int, end: int) -> None: + self._current_field_size += end - start + if self._current_field_size > self.max_part_size: + raise MultiPartException(f"Field exceeded maximum size of {int(self.max_part_size / 1024)}KB.") + message = (FormMessage.FIELD_NAME, data[start:end]) + self.messages.append(message) + + def on_field_data(self, data: bytes, start: int, end: int) -> None: + self._current_field_size += end - start + if self._current_field_size > self.max_part_size: + raise MultiPartException(f"Field exceeded maximum size of {int(self.max_part_size / 1024)}KB.") + message = (FormMessage.FIELD_DATA, data[start:end]) + self.messages.append(message) + + def on_field_end(self) -> None: + self._current_fields += 1 + if self._current_fields > self.max_fields: + raise MultiPartException(f"Too many fields. Maximum number of fields is {self.max_fields}.") + message = (FormMessage.FIELD_END, b"") + self.messages.append(message) + + def on_end(self) -> None: + message = (FormMessage.END, b"") + self.messages.append(message) + + async def parse(self) -> FormData: + # Callbacks dictionary. + callbacks: QuerystringCallbacks = { + "on_field_start": self.on_field_start, + "on_field_name": self.on_field_name, + "on_field_data": self.on_field_data, + "on_field_end": self.on_field_end, + "on_end": self.on_end, + } + + # Create the parser. + parser = multipart.QuerystringParser(callbacks) + field_name = bytearray() + field_value = bytearray() + + items: list[tuple[str, str | UploadFile]] = [] + + # Feed the parser with data from the request. + async for chunk in self.stream: + if chunk: + parser.write(chunk) + else: + parser.finalize() + messages = list(self.messages) + self.messages.clear() + for message_type, message_bytes in messages: + if message_type == FormMessage.FIELD_START: + field_name = bytearray() + field_value = bytearray() + elif message_type == FormMessage.FIELD_NAME: + field_name.extend(message_bytes) + elif message_type == FormMessage.FIELD_DATA: + field_value.extend(message_bytes) + elif message_type == FormMessage.FIELD_END: + name = unquote_plus(field_name.decode("latin-1")) + value = unquote_plus(field_value.decode("latin-1")) + items.append((name, value)) + + return FormData(items) + + +class MultiPartParser: + spool_max_size = 1024 * 1024 # 1MB + """The maximum size of the spooled temporary file used to store file data.""" + max_part_size = 1024 * 1024 # 1MB + """The maximum size of a part in the multipart request.""" + + def __init__( + self, + headers: Headers, + stream: AsyncGenerator[bytes, None], + *, + max_files: int | float = 1000, + max_fields: int | float = 1000, + max_part_size: int = 1024 * 1024, # 1MB + ) -> None: + assert multipart is not None, "The `python-multipart` library must be installed to use form parsing." + self.headers = headers + self.stream = stream + self.max_files = max_files + self.max_fields = max_fields + self.items: list[tuple[str, str | UploadFile]] = [] + self._current_files = 0 + self._current_fields = 0 + self._current_partial_header_name: bytes = b"" + self._current_partial_header_value: bytes = b"" + self._current_part = MultipartPart() + self._charset = "" + self._file_parts_to_write: list[tuple[MultipartPart, bytes]] = [] + self._file_parts_to_finish: list[MultipartPart] = [] + self._files_to_close_on_error: list[SpooledTemporaryFile[bytes]] = [] + self.max_part_size = max_part_size + + def on_part_begin(self) -> None: + self._current_part = MultipartPart() + + def on_part_data(self, data: bytes, start: int, end: int) -> None: + message_bytes = data[start:end] + if self._current_part.file is None: + if len(self._current_part.data) + len(message_bytes) > self.max_part_size: + raise MultiPartException(f"Part exceeded maximum size of {int(self.max_part_size / 1024)}KB.") + self._current_part.data.extend(message_bytes) + else: + self._file_parts_to_write.append((self._current_part, message_bytes)) + + def on_part_end(self) -> None: + if self._current_part.file is None: + self.items.append( + ( + self._current_part.field_name, + _user_safe_decode(self._current_part.data, self._charset), + ) + ) + else: + self._file_parts_to_finish.append(self._current_part) + # The file can be added to the items right now even though it's not + # finished yet, because it will be finished in the `parse()` method, before + # self.items is used in the return value. + self.items.append((self._current_part.field_name, self._current_part.file)) + + def on_header_field(self, data: bytes, start: int, end: int) -> None: + self._current_partial_header_name += data[start:end] + + def on_header_value(self, data: bytes, start: int, end: int) -> None: + self._current_partial_header_value += data[start:end] + + def on_header_end(self) -> None: + field = self._current_partial_header_name.lower() + if field == b"content-disposition": + self._current_part.content_disposition = self._current_partial_header_value + self._current_part.item_headers.append((field, self._current_partial_header_value)) + self._current_partial_header_name = b"" + self._current_partial_header_value = b"" + + def on_headers_finished(self) -> None: + disposition, options = parse_options_header(self._current_part.content_disposition) + try: + self._current_part.field_name = _user_safe_decode(options[b"name"], self._charset) + except KeyError: + raise MultiPartException('The Content-Disposition header field "name" must be provided.') + if b"filename" in options: + self._current_files += 1 + if self._current_files > self.max_files: + raise MultiPartException(f"Too many files. Maximum number of files is {self.max_files}.") + filename = _user_safe_decode(options[b"filename"], self._charset) + tempfile = SpooledTemporaryFile(max_size=self.spool_max_size) + self._files_to_close_on_error.append(tempfile) + self._current_part.file = UploadFile( + file=tempfile, # type: ignore[arg-type] + size=0, + filename=filename, + headers=Headers(raw=self._current_part.item_headers), + ) + else: + self._current_fields += 1 + if self._current_fields > self.max_fields: + raise MultiPartException(f"Too many fields. Maximum number of fields is {self.max_fields}.") + self._current_part.file = None + + def on_end(self) -> None: + pass + + async def parse(self) -> FormData: + # Parse the Content-Type header to get the multipart boundary. + _, params = parse_options_header(self.headers["Content-Type"]) + charset = params.get(b"charset", "utf-8") + if isinstance(charset, bytes): + charset = charset.decode("latin-1") + self._charset = charset + try: + boundary = params[b"boundary"] + except KeyError: + raise MultiPartException("Missing boundary in multipart.") + + # Callbacks dictionary. + callbacks: MultipartCallbacks = { + "on_part_begin": self.on_part_begin, + "on_part_data": self.on_part_data, + "on_part_end": self.on_part_end, + "on_header_field": self.on_header_field, + "on_header_value": self.on_header_value, + "on_header_end": self.on_header_end, + "on_headers_finished": self.on_headers_finished, + "on_end": self.on_end, + } + + try: + parser = multipart.MultipartParser(boundary, callbacks) + # Feed the parser with data from the request. + async for chunk in self.stream: + parser.write(chunk) + # Write file data, it needs to use await with the UploadFile methods + # that call the corresponding file methods *in a threadpool*, + # otherwise, if they were called directly in the callback methods above + # (regular, non-async functions), that would block the event loop in + # the main thread. + for part, data in self._file_parts_to_write: + assert part.file # for type checkers + await part.file.write(data) + for part in self._file_parts_to_finish: + assert part.file # for type checkers + await part.file.seek(0) + self._file_parts_to_write.clear() + self._file_parts_to_finish.clear() + parser.finalize() + except BaseException as exc: + # Close all the files if parsing or reading the request stream fails. + for file in self._files_to_close_on_error: + file.close() + if isinstance(exc, FormParserError): + raise MultiPartException("Invalid multipart data.") from exc + raise + + return FormData(self.items) diff --git a/source/target-vendors/cp312/starlette/middleware/__init__.py b/source/target-vendors/cp312/starlette/middleware/__init__.py new file mode 100644 index 0000000..f8d47d7 --- /dev/null +++ b/source/target-vendors/cp312/starlette/middleware/__init__.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from collections.abc import Awaitable, Callable, Iterator +from typing import Any, ParamSpec, Protocol + +P = ParamSpec("P") + + +_Scope = Any +_Receive = Callable[[], Awaitable[Any]] +_Send = Callable[[Any], Awaitable[None]] +# Since `starlette.types.ASGIApp` type differs from `ASGIApplication` from `asgiref` +# we need to define a more permissive version of ASGIApp that doesn't cause type errors. +_ASGIApp = Callable[[_Scope, _Receive, _Send], Awaitable[None]] + + +class _MiddlewareFactory(Protocol[P]): + def __call__(self, app: _ASGIApp, /, *args: P.args, **kwargs: P.kwargs) -> _ASGIApp: ... # pragma: no cover + + +class Middleware: + def __init__(self, cls: _MiddlewareFactory[P], *args: P.args, **kwargs: P.kwargs) -> None: + self.cls = cls + self.args = args + self.kwargs = kwargs + + def __iter__(self) -> Iterator[Any]: + as_tuple = (self.cls, self.args, self.kwargs) + return iter(as_tuple) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + args_strings = [f"{value!r}" for value in self.args] + option_strings = [f"{key}={value!r}" for key, value in self.kwargs.items()] + name = getattr(self.cls, "__name__", "") + args_repr = ", ".join([name] + args_strings + option_strings) + return f"{class_name}({args_repr})" diff --git a/source/target-vendors/cp312/starlette/middleware/authentication.py b/source/target-vendors/cp312/starlette/middleware/authentication.py new file mode 100644 index 0000000..935385f --- /dev/null +++ b/source/target-vendors/cp312/starlette/middleware/authentication.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from collections.abc import Callable + +from starlette.authentication import ( + AuthCredentials, + AuthenticationBackend, + AuthenticationError, + UnauthenticatedUser, +) +from starlette.requests import HTTPConnection +from starlette.responses import PlainTextResponse, Response +from starlette.types import ASGIApp, Receive, Scope, Send + + +class AuthenticationMiddleware: + def __init__( + self, + app: ASGIApp, + backend: AuthenticationBackend, + on_error: Callable[[HTTPConnection, AuthenticationError], Response] | None = None, + ) -> None: + self.app = app + self.backend = backend + self.on_error: Callable[[HTTPConnection, AuthenticationError], Response] = ( + on_error if on_error is not None else self.default_on_error + ) + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] not in ["http", "websocket"]: + await self.app(scope, receive, send) + return + + conn = HTTPConnection(scope) + try: + auth_result = await self.backend.authenticate(conn) + except AuthenticationError as exc: + response = self.on_error(conn, exc) + if scope["type"] == "websocket": + await send({"type": "websocket.close", "code": 1000}) + else: + await response(scope, receive, send) + return + + if auth_result is None: + auth_result = AuthCredentials(), UnauthenticatedUser() + scope["auth"], scope["user"] = auth_result + await self.app(scope, receive, send) + + @staticmethod + def default_on_error(conn: HTTPConnection, exc: Exception) -> Response: + return PlainTextResponse(str(exc), status_code=400) diff --git a/source/target-vendors/cp312/starlette/middleware/base.py b/source/target-vendors/cp312/starlette/middleware/base.py new file mode 100644 index 0000000..a7ecb80 --- /dev/null +++ b/source/target-vendors/cp312/starlette/middleware/base.py @@ -0,0 +1,248 @@ +from __future__ import annotations + +from collections.abc import AsyncGenerator, AsyncIterable, Awaitable, Callable, Mapping, MutableMapping +from typing import Any, TypeVar + +import anyio + +from starlette._utils import create_collapsing_task_group +from starlette.requests import ClientDisconnect, Request +from starlette.responses import Response +from starlette.types import ASGIApp, Message, Receive, Scope, Send + +RequestResponseEndpoint = Callable[[Request], Awaitable[Response]] +DispatchFunction = Callable[[Request, RequestResponseEndpoint], Awaitable[Response]] +BodyStreamGenerator = AsyncGenerator[bytes | MutableMapping[str, Any], None] +AsyncContentStream = AsyncIterable[str | bytes | memoryview | MutableMapping[str, Any]] +T = TypeVar("T") + + +class _CachedRequest(Request): + """ + If the user calls Request.body() from their dispatch function + we cache the entire request body in memory and pass that to downstream middlewares, + but if they call Request.stream() then all we do is send an + empty body so that downstream things don't hang forever. + """ + + def __init__(self, scope: Scope, receive: Receive): + super().__init__(scope, receive) + self._wrapped_rcv_disconnected = False + self._wrapped_rcv_consumed = False + self._wrapped_rc_stream = self.stream() + + async def wrapped_receive(self) -> Message: + # wrapped_rcv state 1: disconnected + if self._wrapped_rcv_disconnected: + # we've already sent a disconnect to the downstream app + # we don't need to wait to get another one + # (although most ASGI servers will just keep sending it) + return {"type": "http.disconnect"} + # wrapped_rcv state 1: consumed but not yet disconnected + if self._wrapped_rcv_consumed: + # since the downstream app has consumed us all that is left + # is to send it a disconnect + if self._is_disconnected: + # the middleware has already seen the disconnect + # since we know the client is disconnected no need to wait + # for the message + self._wrapped_rcv_disconnected = True + return {"type": "http.disconnect"} + # we don't know yet if the client is disconnected or not + # so we'll wait until we get that message + msg = await self.receive() + if msg["type"] != "http.disconnect": # pragma: no cover + # at this point a disconnect is all that we should be receiving + # if we get something else, things went wrong somewhere + raise RuntimeError(f"Unexpected message received: {msg['type']}") + self._wrapped_rcv_disconnected = True + return msg + + # wrapped_rcv state 3: not yet consumed + if getattr(self, "_body", None) is not None: + # body() was called, we return it even if the client disconnected + self._wrapped_rcv_consumed = True + return { + "type": "http.request", + "body": self._body, + "more_body": False, + } + elif self._stream_consumed: + # stream() was called to completion + # return an empty body so that downstream apps don't hang + # waiting for a disconnect + self._wrapped_rcv_consumed = True + return { + "type": "http.request", + "body": b"", + "more_body": False, + } + else: + # body() was never called and stream() wasn't consumed + try: + stream = self.stream() + chunk = await stream.__anext__() + self._wrapped_rcv_consumed = self._stream_consumed + return { + "type": "http.request", + "body": chunk, + "more_body": not self._stream_consumed, + } + except ClientDisconnect: + self._wrapped_rcv_disconnected = True + return {"type": "http.disconnect"} + + +class BaseHTTPMiddleware: + def __init__(self, app: ASGIApp, dispatch: DispatchFunction | None = None) -> None: + self.app = app + self.dispatch_func = self.dispatch if dispatch is None else dispatch + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] != "http": + await self.app(scope, receive, send) + return + + request = _CachedRequest(scope, receive) + wrapped_receive = request.wrapped_receive + response_sent = anyio.Event() + app_exc: Exception | None = None + exception_already_raised = False + + async def call_next(request: Request) -> Response: + async def receive_or_disconnect() -> Message: + if response_sent.is_set(): + return {"type": "http.disconnect"} + + async with anyio.create_task_group() as task_group: + + async def wrap(func: Callable[[], Awaitable[T]]) -> T: + result = await func() + task_group.cancel_scope.cancel() + return result + + task_group.start_soon(wrap, response_sent.wait) + message = await wrap(wrapped_receive) + + if response_sent.is_set(): + return {"type": "http.disconnect"} + + return message + + async def send_no_error(message: Message) -> None: + try: + await send_stream.send(message) + except anyio.BrokenResourceError: + # recv_stream has been closed, i.e. response_sent has been set. + return + + if message["type"] == "http.response.pathsend" or ( + message["type"] == "http.response.body" and not message.get("more_body", False) + ): + await response_sent.wait() + + async def coro() -> None: + nonlocal app_exc + + with send_stream: + try: + await self.app(scope, receive_or_disconnect, send_no_error) + except Exception as exc: + app_exc = exc + + task_group.start_soon(coro) + + try: + message = await recv_stream.receive() + info = message.get("info", None) + if message["type"] == "http.response.debug" and info is not None: + message = await recv_stream.receive() + except anyio.EndOfStream: + if app_exc is not None: + nonlocal exception_already_raised + exception_already_raised = True + # Prevent `anyio.EndOfStream` from polluting app exception context. + # If both cause and context are None then the context is suppressed + # and `anyio.EndOfStream` is not present in the exception traceback. + # If exception cause is not None then it is propagated with + # reraising here. + # If exception has no cause but has context set then the context is + # propagated as a cause with the reraise. This is necessary in order + # to prevent `anyio.EndOfStream` from polluting the exception + # context. + raise app_exc from app_exc.__cause__ or app_exc.__context__ + raise RuntimeError("No response returned.") + + assert message["type"] == "http.response.start" + + async def body_stream() -> BodyStreamGenerator: + async for message in recv_stream: + if message["type"] == "http.response.pathsend": + yield message + break + assert message["type"] == "http.response.body", f"Unexpected message: {message}" + body = message.get("body", b"") + if body: + yield body + if not message.get("more_body", False): + break + + response = _StreamingResponse(status_code=message["status"], content=body_stream(), info=info) + response.raw_headers = message["headers"] + return response + + send_stream, recv_stream = anyio.create_memory_object_stream[Message]() + with recv_stream, send_stream: + async with create_collapsing_task_group() as task_group: + response = await self.dispatch_func(request, call_next) + await response(scope, wrapped_receive, send) + response_sent.set() + recv_stream.close() + if app_exc is not None and not exception_already_raised: + raise app_exc + + async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: + raise NotImplementedError() # pragma: no cover + + +class _StreamingResponse(Response): + def __init__( + self, + content: AsyncContentStream, + status_code: int = 200, + headers: Mapping[str, str] | None = None, + media_type: str | None = None, + info: Mapping[str, Any] | None = None, + ) -> None: + self.info = info + self.body_iterator = content + self.status_code = status_code + self.media_type = media_type + self.init_headers(headers) + self.background = None + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if self.info is not None: + await send({"type": "http.response.debug", "info": self.info}) + await send( + { + "type": "http.response.start", + "status": self.status_code, + "headers": self.raw_headers, + } + ) + + should_close_body = True + async for chunk in self.body_iterator: + if isinstance(chunk, dict): + # We got an ASGI message which is not response body (eg: pathsend) + should_close_body = False + await send(chunk) + continue + await send({"type": "http.response.body", "body": chunk, "more_body": True}) + + if should_close_body: + await send({"type": "http.response.body", "body": b"", "more_body": False}) + + if self.background: + await self.background() diff --git a/source/target-vendors/cp312/starlette/middleware/body_limit.py b/source/target-vendors/cp312/starlette/middleware/body_limit.py new file mode 100644 index 0000000..159c70d --- /dev/null +++ b/source/target-vendors/cp312/starlette/middleware/body_limit.py @@ -0,0 +1,132 @@ +from __future__ import annotations + +from typing import cast + +from starlette.datastructures import Headers +from starlette.exceptions import HTTPException +from starlette.responses import PlainTextResponse +from starlette.types import ASGIApp, Message, Receive, Scope, Send + +MAX_BODY_SIZE_SCOPE_KEY = "starlette.max_body_size" +_BODY_LIMIT_RESPONDER_SCOPE_KEY = "starlette._body_limit_responder" + + +class _Missing: + __slots__ = () + + +_MISSING = _Missing() + + +class _RequestBodyTooLarge(HTTPException): + def __init__(self) -> None: + super().__init__(status_code=413, detail="Content Too Large") + + +class _RequestBodyLimitResponseSent(Exception): + pass + + +class RequestBodyLimitMiddleware: + """Limit the total size of an HTTP request body.""" + + def __init__(self, app: ASGIApp, max_body_size: int) -> None: + self.app = app + self.max_body_size = max_body_size + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] != "http": + return await self.app(scope, receive, send) + + responder = RequestBodyLimitResponder(self.app, self.max_body_size) + await responder(scope, receive, send) + + +class RequestBodyLimitResponder: + def __init__(self, app: ASGIApp, max_body_size: int) -> None: + self.app = app + self.max_body_size = max_body_size + self._scope: Scope | None = None + self._receive: Receive | None = None + self._send: Send | None = None + self.content_length: int | None = None + self.total_size = 0 + self.response_started = False + + @property + def scope(self) -> Scope: + assert self._scope is not None + return self._scope + + @property + def receive(self) -> Receive: + assert self._receive is not None + return self._receive + + @property + def send(self) -> Send: + assert self._send is not None + return self._send + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + previous_scope_limit = cast(int | _Missing, scope.get(MAX_BODY_SIZE_SCOPE_KEY, _MISSING)) + scope[MAX_BODY_SIZE_SCOPE_KEY] = self.max_body_size + + active_responder = cast(RequestBodyLimitResponder | None, scope.get(_BODY_LIMIT_RESPONDER_SCOPE_KEY)) + if active_responder is not None: + active_responder.max_body_size = self.max_body_size + if active_responder.total_size > active_responder.max_body_size: + raise _RequestBodyTooLarge + return await self.app(scope, receive, send) + + self._scope = scope + self._receive = receive + self._send = send + self.content_length = _get_content_length(scope) + scope[_BODY_LIMIT_RESPONDER_SCOPE_KEY] = self + + try: + await self.app(scope, self.receive_with_limit, self.send_with_limit) + except _RequestBodyTooLarge: + if self.response_started: + raise + response = PlainTextResponse("Content Too Large", status_code=413) + await response(scope, receive, send) + except _RequestBodyLimitResponseSent: + pass + finally: + scope.pop(_BODY_LIMIT_RESPONDER_SCOPE_KEY, None) + if isinstance(previous_scope_limit, _Missing): + scope.pop(MAX_BODY_SIZE_SCOPE_KEY, None) + else: + scope[MAX_BODY_SIZE_SCOPE_KEY] = previous_scope_limit + + async def receive_with_limit(self) -> Message: + if self.content_length is not None and self.content_length > self.max_body_size: + raise _RequestBodyTooLarge + + message = await self.receive() + if message["type"] == "http.request": + self.total_size += len(message.get("body", b"")) + if self.total_size > self.max_body_size: + raise _RequestBodyTooLarge + return message + + async def send_with_limit(self, message: Message) -> None: + if message["type"] == "http.response.start": + self.response_started = True + if self.content_length is not None and self.content_length > self.max_body_size: + response = PlainTextResponse("Content Too Large", status_code=413) + await response(self.scope, self.receive, self.send) + raise _RequestBodyLimitResponseSent + await self.send(message) + + +def _get_content_length(scope: Scope) -> int | None: + content_length = Headers(scope=scope).get("content-length") + if content_length is None: + return None + try: + return int(content_length) + except ValueError: + return None diff --git a/source/target-vendors/cp312/starlette/middleware/cors.py b/source/target-vendors/cp312/starlette/middleware/cors.py new file mode 100644 index 0000000..fe4c234 --- /dev/null +++ b/source/target-vendors/cp312/starlette/middleware/cors.py @@ -0,0 +1,177 @@ +from __future__ import annotations + +import functools +import re +from collections.abc import Collection + +from starlette.datastructures import Headers, MutableHeaders +from starlette.responses import PlainTextResponse, Response +from starlette.types import ASGIApp, Message, Receive, Scope, Send + +ALL_METHODS = ("DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "QUERY") +SAFELISTED_HEADERS = {"Accept", "Accept-Language", "Content-Language", "Content-Type"} + + +class CORSMiddleware: + def __init__( + self, + app: ASGIApp, + allow_origins: Collection[str] = (), + allow_methods: Collection[str] = ("GET",), + allow_headers: Collection[str] = (), + allow_credentials: bool = False, + allow_origin_regex: str | None = None, + allow_private_network: bool = False, + expose_headers: Collection[str] = (), + max_age: int = 600, + ) -> None: + if "*" in allow_methods: + allow_methods = ALL_METHODS + + compiled_allow_origin_regex = None + if allow_origin_regex is not None: + compiled_allow_origin_regex = re.compile(allow_origin_regex) + + allow_all_origins = "*" in allow_origins + allow_all_headers = "*" in allow_headers + preflight_explicit_allow_origin = not allow_all_origins or allow_credentials + + simple_headers: dict[str, str] = {} + if allow_all_origins: + simple_headers["Access-Control-Allow-Origin"] = "*" + if allow_credentials: + simple_headers["Access-Control-Allow-Credentials"] = "true" + if expose_headers: + simple_headers["Access-Control-Expose-Headers"] = ", ".join(expose_headers) + + preflight_headers: dict[str, str] = { + "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network" # noqa: E501 # fmt: skip + } + if not preflight_explicit_allow_origin: + preflight_headers["Access-Control-Allow-Origin"] = "*" + preflight_headers.update( + { + "Access-Control-Allow-Methods": ", ".join(allow_methods), + "Access-Control-Max-Age": str(max_age), + } + ) + allow_headers = sorted(SAFELISTED_HEADERS | set(allow_headers)) + if allow_headers and not allow_all_headers: + preflight_headers["Access-Control-Allow-Headers"] = ", ".join(allow_headers) + if allow_credentials: + preflight_headers["Access-Control-Allow-Credentials"] = "true" + + self.app = app + self.allow_origins = allow_origins + self.allow_methods = allow_methods + self.allow_headers = [h.lower() for h in allow_headers] + self.allow_all_origins = allow_all_origins + self.allow_all_headers = allow_all_headers + self.allow_credentials = allow_credentials + self.preflight_explicit_allow_origin = preflight_explicit_allow_origin + self.allow_origin_regex = compiled_allow_origin_regex + self.allow_private_network = allow_private_network + self.simple_headers = simple_headers + self.preflight_headers = preflight_headers + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] != "http": # pragma: no cover + await self.app(scope, receive, send) + return + + method = scope["method"] + headers = Headers(scope=scope) + origin = headers.get("origin") + + if origin is not None and method == "OPTIONS" and "access-control-request-method" in headers: + response = self.preflight_response(request_headers=headers) + await response(scope, receive, send) + return + + await self.simple_response(scope, receive, send, request_headers=headers) + + def is_allowed_origin(self, origin: str) -> bool: + if self.allow_all_origins: + return True + + if self.allow_origin_regex is not None and self.allow_origin_regex.fullmatch(origin): + return True + + return origin in self.allow_origins + + def preflight_response(self, request_headers: Headers) -> Response: + requested_origin = request_headers["origin"] + requested_method = request_headers["access-control-request-method"] + requested_headers = request_headers.get("access-control-request-headers") + requested_private_network = request_headers.get("access-control-request-private-network") + + headers = dict(self.preflight_headers) + failures: list[str] = [] + + if self.is_allowed_origin(origin=requested_origin): + if self.preflight_explicit_allow_origin: + # The "else" case is already accounted for in self.preflight_headers + # and the value would be "*". + headers["Access-Control-Allow-Origin"] = requested_origin + else: + failures.append("origin") + + if requested_method not in self.allow_methods: + failures.append("method") + + # If we allow all headers, then we have to mirror back any requested + # headers in the response. + if self.allow_all_headers and requested_headers is not None: + headers["Access-Control-Allow-Headers"] = requested_headers + elif requested_headers is not None: + for header in [h.lower() for h in requested_headers.split(",")]: + if header.strip() not in self.allow_headers: + failures.append("headers") + break + + if requested_private_network is not None: + if self.allow_private_network: + headers["Access-Control-Allow-Private-Network"] = "true" + else: + failures.append("private-network") + + # We don't strictly need to use 400 responses here, since its up to + # the browser to enforce the CORS policy, but its more informative + # if we do. + if failures: + failure_text = "Disallowed CORS " + ", ".join(failures) + return PlainTextResponse(failure_text, status_code=400, headers=headers) + + return PlainTextResponse("OK", status_code=200, headers=headers) + + async def simple_response(self, scope: Scope, receive: Receive, send: Send, request_headers: Headers) -> None: + send = functools.partial(self.send, send=send, request_headers=request_headers) + await self.app(scope, receive, send) + + async def send(self, message: Message, send: Send, request_headers: Headers) -> None: + if message["type"] != "http.response.start": + await send(message) + return + + message.setdefault("headers", []) + headers = MutableHeaders(scope=message) + origin = request_headers.get("Origin") + if origin is not None: + headers.update(self.simple_headers) + + # If credentials are allowed, then we must respond with the specific origin instead of '*'. + if origin is not None and self.allow_all_origins and self.allow_credentials: + self.allow_explicit_origin(headers, origin) + + # If we only allow specific origins, then we have to mirror back the Origin header in the response. + elif origin is not None and not self.allow_all_origins and self.is_allowed_origin(origin=origin): + self.allow_explicit_origin(headers, origin) + else: + headers["Vary"] = ", ".join([*headers.getlist("Vary"), "Origin"]) + + await send(message) + + @staticmethod + def allow_explicit_origin(headers: MutableHeaders, origin: str) -> None: + headers["Access-Control-Allow-Origin"] = origin + headers["Vary"] = ", ".join([*headers.getlist("Vary"), "Origin"]) diff --git a/source/target-vendors/cp312/starlette/middleware/errors.py b/source/target-vendors/cp312/starlette/middleware/errors.py new file mode 100644 index 0000000..e0ebd65 --- /dev/null +++ b/source/target-vendors/cp312/starlette/middleware/errors.py @@ -0,0 +1,259 @@ +from __future__ import annotations + +import html +import inspect +import sys +import traceback + +from starlette._utils import is_async_callable +from starlette.concurrency import run_in_threadpool +from starlette.requests import Request +from starlette.responses import HTMLResponse, PlainTextResponse, Response +from starlette.types import ASGIApp, ExceptionHandler, Message, Receive, Scope, Send + +STYLES = """ +p { + color: #211c1c; +} +.traceback-container { + border: 1px solid #038BB8; +} +.traceback-title { + background-color: #038BB8; + color: lemonchiffon; + padding: 12px; + font-size: 20px; + margin-top: 0px; +} +.frame-line { + padding-left: 10px; + font-family: monospace; +} +.frame-filename { + font-family: monospace; +} +.center-line { + background-color: #038BB8; + color: #f9f6e1; + padding: 5px 0px 5px 5px; +} +.lineno { + margin-right: 5px; +} +.frame-title { + font-weight: unset; + padding: 10px 10px 10px 10px; + background-color: #E4F4FD; + margin-right: 10px; + color: #191f21; + font-size: 17px; + border: 1px solid #c7dce8; +} +.collapse-btn { + float: right; + padding: 0px 5px 1px 5px; + border: solid 1px #96aebb; + cursor: pointer; +} +.collapsed { + display: none; +} +.source-code { + font-family: courier; + font-size: small; + padding-bottom: 10px; +} +""" + +JS = """ + +""" + +TEMPLATE = """ + + + + Starlette Debugger + + +

500 Server Error

+

{error}

+
+

Traceback

+
{exc_html}
+
+ {js} + + +""" + +FRAME_TEMPLATE = """ +
+

File {frame_filename}, + line {frame_lineno}, + in {frame_name} + {collapse_button} +

+
{code_context}
+
+""" # noqa: E501 + +LINE = """ +

+{lineno}. {line}

+""" + +CENTER_LINE = """ +

+{lineno}. {line}

+""" + + +class ServerErrorMiddleware: + """ + Handles returning 500 responses when a server error occurs. + + If 'debug' is set, then traceback responses will be returned, + otherwise the designated 'handler' will be called. + + This middleware class should generally be used to wrap *everything* + else up, so that unhandled exceptions anywhere in the stack + always result in an appropriate 500 response. + """ + + def __init__( + self, + app: ASGIApp, + handler: ExceptionHandler | None = None, + debug: bool = False, + ) -> None: + self.app = app + self.handler = handler + self.debug = debug + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] != "http": + await self.app(scope, receive, send) + return + + response_started = False + + async def _send(message: Message) -> None: + nonlocal response_started, send + + if message["type"] == "http.response.start": + response_started = True + await send(message) + + try: + await self.app(scope, receive, _send) + except Exception as exc: + request = Request(scope) + if self.debug: + # In debug mode, return traceback responses. + response = await run_in_threadpool(self.debug_response, request, exc) + elif self.handler is None: + # Use our default 500 error handler. + response = self.error_response(request, exc) + else: + # Use an installed 500 error handler. + if is_async_callable(self.handler): + response = await self.handler(request, exc) # type: ignore[assignment, arg-type] + else: + response = await run_in_threadpool(self.handler, request, exc) # type: ignore[arg-type] + + if not response_started: + await response(scope, receive, send) + + # We always continue to raise the exception. + # This allows servers to log the error, or allows test clients + # to optionally raise the error within the test case. + raise exc + + def format_line(self, index: int, line: str, frame_lineno: int, frame_index: int) -> str: + values = { + # HTML escape - line could contain < or > + "line": html.escape(line).replace(" ", " "), + "lineno": (frame_lineno - frame_index) + index, + } + + if index != frame_index: + return LINE.format(**values) + return CENTER_LINE.format(**values) + + def generate_frame_html(self, frame: inspect.FrameInfo, is_collapsed: bool) -> str: + code_context = "".join( + self.format_line( + index, + line, + frame.lineno, + frame.index, # type: ignore[arg-type] + ) + for index, line in enumerate(frame.code_context or []) + ) + + values = { + # HTML escape - filename could contain < or >, especially if it's a virtual + # file e.g. in the REPL + "frame_filename": html.escape(frame.filename), + "frame_lineno": frame.lineno, + # HTML escape - if you try very hard it's possible to name a function with < + # or > + "frame_name": html.escape(frame.function), + "code_context": code_context, + "collapsed": "collapsed" if is_collapsed else "", + "collapse_button": "+" if is_collapsed else "‒", + } + return FRAME_TEMPLATE.format(**values) + + def generate_html(self, exc: Exception, limit: int = 7) -> str: + traceback_obj = traceback.TracebackException.from_exception(exc, capture_locals=True) + + exc_html = "" + is_collapsed = False + exc_traceback = exc.__traceback__ + if exc_traceback is not None: + frames = inspect.getinnerframes(exc_traceback, limit) + for frame in reversed(frames): + exc_html += self.generate_frame_html(frame, is_collapsed) + is_collapsed = True + + if sys.version_info >= (3, 13): # pragma: no cover + exc_type_str = traceback_obj.exc_type_str + else: # pragma: no cover + exc_type_str = traceback_obj.exc_type.__name__ + + # escape error class and text + error = f"{html.escape(exc_type_str)}: {html.escape(str(traceback_obj))}" + + return TEMPLATE.format(styles=STYLES, js=JS, error=error, exc_html=exc_html) + + def generate_plain_text(self, exc: Exception) -> str: + return "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)) + + def debug_response(self, request: Request, exc: Exception) -> Response: + accept = request.headers.get("accept", "") + + if "text/html" in accept: + content = self.generate_html(exc) + return HTMLResponse(content, status_code=500) + content = self.generate_plain_text(exc) + return PlainTextResponse(content, status_code=500) + + def error_response(self, request: Request, exc: Exception) -> Response: + return PlainTextResponse("Internal Server Error", status_code=500) diff --git a/source/target-vendors/cp312/starlette/middleware/exceptions.py b/source/target-vendors/cp312/starlette/middleware/exceptions.py new file mode 100644 index 0000000..6c98558 --- /dev/null +++ b/source/target-vendors/cp312/starlette/middleware/exceptions.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +from starlette._exception_handler import ( + ExceptionHandlers, + StatusHandlers, + wrap_app_handling_exceptions, +) +from starlette.exceptions import HTTPException, WebSocketException +from starlette.requests import Request +from starlette.responses import PlainTextResponse, Response +from starlette.types import ASGIApp, ExceptionHandler, Receive, Scope, Send +from starlette.websockets import WebSocket + + +class ExceptionMiddleware: + def __init__( + self, + app: ASGIApp, + handlers: Mapping[Any, ExceptionHandler] | None = None, + debug: bool = False, + ) -> None: + self.app = app + self.debug = debug # TODO: We ought to handle 404 cases if debug is set. + self._status_handlers: StatusHandlers = {} + self._exception_handlers: ExceptionHandlers = { + HTTPException: self.http_exception, + WebSocketException: self.websocket_exception, + } + if handlers is not None: # pragma: no branch + for key, value in handlers.items(): + self.add_exception_handler(key, value) + + def add_exception_handler( + self, + exc_class_or_status_code: int | type[Exception], + handler: ExceptionHandler, + ) -> None: + if isinstance(exc_class_or_status_code, int): + self._status_handlers[exc_class_or_status_code] = handler + else: + assert issubclass(exc_class_or_status_code, Exception) + self._exception_handlers[exc_class_or_status_code] = handler + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] not in ("http", "websocket"): + await self.app(scope, receive, send) + return + + scope["starlette.exception_handlers"] = ( + self._exception_handlers, + self._status_handlers, + ) + + conn: Request | WebSocket + if scope["type"] == "http": + conn = Request(scope, receive, send) + else: + conn = WebSocket(scope, receive, send) + + await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) + + async def http_exception(self, request: Request, exc: Exception) -> Response: + assert isinstance(exc, HTTPException) + if exc.status_code in {204, 304}: + return Response(status_code=exc.status_code, headers=exc.headers) + return PlainTextResponse(exc.detail, status_code=exc.status_code, headers=exc.headers) + + async def websocket_exception(self, websocket: WebSocket, exc: Exception) -> None: + assert isinstance(exc, WebSocketException) + await websocket.close(code=exc.code, reason=exc.reason) # pragma: no cover diff --git a/source/target-vendors/cp312/starlette/middleware/gzip.py b/source/target-vendors/cp312/starlette/middleware/gzip.py new file mode 100644 index 0000000..4473f5a --- /dev/null +++ b/source/target-vendors/cp312/starlette/middleware/gzip.py @@ -0,0 +1,222 @@ +from __future__ import annotations + +import zlib +from typing import NoReturn + +import anyio.lowlevel +import anyio.to_thread + +from starlette.datastructures import Headers, MutableHeaders +from starlette.types import ASGIApp, Message, Receive, Scope, Send + +# TODO(v2): We should rename `DEFAULT_EXCLUDED_CONTENT_TYPES` to `DEFAULT_EXCLUDE_CONTENT_TYPES`. +DEFAULT_EXCLUDED_CONTENT_TYPES = ( + "application/gzip", + "application/x-gzip", + "application/zip", + "audio/*", + "font/woff", + "font/woff2", + "image/avif", + "image/gif", + "image/jpeg", + "image/png", + "image/webp", + "text/event-stream", + "video/*", +) + +_gzip_capacity_limiter: anyio.lowlevel.RunVar[anyio.CapacityLimiter] = anyio.lowlevel.RunVar("_gzip_capacity_limiter") + + +def _get_gzip_capacity_limiter() -> anyio.CapacityLimiter: + """Return the capacity limiter used for worker-thread GZip compression.""" + try: + return _gzip_capacity_limiter.get() + except LookupError: + # Keep gzip compression isolated from AnyIO's default worker-thread + # capacity limiter while matching its default concurrency. + limiter = anyio.CapacityLimiter(40) + _gzip_capacity_limiter.set(limiter) + return limiter + + +class GZipMiddleware: + def __init__( + self, + app: ASGIApp, + minimum_size: int = 500, + compresslevel: int = 9, + thread_minimum_size: int = 128 * 1024, # 128 KiB + *, + exclude_content_types: tuple[str, ...] = DEFAULT_EXCLUDED_CONTENT_TYPES, + ) -> None: + self.app = app + self.minimum_size = minimum_size + self.compresslevel = compresslevel + self.thread_minimum_size = thread_minimum_size + self.exclude_content_types = _normalize_content_types(exclude_content_types) + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] != "http": # pragma: no cover + await self.app(scope, receive, send) + return + + headers = Headers(scope=scope) + responder: ASGIApp + if "gzip" in headers.get("Accept-Encoding", ""): + responder = GZipResponder( + self.app, + self.minimum_size, + compresslevel=self.compresslevel, + thread_minimum_size=self.thread_minimum_size, + exclude_content_types=self.exclude_content_types, + ) + else: + responder = IdentityResponder(self.app, self.minimum_size, exclude_content_types=self.exclude_content_types) + + await responder(scope, receive, send) + + +class IdentityResponder: + content_encoding: str + + def __init__( + self, + app: ASGIApp, + minimum_size: int, + *, + exclude_content_types: tuple[str, ...] = DEFAULT_EXCLUDED_CONTENT_TYPES, + ) -> None: + self.app = app + self.minimum_size = minimum_size + self.exclude_content_types = _normalize_content_types(exclude_content_types) + self.send: Send = unattached_send + self.initial_message: Message = {} + self.started = False + self.content_encoding_set = False + self.content_type_is_excluded = False + self.partial_response = False + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + self.send = send + await self.app(scope, receive, self.send_with_compression) + + async def send_with_compression(self, message: Message) -> None: + message_type = message["type"] + if message_type == "http.response.start": + # Don't send the initial message until we've determined how to + # modify the outgoing headers correctly. + self.initial_message = message + headers = Headers(raw=self.initial_message["headers"]) + self.content_encoding_set = "content-encoding" in headers + self.partial_response = message["status"] == 206 + media_type = headers.get("content-type", "").partition(";")[0].strip().lower() + media_types = {media_type, media_type.partition("/")[0] + "/*"} + self.content_type_is_excluded = not media_types.isdisjoint(self.exclude_content_types) + elif message_type == "http.response.body" and ( + self.content_encoding_set or self.partial_response or self.content_type_is_excluded + ): + if not self.started: + self.started = True + await self.send(self.initial_message) + await self.send(message) + elif message_type == "http.response.body" and not self.started: + self.started = True + body = message.get("body", b"") + more_body = message.get("more_body", False) + if len(body) < self.minimum_size and not more_body: + # Don't apply compression to small outgoing responses. + await self.send(self.initial_message) + await self.send(message) + elif not more_body: + # Standard response. + body = await self.apply_compression(body, more_body=False) + + headers = MutableHeaders(raw=self.initial_message["headers"]) + headers.add_vary_header("Accept-Encoding") + if body != message["body"]: + headers["Content-Encoding"] = self.content_encoding + headers["Content-Length"] = str(len(body)) + message["body"] = body + + await self.send(self.initial_message) + await self.send(message) + else: + # Initial body in streaming response. + body = await self.apply_compression(body, more_body=True) + + headers = MutableHeaders(raw=self.initial_message["headers"]) + headers.add_vary_header("Accept-Encoding") + if body != message["body"]: + headers["Content-Encoding"] = self.content_encoding + del headers["Content-Length"] + message["body"] = body + + await self.send(self.initial_message) + await self.send(message) + elif message_type == "http.response.body": + # Remaining body in streaming response. + body = message.get("body", b"") + more_body = message.get("more_body", False) + + message["body"] = await self.apply_compression(body, more_body=more_body) + + await self.send(message) + elif message_type == "http.response.pathsend": # pragma: no branch + # Don't apply GZip to pathsend responses + await self.send(self.initial_message) + await self.send(message) + + async def apply_compression(self, body: bytes, *, more_body: bool) -> bytes: + """Apply compression on the response body. + + If more_body is False, the compression stream is finalized. Compression + resources are only allocated once a body is actually compressed. + """ + return body + + +class GZipResponder(IdentityResponder): + content_encoding = "gzip" + + def __init__( + self, + app: ASGIApp, + minimum_size: int, + compresslevel: int = 9, + *, + thread_minimum_size: int = 128 * 1024, # 128 KiB + exclude_content_types: tuple[str, ...] = DEFAULT_EXCLUDED_CONTENT_TYPES, + ) -> None: + super().__init__(app, minimum_size, exclude_content_types=exclude_content_types) + + self.compresslevel = compresslevel + self.thread_minimum_size = thread_minimum_size + self._compressor: zlib._Compress | None = None + + @property + def compressor(self) -> zlib._Compress: + if self._compressor is None: + self._compressor = zlib.compressobj(self.compresslevel, zlib.DEFLATED, 16 + zlib.MAX_WBITS) + return self._compressor + + async def apply_compression(self, body: bytes, *, more_body: bool) -> bytes: + if len(body) >= self.thread_minimum_size: + # Compressing large chunks inline would block the event loop. + limiter = _get_gzip_capacity_limiter() + return await anyio.to_thread.run_sync(self._compress_body, body, more_body, limiter=limiter) + return self._compress_body(body, more_body) + + def _compress_body(self, body: bytes, more_body: bool) -> bytes: + if more_body: + return self.compressor.compress(body) + self.compressor.flush(zlib.Z_SYNC_FLUSH) + return self.compressor.compress(body) + self.compressor.flush() + + +async def unattached_send(message: Message) -> NoReturn: + raise RuntimeError("send awaitable not set") # pragma: no cover + + +def _normalize_content_types(content_types: tuple[str, ...]) -> tuple[str, ...]: + return tuple(content_type.partition(";")[0].strip().lower() for content_type in content_types) diff --git a/source/target-vendors/cp312/starlette/middleware/httpsredirect.py b/source/target-vendors/cp312/starlette/middleware/httpsredirect.py new file mode 100644 index 0000000..da40919 --- /dev/null +++ b/source/target-vendors/cp312/starlette/middleware/httpsredirect.py @@ -0,0 +1,22 @@ +from starlette.datastructures import URL +from starlette.responses import PlainTextResponse, RedirectResponse +from starlette.types import ASGIApp, Receive, Scope, Send + + +class HTTPSRedirectMiddleware: + def __init__(self, app: ASGIApp) -> None: + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] in ("http", "websocket") and scope["scheme"] in ("http", "ws"): + url = URL(scope=scope) + if not url.netloc: + await PlainTextResponse("Invalid host header", status_code=400)(scope, receive, send) + return + redirect_scheme = {"http": "https", "ws": "wss"}[url.scheme] + netloc = url.hostname if url.port in (80, 443) else url.netloc + url = url.replace(scheme=redirect_scheme, netloc=netloc) + response = RedirectResponse(url, status_code=307) + await response(scope, receive, send) + else: + await self.app(scope, receive, send) diff --git a/source/target-vendors/cp312/starlette/middleware/opentelemetry.py b/source/target-vendors/cp312/starlette/middleware/opentelemetry.py new file mode 100644 index 0000000..666766c --- /dev/null +++ b/source/target-vendors/cp312/starlette/middleware/opentelemetry.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import re +from collections.abc import Sequence + +try: + from opentelemetry import metrics, propagate, trace + from opentelemetry.trace import SpanKind, Status, StatusCode +except ImportError: # pragma: no cover + raise ImportError("The `opentelemetry-api` package is required to use `OpenTelemetryMiddleware`.") from None + +from starlette import __version__ +from starlette.datastructures import URL +from starlette.routing import Mount +from starlette.types import ASGIApp, Message, Receive, Scope, Send + + +class OpenTelemetryMiddleware: + """Create OpenTelemetry server spans for incoming HTTP requests. + + This middleware is experimental. Its API and emitted telemetry may change in minor + releases without a deprecation period. + + Args: + app: The ASGI application to wrap. + excluded_urls: Regular expressions matched against the full request URL. + Pass a comma-separated string or a sequence. By default, exclude no URLs. + tracer_provider: Optional tracer provider. If omitted, use the global tracer provider. + meter_provider: Optional meter provider. If omitted, use the global meter provider. + """ + + def __init__( + self, + app: ASGIApp, + *, + excluded_urls: str | Sequence[str] = (), + tracer_provider: trace.TracerProvider | None = None, + meter_provider: metrics.MeterProvider | None = None, + ) -> None: + self.app = app + self._meter_provider = meter_provider + if isinstance(excluded_urls, str): + excluded_urls = [pattern.strip() for pattern in excluded_urls.split(",")] if excluded_urls else () + self._excluded_urls = tuple(re.compile(pattern) for pattern in excluded_urls) + self._tracer_provider = tracer_provider or trace.get_tracer_provider() + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] != "http" or scope.get("starlette.opentelemetry"): + return await self.app(scope, receive, send) + + scope["starlette.opentelemetry"] = True + try: + tracer_provider = self._tracer_provider + if isinstance(tracer_provider, trace.NoOpTracerProvider): + return await self.app(scope, receive, send) + + url = URL(scope=scope) + if any(pattern.search(str(url)) for pattern in self._excluded_urls): + return await self.app(scope, receive, send) + + original_method = scope.get("method", "") + method = original_method.upper() + + headers: dict[str, list[str]] = {} + for name, value in scope.get("headers", []): + headers.setdefault(name.decode("latin-1").lower(), []).append(value.decode("latin-1")) + + attributes: dict[str, str | int] = { + "http.request.method": method, + "url.path": scope.get("path", ""), + "url.scheme": scope.get("scheme", "http"), + } + if original_method != method: + attributes["http.request.method_original"] = original_method + if url.query: + attributes["url.query"] = url.query + if url.hostname is not None: + attributes["server.address"] = url.hostname + server = scope.get("server") + server_port = url.port if url.port is not None else server[1] if server is not None else None + if server_port is not None: + attributes["server.port"] = server_port + if scope.get("http_version"): + attributes["network.protocol.version"] = scope["http_version"] + if scope.get("client") is not None: + attributes["client.address"] = scope["client"][0] + if headers.get("user-agent"): + attributes["user_agent.original"] = headers["user-agent"][0] + + with tracer_provider.get_tracer("starlette", __version__).start_as_current_span( + method, + context=propagate.extract(headers), + kind=SpanKind.SERVER, + attributes=attributes, + ) as span: + + async def send_with_telemetry(message: Message) -> None: + if message["type"] == "http.response.start": + status_code = message["status"] + span.set_attribute("http.response.status_code", status_code) + if status_code >= 500: + span.set_attribute("error.type", str(status_code)) + span.set_status(Status(StatusCode.ERROR)) + await send(message) + + try: + await self.app(scope, receive, send_with_telemetry) + except Exception as exc: + span.set_attribute("error.type", type(exc).__qualname__) + raise + finally: + route = scope.get("route") + if isinstance(route, Mount): + route_path = scope.get("root_path") or "/" + else: + path_format = getattr(route, "path_format", None) + route_path = ( + scope.get("root_path", "").rstrip("/") + path_format or "/" + if isinstance(path_format, str) + else None + ) + if route_path is not None: + span.update_name(f"{method} {route_path}") + span.set_attribute("http.route", route_path) + finally: + del scope["starlette.opentelemetry"] diff --git a/source/target-vendors/cp312/starlette/middleware/sessions.py b/source/target-vendors/cp312/starlette/middleware/sessions.py new file mode 100644 index 0000000..efe5e9e --- /dev/null +++ b/source/target-vendors/cp312/starlette/middleware/sessions.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +import json +import typing +from base64 import b64decode, b64encode +from typing import Literal + +import itsdangerous +from itsdangerous.exc import BadSignature + +from starlette.datastructures import MutableHeaders, Secret +from starlette.requests import HTTPConnection +from starlette.types import ASGIApp, Message, Receive, Scope, Send + + +class SessionMiddleware: + def __init__( + self, + app: ASGIApp, + secret_key: str | Secret, + session_cookie: str = "session", + max_age: int | None = 14 * 24 * 60 * 60, # 14 days, in seconds + path: str = "/", + same_site: Literal["lax", "strict", "none"] = "lax", + https_only: bool = False, + domain: str | None = None, + partitioned: bool = False, + ) -> None: + self.app = app + self.signer = itsdangerous.TimestampSigner(str(secret_key)) + self.session_cookie = session_cookie + self.max_age = max_age + self.path = path + self.security_flags = "httponly; samesite=" + same_site + if https_only: # Secure flag can be used with HTTPS only + self.security_flags += "; secure" + if domain is not None: + self.security_flags += f"; domain={domain}" + if partitioned: + self.security_flags += "; partitioned" + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] not in ("http", "websocket"): # pragma: no cover + await self.app(scope, receive, send) + return + + connection = HTTPConnection(scope) + initial_session_was_empty = True + + if self.session_cookie in connection.cookies: + data = connection.cookies[self.session_cookie].encode("utf-8") + try: + data = self.signer.unsign(data, max_age=self.max_age) + scope["session"] = Session(json.loads(b64decode(data))) + initial_session_was_empty = False + except BadSignature: + scope["session"] = Session() + else: + scope["session"] = Session() + + async def send_wrapper(message: Message) -> None: + if message["type"] == "http.response.start": + session: Session = scope["session"] + headers = MutableHeaders(scope=message) + if session.accessed: + headers.add_vary_header("Cookie") + if session.modified and session: + # We have session data to persist. + data = b64encode(json.dumps(session).encode("utf-8")) + data = self.signer.sign(data) + header_value = "{session_cookie}={data}; path={path}; {max_age}{security_flags}".format( + session_cookie=self.session_cookie, + data=data.decode("utf-8"), + path=self.path, + max_age=f"Max-Age={self.max_age}; " if self.max_age else "", + security_flags=self.security_flags, + ) + headers.append("Set-Cookie", header_value) + elif session.modified and not initial_session_was_empty: + # The session has been cleared. + header_value = "{session_cookie}={data}; path={path}; {expires}{security_flags}".format( + session_cookie=self.session_cookie, + data="null", + path=self.path, + expires="expires=Thu, 01 Jan 1970 00:00:00 GMT; ", + security_flags=self.security_flags, + ) + headers.append("Set-Cookie", header_value) + await send(message) + + await self.app(scope, receive, send_wrapper) + + +class Session(dict[str, typing.Any]): + accessed: bool = False + modified: bool = False + + def mark_accessed(self) -> None: + self.accessed = True + + def mark_modified(self) -> None: + self.accessed = True + self.modified = True + + def __setitem__(self, key: str, value: typing.Any) -> None: + self.mark_modified() + super().__setitem__(key, value) + + def __delitem__(self, key: str) -> None: + self.mark_modified() + super().__delitem__(key) + + def clear(self) -> None: + self.mark_modified() + super().clear() + + def pop(self, key: str, *args: typing.Any) -> typing.Any: + self.modified = self.modified or key in self + return super().pop(key, *args) + + def popitem(self) -> tuple[str, typing.Any]: + item = super().popitem() + self.mark_modified() + return item + + def setdefault(self, key: str, default: typing.Any = None) -> typing.Any: + if key not in self: + self.mark_modified() + return super().setdefault(key, default) + + def update(self, *args: typing.Any, **kwargs: typing.Any) -> None: + self.mark_modified() + super().update(*args, **kwargs) + + def __ior__(self, other: typing.Any, /) -> Session: # type: ignore[override,misc] + self.mark_modified() + return super().__ior__(other) diff --git a/source/target-vendors/cp312/starlette/middleware/trustedhost.py b/source/target-vendors/cp312/starlette/middleware/trustedhost.py new file mode 100644 index 0000000..c57aa9a --- /dev/null +++ b/source/target-vendors/cp312/starlette/middleware/trustedhost.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +from collections.abc import Sequence + +from starlette._utils import parse_host_header +from starlette.datastructures import URL, Headers +from starlette.responses import PlainTextResponse, RedirectResponse, Response +from starlette.types import ASGIApp, Receive, Scope, Send + +ENFORCE_DOMAIN_WILDCARD = "Domain wildcard patterns must be like '*.example.com'." + + +class TrustedHostMiddleware: + def __init__( + self, + app: ASGIApp, + allowed_hosts: Sequence[str] | None = None, + www_redirect: bool = True, + ) -> None: + if allowed_hosts is None: + allowed_hosts = ["*"] + + for pattern in allowed_hosts: + assert "*" not in pattern[1:], ENFORCE_DOMAIN_WILDCARD + if pattern.startswith("*") and pattern != "*": + assert pattern.startswith("*."), ENFORCE_DOMAIN_WILDCARD + self.app = app + self.allowed_hosts = list(allowed_hosts) + self.allow_any = "*" in allowed_hosts + self.www_redirect = www_redirect + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if self.allow_any or scope["type"] not in ( + "http", + "websocket", + ): # pragma: no cover + await self.app(scope, receive, send) + return + + headers = Headers(scope=scope) + parsed_host = parse_host_header(headers.get("host")) + if parsed_host is None: + await PlainTextResponse("Invalid host header", status_code=400)(scope, receive, send) + return + host = parsed_host.host + + is_valid_host = False + found_www_redirect = False + for pattern in self.allowed_hosts: + if host == pattern or (pattern.startswith("*") and host.endswith(pattern[1:])): + is_valid_host = True + break + elif "www." + host == pattern: + found_www_redirect = True + + if is_valid_host: + await self.app(scope, receive, send) + else: + response: Response + if found_www_redirect and self.www_redirect: + url = URL(scope=scope) + redirect_url = url.replace(netloc="www." + url.netloc) + response = RedirectResponse(url=str(redirect_url)) + else: + response = PlainTextResponse("Invalid host header", status_code=400) + await response(scope, receive, send) diff --git a/source/target-vendors/cp312/starlette/middleware/wsgi.py b/source/target-vendors/cp312/starlette/middleware/wsgi.py new file mode 100644 index 0000000..0871926 --- /dev/null +++ b/source/target-vendors/cp312/starlette/middleware/wsgi.py @@ -0,0 +1,157 @@ +from __future__ import annotations + +import io +import math +import sys +import warnings +from collections.abc import Callable, MutableMapping +from typing import Any + +import anyio.from_thread +import anyio.to_thread +from anyio.abc import ObjectReceiveStream, ObjectSendStream + +from starlette._utils import create_collapsing_task_group +from starlette.exceptions import StarletteDeprecationWarning +from starlette.types import Receive, Scope, Send + +warnings.warn( + "starlette.middleware.wsgi is deprecated and will be removed in a future release. " + "Please refer to https://github.com/abersheeran/a2wsgi as a replacement.", + StarletteDeprecationWarning, + stacklevel=2, +) + + +def build_environ(scope: Scope, body: bytes) -> dict[str, Any]: + """ + Builds a scope and request body into a WSGI environ object. + """ + + script_name = scope.get("root_path", "").encode("utf8").decode("latin1") + path_info = scope["path"].encode("utf8").decode("latin1") + if path_info.startswith(script_name): + path_info = path_info[len(script_name) :] + + environ = { + "REQUEST_METHOD": scope["method"], + "SCRIPT_NAME": script_name, + "PATH_INFO": path_info, + "QUERY_STRING": scope["query_string"].decode("ascii"), + "SERVER_PROTOCOL": f"HTTP/{scope['http_version']}", + "wsgi.version": (1, 0), + "wsgi.url_scheme": scope.get("scheme", "http"), + "wsgi.input": io.BytesIO(body), + "wsgi.errors": sys.stdout, + "wsgi.multithread": True, + "wsgi.multiprocess": True, + "wsgi.run_once": False, + } + + # Get server name and port - required in WSGI, not in ASGI + server = scope.get("server") or ("localhost", 80) + environ["SERVER_NAME"] = server[0] + environ["SERVER_PORT"] = server[1] + + # Get client IP address + if scope.get("client"): + environ["REMOTE_ADDR"] = scope["client"][0] + + # Go through headers and make them into environ entries + for name, value in scope.get("headers", []): + name = name.decode("latin1") + if name == "content-length": + corrected_name = "CONTENT_LENGTH" + elif name == "content-type": + corrected_name = "CONTENT_TYPE" + else: + corrected_name = f"HTTP_{name}".upper().replace("-", "_") + # HTTPbis say only ASCII chars are allowed in headers, but we latin1 just in + # case + value = value.decode("latin1") + if corrected_name in environ: + value = environ[corrected_name] + "," + value + environ[corrected_name] = value + return environ + + +class WSGIMiddleware: + def __init__(self, app: Callable[..., Any]) -> None: + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + assert scope["type"] == "http" + responder = WSGIResponder(self.app, scope) + await responder(receive, send) + + +class WSGIResponder: + stream_send: ObjectSendStream[MutableMapping[str, Any]] + stream_receive: ObjectReceiveStream[MutableMapping[str, Any]] + + def __init__(self, app: Callable[..., Any], scope: Scope) -> None: + self.app = app + self.scope = scope + self.status = None + self.response_headers = None + self.stream_send, self.stream_receive = anyio.create_memory_object_stream(math.inf) + self.response_started = False + self.exc_info: Any = None + + async def __call__(self, receive: Receive, send: Send) -> None: + body = b"" + more_body = True + while more_body: + message = await receive() + body += message.get("body", b"") + more_body = message.get("more_body", False) + environ = build_environ(self.scope, body) + + async with create_collapsing_task_group() as task_group: + task_group.start_soon(self.sender, send) + async with self.stream_send: + await anyio.to_thread.run_sync(self.wsgi, environ, self.start_response) + if self.exc_info is not None: + raise self.exc_info[0].with_traceback(self.exc_info[1], self.exc_info[2]) + + async def sender(self, send: Send) -> None: + async with self.stream_receive: + async for message in self.stream_receive: + await send(message) + + def start_response( + self, + status: str, + response_headers: list[tuple[str, str]], + exc_info: Any = None, + ) -> None: + self.exc_info = exc_info + if not self.response_started: # pragma: no branch + self.response_started = True + status_code_string, _ = status.split(" ", 1) + status_code = int(status_code_string) + headers = [ + (name.strip().encode("ascii").lower(), value.strip().encode("ascii")) + for name, value in response_headers + ] + anyio.from_thread.run( + self.stream_send.send, + { + "type": "http.response.start", + "status": status_code, + "headers": headers, + }, + ) + + def wsgi( + self, + environ: dict[str, Any], + start_response: Callable[..., Any], + ) -> None: + for chunk in self.app(environ, start_response): + anyio.from_thread.run( + self.stream_send.send, + {"type": "http.response.body", "body": chunk, "more_body": True}, + ) + + anyio.from_thread.run(self.stream_send.send, {"type": "http.response.body", "body": b""}) diff --git a/source/target-vendors/cp312/starlette/py.typed b/source/target-vendors/cp312/starlette/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/starlette/requests.py b/source/target-vendors/cp312/starlette/requests.py new file mode 100644 index 0000000..d4f3f4b --- /dev/null +++ b/source/target-vendors/cp312/starlette/requests.py @@ -0,0 +1,348 @@ +from __future__ import annotations + +import json +import sys +from collections.abc import AsyncGenerator, Iterator, Mapping +from http import cookies as http_cookies +from typing import TYPE_CHECKING, Any, Generic, NoReturn, cast + +import anyio + +from starlette._utils import AwaitableOrContextManager, AwaitableOrContextManagerWrapper +from starlette.datastructures import URL, Address, FormData, Headers, QueryParams, State +from starlette.exceptions import HTTPException +from starlette.formparsers import FormParser, MultiPartException, MultiPartParser +from starlette.types import Message, Receive, Scope, Send + +if TYPE_CHECKING: + from python_multipart.multipart import parse_options_header + + from starlette.applications import Starlette + from starlette.middleware.sessions import Session + from starlette.routing import Router +else: + try: + try: + from python_multipart.multipart import parse_options_header + except ModuleNotFoundError: # pragma: no cover + from multipart.multipart import parse_options_header + except ModuleNotFoundError: # pragma: no cover + parse_options_header = None + +if sys.version_info >= (3, 13): # pragma: no cover + from typing import TypeVar +else: # pragma: no cover + from typing_extensions import TypeVar + +SERVER_PUSH_HEADERS_TO_COPY = { + "accept", + "accept-encoding", + "accept-language", + "cache-control", + "user-agent", +} + + +def cookie_parser(cookie_string: str) -> dict[str, str]: + """ + This function parses a ``Cookie`` HTTP header into a dict of key/value pairs. + + It attempts to mimic browser cookie parsing behavior: browsers and web servers + frequently disregard the spec (RFC 6265) when setting and reading cookies, + so we attempt to suit the common scenarios here. + + This function has been adapted from Django 3.1.0. + Note: we are explicitly _NOT_ using `SimpleCookie.load` because it is based + on an outdated spec and will fail on lots of input we want to support + """ + cookie_dict: dict[str, str] = {} + for chunk in cookie_string.split(";"): + if "=" in chunk: + key, val = chunk.split("=", 1) + else: + # Assume an empty name per + # https://bugzilla.mozilla.org/show_bug.cgi?id=169091 + key, val = "", chunk + key, val = key.strip(), val.strip() + if key or val: + # unquote using Python's algorithm. + cookie_dict[key] = http_cookies._unquote(val) + return cookie_dict + + +class ClientDisconnect(Exception): + pass + + +StateT = TypeVar("StateT", bound=Mapping[str, Any] | State, default=State) + + +class HTTPConnection(Mapping[str, Any], Generic[StateT]): + """ + A base class for incoming HTTP connections, that is used to provide + any functionality that is common to both `Request` and `WebSocket`. + """ + + def __init__(self, scope: Scope, receive: Receive | None = None) -> None: + assert scope["type"] in ("http", "websocket") + self.scope = scope + + def __getitem__(self, key: str) -> Any: + return self.scope[key] + + def __iter__(self) -> Iterator[str]: + return iter(self.scope) + + def __len__(self) -> int: + return len(self.scope) + + # Don't use the `abc.Mapping.__eq__` implementation. + # Connection instances should never be considered equal + # unless `self is other`. + __eq__ = object.__eq__ + __hash__ = object.__hash__ + + @property + def app(self) -> Any: + return self.scope["app"] + + @property + def url(self) -> URL: + if not hasattr(self, "_url"): # pragma: no branch + self._url = URL(scope=self.scope) + return self._url + + @property + def base_url(self) -> URL: + if not hasattr(self, "_base_url"): + base_url_scope = dict(self.scope) + # This is used by request.url_for, it might be used inside a Mount which + # would have its own child scope with its own root_path, but the base URL + # for url_for should still be the top level app root path. + app_root_path = base_url_scope.get("app_root_path", base_url_scope.get("root_path", "")) + path = app_root_path + if not path.endswith("/"): + path += "/" + base_url_scope["path"] = path + base_url_scope["query_string"] = b"" + base_url_scope["root_path"] = app_root_path + self._base_url = URL(scope=base_url_scope) + return self._base_url + + @property + def headers(self) -> Headers: + if not hasattr(self, "_headers"): + self._headers = Headers(scope=self.scope) + return self._headers + + @property + def query_params(self) -> QueryParams: + if not hasattr(self, "_query_params"): # pragma: no branch + self._query_params = QueryParams(self.scope["query_string"]) + return self._query_params + + @property + def path_params(self) -> dict[str, Any]: + path_params: dict[str, Any] = self.scope.get("path_params", {}) + return path_params + + @property + def cookies(self) -> dict[str, str]: + if not hasattr(self, "_cookies"): + cookies: dict[str, str] = {} + cookie_headers = self.headers.getlist("cookie") + + for header in cookie_headers: + cookies.update(cookie_parser(header)) + + self._cookies = cookies + return self._cookies + + @property + def client(self) -> Address | None: + # client is a 2 item tuple of (host, port), None if missing + host_port = self.scope.get("client") + if host_port is not None: + return Address(*host_port) + return None + + @property + def session(self) -> dict[str, Any]: + assert "session" in self.scope, "SessionMiddleware must be installed to access request.session" + session: Session = self.scope["session"] + # We keep the hasattr in case people actually use their own `SessionMiddleware` implementation. + if hasattr(session, "mark_accessed"): # pragma: no branch + session.mark_accessed() + return session + + @property + def auth(self) -> Any: + assert "auth" in self.scope, "AuthenticationMiddleware must be installed to access request.auth" + return self.scope["auth"] + + @property + def user(self) -> Any: + assert "user" in self.scope, "AuthenticationMiddleware must be installed to access request.user" + return self.scope["user"] + + @property + def state(self) -> StateT: + if not hasattr(self, "_state"): + # Ensure 'state' has an empty dict if it's not already populated. + self.scope.setdefault("state", {}) + # Create a state instance with a reference to the dict in which it should + # store info + self._state = State(self.scope["state"]) + return cast(StateT, self._state) + + def url_for(self, name: str, /, **path_params: Any) -> URL: + url_path_provider: Router | Starlette | None = self.scope.get("router") or self.scope.get("app") + if url_path_provider is None: + raise RuntimeError("The `url_for` method can only be used inside a Starlette application or with a router.") + url_path = url_path_provider.url_path_for(name, **path_params) + return url_path.make_absolute_url(base_url=self.base_url) + + +async def empty_receive() -> NoReturn: + raise RuntimeError("Receive channel has not been made available") + + +async def empty_send(message: Message) -> NoReturn: + raise RuntimeError("Send channel has not been made available") + + +class Request(HTTPConnection[StateT]): + _form: FormData | None + + def __init__(self, scope: Scope, receive: Receive = empty_receive, send: Send = empty_send): + super().__init__(scope) + assert scope["type"] == "http" + self._receive = receive + self._send = send + self._stream_consumed = False + self._is_disconnected = False + self._form = None + + @property + def method(self) -> str: + return cast(str, self.scope["method"]) + + @property + def receive(self) -> Receive: + return self._receive + + async def stream(self) -> AsyncGenerator[bytes, None]: + if hasattr(self, "_body"): + yield self._body + yield b"" + return + if self._stream_consumed: + raise RuntimeError("Stream consumed") + while not self._stream_consumed: + message = await self._receive() + if message["type"] == "http.request": + body = message.get("body", b"") + if not message.get("more_body", False): + self._stream_consumed = True + if body: + yield body + elif message["type"] == "http.disconnect": # pragma: no branch + self._is_disconnected = True + raise ClientDisconnect() + yield b"" + + async def body(self) -> bytes: + if not hasattr(self, "_body"): + chunks: list[bytes] = [] + async for chunk in self.stream(): + chunks.append(chunk) + self._body = b"".join(chunks) + return self._body + + async def json(self) -> Any: + if not hasattr(self, "_json"): # pragma: no branch + body = await self.body() + self._json = json.loads(body) + return self._json + + async def _get_form( + self, + *, + max_files: int | float = 1000, + max_fields: int | float = 1000, + max_part_size: int = 1024 * 1024, + ) -> FormData: + if self._form is None: # pragma: no branch + assert parse_options_header is not None, ( + "The `python-multipart` library must be installed to use form parsing." + ) + content_type_header = self.headers.get("Content-Type") + content_type: bytes + content_type, _ = parse_options_header(content_type_header) + if content_type == b"multipart/form-data": + try: + multipart_parser = MultiPartParser( + self.headers, + self.stream(), + max_files=max_files, + max_fields=max_fields, + max_part_size=max_part_size, + ) + self._form = await multipart_parser.parse() + except MultiPartException as exc: + if "app" in self.scope: + raise HTTPException(status_code=400, detail=exc.message) + raise exc + elif content_type == b"application/x-www-form-urlencoded": + try: + form_parser = FormParser( + self.headers, + self.stream(), + max_fields=max_fields, + max_part_size=max_part_size, + ) + self._form = await form_parser.parse() + except MultiPartException as exc: + if "app" in self.scope: + raise HTTPException(status_code=400, detail=exc.message) + raise exc + else: + self._form = FormData() + return self._form + + def form( + self, + *, + max_files: int | float = 1000, + max_fields: int | float = 1000, + max_part_size: int = 1024 * 1024, + ) -> AwaitableOrContextManager[FormData]: + return AwaitableOrContextManagerWrapper( + self._get_form(max_files=max_files, max_fields=max_fields, max_part_size=max_part_size) + ) + + async def close(self) -> None: + if self._form is not None: # pragma: no branch + await self._form.close() + + async def is_disconnected(self) -> bool: + if not self._is_disconnected: + message: Message = {} + + # If message isn't immediately available, move on + with anyio.CancelScope() as cs: + cs.cancel() + message = await self._receive() + + if message.get("type") == "http.disconnect": + self._is_disconnected = True + + return self._is_disconnected + + async def send_push_promise(self, path: str) -> None: + if "http.response.push" in self.scope.get("extensions", {}): + raw_headers: list[tuple[bytes, bytes]] = [] + for name in SERVER_PUSH_HEADERS_TO_COPY: + for value in self.headers.getlist(name): + raw_headers.append((name.encode("latin-1"), value.encode("latin-1"))) + await self._send({"type": "http.response.push", "path": path, "headers": raw_headers}) diff --git a/source/target-vendors/cp312/starlette/responses.py b/source/target-vendors/cp312/starlette/responses.py new file mode 100644 index 0000000..bff2467 --- /dev/null +++ b/source/target-vendors/cp312/starlette/responses.py @@ -0,0 +1,579 @@ +from __future__ import annotations + +import hashlib +import http.cookies +import json +import os +import stat +import sys +from collections.abc import AsyncIterable, Awaitable, Callable, Iterable, Mapping, Sequence +from datetime import datetime +from email.utils import format_datetime, formatdate +from functools import partial +from mimetypes import guess_type +from secrets import token_hex +from typing import Any, Literal +from urllib.parse import quote + +import anyio +import anyio.to_thread + +from starlette._utils import create_collapsing_task_group +from starlette.background import BackgroundTask +from starlette.concurrency import iterate_in_threadpool +from starlette.datastructures import URL, Headers, MutableHeaders +from starlette.requests import ClientDisconnect +from starlette.types import Message, Receive, Scope, Send + + +class Response: + media_type = None + charset = "utf-8" + + def __init__( + self, + content: Any = None, + status_code: int = 200, + headers: Mapping[str, str] | None = None, + media_type: str | None = None, + background: BackgroundTask | None = None, + ) -> None: + self.status_code = status_code + if media_type is not None: + self.media_type = media_type + self.background = background + self.body = self.render(content) + self.init_headers(headers) + + def render(self, content: Any) -> bytes | memoryview: + if content is None: + return b"" + if isinstance(content, bytes | memoryview): + return content + return content.encode(self.charset) # type: ignore + + def init_headers(self, headers: Mapping[str, str] | None = None) -> None: + if headers is None: + raw_headers: list[tuple[bytes, bytes]] = [] + populate_content_length = True + populate_content_type = True + else: + raw_headers = [(k.lower().encode("latin-1"), v.encode("latin-1")) for k, v in headers.items()] + keys = [h[0] for h in raw_headers] + populate_content_length = b"content-length" not in keys + populate_content_type = b"content-type" not in keys + + body = getattr(self, "body", None) + if ( + body is not None + and populate_content_length + and not (self.status_code < 200 or self.status_code in (204, 304)) + ): + content_length = str(len(body)) + raw_headers.append((b"content-length", content_length.encode("latin-1"))) + + content_type = self.media_type + if content_type is not None and populate_content_type: + if content_type.startswith("text/") and "charset=" not in content_type.lower(): + content_type += "; charset=" + self.charset + raw_headers.append((b"content-type", content_type.encode("latin-1"))) + + self.raw_headers = raw_headers + + @property + def headers(self) -> MutableHeaders: + if not hasattr(self, "_headers"): + self._headers = MutableHeaders(raw=self.raw_headers) + return self._headers + + def set_cookie( + self, + key: str, + value: str = "", + max_age: int | None = None, + expires: datetime | str | int | None = None, + path: str | None = "/", + domain: str | None = None, + secure: bool = False, + httponly: bool = False, + samesite: Literal["lax", "strict", "none"] | None = "lax", + partitioned: bool = False, + ) -> None: + cookie: http.cookies.BaseCookie[str] = http.cookies.SimpleCookie() + cookie[key] = value + if max_age is not None: + cookie[key]["max-age"] = max_age + if expires is not None: + if isinstance(expires, datetime): + cookie[key]["expires"] = format_datetime(expires, usegmt=True) + else: + cookie[key]["expires"] = expires + if path is not None: + cookie[key]["path"] = path + if domain is not None: + cookie[key]["domain"] = domain + if secure: + cookie[key]["secure"] = True + if httponly: + cookie[key]["httponly"] = True + if samesite is not None: + assert samesite.lower() in [ + "strict", + "lax", + "none", + ], "samesite must be either 'strict', 'lax' or 'none'" + cookie[key]["samesite"] = samesite + if partitioned: + if sys.version_info < (3, 14): + raise ValueError("Partitioned cookies are only supported in Python 3.14 and above.") # pragma: no cover + cookie[key]["partitioned"] = True # pragma: no cover + + cookie_val = cookie.output(header="").strip() + self.raw_headers.append((b"set-cookie", cookie_val.encode("latin-1"))) + + def delete_cookie( + self, + key: str, + path: str = "/", + domain: str | None = None, + secure: bool = False, + httponly: bool = False, + samesite: Literal["lax", "strict", "none"] | None = "lax", + partitioned: bool = False, + ) -> None: + self.set_cookie( + key, + max_age=0, + expires=0, + path=path, + domain=domain, + secure=secure, + httponly=httponly, + samesite=samesite, + partitioned=partitioned, + ) + + def _wrap_websocket_denial_send(self, send: Send) -> Send: + async def wrapped(message: Message) -> None: + message_type = message["type"] + if message_type in {"http.response.start", "http.response.body"}: # pragma: no branch + message = {**message, "type": "websocket." + message_type} + await send(message) + + return wrapped + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] == "websocket": + send = self._wrap_websocket_denial_send(send) + await send({"type": "http.response.start", "status": self.status_code, "headers": self.raw_headers}) + await send({"type": "http.response.body", "body": self.body}) + + if self.background is not None: + await self.background() + + +class HTMLResponse(Response): + media_type = "text/html" + + +class PlainTextResponse(Response): + media_type = "text/plain" + + +class JSONResponse(Response): + media_type = "application/json" + + def __init__( + self, + content: Any, + status_code: int = 200, + headers: Mapping[str, str] | None = None, + media_type: str | None = None, + background: BackgroundTask | None = None, + ) -> None: + super().__init__(content, status_code, headers, media_type, background) + + def render(self, content: Any) -> bytes: + return json.dumps( + content, + ensure_ascii=False, + allow_nan=False, + indent=None, + separators=(",", ":"), + ).encode("utf-8") + + +class RedirectResponse(Response): + def __init__( + self, + url: str | URL, + status_code: int = 307, + headers: Mapping[str, str] | None = None, + background: BackgroundTask | None = None, + ) -> None: + super().__init__(content=b"", status_code=status_code, headers=headers, background=background) + self.headers["location"] = quote(str(url), safe=":/%#?=@[]!$&'()*+,;") + + +Content = str | bytes | memoryview +SyncContentStream = Iterable[Content] +AsyncContentStream = AsyncIterable[Content] +ContentStream = AsyncContentStream | SyncContentStream + + +class StreamingResponse(Response): + body_iterator: AsyncContentStream + + def __init__( + self, + content: ContentStream, + status_code: int = 200, + headers: Mapping[str, str] | None = None, + media_type: str | None = None, + background: BackgroundTask | None = None, + ) -> None: + if isinstance(content, AsyncIterable): + self.body_iterator = content + else: + self.body_iterator = iterate_in_threadpool(content) + self.status_code = status_code + self.media_type = self.media_type if media_type is None else media_type + self.background = background + self.init_headers(headers) + + async def listen_for_disconnect(self, receive: Receive) -> None: + while True: + message = await receive() + if message["type"] == "http.disconnect": + break + + async def stream_response(self, send: Send) -> None: + await send({"type": "http.response.start", "status": self.status_code, "headers": self.raw_headers}) + async for chunk in self.body_iterator: + if not isinstance(chunk, bytes | memoryview): + chunk = chunk.encode(self.charset) + await send({"type": "http.response.body", "body": chunk, "more_body": True}) + + await send({"type": "http.response.body", "body": b"", "more_body": False}) + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] == "websocket": + send = self._wrap_websocket_denial_send(send) + await self.stream_response(send) + if self.background is not None: + await self.background() + return + + spec_version = tuple(map(int, scope.get("asgi", {}).get("spec_version", "2.0").split("."))) + + if spec_version >= (2, 4): + try: + await self.stream_response(send) + except OSError: + raise ClientDisconnect() + else: + async with create_collapsing_task_group() as task_group: + + async def wrap(func: Callable[[], Awaitable[None]]) -> None: + await func() + task_group.cancel_scope.cancel() + + task_group.start_soon(wrap, partial(self.stream_response, send)) + await wrap(partial(self.listen_for_disconnect, receive)) + + if self.background is not None: + await self.background() + + +class MalformedRangeHeader(Exception): + def __init__(self, content: str = "Malformed range header.") -> None: + self.content = content + + +class RangeNotSatisfiable(Exception): + def __init__(self, max_size: int) -> None: + self.max_size = max_size + + +class FileResponse(Response): + chunk_size = 64 * 1024 + max_ranges = 100 + + def __init__( + self, + path: str | os.PathLike[str], + status_code: int = 200, + headers: Mapping[str, str] | None = None, + media_type: str | None = None, + background: BackgroundTask | None = None, + filename: str | None = None, + stat_result: os.stat_result | None = None, + content_disposition_type: str = "attachment", + ) -> None: + self.path = path + self.status_code = status_code + self.filename = filename + if media_type is None: + media_type = guess_type(filename or path)[0] or "application/octet-stream" + self.media_type = media_type + self.background = background + self.init_headers(headers) + self.headers.setdefault("accept-ranges", "bytes") + if self.filename is not None: + content_disposition_filename = quote(self.filename) + if content_disposition_filename != self.filename: + content_disposition = f"{content_disposition_type}; filename*=utf-8''{content_disposition_filename}" + else: + content_disposition = f'{content_disposition_type}; filename="{self.filename}"' + self.headers.setdefault("content-disposition", content_disposition) + self.stat_result = stat_result + if stat_result is not None: + self.set_stat_headers(stat_result) + + def set_stat_headers(self, stat_result: os.stat_result) -> None: + content_length = str(stat_result.st_size) + last_modified = formatdate(stat_result.st_mtime, usegmt=True) + etag_base = str(stat_result.st_mtime) + "-" + str(stat_result.st_size) + etag = f'"{hashlib.md5(etag_base.encode(), usedforsecurity=False).hexdigest()}"' + + self.headers.setdefault("content-length", content_length) + self.headers.setdefault("last-modified", last_modified) + self.headers.setdefault("etag", etag) + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + scope_type = scope["type"] + send_header_only = scope_type == "http" and scope["method"].upper() == "HEAD" + send_pathsend = scope_type == "http" and "http.response.pathsend" in scope.get("extensions", {}) + if scope_type == "websocket": + send = self._wrap_websocket_denial_send(send) + + if self.stat_result is None: + try: + stat_result = await anyio.to_thread.run_sync(os.stat, self.path) + self.set_stat_headers(stat_result) + except FileNotFoundError: + raise RuntimeError(f"File at path {self.path} does not exist.") + else: + mode = stat_result.st_mode + if not stat.S_ISREG(mode): + raise RuntimeError(f"File at path {self.path} is not a file.") + else: + stat_result = self.stat_result + + headers = Headers(scope=scope) + http_range = headers.get("range") + http_if_range = headers.get("if-range") + + if ( + self.status_code != 200 + or http_range is None + or (http_if_range is not None and not self._should_use_range(http_if_range)) + ): + await self._handle_simple(send, send_header_only, send_pathsend) + else: + try: + ranges = self._parse_range_header(http_range, stat_result.st_size) + except MalformedRangeHeader as exc: + return await PlainTextResponse(exc.content, status_code=400)(scope, receive, send) + except RangeNotSatisfiable as exc: + response = PlainTextResponse(status_code=416, headers={"Content-Range": f"bytes */{exc.max_size}"}) + return await response(scope, receive, send) + + if len(ranges) == 0: + await self._handle_simple(send, send_header_only, send_pathsend) + elif len(ranges) == 1: + start, end = ranges[0] + await self._handle_single_range(send, start, end, stat_result.st_size, send_header_only) + else: + await self._handle_multiple_ranges(send, ranges, stat_result.st_size, send_header_only) + + if self.background is not None: + await self.background() + + async def _handle_simple(self, send: Send, send_header_only: bool, send_pathsend: bool) -> None: + await send({"type": "http.response.start", "status": self.status_code, "headers": self.raw_headers}) + if send_header_only: + await send({"type": "http.response.body", "body": b"", "more_body": False}) + elif send_pathsend: + await send({"type": "http.response.pathsend", "path": str(self.path)}) + else: + async with await anyio.open_file(self.path, mode="rb") as file: + more_body = True + while more_body: + chunk = await file.read(self.chunk_size) + more_body = len(chunk) == self.chunk_size + await send({"type": "http.response.body", "body": chunk, "more_body": more_body}) + + async def _handle_single_range( + self, send: Send, start: int, end: int, file_size: int, send_header_only: bool + ) -> None: + headers = MutableHeaders(raw=list(self.raw_headers)) + headers["content-range"] = f"bytes {start}-{end - 1}/{file_size}" + headers["content-length"] = str(end - start) + await send({"type": "http.response.start", "status": 206, "headers": headers.raw}) + if send_header_only: + await send({"type": "http.response.body", "body": b"", "more_body": False}) + else: + async with await anyio.open_file(self.path, mode="rb") as file: + await file.seek(start) + more_body = True + while more_body: + chunk = await file.read(min(self.chunk_size, end - start)) + start += len(chunk) + more_body = len(chunk) == self.chunk_size and start < end + await send({"type": "http.response.body", "body": chunk, "more_body": more_body}) + + async def _handle_multiple_ranges( + self, + send: Send, + ranges: list[tuple[int, int]], + file_size: int, + send_header_only: bool, + ) -> None: + # In firefox and chrome, they use boundary with 95-96 bits entropy (that's roughly 13 bytes). + boundary = token_hex(13) + content_length, header_generator = self.generate_multipart( + ranges, boundary, file_size, self.headers["content-type"] + ) + headers = MutableHeaders(raw=list(self.raw_headers)) + headers["content-type"] = f"multipart/byteranges; boundary={boundary}" + headers["content-length"] = str(content_length) + await send({"type": "http.response.start", "status": 206, "headers": headers.raw}) + if send_header_only: + await send({"type": "http.response.body", "body": b"", "more_body": False}) + else: + async with await anyio.open_file(self.path, mode="rb") as file: + for start, end in ranges: + await send({"type": "http.response.body", "body": header_generator(start, end), "more_body": True}) + await file.seek(start) + while start < end: + chunk = await file.read(min(self.chunk_size, end - start)) + start += len(chunk) + await send({"type": "http.response.body", "body": chunk, "more_body": True}) + await send({"type": "http.response.body", "body": b"\r\n", "more_body": True}) + await send( + { + "type": "http.response.body", + "body": f"--{boundary}--".encode("latin-1"), + "more_body": False, + } + ) + + def _should_use_range(self, http_if_range: str) -> bool: + return http_if_range == self.headers["last-modified"] or http_if_range == self.headers["etag"] + + @classmethod + def _parse_range_header(cls, http_range: str, file_size: int) -> list[tuple[int, int]]: + ranges: list[tuple[int, int]] = [] + try: + units, range_ = http_range.split("=", 1) + except ValueError: + raise MalformedRangeHeader() + + units = units.strip().lower() + + if units != "bytes": + raise MalformedRangeHeader("Only support bytes range") + + if range_.count(",") + 1 > cls.max_ranges: + return [] + + ranges = cls._parse_ranges(range_, file_size) + + if len(ranges) == 0: + raise MalformedRangeHeader("Range header: range must be requested") + + if any(not (0 <= start < file_size) for start, _ in ranges): + raise RangeNotSatisfiable(file_size) + + if any(start >= end for start, end in ranges): + raise MalformedRangeHeader("Range header: start must be less than end") + + if len(ranges) == 1: + return ranges + + # Merge overlapping ranges + ranges.sort() + result: list[tuple[int, int]] = [ranges[0]] + for start, end in ranges[1:]: + last_start, last_end = result[-1] + if start <= last_end: + result[-1] = (last_start, max(last_end, end)) + else: + result.append((start, end)) + + return result + + @classmethod + def _parse_ranges(cls, range_: str, file_size: int) -> list[tuple[int, int]]: + ranges: list[tuple[int, int]] = [] + + for part in range_.split(","): + part = part.strip() + + # If the range is empty or a single dash, we ignore it. + if not part or part == "-": + continue + + # If the range is not in the format "start-end", we ignore it. + if "-" not in part: + continue + + start_str, end_str = part.split("-", 1) + start_str = start_str.strip() + end_str = end_str.strip() + + try: + start = int(start_str) if start_str else max(file_size - int(end_str), 0) + end = int(end_str) + 1 if start_str and end_str and int(end_str) < file_size else file_size + ranges.append((start, end)) + except ValueError: + # If the range is not numeric, we ignore it. + continue + + return ranges + + def generate_multipart( + self, + ranges: Sequence[tuple[int, int]], + boundary: str, + max_size: int, + content_type: str, + ) -> tuple[int, Callable[[int, int], bytes]]: + r""" + Multipart response headers generator. + + ``` + --{boundary}\r\n + Content-Type: {content_type}\r\n + Content-Range: bytes {start}-{end-1}/{max_size}\r\n + \r\n + ..........content...........\r\n + --{boundary}\r\n + Content-Type: {content_type}\r\n + Content-Range: bytes {start}-{end-1}/{max_size}\r\n + \r\n + ..........content...........\r\n + --{boundary}-- + ``` + """ + boundary_len = len(boundary) + static_header_part_len = 49 + boundary_len + len(content_type) + len(str(max_size)) + content_length = sum( + (len(str(start)) + len(str(end - 1)) + static_header_part_len) # Headers + + (end - start) # Content + for start, end in ranges + ) + ( + 4 + boundary_len # --boundary-- + ) + return ( + content_length, + lambda start, end: ( + f"""\ +--{boundary}\r +Content-Type: {content_type}\r +Content-Range: bytes {start}-{end - 1}/{max_size}\r +\r +""" + ).encode("latin-1"), + ) diff --git a/source/target-vendors/cp312/starlette/routing.py b/source/target-vendors/cp312/starlette/routing.py new file mode 100644 index 0000000..9969d38 --- /dev/null +++ b/source/target-vendors/cp312/starlette/routing.py @@ -0,0 +1,765 @@ +from __future__ import annotations + +import contextlib +import functools +import inspect +import re +import traceback +import types +import warnings +from collections.abc import Awaitable, Callable, Collection, Generator, Sequence +from contextlib import AbstractAsyncContextManager, AbstractContextManager, asynccontextmanager +from enum import Enum +from re import Pattern +from typing import Any, TypeVar + +from starlette._exception_handler import wrap_app_handling_exceptions +from starlette._utils import get_route_path, is_async_callable, parse_host_header +from starlette.concurrency import run_in_threadpool +from starlette.convertors import CONVERTOR_TYPES, Convertor +from starlette.datastructures import URL, Headers, URLPath +from starlette.exceptions import HTTPException, StarletteDeprecationWarning +from starlette.middleware import Middleware +from starlette.middleware.body_limit import RequestBodyLimitMiddleware +from starlette.requests import Request +from starlette.responses import PlainTextResponse, RedirectResponse, Response +from starlette.types import ASGIApp, Lifespan, Receive, Scope, Send +from starlette.websockets import WebSocket, WebSocketClose + + +class NoMatchFound(Exception): + """ + Raised by `.url_for(name, **path_params)` and `.url_path_for(name, **path_params)` + if no matching route exists. + """ + + def __init__(self, name: str, path_params: dict[str, Any]) -> None: + params = ", ".join(list(path_params.keys())) + super().__init__(f'No route exists for name "{name}" and params "{params}".') + + +class Match(Enum): + NONE = 0 + PARTIAL = 1 + FULL = 2 + + +def request_response( + func: Callable[[Request], Awaitable[Response] | Response], +) -> ASGIApp: + """ + Takes a function or coroutine `func(request) -> response`, + and returns an ASGI application. + """ + f: Callable[[Request], Awaitable[Response]] = ( + func if is_async_callable(func) else functools.partial(run_in_threadpool, func) # type: ignore[assignment, call-arg] + ) + + async def app(scope: Scope, receive: Receive, send: Send) -> None: + request = Request(scope, receive, send) + + async def app(scope: Scope, receive: Receive, send: Send) -> None: + response = await f(request) + await response(scope, receive, send) + + await wrap_app_handling_exceptions(app, request)(scope, receive, send) + + return app + + +def websocket_session( + func: Callable[[WebSocket], Awaitable[None]], +) -> ASGIApp: + """ + Takes a coroutine `func(session)`, and returns an ASGI application. + """ + # assert asyncio.iscoroutinefunction(func), "WebSocket endpoints must be async" + + async def app(scope: Scope, receive: Receive, send: Send) -> None: + session = WebSocket(scope, receive=receive, send=send) + + async def app(scope: Scope, receive: Receive, send: Send) -> None: + await func(session) + + await wrap_app_handling_exceptions(app, session)(scope, receive, send) + + return app + + +def get_name(endpoint: Callable[..., Any]) -> str: + return getattr(endpoint, "__name__", endpoint.__class__.__name__) + + +def replace_params( + path: str, + param_convertors: dict[str, Convertor[Any]], + path_params: dict[str, str], +) -> tuple[str, dict[str, str]]: + for key, value in list(path_params.items()): + if "{" + key + "}" in path: + convertor = param_convertors[key] + value = convertor.to_string(value) + path = path.replace("{" + key + "}", value) + path_params.pop(key) + return path, path_params + + +# Match parameters in URL paths, eg. '{param}', and '{param:int}' +PARAM_REGEX = re.compile("{([a-zA-Z_][a-zA-Z0-9_]*)(:[a-zA-Z_][a-zA-Z0-9_]*)?}") + + +def compile_path( + path: str, +) -> tuple[Pattern[str], str, dict[str, Convertor[Any]]]: + """ + Given a path string, like: "/{username:str}", + or a host string, like: "{subdomain}.mydomain.org", return a three-tuple + of (regex, format, {param_name:convertor}). + + regex: "/(?P[^/]+)" + format: "/{username}" + convertors: {"username": StringConvertor()} + """ + is_host = not path.startswith("/") + + path_regex = "^" + path_format = "" + duplicated_params: set[str] = set() + + idx = 0 + param_convertors = {} + for match in PARAM_REGEX.finditer(path): + param_name, convertor_type = match.groups("str") + convertor_type = convertor_type.lstrip(":") + assert convertor_type in CONVERTOR_TYPES, f"Unknown path convertor '{convertor_type}'" + convertor = CONVERTOR_TYPES[convertor_type] + + path_regex += re.escape(path[idx : match.start()]) + path_regex += f"(?P<{param_name}>{convertor.regex})" + + path_format += path[idx : match.start()] + path_format += "{%s}" % param_name + + if param_name in param_convertors: + duplicated_params.add(param_name) + + param_convertors[param_name] = convertor + + idx = match.end() + + if duplicated_params: + names = ", ".join(sorted(duplicated_params)) + ending = "s" if len(duplicated_params) > 1 else "" + raise ValueError(f"Duplicated param name{ending} {names} at path {path}") + + if is_host: + # Align with `Host.matches()` behavior, which ignores port. + hostname = path[idx:] + if not hostname.endswith("]"): + hostname = hostname.rsplit(":", 1)[0] + path_regex += re.escape(hostname) + "$" + else: + path_regex += re.escape(path[idx:]) + "$" + + path_format += path[idx:] + + return re.compile(path_regex), path_format, param_convertors + + +class BaseRoute: + def matches(self, scope: Scope) -> tuple[Match, Scope]: + raise NotImplementedError() # pragma: no cover + + def url_path_for(self, name: str, /, **path_params: Any) -> URLPath: + raise NotImplementedError() # pragma: no cover + + async def handle(self, scope: Scope, receive: Receive, send: Send) -> None: + raise NotImplementedError() # pragma: no cover + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + """ + A route may be used in isolation as a stand-alone ASGI app. + This is a somewhat contrived case, as they'll almost always be used + within a Router, but could be useful for some tooling and minimal apps. + """ + match, child_scope = self.matches(scope) + if match == Match.NONE: + if scope["type"] == "http": + response = PlainTextResponse("Not Found", status_code=404) + await response(scope, receive, send) + elif scope["type"] == "websocket": # pragma: no branch + websocket_close = WebSocketClose() + await websocket_close(scope, receive, send) + return + + scope.update(child_scope) + await self.handle(scope, receive, send) + + +class Route(BaseRoute): + def __init__( + self, + path: str, + endpoint: Callable[..., Any], + *, + methods: Collection[str] | None = None, + name: str | None = None, + include_in_schema: bool = True, + middleware: Sequence[Middleware] | None = None, + max_body_size: int | None = None, + ) -> None: + assert path.startswith("/"), "Routed paths must start with '/'" + self.path = path + self.endpoint = endpoint + self.name = get_name(endpoint) if name is None else name + self.include_in_schema = include_in_schema + + endpoint_handler = endpoint + while isinstance(endpoint_handler, functools.partial): + endpoint_handler = endpoint_handler.func + if inspect.isfunction(endpoint_handler) or inspect.ismethod(endpoint_handler): + # Endpoint is function or method. Treat it as `func(request) -> response`. + self.app = request_response(endpoint) + if methods is None: + methods = ["GET"] + else: + # Endpoint is a class. Treat it as ASGI. + self.app = endpoint + + if middleware is not None: + for cls, args, kwargs in reversed(middleware): + self.app = cls(self.app, *args, **kwargs) + if max_body_size is not None: + self.app = RequestBodyLimitMiddleware(self.app, max_body_size=max_body_size) + + if methods is None: + self.methods = None + else: + self.methods = {method.upper() for method in methods} + if "GET" in self.methods: + self.methods.add("HEAD") + + self.path_regex, self.path_format, self.param_convertors = compile_path(path) + + def matches(self, scope: Scope) -> tuple[Match, Scope]: + path_params: dict[str, Any] + if scope["type"] == "http": + route_path = get_route_path(scope) + match = self.path_regex.match(route_path) + if match: + matched_params = match.groupdict() + for key, value in matched_params.items(): + matched_params[key] = self.param_convertors[key].convert(value) + path_params = dict(scope.get("path_params", {})) + path_params.update(matched_params) + child_scope = {"endpoint": self.endpoint, "path_params": path_params} + if self.methods and scope["method"] not in self.methods: + return Match.PARTIAL, child_scope + else: + return Match.FULL, child_scope + return Match.NONE, {} + + def url_path_for(self, name: str, /, **path_params: Any) -> URLPath: + seen_params = set(path_params.keys()) + expected_params = set(self.param_convertors.keys()) + + if name != self.name or seen_params != expected_params: + raise NoMatchFound(name, path_params) + + path, remaining_params = replace_params(self.path_format, self.param_convertors, path_params) + assert not remaining_params + return URLPath(path=path, protocol="http") + + async def handle(self, scope: Scope, receive: Receive, send: Send) -> None: + if self.methods and scope["method"] not in self.methods: + headers = {"Allow": ", ".join(self.methods)} + if "app" in scope: + raise HTTPException(status_code=405, headers=headers) + else: + response = PlainTextResponse("Method Not Allowed", status_code=405, headers=headers) + await response(scope, receive, send) + else: + await self.app(scope, receive, send) + + def __eq__(self, other: Any) -> bool: + return ( + isinstance(other, Route) + and self.path == other.path + and self.endpoint == other.endpoint + and self.methods == other.methods + ) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + methods = sorted(self.methods or []) + path, name = self.path, self.name + return f"{class_name}(path={path!r}, name={name!r}, methods={methods!r})" + + +class WebSocketRoute(BaseRoute): + def __init__( + self, + path: str, + endpoint: Callable[..., Any], + *, + name: str | None = None, + middleware: Sequence[Middleware] | None = None, + ) -> None: + assert path.startswith("/"), "Routed paths must start with '/'" + self.path = path + self.endpoint = endpoint + self.name = get_name(endpoint) if name is None else name + + endpoint_handler = endpoint + while isinstance(endpoint_handler, functools.partial): + endpoint_handler = endpoint_handler.func + if inspect.isfunction(endpoint_handler) or inspect.ismethod(endpoint_handler): + # Endpoint is function or method. Treat it as `func(websocket)`. + self.app = websocket_session(endpoint) + else: + # Endpoint is a class. Treat it as ASGI. + self.app = endpoint + + if middleware is not None: + for cls, args, kwargs in reversed(middleware): + self.app = cls(self.app, *args, **kwargs) + + self.path_regex, self.path_format, self.param_convertors = compile_path(path) + + def matches(self, scope: Scope) -> tuple[Match, Scope]: + path_params: dict[str, Any] + if scope["type"] == "websocket": + route_path = get_route_path(scope) + match = self.path_regex.match(route_path) + if match: + matched_params = match.groupdict() + for key, value in matched_params.items(): + matched_params[key] = self.param_convertors[key].convert(value) + path_params = dict(scope.get("path_params", {})) + path_params.update(matched_params) + child_scope = {"endpoint": self.endpoint, "path_params": path_params} + return Match.FULL, child_scope + return Match.NONE, {} + + def url_path_for(self, name: str, /, **path_params: Any) -> URLPath: + seen_params = set(path_params.keys()) + expected_params = set(self.param_convertors.keys()) + + if name != self.name or seen_params != expected_params: + raise NoMatchFound(name, path_params) + + path, remaining_params = replace_params(self.path_format, self.param_convertors, path_params) + assert not remaining_params + return URLPath(path=path, protocol="websocket") + + async def handle(self, scope: Scope, receive: Receive, send: Send) -> None: + await self.app(scope, receive, send) + + def __eq__(self, other: Any) -> bool: + return isinstance(other, WebSocketRoute) and self.path == other.path and self.endpoint == other.endpoint + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(path={self.path!r}, name={self.name!r})" + + +class Mount(BaseRoute): + def __init__( + self, + path: str, + app: ASGIApp | None = None, + routes: Sequence[BaseRoute] | None = None, + name: str | None = None, + *, + middleware: Sequence[Middleware] | None = None, + max_body_size: int | None = None, + ) -> None: + assert path == "" or path.startswith("/"), "Routed paths must start with '/'" + assert app is not None or routes is not None, "Either 'app=...', or 'routes=' must be specified" + self.path = path.rstrip("/") + if app is not None: + self._base_app: ASGIApp = app + else: + self._base_app = Router(routes=routes) + self.app = self._base_app + if middleware is not None: + for cls, args, kwargs in reversed(middleware): + self.app = cls(self.app, *args, **kwargs) + if max_body_size is not None: + self.app = RequestBodyLimitMiddleware(self.app, max_body_size=max_body_size) + self.name = name + self.path_regex, self.path_format, self.param_convertors = compile_path(self.path + "/{path:path}") + + @property + def routes(self) -> list[BaseRoute]: + return getattr(self._base_app, "routes", []) + + def matches(self, scope: Scope) -> tuple[Match, Scope]: + path_params: dict[str, Any] + if scope["type"] in ("http", "websocket"): # pragma: no branch + root_path = scope.get("root_path", "") + route_path = get_route_path(scope) + match = self.path_regex.match(route_path) + if match: + matched_params = match.groupdict() + for key, value in matched_params.items(): + matched_params[key] = self.param_convertors[key].convert(value) + remaining_path = "/" + matched_params.pop("path") + matched_path = route_path[: -len(remaining_path)] + path_params = dict(scope.get("path_params", {})) + path_params.update(matched_params) + child_scope = { + "path_params": path_params, + # app_root_path will only be set at the top level scope, + # initialized with the (optional) value of a root_path + # set above/before Starlette. And even though any + # mount will have its own child scope with its own respective + # root_path, the app_root_path will always be available in all + # the child scopes with the same top level value because it's + # set only once here with a default, any other child scope will + # just inherit that app_root_path default value stored in the + # scope. All this is needed to support Request.url_for(), as it + # uses the app_root_path to build the URL path. + "app_root_path": scope.get("app_root_path", root_path), + "root_path": root_path + matched_path, + "endpoint": self.app, + } + return Match.FULL, child_scope + return Match.NONE, {} + + def url_path_for(self, name: str, /, **path_params: Any) -> URLPath: + if self.name is not None and name == self.name and "path" in path_params: + # 'name' matches "". + path_params["path"] = path_params["path"].lstrip("/") + path, remaining_params = replace_params(self.path_format, self.param_convertors, path_params) + if not remaining_params: + return URLPath(path=path) + elif self.name is None or name.startswith(self.name + ":"): + if self.name is None: + # No mount name. + remaining_name = name + else: + # 'name' matches ":". + remaining_name = name[len(self.name) + 1 :] + path_kwarg = path_params.get("path") + path_params["path"] = "" + path_prefix, remaining_params = replace_params(self.path_format, self.param_convertors, path_params) + if path_kwarg is not None: + remaining_params["path"] = path_kwarg + for route in self.routes or []: + try: + url = route.url_path_for(remaining_name, **remaining_params) + return URLPath(path=path_prefix.rstrip("/") + str(url), protocol=url.protocol) + except NoMatchFound: + pass + raise NoMatchFound(name, path_params) + + async def handle(self, scope: Scope, receive: Receive, send: Send) -> None: + await self.app(scope, receive, send) + + def __eq__(self, other: Any) -> bool: + return isinstance(other, Mount) and self.path == other.path and self.app == other.app + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + name = self.name or "" + return f"{class_name}(path={self.path!r}, name={name!r}, app={self.app!r})" + + +class Host(BaseRoute): + def __init__(self, host: str, app: ASGIApp, name: str | None = None) -> None: + assert not host.startswith("/"), "Host must not start with '/'" + self.host = host + self.app = app + self.name = name + self.host_regex, self.host_format, self.param_convertors = compile_path(host) + + @property + def routes(self) -> list[BaseRoute]: + return getattr(self.app, "routes", []) + + def matches(self, scope: Scope) -> tuple[Match, Scope]: + if scope["type"] in ("http", "websocket"): # pragma:no branch + headers = Headers(scope=scope) + parsed_host = parse_host_header(headers.get("host")) + if parsed_host is None: + return Match.NONE, {} + host = parsed_host.host + + match = self.host_regex.match(host) + if match: + matched_params = match.groupdict() + for key, value in matched_params.items(): + matched_params[key] = self.param_convertors[key].convert(value) + path_params = dict(scope.get("path_params", {})) + path_params.update(matched_params) + child_scope = {"path_params": path_params, "endpoint": self.app} + return Match.FULL, child_scope + return Match.NONE, {} + + def url_path_for(self, name: str, /, **path_params: Any) -> URLPath: + if self.name is not None and name == self.name and "path" in path_params: + # 'name' matches "". + path = path_params.pop("path") + host, remaining_params = replace_params(self.host_format, self.param_convertors, path_params) + if not remaining_params: + return URLPath(path=path, host=host) + elif self.name is None or name.startswith(self.name + ":"): + if self.name is None: + # No mount name. + remaining_name = name + else: + # 'name' matches ":". + remaining_name = name[len(self.name) + 1 :] + host, remaining_params = replace_params(self.host_format, self.param_convertors, path_params) + for route in self.routes or []: + try: + url = route.url_path_for(remaining_name, **remaining_params) + return URLPath(path=str(url), protocol=url.protocol, host=host) + except NoMatchFound: + pass + raise NoMatchFound(name, path_params) + + async def handle(self, scope: Scope, receive: Receive, send: Send) -> None: + await self.app(scope, receive, send) + + def __eq__(self, other: Any) -> bool: + return isinstance(other, Host) and self.host == other.host and self.app == other.app + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + name = self.name or "" + return f"{class_name}(host={self.host!r}, name={name!r}, app={self.app!r})" + + +_T = TypeVar("_T") + + +class _AsyncLiftContextManager(AbstractAsyncContextManager[_T]): + def __init__(self, cm: AbstractContextManager[_T]): + self._cm = cm + + async def __aenter__(self) -> _T: + return self._cm.__enter__() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: types.TracebackType | None, + ) -> bool | None: + return self._cm.__exit__(exc_type, exc_value, traceback) + + +def _wrap_gen_lifespan_context( + lifespan_context: Callable[[Any], Generator[Any, Any, Any]], +) -> Callable[[Any], AbstractAsyncContextManager[Any]]: + cmgr = contextlib.contextmanager(lifespan_context) + + @functools.wraps(cmgr) + def wrapper(app: Any) -> _AsyncLiftContextManager[Any]: + return _AsyncLiftContextManager(cmgr(app)) + + return wrapper + + +class _DefaultLifespan: + def __init__(self, router: Router): + self._router = router + + async def __aenter__(self) -> None: + pass + + async def __aexit__(self, *exc_info: object) -> None: + pass + + def __call__(self: _T, app: object) -> _T: + return self + + +class Router: + def __init__( + self, + routes: Sequence[BaseRoute] | None = None, + redirect_slashes: bool = True, + default: ASGIApp | None = None, + # the generic to Lifespan[AppType] is the type of the top level application + # which the router cannot know statically, so we use Any + lifespan: Lifespan[Any] | None = None, + *, + middleware: Sequence[Middleware] | None = None, + max_body_size: int | None = None, + ) -> None: + self.routes = [] if routes is None else list(routes) + self.redirect_slashes = redirect_slashes + self.default = self.not_found if default is None else default + + if lifespan is None: + self.lifespan_context: Lifespan[Any] = _DefaultLifespan(self) + + elif inspect.isasyncgenfunction(lifespan): + warnings.warn( + "async generator function lifespans are deprecated, " + "use an @contextlib.asynccontextmanager function instead", + StarletteDeprecationWarning, + ) + self.lifespan_context = asynccontextmanager(lifespan) + elif inspect.isgeneratorfunction(lifespan): + warnings.warn( + "generator function lifespans are deprecated, use an @contextlib.asynccontextmanager function instead", + StarletteDeprecationWarning, + ) + self.lifespan_context = _wrap_gen_lifespan_context(lifespan) + else: + self.lifespan_context = lifespan + + self.middleware_stack = self.app + if middleware: + for cls, args, kwargs in reversed(middleware): + self.middleware_stack = cls(self.middleware_stack, *args, **kwargs) + if max_body_size is not None: + self.middleware_stack = RequestBodyLimitMiddleware(self.middleware_stack, max_body_size=max_body_size) + + async def not_found(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] == "websocket": + websocket_close = WebSocketClose() + await websocket_close(scope, receive, send) + return + + # If we're running inside a starlette application then raise an + # exception, so that the configurable exception handler can deal with + # returning the response. For plain ASGI apps, just return the response. + if "app" in scope: + raise HTTPException(status_code=404) + else: + response = PlainTextResponse("Not Found", status_code=404) + await response(scope, receive, send) + + def url_path_for(self, name: str, /, **path_params: Any) -> URLPath: + for route in self.routes: + try: + return route.url_path_for(name, **path_params) + except NoMatchFound: + pass + raise NoMatchFound(name, path_params) + + async def lifespan(self, scope: Scope, receive: Receive, send: Send) -> None: + """ + Handle ASGI lifespan messages, which allows us to manage application + startup and shutdown events. + """ + started = False + app: Any = scope.get("app") + await receive() + try: + async with self.lifespan_context(app) as maybe_state: + if maybe_state is not None: + if "state" not in scope: + raise RuntimeError('The server does not support "state" in the lifespan scope.') + scope["state"].update(maybe_state) + await send({"type": "lifespan.startup.complete"}) + started = True + await receive() + except BaseException: + exc_text = traceback.format_exc() + if started: + await send({"type": "lifespan.shutdown.failed", "message": exc_text}) + else: + await send({"type": "lifespan.startup.failed", "message": exc_text}) + raise + else: + await send({"type": "lifespan.shutdown.complete"}) + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + """ + The main entry point to the Router class. + """ + await self.middleware_stack(scope, receive, send) + + async def app(self, scope: Scope, receive: Receive, send: Send) -> None: + assert scope["type"] in ("http", "websocket", "lifespan") + + if "router" not in scope: + scope["router"] = self + + if scope["type"] == "lifespan": + await self.lifespan(scope, receive, send) + return + + partial = None + + for route in self.routes: + # Determine if any route matches the incoming scope, + # and hand over to the matching route if found. + match, child_scope = route.matches(scope) + if match == Match.FULL: + scope["route"] = route + scope.update(child_scope) + await route.handle(scope, receive, send) + return + elif match == Match.PARTIAL and partial is None: + partial = route + partial_scope = child_scope + + if partial is not None: + #  Handle partial matches. These are cases where an endpoint is + # able to handle the request, but is not a preferred option. + # We use this in particular to deal with "405 Method Not Allowed". + scope["route"] = partial + scope.update(partial_scope) + await partial.handle(scope, receive, send) + return + + route_path = get_route_path(scope) + if scope["type"] == "http" and self.redirect_slashes and route_path != "/": + redirect_scope = dict(scope) + if route_path.endswith("/"): + redirect_scope["path"] = redirect_scope["path"].rstrip("/") + else: + redirect_scope["path"] = redirect_scope["path"] + "/" + + for route in self.routes: + match, child_scope = route.matches(redirect_scope) + if match != Match.NONE: + redirect_url = URL(scope=redirect_scope) + response = RedirectResponse(url=str(redirect_url)) + await response(scope, receive, send) + return + + await self.default(scope, receive, send) + + def __eq__(self, other: Any) -> bool: + return isinstance(other, Router) and self.routes == other.routes + + def mount(self, path: str, app: ASGIApp, name: str | None = None) -> None: # pragma: no cover + route = Mount(path, app=app, name=name) + self.routes.append(route) + + def host(self, host: str, app: ASGIApp, name: str | None = None) -> None: # pragma: no cover + route = Host(host, app=app, name=name) + self.routes.append(route) + + def add_route( + self, + path: str, + endpoint: Callable[[Request], Awaitable[Response] | Response], + methods: Collection[str] | None = None, + name: str | None = None, + include_in_schema: bool = True, + ) -> None: # pragma: no cover + route = Route( + path, + endpoint=endpoint, + methods=methods, + name=name, + include_in_schema=include_in_schema, + ) + self.routes.append(route) + + def add_websocket_route( + self, + path: str, + endpoint: Callable[[WebSocket], Awaitable[None]], + name: str | None = None, + ) -> None: # pragma: no cover + route = WebSocketRoute(path, endpoint=endpoint, name=name) + self.routes.append(route) diff --git a/source/target-vendors/cp312/starlette/schemas.py b/source/target-vendors/cp312/starlette/schemas.py new file mode 100644 index 0000000..484664c --- /dev/null +++ b/source/target-vendors/cp312/starlette/schemas.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +import inspect +import re +from collections.abc import Callable +from typing import Any, NamedTuple + +from starlette.requests import Request +from starlette.responses import Response +from starlette.routing import BaseRoute, Host, Mount, Route + +try: + import yaml +except ModuleNotFoundError: # pragma: no cover + yaml = None # type: ignore[assignment] + + +class OpenAPIResponse(Response): + media_type = "application/vnd.oai.openapi" + + def render(self, content: Any) -> bytes: + assert yaml is not None, "`pyyaml` must be installed to use OpenAPIResponse." + assert isinstance(content, dict), "The schema passed to OpenAPIResponse should be a dictionary." + return yaml.dump(content, default_flow_style=False).encode("utf-8") + + +class EndpointInfo(NamedTuple): + path: str + http_method: str + func: Callable[..., Any] + + +_remove_converter_pattern = re.compile(r":\w+}") + + +class BaseSchemaGenerator: + def get_schema(self, routes: list[BaseRoute]) -> dict[str, Any]: + raise NotImplementedError() # pragma: no cover + + def get_endpoints(self, routes: list[BaseRoute]) -> list[EndpointInfo]: + """ + Given the routes, yields the following information: + + - path + eg: /users/ + - http_method + one of 'get', 'post', 'put', 'patch', 'delete', 'options', 'query' + - func + method ready to extract the docstring + """ + endpoints_info: list[EndpointInfo] = [] + + for route in routes: + if isinstance(route, Mount | Host): + routes = route.routes or [] + if isinstance(route, Mount): + path = self._remove_converter(route.path) + else: + path = "" + sub_endpoints = [ + EndpointInfo( + path="".join((path, sub_endpoint.path)), + http_method=sub_endpoint.http_method, + func=sub_endpoint.func, + ) + for sub_endpoint in self.get_endpoints(routes) + ] + endpoints_info.extend(sub_endpoints) + + elif not isinstance(route, Route) or not route.include_in_schema: + continue + + elif inspect.isfunction(route.endpoint) or inspect.ismethod(route.endpoint): + path = self._remove_converter(route.path) + for method in route.methods or ["GET"]: + if method == "HEAD": + continue + endpoints_info.append(EndpointInfo(path, method.lower(), route.endpoint)) + else: + path = self._remove_converter(route.path) + for method in ["get", "post", "put", "patch", "delete", "options", "query"]: + if not hasattr(route.endpoint, method): + continue + func = getattr(route.endpoint, method) + endpoints_info.append(EndpointInfo(path, method.lower(), func)) + + return endpoints_info + + def _remove_converter(self, path: str) -> str: + """ + Remove the converter from the path. + For example, a route like this: + Route("/users/{id:int}", endpoint=get_user, methods=["GET"]) + Should be represented as `/users/{id}` in the OpenAPI schema. + """ + return _remove_converter_pattern.sub("}", path) + + def parse_docstring(self, func_or_method: Callable[..., Any]) -> dict[str, Any]: + """ + Given a function, parse the docstring as YAML and return a dictionary of info. + """ + docstring = func_or_method.__doc__ + if not docstring: + return {} + + assert yaml is not None, "`pyyaml` must be installed to use parse_docstring." + + # We support having regular docstrings before the schema + # definition. Here we return just the schema part from + # the docstring. + docstring = docstring.split("---")[-1] + + parsed = yaml.safe_load(docstring) + + if not isinstance(parsed, dict): + # A regular docstring (not yaml formatted) can return + # a simple string here, which wouldn't follow the schema. + return {} + + return parsed + + def OpenAPIResponse(self, request: Request) -> Response: + routes = request.app.routes + schema = self.get_schema(routes=routes) + return OpenAPIResponse(schema) + + +class SchemaGenerator(BaseSchemaGenerator): + def __init__(self, base_schema: dict[str, Any]) -> None: + self.base_schema = base_schema + + def get_schema(self, routes: list[BaseRoute]) -> dict[str, Any]: + schema = dict(self.base_schema) + schema.setdefault("paths", {}) + openapi_version_match = re.match(r"^(\d+)\.(\d+)", str(schema.get("openapi", ""))) + supports_query = openapi_version_match is not None and tuple(map(int, openapi_version_match.groups())) >= (3, 2) + endpoints_info = self.get_endpoints(routes) + + for endpoint in endpoints_info: + if endpoint.http_method == "query" and not supports_query: + continue + parsed = self.parse_docstring(endpoint.func) + + if not parsed: + continue + + if endpoint.path not in schema["paths"]: + schema["paths"][endpoint.path] = {} + + schema["paths"][endpoint.path][endpoint.http_method] = parsed + + return schema diff --git a/source/target-vendors/cp312/starlette/staticfiles.py b/source/target-vendors/cp312/starlette/staticfiles.py new file mode 100644 index 0000000..77b62e1 --- /dev/null +++ b/source/target-vendors/cp312/starlette/staticfiles.py @@ -0,0 +1,231 @@ +from __future__ import annotations + +import errno +import importlib.util +import os +import stat +from email.utils import parsedate +from typing import Union + +import anyio +import anyio.to_thread + +from starlette._utils import get_route_path +from starlette.datastructures import URL, Headers +from starlette.exceptions import HTTPException +from starlette.responses import FileResponse, RedirectResponse, Response +from starlette.types import Receive, Scope, Send +from starlette.websockets import WebSocketClose + +PathLike = Union[str, "os.PathLike[str]"] + + +class NotModifiedResponse(Response): + NOT_MODIFIED_HEADERS = ( + "cache-control", + "content-location", + "date", + "etag", + "expires", + "vary", + ) + + def __init__(self, headers: Headers): + super().__init__( + status_code=304, + headers={name: value for name, value in headers.items() if name in self.NOT_MODIFIED_HEADERS}, + ) + + +class StaticFiles: + def __init__( + self, + *, + directory: PathLike | None = None, + packages: list[str | tuple[str, str]] | None = None, + html: bool = False, + check_dir: bool = True, + follow_symlink: bool = False, + ) -> None: + self.directory = directory + self.packages = packages + self.all_directories = self.get_directories(directory, packages) + self.html = html + self.config_checked = False + self.follow_symlink = follow_symlink + if check_dir and directory is not None and not os.path.isdir(directory): + raise RuntimeError(f"Directory '{directory}' does not exist") + + def get_directories( + self, + directory: PathLike | None = None, + packages: list[str | tuple[str, str]] | None = None, + ) -> list[PathLike]: + """ + Given `directory` and `packages` arguments, return a list of all the + directories that should be used for serving static files from. + """ + directories = [] + if directory is not None: + directories.append(directory) + + for package in packages or []: + if isinstance(package, tuple): + package, statics_dir = package + else: + statics_dir = "statics" + spec = importlib.util.find_spec(package) + assert spec is not None, f"Package {package!r} could not be found." + assert spec.origin is not None, f"Package {package!r} could not be found." + package_directory = os.path.normpath(os.path.join(spec.origin, "..", statics_dir)) + assert os.path.isdir(package_directory), ( + f"Directory '{statics_dir!r}' in package {package!r} could not be found." + ) + directories.append(package_directory) + + return directories + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + """ + The ASGI entry point. + """ + if scope["type"] == "websocket": + websocket_close = WebSocketClose() + await websocket_close(scope, receive, send) + return + + assert scope["type"] == "http" + + if not self.config_checked: + await self.check_config() + self.config_checked = True + + path = self.get_path(scope) + response = await self.get_response(path, scope) + await response(scope, receive, send) + + def get_path(self, scope: Scope) -> str: + """ + Given the ASGI scope, return the `path` string to serve up, + with OS specific path separators, and any '..', '.' components removed. + """ + route_path = get_route_path(scope) + return os.path.normpath(os.path.join(*route_path.split("/"))) + + async def get_response(self, path: str, scope: Scope) -> Response: + """ + Returns an HTTP response, given the incoming path, method and request headers. + """ + if scope["method"] not in ("GET", "HEAD"): + raise HTTPException(status_code=405) + + try: + full_path, stat_result = await anyio.to_thread.run_sync(self.lookup_path, path) + except PermissionError: + raise HTTPException(status_code=401) + except OSError as exc: + # Filename is too long, so it can't be a valid static file. + if exc.errno == errno.ENAMETOOLONG: + raise HTTPException(status_code=404) + + raise exc + except ValueError: + # Null bytes or other invalid characters in the path. + raise HTTPException(status_code=404) + + if stat_result and stat.S_ISREG(stat_result.st_mode): + # We have a static file to serve. + return self.file_response(full_path, stat_result, scope) + + elif stat_result and stat.S_ISDIR(stat_result.st_mode) and self.html: + # We're in HTML mode, and have got a directory URL. + # Check if we have 'index.html' file to serve. + index_path = os.path.join(path, "index.html") + full_path, stat_result = await anyio.to_thread.run_sync(self.lookup_path, index_path) + if stat_result is not None and stat.S_ISREG(stat_result.st_mode): + if not scope["path"].endswith("/"): + # Directory URLs should redirect to always end in "/". + url = URL(scope=scope) + url = url.replace(path=url.path + "/") + return RedirectResponse(url=url) + return self.file_response(full_path, stat_result, scope) + + if self.html: + # Check for '404.html' if we're in HTML mode. + full_path, stat_result = await anyio.to_thread.run_sync(self.lookup_path, "404.html") + if stat_result and stat.S_ISREG(stat_result.st_mode): + return FileResponse(full_path, stat_result=stat_result, status_code=404) + raise HTTPException(status_code=404) + + def lookup_path(self, path: str) -> tuple[str, os.stat_result | None]: + # Reject absolute paths so they cannot escape the served directory. + if path.startswith(("/", "\\")): + return "", None + for directory in self.all_directories: + joined_path = os.path.join(directory, path) + if self.follow_symlink: + full_path = os.path.abspath(joined_path) + directory = os.path.abspath(directory) + else: + full_path = os.path.realpath(joined_path) + directory = os.path.realpath(directory) + if os.path.commonpath([full_path, directory]) != str(directory): + # Don't allow misbehaving clients to break out of the static files directory. + continue + try: + return full_path, os.stat(full_path) + except (FileNotFoundError, NotADirectoryError): + continue + return "", None + + def file_response( + self, + full_path: PathLike, + stat_result: os.stat_result, + scope: Scope, + status_code: int = 200, + ) -> Response: + request_headers = Headers(scope=scope) + + response = FileResponse(full_path, status_code=status_code, stat_result=stat_result) + if self.is_not_modified(response.headers, request_headers): + return NotModifiedResponse(response.headers) + return response + + async def check_config(self) -> None: + """ + Perform a one-off configuration check that StaticFiles is actually + pointed at a directory, so that we can raise loud errors rather than + just returning 404 responses. + """ + if self.directory is None: + return + + try: + stat_result = await anyio.to_thread.run_sync(os.stat, self.directory) + except FileNotFoundError: + raise RuntimeError(f"StaticFiles directory '{self.directory}' does not exist.") + if not (stat.S_ISDIR(stat_result.st_mode) or stat.S_ISLNK(stat_result.st_mode)): + raise RuntimeError(f"StaticFiles path '{self.directory}' is not a directory.") + + def is_not_modified(self, response_headers: Headers, request_headers: Headers) -> bool: + """ + Given the request and response headers, return `True` if an HTTP + "Not Modified" response could be returned instead. + """ + if if_none_match := request_headers.get("if-none-match"): + if if_none_match.strip() == "*": + return True + # The "etag" header is added by FileResponse, so it's always present. + etag = response_headers["etag"] + return etag in [tag.strip().removeprefix("W/") for tag in if_none_match.split(",")] + + try: + if_modified_since = parsedate(request_headers["if-modified-since"]) + last_modified = parsedate(response_headers["last-modified"]) + if if_modified_since is not None and last_modified is not None and if_modified_since >= last_modified: + return True + except KeyError: + pass + + return False diff --git a/source/target-vendors/cp312/starlette/status.py b/source/target-vendors/cp312/starlette/status.py new file mode 100644 index 0000000..202b6b5 --- /dev/null +++ b/source/target-vendors/cp312/starlette/status.py @@ -0,0 +1,211 @@ +""" +HTTP codes +See HTTP Status Code Registry: +https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + +And RFC 9110 - https://www.rfc-editor.org/rfc/rfc9110 +""" + +from __future__ import annotations + +import warnings + +from starlette.exceptions import StarletteDeprecationWarning + +__all__ = [ + "HTTP_100_CONTINUE", + "HTTP_101_SWITCHING_PROTOCOLS", + "HTTP_102_PROCESSING", + "HTTP_103_EARLY_HINTS", + "HTTP_200_OK", + "HTTP_201_CREATED", + "HTTP_202_ACCEPTED", + "HTTP_203_NON_AUTHORITATIVE_INFORMATION", + "HTTP_204_NO_CONTENT", + "HTTP_205_RESET_CONTENT", + "HTTP_206_PARTIAL_CONTENT", + "HTTP_207_MULTI_STATUS", + "HTTP_208_ALREADY_REPORTED", + "HTTP_226_IM_USED", + "HTTP_300_MULTIPLE_CHOICES", + "HTTP_301_MOVED_PERMANENTLY", + "HTTP_302_FOUND", + "HTTP_303_SEE_OTHER", + "HTTP_304_NOT_MODIFIED", + "HTTP_305_USE_PROXY", + "HTTP_306_RESERVED", + "HTTP_307_TEMPORARY_REDIRECT", + "HTTP_308_PERMANENT_REDIRECT", + "HTTP_400_BAD_REQUEST", + "HTTP_401_UNAUTHORIZED", + "HTTP_402_PAYMENT_REQUIRED", + "HTTP_403_FORBIDDEN", + "HTTP_404_NOT_FOUND", + "HTTP_405_METHOD_NOT_ALLOWED", + "HTTP_406_NOT_ACCEPTABLE", + "HTTP_407_PROXY_AUTHENTICATION_REQUIRED", + "HTTP_408_REQUEST_TIMEOUT", + "HTTP_409_CONFLICT", + "HTTP_410_GONE", + "HTTP_411_LENGTH_REQUIRED", + "HTTP_412_PRECONDITION_FAILED", + "HTTP_413_CONTENT_TOO_LARGE", + "HTTP_414_URI_TOO_LONG", + "HTTP_415_UNSUPPORTED_MEDIA_TYPE", + "HTTP_416_RANGE_NOT_SATISFIABLE", + "HTTP_417_EXPECTATION_FAILED", + "HTTP_418_IM_A_TEAPOT", + "HTTP_421_MISDIRECTED_REQUEST", + "HTTP_422_UNPROCESSABLE_CONTENT", + "HTTP_423_LOCKED", + "HTTP_424_FAILED_DEPENDENCY", + "HTTP_425_TOO_EARLY", + "HTTP_426_UPGRADE_REQUIRED", + "HTTP_428_PRECONDITION_REQUIRED", + "HTTP_429_TOO_MANY_REQUESTS", + "HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE", + "HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS", + "HTTP_500_INTERNAL_SERVER_ERROR", + "HTTP_501_NOT_IMPLEMENTED", + "HTTP_502_BAD_GATEWAY", + "HTTP_503_SERVICE_UNAVAILABLE", + "HTTP_504_GATEWAY_TIMEOUT", + "HTTP_505_HTTP_VERSION_NOT_SUPPORTED", + "HTTP_506_VARIANT_ALSO_NEGOTIATES", + "HTTP_507_INSUFFICIENT_STORAGE", + "HTTP_508_LOOP_DETECTED", + "HTTP_510_NOT_EXTENDED", + "HTTP_511_NETWORK_AUTHENTICATION_REQUIRED", + "WS_1000_NORMAL_CLOSURE", + "WS_1001_GOING_AWAY", + "WS_1002_PROTOCOL_ERROR", + "WS_1003_UNSUPPORTED_DATA", + "WS_1005_NO_STATUS_RCVD", + "WS_1006_ABNORMAL_CLOSURE", + "WS_1007_INVALID_FRAME_PAYLOAD_DATA", + "WS_1008_POLICY_VIOLATION", + "WS_1009_MESSAGE_TOO_BIG", + "WS_1010_MANDATORY_EXT", + "WS_1011_INTERNAL_ERROR", + "WS_1012_SERVICE_RESTART", + "WS_1013_TRY_AGAIN_LATER", + "WS_1014_BAD_GATEWAY", + "WS_1015_TLS_HANDSHAKE", +] + +HTTP_100_CONTINUE = 100 +HTTP_101_SWITCHING_PROTOCOLS = 101 +HTTP_102_PROCESSING = 102 +HTTP_103_EARLY_HINTS = 103 +HTTP_200_OK = 200 +HTTP_201_CREATED = 201 +HTTP_202_ACCEPTED = 202 +HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203 +HTTP_204_NO_CONTENT = 204 +HTTP_205_RESET_CONTENT = 205 +HTTP_206_PARTIAL_CONTENT = 206 +HTTP_207_MULTI_STATUS = 207 +HTTP_208_ALREADY_REPORTED = 208 +HTTP_226_IM_USED = 226 +HTTP_300_MULTIPLE_CHOICES = 300 +HTTP_301_MOVED_PERMANENTLY = 301 +HTTP_302_FOUND = 302 +HTTP_303_SEE_OTHER = 303 +HTTP_304_NOT_MODIFIED = 304 +HTTP_305_USE_PROXY = 305 +HTTP_306_RESERVED = 306 +HTTP_307_TEMPORARY_REDIRECT = 307 +HTTP_308_PERMANENT_REDIRECT = 308 +HTTP_400_BAD_REQUEST = 400 +HTTP_401_UNAUTHORIZED = 401 +HTTP_402_PAYMENT_REQUIRED = 402 +HTTP_403_FORBIDDEN = 403 +HTTP_404_NOT_FOUND = 404 +HTTP_405_METHOD_NOT_ALLOWED = 405 +HTTP_406_NOT_ACCEPTABLE = 406 +HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407 +HTTP_408_REQUEST_TIMEOUT = 408 +HTTP_409_CONFLICT = 409 +HTTP_410_GONE = 410 +HTTP_411_LENGTH_REQUIRED = 411 +HTTP_412_PRECONDITION_FAILED = 412 +HTTP_413_CONTENT_TOO_LARGE = 413 +HTTP_414_URI_TOO_LONG = 414 +HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415 +HTTP_416_RANGE_NOT_SATISFIABLE = 416 +HTTP_417_EXPECTATION_FAILED = 417 +HTTP_418_IM_A_TEAPOT = 418 +HTTP_421_MISDIRECTED_REQUEST = 421 +HTTP_422_UNPROCESSABLE_CONTENT = 422 +HTTP_423_LOCKED = 423 +HTTP_424_FAILED_DEPENDENCY = 424 +HTTP_425_TOO_EARLY = 425 +HTTP_426_UPGRADE_REQUIRED = 426 +HTTP_428_PRECONDITION_REQUIRED = 428 +HTTP_429_TOO_MANY_REQUESTS = 429 +HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431 +HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS = 451 +HTTP_500_INTERNAL_SERVER_ERROR = 500 +HTTP_501_NOT_IMPLEMENTED = 501 +HTTP_502_BAD_GATEWAY = 502 +HTTP_503_SERVICE_UNAVAILABLE = 503 +HTTP_504_GATEWAY_TIMEOUT = 504 +HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505 +HTTP_506_VARIANT_ALSO_NEGOTIATES = 506 +HTTP_507_INSUFFICIENT_STORAGE = 507 +HTTP_508_LOOP_DETECTED = 508 +HTTP_510_NOT_EXTENDED = 510 +HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511 + + +""" +WebSocket codes +https://www.iana.org/assignments/websocket/websocket.xml#close-code-number +https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent +""" +WS_1000_NORMAL_CLOSURE = 1000 +WS_1001_GOING_AWAY = 1001 +WS_1002_PROTOCOL_ERROR = 1002 +WS_1003_UNSUPPORTED_DATA = 1003 +WS_1005_NO_STATUS_RCVD = 1005 +WS_1006_ABNORMAL_CLOSURE = 1006 +WS_1007_INVALID_FRAME_PAYLOAD_DATA = 1007 +WS_1008_POLICY_VIOLATION = 1008 +WS_1009_MESSAGE_TOO_BIG = 1009 +WS_1010_MANDATORY_EXT = 1010 +WS_1011_INTERNAL_ERROR = 1011 +WS_1012_SERVICE_RESTART = 1012 +WS_1013_TRY_AGAIN_LATER = 1013 +WS_1014_BAD_GATEWAY = 1014 +WS_1015_TLS_HANDSHAKE = 1015 + +__deprecated__ = { + "HTTP_413_REQUEST_ENTITY_TOO_LARGE": 413, + "HTTP_414_REQUEST_URI_TOO_LONG": 414, + "HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE": 416, + "HTTP_422_UNPROCESSABLE_ENTITY": 422, +} + + +def __getattr__(name: str) -> int: + deprecation_changes = { + "HTTP_413_REQUEST_ENTITY_TOO_LARGE": "HTTP_413_CONTENT_TOO_LARGE", + "HTTP_414_REQUEST_URI_TOO_LONG": "HTTP_414_URI_TOO_LONG", + "HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE": "HTTP_416_RANGE_NOT_SATISFIABLE", + "HTTP_422_UNPROCESSABLE_ENTITY": "HTTP_422_UNPROCESSABLE_CONTENT", + } + + deprecated = __deprecated__.get(name) + if deprecated: + warnings.warn( + f"'{name}' is deprecated. Use '{deprecation_changes[name]}' instead.", + category=StarletteDeprecationWarning, + stacklevel=3, + ) + return deprecated + + raise AttributeError(f"module 'starlette.status' has no attribute '{name}'") + + +def __dir__() -> list[str]: + return sorted(list(__all__) + list(__deprecated__.keys())) # pragma: no cover diff --git a/source/target-vendors/cp312/starlette/templating.py b/source/target-vendors/cp312/starlette/templating.py new file mode 100644 index 0000000..67872b3 --- /dev/null +++ b/source/target-vendors/cp312/starlette/templating.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +from collections.abc import Callable, Mapping, Sequence +from os import PathLike +from typing import TYPE_CHECKING, Any, overload + +from starlette.background import BackgroundTask +from starlette.datastructures import URL +from starlette.requests import Request +from starlette.responses import HTMLResponse +from starlette.types import Receive, Scope, Send + +try: + import jinja2 + + # @contextfunction was renamed to @pass_context in Jinja 3.0, and was removed in 3.1 + # hence we try to get pass_context (most installs will be >=3.1) + # and fall back to contextfunction, + # adding a type ignore for mypy to let us access an attribute that may not exist + if TYPE_CHECKING: + pass_context = jinja2.pass_context + else: + if hasattr(jinja2, "pass_context"): + pass_context = jinja2.pass_context + else: # pragma: no cover + pass_context = jinja2.contextfunction # type: ignore[attr-defined] +except ImportError as _import_error: # pragma: no cover + raise ImportError("jinja2 must be installed to use Jinja2Templates") from _import_error + + +class _TemplateResponse(HTMLResponse): + def __init__( + self, + template: Any, + context: dict[str, Any], + status_code: int = 200, + headers: Mapping[str, str] | None = None, + media_type: str | None = None, + background: BackgroundTask | None = None, + ): + self.template = template + self.context = context + content = template.render(context) + super().__init__(content, status_code, headers, media_type, background) + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + request = self.context.get("request", {}) + extensions = request.get("extensions", {}) + if "http.response.debug" in extensions: # pragma: no branch + await send({"type": "http.response.debug", "info": {"template": self.template, "context": self.context}}) + await super().__call__(scope, receive, send) + + +class Jinja2Templates: + """Jinja2 template renderer. + + Example: + ```python + from starlette.templating import Jinja2Templates + + templates = Jinja2Templates(directory="templates") + + async def homepage(request: Request) -> Response: + return templates.TemplateResponse(request, "index.html") + ``` + """ + + @overload + def __init__( + self, + directory: str | PathLike[str] | Sequence[str | PathLike[str]], + *, + context_processors: list[Callable[[Request], dict[str, Any]]] | None = None, + ) -> None: ... + + @overload + def __init__( + self, + *, + env: jinja2.Environment, + context_processors: list[Callable[[Request], dict[str, Any]]] | None = None, + ) -> None: ... + + def __init__( + self, + directory: str | PathLike[str] | Sequence[str | PathLike[str]] | None = None, + *, + context_processors: list[Callable[[Request], dict[str, Any]]] | None = None, + env: jinja2.Environment | None = None, + ) -> None: + assert bool(directory) ^ bool(env), "either 'directory' or 'env' arguments must be passed" + self.context_processors = context_processors or [] + if directory is not None: + loader = jinja2.FileSystemLoader(directory) + self.env = jinja2.Environment(loader=loader, autoescape=jinja2.select_autoescape()) + elif env is not None: # pragma: no branch + self.env = env + + self._setup_env_defaults(self.env) + + def _setup_env_defaults(self, env: jinja2.Environment) -> None: + @pass_context + def url_for( + context: dict[str, Any], + name: str, + /, + **path_params: Any, + ) -> URL: + request: Request = context["request"] + return request.url_for(name, **path_params) + + env.globals.setdefault("url_for", url_for) + + def get_template(self, name: str) -> jinja2.Template: + return self.env.get_template(name) + + def TemplateResponse( + self, + request: Request, + name: str, + context: dict[str, Any] | None = None, + status_code: int = 200, + headers: Mapping[str, str] | None = None, + media_type: str | None = None, + background: BackgroundTask | None = None, + ) -> _TemplateResponse: + """ + Render a template and return an HTML response. + + Args: + request: The incoming request instance. + name: The template file name to render. + context: Variables to pass to the template. + status_code: HTTP status code for the response. + headers: Additional headers to include in the response. + media_type: Media type for the response. + background: Background task to run after response is sent. + + Returns: + An HTML response with the rendered template content. + """ + context = context or {} + + context.setdefault("request", request) + for context_processor in self.context_processors: + context.update(context_processor(request)) + + template = self.get_template(name) + return _TemplateResponse( + template, + context, + status_code=status_code, + headers=headers, + media_type=media_type, + background=background, + ) diff --git a/source/target-vendors/cp312/starlette/testclient.py b/source/target-vendors/cp312/starlette/testclient.py new file mode 100644 index 0000000..2f3f468 --- /dev/null +++ b/source/target-vendors/cp312/starlette/testclient.py @@ -0,0 +1,568 @@ +from __future__ import annotations + +import contextlib +import inspect +import io +import json +import math +import sys +import warnings +from collections.abc import Awaitable, Callable, Generator, Iterable, Mapping, Sequence +from concurrent.futures import Future +from contextlib import AbstractContextManager +from types import GeneratorType +from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeGuard, cast +from urllib.parse import unquote, urljoin + +import anyio +import anyio.abc +import anyio.from_thread +from anyio.streams.stapled import StapledObjectStream + +from starlette._utils import is_async_callable +from starlette.exceptions import StarletteDeprecationWarning +from starlette.types import ASGIApp, Message, Receive, Scope, Send +from starlette.websockets import WebSocketDisconnect + +if sys.version_info >= (3, 11): # pragma: no cover + from typing import Self +else: # pragma: no cover + from typing_extensions import Self + +if TYPE_CHECKING: + import httpx2 as httpx +else: + try: + import httpx2 as httpx + except ModuleNotFoundError: # pragma: no cover + try: + import httpx + except ModuleNotFoundError: + raise RuntimeError( + "The starlette.testclient module requires the httpx2 package to be installed.\n" + "You can install this with:\n" + " $ pip install httpx2\n" + ) from None + else: + warnings.warn( + "Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.", + StarletteDeprecationWarning, + stacklevel=2, + ) + +_PortalFactoryType = Callable[[], AbstractContextManager[anyio.from_thread.BlockingPortal]] + +ASGIInstance = Callable[[Receive, Send], Awaitable[None]] +ASGI2App = Callable[[Scope], ASGIInstance] +ASGI3App = Callable[[Scope, Receive, Send], Awaitable[None]] + + +_RequestData = Mapping[str, str | Iterable[str] | bytes] + + +def _is_asgi3(app: ASGI2App | ASGI3App) -> TypeGuard[ASGI3App]: + if inspect.isclass(app): + return hasattr(app, "__await__") + return is_async_callable(app) + + +class _WrapASGI2: + """ + Provide an ASGI3 interface onto an ASGI2 app. + """ + + def __init__(self, app: ASGI2App) -> None: + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + instance = self.app(scope) + await instance(receive, send) + + +class _AsyncBackend(TypedDict): + backend: str + backend_options: dict[str, Any] + + +class _Upgrade(Exception): + def __init__(self, session: WebSocketTestSession) -> None: + self.session = session + + +class WebSocketDenialResponse( # type: ignore[misc] + httpx.Response, + WebSocketDisconnect, +): + """ + A special case of `WebSocketDisconnect`, raised in the `TestClient` if the + `WebSocket` is closed before being accepted with a `send_denial_response()`. + """ + + +class WebSocketTestSession: + def __init__( + self, + app: ASGI3App, + scope: Scope, + portal_factory: _PortalFactoryType, + ) -> None: + self.app = app + self.scope = scope + self.accepted_subprotocol = None + self.portal_factory = portal_factory + self.extra_headers = None + + def __enter__(self) -> Self: + with contextlib.ExitStack() as stack: + self.portal = portal = stack.enter_context(self.portal_factory()) + fut, cs = portal.start_task(self._run) + stack.callback(fut.result) + stack.callback(portal.call, cs.cancel) + self.send({"type": "websocket.connect"}) + message = self.receive() + self._raise_on_close(message) + self.accepted_subprotocol = message.get("subprotocol", None) + self.extra_headers = message.get("headers", None) + stack.callback(self.close, 1000) + self.exit_stack = stack.pop_all() + return self + + def __exit__(self, *args: Any) -> bool | None: + return self.exit_stack.__exit__(*args) + + async def _run(self, *, task_status: anyio.abc.TaskStatus[anyio.CancelScope]) -> None: + """ + The sub-thread in which the websocket session runs. + """ + send_tx, send_rx = anyio.create_memory_object_stream[Message](math.inf) + receive_tx, receive_rx = anyio.create_memory_object_stream[Message](math.inf) + with send_tx, send_rx, receive_tx, receive_rx, anyio.CancelScope() as cs: + self._receive_tx = receive_tx + self._send_rx = send_rx + task_status.started(cs) + await self.app(self.scope, receive_rx.receive, send_tx.send) + + # wait for cs.cancel to be called before closing streams + await anyio.sleep_forever() + + def _raise_on_close(self, message: Message) -> None: + if message["type"] == "websocket.close": + raise WebSocketDisconnect(code=message.get("code", 1000), reason=message.get("reason", "")) + elif message["type"] == "websocket.http.response.start": + status_code: int = message["status"] + headers: list[tuple[bytes, bytes]] = message["headers"] + body: list[bytes] = [] + while True: + message = self.receive() + assert message["type"] == "websocket.http.response.body" + body.append(message["body"]) + if not message.get("more_body", False): + break + raise WebSocketDenialResponse(status_code=status_code, headers=headers, content=b"".join(body)) + + def send(self, message: Message) -> None: + self.portal.call(self._receive_tx.send, message) + + def send_text(self, data: str) -> None: + self.send({"type": "websocket.receive", "text": data}) + + def send_bytes(self, data: bytes) -> None: + self.send({"type": "websocket.receive", "bytes": data}) + + def send_json(self, data: Any, mode: Literal["text", "binary"] = "text") -> None: + text = json.dumps(data, separators=(",", ":"), ensure_ascii=False) + if mode == "text": + self.send({"type": "websocket.receive", "text": text}) + else: + self.send({"type": "websocket.receive", "bytes": text.encode("utf-8")}) + + def close(self, code: int = 1000, reason: str | None = None) -> None: + self.send({"type": "websocket.disconnect", "code": code, "reason": reason}) + + def receive(self) -> Message: + return self.portal.call(self._send_rx.receive) + + def receive_text(self) -> str: + message = self.receive() + self._raise_on_close(message) + return cast(str, message["text"]) + + def receive_bytes(self) -> bytes: + message = self.receive() + self._raise_on_close(message) + return cast(bytes, message["bytes"]) + + def receive_json(self, mode: Literal["text", "binary"] = "text") -> Any: + message = self.receive() + self._raise_on_close(message) + if mode == "text": + text = message["text"] + else: + text = message["bytes"].decode("utf-8") + return json.loads(text) + + +class _TestClientTransport(httpx.BaseTransport): + def __init__( + self, + app: ASGI3App, + portal_factory: _PortalFactoryType, + raise_server_exceptions: bool = True, + root_path: str = "", + *, + client: tuple[str, int], + app_state: dict[str, Any], + ) -> None: + self.app = app + self.raise_server_exceptions = raise_server_exceptions + self.root_path = root_path + self.portal_factory = portal_factory + self.app_state = app_state + self.client = client + + def handle_request(self, request: httpx.Request) -> httpx.Response: + scheme = request.url.scheme + host = request.url.raw_host.decode(encoding="ascii") + path = request.url.path + raw_path = request.url.raw_path + query = request.url.query.decode(encoding="ascii") + + default_port = {"http": 80, "ws": 80, "https": 443, "wss": 443}[scheme] + port = request.url.port + if port is None: + port = default_port + + # Include the 'host' header. + if "host" in request.headers: + headers: list[tuple[bytes, bytes]] = [] + else: # pragma: no cover + headers = [(b"host", request.url.netloc)] + + # Include other request headers. + headers += [(key.lower().encode(), value.encode()) for key, value in request.headers.multi_items()] + + scope: dict[str, Any] + + if scheme in {"ws", "wss"}: + subprotocol = request.headers.get("sec-websocket-protocol", None) + if subprotocol is None: + subprotocols: Sequence[str] = [] + else: + subprotocols = [value.strip() for value in subprotocol.split(",")] + scope = { + "type": "websocket", + "path": unquote(path), + "raw_path": raw_path.split(b"?", 1)[0], + "root_path": self.root_path, + "scheme": scheme, + "query_string": query.encode(), + "headers": headers, + "client": self.client, + "server": [host, port], + "subprotocols": subprotocols, + "state": self.app_state.copy(), + "extensions": {"websocket.http.response": {}}, + } + session = WebSocketTestSession(self.app, scope, self.portal_factory) + raise _Upgrade(session) + + scope = { + "type": "http", + "http_version": "1.1", + "method": request.method, + "path": unquote(path), + "raw_path": raw_path.split(b"?", 1)[0], + "root_path": self.root_path, + "scheme": scheme, + "query_string": query.encode(), + "headers": headers, + "client": self.client, + "server": [host, port], + "extensions": {"http.response.debug": {}, "http.response.trailers": {}}, + "state": self.app_state.copy(), + } + + request_complete = False + response_started = False + body_complete = False + trailers_expected = False + trailers: list[tuple[bytes, bytes]] = [] + response_complete: anyio.Event + raw_kwargs: dict[str, Any] = {"stream": io.BytesIO()} + debug_info: dict[str, Any] | None = None + + async def receive() -> Message: + nonlocal request_complete + + if request_complete: + if not response_complete.is_set(): + await response_complete.wait() + return {"type": "http.disconnect"} + + body = request.read() + if isinstance(body, str): + body_bytes: bytes = body.encode("utf-8") # pragma: no cover + elif body is None: + body_bytes = b"" # pragma: no cover + elif isinstance(body, GeneratorType): + try: # pragma: no cover + chunk = body.send(None) + if isinstance(chunk, str): + chunk = chunk.encode("utf-8") + return {"type": "http.request", "body": chunk, "more_body": True} + except StopIteration: # pragma: no cover + request_complete = True + return {"type": "http.request", "body": b""} + else: + body_bytes = body + + request_complete = True + return {"type": "http.request", "body": body_bytes} + + async def send(message: Message) -> None: + nonlocal raw_kwargs, response_started, debug_info, body_complete, trailers_expected + + if message["type"] == "http.response.start": + assert not response_started, 'Received multiple "http.response.start" messages.' + raw_kwargs["status_code"] = message["status"] + raw_kwargs["headers"] = [(key.decode(), value.decode()) for key, value in message.get("headers", [])] + response_started = True + trailers_expected = message.get("trailers", False) + elif message["type"] == "http.response.body": + assert response_started, 'Received "http.response.body" without "http.response.start".' + assert not response_complete.is_set(), 'Received "http.response.body" after response completed.' + assert not body_complete, 'Received "http.response.body" after body completed.' + body = message.get("body", b"") + more_body = message.get("more_body", False) + if request.method != "HEAD": + raw_kwargs["stream"].write(body) + if not more_body: + raw_kwargs["stream"].seek(0) + body_complete = True + if not trailers_expected: + response_complete.set() + elif message["type"] == "http.response.trailers": + assert trailers_expected, 'Received "http.response.trailers" without declaring trailers.' + assert body_complete, 'Received "http.response.trailers" before body completed.' + assert not response_complete.is_set(), 'Received "http.response.trailers" after response completed.' + trailers.extend(message.get("headers", [])) + if not message.get("more_trailers", False): + response_complete.set() + elif message["type"] == "http.response.debug": + debug_info = message["info"] + + try: + with self.portal_factory() as portal: + response_complete = portal.call(anyio.Event) + portal.call(self.app, scope, receive, send) + except BaseException as exc: + if self.raise_server_exceptions: + raise exc + + if self.raise_server_exceptions: + assert response_started, "TestClient did not receive any response." + elif not response_started: + raw_kwargs = { + "status_code": 500, + "headers": [], + "stream": io.BytesIO(), + } + + raw_kwargs["stream"] = httpx.ByteStream(raw_kwargs["stream"].read()) + + response = httpx.Response(**raw_kwargs, request=request) + if trailers_expected: + response.extensions["http.response.trailers"] = trailers + if debug_info is not None: + response.extensions["http.response.debug"] = debug_info + if "template" in debug_info: + response.template = debug_info["template"] # type: ignore[attr-defined] + if "context" in debug_info: + response.context = debug_info["context"] # type: ignore[attr-defined] + return response + + +class TestClient(httpx.Client): + __test__ = False + task: Future[None] + portal: anyio.from_thread.BlockingPortal | None = None + + def __init__( + self, + app: ASGIApp, + base_url: str = "http://testserver", + raise_server_exceptions: bool = True, + root_path: str = "", + backend: Literal["asyncio", "trio"] = "asyncio", + backend_options: dict[str, Any] | None = None, + cookies: httpx._types.CookieTypes | None = None, + headers: dict[str, str] | None = None, + follow_redirects: bool = True, + client: tuple[str, int] = ("testclient", 50000), + ) -> None: + self.async_backend = _AsyncBackend(backend=backend, backend_options=backend_options or {}) + if _is_asgi3(app): + asgi_app = app + else: + app = cast(ASGI2App, app) # type: ignore[assignment] + asgi_app = _WrapASGI2(app) # type: ignore[arg-type] + self.app = asgi_app + self.app_state: dict[str, Any] = {} + transport = _TestClientTransport( + self.app, + portal_factory=self._portal_factory, + raise_server_exceptions=raise_server_exceptions, + root_path=root_path, + app_state=self.app_state, + client=client, + ) + if headers is None: + headers = {} + headers.setdefault("user-agent", "testclient") + super().__init__( + base_url=base_url, + headers=headers, + transport=transport, + follow_redirects=follow_redirects, + cookies=cookies, + ) + + @contextlib.contextmanager + def _portal_factory(self) -> Generator[anyio.from_thread.BlockingPortal, None, None]: + if self.portal is not None: + yield self.portal + else: + with anyio.from_thread.start_blocking_portal(**self.async_backend) as portal: + yield portal + + def request( # type: ignore[override] + self, + method: str, + url: httpx._types.URLTypes, + *, + content: httpx._types.RequestContent | None = None, + data: _RequestData | None = None, + files: httpx._types.RequestFiles | None = None, + json: Any = None, + params: httpx._types.QueryParamTypes | None = None, + headers: httpx._types.HeaderTypes | None = None, + cookies: httpx._types.CookieTypes | None = None, + auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + extensions: dict[str, Any] | None = None, + ) -> httpx.Response: + if timeout is not httpx.USE_CLIENT_DEFAULT: + warnings.warn( + "You should not use the 'timeout' argument with the TestClient. " + "See https://github.com/Kludex/starlette/issues/1108 for more information.", + StarletteDeprecationWarning, + stacklevel=2, + ) + url = self._merge_url(url) + return super().request( + method, + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def websocket_connect( + self, + url: str, + subprotocols: Sequence[str] | None = None, + **kwargs: Any, + ) -> WebSocketTestSession: + url = urljoin("ws://testserver", url) + headers = kwargs.get("headers", {}) + headers.setdefault("connection", "upgrade") + headers.setdefault("sec-websocket-key", "testserver==") + headers.setdefault("sec-websocket-version", "13") + if subprotocols is not None: + headers.setdefault("sec-websocket-protocol", ", ".join(subprotocols)) + kwargs["headers"] = headers + try: + super().request("GET", url, **kwargs) + except _Upgrade as exc: + session = exc.session + else: + raise RuntimeError("Expected WebSocket upgrade") # pragma: no cover + + return session + + def __enter__(self) -> Self: + with contextlib.ExitStack() as stack: + self.portal = portal = stack.enter_context(anyio.from_thread.start_blocking_portal(**self.async_backend)) + + @stack.callback + def reset_portal() -> None: + self.portal = None + + send = anyio.create_memory_object_stream[Message | None](math.inf) + receive = anyio.create_memory_object_stream[Message](math.inf) + for channel in (*send, *receive): + stack.callback(channel.close) + self.stream_send = StapledObjectStream(*send) + self.stream_receive = StapledObjectStream(*receive) + self.task = portal.start_task_soon(self.lifespan) + portal.call(self.wait_startup) + + @stack.callback + def wait_shutdown() -> None: + portal.call(self.wait_shutdown) + + self.exit_stack = stack.pop_all() + + return self + + def __exit__(self, *args: Any) -> None: + self.exit_stack.close() + + async def lifespan(self) -> None: + scope = {"type": "lifespan", "state": self.app_state} + try: + await self.app(scope, self.stream_receive.receive, self.stream_send.send) + finally: + await self.stream_send.send(None) + + async def wait_startup(self) -> None: + await self.stream_receive.send({"type": "lifespan.startup"}) + + async def receive() -> Any: + message = await self.stream_send.receive() + if message is None: + self.task.result() + return message + + message = await receive() + assert message["type"] in ( + "lifespan.startup.complete", + "lifespan.startup.failed", + ) + if message["type"] == "lifespan.startup.failed": + await receive() + + async def wait_shutdown(self) -> None: + async def receive() -> Any: + message = await self.stream_send.receive() + if message is None: + self.task.result() + return message + + await self.stream_receive.send({"type": "lifespan.shutdown"}) + message = await receive() + assert message["type"] in ( + "lifespan.shutdown.complete", + "lifespan.shutdown.failed", + ) + if message["type"] == "lifespan.shutdown.failed": + await receive() diff --git a/source/target-vendors/cp312/starlette/types.py b/source/target-vendors/cp312/starlette/types.py new file mode 100644 index 0000000..b767ae1 --- /dev/null +++ b/source/target-vendors/cp312/starlette/types.py @@ -0,0 +1,26 @@ +from collections.abc import Awaitable, Callable, Mapping, MutableMapping +from contextlib import AbstractAsyncContextManager +from typing import TYPE_CHECKING, Any, TypeVar + +if TYPE_CHECKING: + from starlette.requests import Request + from starlette.responses import Response + from starlette.websockets import WebSocket + +AppType = TypeVar("AppType") + +Scope = MutableMapping[str, Any] +Message = MutableMapping[str, Any] + +Receive = Callable[[], Awaitable[Message]] +Send = Callable[[Message], Awaitable[None]] + +ASGIApp = Callable[[Scope, Receive, Send], Awaitable[None]] + +StatelessLifespan = Callable[[AppType], AbstractAsyncContextManager[None]] +StatefulLifespan = Callable[[AppType], AbstractAsyncContextManager[Mapping[str, Any]]] +Lifespan = StatelessLifespan[AppType] | StatefulLifespan[AppType] + +HTTPExceptionHandler = Callable[["Request", Exception], "Response | Awaitable[Response]"] +WebSocketExceptionHandler = Callable[["WebSocket", Exception], Awaitable[None]] +ExceptionHandler = HTTPExceptionHandler | WebSocketExceptionHandler diff --git a/source/target-vendors/cp312/starlette/websockets.py b/source/target-vendors/cp312/starlette/websockets.py new file mode 100644 index 0000000..cf48e87 --- /dev/null +++ b/source/target-vendors/cp312/starlette/websockets.py @@ -0,0 +1,202 @@ +from __future__ import annotations + +import enum +import json +from collections.abc import AsyncIterator, Iterable +from typing import Any, cast + +from starlette.requests import HTTPConnection, StateT +from starlette.responses import Response +from starlette.types import Message, Receive, Scope, Send + + +class WebSocketState(enum.Enum): + CONNECTING = 0 + CONNECTED = 1 + DISCONNECTED = 2 + RESPONSE = 3 + + +class WebSocketDisconnect(Exception): + def __init__(self, code: int = 1000, reason: str | None = None) -> None: + self.code = code + self.reason = reason or "" + + +class WebSocketDisconnected(RuntimeError): + """ + Raised when attempting to use a disconnected WebSocket. + """ + + +class WebSocket(HTTPConnection[StateT]): + def __init__(self, scope: Scope, receive: Receive, send: Send) -> None: + super().__init__(scope) + assert scope["type"] == "websocket" + self._receive = receive + self._send = send + self.client_state = WebSocketState.CONNECTING + self.application_state = WebSocketState.CONNECTING + + async def receive(self) -> Message: + """ + Receive ASGI websocket messages, ensuring valid state transitions. + """ + if self.client_state == WebSocketState.CONNECTING: + message = await self._receive() + message_type = message["type"] + if message_type != "websocket.connect": + raise RuntimeError(f'Expected ASGI message "websocket.connect", but got {message_type!r}') + self.client_state = WebSocketState.CONNECTED + return message + elif self.client_state == WebSocketState.CONNECTED: + message = await self._receive() + message_type = message["type"] + if message_type not in {"websocket.receive", "websocket.disconnect"}: + raise RuntimeError( + f'Expected ASGI message "websocket.receive" or "websocket.disconnect", but got {message_type!r}' + ) + if message_type == "websocket.disconnect": + self.client_state = WebSocketState.DISCONNECTED + return message + else: + raise WebSocketDisconnected('Cannot call "receive" once a disconnect message has been received.') + + async def send(self, message: Message) -> None: + """ + Send ASGI websocket messages, ensuring valid state transitions. + """ + if self.application_state == WebSocketState.CONNECTING: + message_type = message["type"] + if message_type not in {"websocket.accept", "websocket.close", "websocket.http.response.start"}: + raise RuntimeError( + 'Expected ASGI message "websocket.accept", "websocket.close" or "websocket.http.response.start", ' + f"but got {message_type!r}" + ) + if message_type == "websocket.close": + self.application_state = WebSocketState.DISCONNECTED + elif message_type == "websocket.http.response.start": + self.application_state = WebSocketState.RESPONSE + else: + self.application_state = WebSocketState.CONNECTED + await self._send(message) + elif self.application_state == WebSocketState.CONNECTED: + message_type = message["type"] + if message_type not in {"websocket.send", "websocket.close"}: + raise RuntimeError( + f'Expected ASGI message "websocket.send" or "websocket.close", but got {message_type!r}' + ) + if message_type == "websocket.close": + self.application_state = WebSocketState.DISCONNECTED + try: + await self._send(message) + except OSError: + self.application_state = WebSocketState.DISCONNECTED + raise WebSocketDisconnect(code=1006) + elif self.application_state == WebSocketState.RESPONSE: + message_type = message["type"] + if message_type != "websocket.http.response.body": + raise RuntimeError(f'Expected ASGI message "websocket.http.response.body", but got {message_type!r}') + if not message.get("more_body", False): + self.application_state = WebSocketState.DISCONNECTED + await self._send(message) + else: + raise WebSocketDisconnected('Cannot call "send" once a close message has been sent.') + + async def accept( + self, + subprotocol: str | None = None, + headers: Iterable[tuple[bytes, bytes]] | None = None, + ) -> None: + headers = headers or [] + + if self.client_state == WebSocketState.CONNECTING: # pragma: no branch + # If we haven't yet seen the 'connect' message, then wait for it first. + await self.receive() + await self.send({"type": "websocket.accept", "subprotocol": subprotocol, "headers": headers}) + + def _raise_on_disconnect(self, message: Message) -> None: + if message["type"] == "websocket.disconnect": + raise WebSocketDisconnect(message["code"], message.get("reason")) + + async def receive_text(self) -> str: + if self.application_state != WebSocketState.CONNECTED: + raise WebSocketDisconnected('WebSocket is not connected. Need to call "accept" first.') + message = await self.receive() + self._raise_on_disconnect(message) + return cast(str, message["text"]) + + async def receive_bytes(self) -> bytes: + if self.application_state != WebSocketState.CONNECTED: + raise WebSocketDisconnected('WebSocket is not connected. Need to call "accept" first.') + message = await self.receive() + self._raise_on_disconnect(message) + return cast(bytes, message["bytes"]) + + async def receive_json(self, mode: str = "text") -> Any: + if mode not in {"text", "binary"}: + raise RuntimeError('The "mode" argument should be "text" or "binary".') + if self.application_state != WebSocketState.CONNECTED: + raise WebSocketDisconnected('WebSocket is not connected. Need to call "accept" first.') + message = await self.receive() + self._raise_on_disconnect(message) + + if mode == "text": + text = message["text"] + else: + text = message["bytes"].decode("utf-8") + return json.loads(text) + + async def iter_text(self) -> AsyncIterator[str]: + try: + while True: + yield await self.receive_text() + except WebSocketDisconnect: + pass + + async def iter_bytes(self) -> AsyncIterator[bytes]: + try: + while True: + yield await self.receive_bytes() + except WebSocketDisconnect: + pass + + async def iter_json(self) -> AsyncIterator[Any]: + try: + while True: + yield await self.receive_json() + except WebSocketDisconnect: + pass + + async def send_text(self, data: str) -> None: + await self.send({"type": "websocket.send", "text": data}) + + async def send_bytes(self, data: bytes) -> None: + await self.send({"type": "websocket.send", "bytes": data}) + + async def send_json(self, data: Any, mode: str = "text") -> None: + if mode not in {"text", "binary"}: + raise RuntimeError('The "mode" argument should be "text" or "binary".') + text = json.dumps(data, separators=(",", ":"), ensure_ascii=False) + if mode == "text": + await self.send({"type": "websocket.send", "text": text}) + else: + await self.send({"type": "websocket.send", "bytes": text.encode("utf-8")}) + + async def close(self, code: int = 1000, reason: str | None = None) -> None: + await self.send({"type": "websocket.close", "code": code, "reason": reason or ""}) + + async def send_denial_response(self, response: Response) -> None: + if "websocket.http.response" in self.scope.get("extensions", {}): + await response(self.scope, self.receive, self.send) + else: + raise RuntimeError("The server doesn't support the Websocket Denial Response extension.") + + +class WebSocketClose: + def __init__(self, code: int = 1000, reason: str | None = None) -> None: + self.code = code + self.reason = reason or "" + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + await send({"type": "websocket.close", "code": self.code, "reason": self.reason}) diff --git a/source/target-vendors/cp312/typing_extensions-4.16.0.dist-info/METADATA b/source/target-vendors/cp312/typing_extensions-4.16.0.dist-info/METADATA new file mode 100644 index 0000000..97fd818 --- /dev/null +++ b/source/target-vendors/cp312/typing_extensions-4.16.0.dist-info/METADATA @@ -0,0 +1,73 @@ +Metadata-Version: 2.4 +Name: typing_extensions +Version: 4.16.0 +Summary: Backported and Experimental Type Hints for Python 3.9+ +Keywords: annotations,backport,checker,checking,function,hinting,hints,type,typechecking,typehinting,typehints,typing +Author-email: "Guido van Rossum, Jukka Lehtosalo, Łukasz Langa, Michael Lee" +Requires-Python: >=3.9 +Description-Content-Type: text/markdown +License-Expression: PSF-2.0 +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: 3.15 +Classifier: Topic :: Software Development +License-File: LICENSE +Project-URL: Bug Tracker, https://github.com/python/typing_extensions/issues +Project-URL: Changes, https://github.com/python/typing_extensions/blob/main/CHANGELOG.md +Project-URL: Documentation, https://typing-extensions.readthedocs.io/ +Project-URL: Home, https://github.com/python/typing_extensions +Project-URL: Q & A, https://github.com/python/typing/discussions +Project-URL: Repository, https://github.com/python/typing_extensions + +# Typing Extensions + +[![Chat at https://gitter.im/python/typing](https://badges.gitter.im/python/typing.svg)](https://gitter.im/python/typing) + +[Documentation](https://typing-extensions.readthedocs.io/en/latest/#) – +[PyPI](https://pypi.org/project/typing-extensions/) + +## Overview + +The `typing_extensions` module serves two related purposes: + +- Enable use of new type system features on older Python versions. For example, + `typing.TypeGuard` is new in Python 3.10, but `typing_extensions` allows + users on previous Python versions to use it too. +- Enable experimentation with new type system PEPs before they are accepted and + added to the `typing` module. + +`typing_extensions` is treated specially by static type checkers such as +mypy and pyright. Objects defined in `typing_extensions` are treated the same +way as equivalent forms in `typing`. + +`typing_extensions` uses +[Semantic Versioning](https://semver.org/). The +major version will be incremented only for backwards-incompatible changes. +Therefore, it's safe to depend +on `typing_extensions` like this: `typing_extensions ~=x.y`, +where `x.y` is the first version that includes all features you need. +[This](https://packaging.python.org/en/latest/specifications/version-specifiers/#compatible-release) +is equivalent to `typing_extensions >=x.y, <(x+1)`. Do not depend on `~= x.y.z` +unless you really know what you're doing; that defeats the purpose of +semantic versioning. + +## Included items + +See [the documentation](https://typing-extensions.readthedocs.io/en/latest/#) for a +complete listing of module contents. + +## Contributing + +See [CONTRIBUTING.md](https://github.com/python/typing_extensions/blob/main/CONTRIBUTING.md) +for how to contribute to `typing_extensions`. + diff --git a/source/target-vendors/cp312/typing_extensions-4.16.0.dist-info/RECORD b/source/target-vendors/cp312/typing_extensions-4.16.0.dist-info/RECORD new file mode 100644 index 0000000..b6d2d5e --- /dev/null +++ b/source/target-vendors/cp312/typing_extensions-4.16.0.dist-info/RECORD @@ -0,0 +1,5 @@ +typing_extensions.py,sha256=QEDKGh7L7gDROFwSqTCE0cW9RvC3dPB-WufpHE9V5pY,165012 +typing_extensions-4.16.0.dist-info/licenses/LICENSE,sha256=Oy-B_iHRgcSZxZolbI4ZaEVdZonSaaqFNzv7avQdo78,13936 +typing_extensions-4.16.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 +typing_extensions-4.16.0.dist-info/METADATA,sha256=sFCEyh1Qh5hlF42f_5-r6rYb37HzYb-96VQh_8j5vkY,3310 +typing_extensions-4.16.0.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/typing_extensions-4.16.0.dist-info/WHEEL b/source/target-vendors/cp312/typing_extensions-4.16.0.dist-info/WHEEL new file mode 100644 index 0000000..d8b9936 --- /dev/null +++ b/source/target-vendors/cp312/typing_extensions-4.16.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.12.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/source/target-vendors/cp312/typing_extensions-4.16.0.dist-info/licenses/LICENSE b/source/target-vendors/cp312/typing_extensions-4.16.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000..f26bcf4 --- /dev/null +++ b/source/target-vendors/cp312/typing_extensions-4.16.0.dist-info/licenses/LICENSE @@ -0,0 +1,279 @@ +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see https://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations, which became +Zope Corporation. In 2001, the Python Software Foundation (PSF, see +https://www.python.org/psf/) was formed, a non-profit organization +created specifically to own Python-related Intellectual Property. +Zope Corporation was a sponsoring member of the PSF. + +All Python releases are Open Source (see https://opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +Python software and documentation are licensed under the +Python Software Foundation License Version 2. + +Starting with Python 3.8.6, examples, recipes, and other code in +the documentation are dual licensed under the PSF License Version 2 +and the Zero-Clause BSD license. + +Some software incorporated into Python is under different licenses. +The licenses are listed with code falling under that license. + + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION +---------------------------------------------------------------------- + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/source/target-vendors/cp312/typing_extensions.py b/source/target-vendors/cp312/typing_extensions.py new file mode 100644 index 0000000..ced7837 --- /dev/null +++ b/source/target-vendors/cp312/typing_extensions.py @@ -0,0 +1,4422 @@ +import abc +import builtins +import collections +import collections.abc +import contextlib +import enum +import functools +import inspect +import io +import keyword +import operator +import sys +import types as _types +import typing +import warnings + +# Breakpoint: https://github.com/python/cpython/pull/119891 +if sys.version_info >= (3, 14): + import annotationlib + +__all__ = [ + # Super-special typing primitives. + 'Any', + 'ClassVar', + 'Concatenate', + 'Final', + 'LiteralString', + 'ParamSpec', + 'ParamSpecArgs', + 'ParamSpecKwargs', + 'Self', + 'Type', + 'TypeVar', + 'TypeVarTuple', + 'Unpack', + + # ABCs (from collections.abc). + 'Awaitable', + 'AsyncIterator', + 'AsyncIterable', + 'Coroutine', + 'AsyncGenerator', + 'AsyncContextManager', + 'Buffer', + 'ChainMap', + + # Concrete collection types. + 'ContextManager', + 'Counter', + 'Deque', + 'DefaultDict', + 'NamedTuple', + 'OrderedDict', + 'TypedDict', + + # Structural checks, a.k.a. protocols. + 'SupportsAbs', + 'SupportsBytes', + 'SupportsComplex', + 'SupportsFloat', + 'SupportsIndex', + 'SupportsInt', + 'SupportsRound', + 'Reader', + 'Writer', + + # One-off things. + 'Annotated', + 'assert_never', + 'assert_type', + 'clear_overloads', + 'dataclass_transform', + 'deprecated', + 'disjoint_base', + 'Doc', + 'evaluate_forward_ref', + 'get_overloads', + 'final', + 'Format', + 'get_annotations', + 'get_args', + 'get_origin', + 'get_original_bases', + 'get_protocol_members', + 'get_type_hints', + 'IntVar', + 'is_protocol', + 'is_typeddict', + 'Literal', + 'NewType', + 'overload', + 'override', + 'Protocol', + 'sentinel', + 'Sentinel', + 'reveal_type', + 'runtime', + 'runtime_checkable', + 'Text', + 'TypeAlias', + 'TypeAliasType', + 'TypeForm', + 'TypeGuard', + 'TypeIs', + 'TYPE_CHECKING', + 'type_repr', + 'Never', + 'NoReturn', + 'ReadOnly', + 'Required', + 'NotRequired', + 'NoDefault', + 'NoExtraItems', + + # Pure aliases, have always been in typing + 'AbstractSet', + 'AnyStr', + 'BinaryIO', + 'Callable', + 'Collection', + 'Container', + 'Dict', + 'ForwardRef', + 'FrozenSet', + 'Generator', + 'Generic', + 'Hashable', + 'IO', + 'ItemsView', + 'Iterable', + 'Iterator', + 'KeysView', + 'List', + 'Mapping', + 'MappingView', + 'Match', + 'MutableMapping', + 'MutableSequence', + 'MutableSet', + 'Optional', + 'Pattern', + 'Reversible', + 'Sequence', + 'Set', + 'Sized', + 'TextIO', + 'Tuple', + 'Union', + 'ValuesView', + 'cast', + 'no_type_check', +] + +# for backward compatibility +PEP_560 = True +GenericMeta = type +# Breakpoint: https://github.com/python/cpython/pull/116129 +_PEP_696_IMPLEMENTED = sys.version_info >= (3, 13, 0, "beta") + +# Added with bpo-45166 to 3.10.1+ and some 3.9 versions +_FORWARD_REF_HAS_CLASS = "__forward_is_class__" in typing.ForwardRef.__slots__ + + +def _caller(depth=1, default='__main__'): + try: + return sys._getframemodulename(depth + 1) or default + except AttributeError: # For platforms without _getframemodulename() + pass + try: + return sys._getframe(depth + 1).f_globals.get('__name__', default) + except (AttributeError, ValueError): # For platforms without _getframe() + pass + return None + + +# Placeholder for sentinel methods, because sentinels can not have their own sentinels +_sentinel_placeholder = object() + +if hasattr(builtins, "sentinel"): # 3.15+ + sentinel = builtins.sentinel +else: + class sentinel: + """Create a unique sentinel object. + + *name* should be the name of the variable to which the return value + shall be assigned. + """ + + def __init__( + self, + __name: str = _sentinel_placeholder, + __repr: typing.Optional[str] = _sentinel_placeholder, + /, + *, + repr: typing.Optional[str] = None, + name: str = _sentinel_placeholder, + ) -> None: + if name is not _sentinel_placeholder: + warnings.warn( + "Passing 'name' as a keyword argument is deprecated; " + "pass it positionally instead.", + DeprecationWarning, + stacklevel=2, + ) + __name = name + if __name is _sentinel_placeholder: + raise TypeError("First parameter 'name' is required") + if __repr is not _sentinel_placeholder: + warnings.warn( + "Passing 'repr' as a positional argument is deprecated; " + "pass it by keyword instead.", + DeprecationWarning, + stacklevel=2, + ) + repr = __repr + + self._name = __name + self._repr = repr if repr is not None else __name + + # For pickling as a singleton: + self.__module__ = _caller() + + def __init_subclass__(cls): + warnings.warn( + "Subclassing sentinel is deprecated " + "and will be disallowed in Python 3.15", + DeprecationWarning, + stacklevel=2, + ) + super().__init_subclass__() + + def __setattr__(self, attr: str, value: object) -> None: + if attr not in {"_name", "_repr", "__module__"}: + warnings.warn( + f"Setting attribute {attr!r} on sentinel objects is deprecated " + "and will be disallowed in Python 3.15.", + DeprecationWarning, + stacklevel=2, + ) + super().__setattr__(attr, value) + + @property + def __name__(self) -> str: + return self._name + + @__name__.setter + def __name__(self, value: str) -> None: + self._name = value + + def __repr__(self) -> str: + return self._repr + + if sys.version_info < (3, 11): + # The presence of this method convinces typing._type_check + # that Sentinels are types. + def __call__(self, *args, **kwargs): + raise TypeError(f"{type(self).__name__!r} object is not callable") + + # Breakpoint: https://github.com/python/cpython/pull/21515 + if sys.version_info >= (3, 10): + def __or__(self, other): + return typing.Union[self, other] + + def __ror__(self, other): + return typing.Union[other, self] + + def __reduce__(self) -> str: + """Reduce this sentinel to a singleton.""" + return self.__name__ # Module is taken from the __module__ attribute + +Sentinel = sentinel + +_marker = sentinel("sentinel") + + +# The functions below are modified copies of typing internal helpers. +# They are needed by _ProtocolMeta and they provide support for PEP 646. + +# Breakpoint: https://github.com/python/cpython/pull/27342 +if sys.version_info >= (3, 10): + def _should_collect_from_parameters(t): + return isinstance( + t, (typing._GenericAlias, _types.GenericAlias, _types.UnionType) + ) +else: + def _should_collect_from_parameters(t): + return isinstance(t, (typing._GenericAlias, _types.GenericAlias)) + + +NoReturn = typing.NoReturn + +# Some unconstrained type variables. These are used by the container types. +# (These are not for export.) +T = typing.TypeVar('T') # Any type. +KT = typing.TypeVar('KT') # Key type. +VT = typing.TypeVar('VT') # Value type. +T_co = typing.TypeVar('T_co', covariant=True) # Any type covariant containers. +T_contra = typing.TypeVar('T_contra', contravariant=True) # Ditto contravariant. + + +# Breakpoint: https://github.com/python/cpython/pull/31841 +if sys.version_info >= (3, 11): + from typing import Any +else: + + class _AnyMeta(type): + def __instancecheck__(self, obj): + if self is Any: + raise TypeError("typing_extensions.Any cannot be used with isinstance()") + return super().__instancecheck__(obj) + + def __repr__(self): + if self is Any: + return "typing_extensions.Any" + return super().__repr__() + + class Any(metaclass=_AnyMeta): + """Special type indicating an unconstrained type. + - Any is compatible with every type. + - Any assumed to have all methods. + - All values assumed to be instances of Any. + Note that all the above statements are true from the point of view of + static type checkers. At runtime, Any should not be used with instance + checks. + """ + def __new__(cls, *args, **kwargs): + if cls is Any: + raise TypeError("Any cannot be instantiated") + return super().__new__(cls, *args, **kwargs) + + +ClassVar = typing.ClassVar + +# Vendored from cpython typing._SpecialFrom +# Having a separate class means that instances will not be rejected by +# typing._type_check. +class _SpecialForm(typing._Final, _root=True): + __slots__ = ('_name', '__doc__', '_getitem') + + def __init__(self, getitem): + self._getitem = getitem + self._name = getitem.__name__ + self.__doc__ = getitem.__doc__ + + def __getattr__(self, item): + if item in {'__name__', '__qualname__'}: + return self._name + + raise AttributeError(item) + + def __mro_entries__(self, bases): + raise TypeError(f"Cannot subclass {self!r}") + + def __repr__(self): + return f'typing_extensions.{self._name}' + + def __reduce__(self): + return self._name + + def __call__(self, *args, **kwds): + raise TypeError(f"Cannot instantiate {self!r}") + + def __or__(self, other): + return typing.Union[self, other] + + def __ror__(self, other): + return typing.Union[other, self] + + def __instancecheck__(self, obj): + raise TypeError(f"{self} cannot be used with isinstance()") + + def __subclasscheck__(self, cls): + raise TypeError(f"{self} cannot be used with issubclass()") + + @typing._tp_cache + def __getitem__(self, parameters): + return self._getitem(self, parameters) + + +# Note that inheriting from this class means that the object will be +# rejected by typing._type_check, so do not use it if the special form +# is arguably valid as a type by itself. +class _ExtensionsSpecialForm(typing._SpecialForm, _root=True): + def __repr__(self): + return 'typing_extensions.' + self._name + + +Final = typing.Final + +# Breakpoint: https://github.com/python/cpython/pull/30530 +if sys.version_info >= (3, 11): + final = typing.final +else: + # @final exists in 3.8+, but we backport it for all versions + # before 3.11 to keep support for the __final__ attribute. + # See https://bugs.python.org/issue46342 + def final(f): + """This decorator can be used to indicate to type checkers that + the decorated method cannot be overridden, and decorated class + cannot be subclassed. For example: + + class Base: + @final + def done(self) -> None: + ... + class Sub(Base): + def done(self) -> None: # Error reported by type checker + ... + @final + class Leaf: + ... + class Other(Leaf): # Error reported by type checker + ... + + There is no runtime checking of these properties. The decorator + sets the ``__final__`` attribute to ``True`` on the decorated object + to allow runtime introspection. + """ + try: + f.__final__ = True + except (AttributeError, TypeError): + # Skip the attribute silently if it is not writable. + # AttributeError happens if the object has __slots__ or a + # read-only property, TypeError if it's a builtin class. + pass + return f + + +if hasattr(typing, "disjoint_base"): # 3.15 + disjoint_base = typing.disjoint_base +else: + def disjoint_base(cls): + """This decorator marks a class as a disjoint base. + + Child classes of a disjoint base cannot inherit from other disjoint bases that are + not parent classes of the disjoint base. + + For example: + + @disjoint_base + class Disjoint1: pass + + @disjoint_base + class Disjoint2: pass + + class Disjoint3(Disjoint1, Disjoint2): pass # Type checker error + + Type checkers can use knowledge of disjoint bases to detect unreachable code + and determine when two types can overlap. + + See PEP 800.""" + cls.__disjoint_base__ = True + return cls + + +def IntVar(name): + return typing.TypeVar(name) + + +# A Literal bug was fixed in 3.11.0, 3.10.1 and 3.9.8 +# Breakpoint: https://github.com/python/cpython/pull/29334 +if sys.version_info >= (3, 10, 1): + Literal = typing.Literal +else: + def _flatten_literal_params(parameters): + """An internal helper for Literal creation: flatten Literals among parameters""" + params = [] + for p in parameters: + if isinstance(p, _LiteralGenericAlias): + params.extend(p.__args__) + else: + params.append(p) + return tuple(params) + + def _value_and_type_iter(params): + for p in params: + yield p, type(p) + + class _LiteralGenericAlias(typing._GenericAlias, _root=True): + def __eq__(self, other): + if not isinstance(other, _LiteralGenericAlias): + return NotImplemented + these_args_deduped = set(_value_and_type_iter(self.__args__)) + other_args_deduped = set(_value_and_type_iter(other.__args__)) + return these_args_deduped == other_args_deduped + + def __hash__(self): + return hash(frozenset(_value_and_type_iter(self.__args__))) + + class _LiteralForm(_ExtensionsSpecialForm, _root=True): + def __init__(self, doc: str): + self._name = 'Literal' + self._doc = self.__doc__ = doc + + def __getitem__(self, parameters): + if not isinstance(parameters, tuple): + parameters = (parameters,) + + parameters = _flatten_literal_params(parameters) + + val_type_pairs = list(_value_and_type_iter(parameters)) + try: + deduped_pairs = set(val_type_pairs) + except TypeError: + # unhashable parameters + pass + else: + # similar logic to typing._deduplicate on Python 3.9+ + if len(deduped_pairs) < len(val_type_pairs): + new_parameters = [] + for pair in val_type_pairs: + if pair in deduped_pairs: + new_parameters.append(pair[0]) + deduped_pairs.remove(pair) + assert not deduped_pairs, deduped_pairs + parameters = tuple(new_parameters) + + return _LiteralGenericAlias(self, parameters) + + Literal = _LiteralForm(doc="""\ + A type that can be used to indicate to type checkers + that the corresponding value has a value literally equivalent + to the provided parameter. For example: + + var: Literal[4] = 4 + + The type checker understands that 'var' is literally equal to + the value 4 and no other value. + + Literal[...] cannot be subclassed. There is no runtime + checking verifying that the parameter is actually a value + instead of a type.""") + + +_overload_dummy = typing._overload_dummy + + +if hasattr(typing, "get_overloads"): # 3.11+ + overload = typing.overload + get_overloads = typing.get_overloads + clear_overloads = typing.clear_overloads +else: + # {module: {qualname: {firstlineno: func}}} + _overload_registry = collections.defaultdict( + functools.partial(collections.defaultdict, dict) + ) + + def overload(func): + """Decorator for overloaded functions/methods. + + In a stub file, place two or more stub definitions for the same + function in a row, each decorated with @overload. For example: + + @overload + def utf8(value: None) -> None: ... + @overload + def utf8(value: bytes) -> bytes: ... + @overload + def utf8(value: str) -> bytes: ... + + In a non-stub file (i.e. a regular .py file), do the same but + follow it with an implementation. The implementation should *not* + be decorated with @overload. For example: + + @overload + def utf8(value: None) -> None: ... + @overload + def utf8(value: bytes) -> bytes: ... + @overload + def utf8(value: str) -> bytes: ... + def utf8(value): + # implementation goes here + + The overloads for a function can be retrieved at runtime using the + get_overloads() function. + """ + # classmethod and staticmethod + f = getattr(func, "__func__", func) + try: + _overload_registry[f.__module__][f.__qualname__][ + f.__code__.co_firstlineno + ] = func + except AttributeError: + # Not a normal function; ignore. + pass + return _overload_dummy + + def get_overloads(func): + """Return all defined overloads for *func* as a sequence.""" + # classmethod and staticmethod + f = getattr(func, "__func__", func) + if f.__module__ not in _overload_registry: + return [] + mod_dict = _overload_registry[f.__module__] + if f.__qualname__ not in mod_dict: + return [] + return list(mod_dict[f.__qualname__].values()) + + def clear_overloads(): + """Clear all overloads in the registry.""" + _overload_registry.clear() + + +# This is not a real generic class. Don't use outside annotations. +Type = typing.Type + +# Various ABCs mimicking those in collections.abc. +# A few are simply re-exported for completeness. +Awaitable = typing.Awaitable +Coroutine = typing.Coroutine +AsyncIterable = typing.AsyncIterable +AsyncIterator = typing.AsyncIterator +Deque = typing.Deque +DefaultDict = typing.DefaultDict +OrderedDict = typing.OrderedDict +Counter = typing.Counter +ChainMap = typing.ChainMap +Text = typing.Text +TYPE_CHECKING = typing.TYPE_CHECKING + + +# Breakpoint: https://github.com/python/cpython/pull/118681 +if sys.version_info >= (3, 13, 0, "beta"): + from typing import AsyncContextManager, AsyncGenerator, ContextManager, Generator +else: + def _is_dunder(attr): + return attr.startswith('__') and attr.endswith('__') + + + class _SpecialGenericAlias(typing._SpecialGenericAlias, _root=True): + def __init__(self, origin, nparams, *, defaults, inst=True, name=None): + assert nparams > 0, "`nparams` must be a positive integer" + assert defaults, "Must always specify a non-empty sequence for `defaults`" + super().__init__(origin, nparams, inst=inst, name=name) + self._defaults = defaults + + def __setattr__(self, attr, val): + allowed_attrs = {'_name', '_inst', '_nparams', '_defaults'} + if _is_dunder(attr) or attr in allowed_attrs: + object.__setattr__(self, attr, val) + else: + setattr(self.__origin__, attr, val) + + @typing._tp_cache + def __getitem__(self, params): + if not isinstance(params, tuple): + params = (params,) + msg = "Parameters to generic types must be types." + params = tuple(typing._type_check(p, msg) for p in params) + if ( + len(params) < self._nparams + and len(params) + len(self._defaults) >= self._nparams + ): + params = (*params, *self._defaults[len(params) - self._nparams:]) + actual_len = len(params) + + if actual_len != self._nparams: + expected = f"at least {self._nparams - len(self._defaults)}" + raise TypeError( + f"Too {'many' if actual_len > self._nparams else 'few'}" + f" arguments for {self};" + f" actual {actual_len}, expected {expected}" + ) + return self.copy_with(params) + + _NoneType = type(None) + Generator = _SpecialGenericAlias( + collections.abc.Generator, 3, defaults=(_NoneType, _NoneType) + ) + AsyncGenerator = _SpecialGenericAlias( + collections.abc.AsyncGenerator, 2, defaults=(_NoneType,) + ) + ContextManager = _SpecialGenericAlias( + contextlib.AbstractContextManager, + 2, + name="ContextManager", + defaults=(typing.Optional[bool],) + ) + AsyncContextManager = _SpecialGenericAlias( + contextlib.AbstractAsyncContextManager, + 2, + name="AsyncContextManager", + defaults=(typing.Optional[bool],) + ) + + +_PROTO_ALLOWLIST = { + 'collections.abc': [ + 'Callable', 'Awaitable', 'Iterable', 'Iterator', 'AsyncIterable', + 'AsyncIterator', 'Hashable', 'Sized', 'Container', 'Collection', + 'Reversible', 'Buffer', + ], + 'contextlib': ['AbstractContextManager', 'AbstractAsyncContextManager'], + 'io': ['Reader', 'Writer'], + 'typing_extensions': ['Buffer'], + 'os': ['PathLike'], +} + + +_EXCLUDED_ATTRS = frozenset(typing.EXCLUDED_ATTRIBUTES) | { + "__match_args__", "__protocol_attrs__", "__non_callable_proto_members__", + "__final__", +} + + +def _get_protocol_attrs(cls): + attrs = set() + for base in cls.__mro__[:-1]: # without object + if base.__name__ in {'Protocol', 'Generic'}: + continue + annotations = getattr(base, '__annotations__', {}) + for attr in (*base.__dict__, *annotations): + if (not attr.startswith('_abc_') and attr not in _EXCLUDED_ATTRS): + attrs.add(attr) + return attrs + + +# `__match_args__` attribute was removed from protocol members in 3.13, +# we want to backport this change to older Python versions. +# 3.14 additionally added `io.Reader`, `io.Writer` and `os.PathLike` to +# the list of allowed protocol allowlist. +# https://github.com/python/cpython/issues/127647 +if sys.version_info >= (3, 14): + Protocol = typing.Protocol +else: + def _allow_reckless_class_checks(depth=2): + """Allow instance and class checks for special stdlib modules. + The abc and functools modules indiscriminately call isinstance() and + issubclass() on the whole MRO of a user class, which may contain protocols. + """ + return _caller(depth) in {'abc', 'functools', None} + + def _no_init(self, *args, **kwargs): + if type(self)._is_protocol: + raise TypeError('Protocols cannot be instantiated') + + def _type_check_issubclass_arg_1(arg): + """Raise TypeError if `arg` is not an instance of `type` + in `issubclass(arg, )`. + + In most cases, this is verified by type.__subclasscheck__. + Checking it again unnecessarily would slow down issubclass() checks, + so, we don't perform this check unless we absolutely have to. + + For various error paths, however, + we want to ensure that *this* error message is shown to the user + where relevant, rather than a typing.py-specific error message. + """ + if not isinstance(arg, type): + # Same error message as for issubclass(1, int). + raise TypeError('issubclass() arg 1 must be a class') + + # Inheriting from typing._ProtocolMeta isn't actually desirable, + # but is necessary to allow typing.Protocol and typing_extensions.Protocol + # to mix without getting TypeErrors about "metaclass conflict" + class _ProtocolMeta(type(typing.Protocol)): + # This metaclass is somewhat unfortunate, + # but is necessary for several reasons... + # + # NOTE: DO NOT call super() in any methods in this class + # That would call the methods on typing._ProtocolMeta on Python <=3.11 + # and those are slow + def __new__(mcls, name, bases, namespace, **kwargs): + if name == "Protocol" and len(bases) < 2: + pass + elif {Protocol, typing.Protocol} & set(bases): + for base in bases: + if not ( + base in {object, typing.Generic, Protocol, typing.Protocol} + or base.__name__ in _PROTO_ALLOWLIST.get(base.__module__, []) + or is_protocol(base) + ): + raise TypeError( + f"Protocols can only inherit from other protocols, " + f"got {base!r}" + ) + return abc.ABCMeta.__new__(mcls, name, bases, namespace, **kwargs) + + def __init__(cls, *args, **kwargs): + abc.ABCMeta.__init__(cls, *args, **kwargs) + if getattr(cls, "_is_protocol", False): + cls.__protocol_attrs__ = _get_protocol_attrs(cls) + + def __subclasscheck__(cls, other): + if cls is Protocol: + return type.__subclasscheck__(cls, other) + if ( + getattr(cls, '_is_protocol', False) + and not _allow_reckless_class_checks() + ): + if not getattr(cls, '_is_runtime_protocol', False): + _type_check_issubclass_arg_1(other) + raise TypeError( + "Instance and class checks can only be used with " + "@runtime_checkable protocols" + ) + if ( + # this attribute is set by @runtime_checkable: + cls.__non_callable_proto_members__ + and cls.__dict__.get("__subclasshook__") is _proto_hook + ): + _type_check_issubclass_arg_1(other) + non_method_attrs = sorted(cls.__non_callable_proto_members__) + raise TypeError( + "Protocols with non-method members don't support issubclass()." + f" Non-method members: {str(non_method_attrs)[1:-1]}." + ) + return abc.ABCMeta.__subclasscheck__(cls, other) + + def __instancecheck__(cls, instance): + # We need this method for situations where attributes are + # assigned in __init__. + if cls is Protocol: + return type.__instancecheck__(cls, instance) + if not getattr(cls, "_is_protocol", False): + # i.e., it's a concrete subclass of a protocol + return abc.ABCMeta.__instancecheck__(cls, instance) + + if ( + not getattr(cls, '_is_runtime_protocol', False) and + not _allow_reckless_class_checks() + ): + raise TypeError("Instance and class checks can only be used with" + " @runtime_checkable protocols") + + if abc.ABCMeta.__instancecheck__(cls, instance): + return True + + for attr in cls.__protocol_attrs__: + try: + val = inspect.getattr_static(instance, attr) + except AttributeError: + break + # this attribute is set by @runtime_checkable: + if val is None and attr not in cls.__non_callable_proto_members__: + break + else: + return True + + return False + + def __eq__(cls, other): + # Hack so that typing.Generic.__class_getitem__ + # treats typing_extensions.Protocol + # as equivalent to typing.Protocol + if abc.ABCMeta.__eq__(cls, other) is True: + return True + return cls is Protocol and other is typing.Protocol + + # This has to be defined, or the abc-module cache + # complains about classes with this metaclass being unhashable, + # if we define only __eq__! + def __hash__(cls) -> int: + return type.__hash__(cls) + + @classmethod + def _proto_hook(cls, other): + if not cls.__dict__.get('_is_protocol', False): + return NotImplemented + + for attr in cls.__protocol_attrs__: + for base in other.__mro__: + # Check if the members appears in the class dictionary... + if attr in base.__dict__: + if base.__dict__[attr] is None: + return NotImplemented + break + + # ...or in annotations, if it is a sub-protocol. + annotations = getattr(base, '__annotations__', {}) + if ( + isinstance(annotations, collections.abc.Mapping) + and attr in annotations + and is_protocol(other) + ): + break + else: + return NotImplemented + return True + + class Protocol(typing.Generic, metaclass=_ProtocolMeta): + __doc__ = typing.Protocol.__doc__ + __slots__ = () + _is_protocol = True + _is_runtime_protocol = False + + def __init_subclass__(cls, *args, **kwargs): + super().__init_subclass__(*args, **kwargs) + + # Determine if this is a protocol or a concrete subclass. + if not cls.__dict__.get('_is_protocol', False): + cls._is_protocol = any(b is Protocol for b in cls.__bases__) + + # Set (or override) the protocol subclass hook. + if '__subclasshook__' not in cls.__dict__: + cls.__subclasshook__ = _proto_hook + + # Prohibit instantiation for protocol classes + if cls._is_protocol and cls.__init__ is Protocol.__init__: + cls.__init__ = _no_init + + +# Breakpoint: https://github.com/python/cpython/pull/113401 +if sys.version_info >= (3, 13): + runtime_checkable = typing.runtime_checkable +else: + def runtime_checkable(cls): + """Mark a protocol class as a runtime protocol. + + Such protocol can be used with isinstance() and issubclass(). + Raise TypeError if applied to a non-protocol class. + This allows a simple-minded structural check very similar to + one trick ponies in collections.abc such as Iterable. + + For example:: + + @runtime_checkable + class Closable(Protocol): + def close(self): ... + + assert isinstance(open('/some/file'), Closable) + + Warning: this will check only the presence of the required methods, + not their type signatures! + """ + if not issubclass(cls, typing.Generic) or not getattr(cls, '_is_protocol', False): + raise TypeError(f'@runtime_checkable can be only applied to protocol classes,' + f' got {cls!r}') + cls._is_runtime_protocol = True + + # typing.Protocol classes on <=3.11 break if we execute this block, + # because typing.Protocol classes on <=3.11 don't have a + # `__protocol_attrs__` attribute, and this block relies on the + # `__protocol_attrs__` attribute. Meanwhile, typing.Protocol classes on 3.12.2+ + # break if we *don't* execute this block, because *they* assume that all + # protocol classes have a `__non_callable_proto_members__` attribute + # (which this block sets) + if isinstance(cls, _ProtocolMeta) or sys.version_info >= (3, 12, 2): + # PEP 544 prohibits using issubclass() + # with protocols that have non-method members. + # See gh-113320 for why we compute this attribute here, + # rather than in `_ProtocolMeta.__init__` + cls.__non_callable_proto_members__ = set() + for attr in cls.__protocol_attrs__: + try: + is_callable = callable(getattr(cls, attr, None)) + except Exception as e: + raise TypeError( + f"Failed to determine whether protocol member {attr!r} " + "is a method member" + ) from e + else: + if not is_callable: + cls.__non_callable_proto_members__.add(attr) + + return cls + + +# The "runtime" alias exists for backwards compatibility. +runtime = runtime_checkable + + +# Our version of runtime-checkable protocols is faster on Python <=3.11 +# Breakpoint: https://github.com/python/cpython/pull/112717 +if sys.version_info >= (3, 12): + SupportsInt = typing.SupportsInt + SupportsFloat = typing.SupportsFloat + SupportsComplex = typing.SupportsComplex + SupportsBytes = typing.SupportsBytes + SupportsIndex = typing.SupportsIndex + SupportsAbs = typing.SupportsAbs + SupportsRound = typing.SupportsRound +else: + @runtime_checkable + class SupportsInt(Protocol): + """An ABC with one abstract method __int__.""" + __slots__ = () + + @abc.abstractmethod + def __int__(self) -> int: + pass + + @runtime_checkable + class SupportsFloat(Protocol): + """An ABC with one abstract method __float__.""" + __slots__ = () + + @abc.abstractmethod + def __float__(self) -> float: + pass + + @runtime_checkable + class SupportsComplex(Protocol): + """An ABC with one abstract method __complex__.""" + __slots__ = () + + @abc.abstractmethod + def __complex__(self) -> complex: + pass + + @runtime_checkable + class SupportsBytes(Protocol): + """An ABC with one abstract method __bytes__.""" + __slots__ = () + + @abc.abstractmethod + def __bytes__(self) -> bytes: + pass + + @runtime_checkable + class SupportsIndex(Protocol): + __slots__ = () + + @abc.abstractmethod + def __index__(self) -> int: + pass + + @runtime_checkable + class SupportsAbs(Protocol[T_co]): + """ + An ABC with one abstract method __abs__ that is covariant in its return type. + """ + __slots__ = () + + @abc.abstractmethod + def __abs__(self) -> T_co: + pass + + @runtime_checkable + class SupportsRound(Protocol[T_co]): + """ + An ABC with one abstract method __round__ that is covariant in its return type. + """ + __slots__ = () + + @abc.abstractmethod + def __round__(self, ndigits: int = 0) -> T_co: + pass + + +if hasattr(io, "Reader") and hasattr(io, "Writer"): + Reader = io.Reader + Writer = io.Writer +else: + @runtime_checkable + class Reader(Protocol[T_co]): + """Protocol for simple I/O reader instances. + + This protocol only supports blocking I/O. + """ + + __slots__ = () + + @abc.abstractmethod + def read(self, size: int = ..., /) -> T_co: + """Read data from the input stream and return it. + + If *size* is specified, at most *size* items (bytes/characters) will be + read. + """ + + @runtime_checkable + class Writer(Protocol[T_contra]): + """Protocol for simple I/O writer instances. + + This protocol only supports blocking I/O. + """ + + __slots__ = () + + @abc.abstractmethod + def write(self, data: T_contra, /) -> int: + """Write *data* to the output stream and return the number of items written.""" # noqa: E501 + + +_NEEDS_SINGLETONMETA = ( + not hasattr(typing, "NoDefault") or not hasattr(typing, "NoExtraItems") +) + +if _NEEDS_SINGLETONMETA: + class SingletonMeta(type): + def __setattr__(cls, attr, value): + # TypeError is consistent with the behavior of NoneType + raise TypeError( + f"cannot set {attr!r} attribute of immutable type {cls.__name__!r}" + ) + + +if hasattr(typing, "NoDefault"): + NoDefault = typing.NoDefault +else: + class NoDefaultType(metaclass=SingletonMeta): + """The type of the NoDefault singleton.""" + + __slots__ = () + + def __new__(cls): + return globals().get("NoDefault") or object.__new__(cls) + + def __repr__(self): + return "typing_extensions.NoDefault" + + def __reduce__(self): + return "NoDefault" + + NoDefault = NoDefaultType() + del NoDefaultType + +if hasattr(typing, "NoExtraItems"): + NoExtraItems = typing.NoExtraItems +else: + class NoExtraItemsType(metaclass=SingletonMeta): + """The type of the NoExtraItems singleton.""" + + __slots__ = () + + def __new__(cls): + return globals().get("NoExtraItems") or object.__new__(cls) + + def __repr__(self): + return "typing_extensions.NoExtraItems" + + def __reduce__(self): + return "NoExtraItems" + + NoExtraItems = NoExtraItemsType() + del NoExtraItemsType + +if _NEEDS_SINGLETONMETA: + del SingletonMeta + + +# Update this to something like >=3.13.0b1 if and when +# PEP 764 is implemented in CPython +_PEP_764_IMPLEMENTED = False + +if _PEP_764_IMPLEMENTED: + # The standard library TypedDict in Python 3.9.0/1 does not honour the "total" + # keyword with old-style TypedDict(). See https://bugs.python.org/issue42059 + # The standard library TypedDict below Python 3.11 does not store runtime + # information about optional and required keys when using Required or NotRequired. + # Generic TypedDicts are also impossible using typing.TypedDict on Python <3.11. + # Aaaand on 3.12 we add __orig_bases__ to TypedDict + # to enable better runtime introspection. + # On 3.13 we deprecate some odd ways of creating TypedDicts. + # Also on 3.13, PEP 705 adds the ReadOnly[] qualifier. + # PEP 728 (Python 3.15+) adds the `extra_items` and `closed` keywords. + # PEP 764 (still pending) allows the `TypedDict` special form to be subscripted. + TypedDict = typing.TypedDict + _TypedDictMeta = typing._TypedDictMeta + is_typeddict = typing.is_typeddict +else: + # 3.10.0 and later + _TAKES_MODULE = "module" in inspect.signature(typing._type_check).parameters + + def _get_typeddict_qualifiers(annotation_type): + while True: + annotation_origin = get_origin(annotation_type) + if annotation_origin is Annotated: + annotation_args = get_args(annotation_type) + if annotation_args: + annotation_type = annotation_args[0] + else: + break + elif annotation_origin is Required: + yield Required + annotation_type, = get_args(annotation_type) + elif annotation_origin is NotRequired: + yield NotRequired + annotation_type, = get_args(annotation_type) + elif annotation_origin is ReadOnly: + yield ReadOnly + annotation_type, = get_args(annotation_type) + else: + break + + class _TypedDictMeta(type): + + def __new__(cls, name, bases, ns, *, total=True, closed=None, + extra_items=NoExtraItems): + """Create new typed dict class object. + + This method is called when TypedDict is subclassed, + or when TypedDict is instantiated. This way + TypedDict supports all three syntax forms described in its docstring. + Subclasses and instances of TypedDict return actual dictionaries. + """ + for base in bases: + if type(base) is not _TypedDictMeta and base is not typing.Generic: + raise TypeError('cannot inherit from both a TypedDict type ' + 'and a non-TypedDict base class') + if closed is not None and extra_items is not NoExtraItems: + raise TypeError(f"Cannot combine closed={closed!r} and extra_items") + + if any(issubclass(b, typing.Generic) for b in bases): + generic_base = (typing.Generic,) + else: + generic_base = () + + ns_annotations = ns.pop('__annotations__', None) + + # typing.py generally doesn't let you inherit from plain Generic, unless + # the name of the class happens to be "Protocol" + tp_dict = type.__new__(_TypedDictMeta, "Protocol", (*generic_base, dict), ns) + tp_dict.__name__ = name + if tp_dict.__qualname__ == "Protocol": + tp_dict.__qualname__ = name + + if not hasattr(tp_dict, '__orig_bases__'): + tp_dict.__orig_bases__ = bases + + annotations = {} + own_annotate = None + if ns_annotations is not None: + own_annotations = ns_annotations + elif sys.version_info >= (3, 14): + if hasattr(annotationlib, "get_annotate_from_class_namespace"): + own_annotate = annotationlib.get_annotate_from_class_namespace(ns) + else: + # 3.14.0a7 and earlier + own_annotate = ns.get("__annotate__") + if own_annotate is not None: + own_annotations = annotationlib.call_annotate_function( + own_annotate, Format.FORWARDREF, owner=tp_dict + ) + else: + own_annotations = {} + else: + own_annotations = {} + msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" + if _TAKES_MODULE: + own_checked_annotations = { + n: typing._type_check(tp, msg, module=tp_dict.__module__) + for n, tp in own_annotations.items() + } + else: + own_checked_annotations = { + n: typing._type_check(tp, msg) + for n, tp in own_annotations.items() + } + required_keys = set() + optional_keys = set() + readonly_keys = set() + mutable_keys = set() + extra_items_type = extra_items + + for base in bases: + base_dict = base.__dict__ + + if sys.version_info <= (3, 14): + annotations.update(base_dict.get('__annotations__', {})) + base_required = base_dict.get('__required_keys__', set()) + required_keys |= base_required + optional_keys -= base_required + + base_optional = base_dict.get('__optional_keys__', set()) + required_keys -= base_optional + optional_keys |= base_optional + + readonly_keys.update(base_dict.get('__readonly_keys__', ())) + mutable_keys.update(base_dict.get('__mutable_keys__', ())) + + # This was specified in an earlier version of PEP 728. Support + # is retained for backwards compatibility, but only for Python + # 3.13 and lower. + if (closed and sys.version_info < (3, 14) + and "__extra_items__" in own_checked_annotations): + annotation_type = own_checked_annotations.pop("__extra_items__") + qualifiers = set(_get_typeddict_qualifiers(annotation_type)) + if Required in qualifiers: + raise TypeError( + "Special key __extra_items__ does not support " + "Required" + ) + if NotRequired in qualifiers: + raise TypeError( + "Special key __extra_items__ does not support " + "NotRequired" + ) + extra_items_type = annotation_type + + annotations.update(own_checked_annotations) + for annotation_key, annotation_type in own_checked_annotations.items(): + qualifiers = set(_get_typeddict_qualifiers(annotation_type)) + + if Required in qualifiers: + is_required = True + elif NotRequired in qualifiers: + is_required = False + else: + is_required = total + + if is_required: + required_keys.add(annotation_key) + optional_keys.discard(annotation_key) + else: + optional_keys.add(annotation_key) + required_keys.discard(annotation_key) + + if ReadOnly in qualifiers: + mutable_keys.discard(annotation_key) + readonly_keys.add(annotation_key) + else: + mutable_keys.add(annotation_key) + readonly_keys.discard(annotation_key) + + # Breakpoint: https://github.com/python/cpython/pull/119891 + if sys.version_info >= (3, 14): + def __annotate__(format): + annos = {} + for base in bases: + if base is Generic: + continue + base_annotate = base.__annotate__ + if base_annotate is None: + continue + base_annos = annotationlib.call_annotate_function( + base_annotate, format, owner=base) + annos.update(base_annos) + if own_annotate is not None: + own = annotationlib.call_annotate_function( + own_annotate, format, owner=tp_dict) + if format != Format.STRING: + own = { + n: typing._type_check(tp, msg, module=tp_dict.__module__) + for n, tp in own.items() + } + elif format == Format.STRING: + own = annotationlib.annotations_to_string(own_annotations) + elif format in (Format.FORWARDREF, Format.VALUE): + own = own_checked_annotations + else: + raise NotImplementedError(format) + annos.update(own) + return annos + + tp_dict.__annotate__ = __annotate__ + else: + tp_dict.__annotations__ = annotations + tp_dict.__required_keys__ = frozenset(required_keys) + tp_dict.__optional_keys__ = frozenset(optional_keys) + tp_dict.__readonly_keys__ = frozenset(readonly_keys) + tp_dict.__mutable_keys__ = frozenset(mutable_keys) + tp_dict.__total__ = total + tp_dict.__closed__ = closed + tp_dict.__extra_items__ = extra_items_type + return tp_dict + + __call__ = dict # static method + + def __subclasscheck__(cls, other): + # Typed dicts are only for static structural subtyping. + raise TypeError('TypedDict does not support instance and class checks') + + __instancecheck__ = __subclasscheck__ + + _TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {}) + + def _create_typeddict( + typename, + fields, + /, + *, + typing_is_inline, + total, + closed, + extra_items, + **kwargs, + ): + if fields is _marker or fields is None: + if fields is _marker: + deprecated_thing = ( + "Failing to pass a value for the 'fields' parameter" + ) + else: + deprecated_thing = "Passing `None` as the 'fields' parameter" + + example = f"`{typename} = TypedDict({typename!r}, {{}})`" + deprecation_msg = ( + f"{deprecated_thing} is deprecated and will be disallowed in " + "Python 3.15. To create a TypedDict class with 0 fields " + "using the functional syntax, pass an empty dictionary, e.g. " + ) + example + "." + warnings.warn(deprecation_msg, DeprecationWarning, stacklevel=2) + # Support a field called "closed" + if closed is not False and closed is not True and closed is not None: + kwargs["closed"] = closed + closed = None + # Or "extra_items" + if extra_items is not NoExtraItems: + kwargs["extra_items"] = extra_items + extra_items = NoExtraItems + fields = kwargs + elif kwargs: + raise TypeError("TypedDict takes either a dict or keyword arguments," + " but not both") + if kwargs: + # Breakpoint: https://github.com/python/cpython/pull/104891 + if sys.version_info >= (3, 13): + raise TypeError("TypedDict takes no keyword arguments") + warnings.warn( + "The kwargs-based syntax for TypedDict definitions is deprecated " + "in Python 3.11, will be removed in Python 3.13, and may not be " + "understood by third-party type checkers.", + DeprecationWarning, + stacklevel=2, + ) + + ns = {'__annotations__': dict(fields)} + module = _caller(depth=4 if typing_is_inline else 2) + if module is not None: + # Setting correct module is necessary to make typed dict classes + # pickleable. + ns['__module__'] = module + + td = _TypedDictMeta(typename, (), ns, total=total, closed=closed, + extra_items=extra_items) + td.__orig_bases__ = (TypedDict,) + return td + + class _TypedDictSpecialForm(_SpecialForm, _root=True): + def __call__( + self, + typename, + fields=_marker, + /, + *, + total=True, + closed=None, + extra_items=NoExtraItems, + **kwargs + ): + return _create_typeddict( + typename, + fields, + typing_is_inline=False, + total=total, + closed=closed, + extra_items=extra_items, + **kwargs, + ) + + def __mro_entries__(self, bases): + return (_TypedDict,) + + @_TypedDictSpecialForm + def TypedDict(self, args): + """A simple typed namespace. At runtime it is equivalent to a plain dict. + + TypedDict creates a dictionary type such that a type checker will expect all + instances to have a certain set of keys, where each key is + associated with a value of a consistent type. This expectation + is not checked at runtime. + + Usage:: + + class Point2D(TypedDict): + x: int + y: int + label: str + + a: Point2D = {'x': 1, 'y': 2, 'label': 'good'} # OK + b: Point2D = {'z': 3, 'label': 'bad'} # Fails type check + + assert Point2D(x=1, y=2, label='first') == dict(x=1, y=2, label='first') + + The type info can be accessed via the Point2D.__annotations__ dict, and + the Point2D.__required_keys__ and Point2D.__optional_keys__ frozensets. + TypedDict supports an additional equivalent form:: + + Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str}) + + By default, all keys must be present in a TypedDict. It is possible + to override this by specifying totality:: + + class Point2D(TypedDict, total=False): + x: int + y: int + + This means that a Point2D TypedDict can have any of the keys omitted. A type + checker is only expected to support a literal False or True as the value of + the total argument. True is the default, and makes all items defined in the + class body be required. + + The Required and NotRequired special forms can also be used to mark + individual keys as being required or not required:: + + class Point2D(TypedDict): + x: int # the "x" key must always be present (Required is the default) + y: NotRequired[int] # the "y" key can be omitted + + See PEP 655 for more details on Required and NotRequired. + """ + # This runs when creating inline TypedDicts: + if not isinstance(args, dict): + raise TypeError( + "TypedDict[...] should be used with a single dict argument" + ) + + return _create_typeddict( + "", + args, + typing_is_inline=True, + total=True, + closed=True, + extra_items=NoExtraItems, + ) + + _TYPEDDICT_TYPES = (typing._TypedDictMeta, _TypedDictMeta) + + def is_typeddict(tp): + """Check if an annotation is a TypedDict class + + For example:: + class Film(TypedDict): + title: str + year: int + + is_typeddict(Film) # => True + is_typeddict(Union[list, str]) # => False + """ + return isinstance(tp, _TYPEDDICT_TYPES) + + +if hasattr(typing, "assert_type"): + assert_type = typing.assert_type + +else: + def assert_type(val, typ, /): + """Assert (to the type checker) that the value is of the given type. + + When the type checker encounters a call to assert_type(), it + emits an error if the value is not of the specified type:: + + def greet(name: str) -> None: + assert_type(name, str) # ok + assert_type(name, int) # type checker error + + At runtime this returns the first argument unchanged and otherwise + does nothing. + """ + return val + + +if hasattr(typing, "ReadOnly"): # 3.13+ + get_type_hints = typing.get_type_hints +else: # <=3.13 + # replaces _strip_annotations() + def _strip_extras(t): + """Strips Annotated, Required and NotRequired from a given type.""" + if isinstance(t, typing._AnnotatedAlias): + return _strip_extras(t.__origin__) + if hasattr(t, "__origin__") and t.__origin__ in (Required, NotRequired, ReadOnly): + return _strip_extras(t.__args__[0]) + if isinstance(t, typing._GenericAlias): + stripped_args = tuple(_strip_extras(a) for a in t.__args__) + if stripped_args == t.__args__: + return t + return t.copy_with(stripped_args) + if hasattr(_types, "GenericAlias") and isinstance(t, _types.GenericAlias): + stripped_args = tuple(_strip_extras(a) for a in t.__args__) + if stripped_args == t.__args__: + return t + return _types.GenericAlias(t.__origin__, stripped_args) + if hasattr(_types, "UnionType") and isinstance(t, _types.UnionType): + stripped_args = tuple(_strip_extras(a) for a in t.__args__) + if stripped_args == t.__args__: + return t + return functools.reduce(operator.or_, stripped_args) + + return t + + def get_type_hints(obj, globalns=None, localns=None, include_extras=False): + """Return type hints for an object. + + This is often the same as obj.__annotations__, but it handles + forward references encoded as string literals, adds Optional[t] if a + default value equal to None is set and recursively replaces all + 'Annotated[T, ...]', 'Required[T]' or 'NotRequired[T]' with 'T' + (unless 'include_extras=True'). + + The argument may be a module, class, method, or function. The annotations + are returned as a dictionary. For classes, annotations include also + inherited members. + + TypeError is raised if the argument is not of a type that can contain + annotations, and an empty dictionary is returned if no annotations are + present. + + BEWARE -- the behavior of globalns and localns is counterintuitive + (unless you are familiar with how eval() and exec() work). The + search order is locals first, then globals. + + - If no dict arguments are passed, an attempt is made to use the + globals from obj (or the respective module's globals for classes), + and these are also used as the locals. If the object does not appear + to have globals, an empty dictionary is used. + + - If one dict argument is passed, it is used for both globals and + locals. + + - If two dict arguments are passed, they specify globals and + locals, respectively. + """ + hint = typing.get_type_hints( + obj, globalns=globalns, localns=localns, include_extras=True + ) + # Breakpoint: https://github.com/python/cpython/pull/30304 + if sys.version_info < (3, 11): + _clean_optional(obj, hint, globalns, localns) + if include_extras: + return hint + return {k: _strip_extras(t) for k, t in hint.items()} + + _NoneType = type(None) + + def _could_be_inserted_optional(t): + """detects Union[..., None] pattern""" + if not isinstance(t, typing._UnionGenericAlias): + return False + # Assume if last argument is not None they are user defined + if t.__args__[-1] is not _NoneType: + return False + return True + + # < 3.11 + def _clean_optional(obj, hints, globalns=None, localns=None): + # reverts injected Union[..., None] cases from typing.get_type_hints + # when a None default value is used. + # see https://github.com/python/typing_extensions/issues/310 + if not hints or isinstance(obj, type): + return + defaults = typing._get_defaults(obj) # avoid accessing __annotations___ + if not defaults: + return + original_hints = obj.__annotations__ + for name, value in hints.items(): + # Not a Union[..., None] or replacement conditions not fullfilled + if (not _could_be_inserted_optional(value) + or name not in defaults + or defaults[name] is not None + ): + continue + original_value = original_hints[name] + # value=NoneType should have caused a skip above but check for safety + if original_value is None: + original_value = _NoneType + # Forward reference + if isinstance(original_value, str): + if globalns is None: + if isinstance(obj, _types.ModuleType): + globalns = obj.__dict__ + else: + nsobj = obj + # Find globalns for the unwrapped object. + while hasattr(nsobj, '__wrapped__'): + nsobj = nsobj.__wrapped__ + globalns = getattr(nsobj, '__globals__', {}) + if localns is None: + localns = globalns + elif localns is None: + localns = globalns + + original_value = ForwardRef( + original_value, + is_argument=not isinstance(obj, _types.ModuleType) + ) + original_evaluated = typing._eval_type(original_value, globalns, localns) + # Compare if values differ. Note that even if equal + # value might be cached by typing._tp_cache contrary to original_evaluated + if original_evaluated != value or ( + # 3.10: ForwardRefs of UnionType might be turned into _UnionGenericAlias + hasattr(_types, "UnionType") + and isinstance(original_evaluated, _types.UnionType) + and not isinstance(value, _types.UnionType) + ): + hints[name] = original_evaluated + +# Python 3.9 has get_origin() and get_args() but those implementations don't support +# ParamSpecArgs and ParamSpecKwargs, so only Python 3.10's versions will do. +# Breakpoint: https://github.com/python/cpython/pull/25298 +if sys.version_info >= (3, 10): + get_origin = typing.get_origin + get_args = typing.get_args +# 3.9 +else: + def get_origin(tp): + """Get the unsubscripted version of a type. + + This supports generic types, Callable, Tuple, Union, Literal, Final, ClassVar + and Annotated. Return None for unsupported types. Examples:: + + get_origin(Literal[42]) is Literal + get_origin(int) is None + get_origin(ClassVar[int]) is ClassVar + get_origin(Generic) is Generic + get_origin(Generic[T]) is Generic + get_origin(Union[T, int]) is Union + get_origin(List[Tuple[T, T]][int]) == list + get_origin(P.args) is P + """ + if isinstance(tp, typing._AnnotatedAlias): + return Annotated + if isinstance(tp, (typing._BaseGenericAlias, _types.GenericAlias, + ParamSpecArgs, ParamSpecKwargs)): + return tp.__origin__ + if tp is typing.Generic: + return typing.Generic + return None + + def get_args(tp): + """Get type arguments with all substitutions performed. + + For unions, basic simplifications used by Union constructor are performed. + Examples:: + get_args(Dict[str, int]) == (str, int) + get_args(int) == () + get_args(Union[int, Union[T, int], str][int]) == (int, str) + get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int]) + get_args(Callable[[], T][int]) == ([], int) + """ + if isinstance(tp, typing._AnnotatedAlias): + return (tp.__origin__, *tp.__metadata__) + if isinstance(tp, (typing._GenericAlias, _types.GenericAlias)): + res = tp.__args__ + if get_origin(tp) is collections.abc.Callable and res[0] is not Ellipsis: + res = (list(res[:-1]), res[-1]) + return res + return () + + +# 3.10+ +if hasattr(typing, 'TypeAlias'): + TypeAlias = typing.TypeAlias +# 3.9 +else: + @_ExtensionsSpecialForm + def TypeAlias(self, parameters): + """Special marker indicating that an assignment should + be recognized as a proper type alias definition by type + checkers. + + For example:: + + Predicate: TypeAlias = Callable[..., bool] + + It's invalid when used anywhere except as in the example above. + """ + raise TypeError(f"{self} is not subscriptable") + + +def _set_default(type_param, default): + type_param.has_default = lambda: default is not NoDefault + type_param.__default__ = default + + +def _set_module(typevarlike): + # for pickling: + def_mod = _caller(depth=2) + if def_mod != 'typing_extensions': + typevarlike.__module__ = def_mod + + +class _DefaultMixin: + """Mixin for TypeVarLike defaults.""" + + __slots__ = () + __init__ = _set_default + + +# Classes using this metaclass must provide a _backported_typevarlike ClassVar +class _TypeVarLikeMeta(type): + def __instancecheck__(cls, __instance: Any) -> bool: + return isinstance(__instance, cls._backported_typevarlike) + + +if _PEP_696_IMPLEMENTED: + from typing import TypeVar +else: + # Add default and infer_variance parameters from PEP 696 and 695 + class TypeVar(metaclass=_TypeVarLikeMeta): + """Type variable.""" + + _backported_typevarlike = typing.TypeVar + + def __new__(cls, name, *constraints, bound=None, + covariant=False, contravariant=False, + default=NoDefault, infer_variance=False): + if hasattr(typing, "TypeAliasType"): + # PEP 695 implemented (3.12+), can pass infer_variance to typing.TypeVar + typevar = typing.TypeVar(name, *constraints, bound=bound, + covariant=covariant, contravariant=contravariant, + infer_variance=infer_variance) + else: + typevar = typing.TypeVar(name, *constraints, bound=bound, + covariant=covariant, contravariant=contravariant) + if infer_variance and (covariant or contravariant): + raise ValueError("Variance cannot be specified with infer_variance.") + typevar.__infer_variance__ = infer_variance + + _set_default(typevar, default) + _set_module(typevar) + + def _tvar_prepare_subst(alias, args): + if ( + typevar.has_default() + and alias.__parameters__.index(typevar) == len(args) + ): + args += (typevar.__default__,) + return args + + typevar.__typing_prepare_subst__ = _tvar_prepare_subst + return typevar + + def __init_subclass__(cls) -> None: + raise TypeError(f"type '{__name__}.TypeVar' is not an acceptable base type") + + +# Python 3.10+ has PEP 612 +if hasattr(typing, 'ParamSpecArgs'): + ParamSpecArgs = typing.ParamSpecArgs + ParamSpecKwargs = typing.ParamSpecKwargs +# 3.9 +else: + class _Immutable: + """Mixin to indicate that object should not be copied.""" + __slots__ = () + + def __copy__(self): + return self + + def __deepcopy__(self, memo): + return self + + class ParamSpecArgs(_Immutable): + """The args for a ParamSpec object. + + Given a ParamSpec object P, P.args is an instance of ParamSpecArgs. + + ParamSpecArgs objects have a reference back to their ParamSpec: + + P.args.__origin__ is P + + This type is meant for runtime introspection and has no special meaning to + static type checkers. + """ + def __init__(self, origin): + self.__origin__ = origin + + def __repr__(self): + return f"{self.__origin__.__name__}.args" + + def __eq__(self, other): + if not isinstance(other, ParamSpecArgs): + return NotImplemented + return self.__origin__ == other.__origin__ + + class ParamSpecKwargs(_Immutable): + """The kwargs for a ParamSpec object. + + Given a ParamSpec object P, P.kwargs is an instance of ParamSpecKwargs. + + ParamSpecKwargs objects have a reference back to their ParamSpec: + + P.kwargs.__origin__ is P + + This type is meant for runtime introspection and has no special meaning to + static type checkers. + """ + def __init__(self, origin): + self.__origin__ = origin + + def __repr__(self): + return f"{self.__origin__.__name__}.kwargs" + + def __eq__(self, other): + if not isinstance(other, ParamSpecKwargs): + return NotImplemented + return self.__origin__ == other.__origin__ + + +if _PEP_696_IMPLEMENTED: + from typing import ParamSpec + +# 3.10+ +elif hasattr(typing, 'ParamSpec'): + + # Add default parameter - PEP 696 + class ParamSpec(metaclass=_TypeVarLikeMeta): + """Parameter specification.""" + + _backported_typevarlike = typing.ParamSpec + + def __new__(cls, name, *, bound=None, + covariant=False, contravariant=False, + infer_variance=False, default=NoDefault): + if hasattr(typing, "TypeAliasType"): + # PEP 695 implemented, can pass infer_variance to typing.TypeVar + paramspec = typing.ParamSpec(name, bound=bound, + covariant=covariant, + contravariant=contravariant, + infer_variance=infer_variance) + else: + paramspec = typing.ParamSpec(name, bound=bound, + covariant=covariant, + contravariant=contravariant) + paramspec.__infer_variance__ = bool(infer_variance) + + _set_default(paramspec, default) + _set_module(paramspec) + + def _paramspec_prepare_subst(alias, args): + params = alias.__parameters__ + i = params.index(paramspec) + if i == len(args) and paramspec.has_default(): + args = [*args, paramspec.__default__] + if i >= len(args): + raise TypeError(f"Too few arguments for {alias}") + # Special case where Z[[int, str, bool]] == Z[int, str, bool] in PEP 612. + if len(params) == 1 and not typing._is_param_expr(args[0]): + assert i == 0 + args = (args,) + # Convert lists to tuples to help other libraries cache the results. + elif isinstance(args[i], list): + args = (*args[:i], tuple(args[i]), *args[i + 1:]) + return args + + paramspec.__typing_prepare_subst__ = _paramspec_prepare_subst + return paramspec + + def __init_subclass__(cls) -> None: + raise TypeError(f"type '{__name__}.ParamSpec' is not an acceptable base type") + +# 3.9 +else: + + # Inherits from list as a workaround for Callable checks in Python < 3.9.2. + class ParamSpec(list, _DefaultMixin): + """Parameter specification variable. + + Usage:: + + P = ParamSpec('P') + + Parameter specification variables exist primarily for the benefit of static + type checkers. They are used to forward the parameter types of one + callable to another callable, a pattern commonly found in higher order + functions and decorators. They are only valid when used in ``Concatenate``, + or s the first argument to ``Callable``. In Python 3.10 and higher, + they are also supported in user-defined Generics at runtime. + See class Generic for more information on generic types. An + example for annotating a decorator:: + + T = TypeVar('T') + P = ParamSpec('P') + + def add_logging(f: Callable[P, T]) -> Callable[P, T]: + '''A type-safe decorator to add logging to a function.''' + def inner(*args: P.args, **kwargs: P.kwargs) -> T: + logging.info(f'{f.__name__} was called') + return f(*args, **kwargs) + return inner + + @add_logging + def add_two(x: float, y: float) -> float: + '''Add two numbers together.''' + return x + y + + Parameter specification variables defined with covariant=True or + contravariant=True can be used to declare covariant or contravariant + generic types. These keyword arguments are valid, but their actual semantics + are yet to be decided. See PEP 612 for details. + + Parameter specification variables can be introspected. e.g.: + + P.__name__ == 'T' + P.__bound__ == None + P.__covariant__ == False + P.__contravariant__ == False + + Note that only parameter specification variables defined in global scope can + be pickled. + """ + + # Trick Generic __parameters__. + __class__ = typing.TypeVar + + @property + def args(self): + return ParamSpecArgs(self) + + @property + def kwargs(self): + return ParamSpecKwargs(self) + + def __init__(self, name, *, bound=None, covariant=False, contravariant=False, + infer_variance=False, default=NoDefault): + list.__init__(self, [self]) + self.__name__ = name + self.__covariant__ = bool(covariant) + self.__contravariant__ = bool(contravariant) + self.__infer_variance__ = bool(infer_variance) + self.__bound__ = bound + _DefaultMixin.__init__(self, default) + + # for pickling: + def_mod = _caller() + if def_mod != 'typing_extensions': + self.__module__ = def_mod + + def __repr__(self): + if self.__infer_variance__: + prefix = '' + elif self.__covariant__: + prefix = '+' + elif self.__contravariant__: + prefix = '-' + else: + prefix = '~' + return prefix + self.__name__ + + def __hash__(self): + return object.__hash__(self) + + def __eq__(self, other): + return self is other + + def __reduce__(self): + return self.__name__ + + # Hack to get typing._type_check to pass. + def __call__(self, *args, **kwargs): + pass + + def __init_subclass__(cls) -> None: + raise TypeError(f"type '{__name__}.ParamSpec' is not an acceptable base type") + + +# 3.9 +if not hasattr(typing, 'Concatenate'): + # Inherits from list as a workaround for Callable checks in Python < 3.9.2. + + # 3.9.0-1 + if not hasattr(typing, '_type_convert'): + def _type_convert(arg, module=None, *, allow_special_forms=False): + """For converting None to type(None), and strings to ForwardRef.""" + if arg is None: + return type(None) + if isinstance(arg, str): + if sys.version_info <= (3, 9, 6): + return ForwardRef(arg) + if sys.version_info <= (3, 9, 7): + return ForwardRef(arg, module=module) + return ForwardRef(arg, module=module, is_class=allow_special_forms) + return arg + else: + _type_convert = typing._type_convert + + class _ConcatenateGenericAlias(list): + + # Trick Generic into looking into this for __parameters__. + __class__ = typing._GenericAlias + + def __init__(self, origin, args): + # Cannot use `super().__init__` here because of the `__class__` assignment + # in the class body (https://github.com/python/typing_extensions/issues/661) + list.__init__(self, args) + self.__origin__ = origin + self.__args__ = args + + def __repr__(self): + _type_repr = typing._type_repr + return (f'{_type_repr(self.__origin__)}' + f'[{", ".join(_type_repr(arg) for arg in self.__args__)}]') + + def __hash__(self): + return hash((self.__origin__, self.__args__)) + + # Hack to get typing._type_check to pass in Generic. + def __call__(self, *args, **kwargs): + pass + + @property + def __parameters__(self): + return tuple( + tp for tp in self.__args__ if isinstance(tp, (typing.TypeVar, ParamSpec)) + ) + + # 3.9 used by __getitem__ below + def copy_with(self, params): + if isinstance(params[-1], _ConcatenateGenericAlias): + params = (*params[:-1], *params[-1].__args__) + elif isinstance(params[-1], (list, tuple)): + return (*params[:-1], *params[-1]) + elif (not (params[-1] is ... or isinstance(params[-1], ParamSpec))): + raise TypeError("The last parameter to Concatenate should be a " + "ParamSpec variable or ellipsis.") + return self.__class__(self.__origin__, params) + + # 3.9; accessed during GenericAlias.__getitem__ when substituting + def __getitem__(self, args): + if self.__origin__ in (Generic, Protocol): + # Can't subscript Generic[...] or Protocol[...]. + raise TypeError(f"Cannot subscript already-subscripted {self}") + if not self.__parameters__: + raise TypeError(f"{self} is not a generic class") + + if not isinstance(args, tuple): + args = (args,) + args = _unpack_args(*(_type_convert(p) for p in args)) + params = self.__parameters__ + for param in params: + prepare = getattr(param, "__typing_prepare_subst__", None) + if prepare is not None: + args = prepare(self, args) + # 3.9 & typing.ParamSpec + elif isinstance(param, ParamSpec): + i = params.index(param) + if ( + i == len(args) + and getattr(param, '__default__', NoDefault) is not NoDefault + ): + args = [*args, param.__default__] + if i >= len(args): + raise TypeError(f"Too few arguments for {self}") + # Special case for Z[[int, str, bool]] == Z[int, str, bool] + if len(params) == 1 and not _is_param_expr(args[0]): + assert i == 0 + args = (args,) + elif ( + isinstance(args[i], list) + # 3.9 + # This class inherits from list do not convert + and not isinstance(args[i], _ConcatenateGenericAlias) + ): + args = (*args[:i], tuple(args[i]), *args[i + 1:]) + + alen = len(args) + plen = len(params) + if alen != plen: + raise TypeError( + f"Too {'many' if alen > plen else 'few'} arguments for {self};" + f" actual {alen}, expected {plen}" + ) + + subst = dict(zip(self.__parameters__, args)) + # determine new args + new_args = [] + for arg in self.__args__: + if isinstance(arg, type): + new_args.append(arg) + continue + if isinstance(arg, TypeVar): + arg = subst[arg] + if ( + (isinstance(arg, typing._GenericAlias) and _is_unpack(arg)) + or ( + hasattr(_types, "GenericAlias") + and isinstance(arg, _types.GenericAlias) + and getattr(arg, "__unpacked__", False) + ) + ): + raise TypeError(f"{arg} is not valid as type argument") + + elif isinstance(arg, + typing._GenericAlias + if not hasattr(_types, "GenericAlias") else + (typing._GenericAlias, _types.GenericAlias) + ): + subparams = arg.__parameters__ + if subparams: + subargs = tuple(subst[x] for x in subparams) + arg = arg[subargs] + new_args.append(arg) + return self.copy_with(tuple(new_args)) + +# 3.10+ +else: + _ConcatenateGenericAlias = typing._ConcatenateGenericAlias + + # 3.10 + if sys.version_info < (3, 11): + + class _ConcatenateGenericAlias(typing._ConcatenateGenericAlias, _root=True): + # needed for checks in collections.abc.Callable to accept this class + __module__ = "typing" + + def copy_with(self, params): + if isinstance(params[-1], (list, tuple)): + return (*params[:-1], *params[-1]) + if isinstance(params[-1], typing._ConcatenateGenericAlias): + params = (*params[:-1], *params[-1].__args__) + elif not (params[-1] is ... or isinstance(params[-1], ParamSpec)): + raise TypeError("The last parameter to Concatenate should be a " + "ParamSpec variable or ellipsis.") + return super(typing._ConcatenateGenericAlias, self).copy_with(params) + + def __getitem__(self, args): + value = super().__getitem__(args) + if isinstance(value, tuple) and any(_is_unpack(t) for t in value): + return tuple(_unpack_args(*(n for n in value))) + return value + + +# 3.9.2 +class _EllipsisDummy: ... + + +# <=3.10 +def _create_concatenate_alias(origin, parameters): + if parameters[-1] is ... and sys.version_info < (3, 9, 2): + # Hack: Arguments must be types, replace it with one. + parameters = (*parameters[:-1], _EllipsisDummy) + if sys.version_info >= (3, 10, 3): + concatenate = _ConcatenateGenericAlias(origin, parameters, + _typevar_types=(TypeVar, ParamSpec), + _paramspec_tvars=True) + else: + concatenate = _ConcatenateGenericAlias(origin, parameters) + if parameters[-1] is not _EllipsisDummy: + return concatenate + # Remove dummy again + concatenate.__args__ = tuple(p if p is not _EllipsisDummy else ... + for p in concatenate.__args__) + if sys.version_info < (3, 10): + # backport needs __args__ adjustment only + return concatenate + concatenate.__parameters__ = tuple(p for p in concatenate.__parameters__ + if p is not _EllipsisDummy) + return concatenate + + +# <=3.10 +@typing._tp_cache +def _concatenate_getitem(self, parameters): + if parameters == (): + raise TypeError("Cannot take a Concatenate of no types.") + if not isinstance(parameters, tuple): + parameters = (parameters,) + if not (parameters[-1] is ... or isinstance(parameters[-1], ParamSpec)): + raise TypeError("The last parameter to Concatenate should be a " + "ParamSpec variable or ellipsis.") + msg = "Concatenate[arg, ...]: each arg must be a type." + parameters = (*(typing._type_check(p, msg) for p in parameters[:-1]), + parameters[-1]) + return _create_concatenate_alias(self, parameters) + + +# 3.11+; Concatenate does not accept ellipsis in 3.10 +# Breakpoint: https://github.com/python/cpython/pull/30969 +if sys.version_info >= (3, 11): + Concatenate = typing.Concatenate +# <=3.10 +else: + @_ExtensionsSpecialForm + def Concatenate(self, parameters): + """Used in conjunction with ``ParamSpec`` and ``Callable`` to represent a + higher order function which adds, removes or transforms parameters of a + callable. + + For example:: + + Callable[Concatenate[int, P], int] + + See PEP 612 for detailed information. + """ + return _concatenate_getitem(self, parameters) + + +# 3.10+ +if hasattr(typing, 'TypeGuard'): + TypeGuard = typing.TypeGuard +# 3.9 +else: + @_ExtensionsSpecialForm + def TypeGuard(self, parameters): + """Special typing form used to annotate the return type of a user-defined + type guard function. ``TypeGuard`` only accepts a single type argument. + At runtime, functions marked this way should return a boolean. + + ``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static + type checkers to determine a more precise type of an expression within a + program's code flow. Usually type narrowing is done by analyzing + conditional code flow and applying the narrowing to a block of code. The + conditional expression here is sometimes referred to as a "type guard". + + Sometimes it would be convenient to use a user-defined boolean function + as a type guard. Such a function should use ``TypeGuard[...]`` as its + return type to alert static type checkers to this intention. + + Using ``-> TypeGuard`` tells the static type checker that for a given + function: + + 1. The return value is a boolean. + 2. If the return value is ``True``, the type of its argument + is the type inside ``TypeGuard``. + + For example:: + + def is_str(val: Union[str, float]): + # "isinstance" type guard + if isinstance(val, str): + # Type of ``val`` is narrowed to ``str`` + ... + else: + # Else, type of ``val`` is narrowed to ``float``. + ... + + Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower + form of ``TypeA`` (it can even be a wider form) and this may lead to + type-unsafe results. The main reason is to allow for things like + narrowing ``List[object]`` to ``List[str]`` even though the latter is not + a subtype of the former, since ``List`` is invariant. The responsibility of + writing type-safe type guards is left to the user. + + ``TypeGuard`` also works with type variables. For more information, see + PEP 647 (User-Defined Type Guards). + """ + item = typing._type_check(parameters, f'{self} accepts only a single type.') + return typing._GenericAlias(self, (item,)) + + +# 3.13+ +if hasattr(typing, 'TypeIs'): + TypeIs = typing.TypeIs +# <=3.12 +else: + @_ExtensionsSpecialForm + def TypeIs(self, parameters): + """Special typing form used to annotate the return type of a user-defined + type narrower function. ``TypeIs`` only accepts a single type argument. + At runtime, functions marked this way should return a boolean. + + ``TypeIs`` aims to benefit *type narrowing* -- a technique used by static + type checkers to determine a more precise type of an expression within a + program's code flow. Usually type narrowing is done by analyzing + conditional code flow and applying the narrowing to a block of code. The + conditional expression here is sometimes referred to as a "type guard". + + Sometimes it would be convenient to use a user-defined boolean function + as a type guard. Such a function should use ``TypeIs[...]`` as its + return type to alert static type checkers to this intention. + + Using ``-> TypeIs`` tells the static type checker that for a given + function: + + 1. The return value is a boolean. + 2. If the return value is ``True``, the type of its argument + is the intersection of the type inside ``TypeIs`` and the argument's + previously known type. + + For example:: + + def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]: + return hasattr(val, '__await__') + + def f(val: Union[int, Awaitable[int]]) -> int: + if is_awaitable(val): + assert_type(val, Awaitable[int]) + else: + assert_type(val, int) + + ``TypeIs`` also works with type variables. For more information, see + PEP 742 (Narrowing types with TypeIs). + """ + item = typing._type_check(parameters, f'{self} accepts only a single type.') + return typing._GenericAlias(self, (item,)) + + +# 3.15+? +if hasattr(typing, 'TypeForm'): + TypeForm = typing.TypeForm +# <=3.14 +else: + class _TypeFormForm(_ExtensionsSpecialForm, _root=True): + # TypeForm(X) is equivalent to X but indicates to the type checker + # that the object is a TypeForm. + def __call__(self, obj, /): + return obj + + @_TypeFormForm + def TypeForm(self, parameters): + """A special form representing the value that results from the evaluation + of a type expression. This value encodes the information supplied in the + type expression, and it represents the type described by that type expression. + + When used in a type expression, TypeForm describes a set of type form objects. + It accepts a single type argument, which must be a valid type expression. + ``TypeForm[T]`` describes the set of all type form objects that represent + the type T or types that are assignable to T. + + Usage: + + def cast[T](typ: TypeForm[T], value: Any) -> T: ... + + reveal_type(cast(int, "x")) # int + + See PEP 747 for more information. + """ + item = typing._type_check(parameters, f'{self} accepts only a single type.') + return typing._GenericAlias(self, (item,)) + + + + +if hasattr(typing, "LiteralString"): # 3.11+ + LiteralString = typing.LiteralString +else: + @_SpecialForm + def LiteralString(self, params): + """Represents an arbitrary literal string. + + Example:: + + from typing_extensions import LiteralString + + def query(sql: LiteralString) -> ...: + ... + + query("SELECT * FROM table") # ok + query(f"SELECT * FROM {input()}") # not ok + + See PEP 675 for details. + + """ + raise TypeError(f"{self} is not subscriptable") + + +if hasattr(typing, "Self"): # 3.11+ + Self = typing.Self +else: + @_SpecialForm + def Self(self, params): + """Used to spell the type of "self" in classes. + + Example:: + + from typing import Self + + class ReturnsSelf: + def parse(self, data: bytes) -> Self: + ... + return self + + """ + + raise TypeError(f"{self} is not subscriptable") + + +if hasattr(typing, "Never"): # 3.11+ + Never = typing.Never +else: + @_SpecialForm + def Never(self, params): + """The bottom type, a type that has no members. + + This can be used to define a function that should never be + called, or a function that never returns:: + + from typing_extensions import Never + + def never_call_me(arg: Never) -> None: + pass + + def int_or_str(arg: int | str) -> None: + never_call_me(arg) # type checker error + match arg: + case int(): + print("It's an int") + case str(): + print("It's a str") + case _: + never_call_me(arg) # ok, arg is of type Never + + """ + + raise TypeError(f"{self} is not subscriptable") + + +if hasattr(typing, 'Required'): # 3.11+ + Required = typing.Required + NotRequired = typing.NotRequired +else: # <=3.10 + @_ExtensionsSpecialForm + def Required(self, parameters): + """A special typing construct to mark a key of a total=False TypedDict + as required. For example: + + class Movie(TypedDict, total=False): + title: Required[str] + year: int + + m = Movie( + title='The Matrix', # typechecker error if key is omitted + year=1999, + ) + + There is no runtime checking that a required key is actually provided + when instantiating a related TypedDict. + """ + item = typing._type_check(parameters, f'{self._name} accepts only a single type.') + return typing._GenericAlias(self, (item,)) + + @_ExtensionsSpecialForm + def NotRequired(self, parameters): + """A special typing construct to mark a key of a TypedDict as + potentially missing. For example: + + class Movie(TypedDict): + title: str + year: NotRequired[int] + + m = Movie( + title='The Matrix', # typechecker error if key is omitted + year=1999, + ) + """ + item = typing._type_check(parameters, f'{self._name} accepts only a single type.') + return typing._GenericAlias(self, (item,)) + + +if hasattr(typing, 'ReadOnly'): + ReadOnly = typing.ReadOnly +else: # <=3.12 + @_ExtensionsSpecialForm + def ReadOnly(self, parameters): + """A special typing construct to mark an item of a TypedDict as read-only. + + For example: + + class Movie(TypedDict): + title: ReadOnly[str] + year: int + + def mutate_movie(m: Movie) -> None: + m["year"] = 1992 # allowed + m["title"] = "The Matrix" # typechecker error + + There is no runtime checking for this property. + """ + item = typing._type_check(parameters, f'{self._name} accepts only a single type.') + return typing._GenericAlias(self, (item,)) + + +_UNPACK_DOC = """\ +Type unpack operator. + +The type unpack operator takes the child types from some container type, +such as `tuple[int, str]` or a `TypeVarTuple`, and 'pulls them out'. For +example: + + # For some generic class `Foo`: + Foo[Unpack[tuple[int, str]]] # Equivalent to Foo[int, str] + + Ts = TypeVarTuple('Ts') + # Specifies that `Bar` is generic in an arbitrary number of types. + # (Think of `Ts` as a tuple of an arbitrary number of individual + # `TypeVar`s, which the `Unpack` is 'pulling out' directly into the + # `Generic[]`.) + class Bar(Generic[Unpack[Ts]]): ... + Bar[int] # Valid + Bar[int, str] # Also valid + +From Python 3.11, this can also be done using the `*` operator: + + Foo[*tuple[int, str]] + class Bar(Generic[*Ts]): ... + +The operator can also be used along with a `TypedDict` to annotate +`**kwargs` in a function signature. For instance: + + class Movie(TypedDict): + name: str + year: int + + # This function expects two keyword arguments - *name* of type `str` and + # *year* of type `int`. + def foo(**kwargs: Unpack[Movie]): ... + +Note that there is only some runtime checking of this operator. Not +everything the runtime allows may be accepted by static type checkers. + +For more information, see PEP 646 and PEP 692. +""" + + +# PEP 692 changed the repr of Unpack[] +# Breakpoint: https://github.com/python/cpython/pull/104048 +if sys.version_info >= (3, 12): + Unpack = typing.Unpack + + def _is_unpack(obj): + return get_origin(obj) is Unpack + +else: # <=3.11 + class _UnpackSpecialForm(_ExtensionsSpecialForm, _root=True): + def __init__(self, getitem): + super().__init__(getitem) + self.__doc__ = _UNPACK_DOC + + class _UnpackAlias(typing._GenericAlias, _root=True): + if sys.version_info < (3, 11): + # needed for compatibility with Generic[Unpack[Ts]] + __class__ = typing.TypeVar + + @property + def __typing_unpacked_tuple_args__(self): + assert self.__origin__ is Unpack + assert len(self.__args__) == 1 + arg, = self.__args__ + if isinstance(arg, (typing._GenericAlias, _types.GenericAlias)): + if arg.__origin__ is not tuple: + raise TypeError("Unpack[...] must be used with a tuple type") + return arg.__args__ + return None + + @property + def __typing_is_unpacked_typevartuple__(self): + assert self.__origin__ is Unpack + assert len(self.__args__) == 1 + return isinstance(self.__args__[0], TypeVarTuple) + + def __getitem__(self, args): + if self.__typing_is_unpacked_typevartuple__: + return args + # Cannot use `super().__getitem__` here because of the `__class__` assignment + # in the class body on Python <=3.11 + # (https://github.com/python/typing_extensions/issues/661) + return typing._GenericAlias.__getitem__(self, args) + + @_UnpackSpecialForm + def Unpack(self, parameters): + item = typing._type_check(parameters, f'{self._name} accepts only a single type.') + return _UnpackAlias(self, (item,)) + + def _is_unpack(obj): + return isinstance(obj, _UnpackAlias) + + +def _unpack_args(*args): + newargs = [] + for arg in args: + subargs = getattr(arg, '__typing_unpacked_tuple_args__', None) + if subargs is not None and (not (subargs and subargs[-1] is ...)): + newargs.extend(subargs) + else: + newargs.append(arg) + return newargs + + +if sys.version_info >= (3, 15): + from typing import TypeVarTuple + +elif hasattr(typing, "TypeVarTuple"): # 3.11+ + + # Add default parameter - PEP 696 and bound/variance parameters + class TypeVarTuple(metaclass=_TypeVarLikeMeta): + """Type variable tuple.""" + + _backported_typevarlike = typing.TypeVarTuple + + def __new__(cls, name, *, bound=None, + covariant=False, contravariant=False, + infer_variance=False, default=NoDefault): + + if _PEP_696_IMPLEMENTED: + # can pass default argument + tvt = typing.TypeVarTuple(name, default=default) + else: + tvt = typing.TypeVarTuple(name) + _set_default(tvt, default) + + tvt.__bound__ = bound + tvt.__covariant__ = bool(covariant) + tvt.__contravariant__ = bool(contravariant) + tvt.__infer_variance__ = bool(infer_variance) + + _set_module(tvt) + + def _typevartuple_prepare_subst(alias, args): + params = alias.__parameters__ + typevartuple_index = params.index(tvt) + for param in params[typevartuple_index + 1:]: + if isinstance(param, TypeVarTuple): + raise TypeError( + f"More than one TypeVarTuple parameter in {alias}" + ) + + alen = len(args) + plen = len(params) + left = typevartuple_index + right = plen - typevartuple_index - 1 + var_tuple_index = None + fillarg = None + for k, arg in enumerate(args): + if not isinstance(arg, type): + subargs = getattr(arg, '__typing_unpacked_tuple_args__', None) + if subargs and len(subargs) == 2 and subargs[-1] is ...: + if var_tuple_index is not None: + raise TypeError( + "More than one unpacked " + "arbitrary-length tuple argument" + ) + var_tuple_index = k + fillarg = subargs[0] + if var_tuple_index is not None: + left = min(left, var_tuple_index) + right = min(right, alen - var_tuple_index - 1) + elif left + right > alen: + raise TypeError(f"Too few arguments for {alias};" + f" actual {alen}, expected at least {plen - 1}") + if left == alen - right and tvt.has_default(): + replacement = _unpack_args(tvt.__default__) + else: + replacement = args[left: alen - right] + + return ( + *args[:left], + *([fillarg] * (typevartuple_index - left)), + replacement, + *([fillarg] * (plen - right - left - typevartuple_index - 1)), + *args[alen - right:], + ) + + tvt.__typing_prepare_subst__ = _typevartuple_prepare_subst + return tvt + + def __init_subclass__(self, *args, **kwds): + raise TypeError("Cannot subclass special typing classes") + +else: # <=3.10 + class TypeVarTuple(_DefaultMixin): + """Type variable tuple. + + Usage:: + + Ts = TypeVarTuple('Ts') + + In the same way that a normal type variable is a stand-in for a single + type such as ``int``, a type variable *tuple* is a stand-in for a *tuple* + type such as ``Tuple[int, str]``. + + Type variable tuples can be used in ``Generic`` declarations. + Consider the following example:: + + class Array(Generic[*Ts]): ... + + The ``Ts`` type variable tuple here behaves like ``tuple[T1, T2]``, + where ``T1`` and ``T2`` are type variables. To use these type variables + as type parameters of ``Array``, we must *unpack* the type variable tuple using + the star operator: ``*Ts``. The signature of ``Array`` then behaves + as if we had simply written ``class Array(Generic[T1, T2]): ...``. + In contrast to ``Generic[T1, T2]``, however, ``Generic[*Shape]`` allows + us to parameterise the class with an *arbitrary* number of type parameters. + + Type variable tuples can be used anywhere a normal ``TypeVar`` can. + This includes class definitions, as shown above, as well as function + signatures and variable annotations:: + + class Array(Generic[*Ts]): + + def __init__(self, shape: Tuple[*Ts]): + self._shape: Tuple[*Ts] = shape + + def get_shape(self) -> Tuple[*Ts]: + return self._shape + + shape = (Height(480), Width(640)) + x: Array[Height, Width] = Array(shape) + y = abs(x) # Inferred type is Array[Height, Width] + z = x + x # ... is Array[Height, Width] + x.get_shape() # ... is tuple[Height, Width] + + """ + + # Trick Generic __parameters__. + __class__ = typing.TypeVar + + def __iter__(self): + yield self.__unpacked__ + + def __init__(self, name, *, bound=None, covariant=False, contravariant=False, + infer_variance=False, default=NoDefault): + self.__name__ = name + self.__covariant__ = bool(covariant) + self.__contravariant__ = bool(contravariant) + self.__infer_variance__ = bool(infer_variance) + self.__bound__ = bound + _DefaultMixin.__init__(self, default) + + # for pickling: + def_mod = _caller() + if def_mod != 'typing_extensions': + self.__module__ = def_mod + + self.__unpacked__ = Unpack[self] + + def __repr__(self): + if self.__infer_variance__: + prefix = '' + elif self.__covariant__: + prefix = '+' + elif self.__contravariant__: + prefix = '-' + else: + prefix = '~' + return prefix + self.__name__ + + def __hash__(self): + return object.__hash__(self) + + def __eq__(self, other): + return self is other + + def __reduce__(self): + return self.__name__ + + def __init_subclass__(self, *args, **kwds): + if '_root' not in kwds: + raise TypeError("Cannot subclass special typing classes") + + +if hasattr(typing, "reveal_type"): # 3.11+ + reveal_type = typing.reveal_type +else: # <=3.10 + def reveal_type(obj: T, /) -> T: + """Reveal the inferred type of a variable. + + When a static type checker encounters a call to ``reveal_type()``, + it will emit the inferred type of the argument:: + + x: int = 1 + reveal_type(x) + + Running a static type checker (e.g., ``mypy``) on this example + will produce output similar to 'Revealed type is "builtins.int"'. + + At runtime, the function prints the runtime type of the + argument and returns it unchanged. + + """ + print(f"Runtime type is {type(obj).__name__!r}", file=sys.stderr) + return obj + + +if hasattr(typing, "_ASSERT_NEVER_REPR_MAX_LENGTH"): # 3.11+ + _ASSERT_NEVER_REPR_MAX_LENGTH = typing._ASSERT_NEVER_REPR_MAX_LENGTH +else: # <=3.10 + _ASSERT_NEVER_REPR_MAX_LENGTH = 100 + + +if hasattr(typing, "assert_never"): # 3.11+ + assert_never = typing.assert_never +else: # <=3.10 + def assert_never(arg: Never, /) -> Never: + """Assert to the type checker that a line of code is unreachable. + + Example:: + + def int_or_str(arg: int | str) -> None: + match arg: + case int(): + print("It's an int") + case str(): + print("It's a str") + case _: + assert_never(arg) + + If a type checker finds that a call to assert_never() is + reachable, it will emit an error. + + At runtime, this throws an exception when called. + + """ + value = repr(arg) + if len(value) > _ASSERT_NEVER_REPR_MAX_LENGTH: + value = value[:_ASSERT_NEVER_REPR_MAX_LENGTH] + '...' + raise AssertionError(f"Expected code to be unreachable, but got: {value}") + + +# dataclass_transform exists in 3.11 but lacks the frozen_default parameter +# Breakpoint: https://github.com/python/cpython/pull/99958 +if sys.version_info >= (3, 12): # 3.12+ + dataclass_transform = typing.dataclass_transform +else: # <=3.11 + def dataclass_transform( + *, + eq_default: bool = True, + order_default: bool = False, + kw_only_default: bool = False, + frozen_default: bool = False, + field_specifiers: typing.Tuple[ + typing.Union[typing.Type[typing.Any], typing.Callable[..., typing.Any]], + ... + ] = (), + **kwargs: typing.Any, + ) -> typing.Callable[[T], T]: + """Decorator that marks a function, class, or metaclass as providing + dataclass-like behavior. + + Example: + + from typing_extensions import dataclass_transform + + _T = TypeVar("_T") + + # Used on a decorator function + @dataclass_transform() + def create_model(cls: type[_T]) -> type[_T]: + ... + return cls + + @create_model + class CustomerModel: + id: int + name: str + + # Used on a base class + @dataclass_transform() + class ModelBase: ... + + class CustomerModel(ModelBase): + id: int + name: str + + # Used on a metaclass + @dataclass_transform() + class ModelMeta(type): ... + + class ModelBase(metaclass=ModelMeta): ... + + class CustomerModel(ModelBase): + id: int + name: str + + Each of the ``CustomerModel`` classes defined in this example will now + behave similarly to a dataclass created with the ``@dataclasses.dataclass`` + decorator. For example, the type checker will synthesize an ``__init__`` + method. + + The arguments to this decorator can be used to customize this behavior: + - ``eq_default`` indicates whether the ``eq`` parameter is assumed to be + True or False if it is omitted by the caller. + - ``order_default`` indicates whether the ``order`` parameter is + assumed to be True or False if it is omitted by the caller. + - ``kw_only_default`` indicates whether the ``kw_only`` parameter is + assumed to be True or False if it is omitted by the caller. + - ``frozen_default`` indicates whether the ``frozen`` parameter is + assumed to be True or False if it is omitted by the caller. + - ``field_specifiers`` specifies a static list of supported classes + or functions that describe fields, similar to ``dataclasses.field()``. + + At runtime, this decorator records its arguments in the + ``__dataclass_transform__`` attribute on the decorated object. + + See PEP 681 for details. + + """ + def decorator(cls_or_fn): + cls_or_fn.__dataclass_transform__ = { + "eq_default": eq_default, + "order_default": order_default, + "kw_only_default": kw_only_default, + "frozen_default": frozen_default, + "field_specifiers": field_specifiers, + "kwargs": kwargs, + } + return cls_or_fn + return decorator + + +if hasattr(typing, "override"): # 3.12+ + override = typing.override +else: # <=3.11 + _F = typing.TypeVar("_F", bound=typing.Callable[..., typing.Any]) + + def override(arg: _F, /) -> _F: + """Indicate that a method is intended to override a method in a base class. + + Usage: + + class Base: + def method(self) -> None: + pass + + class Child(Base): + @override + def method(self) -> None: + super().method() + + When this decorator is applied to a method, the type checker will + validate that it overrides a method with the same name on a base class. + This helps prevent bugs that may occur when a base class is changed + without an equivalent change to a child class. + + There is no runtime checking of these properties. The decorator + sets the ``__override__`` attribute to ``True`` on the decorated object + to allow runtime introspection. + + See PEP 698 for details. + + """ + try: + arg.__override__ = True + except (AttributeError, TypeError): + # Skip the attribute silently if it is not writable. + # AttributeError happens if the object has __slots__ or a + # read-only property, TypeError if it's a builtin class. + pass + return arg + + +# Python 3.13.8+ and 3.14.1+ contain a fix for the wrapped __init_subclass__ +# Breakpoint: https://github.com/python/cpython/pull/138210 +if ((3, 13, 8) <= sys.version_info < (3, 14)) or sys.version_info >= (3, 14, 1): + deprecated = warnings.deprecated +else: + _T = typing.TypeVar("_T") + + class deprecated: + """Indicate that a class, function or overload is deprecated. + + When this decorator is applied to an object, the type checker + will generate a diagnostic on usage of the deprecated object. + + Usage: + + @deprecated("Use B instead") + class A: + pass + + @deprecated("Use g instead") + def f(): + pass + + @overload + @deprecated("int support is deprecated") + def g(x: int) -> int: ... + @overload + def g(x: str) -> int: ... + + The warning specified by *category* will be emitted at runtime + on use of deprecated objects. For functions, that happens on calls; + for classes, on instantiation and on creation of subclasses. + If the *category* is ``None``, no warning is emitted at runtime. + The *stacklevel* determines where the + warning is emitted. If it is ``1`` (the default), the warning + is emitted at the direct caller of the deprecated object; if it + is higher, it is emitted further up the stack. + Static type checker behavior is not affected by the *category* + and *stacklevel* arguments. + + The deprecation message passed to the decorator is saved in the + ``__deprecated__`` attribute on the decorated object. + If applied to an overload, the decorator + must be after the ``@overload`` decorator for the attribute to + exist on the overload as returned by ``get_overloads()``. + + See PEP 702 for details. + + """ + def __init__( + self, + message: str, + /, + *, + category: typing.Optional[typing.Type[Warning]] = DeprecationWarning, + stacklevel: int = 1, + ) -> None: + if not isinstance(message, str): + raise TypeError( + "Expected an object of type str for 'message', not " + f"{type(message).__name__!r}" + ) + self.message = message + self.category = category + self.stacklevel = stacklevel + + def __call__(self, arg: _T, /) -> _T: + # Make sure the inner functions created below don't + # retain a reference to self. + msg = self.message + category = self.category + stacklevel = self.stacklevel + if category is None: + arg.__deprecated__ = msg + return arg + elif isinstance(arg, type): + import functools + from types import MethodType + + original_new = arg.__new__ + + @functools.wraps(original_new) + def __new__(cls, /, *args, **kwargs): + if cls is arg: + warnings.warn(msg, category=category, stacklevel=stacklevel + 1) + if original_new is not object.__new__: + return original_new(cls, *args, **kwargs) + # Mirrors a similar check in object.__new__. + elif cls.__init__ is object.__init__ and (args or kwargs): + raise TypeError(f"{cls.__name__}() takes no arguments") + else: + return original_new(cls) + + arg.__new__ = staticmethod(__new__) + + if "__init_subclass__" in arg.__dict__: + # __init_subclass__ is directly present on the decorated class. + # Synthesize a wrapper that calls this method directly. + original_init_subclass = arg.__init_subclass__ + # We need slightly different behavior if __init_subclass__ + # is a bound method (likely if it was implemented in Python). + # Otherwise, it likely means it's a builtin such as + # object's implementation of __init_subclass__. + if isinstance(original_init_subclass, MethodType): + original_init_subclass = original_init_subclass.__func__ + + @functools.wraps(original_init_subclass) + def __init_subclass__(*args, **kwargs): + warnings.warn(msg, category=category, stacklevel=stacklevel + 1) + return original_init_subclass(*args, **kwargs) + else: + def __init_subclass__(cls, *args, **kwargs): + warnings.warn(msg, category=category, stacklevel=stacklevel + 1) + return super(arg, cls).__init_subclass__(*args, **kwargs) + + arg.__init_subclass__ = classmethod(__init_subclass__) + + arg.__deprecated__ = __new__.__deprecated__ = msg + __init_subclass__.__deprecated__ = msg + return arg + elif callable(arg): + import functools + import inspect + + @functools.wraps(arg) + def wrapper(*args, **kwargs): + warnings.warn(msg, category=category, stacklevel=stacklevel + 1) + return arg(*args, **kwargs) + + if inspect.iscoroutinefunction(arg): + # Breakpoint: https://github.com/python/cpython/pull/99247 + if sys.version_info >= (3, 12): + wrapper = inspect.markcoroutinefunction(wrapper) + else: + import asyncio.coroutines + + wrapper._is_coroutine = asyncio.coroutines._is_coroutine + + arg.__deprecated__ = wrapper.__deprecated__ = msg + return wrapper + else: + raise TypeError( + "@deprecated decorator with non-None category must be applied to " + f"a class or callable, not {arg!r}" + ) + +# Breakpoint: https://github.com/python/cpython/pull/23702 +if sys.version_info < (3, 10): + def _is_param_expr(arg): + return arg is ... or isinstance( + arg, (tuple, list, ParamSpec, _ConcatenateGenericAlias) + ) +else: + def _is_param_expr(arg): + return arg is ... or isinstance( + arg, + ( + tuple, + list, + ParamSpec, + _ConcatenateGenericAlias, + typing._ConcatenateGenericAlias, + ), + ) + + +# We have to do some monkey patching to deal with the dual nature of +# Unpack/TypeVarTuple: +# - We want Unpack to be a kind of TypeVar so it gets accepted in +# Generic[Unpack[Ts]] +# - We want it to *not* be treated as a TypeVar for the purposes of +# counting generic parameters, so that when we subscript a generic, +# the runtime doesn't try to substitute the Unpack with the subscripted type. +if not hasattr(typing, "TypeVarTuple"): + def _check_generic(cls, parameters, elen=_marker): + """Check correct count for parameters of a generic cls (internal helper). + + This gives a nice error message in case of count mismatch. + """ + # If substituting a single ParamSpec with multiple arguments + # we do not check the count + if (inspect.isclass(cls) and issubclass(cls, typing.Generic) + and len(cls.__parameters__) == 1 + and isinstance(cls.__parameters__[0], ParamSpec) + and parameters + and not _is_param_expr(parameters[0]) + ): + # Generic modifies parameters variable, but here we cannot do this + return + + if not elen: + raise TypeError(f"{cls} is not a generic class") + if elen is _marker: + if not hasattr(cls, "__parameters__") or not cls.__parameters__: + raise TypeError(f"{cls} is not a generic class") + elen = len(cls.__parameters__) + alen = len(parameters) + if alen != elen: + expect_val = elen + if hasattr(cls, "__parameters__"): + parameters = [p for p in cls.__parameters__ if not _is_unpack(p)] + num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters) + if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples): + return + + # deal with TypeVarLike defaults + # required TypeVarLikes cannot appear after a defaulted one. + if alen < elen: + # since we validate TypeVarLike default in _collect_type_vars + # or _collect_parameters we can safely check parameters[alen] + if ( + getattr(parameters[alen], '__default__', NoDefault) + is not NoDefault + ): + return + + num_default_tv = sum(getattr(p, '__default__', NoDefault) + is not NoDefault for p in parameters) + + elen -= num_default_tv + + expect_val = f"at least {elen}" + + # Breakpoint: https://github.com/python/cpython/pull/27515 + things = "arguments" if sys.version_info >= (3, 10) else "parameters" + raise TypeError(f"Too {'many' if alen > elen else 'few'} {things}" + f" for {cls}; actual {alen}, expected {expect_val}") +else: + # Python 3.11+ + + def _check_generic(cls, parameters, elen): + """Check correct count for parameters of a generic cls (internal helper). + + This gives a nice error message in case of count mismatch. + """ + if not elen: + raise TypeError(f"{cls} is not a generic class") + alen = len(parameters) + if alen != elen: + expect_val = elen + if hasattr(cls, "__parameters__"): + parameters = [p for p in cls.__parameters__ if not _is_unpack(p)] + + # deal with TypeVarLike defaults + # required TypeVarLikes cannot appear after a defaulted one. + if alen < elen: + # since we validate TypeVarLike default in _collect_type_vars + # or _collect_parameters we can safely check parameters[alen] + if ( + getattr(parameters[alen], '__default__', NoDefault) + is not NoDefault + ): + return + + num_default_tv = sum(getattr(p, '__default__', NoDefault) + is not NoDefault for p in parameters) + + elen -= num_default_tv + + expect_val = f"at least {elen}" + + raise TypeError(f"Too {'many' if alen > elen else 'few'} arguments" + f" for {cls}; actual {alen}, expected {expect_val}") + +if not _PEP_696_IMPLEMENTED: + typing._check_generic = _check_generic + + +def _has_generic_or_protocol_as_origin() -> bool: + try: + frame = sys._getframe(2) + # - Catch AttributeError: not all Python implementations have sys._getframe() + # - Catch ValueError: maybe we're called from an unexpected module + # and the call stack isn't deep enough + except (AttributeError, ValueError): + return False # err on the side of leniency + else: + # If we somehow get invoked from outside typing.py, + # also err on the side of leniency + if frame.f_globals.get("__name__") != "typing": + return False + origin = frame.f_locals.get("origin") + # Cannot use "in" because origin may be an object with a buggy __eq__ that + # throws an error. + return origin is typing.Generic or origin is Protocol or origin is typing.Protocol + + +_TYPEVARTUPLE_TYPES = {TypeVarTuple, getattr(typing, "TypeVarTuple", None)} + + +def _is_unpacked_typevartuple(x) -> bool: + if get_origin(x) is not Unpack: + return False + args = get_args(x) + return ( + bool(args) + and len(args) == 1 + and type(args[0]) in _TYPEVARTUPLE_TYPES + ) + + +# Python 3.11+ _collect_type_vars was renamed to _collect_parameters +if hasattr(typing, '_collect_type_vars'): + def _collect_type_vars(types, typevar_types=None): + """Collect all type variable contained in types in order of + first appearance (lexicographic order). For example:: + + _collect_type_vars((T, List[S, T])) == (T, S) + """ + if typevar_types is None: + typevar_types = typing.TypeVar + tvars = [] + + # A required TypeVarLike cannot appear after a TypeVarLike with a default + # if it was a direct call to `Generic[]` or `Protocol[]` + enforce_default_ordering = _has_generic_or_protocol_as_origin() + default_encountered = False + + # Also, a TypeVarLike with a default cannot appear after a TypeVarTuple + type_var_tuple_encountered = False + + for t in types: + if _is_unpacked_typevartuple(t): + type_var_tuple_encountered = True + elif ( + isinstance(t, typevar_types) and not isinstance(t, _UnpackAlias) + and t not in tvars + ): + if enforce_default_ordering: + has_default = getattr(t, '__default__', NoDefault) is not NoDefault + if has_default: + if type_var_tuple_encountered: + raise TypeError('Type parameter with a default' + ' follows TypeVarTuple') + default_encountered = True + elif default_encountered: + raise TypeError(f'Type parameter {t!r} without a default' + ' follows type parameter with a default') + + tvars.append(t) + if _should_collect_from_parameters(t): + tvars.extend([t for t in t.__parameters__ if t not in tvars]) + elif isinstance(t, tuple): + # Collect nested type_vars + # tuple wrapped by _prepare_paramspec_params(cls, params) + for x in t: + for collected in _collect_type_vars([x]): + if collected not in tvars: + tvars.append(collected) + return tuple(tvars) + + typing._collect_type_vars = _collect_type_vars +else: + def _collect_parameters(args): + """Collect all type variables and parameter specifications in args + in order of first appearance (lexicographic order). + + For example:: + + assert _collect_parameters((T, Callable[P, T])) == (T, P) + """ + parameters = [] + + # A required TypeVarLike cannot appear after a TypeVarLike with default + # if it was a direct call to `Generic[]` or `Protocol[]` + enforce_default_ordering = _has_generic_or_protocol_as_origin() + default_encountered = False + + # Also, a TypeVarLike with a default cannot appear after a TypeVarTuple + type_var_tuple_encountered = False + + for t in args: + if isinstance(t, type): + # We don't want __parameters__ descriptor of a bare Python class. + pass + elif isinstance(t, tuple): + # `t` might be a tuple, when `ParamSpec` is substituted with + # `[T, int]`, or `[int, *Ts]`, etc. + for x in t: + for collected in _collect_parameters([x]): + if collected not in parameters: + parameters.append(collected) + elif hasattr(t, '__typing_subst__'): + if t not in parameters: + if enforce_default_ordering: + has_default = ( + getattr(t, '__default__', NoDefault) is not NoDefault + ) + + if type_var_tuple_encountered and has_default: + raise TypeError('Type parameter with a default' + ' follows TypeVarTuple') + + if has_default: + default_encountered = True + elif default_encountered: + raise TypeError(f'Type parameter {t!r} without a default' + ' follows type parameter with a default') + + parameters.append(t) + else: + if _is_unpacked_typevartuple(t): + type_var_tuple_encountered = True + for x in getattr(t, '__parameters__', ()): + if x not in parameters: + parameters.append(x) + + return tuple(parameters) + + if not _PEP_696_IMPLEMENTED: + typing._collect_parameters = _collect_parameters + +# Backport typing.NamedTuple as it exists in Python 3.13. +# In 3.11, the ability to define generic `NamedTuple`s was supported. +# This was explicitly disallowed in 3.9-3.10, and only half-worked in <=3.8. +# On 3.12, we added __orig_bases__ to call-based NamedTuples +# On 3.13, we deprecated kwargs-based NamedTuples +# Breakpoint: https://github.com/python/cpython/pull/105609 +if sys.version_info >= (3, 13): + NamedTuple = typing.NamedTuple +else: + def _make_nmtuple(name, types, module, defaults=()): + fields = [n for n, t in types] + annotations = {n: typing._type_check(t, f"field {n} annotation must be a type") + for n, t in types} + nm_tpl = collections.namedtuple(name, fields, + defaults=defaults, module=module) + nm_tpl.__annotations__ = nm_tpl.__new__.__annotations__ = annotations + return nm_tpl + + _prohibited_namedtuple_fields = typing._prohibited + _special_namedtuple_fields = frozenset({'__module__', '__name__', '__annotations__'}) + + class _NamedTupleMeta(type): + def __new__(cls, typename, bases, ns): + assert _NamedTuple in bases + for base in bases: + if base is not _NamedTuple and base is not typing.Generic: + raise TypeError( + 'can only inherit from a NamedTuple type and Generic') + bases = tuple(tuple if base is _NamedTuple else base for base in bases) + if "__annotations__" in ns: + types = ns["__annotations__"] + elif "__annotate__" in ns: + # TODO: Use inspect.VALUE here, and make the annotations lazily evaluated + types = ns["__annotate__"](1) + else: + types = {} + default_names = [] + for field_name in types: + if field_name in ns: + default_names.append(field_name) + elif default_names: + raise TypeError(f"Non-default namedtuple field {field_name} " + f"cannot follow default field" + f"{'s' if len(default_names) > 1 else ''} " + f"{', '.join(default_names)}") + nm_tpl = _make_nmtuple( + typename, types.items(), + defaults=[ns[n] for n in default_names], + module=ns['__module__'] + ) + nm_tpl.__bases__ = bases + if typing.Generic in bases: + if hasattr(typing, '_generic_class_getitem'): # 3.12+ + nm_tpl.__class_getitem__ = classmethod(typing._generic_class_getitem) + else: + class_getitem = typing.Generic.__class_getitem__.__func__ + nm_tpl.__class_getitem__ = classmethod(class_getitem) + # update from user namespace without overriding special namedtuple attributes + for key, val in ns.items(): + if key in _prohibited_namedtuple_fields: + raise AttributeError("Cannot overwrite NamedTuple attribute " + key) + elif key not in _special_namedtuple_fields: + if key not in nm_tpl._fields: + setattr(nm_tpl, key, ns[key]) + try: + set_name = type(val).__set_name__ + except AttributeError: + pass + else: + try: + set_name(val, nm_tpl, key) + except BaseException as e: + msg = ( + f"Error calling __set_name__ on {type(val).__name__!r} " + f"instance {key!r} in {typename!r}" + ) + # BaseException.add_note() existed on py311, + # but the __set_name__ machinery didn't start + # using add_note() until py312. + # Making sure exceptions are raised in the same way + # as in "normal" classes seems most important here. + # Breakpoint: https://github.com/python/cpython/pull/95915 + if sys.version_info >= (3, 12): + e.add_note(msg) + raise + else: + raise RuntimeError(msg) from e + + if typing.Generic in bases: + nm_tpl.__init_subclass__() + return nm_tpl + + _NamedTuple = type.__new__(_NamedTupleMeta, 'NamedTuple', (), {}) + + def _namedtuple_mro_entries(bases): + assert NamedTuple in bases + return (_NamedTuple,) + + def NamedTuple(typename, fields=_marker, /, **kwargs): + """Typed version of namedtuple. + + Usage:: + + class Employee(NamedTuple): + name: str + id: int + + This is equivalent to:: + + Employee = collections.namedtuple('Employee', ['name', 'id']) + + The resulting class has an extra __annotations__ attribute, giving a + dict that maps field names to types. (The field names are also in + the _fields attribute, which is part of the namedtuple API.) + An alternative equivalent functional syntax is also accepted:: + + Employee = NamedTuple('Employee', [('name', str), ('id', int)]) + """ + if fields is _marker: + if kwargs: + deprecated_thing = "Creating NamedTuple classes using keyword arguments" + deprecation_msg = ( + "{name} is deprecated and will be disallowed in Python {remove}. " + "Use the class-based or functional syntax instead." + ) + else: + deprecated_thing = "Failing to pass a value for the 'fields' parameter" + example = f"`{typename} = NamedTuple({typename!r}, [])`" + deprecation_msg = ( + "{name} is deprecated and will be disallowed in Python {remove}. " + "To create a NamedTuple class with 0 fields " + "using the functional syntax, " + "pass an empty list, e.g. " + ) + example + "." + elif fields is None: + if kwargs: + raise TypeError( + "Cannot pass `None` as the 'fields' parameter " + "and also specify fields using keyword arguments" + ) + else: + deprecated_thing = "Passing `None` as the 'fields' parameter" + example = f"`{typename} = NamedTuple({typename!r}, [])`" + deprecation_msg = ( + "{name} is deprecated and will be disallowed in Python {remove}. " + "To create a NamedTuple class with 0 fields " + "using the functional syntax, " + "pass an empty list, e.g. " + ) + example + "." + elif kwargs: + raise TypeError("Either list of fields or keywords" + " can be provided to NamedTuple, not both") + if fields is _marker or fields is None: + warnings.warn( + deprecation_msg.format(name=deprecated_thing, remove="3.15"), + DeprecationWarning, + stacklevel=2, + ) + fields = kwargs.items() + nt = _make_nmtuple(typename, fields, module=_caller()) + nt.__orig_bases__ = (NamedTuple,) + return nt + + NamedTuple.__mro_entries__ = _namedtuple_mro_entries + + +if hasattr(collections.abc, "Buffer"): + Buffer = collections.abc.Buffer +else: + class Buffer(abc.ABC): # noqa: B024 + """Base class for classes that implement the buffer protocol. + + The buffer protocol allows Python objects to expose a low-level + memory buffer interface. Before Python 3.12, it is not possible + to implement the buffer protocol in pure Python code, or even + to check whether a class implements the buffer protocol. In + Python 3.12 and higher, the ``__buffer__`` method allows access + to the buffer protocol from Python code, and the + ``collections.abc.Buffer`` ABC allows checking whether a class + implements the buffer protocol. + + To indicate support for the buffer protocol in earlier versions, + inherit from this ABC, either in a stub file or at runtime, + or use ABC registration. This ABC provides no methods, because + there is no Python-accessible methods shared by pre-3.12 buffer + classes. It is useful primarily for static checks. + + """ + + # As a courtesy, register the most common stdlib buffer classes. + Buffer.register(memoryview) + Buffer.register(bytearray) + Buffer.register(bytes) + + +# Backport of types.get_original_bases, available on 3.12+ in CPython +if hasattr(_types, "get_original_bases"): + get_original_bases = _types.get_original_bases +else: + def get_original_bases(cls, /): + """Return the class's "original" bases prior to modification by `__mro_entries__`. + + Examples:: + + from typing import TypeVar, Generic + from typing_extensions import NamedTuple, TypedDict + + T = TypeVar("T") + class Foo(Generic[T]): ... + class Bar(Foo[int], float): ... + class Baz(list[str]): ... + Eggs = NamedTuple("Eggs", [("a", int), ("b", str)]) + Spam = TypedDict("Spam", {"a": int, "b": str}) + + assert get_original_bases(Bar) == (Foo[int], float) + assert get_original_bases(Baz) == (list[str],) + assert get_original_bases(Eggs) == (NamedTuple,) + assert get_original_bases(Spam) == (TypedDict,) + assert get_original_bases(int) == (object,) + """ + try: + return cls.__dict__.get("__orig_bases__", cls.__bases__) + except AttributeError: + raise TypeError( + f'Expected an instance of type, not {type(cls).__name__!r}' + ) from None + + +# NewType is a class on Python 3.10+, making it pickleable +# The error message for subclassing instances of NewType was improved on 3.11+ +# Breakpoint: https://github.com/python/cpython/pull/30268 +if sys.version_info >= (3, 11): + NewType = typing.NewType +else: + class NewType: + """NewType creates simple unique types with almost zero + runtime overhead. NewType(name, tp) is considered a subtype of tp + by static type checkers. At runtime, NewType(name, tp) returns + a dummy callable that simply returns its argument. Usage:: + UserId = NewType('UserId', int) + def name_by_id(user_id: UserId) -> str: + ... + UserId('user') # Fails type check + name_by_id(42) # Fails type check + name_by_id(UserId(42)) # OK + num = UserId(5) + 1 # type: int + """ + + def __call__(self, obj, /): + return obj + + def __init__(self, name, tp): + self.__qualname__ = name + if '.' in name: + name = name.rpartition('.')[-1] + self.__name__ = name + self.__supertype__ = tp + def_mod = _caller() + if def_mod != 'typing_extensions': + self.__module__ = def_mod + + def __mro_entries__(self, bases): + # We defined __mro_entries__ to get a better error message + # if a user attempts to subclass a NewType instance. bpo-46170 + supercls_name = self.__name__ + + class Dummy: + def __init_subclass__(cls): + subcls_name = cls.__name__ + raise TypeError( + f"Cannot subclass an instance of NewType. " + f"Perhaps you were looking for: " + f"`{subcls_name} = NewType({subcls_name!r}, {supercls_name})`" + ) + + return (Dummy,) + + def __repr__(self): + return f'{self.__module__}.{self.__qualname__}' + + def __reduce__(self): + return self.__qualname__ + + # Breakpoint: https://github.com/python/cpython/pull/21515 + if sys.version_info >= (3, 10): + # PEP 604 methods + # It doesn't make sense to have these methods on Python <3.10 + + def __or__(self, other): + return typing.Union[self, other] + + def __ror__(self, other): + return typing.Union[other, self] + + +# Breakpoint: https://github.com/python/cpython/pull/149172 +if sys.version_info >= (3, 15): + TypeAliasType = typing.TypeAliasType +# <=3.14 +else: + # Breakpoint: https://github.com/python/cpython/pull/103764 + if sys.version_info >= (3, 12): + # 3.12-3.14 + def _is_unionable(obj): + """Corresponds to is_unionable() in unionobject.c in CPython.""" + return obj is None or isinstance(obj, ( + type, + _types.GenericAlias, + _types.UnionType, + typing.TypeAliasType, + TypeAliasType, + )) + else: + # <=3.11 + def _is_unionable(obj): + """Corresponds to is_unionable() in unionobject.c in CPython.""" + return obj is None or isinstance(obj, ( + type, + _types.GenericAlias, + _types.UnionType, + TypeAliasType, + )) + + if sys.version_info < (3, 10): + # Copied and pasted from https://github.com/python/cpython/blob/986a4e1b6fcae7fe7a1d0a26aea446107dd58dd2/Objects/genericaliasobject.c#L568-L582, + # so that we emulate the behaviour of `types.GenericAlias` + # on the latest versions of CPython + _ATTRIBUTE_DELEGATION_EXCLUSIONS = frozenset({ + "__class__", + "__bases__", + "__origin__", + "__args__", + "__unpacked__", + "__parameters__", + "__typing_unpacked_tuple_args__", + "__mro_entries__", + "__reduce_ex__", + "__reduce__", + "__copy__", + "__deepcopy__", + }) + + class _TypeAliasGenericAlias(typing._GenericAlias, _root=True): + def __getattr__(self, attr): + if attr in _ATTRIBUTE_DELEGATION_EXCLUSIONS: + return object.__getattr__(self, attr) + return getattr(self.__origin__, attr) + + + class TypeAliasType: + """Create named, parameterized type aliases. + + This provides a backport of the new `type` statement in Python 3.12: + + type ListOrSet[T] = list[T] | set[T] + + is equivalent to: + + T = TypeVar("T") + ListOrSet = TypeAliasType("ListOrSet", list[T] | set[T], type_params=(T,)) + + The name ListOrSet can then be used as an alias for the type it refers to. + + The type_params argument should contain all the type parameters used + in the value of the type alias. If the alias is not generic, this + argument is omitted. + + Static type checkers should only support type aliases declared using + TypeAliasType that follow these rules: + + - The first argument (the name) must be a string literal. + - The TypeAliasType instance must be immediately assigned to a variable + of the same name. (For example, 'X = TypeAliasType("Y", int)' is invalid, + as is 'X, Y = TypeAliasType("X", int), TypeAliasType("Y", int)'). + + """ + + def __init__(self, name: str, value, *, type_params=()): + if not isinstance(name, str): + raise TypeError("TypeAliasType name must be a string") + if not isinstance(type_params, tuple): + raise TypeError("type_params must be a tuple") + self.__value__ = value + self.__type_params__ = type_params + + default_value_encountered = False + parameters = [] + for type_param in type_params: + if ( + not isinstance(type_param, (TypeVar, TypeVarTuple, ParamSpec)) + # <=3.11 + # Unpack Backport passes isinstance(type_param, TypeVar) + or _is_unpack(type_param) + ): + raise TypeError(f"Expected a type param, got {type_param!r}") + has_default = ( + getattr(type_param, '__default__', NoDefault) is not NoDefault + ) + if default_value_encountered and not has_default: + raise TypeError(f"non-default type parameter '{type_param!r}'" + " follows default type parameter") + if has_default: + default_value_encountered = True + if isinstance(type_param, TypeVarTuple): + parameters.extend(type_param) + else: + parameters.append(type_param) + self.__parameters__ = tuple(parameters) + def_mod = _caller() + if def_mod != 'typing_extensions': + self.__module__ = def_mod + # Setting this attribute closes the TypeAliasType from further modification + self.__name__ = name + + def __setattr__(self, name: str, value: object, /) -> None: + if hasattr(self, "__name__") and name != "__module__": + self._raise_attribute_error(name) + super().__setattr__(name, value) + + def __delattr__(self, name: str, /) -> Never: + self._raise_attribute_error(name) + + def _raise_attribute_error(self, name: str) -> Never: + # Match the Python 3.12 error messages exactly + if name == "__name__": + raise AttributeError("readonly attribute") + elif name in {"__value__", "__type_params__", "__parameters__"}: + raise AttributeError( + f"attribute '{name}' of 'typing.TypeAliasType' objects " + "is not writable" + ) + else: + raise AttributeError( + f"'typing.TypeAliasType' object has no attribute '{name}'" + ) + + def __repr__(self) -> str: + return self.__name__ + + if sys.version_info < (3, 11): + def _check_single_param(self, param, recursion=0): + # Allow [], [int], [int, str], [int, ...], [int, T] + if param is ...: + return ... + if param is None: + return None + # Note in <= 3.9 _ConcatenateGenericAlias inherits from list + if isinstance(param, list) and recursion == 0: + return [self._check_single_param(arg, recursion+1) + for arg in param] + return typing._type_check( + param, f'Subscripting {self.__name__} requires a type.' + ) + + def _check_parameters(self, parameters): + if sys.version_info < (3, 11): + return tuple( + self._check_single_param(item) + for item in parameters + ) + return tuple(typing._type_check( + item, f'Subscripting {self.__name__} requires a type.' + ) + for item in parameters + ) + + def __getitem__(self, parameters): + if not self.__type_params__: + raise TypeError("Only generic type aliases are subscriptable") + if not isinstance(parameters, tuple): + parameters = (parameters,) + # Using 3.9 here will create problems with Concatenate + if sys.version_info >= (3, 10): + return _types.GenericAlias(self, parameters) + type_vars = _collect_type_vars(parameters) + parameters = self._check_parameters(parameters) + alias = _TypeAliasGenericAlias(self, parameters) + # alias.__parameters__ is not complete if Concatenate is present + # as it is converted to a list from which no parameters are extracted. + if alias.__parameters__ != type_vars: + alias.__parameters__ = type_vars + return alias + + def __reduce__(self): + return self.__name__ + + def __init_subclass__(cls, *args, **kwargs): + raise TypeError( + "type 'typing_extensions.TypeAliasType' is not an acceptable base type" + ) + + # The presence of this method convinces typing._type_check + # that TypeAliasTypes are types. + def __call__(self): + raise TypeError("Type alias is not callable") + + # Breakpoint: https://github.com/python/cpython/pull/21515 + if sys.version_info >= (3, 10): + def __or__(self, right): + # For forward compatibility with 3.12, reject Unions + # that are not accepted by the built-in Union. + if not _is_unionable(right): + return NotImplemented + return typing.Union[self, right] + + def __ror__(self, left): + if not _is_unionable(left): + return NotImplemented + return typing.Union[left, self] + + +if hasattr(typing, "is_protocol"): + is_protocol = typing.is_protocol + get_protocol_members = typing.get_protocol_members +else: + def is_protocol(tp: type, /) -> bool: + """Return True if the given type is a Protocol. + + Example:: + + >>> from typing_extensions import Protocol, is_protocol + >>> class P(Protocol): + ... def a(self) -> str: ... + ... b: int + >>> is_protocol(P) + True + >>> is_protocol(int) + False + """ + return ( + isinstance(tp, type) + and getattr(tp, '_is_protocol', False) + and tp is not Protocol + and tp is not typing.Protocol + ) + + def get_protocol_members(tp: type, /) -> typing.FrozenSet[str]: + """Return the set of members defined in a Protocol. + + Example:: + + >>> from typing_extensions import Protocol, get_protocol_members + >>> class P(Protocol): + ... def a(self) -> str: ... + ... b: int + >>> get_protocol_members(P) == frozenset({'a', 'b'}) + True + + Raise a TypeError for arguments that are not Protocols. + """ + if not is_protocol(tp): + raise TypeError(f'{tp!r} is not a Protocol') + if hasattr(tp, '__protocol_attrs__'): + return frozenset(tp.__protocol_attrs__) + return frozenset(_get_protocol_attrs(tp)) + + +if hasattr(typing, "Doc"): + Doc = typing.Doc +else: + class Doc: + """Define the documentation of a type annotation using ``Annotated``, to be + used in class attributes, function and method parameters, return values, + and variables. + + The value should be a positional-only string literal to allow static tools + like editors and documentation generators to use it. + + This complements docstrings. + + The string value passed is available in the attribute ``documentation``. + + Example:: + + >>> from typing_extensions import Annotated, Doc + >>> def hi(to: Annotated[str, Doc("Who to say hi to")]) -> None: ... + """ + def __init__(self, documentation: str, /) -> None: + self.documentation = documentation + + def __repr__(self) -> str: + return f"Doc({self.documentation!r})" + + def __hash__(self) -> int: + return hash(self.documentation) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Doc): + return NotImplemented + return self.documentation == other.documentation + + +_CapsuleType = getattr(_types, "CapsuleType", None) + +if _CapsuleType is None: + try: + import _socket + except ImportError: + pass + else: + _CAPI = getattr(_socket, "CAPI", None) + if _CAPI is not None: + _CapsuleType = type(_CAPI) + +if _CapsuleType is not None: + CapsuleType = _CapsuleType + __all__.append("CapsuleType") + + +if sys.version_info >= (3, 14): + from annotationlib import Format, get_annotations +else: + # Available since Python 3.14.0a3 + # PR: https://github.com/python/cpython/pull/124415 + class Format(enum.IntEnum): + VALUE = 1 + VALUE_WITH_FAKE_GLOBALS = 2 + FORWARDREF = 3 + STRING = 4 + + # Available since Python 3.14.0a1 + # PR: https://github.com/python/cpython/pull/119891 + def get_annotations(obj, *, globals=None, locals=None, eval_str=False, + format=Format.VALUE): + """Compute the annotations dict for an object. + + obj may be a callable, class, or module. + Passing in an object of any other type raises TypeError. + + Returns a dict. get_annotations() returns a new dict every time + it's called; calling it twice on the same object will return two + different but equivalent dicts. + + This is a backport of `inspect.get_annotations`, which has been + in the standard library since Python 3.10. See the standard library + documentation for more: + + https://docs.python.org/3/library/inspect.html#inspect.get_annotations + + This backport adds the *format* argument introduced by PEP 649. The + three formats supported are: + * VALUE: the annotations are returned as-is. This is the default and + it is compatible with the behavior on previous Python versions. + * FORWARDREF: return annotations as-is if possible, but replace any + undefined names with ForwardRef objects. The implementation proposed by + PEP 649 relies on language changes that cannot be backported; the + typing-extensions implementation simply returns the same result as VALUE. + * STRING: return annotations as strings, in a format close to the original + source. Again, this behavior cannot be replicated directly in a backport. + As an approximation, typing-extensions retrieves the annotations under + VALUE semantics and then stringifies them. + + The purpose of this backport is to allow users who would like to use + FORWARDREF or STRING semantics once PEP 649 is implemented, but who also + want to support earlier Python versions, to simply write: + + typing_extensions.get_annotations(obj, format=Format.FORWARDREF) + + """ + format = Format(format) + if format is Format.VALUE_WITH_FAKE_GLOBALS: + raise ValueError( + "The VALUE_WITH_FAKE_GLOBALS format is for internal use only" + ) + + if eval_str and format is not Format.VALUE: + raise ValueError("eval_str=True is only supported with format=Format.VALUE") + + if isinstance(obj, type): + # class + obj_dict = getattr(obj, '__dict__', None) + if obj_dict and hasattr(obj_dict, 'get'): + ann = obj_dict.get('__annotations__', None) + if isinstance(ann, _types.GetSetDescriptorType): + ann = None + else: + ann = None + + obj_globals = None + module_name = getattr(obj, '__module__', None) + if module_name: + module = sys.modules.get(module_name, None) + if module: + obj_globals = getattr(module, '__dict__', None) + obj_locals = dict(vars(obj)) + unwrap = obj + elif isinstance(obj, _types.ModuleType): + # module + ann = getattr(obj, '__annotations__', None) + obj_globals = obj.__dict__ + obj_locals = None + unwrap = None + elif callable(obj): + # this includes types.Function, types.BuiltinFunctionType, + # types.BuiltinMethodType, functools.partial, functools.singledispatch, + # "class funclike" from Lib/test/test_inspect... on and on it goes. + ann = getattr(obj, '__annotations__', None) + obj_globals = getattr(obj, '__globals__', None) + obj_locals = None + unwrap = obj + elif hasattr(obj, '__annotations__'): + ann = obj.__annotations__ + obj_globals = obj_locals = unwrap = None + else: + raise TypeError(f"{obj!r} is not a module, class, or callable.") + + if ann is None: + return {} + + if not isinstance(ann, dict): + raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None") + + if not ann: + return {} + + if not eval_str: + if format is Format.STRING: + return { + key: value if isinstance(value, str) else typing._type_repr(value) + for key, value in ann.items() + } + return dict(ann) + + if unwrap is not None: + while True: + if hasattr(unwrap, '__wrapped__'): + unwrap = unwrap.__wrapped__ + continue + if isinstance(unwrap, functools.partial): + unwrap = unwrap.func + continue + break + if hasattr(unwrap, "__globals__"): + obj_globals = unwrap.__globals__ + + if globals is None: + globals = obj_globals + if locals is None: + locals = obj_locals or {} + + # "Inject" type parameters into the local namespace + # (unless they are shadowed by assignments *in* the local namespace), + # as a way of emulating annotation scopes when calling `eval()` + if type_params := getattr(obj, "__type_params__", ()): + locals = {param.__name__: param for param in type_params} | locals + + return_value = {key: + value if not isinstance(value, str) else eval(value, globals, locals) + for key, value in ann.items() } + return return_value + + +if hasattr(typing, "evaluate_forward_ref"): + evaluate_forward_ref = typing.evaluate_forward_ref +else: + # Implements annotationlib.ForwardRef.evaluate + def _eval_with_owner( + forward_ref, *, owner=None, globals=None, locals=None, type_params=None + ): + if forward_ref.__forward_evaluated__: + return forward_ref.__forward_value__ + if getattr(forward_ref, "__cell__", None) is not None: + try: + value = forward_ref.__cell__.cell_contents + except ValueError: + pass + else: + forward_ref.__forward_evaluated__ = True + forward_ref.__forward_value__ = value + return value + if owner is None: + owner = getattr(forward_ref, "__owner__", None) + + if ( + globals is None + and getattr(forward_ref, "__forward_module__", None) is not None + ): + globals = getattr( + sys.modules.get(forward_ref.__forward_module__, None), "__dict__", None + ) + if globals is None: + globals = getattr(forward_ref, "__globals__", None) + if globals is None: + if isinstance(owner, type): + module_name = getattr(owner, "__module__", None) + if module_name: + module = sys.modules.get(module_name, None) + if module: + globals = getattr(module, "__dict__", None) + elif isinstance(owner, _types.ModuleType): + globals = getattr(owner, "__dict__", None) + elif callable(owner): + globals = getattr(owner, "__globals__", None) + + # If we pass None to eval() below, the globals of this module are used. + if globals is None: + globals = {} + + if locals is None: + locals = {} + if isinstance(owner, type): + locals.update(vars(owner)) + + if type_params is None and owner is not None: + # "Inject" type parameters into the local namespace + # (unless they are shadowed by assignments *in* the local namespace), + # as a way of emulating annotation scopes when calling `eval()` + type_params = getattr(owner, "__type_params__", None) + + # Type parameters exist in their own scope, which is logically + # between the locals and the globals. We simulate this by adding + # them to the globals. + if type_params is not None: + globals = dict(globals) + for param in type_params: + globals[param.__name__] = param + + arg = forward_ref.__forward_arg__ + if arg.isidentifier() and not keyword.iskeyword(arg): + if arg in locals: + value = locals[arg] + elif arg in globals: + value = globals[arg] + elif hasattr(builtins, arg): + return getattr(builtins, arg) + else: + raise NameError(arg) + else: + code = forward_ref.__forward_code__ + value = eval(code, globals, locals) + forward_ref.__forward_evaluated__ = True + forward_ref.__forward_value__ = value + return value + + def evaluate_forward_ref( + forward_ref, + *, + owner=None, + globals=None, + locals=None, + type_params=None, + format=None, + _recursive_guard=frozenset(), + ): + """Evaluate a forward reference as a type hint. + + This is similar to calling the ForwardRef.evaluate() method, + but unlike that method, evaluate_forward_ref() also: + + * Recursively evaluates forward references nested within the type hint. + * Rejects certain objects that are not valid type hints. + * Replaces type hints that evaluate to None with types.NoneType. + * Supports the *FORWARDREF* and *STRING* formats. + + *forward_ref* must be an instance of ForwardRef. *owner*, if given, + should be the object that holds the annotations that the forward reference + derived from, such as a module, class object, or function. It is used to + infer the namespaces to use for looking up names. *globals* and *locals* + can also be explicitly given to provide the global and local namespaces. + *type_params* is a tuple of type parameters that are in scope when + evaluating the forward reference. This parameter must be provided (though + it may be an empty tuple) if *owner* is not given and the forward reference + does not already have an owner set. *format* specifies the format of the + annotation and is a member of the annotationlib.Format enum. + + """ + if format == Format.STRING: + return forward_ref.__forward_arg__ + if forward_ref.__forward_arg__ in _recursive_guard: + return forward_ref + + # Evaluate the forward reference + try: + value = _eval_with_owner( + forward_ref, + owner=owner, + globals=globals, + locals=locals, + type_params=type_params, + ) + except NameError: + if format == Format.FORWARDREF: + return forward_ref + else: + raise + + if isinstance(value, str): + value = ForwardRef(value) + + # Recursively evaluate the type + if isinstance(value, ForwardRef): + if getattr(value, "__forward_module__", True) is not None: + globals = None + return evaluate_forward_ref( + value, + globals=globals, + locals=locals, + type_params=type_params, owner=owner, + _recursive_guard=_recursive_guard, format=format + ) + if sys.version_info < (3, 12, 5) and type_params: + # Make use of type_params + locals = dict(locals) if locals else {} + for tvar in type_params: + if tvar.__name__ not in locals: # lets not overwrite something present + locals[tvar.__name__] = tvar + if sys.version_info < (3, 12, 5): + return typing._eval_type( + value, + globals, + locals, + recursive_guard=_recursive_guard | {forward_ref.__forward_arg__}, + ) + else: + return typing._eval_type( + value, + globals, + locals, + type_params, + recursive_guard=_recursive_guard | {forward_ref.__forward_arg__}, + ) + + +if sys.version_info >= (3, 14, 0, "beta"): + type_repr = annotationlib.type_repr +else: + def type_repr(value): + """Convert a Python value to a format suitable for use with the STRING format. + + This is intended as a helper for tools that support the STRING format but do + not have access to the code that originally produced the annotations. It uses + repr() for most objects. + + """ + if isinstance(value, (type, _types.FunctionType, _types.BuiltinFunctionType)): + if value.__module__ == "builtins": + return value.__qualname__ + return f"{value.__module__}.{value.__qualname__}" + if value is ...: + return "..." + return repr(value) + + +# Aliases for items that are in typing in all supported versions. +# We use hasattr() checks so this library will continue to import on +# future versions of Python that may remove these names. +_typing_names = [ + "AbstractSet", + "AnyStr", + "BinaryIO", + "Callable", + "Collection", + "Container", + "Dict", + "FrozenSet", + "Hashable", + "IO", + "ItemsView", + "Iterable", + "Iterator", + "KeysView", + "List", + "Mapping", + "MappingView", + "Match", + "MutableMapping", + "MutableSequence", + "MutableSet", + "Optional", + "Pattern", + "Reversible", + "Sequence", + "Set", + "Sized", + "TextIO", + "Tuple", + "Union", + "ValuesView", + "cast", + "no_type_check", + # This is private, but it was defined by typing_extensions for a long time + # and some users rely on it. + "_AnnotatedAlias", +] + +# Breakpoint: https://github.com/python/cpython/pull/133602 +if sys.version_info < (3, 15, 0): + _typing_names.append("no_type_check_decorator") + __all__.append("no_type_check_decorator") + +globals().update( + {name: getattr(typing, name) for name in _typing_names if hasattr(typing, name)} +) +# These are defined unconditionally because they are used in +# typing-extensions itself. +Generic = typing.Generic +ForwardRef = typing.ForwardRef +Annotated = typing.Annotated diff --git a/source/target-vendors/cp312/typing_inspection-0.4.4.dist-info/METADATA b/source/target-vendors/cp312/typing_inspection-0.4.4.dist-info/METADATA new file mode 100644 index 0000000..886db47 --- /dev/null +++ b/source/target-vendors/cp312/typing_inspection-0.4.4.dist-info/METADATA @@ -0,0 +1,49 @@ +Metadata-Version: 2.5 +Name: typing-inspection +Version: 0.4.4 +Summary: Runtime typing introspection tools +Project-URL: Homepage, https://github.com/pydantic/typing-inspection +Project-URL: Documentation, https://pydantic.github.io/typing-inspection/dev/ +Project-URL: Source, https://github.com/pydantic/typing-inspection +Project-URL: Changelog, https://github.com/pydantic/typing-inspection/blob/main/HISTORY.md +Author-email: Victorien Plot +License-Expression: MIT +License-File: LICENSE +Classifier: Development Status :: 3 - Alpha +Classifier: Intended Audience :: Developers +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: 3.15 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Typing :: Typed +Requires-Python: >=3.10 +Requires-Dist: typing-extensions>=4.15.0 +Description-Content-Type: text/markdown + +# typing-inspection + +[![CI](https://img.shields.io/github/actions/workflow/status/pydantic/typing-inspection/ci.yml?branch=main&logo=github&label=CI)](https://github.com/pydantic/typing-inspection/actions?query=event%3Apush+branch%3Amain+workflow%3ACI) +[![Coverage](https://coverage-badge.samuelcolvin.workers.dev/pydantic/typing-inspection.svg)](https://coverage-badge.samuelcolvin.workers.dev/redirect/pydantic/typing-inspection) +[![PyPI](https://img.shields.io/pypi/v/typing-inspection.svg)](https://pypi.org/project/typing-inspection/) +[![Versions](https://img.shields.io/pypi/pyversions/typing-inspection.svg)](https://github.com/pydantic/typing-inspection) +[![License](https://img.shields.io/github/license/pydantic/typing-inspection.svg)](https://github.com/pydantic/typing-inspection/blob/main/LICENSE) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) + +`typing-inspection` provides tools to inspect type annotations at runtime. + +## Installation + +From [PyPI](https://pypi.org/project/typing-inspection/): + +```bash +pip install typing-inspection +``` + +The library can be imported from the `typing_inspection` module. diff --git a/source/target-vendors/cp312/typing_inspection-0.4.4.dist-info/RECORD b/source/target-vendors/cp312/typing_inspection-0.4.4.dist-info/RECORD new file mode 100644 index 0000000..cd59891 --- /dev/null +++ b/source/target-vendors/cp312/typing_inspection-0.4.4.dist-info/RECORD @@ -0,0 +1,9 @@ +typing_inspection/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +typing_inspection/introspection.py,sha256=WIjEW9Yn2mktlSdpnj9Umt81ZRWWsWVncjEOEhgbQ5Y,22550 +typing_inspection/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +typing_inspection/typing_objects.py,sha256=p3jWKQ1RWBycmiPgJjrypGBMdt71xNQKfNGjdbAEb_Y,17838 +typing_inspection/typing_objects.pyi,sha256=O9JN2S0CcNMdZP1Aq8HvhpP1Miqrmnius8W2L5kvhL4,9635 +typing_inspection-0.4.4.dist-info/METADATA,sha256=utPZ5jQu9nUM9RKOyUIye4vr3OC25Nb2fLFbJ65RGNo,2554 +typing_inspection-0.4.4.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87 +typing_inspection-0.4.4.dist-info/licenses/LICENSE,sha256=gEtZsl8sMb0nj5ICoZrkmjlFqiZkOH4tChKMfKzGHsM,1090 +typing_inspection-0.4.4.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/typing_inspection-0.4.4.dist-info/WHEEL b/source/target-vendors/cp312/typing_inspection-0.4.4.dist-info/WHEEL new file mode 100644 index 0000000..51abe1f --- /dev/null +++ b/source/target-vendors/cp312/typing_inspection-0.4.4.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.32.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/source/target-vendors/cp312/typing_inspection-0.4.4.dist-info/licenses/LICENSE b/source/target-vendors/cp312/typing_inspection-0.4.4.dist-info/licenses/LICENSE new file mode 100644 index 0000000..e825ad5 --- /dev/null +++ b/source/target-vendors/cp312/typing_inspection-0.4.4.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Pydantic Services Inc. 2025 to present + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/source/target-vendors/cp312/typing_inspection/__init__.py b/source/target-vendors/cp312/typing_inspection/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/typing_inspection/introspection.py b/source/target-vendors/cp312/typing_inspection/introspection.py new file mode 100644 index 0000000..458c4ee --- /dev/null +++ b/source/target-vendors/cp312/typing_inspection/introspection.py @@ -0,0 +1,587 @@ +"""High-level introspection utilities, used to inspect type annotations.""" + +from __future__ import annotations + +import sys +import types +from collections.abc import Generator +from dataclasses import InitVar +from enum import Enum, IntEnum, auto +from typing import Any, Literal, NamedTuple, TypeAlias, cast + +from typing_extensions import assert_never, get_args, get_origin # noqa: UP035 + +from . import typing_objects + +__all__ = ( + 'AnnotationSource', + 'ForbiddenQualifier', + 'InspectedAnnotation', + 'Qualifier', + 'get_literal_values', + 'inspect_annotation', + 'is_union_origin', +) + +if sys.version_info >= (3, 14): + + def is_union_origin(obj: Any, /) -> bool: + """Return whether the provided origin is the union form. + + ```pycon + >>> is_union_origin(typing.Union) + True + >>> is_union_origin(get_origin(int | str)) + True + >>> is_union_origin(types.UnionType) + True + ``` + + !!! note + Since Python 3.14, both `Union[, , ...]` and ` | | ...` forms create instances + of the same [`typing.Union`][] class. As such, it is recommended to not use this function + anymore (provided that you only support Python 3.14 or greater), and instead use the + [`typing_objects.is_union()`][typing_inspection.typing_objects.is_union] function directly: + + ```python + from typing import Union, get_origin + + from typing_inspection import typing_objects + + typ = int | str # Or Union[int, str] + origin = get_origin(typ) + if typing_objects.is_union(origin): + ... + ``` + """ + return typing_objects.is_union(obj) + + +else: + + def is_union_origin(obj: Any, /) -> bool: + """Return whether the provided origin is the union form. + + ```pycon + >>> is_union_origin(typing.Union) + True + >>> is_union_origin(get_origin(int | str)) + True + >>> is_union_origin(types.UnionType) + True + ``` + + !!! note + Since Python 3.14, both `Union[, , ...]` and ` | | ...` forms create instances + of the same [`typing.Union`][] class. As such, it is recommended to not use this function + anymore (provided that you only support Python 3.14 or greater), and instead use the + [`typing_objects.is_union()`][typing_inspection.typing_objects.is_union] function directly: + + ```python + from typing import Union, get_origin + + from typing_inspection import typing_objects + + typ = int | str # Or Union[int, str] + origin = get_origin(typ) + if typing_objects.is_union(origin): + ... + ``` + """ + return typing_objects.is_union(obj) or obj is types.UnionType + + +_literal_allowed_types = int | bytes | str | bool | Enum | typing_objects.NoneType + + +def _literal_type_check(value: Any, /) -> None: + """Type check the provided literal value against the legal parameters.""" + if not isinstance(value, _literal_allowed_types) and value is not typing_objects.NoneType: + raise TypeError(f'{value} is not a valid literal value, must be one of: int, bytes, str, Enum, None.') + + +def get_literal_values( + annotation: Any, + /, + *, + type_check: bool = False, + unpack_type_aliases: Literal['skip', 'lenient', 'eager'] = 'eager', +) -> Generator[Any]: + """Yield the values contained in the provided [`Literal`][typing.Literal] [special form][]. + + Args: + annotation: The [`Literal`][typing.Literal] [special form][] to unpack. + type_check: Whether to check if the literal values are [legal parameters][literal-legal-parameters]. + Raises a [`TypeError`][] otherwise. + unpack_type_aliases: What to do when encountering [PEP 695](https://peps.python.org/pep-0695/) + [type aliases][type-aliases]. Can be one of: + + - `'skip'`: Do not try to parse type aliases. Note that this can lead to incorrect results: + ```pycon + >>> type MyAlias = Literal[1, 2] + >>> list(get_literal_values(Literal[MyAlias, 3], unpack_type_aliases="skip")) + [MyAlias, 3] + ``` + + - `'lenient'`: Try to parse type aliases, and fallback to `'skip'` if the type alias can't be inspected + (because of an undefined forward reference). + + - `'eager'`: Parse type aliases and raise any encountered [`NameError`][] exceptions (the default): + ```pycon + >>> type MyAlias = Literal[1, 2] + >>> list(get_literal_values(Literal[MyAlias, 3], unpack_type_aliases="eager")) + [1, 2, 3] + ``` + + Note: + While `None` is [equivalent to][none] `type(None)`, the runtime implementation of [`Literal`][typing.Literal] + does not de-duplicate them. This function makes sure this de-duplication is applied: + + ```pycon + >>> list(get_literal_values(Literal[NoneType, None])) + [None] + ``` + + Example: + ```pycon + >>> type Ints = Literal[1, 2] + >>> list(get_literal_values(Literal[1, Ints], unpack_type_alias="skip")) + ["a", Ints] + >>> list(get_literal_values(Literal[1, Ints])) + [1, 2] + >>> list(get_literal_values(Literal[1.0], type_check=True)) + Traceback (most recent call last): + ... + TypeError: 1.0 is not a valid literal value, must be one of: int, bytes, str, Enum, None. + ``` + """ + # `literal` is guaranteed to be a `Literal[...]` special form, so use + # `__args__` directly instead of calling `get_args()`. + + if unpack_type_aliases == 'skip': + _has_none = False + # `Literal` parameters are already deduplicated, no need to do it ourselves. + # (we only check for `None` and `NoneType`, which should be considered as duplicates). + for arg in annotation.__args__: + if type_check: + _literal_type_check(arg) + if arg is None or arg is typing_objects.NoneType: + if not _has_none: + yield None + _has_none = True + else: + yield arg + else: + # We'll need to manually deduplicate parameters, see the `Literal` implementation in `typing`. + values_and_type: list[tuple[Any, type[Any]]] = [] + + for arg in annotation.__args__: + # Note: we could also check for generic aliases with a type alias as an origin. + # However, it is very unlikely that this happens as type variables can't appear in + # `Literal` forms, so the only valid (but unnecessary) use case would be something like: + # `type Test[T] = Literal['a']` (and then use `Test[SomeType]`). + if typing_objects.is_typealiastype(arg): + try: + alias_value = arg.__value__ + except NameError: + if unpack_type_aliases == 'eager': + raise + # unpack_type_aliases == "lenient": + if type_check: + _literal_type_check(arg) + values_and_type.append((arg, type(arg))) + else: + sub_args = get_literal_values( + alias_value, type_check=type_check, unpack_type_aliases=unpack_type_aliases + ) + values_and_type.extend((a, type(a)) for a in sub_args) # pyright: ignore[reportUnknownArgumentType] + else: + if type_check: + _literal_type_check(arg) + if arg is typing_objects.NoneType: + values_and_type.append((None, typing_objects.NoneType)) + else: + values_and_type.append((arg, type(arg))) # pyright: ignore[reportUnknownArgumentType] + + try: + dct = dict.fromkeys(values_and_type) + except TypeError: + # Unhashable parameters, the Python implementation allows them + yield from (p for p, _ in values_and_type) + else: + yield from (p for p, _ in dct) + + +Qualifier: TypeAlias = Literal['required', 'not_required', 'read_only', 'class_var', 'init_var', 'final'] +"""A [type qualifier][].""" + +_all_qualifiers: set[Qualifier] = set(get_args(Qualifier)) + + +# TODO at some point, we could switch to an enum flag, so that multiple sources +# can be combined. However, is there a need for this? +class AnnotationSource(IntEnum): + # TODO if/when https://peps.python.org/pep-0767/ is accepted, add 'read_only' + # to CLASS and NAMED_TUPLE (even though for named tuples it is redundant). + + """The source of an annotation, e.g. a class or a function. + + Depending on the source, different [type qualifiers][type qualifier] may be (dis)allowed. + """ + + ASSIGNMENT_OR_VARIABLE = auto() + """An annotation used in an assignment or variable annotation: + + ```python + x: Final[int] = 1 + y: Final[str] + ``` + + **Allowed type qualifiers:** [`Final`][typing.Final]. + """ + + CLASS = auto() + """An annotation used in the body of a class: + + ```python + class Test: + x: Final[int] = 1 + y: ClassVar[str] + ``` + + **Allowed type qualifiers:** [`ClassVar`][typing.ClassVar], [`Final`][typing.Final]. + """ + + DATACLASS = auto() + """An annotation used in the body of a dataclass: + + ```python + @dataclass + class Test: + x: Final[int] = 1 + y: InitVar[str] = 'test' + ``` + + **Allowed type qualifiers:** [`ClassVar`][typing.ClassVar], [`Final`][typing.Final], [`InitVar`][dataclasses.InitVar]. + """ # noqa: E501 + + TYPED_DICT = auto() + """An annotation used in the body of a [`TypedDict`][typing.TypedDict]: + + ```python + class TD(TypedDict): + x: Required[ReadOnly[int]] + y: ReadOnly[NotRequired[str]] + ``` + + **Allowed type qualifiers:** [`ReadOnly`][typing.ReadOnly], [`Required`][typing.Required], + [`NotRequired`][typing.NotRequired]. + """ + + NAMED_TUPLE = auto() + """An annotation used in the body of a [`NamedTuple`][typing.NamedTuple]. + + ```python + class NT(NamedTuple): + x: int + y: str + ``` + + **Allowed type qualifiers:** none. + """ + + FUNCTION = auto() + """An annotation used in a function, either for a parameter or the return value. + + ```python + def func(a: int) -> str: + ... + ``` + + **Allowed type qualifiers:** none. + """ + + ANY = auto() + """An annotation that might come from any source. + + **Allowed type qualifiers:** all. + """ + + BARE = auto() + """An annotation that is inspected as is. + + **Allowed type qualifiers:** none. + """ + + @property + def allowed_qualifiers(self) -> set[Qualifier]: + """The allowed [type qualifiers][type qualifier] for this annotation source.""" + # TODO use a match statement when Python 3.9 support is dropped. + if self is AnnotationSource.ASSIGNMENT_OR_VARIABLE: + return {'final'} + elif self is AnnotationSource.CLASS: + return {'final', 'class_var'} + elif self is AnnotationSource.DATACLASS: + return {'final', 'class_var', 'init_var'} + elif self is AnnotationSource.TYPED_DICT: + return {'required', 'not_required', 'read_only'} + elif self in (AnnotationSource.NAMED_TUPLE, AnnotationSource.FUNCTION, AnnotationSource.BARE): + return set() + elif self is AnnotationSource.ANY: + return _all_qualifiers + else: # pragma: no cover + assert_never(self) + + +class ForbiddenQualifier(Exception): + """The provided [type qualifier][] is forbidden.""" + + qualifier: Qualifier + """The forbidden qualifier.""" + + def __init__(self, qualifier: Qualifier, /) -> None: + self.qualifier = qualifier + + +class _UnknownTypeEnum(Enum): + UNKNOWN = auto() + + def __str__(self) -> str: + return 'UNKNOWN' + + def __repr__(self) -> str: + return '' + + +UNKNOWN = _UnknownTypeEnum.UNKNOWN +"""A sentinel value used when no [type expression][] is present.""" + +_UnknownType: TypeAlias = Literal[_UnknownTypeEnum.UNKNOWN] +"""The type of the [`UNKNOWN`][typing_inspection.introspection.UNKNOWN] sentinel value.""" + + +class InspectedAnnotation(NamedTuple): + """The result of the inspected annotation.""" + + type: Any | _UnknownType + """The final [type expression][], with [type qualifiers][type qualifier] and annotated metadata stripped. + + If no type expression is available, the [`UNKNOWN`][typing_inspection.introspection.UNKNOWN] sentinel + value is used instead. This is the case when a [type qualifier][] is used with no type annotation: + + ```python + ID: Final = 1 + + class C: + x: ClassVar = 'test' + ``` + """ + + qualifiers: set[Qualifier] + """The [type qualifiers][type qualifier] present on the annotation.""" + + metadata: list[Any] + """The annotated metadata.""" + + +def inspect_annotation( # noqa: PLR0915 + annotation: Any, + /, + *, + annotation_source: AnnotationSource, + unpack_type_aliases: Literal['skip', 'lenient', 'eager'] = 'skip', +) -> InspectedAnnotation: + """Inspect an [annotation expression][], extracting any [type qualifier][] and metadata. + + An [annotation expression][] is a [type expression][] optionally surrounded by one or more + [type qualifiers][type qualifier] or by [`Annotated`][typing.Annotated]. This function will: + + - Unwrap the type expression, keeping track of the type qualifiers. + - Unwrap [`Annotated`][typing.Annotated] forms, keeping track of the annotated metadata. + + Args: + annotation: The annotation expression to be inspected. + annotation_source: The source of the annotation. Depending on the source (e.g. a class), different type + qualifiers may be (dis)allowed. To allow any type qualifier, use + [`AnnotationSource.ANY`][typing_inspection.introspection.AnnotationSource.ANY]. + unpack_type_aliases: What to do when encountering [PEP 695](https://peps.python.org/pep-0695/) + [type aliases][type-aliases]. Can be one of: + + - `'skip'`: Do not try to parse type aliases (the default): + ```pycon + >>> type MyInt = Annotated[int, 'meta'] + >>> inspect_annotation(MyInt, annotation_source=AnnotationSource.BARE, unpack_type_aliases='skip') + InspectedAnnotation(type=MyInt, qualifiers={}, metadata=[]) + ``` + + - `'lenient'`: Try to parse type aliases, and fallback to `'skip'` if the type alias + can't be inspected (because of an undefined forward reference): + ```pycon + >>> type MyInt = Annotated[Undefined, 'meta'] + >>> inspect_annotation(MyInt, annotation_source=AnnotationSource.BARE, unpack_type_aliases='lenient') + InspectedAnnotation(type=MyInt, qualifiers={}, metadata=[]) + >>> Undefined = int + >>> inspect_annotation(MyInt, annotation_source=AnnotationSource.BARE, unpack_type_aliases='lenient') + InspectedAnnotation(type=int, qualifiers={}, metadata=['meta']) + ``` + + - `'eager'`: Parse type aliases and raise any encountered [`NameError`][] exceptions. + + Returns: + The result of the inspected annotation, where the type expression, used qualifiers and metadata is stored. + + Example: + ```pycon + >>> inspect_annotation( + ... Final[Annotated[ClassVar[Annotated[int, 'meta_1']], 'meta_2']], + ... annotation_source=AnnotationSource.CLASS, + ... ) + ... + InspectedAnnotation(type=int, qualifiers={'class_var', 'final'}, metadata=['meta_1', 'meta_2']) + ``` + """ + allowed_qualifiers = annotation_source.allowed_qualifiers + qualifiers: set[Qualifier] = set() + metadata: list[Any] = [] + + while True: + annotation, _meta = _unpack_annotated(annotation, unpack_type_aliases=unpack_type_aliases) + if _meta: + metadata = _meta + metadata + continue + + origin = get_origin(annotation) + if origin is not None: + if typing_objects.is_classvar(origin): + if 'class_var' not in allowed_qualifiers: + raise ForbiddenQualifier('class_var') + qualifiers.add('class_var') + annotation = annotation.__args__[0] + elif typing_objects.is_final(origin): + if 'final' not in allowed_qualifiers: + raise ForbiddenQualifier('final') + qualifiers.add('final') + annotation = annotation.__args__[0] + elif typing_objects.is_required(origin): + if 'required' not in allowed_qualifiers: + raise ForbiddenQualifier('required') + qualifiers.add('required') + annotation = annotation.__args__[0] + elif typing_objects.is_notrequired(origin): + if 'not_required' not in allowed_qualifiers: + raise ForbiddenQualifier('not_required') + qualifiers.add('not_required') + annotation = annotation.__args__[0] + elif typing_objects.is_readonly(origin): + if 'read_only' not in allowed_qualifiers: + raise ForbiddenQualifier('not_required') + qualifiers.add('read_only') + annotation = annotation.__args__[0] + else: + # origin is not None but not a type qualifier nor `Annotated` (e.g. `list[int]`): + break + elif isinstance(annotation, InitVar): + if 'init_var' not in allowed_qualifiers: + raise ForbiddenQualifier('init_var') + qualifiers.add('init_var') + annotation = cast(Any, annotation.type) + else: + break + + # `Final`, `ClassVar` and `InitVar` are type qualifiers allowed to be used as a bare annotation: + if typing_objects.is_final(annotation): + if 'final' not in allowed_qualifiers: + raise ForbiddenQualifier('final') + qualifiers.add('final') + annotation = UNKNOWN + elif typing_objects.is_classvar(annotation): + if 'class_var' not in allowed_qualifiers: + raise ForbiddenQualifier('class_var') + qualifiers.add('class_var') + annotation = UNKNOWN + elif annotation is InitVar: + if 'init_var' not in allowed_qualifiers: + raise ForbiddenQualifier('init_var') + qualifiers.add('init_var') + annotation = UNKNOWN + + return InspectedAnnotation(annotation, qualifiers, metadata) + + +def _unpack_annotated_inner( + annotation: Any, unpack_type_aliases: Literal['lenient', 'eager'], check_annotated: bool +) -> tuple[Any, list[Any]]: + origin = get_origin(annotation) + if check_annotated and typing_objects.is_annotated(origin): + annotated_type = annotation.__origin__ + metadata = list(annotation.__metadata__) + + # The annotated type might be a PEP 695 type alias, so we need to recursively + # unpack it. Because Python already flattens `Annotated[Annotated[, ...], ...]` forms, + # we can skip the `is_annotated()` check in the next call: + annotated_type, sub_meta = _unpack_annotated_inner( + annotated_type, unpack_type_aliases=unpack_type_aliases, check_annotated=False + ) + metadata = sub_meta + metadata + return annotated_type, metadata + elif typing_objects.is_typealiastype(annotation): + try: + value = annotation.__value__ + except NameError: + if unpack_type_aliases == 'eager': + raise + else: + typ, metadata = _unpack_annotated_inner( + value, unpack_type_aliases=unpack_type_aliases, check_annotated=True + ) + if metadata: + # Having metadata means the type alias' `__value__` was an `Annotated` form + # (or, recursively, a type alias to an `Annotated` form). It is important to check + # for this, as we don't want to unpack other type aliases (e.g. `type MyInt = int`). + return typ, metadata + return annotation, [] + elif typing_objects.is_typealiastype(origin): + # When parameterized, PEP 695 type aliases become generic aliases + # (e.g. with `type MyList[T] = Annotated[list[T], ...]`, `MyList[int]` + # is a generic alias). + try: + value = origin.__value__ + except NameError: + if unpack_type_aliases == 'eager': + raise + else: + # While Python already handles type variable replacement for simple `Annotated` forms, + # we need to manually apply the same logic for PEP 695 type aliases: + # - With `MyList = Annotated[list[T], ...]`, `MyList[int] == Annotated[list[int], ...]` + # - With `type MyList[T] = Annotated[list[T], ...]`, `MyList[int].__value__ == Annotated[list[T], ...]`. + + try: + # To do so, we emulate the parameterization of the value with the arguments: + # with `type MyList[T] = Annotated[list[T], ...]`, to emulate `MyList[int]`, + # we do `Annotated[list[T], ...][int]` (which gives `Annotated[list[T], ...]`): + value = value[annotation.__args__] + except TypeError: + # Might happen if the type alias is parameterized, but its value doesn't have any + # type variables, e.g. `type MyInt[T] = int`. + pass + typ, metadata = _unpack_annotated_inner( + value, unpack_type_aliases=unpack_type_aliases, check_annotated=True + ) + if metadata: + return typ, metadata + return annotation, [] + + return annotation, [] + + +# This could eventually be made public: +def _unpack_annotated( + annotation: Any, /, *, unpack_type_aliases: Literal['skip', 'lenient', 'eager'] = 'eager' +) -> tuple[Any, list[Any]]: + if unpack_type_aliases == 'skip': + if typing_objects.is_annotated(get_origin(annotation)): + return annotation.__origin__, list(annotation.__metadata__) + else: + return annotation, [] + + return _unpack_annotated_inner(annotation, unpack_type_aliases=unpack_type_aliases, check_annotated=True) diff --git a/source/target-vendors/cp312/typing_inspection/py.typed b/source/target-vendors/cp312/typing_inspection/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/typing_inspection/typing_objects.py b/source/target-vendors/cp312/typing_inspection/typing_objects.py new file mode 100644 index 0000000..610f592 --- /dev/null +++ b/source/target-vendors/cp312/typing_inspection/typing_objects.py @@ -0,0 +1,620 @@ +"""Low-level introspection utilities for [`typing`][] members. + +The provided functions in this module check against both the [`typing`][] and [`typing_extensions`][] +variants, if they exists and are different. +""" +# ruff: noqa: UP006 + +import collections.abc +import contextlib +import re +import sys +import typing +import warnings +from textwrap import dedent +from types import FunctionType, GenericAlias, NoneType +from typing import Any, Final + +import typing_extensions +from typing_extensions import LiteralString, TypeAliasType, TypeIs, deprecated + +__all__ = ( + 'DEPRECATED_ALIASES', + 'DEPRECATED_ALIASES_IDS', + 'NoneType', + 'is_annotated', + 'is_any', + 'is_classvar', + 'is_concatenate', + 'is_deprecated', + 'is_final', + 'is_forwardref', + 'is_generic', + 'is_literal', + 'is_literalstring', + 'is_namedtuple', + 'is_never', + 'is_newtype', + 'is_nodefault', + 'is_noextraitems', + 'is_noreturn', + 'is_notrequired', + 'is_paramspec', + 'is_paramspecargs', + 'is_paramspeckwargs', + 'is_readonly', + 'is_required', + 'is_self', + 'is_typealias', + 'is_typealiastype', + 'is_typeguard', + 'is_typeis', + 'is_typevar', + 'is_typevartuple', + 'is_union', + 'is_unpack', +) + +_IS_PY310 = sys.version_info[:2] == (3, 10) + + +def _compile_identity_check_function(member: LiteralString, function_name: LiteralString) -> FunctionType: + """Create a function checking that the function argument is the (unparameterized) typing `member`. + + The function will make sure to check against both the `typing` and `typing_extensions` + variants as depending on the Python version, the `typing_extensions` variant might be different. + For instance, on Python 3.9: + + ```pycon + >>> from typing import Literal as t_Literal + >>> from typing_extensions import Literal as te_Literal, get_origin + + >>> t_Literal is te_Literal + False + >>> get_origin(t_Literal[1]) + typing.Literal + >>> get_origin(te_Literal[1]) + typing_extensions.Literal + ``` + """ + in_typing = hasattr(typing, member) + in_typing_extensions = hasattr(typing_extensions, member) + + globals_: dict[str, Any] = {'Any': Any} + + if in_typing and in_typing_extensions: + # For performance reasons, cache the objects in `globals_` so the generated function avoids + # repeated module attribute lookups (and `typing`'s module-level `__getattr__()`): + t_obj = getattr(typing, member) + te_obj = getattr(typing_extensions, member) + if t_obj is te_obj: + globals_['_obj'] = t_obj + check_code = 'obj is _obj' + else: + globals_['_t_obj'] = t_obj + globals_['_te_obj'] = te_obj + check_code = 'obj is _t_obj or obj is _te_obj' + elif in_typing and not in_typing_extensions: + globals_['_obj'] = getattr(typing, member) + check_code = 'obj is _obj' + elif not in_typing and in_typing_extensions: + globals_['_obj'] = getattr(typing_extensions, member) + check_code = 'obj is _obj' + else: + check_code = 'False' + + func_code = dedent(f""" + def {function_name}(obj: Any, /) -> bool: + return {check_code} + """) + + locals_: dict[str, Any] = {} + exec(func_code, globals_, locals_) + return locals_[function_name] + + +def _compile_isinstance_check_function(member: LiteralString, function_name: LiteralString) -> FunctionType: + """Create a function checking that the function is an instance of the typing `member`. + + The function will make sure to check against both the `typing` and `typing_extensions` + variants as depending on the Python version, the `typing_extensions` variant might be different. + """ + in_typing = hasattr(typing, member) + in_typing_extensions = hasattr(typing_extensions, member) + + globals_: dict[str, Any] = {'Any': Any} + + if in_typing and in_typing_extensions: + # For performance reasons, cache the objects in `globals_` so the generated function avoids + # repeated module attribute lookups (and `typing`'s module-level `__getattr__()`): + t_obj = getattr(typing, member) + te_obj = getattr(typing_extensions, member) + if t_obj is te_obj: + globals_['_obj'] = t_obj + check_code = 'isinstance(obj, _obj)' + else: + globals_['_objs'] = (t_obj, te_obj) + check_code = 'isinstance(obj, _objs)' + elif in_typing and not in_typing_extensions: + globals_['_obj'] = getattr(typing, member) + check_code = 'isinstance(obj, _obj)' + elif not in_typing and in_typing_extensions: + globals_['_obj'] = getattr(typing_extensions, member) + check_code = 'isinstance(obj, _obj)' + else: + check_code = 'False' + + func_code = dedent(f""" + def {function_name}(obj: Any, /) -> 'TypeIs[{member}]': + return {check_code} + """) + + locals_: dict[str, Any] = {} + exec(func_code, globals_, locals_) + return locals_[function_name] + + +# Keep this ordered, as per `typing.__all__`: + +is_annotated = _compile_identity_check_function('Annotated', 'is_annotated') +is_annotated.__doc__ = """ +Return whether the argument is the [`Annotated`][typing.Annotated] [special form][]. + +```pycon +>>> is_annotated(Annotated) +True +>>> is_annotated(Annotated[int, ...]) +False +``` +""" + +is_any = _compile_identity_check_function('Any', 'is_any') +is_any.__doc__ = """ +Return whether the argument is the [`Any`][typing.Any] [special form][]. + +```pycon +>>> is_any(Any) +True +``` +""" + +is_classvar = _compile_identity_check_function('ClassVar', 'is_classvar') +is_classvar.__doc__ = """ +Return whether the argument is the [`ClassVar`][typing.ClassVar] [type qualifier][]. + +```pycon +>>> is_classvar(ClassVar) +True +>>> is_classvar(ClassVar[int]) +>>> False +``` +""" + +is_concatenate = _compile_identity_check_function('Concatenate', 'is_concatenate') +is_concatenate.__doc__ = """ +Return whether the argument is the [`Concatenate`][typing.Concatenate] [special form][]. + +```pycon +>>> is_concatenate(Concatenate) +True +>>> is_concatenate(Concatenate[int, P]) +False +``` +""" + +is_final = _compile_identity_check_function('Final', 'is_final') +is_final.__doc__ = """ +Return whether the argument is the [`Final`][typing.Final] [type qualifier][]. + +```pycon +>>> is_final(Final) +True +>>> is_final(Final[int]) +False +``` +""" + + +# Unlikely to have a different version in `typing-extensions`, but keep it consistent. +# Also note that starting in 3.14, this is an alias to `annotationlib.ForwardRef`, but +# accessing it from `typing` doesn't seem to be deprecated. +is_forwardref = _compile_isinstance_check_function('ForwardRef', 'is_forwardref') +is_forwardref.__doc__ = """ +Return whether the argument is an instance of [`ForwardRef`][typing.ForwardRef]. + +```pycon +>>> is_forwardref(ForwardRef('T')) +True +``` +""" + + +is_generic = _compile_identity_check_function('Generic', 'is_generic') +is_generic.__doc__ = """ +Return whether the argument is the [`Generic`][typing.Generic] [special form][]. + +```pycon +>>> is_generic(Generic) +True +>>> is_generic(Generic[T]) +False +``` +""" + +is_literal = _compile_identity_check_function('Literal', 'is_literal') +is_literal.__doc__ = """ +Return whether the argument is the [`Literal`][typing.Literal] [special form][]. + +```pycon +>>> is_literal(Literal) +True +>>> is_literal(Literal["a"]) +False +``` +""" + + +# `get_origin(Optional[int]) is Union`, so `is_optional()` isn't implemented. + +is_paramspec = _compile_isinstance_check_function('ParamSpec', 'is_paramspec') +is_paramspec.__doc__ = """ +Return whether the argument is an instance of [`ParamSpec`][typing.ParamSpec]. + +```pycon +>>> P = ParamSpec('P') +>>> is_paramspec(P) +True +``` +""" + +# Protocol? + +is_typevar = _compile_isinstance_check_function('TypeVar', 'is_typevar') +is_typevar.__doc__ = """ +Return whether the argument is an instance of [`TypeVar`][typing.TypeVar]. + +```pycon +>>> T = TypeVar('T') +>>> is_typevar(T) +True +``` +""" + +is_typevartuple = _compile_isinstance_check_function('TypeVarTuple', 'is_typevartuple') +is_typevartuple.__doc__ = """ +Return whether the argument is an instance of [`TypeVarTuple`][typing.TypeVarTuple]. + +```pycon +>>> Ts = TypeVarTuple('Ts') +>>> is_typevartuple(Ts) +True +``` +""" + +is_union = _compile_identity_check_function('Union', 'is_union') +is_union.__doc__ = """ +Return whether the argument is the [`Union`][typing.Union] [special form][]. + +This function can also be used to check for the [`Optional`][typing.Optional] [special form][], +as at runtime, `Optional[int]` is equivalent to `Union[int, None]`. + +```pycon +>>> is_union(Union) +True +>>> is_union(Union[int, str]) +False +``` + +!!! warning + This does not check for unions using the [new syntax][types-union] (e.g. `int | str`). +""" + + +def is_namedtuple(obj: Any, /) -> bool: + """Return whether the argument is a named tuple type. + + This includes [`NamedTuple`][typing.NamedTuple] subclasses and classes created from the + [`collections.namedtuple`][] factory function. + + ```pycon + >>> class User(NamedTuple): + ... name: str + ... + >>> is_namedtuple(User) + True + >>> City = collections.namedtuple('City', []) + >>> is_namedtuple(City) + True + >>> is_namedtuple(NamedTuple) + False + ``` + """ + return isinstance(obj, type) and issubclass(obj, tuple) and hasattr(obj, '_fields') # pyright: ignore[reportUnknownArgumentType] + + +# TypedDict? + +# BinaryIO? IO? TextIO? + +is_literalstring = _compile_identity_check_function('LiteralString', 'is_literalstring') +is_literalstring.__doc__ = """ +Return whether the argument is the [`LiteralString`][typing.LiteralString] [special form][]. + +```pycon +>>> is_literalstring(LiteralString) +True +``` +""" + +is_never = _compile_identity_check_function('Never', 'is_never') +is_never.__doc__ = """ +Return whether the argument is the [`Never`][typing.Never] [special form][]. + +```pycon +>>> is_never(Never) +True +``` +""" + +is_newtype = _compile_isinstance_check_function('NewType', 'is_newtype') +is_newtype.__doc__ = """ +Return whether the argument is a [`NewType`][typing.NewType]. + +```pycon +>>> UserId = NewType("UserId", int) +>>> is_newtype(UserId) +True +``` +""" + +is_nodefault = _compile_identity_check_function('NoDefault', 'is_nodefault') +is_nodefault.__doc__ = """ +Return whether the argument is the [`NoDefault`][typing.NoDefault] sentinel object. + +```pycon +>>> is_nodefault(NoDefault) +True +``` +""" + +is_noextraitems = _compile_identity_check_function('NoExtraItems', 'is_noextraitems') +is_noextraitems.__doc__ = """ +Return whether the argument is the `NoExtraItems` sentinel object. + +```pycon +>>> is_noextraitems(NoExtraItems) +True +``` +""" + +is_noreturn = _compile_identity_check_function('NoReturn', 'is_noreturn') +is_noreturn.__doc__ = """ +Return whether the argument is the [`NoReturn`][typing.NoReturn] [special form][]. + +```pycon +>>> is_noreturn(NoReturn) +True +>>> is_noreturn(Never) +False +``` +""" + +is_notrequired = _compile_identity_check_function('NotRequired', 'is_notrequired') +is_notrequired.__doc__ = """ +Return whether the argument is the [`NotRequired`][typing.NotRequired] [special form][]. + +```pycon +>>> is_notrequired(NotRequired) +True +``` +""" + +is_paramspecargs = _compile_isinstance_check_function('ParamSpecArgs', 'is_paramspecargs') +is_paramspecargs.__doc__ = """ +Return whether the argument is an instance of [`ParamSpecArgs`][typing.ParamSpecArgs]. + +```pycon +>>> P = ParamSpec('P') +>>> is_paramspecargs(P.args) +True +``` +""" + +is_paramspeckwargs = _compile_isinstance_check_function('ParamSpecKwargs', 'is_paramspeckwargs') +is_paramspeckwargs.__doc__ = """ +Return whether the argument is an instance of [`ParamSpecKwargs`][typing.ParamSpecKwargs]. + +```pycon +>>> P = ParamSpec('P') +>>> is_paramspeckwargs(P.kwargs) +True +``` +""" + +is_readonly = _compile_identity_check_function('ReadOnly', 'is_readonly') +is_readonly.__doc__ = """ +Return whether the argument is the [`ReadOnly`][typing.ReadOnly] [special form][]. + +```pycon +>>> is_readonly(ReadOnly) +True +``` +""" + +is_required = _compile_identity_check_function('Required', 'is_required') +is_required.__doc__ = """ +Return whether the argument is the [`Required`][typing.Required] [special form][]. + +```pycon +>>> is_required(Required) +True +``` +""" + +is_self = _compile_identity_check_function('Self', 'is_self') +is_self.__doc__ = """ +Return whether the argument is the [`Self`][typing.Self] [special form][]. + +```pycon +>>> is_self(Self) +True +``` +""" + +# TYPE_CHECKING? + +is_typealias = _compile_identity_check_function('TypeAlias', 'is_typealias') +is_typealias.__doc__ = """ +Return whether the argument is the [`TypeAlias`][typing.TypeAlias] [special form][]. + +```pycon +>>> is_typealias(TypeAlias) +True +``` +""" + +is_typeguard = _compile_identity_check_function('TypeGuard', 'is_typeguard') +is_typeguard.__doc__ = """ +Return whether the argument is the [`TypeGuard`][typing.TypeGuard] [special form][]. + +```pycon +>>> is_typeguard(TypeGuard) +True +``` +""" + +is_typeis = _compile_identity_check_function('TypeIs', 'is_typeis') +is_typeis.__doc__ = """ +Return whether the argument is the [`TypeIs`][typing.TypeIs] [special form][]. + +```pycon +>>> is_typeis(TypeIs) +True +``` +""" + +_is_typealiastype_inner = _compile_isinstance_check_function('TypeAliasType', '_is_typealiastype_inner') + + +if _IS_PY310: + # Parameterized PEP 695 type aliases are instances of `types.GenericAlias` in typing_extensions>=4.13.0. + # On Python 3.10, with `Alias[int]` being such an instance of `GenericAlias`, + # `isinstance(Alias[int], TypeAliasType)` returns `True`. + # See https://github.com/python/cpython/issues/89828. + def is_typealiastype(obj: Any, /) -> 'TypeIs[TypeAliasType]': + return type(obj) is not GenericAlias and _is_typealiastype_inner(obj) +else: + is_typealiastype = _compile_isinstance_check_function('TypeAliasType', 'is_typealiastype') + +is_typealiastype.__doc__ = """ +Return whether the argument is a [`TypeAliasType`][typing.TypeAliasType] instance. + +```pycon +>>> type MyInt = int +>>> is_typealiastype(MyInt) +True +>>> MyStr = TypeAliasType("MyStr", str) +>>> is_typealiastype(MyStr): +True +>>> type MyList[T] = list[T] +>>> is_typealiastype(MyList[int]) +False +``` +""" + +is_unpack = _compile_identity_check_function('Unpack', 'is_unpack') +is_unpack.__doc__ = """ +Return whether the argument is the [`Unpack`][typing.Unpack] [special form][]. + +```pycon +>>> is_unpack(Unpack) +True +>>> is_unpack(Unpack[Ts]) +False +``` +""" + + +if sys.version_info >= (3, 13): + _deprecated_types = (warnings.deprecated, typing_extensions.deprecated) + + def is_deprecated(obj: Any, /) -> 'TypeIs[deprecated]': + return isinstance(obj, _deprecated_types) + +else: + _deprecated_type = typing_extensions.deprecated + + def is_deprecated(obj: Any, /) -> 'TypeIs[deprecated]': + return isinstance(obj, _deprecated_type) + + +is_deprecated.__doc__ = """ +Return whether the argument is a [`deprecated`][warnings.deprecated] instance. + +This also includes the [`typing_extensions` backport][typing_extensions.deprecated]. + +```pycon +>>> is_deprecated(warnings.deprecated('message')) +True +>>> is_deprecated(typing_extensions.deprecated('message')) +True +``` +""" + + +# Aliases defined in the `typing` module using `typing._SpecialGenericAlias` (itself aliased as `alias()`): +DEPRECATED_ALIASES: Final[dict[Any, type[Any]]] = { + typing.Hashable: collections.abc.Hashable, + typing.Awaitable: collections.abc.Awaitable, + typing.Coroutine: collections.abc.Coroutine, + typing.AsyncIterable: collections.abc.AsyncIterable, + typing.AsyncIterator: collections.abc.AsyncIterator, + typing.Iterable: collections.abc.Iterable, + typing.Iterator: collections.abc.Iterator, + typing.Reversible: collections.abc.Reversible, + typing.Sized: collections.abc.Sized, + typing.Container: collections.abc.Container, + typing.Collection: collections.abc.Collection, + # type ignore reason: https://github.com/python/typeshed/issues/6257: + typing.Callable: collections.abc.Callable, # pyright: ignore[reportAssignmentType, reportUnknownMemberType] + typing.AbstractSet: collections.abc.Set, + typing.MutableSet: collections.abc.MutableSet, + typing.Mapping: collections.abc.Mapping, + typing.MutableMapping: collections.abc.MutableMapping, + typing.Sequence: collections.abc.Sequence, + typing.MutableSequence: collections.abc.MutableSequence, + typing.Tuple: tuple, + typing.List: list, + typing.Deque: collections.deque, + typing.Set: set, + typing.FrozenSet: frozenset, + typing.MappingView: collections.abc.MappingView, + typing.KeysView: collections.abc.KeysView, + typing.ItemsView: collections.abc.ItemsView, + typing.ValuesView: collections.abc.ValuesView, + typing.Dict: dict, + typing.DefaultDict: collections.defaultdict, + typing.OrderedDict: collections.OrderedDict, + typing.Counter: collections.Counter, + typing.ChainMap: collections.ChainMap, + typing.Generator: collections.abc.Generator, + typing.AsyncGenerator: collections.abc.AsyncGenerator, + typing.Type: type, + # Defined in `typing.__getattr__`: + typing.Pattern: re.Pattern, + typing.Match: re.Match, + typing.ContextManager: contextlib.AbstractContextManager, + typing.AsyncContextManager: contextlib.AbstractAsyncContextManager, + # Skipped: `ByteString` (deprecated, removed in 3.14) +} +"""A mapping between the deprecated typing aliases to their replacement, as per [PEP 585](https://peps.python.org/pep-0585/).""" + + +DEPRECATED_ALIASES_IDS: Final[dict[int, type[Any]]] = {id(k): v for k, v in DEPRECATED_ALIASES.items()} +"""A mapping between the [identity][id] of the deprecated typing aliases to their replacement, as per [PEP 585](https://peps.python.org/pep-0585/).""" + + +# Add the `typing_extensions` aliases: +for alias, target in list(DEPRECATED_ALIASES.items()): + if (te_alias := getattr(typing_extensions, alias.__name__, None)) is not None: + DEPRECATED_ALIASES[te_alias] = target diff --git a/source/target-vendors/cp312/typing_inspection/typing_objects.pyi b/source/target-vendors/cp312/typing_inspection/typing_objects.pyi new file mode 100644 index 0000000..69c731b --- /dev/null +++ b/source/target-vendors/cp312/typing_inspection/typing_objects.pyi @@ -0,0 +1,421 @@ +# Stub file generated using: +# `stubgen --inspect-mode --include-docstrings -m typing_inspection.typing_objects` +# (manual edits need to be applied). +"""Low-level introspection utilities for [`typing`][] members. + +The provided functions in this module check against both the [`typing`][] and [`typing_extensions`][] +variants, if they exists and are different. +""" + +import sys +from typing import Any, Final, ForwardRef, NewType, TypeVar + +from typing_extensions import ParamSpec, ParamSpecArgs, ParamSpecKwargs, TypeAliasType, TypeIs, TypeVarTuple, deprecated + +__all__ = [ + 'DEPRECATED_ALIASES', + 'DEPRECATED_ALIASES_IDS', + 'NoneType', + 'is_annotated', + 'is_any', + 'is_classvar', + 'is_concatenate', + 'is_deprecated', + 'is_final', + 'is_generic', + 'is_literal', + 'is_literalstring', + 'is_namedtuple', + 'is_never', + 'is_newtype', + 'is_nodefault', + 'is_noextraitems', + 'is_noreturn', + 'is_notrequired', + 'is_paramspec', + 'is_paramspecargs', + 'is_paramspeckwargs', + 'is_readonly', + 'is_required', + 'is_self', + 'is_typealias', + 'is_typealiastype', + 'is_typeguard', + 'is_typeis', + 'is_typevar', + 'is_typevartuple', + 'is_union', + 'is_unpack', +] + +if sys.version_info >= (3, 10): + from types import NoneType +else: + NoneType = type(None) + +def is_annotated(obj: Any, /) -> bool: + """ + Return whether the argument is the [`Annotated`][typing.Annotated] [special form][]. + + ```pycon + >>> is_annotated(Annotated) + True + >>> is_annotated(Annotated[int, ...]) + False + ``` + """ + +def is_any(obj: Any, /) -> bool: + """ + Return whether the argument is the [`Any`][typing.Any] [special form][]. + + ```pycon + >>> is_any(Any) + True + ``` + """ + +def is_classvar(obj: Any, /) -> bool: + """ + Return whether the argument is the [`ClassVar`][typing.ClassVar] [type qualifier][]. + + ```pycon + >>> is_classvar(ClassVar) + True + >>> is_classvar(ClassVar[int]) + >>> False + ``` + """ + +def is_concatenate(obj: Any, /) -> bool: + """ + Return whether the argument is the [`Concatenate`][typing.Concatenate] [special form][]. + + ```pycon + >>> is_concatenate(Concatenate) + True + >>> is_concatenate(Concatenate[int, P]) + False + ``` + """ + +def is_final(obj: Any, /) -> bool: + """ + Return whether the argument is the [`Final`][typing.Final] [type qualifier][]. + + ```pycon + >>> is_final(Final) + True + >>> is_final(Final[int]) + False + ``` + """ + +def is_forwardref(obj: Any, /) -> TypeIs[ForwardRef]: + """ + Return whether the argument is an instance of [`ForwardRef`][typing.ForwardRef]. + + ```pycon + >>> is_forwardref(ForwardRef('T')) + True + ``` + """ + +def is_generic(obj: Any, /) -> bool: + """ + Return whether the argument is the [`Generic`][typing.Generic] [special form][]. + + ```pycon + >>> is_generic(Generic) + True + >>> is_generic(Generic[T]) + False + ``` + """ + +def is_literal(obj: Any, /) -> bool: + """ + Return whether the argument is the [`Literal`][typing.Literal] [special form][]. + + ```pycon + >>> is_literal(Literal) + True + >>> is_literal(Literal["a"]) + False + ``` + """ + +def is_paramspec(obj: Any, /) -> TypeIs[ParamSpec]: + """ + Return whether the argument is an instance of [`ParamSpec`][typing.ParamSpec]. + + ```pycon + >>> P = ParamSpec('P') + >>> is_paramspec(P) + True + ``` + """ + +def is_typevar(obj: Any, /) -> TypeIs[TypeVar]: + """ + Return whether the argument is an instance of [`TypeVar`][typing.TypeVar]. + + ```pycon + >>> T = TypeVar('T') + >>> is_typevar(T) + True + ``` + """ + +def is_typevartuple(obj: Any, /) -> TypeIs[TypeVarTuple]: + """ + Return whether the argument is an instance of [`TypeVarTuple`][typing.TypeVarTuple]. + + ```pycon + >>> Ts = TypeVarTuple('Ts') + >>> is_typevartuple(Ts) + True + ``` + """ + +def is_union(obj: Any, /) -> bool: + """ + Return whether the argument is the [`Union`][typing.Union] [special form][]. + + This function can also be used to check for the [`Optional`][typing.Optional] [special form][], + as at runtime, `Optional[int]` is equivalent to `Union[int, None]`. + + ```pycon + >>> is_union(Union) + True + >>> is_union(Union[int, str]) + False + ``` + + !!! warning + This does not check for unions using the [new syntax][types-union] (e.g. `int | str`). + """ + +def is_namedtuple(obj: Any, /) -> bool: + """Return whether the argument is a named tuple type. + + This includes [`NamedTuple`][typing.NamedTuple] subclasses and classes created from the + [`collections.namedtuple`][] factory function. + + ```pycon + >>> class User(NamedTuple): + ... name: str + ... + >>> is_namedtuple(User) + True + >>> City = collections.namedtuple('City', []) + >>> is_namedtuple(City) + True + >>> is_namedtuple(NamedTuple) + False + ``` + """ + +def is_literalstring(obj: Any, /) -> bool: + """ + Return whether the argument is the [`LiteralString`][typing.LiteralString] [special form][]. + + ```pycon + >>> is_literalstring(LiteralString) + True + ``` + """ + +def is_never(obj: Any, /) -> bool: + """ + Return whether the argument is the [`Never`][typing.Never] [special form][]. + + ```pycon + >>> is_never(Never) + True + ``` + """ + +def is_newtype(obj: Any, /) -> TypeIs[NewType]: + """ + Return whether the argument is a [`NewType`][typing.NewType]. + + ```pycon + >>> UserId = NewType("UserId", int) + >>> is_newtype(UserId) + True + ``` + """ + +def is_nodefault(obj: Any, /) -> bool: + """ + Return whether the argument is the [`NoDefault`][typing.NoDefault] sentinel object. + + ```pycon + >>> is_nodefault(NoDefault) + True + ``` + """ + +def is_noextraitems(obj: Any, /) -> bool: + """ + Return whether the argument is the `NoExtraItems` sentinel object. + + ```pycon + >>> is_noextraitems(NoExtraItems) + True + ``` + """ + +def is_noreturn(obj: Any, /) -> bool: + """ + Return whether the argument is the [`NoReturn`][typing.NoReturn] [special form][]. + + ```pycon + >>> is_noreturn(NoReturn) + True + >>> is_noreturn(Never) + False + ``` + """ + +def is_notrequired(obj: Any, /) -> bool: + """ + Return whether the argument is the [`NotRequired`][typing.NotRequired] [special form][]. + + ```pycon + >>> is_notrequired(NotRequired) + True + ``` + """ + +def is_paramspecargs(obj: Any, /) -> TypeIs[ParamSpecArgs]: + """ + Return whether the argument is an instance of [`ParamSpecArgs`][typing.ParamSpecArgs]. + + ```pycon + >>> P = ParamSpec('P') + >>> is_paramspecargs(P.args) + True + ``` + """ + +def is_paramspeckwargs(obj: Any, /) -> TypeIs[ParamSpecKwargs]: + """ + Return whether the argument is an instance of [`ParamSpecKwargs`][typing.ParamSpecKwargs]. + + ```pycon + >>> P = ParamSpec('P') + >>> is_paramspeckwargs(P.kwargs) + True + ``` + """ + +def is_readonly(obj: Any, /) -> bool: + """ + Return whether the argument is the [`ReadOnly`][typing.ReadOnly] [special form][]. + + ```pycon + >>> is_readonly(ReadOnly) + True + ``` + """ + +def is_required(obj: Any, /) -> bool: + """ + Return whether the argument is the [`Required`][typing.Required] [special form][]. + + ```pycon + >>> is_required(Required) + True + ``` + """ + +def is_self(obj: Any, /) -> bool: + """ + Return whether the argument is the [`Self`][typing.Self] [special form][]. + + ```pycon + >>> is_self(Self) + True + ``` + """ + +def is_typealias(obj: Any, /) -> bool: + """ + Return whether the argument is the [`TypeAlias`][typing.TypeAlias] [special form][]. + + ```pycon + >>> is_typealias(TypeAlias) + True + ``` + """ + +def is_typeguard(obj: Any, /) -> bool: + """ + Return whether the argument is the [`TypeGuard`][typing.TypeGuard] [special form][]. + + ```pycon + >>> is_typeguard(TypeGuard) + True + ``` + """ + +def is_typeis(obj: Any, /) -> bool: + """ + Return whether the argument is the [`TypeIs`][typing.TypeIs] [special form][]. + + ```pycon + >>> is_typeis(TypeIs) + True + ``` + """ + +def is_typealiastype(obj: Any, /) -> TypeIs[TypeAliasType]: + """ + Return whether the argument is a [`TypeAliasType`][typing.TypeAliasType] instance. + + ```pycon + >>> type MyInt = int + >>> is_typealiastype(MyInt) + True + >>> MyStr = TypeAliasType("MyStr", str) + >>> is_typealiastype(MyStr): + True + >>> type MyList[T] = list[T] + >>> is_typealiastype(MyList[int]) + False + ``` + """ + +def is_unpack(obj: Any, /) -> bool: + """ + Return whether the argument is the [`Unpack`][typing.Unpack] [special form][]. + + ```pycon + >>> is_unpack(Unpack) + True + >>> is_unpack(Unpack[Ts]) + False + ``` + """ + +def is_deprecated(obj: Any, /) -> TypeIs[deprecated]: + """ + Return whether the argument is a [`deprecated`][warnings.deprecated] instance. + + This also includes the [`typing_extensions` backport][typing_extensions.deprecated]. + + ```pycon + >>> is_deprecated(warnings.deprecated('message')) + True + >>> is_deprecated(typing_extensions.deprecated('deprecated')) + True + ``` + """ + +DEPRECATED_ALIASES: Final[dict[Any, type[Any]]] +"""A mapping between the deprecated typing aliases to their replacement, as per [PEP 585](https://peps.python.org/pep-0585/).""" + +DEPRECATED_ALIASES_IDS: Final[dict[int, type[Any]]] +"""A mapping between the [identity][id] of the deprecated typing aliases to their replacement, as per [PEP 585](https://peps.python.org/pep-0585/).""" diff --git a/source/target-vendors/cp312/uvicorn-0.53.0.dist-info/METADATA b/source/target-vendors/cp312/uvicorn-0.53.0.dist-info/METADATA new file mode 100644 index 0000000..4985a6e --- /dev/null +++ b/source/target-vendors/cp312/uvicorn-0.53.0.dist-info/METADATA @@ -0,0 +1,189 @@ +Metadata-Version: 2.5 +Name: uvicorn +Version: 0.53.0 +Summary: The lightning-fast ASGI server. +Project-URL: Changelog, https://uvicorn.dev/release-notes +Project-URL: Funding, https://github.com/sponsors/Kludex +Project-URL: Homepage, https://uvicorn.dev/ +Project-URL: Source, https://github.com/Kludex/uvicorn +Author-email: Tom Christie +Maintainer-email: Marcelo Trylesinski +License-Expression: BSD-3-Clause +License-File: LICENSE.md +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Internet :: WWW/HTTP +Requires-Python: >=3.10 +Requires-Dist: click>=7.0 +Requires-Dist: h11>=0.8 +Requires-Dist: typing-extensions>=4.0; python_version < '3.11' +Provides-Extra: standard +Requires-Dist: httptools>=0.8.0; extra == 'standard' +Requires-Dist: python-dotenv>=0.13; extra == 'standard' +Requires-Dist: pyyaml>=5.1; extra == 'standard' +Requires-Dist: uvloop>=0.15.1; (sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')) and extra == 'standard' +Requires-Dist: watchfiles>=0.20; extra == 'standard' +Requires-Dist: websockets>=13.0; extra == 'standard' +Description-Content-Type: text/markdown + +

+ uvicorn +

+ +

+An ASGI web server, for Python. +

+ +--- + +[![Build Status](https://github.com/Kludex/uvicorn/workflows/Test%20Suite/badge.svg)](https://github.com/Kludex/uvicorn/actions) +[![Package version](https://badge.fury.io/py/uvicorn.svg)](https://pypi.python.org/pypi/uvicorn) +[![Supported Python Version](https://img.shields.io/pypi/pyversions/uvicorn.svg?color=%2334D058)](https://pypi.org/project/uvicorn) +[![Discord](https://img.shields.io/discord/1051468649518616576?logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/RxKUF5JuHs) + +--- + +**Documentation**: [https://uvicorn.dev](https://uvicorn.dev) + +**Source Code**: [https://www.github.com/Kludex/uvicorn](https://www.github.com/Kludex/uvicorn) + +--- + +Uvicorn is an ASGI web server implementation for Python. + +Until recently Python has lacked a minimal low-level server/application interface for +async frameworks. The [ASGI specification][asgi] fills this gap, and means we're now able to +start building a common set of tooling usable across all async frameworks. + +Uvicorn currently supports HTTP/1.1, HTTP/2 and WebSockets. + +## Quickstart + +Install using `pip`: + +```shell +$ pip install uvicorn +``` + +This will install uvicorn with minimal (pure Python) dependencies. + +```shell +$ pip install 'uvicorn[standard]' +``` + +This will install uvicorn with "Cython-based" dependencies (where possible) and other "optional extras". + +In this context, "Cython-based" means the following: + +- the event loop `uvloop` will be installed and used if possible. +- the http protocol will be handled by `httptools` if possible. + +Moreover, "optional extras" means that: + +- the websocket protocol will be handled by `websockets` (should you want to use `wsproto` you'd need to install it manually) if possible. +- the `--reload` flag in development mode will use `watchfiles`. +- `python-dotenv` will be installed should you want to use the `--env-file` option. +- `PyYAML` will be installed to allow you to provide a `.yaml` file to `--log-config`, if desired. + +Create an application, in `example.py`: + +```python +async def app(scope, receive, send): + assert scope['type'] == 'http' + + await send({ + 'type': 'http.response.start', + 'status': 200, + 'headers': [ + (b'content-type', b'text/plain'), + ], + }) + await send({ + 'type': 'http.response.body', + 'body': b'Hello, world!', + }) +``` + +Run the server: + +```shell +$ uvicorn example:app +``` + +--- + +## Why ASGI? + +Most well established Python Web frameworks started out as WSGI-based frameworks. + +WSGI applications are a single, synchronous callable that takes a request and returns a response. +This doesn’t allow for long-lived connections, like you get with long-poll HTTP or WebSocket connections, +which WSGI doesn't support well. + +Having an async concurrency model also allows for options such as lightweight background tasks, +and can be less of a limiting factor for endpoints that have long periods being blocked on network +I/O such as dealing with slow HTTP requests. + +--- + +## Alternative ASGI servers + +A strength of the ASGI protocol is that it decouples the server implementation +from the application framework. This allows for an ecosystem of interoperating +webservers and application frameworks. + +### Daphne + +The first ASGI server implementation, originally developed to power Django Channels, is [the Daphne webserver][daphne]. + +It is run widely in production, and supports HTTP/1.1, HTTP/2, and WebSockets. + +Any of the example applications given here can equally well be run using `daphne` instead. + +``` +$ pip install daphne +$ daphne app:App +``` + +### Hypercorn + +[Hypercorn][hypercorn] was initially part of the Quart web framework, before +being separated out into a standalone ASGI server. + +Hypercorn supports HTTP/1.1, HTTP/2, and WebSockets. + +It also supports [the excellent `trio` async framework][trio], as an alternative to `asyncio`. + +``` +$ pip install hypercorn +$ hypercorn app:App +``` + +### Mangum + +[Mangum][mangum] is an adapter for using ASGI applications with AWS Lambda & API Gateway. + +### Granian + +[Granian][granian] is an ASGI compatible Rust HTTP server which supports HTTP/2, TLS and WebSockets. + +--- + +

Uvicorn is BSD licensed code.
Designed & crafted with care.

— 🦄 —

+ +[asgi]: https://asgi.readthedocs.io/en/latest/ +[daphne]: https://github.com/django/daphne +[hypercorn]: https://github.com/pgjones/hypercorn +[trio]: https://trio.readthedocs.io +[mangum]: https://github.com/jordaneremieff/mangum +[granian]: https://github.com/emmett-framework/granian diff --git a/source/target-vendors/cp312/uvicorn-0.53.0.dist-info/RECORD b/source/target-vendors/cp312/uvicorn-0.53.0.dist-info/RECORD new file mode 100644 index 0000000..7cf1539 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn-0.53.0.dist-info/RECORD @@ -0,0 +1,51 @@ +uvicorn/__init__.py,sha256=YzDsuEOzmpGq7bIZ89KtaKf-YjzknovmzaIlek9fkDs,147 +uvicorn/__main__.py,sha256=DQizy6nKP0ywhPpnCHgmRDYIMfcqZKVEzNIWQZjqtVQ,62 +uvicorn/_ansi.py,sha256=s8RPpmPTO6QGRgEYbsNGeasSWqiEnCsTroyA8Z59Q00,617 +uvicorn/_compat.py,sha256=6X49c9ovzmMHir2S3syinOQMzSPBcp8MNuI9ZQrbYiU,2916 +uvicorn/_subprocess.py,sha256=fQ4JTajuXay6mVc8KQNGfXu6JHoab46aS6kyIMWJMPk,2776 +uvicorn/_types.py,sha256=JDM6E-hqZ6o_cSZvjDULjAv77PFH2pLe_Vgqshe8qH4,7677 +uvicorn/config.py,sha256=z5HG_64oOisgINqVS0zE7UfTJGywYq_Qn7d9mOTFyoc,25482 +uvicorn/importer.py,sha256=nRt0QQ3qpi264-n_mR0l55C2ddM8nowTNzT1jsWaam8,1128 +uvicorn/logging.py,sha256=kCB1n45B5O55SSApiv2x6UQG3LnFqNlGvcohGyAwblU,4256 +uvicorn/main.py,sha256=7Lg1RLSBJtcA6APR1VjvFYNGlhqC0Pxf1f532L76hd8,19049 +uvicorn/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 +uvicorn/server.py,sha256=wb6dEzRnpCq4jXFo5AjvWpAldmGpAYmUJiFKPR_C0CY,13705 +uvicorn/workers.py,sha256=lMCubn1Wi-4h2jxrXNMmhOJZy7ZP8QxbZbP3_2O1iHE,3873 +uvicorn/lifespan/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +uvicorn/lifespan/off.py,sha256=nfI6qHAUo_8-BEXMBKoHQ9wUbsXrPaXLCbDSS0vKSr8,332 +uvicorn/lifespan/on.py,sha256=iETPFj6SFhPDPcdeGxfa43LKImJU7MI5i7QUUepBcPE,5173 +uvicorn/loops/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +uvicorn/loops/asyncio.py,sha256=a8wO4fB4pCk-RLICq7CnGr_cxQI7MBLJj-Go4UQldD8,333 +uvicorn/loops/auto.py,sha256=pdTsMcqkbE8fvdNgLs1y4c6hsuN598mZOSOlC0a23S4,566 +uvicorn/loops/uvloop.py,sha256=Ca-TL-W8tbdEW5uvL_GrMih7G8e_t_vhNevi2NKYjEU,236 +uvicorn/loops/zuvloop.py,sha256=6ei_QjRoIYqMXKB4UnZSkO6k6a8PqcjWV5mvzYkR160,239 +uvicorn/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +uvicorn/middleware/asgi2.py,sha256=YQrQNm3RehFts3mzk3k4yw8aD8Egtj0tRS3N45YkQa0,394 +uvicorn/middleware/message_logger.py,sha256=IHEZUSnFNaMFUFdwtZO3AuFATnYcSor-gVtOjbCzt8M,2859 +uvicorn/middleware/proxy_headers.py,sha256=pNZk80NNscLEwi8pGn6cIbaF0bSfJhCKn3pHjqqfW90,7421 +uvicorn/middleware/wsgi.py,sha256=VnbnkdCL3_ctLWzk1j39xvxysuoAB3kSYmi8TsOLpNs,7125 +uvicorn/protocols/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +uvicorn/protocols/utils.py,sha256=1hKdDCmo_GtGrK1zZfa70jL7Hr3mDE8RnL69O4rHQRg,2058 +uvicorn/protocols/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +uvicorn/protocols/http/auto.py,sha256=YfXGyzWTaaE2p_jkTPWrJCXsxEaQnC3NK0-G7Wgmnls,403 +uvicorn/protocols/http/auto_zttp_impl.py,sha256=cvqOUvV38tc5gD4tpNhiL7IeIKyIoCyCCGe9RtnQtKk,3272 +uvicorn/protocols/http/flow_control.py,sha256=050WVg31EvPOkHwynCoMP1zXFl_vO3U4durlc5vyp4U,1701 +uvicorn/protocols/http/h11_impl.py,sha256=oxVKbCz6quvhmseqJV3-m2t7SQ3h2pUnTAM6oJPowyI,20886 +uvicorn/protocols/http/httptools_impl.py,sha256=HU4cV530FoQzphoB8QxiizHBeQ5y3Zdp0rhjOyIhQDM,22564 +uvicorn/protocols/http/zttp_h2_impl.py,sha256=HdW4vaZC2uyMKbsobhzeLGgcR_9IhpFCWba6WeqaU6Q,19950 +uvicorn/protocols/http/zttp_impl.py,sha256=5jCnrVuza4pozajsiOhYVwQLXPKgCiwUVn14p0-z2YU,21835 +uvicorn/protocols/websockets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +uvicorn/protocols/websockets/auto.py,sha256=QOnMKZfXHfOcZHdq1OITHwv7Awx1BTS3eJfZswuGEU0,611 +uvicorn/protocols/websockets/websockets_impl.py,sha256=ZpRwX6nSkMrojCaAgqAZjeAoMxZAoJW9bfUvvyOLsUE,15784 +uvicorn/protocols/websockets/websockets_sansio_impl.py,sha256=izKpehYIeIu6UlyLHg0-sKk80J0PT0G5uDzr1SczbEk,23861 +uvicorn/protocols/websockets/wsproto_impl.py,sha256=uzCxXnvNv0akCIWNY1q2DnuONcdmrYYDmgORsxeTc8M,21138 +uvicorn/supervisors/__init__.py,sha256=wT8eOEIqT1yWQgytZtv5taWMul7xoTIY0xm1m4oyPTw,507 +uvicorn/supervisors/basereload.py,sha256=C-YD18flOKKVRNTRsSCOEvyqGf3ZxpvuUvR0z0Y1Wpg,4018 +uvicorn/supervisors/multiprocess.py,sha256=U1QZdiJ_7Auazix6XOj1ku0Nj_N_PJPCoYuq6SNjZcI,10271 +uvicorn/supervisors/statreload.py,sha256=XPaEtVXaasII1kkG32UUG8oD7-fS0FwyXkQLF9QYPbA,1548 +uvicorn/supervisors/watchfilesreload.py,sha256=u-MuYm0LsZMN670u_TtSWiMBUA2z4j1sIe3O2A67Ep4,2923 +uvicorn-0.53.0.dist-info/METADATA,sha256=vPs7Yx0x4_98aZJCiwSAoJ1AfsgpkFqpMB9za9hUy0U,6607 +uvicorn-0.53.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87 +uvicorn-0.53.0.dist-info/entry_points.txt,sha256=FW1w-hkc9QgwaGoovMvm0ZY73w_NcycWdGAUfDsNGxw,46 +uvicorn-0.53.0.dist-info/licenses/LICENSE.md,sha256=7-Gs8-YvuZwoiw7HPlp3O3Jo70Mg_nV-qZQhTktjw3E,1526 +uvicorn-0.53.0.dist-info/RECORD,, diff --git a/source/target-vendors/cp312/uvicorn-0.53.0.dist-info/WHEEL b/source/target-vendors/cp312/uvicorn-0.53.0.dist-info/WHEEL new file mode 100644 index 0000000..51abe1f --- /dev/null +++ b/source/target-vendors/cp312/uvicorn-0.53.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.32.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/source/target-vendors/cp312/uvicorn-0.53.0.dist-info/entry_points.txt b/source/target-vendors/cp312/uvicorn-0.53.0.dist-info/entry_points.txt new file mode 100644 index 0000000..4b00fcb --- /dev/null +++ b/source/target-vendors/cp312/uvicorn-0.53.0.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +uvicorn = uvicorn.main:main diff --git a/source/target-vendors/cp312/uvicorn-0.53.0.dist-info/licenses/LICENSE.md b/source/target-vendors/cp312/uvicorn-0.53.0.dist-info/licenses/LICENSE.md new file mode 100644 index 0000000..a6bba14 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn-0.53.0.dist-info/licenses/LICENSE.md @@ -0,0 +1,27 @@ +Copyright © 2017-present, [Encode OSS Ltd](https://www.encode.io/). +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/source/target-vendors/cp312/uvicorn/__init__.py b/source/target-vendors/cp312/uvicorn/__init__.py new file mode 100644 index 0000000..f27ddd2 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/__init__.py @@ -0,0 +1,5 @@ +from uvicorn.config import Config +from uvicorn.main import Server, main, run + +__version__ = "0.53.0" +__all__ = ["main", "run", "Config", "Server"] diff --git a/source/target-vendors/cp312/uvicorn/__main__.py b/source/target-vendors/cp312/uvicorn/__main__.py new file mode 100644 index 0000000..8a1dc97 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/__main__.py @@ -0,0 +1,4 @@ +import uvicorn + +if __name__ == "__main__": + uvicorn.main() diff --git a/source/target-vendors/cp312/uvicorn/_ansi.py b/source/target-vendors/cp312/uvicorn/_ansi.py new file mode 100644 index 0000000..8f60f40 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/_ansi.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +_ANSI_COLORS = { + "black": 30, + "red": 31, + "green": 32, + "yellow": 33, + "blue": 34, + "magenta": 35, + "cyan": 36, + "white": 37, + "bright_black": 90, + "bright_red": 91, + "bright_green": 92, + "bright_yellow": 93, + "bright_blue": 94, + "bright_magenta": 95, + "bright_cyan": 96, + "bright_white": 97, +} + + +def style(text: str, fg: str | None = None, bold: bool = False) -> str: + prefix = "" + if fg is not None: + prefix += f"\033[{_ANSI_COLORS[fg]}m" + if bold: + prefix += "\033[1m" + return f"{prefix}{text}\033[0m" diff --git a/source/target-vendors/cp312/uvicorn/_compat.py b/source/target-vendors/cp312/uvicorn/_compat.py new file mode 100644 index 0000000..118a19a --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/_compat.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +import asyncio +import sys +from collections.abc import Callable, Coroutine +from typing import Any, TypeVar + +__all__ = ["asyncio_run", "iscoroutinefunction"] + +if sys.version_info >= (3, 14): + from inspect import iscoroutinefunction +else: + from asyncio import iscoroutinefunction + +_T = TypeVar("_T") + +if sys.version_info >= (3, 12): + asyncio_run = asyncio.run +elif sys.version_info >= (3, 11): + + def asyncio_run( + main: Coroutine[Any, Any, _T], + *, + debug: bool = False, + loop_factory: Callable[[], asyncio.AbstractEventLoop] | None = None, + ) -> _T: + # asyncio.run from Python 3.12 + # https://docs.python.org/3/license.html#psf-license + with asyncio.Runner(debug=debug, loop_factory=loop_factory) as runner: + return runner.run(main) + +else: + # modified version of asyncio.run from Python 3.10 to add loop_factory kwarg + # https://docs.python.org/3/license.html#psf-license + def asyncio_run( + main: Coroutine[Any, Any, _T], + *, + debug: bool = False, + loop_factory: Callable[[], asyncio.AbstractEventLoop] | None = None, + ) -> _T: + try: + asyncio.get_running_loop() + except RuntimeError: + pass + else: + raise RuntimeError("asyncio.run() cannot be called from a running event loop") + + if not asyncio.iscoroutine(main): + raise ValueError(f"a coroutine was expected, got {main!r}") + + if loop_factory is None: + loop = asyncio.new_event_loop() + else: + loop = loop_factory() + try: + if loop_factory is None: + asyncio.set_event_loop(loop) + if debug is not None: + loop.set_debug(debug) + return loop.run_until_complete(main) + finally: + try: + _cancel_all_tasks(loop) + loop.run_until_complete(loop.shutdown_asyncgens()) + loop.run_until_complete(loop.shutdown_default_executor()) + finally: + if loop_factory is None: + asyncio.set_event_loop(None) + loop.close() + + def _cancel_all_tasks(loop: asyncio.AbstractEventLoop) -> None: + to_cancel = asyncio.all_tasks(loop) + if not to_cancel: + return + + for task in to_cancel: + task.cancel() + + loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True)) + + for task in to_cancel: + if task.cancelled(): + continue + if task.exception() is not None: + loop.call_exception_handler( + { + "message": "unhandled exception during asyncio.run() shutdown", + "exception": task.exception(), + "task": task, + } + ) diff --git a/source/target-vendors/cp312/uvicorn/_subprocess.py b/source/target-vendors/cp312/uvicorn/_subprocess.py new file mode 100644 index 0000000..c084ec9 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/_subprocess.py @@ -0,0 +1,84 @@ +""" +Some light wrappers around Python's multiprocessing, to deal with cleanly +starting child processes. +""" + +from __future__ import annotations + +import multiprocessing +import os +import sys +from collections.abc import Callable +from multiprocessing.context import SpawnProcess +from socket import socket + +from uvicorn.config import Config + +multiprocessing.allow_connection_pickling() +spawn = multiprocessing.get_context("spawn") + + +def get_subprocess( + config: Config, + target: Callable[..., None], + sockets: list[socket], +) -> SpawnProcess: + """ + Called in the parent process, to instantiate a new child process instance. + The child is not yet started at this point. + + * config - The Uvicorn configuration instance. + * target - A callable that accepts a list of sockets. In practice this will + be the `Server.run()` method. + * sockets - A list of sockets to pass to the server. Sockets are bound once + by the parent process, and then passed to the child processes. + """ + # We pass across the stdin fileno, and reopen it in the child process. + # This is required for some debugging environments. + try: + stdin_fileno = sys.stdin.fileno() + # The `sys.stdin` can be `None`, see https://docs.python.org/3/library/sys.html#sys.__stdin__. + except (AttributeError, OSError): + stdin_fileno = None + + kwargs = { + "config": config, + "target": target, + "sockets": sockets, + "stdin_fileno": stdin_fileno, + } + + return spawn.Process(target=subprocess_started, kwargs=kwargs) + + +def subprocess_started( + config: Config, + target: Callable[..., None], + sockets: list[socket], + stdin_fileno: int | None, +) -> None: + """ + Called when the child process starts. + + * config - The Uvicorn configuration instance. + * target - A callable that accepts a list of sockets. In practice this will + be the `Server.run()` method. + * sockets - A list of sockets to pass to the server. Sockets are bound once + by the parent process, and then passed to the child processes. + * stdin_fileno - The file number of sys.stdin, so that it can be reattached + to the child process. + """ + # Re-open stdin. + if stdin_fileno is not None: + sys.stdin = os.fdopen(stdin_fileno) # pragma: full coverage + + # Logging needs to be setup again for each child. + config.configure_logging() + + try: + # Now we can call into `Server.run(sockets=sockets)` + target(sockets=sockets) + except KeyboardInterrupt: # pragma: no cover + # suppress the exception to avoid a traceback from subprocess.Popen + # the parent already expects us to end, so no vital information is lost + pass diff --git a/source/target-vendors/cp312/uvicorn/_types.py b/source/target-vendors/cp312/uvicorn/_types.py new file mode 100644 index 0000000..822445a --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/_types.py @@ -0,0 +1,274 @@ +""" +Copyright (c) Django Software Foundation and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of Django nor the names of its contributors may be used + to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +from __future__ import annotations + +import sys +import types +from collections.abc import Awaitable, Callable, Iterable, MutableMapping +from typing import Any, Literal, Protocol, TypedDict + +if sys.version_info >= (3, 11): # pragma: py-lt-311 + from typing import NotRequired +else: # pragma: py-gte-311 + from typing_extensions import NotRequired + +# WSGI +Environ = MutableMapping[str, Any] +ExcInfo = tuple[type[BaseException], BaseException, types.TracebackType | None] +StartResponse = Callable[[str, Iterable[tuple[str, str]], ExcInfo | None], None] +WSGIApp = Callable[[Environ, StartResponse], Iterable[bytes] | BaseException] + + +# ASGI +class ASGIVersions(TypedDict): + spec_version: str + version: Literal["2.0"] | Literal["3.0"] + + +class HTTPScope(TypedDict): + type: Literal["http"] + asgi: ASGIVersions + http_version: str + method: str + scheme: str + path: str + raw_path: bytes + query_string: bytes + root_path: str + headers: Iterable[tuple[bytes, bytes]] + client: tuple[str, int] | None + server: tuple[str, int | None] | None + state: NotRequired[dict[str, Any]] + extensions: NotRequired[dict[str, dict[object, object]]] + + +class WebSocketScope(TypedDict): + type: Literal["websocket"] + asgi: ASGIVersions + http_version: str + scheme: str + path: str + raw_path: bytes + query_string: bytes + root_path: str + headers: Iterable[tuple[bytes, bytes]] + client: tuple[str, int] | None + server: tuple[str, int | None] | None + subprotocols: Iterable[str] + state: NotRequired[dict[str, Any]] + extensions: NotRequired[dict[str, dict[object, object]]] + + +class LifespanScope(TypedDict): + type: Literal["lifespan"] + asgi: ASGIVersions + state: NotRequired[dict[str, Any]] + + +WWWScope = HTTPScope | WebSocketScope +Scope = HTTPScope | WebSocketScope | LifespanScope + + +class HTTPRequestEvent(TypedDict): + type: Literal["http.request"] + body: bytes + more_body: bool + + +class HTTPResponseDebugEvent(TypedDict): + type: Literal["http.response.debug"] + info: dict[str, object] + + +class HTTPResponseStartEvent(TypedDict): + type: Literal["http.response.start"] + status: int + headers: NotRequired[Iterable[tuple[bytes, bytes]]] + trailers: NotRequired[bool] + + +class HTTPResponseBodyEvent(TypedDict): + type: Literal["http.response.body"] + body: bytes + more_body: NotRequired[bool] + + +class HTTPResponseTrailersEvent(TypedDict): + type: Literal["http.response.trailers"] + headers: Iterable[tuple[bytes, bytes]] + more_trailers: bool + + +class HTTPServerPushEvent(TypedDict): + type: Literal["http.response.push"] + path: str + headers: Iterable[tuple[bytes, bytes]] + + +class HTTPDisconnectEvent(TypedDict): + type: Literal["http.disconnect"] + + +class WebSocketConnectEvent(TypedDict): + type: Literal["websocket.connect"] + + +class WebSocketAcceptEvent(TypedDict): + type: Literal["websocket.accept"] + subprotocol: NotRequired[str | None] + headers: NotRequired[Iterable[tuple[bytes, bytes]]] + + +class _WebSocketReceiveEventBytes(TypedDict): + type: Literal["websocket.receive"] + bytes: bytes + text: NotRequired[None] + + +class _WebSocketReceiveEventText(TypedDict): + type: Literal["websocket.receive"] + bytes: NotRequired[None] + text: str + + +WebSocketReceiveEvent = _WebSocketReceiveEventBytes | _WebSocketReceiveEventText + + +class _WebSocketSendEventBytes(TypedDict): + type: Literal["websocket.send"] + bytes: bytes + text: NotRequired[None] + + +class _WebSocketSendEventText(TypedDict): + type: Literal["websocket.send"] + bytes: NotRequired[None] + text: str + + +WebSocketSendEvent = _WebSocketSendEventBytes | _WebSocketSendEventText + + +class WebSocketResponseStartEvent(TypedDict): + type: Literal["websocket.http.response.start"] + status: int + headers: Iterable[tuple[bytes, bytes]] + + +class WebSocketResponseBodyEvent(TypedDict): + type: Literal["websocket.http.response.body"] + body: bytes + more_body: NotRequired[bool] + + +class WebSocketDisconnectEvent(TypedDict): + type: Literal["websocket.disconnect"] + code: int + reason: NotRequired[str | None] + + +class WebSocketCloseEvent(TypedDict): + type: Literal["websocket.close"] + code: NotRequired[int] + reason: NotRequired[str | None] + + +class LifespanStartupEvent(TypedDict): + type: Literal["lifespan.startup"] + + +class LifespanShutdownEvent(TypedDict): + type: Literal["lifespan.shutdown"] + + +class LifespanStartupCompleteEvent(TypedDict): + type: Literal["lifespan.startup.complete"] + + +class LifespanStartupFailedEvent(TypedDict): + type: Literal["lifespan.startup.failed"] + message: str + + +class LifespanShutdownCompleteEvent(TypedDict): + type: Literal["lifespan.shutdown.complete"] + + +class LifespanShutdownFailedEvent(TypedDict): + type: Literal["lifespan.shutdown.failed"] + message: str + + +WebSocketEvent = WebSocketReceiveEvent | WebSocketDisconnectEvent | WebSocketConnectEvent + + +ASGIReceiveEvent = ( + HTTPRequestEvent + | HTTPDisconnectEvent + | WebSocketConnectEvent + | WebSocketReceiveEvent + | WebSocketDisconnectEvent + | LifespanStartupEvent + | LifespanShutdownEvent +) + + +ASGISendEvent = ( + HTTPResponseStartEvent + | HTTPResponseBodyEvent + | HTTPResponseTrailersEvent + | HTTPServerPushEvent + | HTTPDisconnectEvent + | WebSocketAcceptEvent + | WebSocketSendEvent + | WebSocketResponseStartEvent + | WebSocketResponseBodyEvent + | WebSocketCloseEvent + | LifespanStartupCompleteEvent + | LifespanStartupFailedEvent + | LifespanShutdownCompleteEvent + | LifespanShutdownFailedEvent +) + + +ASGIReceiveCallable = Callable[[], Awaitable[ASGIReceiveEvent]] +ASGISendCallable = Callable[[ASGISendEvent], Awaitable[None]] + + +class ASGI2Protocol(Protocol): + def __init__(self, scope: Scope) -> None: ... # pragma: no cover + + async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... # pragma: no cover + + +ASGI2Application = type[ASGI2Protocol] +ASGI3Application = Callable[[Scope, ASGIReceiveCallable, ASGISendCallable], Awaitable[None]] +ASGIApplication = ASGI2Application | ASGI3Application diff --git a/source/target-vendors/cp312/uvicorn/config.py b/source/target-vendors/cp312/uvicorn/config.py new file mode 100644 index 0000000..37b3c0d --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/config.py @@ -0,0 +1,618 @@ +from __future__ import annotations + +import asyncio +import inspect +import json +import logging +import logging.config +import os +import socket +import ssl +import sys +from collections.abc import Awaitable, Callable +from configparser import RawConfigParser +from pathlib import Path +from typing import IO, Any, Literal + +from uvicorn._ansi import style +from uvicorn._compat import iscoroutinefunction +from uvicorn._types import ASGIApplication +from uvicorn.importer import ImportFromStringError, import_from_string +from uvicorn.logging import TRACE_LOG_LEVEL +from uvicorn.middleware.asgi2 import ASGI2Middleware +from uvicorn.middleware.message_logger import MessageLoggerMiddleware +from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware +from uvicorn.middleware.wsgi import WSGIMiddleware + + +class UvicornDeprecationWarning(UserWarning): + """A custom deprecation warning for Uvicorn. + + Unlike the built-in DeprecationWarning, this inherits from UserWarning to ensure it is visible by default, helping + users discover deprecated features without needing to enable warnings explicitly. + + Reference: https://sethmlarson.dev/deprecations-via-warnings-dont-work-for-python-libraries + """ + + +HTTPProtocolType = Literal["auto", "h11", "httptools", "zttp"] +WSProtocolType = Literal["auto", "none", "websockets", "websockets-sansio", "wsproto"] +LifespanType = Literal["auto", "on", "off"] +LoopFactoryType = Literal["none", "auto", "asyncio", "uvloop", "zuvloop"] +InterfaceType = Literal["auto", "asgi3", "asgi2", "wsgi"] + +LOG_LEVELS: dict[str, int] = { + "critical": logging.CRITICAL, + "error": logging.ERROR, + "warning": logging.WARNING, + "info": logging.INFO, + "debug": logging.DEBUG, + "trace": TRACE_LOG_LEVEL, +} +HTTP_PROTOCOLS: dict[str, str] = { + "auto": "uvicorn.protocols.http.auto:AutoHTTPProtocol", + "h11": "uvicorn.protocols.http.h11_impl:H11Protocol", + "httptools": "uvicorn.protocols.http.httptools_impl:HttpToolsProtocol", + "zttp": "uvicorn.protocols.http.zttp_impl:ZttpProtocol", +} +WS_PROTOCOLS: dict[str, str | None] = { + "auto": "uvicorn.protocols.websockets.auto:AutoWebSocketsProtocol", + "none": None, + "websockets": "uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol", + "websockets-sansio": "uvicorn.protocols.websockets.websockets_sansio_impl:WebSocketsSansIOProtocol", + "wsproto": "uvicorn.protocols.websockets.wsproto_impl:WSProtocol", +} +LIFESPAN: dict[str, str] = { + "auto": "uvicorn.lifespan.on:LifespanOn", + "on": "uvicorn.lifespan.on:LifespanOn", + "off": "uvicorn.lifespan.off:LifespanOff", +} +LOOP_FACTORIES: dict[str, str | None] = { + "none": None, + "auto": "uvicorn.loops.auto:auto_loop_factory", + "asyncio": "uvicorn.loops.asyncio:asyncio_loop_factory", + "uvloop": "uvicorn.loops.uvloop:uvloop_loop_factory", + "zuvloop": "uvicorn.loops.zuvloop:zuvloop_loop_factory", +} +INTERFACES: list[InterfaceType] = ["auto", "asgi3", "asgi2", "wsgi"] + +SSL_PROTOCOL_VERSION: int = ssl.PROTOCOL_TLS_SERVER + +STARTUP_FAILURE = 3 + +LOGGING_CONFIG: dict[str, Any] = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "default": { + "()": "uvicorn.logging.DefaultFormatter", + "fmt": "%(levelprefix)s %(message)s", + "use_colors": None, + }, + "access": { + "()": "uvicorn.logging.AccessFormatter", + "fmt": '%(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s', # noqa: E501 + }, + }, + "handlers": { + "default": { + "formatter": "default", + "class": "logging.StreamHandler", + "stream": "ext://sys.stderr", + }, + "access": { + "formatter": "access", + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", + }, + }, + "loggers": { + "uvicorn": {"handlers": ["default"], "level": "INFO", "propagate": False}, + "uvicorn.error": {"level": "INFO"}, + "uvicorn.access": {"handlers": ["access"], "level": "INFO", "propagate": False}, + }, +} + +logger = logging.getLogger("uvicorn.error") + + +def create_ssl_context( + certfile: str | os.PathLike[str], + keyfile: str | os.PathLike[str] | None, + password: str | None, + ssl_version: int, + cert_reqs: int, + ca_certs: str | os.PathLike[str] | None, + ciphers: str | None, + alpn_protocols: list[str] | None = None, +) -> ssl.SSLContext: + ctx = ssl.SSLContext(ssl_version) + get_password = (lambda: password) if password else None + ctx.load_cert_chain(certfile, keyfile, get_password) + ctx.verify_mode = ssl.VerifyMode(cert_reqs) + if ca_certs: + ctx.load_verify_locations(ca_certs) + if ciphers: + ctx.set_ciphers(ciphers) + if alpn_protocols: # pragma: no-zttp-h2 + ctx.set_alpn_protocols(alpn_protocols) + return ctx + + +def is_dir(path: Path) -> bool: + try: + if not path.is_absolute(): + path = path.resolve() + return path.is_dir() + except OSError: # pragma: full coverage + return False + + +def resolve_reload_patterns(patterns_list: list[str], directories_list: list[str]) -> tuple[list[str], list[Path]]: + directories: list[Path] = list(set(map(Path, directories_list.copy()))) + patterns: list[str] = patterns_list.copy() + + current_working_directory = Path.cwd() + for pattern in patterns_list: + # Special case for the .* pattern, otherwise this would only match + # hidden directories which is probably undesired + if pattern == ".*": + continue # pragma: py-not-linux + patterns.append(pattern) + if is_dir(Path(pattern)): + directories.append(Path(pattern)) + else: + for match in current_working_directory.glob(pattern): + if is_dir(match): + directories.append(match) + + directories = list(set(directories)) + directories = list(map(Path, directories)) + directories = list(map(lambda x: x.resolve(), directories)) + directories = list({reload_path for reload_path in directories if is_dir(reload_path)}) + + children = [] + for j in range(len(directories)): + for k in range(j + 1, len(directories)): # pragma: full coverage + if directories[j] in directories[k].parents: + children.append(directories[k]) + elif directories[k] in directories[j].parents: + children.append(directories[j]) + + directories = list(set(directories).difference(set(children))) + + return list(set(patterns)), directories + + +def _normalize_dirs(dirs: list[str] | str | None) -> list[str]: + if dirs is None: + return [] + if isinstance(dirs, str): + return [dirs] + return list(set(dirs)) + + +class Config: + def __init__( + self, + app: ASGIApplication | Callable[..., Any] | str, + host: str = "127.0.0.1", + port: int = 8000, + uds: str | None = None, + fd: int | None = None, + loop: LoopFactoryType | str = "auto", + http: type[asyncio.Protocol] | HTTPProtocolType | str = "auto", + http2: bool = False, + ws: type[asyncio.Protocol] | WSProtocolType | str = "auto", + ws_max_size: int = 16 * 1024 * 1024, + ws_max_queue: int = 32, + ws_ping_interval: float | None = 20.0, + ws_ping_timeout: float | None = 20.0, + ws_per_message_deflate: bool = True, + lifespan: LifespanType = "auto", + env_file: str | os.PathLike[str] | None = None, + log_config: dict[str, Any] | str | os.PathLike[str] | RawConfigParser | IO[Any] | None = LOGGING_CONFIG, + log_level: str | int | None = None, + access_log: bool = True, + use_colors: bool | None = None, + interface: InterfaceType = "auto", + reload: bool = False, + reload_dirs: list[str] | str | None = None, + reload_delay: float = 0.25, + reload_includes: list[str] | str | None = None, + reload_excludes: list[str] | str | None = None, + workers: int | None = None, + proxy_headers: bool = True, + server_header: bool = True, + date_header: bool = True, + forwarded_allow_ips: list[str] | str | None = None, + root_path: str = "", + limit_concurrency: int | None = None, + limit_max_requests: int | None = None, + limit_max_requests_jitter: int = 0, + backlog: int = 2048, + timeout_keep_alive: int = 5, + timeout_notify: int = 30, + timeout_graceful_shutdown: int | None = None, + timeout_worker_healthcheck: int = 5, + callback_notify: Callable[..., Awaitable[None]] | None = None, + ssl_keyfile: str | os.PathLike[str] | None = None, + ssl_certfile: str | os.PathLike[str] | None = None, + ssl_keyfile_password: str | None = None, + ssl_version: int = SSL_PROTOCOL_VERSION, + ssl_cert_reqs: int = ssl.CERT_NONE, + ssl_ca_certs: str | os.PathLike[str] | None = None, + ssl_ciphers: str | None = None, + ssl_context_factory: Callable[[Config, Callable[[], ssl.SSLContext]], ssl.SSLContext] | None = None, + headers: list[tuple[str, str]] | None = None, + factory: bool = False, + h11_max_incomplete_event_size: int | None = None, + reset_contextvars: bool = False, + ): + self.app = app + self.host = host + self.port = port + self.uds = uds + self.fd = fd + self.loop = loop + self.http = http + self.http2 = http2 + self.ws = ws + self.ws_max_size = ws_max_size + self.ws_max_queue = ws_max_queue + self.ws_ping_interval = ws_ping_interval + self.ws_ping_timeout = ws_ping_timeout + self.ws_per_message_deflate = ws_per_message_deflate + self.lifespan = lifespan + self.log_config = log_config + self.log_level = log_level + self.access_log = access_log + self.use_colors = use_colors + self.interface = interface + self.reload = reload + self.reload_delay = reload_delay + self.workers = workers or 1 + self.proxy_headers = proxy_headers + self.server_header = server_header + self.date_header = date_header + self.root_path = root_path + self.limit_concurrency = limit_concurrency + self.limit_max_requests = limit_max_requests + self.limit_max_requests_jitter = limit_max_requests_jitter + self.backlog = backlog + self.timeout_keep_alive = timeout_keep_alive + self.timeout_notify = timeout_notify + self.timeout_graceful_shutdown = timeout_graceful_shutdown + self.timeout_worker_healthcheck = timeout_worker_healthcheck + self.callback_notify = callback_notify + self.ssl_keyfile = ssl_keyfile + self.ssl_certfile = ssl_certfile + self.ssl_keyfile_password = ssl_keyfile_password + self.ssl_version = ssl_version + self.ssl_cert_reqs = ssl_cert_reqs + self.ssl_ca_certs = ssl_ca_certs + self.ssl_ciphers = ssl_ciphers + self.ssl_context_factory = ssl_context_factory + self.headers: list[tuple[str, str]] = headers or [] + self.encoded_headers: list[tuple[bytes, bytes]] = [] + self.factory = factory + self.h11_max_incomplete_event_size = h11_max_incomplete_event_size + self.reset_contextvars = reset_contextvars + + self.loaded = False + self.configure_logging() + + self.reload_dirs: list[Path] = [] + self.reload_dirs_excludes: list[Path] = [] + self.reload_includes: list[str] = [] + self.reload_excludes: list[str] = [] + + if (reload_dirs or reload_includes or reload_excludes) and not self.should_reload: + logger.warning( + "Current configuration will not reload as not all conditions are met, please refer to documentation." + ) + + if self.should_reload: + reload_dirs = _normalize_dirs(reload_dirs) + reload_includes = _normalize_dirs(reload_includes) + reload_excludes = _normalize_dirs(reload_excludes) + + self.reload_includes, self.reload_dirs = resolve_reload_patterns(reload_includes, reload_dirs) + + self.reload_excludes, self.reload_dirs_excludes = resolve_reload_patterns(reload_excludes, []) + + reload_dirs_tmp = self.reload_dirs.copy() + + for directory in self.reload_dirs_excludes: + for reload_directory in reload_dirs_tmp: + if directory == reload_directory or directory in reload_directory.parents: + try: + self.reload_dirs.remove(reload_directory) + except ValueError: # pragma: full coverage + pass + + for pattern in self.reload_excludes: + if pattern in self.reload_includes: + self.reload_includes.remove(pattern) # pragma: full coverage + + if not self.reload_dirs: + if reload_dirs: + logger.warning( + "Provided reload directories %s did not contain valid " + + "directories, watching current working directory.", + reload_dirs, + ) + self.reload_dirs = [Path.cwd()] + + logger.info( + "Will watch for changes in these directories: %s", + sorted(list(map(str, self.reload_dirs))), + ) + + if env_file is not None: + from dotenv import load_dotenv + + logger.info("Loading environment from '%s'", env_file) + load_dotenv(dotenv_path=env_file) + + if workers is None and "WEB_CONCURRENCY" in os.environ: + self.workers = int(os.environ["WEB_CONCURRENCY"]) + + self.forwarded_allow_ips: list[str] | str + if forwarded_allow_ips is None: + self.forwarded_allow_ips = os.environ.get("FORWARDED_ALLOW_IPS", "127.0.0.1,::1") + else: + self.forwarded_allow_ips = forwarded_allow_ips # pragma: full coverage + + if self.reload and self.workers > 1: + logger.warning('"workers" flag is ignored when reloading is enabled.') + + @property + def asgi_version(self) -> Literal["2.0", "3.0"]: + mapping: dict[str, Literal["2.0", "3.0"]] = { + "asgi2": "2.0", + "asgi3": "3.0", + "wsgi": "3.0", + } + return mapping[self.interface] + + @property + def is_ssl(self) -> bool: + return bool(self.ssl_keyfile or self.ssl_certfile or self.ssl_context_factory) + + @property + def use_subprocess(self) -> bool: + return bool(self.reload or self.workers > 1) + + def configure_logging(self) -> None: + logging.addLevelName(TRACE_LOG_LEVEL, "TRACE") + + if self.log_config is not None: + if isinstance(self.log_config, os.PathLike): + self.log_config = os.fspath(self.log_config) + + if isinstance(self.log_config, dict): + if self.use_colors in (True, False): + self.log_config["formatters"]["default"]["use_colors"] = self.use_colors + self.log_config["formatters"]["access"]["use_colors"] = self.use_colors + logging.config.dictConfig(self.log_config) + elif isinstance(self.log_config, str) and self.log_config.endswith(".json"): + with open(self.log_config) as file: + loaded_config = json.load(file) + logging.config.dictConfig(loaded_config) + elif isinstance(self.log_config, str) and self.log_config.endswith((".yaml", ".yml")): + try: + import yaml + except ImportError as e: + raise ImportError( + "Install the PyYAML package or uvicorn[standard] to use `--log-config` with YAML files." + ) from e + + with open(self.log_config) as file: + loaded_config = yaml.safe_load(file) + logging.config.dictConfig(loaded_config) + else: + # See the note about fileConfig() here: + # https://docs.python.org/3/library/logging.config.html#configuration-file-format + logging.config.fileConfig(self.log_config, disable_existing_loggers=False) + + if self.log_level is not None: + if isinstance(self.log_level, str): + log_level = LOG_LEVELS[self.log_level.lower()] + else: + log_level = self.log_level + logging.getLogger("uvicorn.error").setLevel(log_level) + logging.getLogger("uvicorn.access").setLevel(log_level) + logging.getLogger("uvicorn.asgi").setLevel(log_level) + if self.access_log is False: + logging.getLogger("uvicorn.access").handlers = [] + logging.getLogger("uvicorn.access").propagate = False + + def load_app(self) -> Any: + """Import the app and return it. Exits on failure.""" + try: + return import_from_string(self.app) + except ImportFromStringError as exc: + logger.error("Error loading ASGI app. %s" % exc) + sys.exit(STARTUP_FAILURE) + + def load(self) -> None: + assert not self.loaded + + if self.http2: + if self.http != "zttp": + raise ValueError( + "HTTP/2 requires the `zttp` HTTP protocol. Install it with `pip install zttp`, then select it " + "with `http='zttp'`. See https://uvicorn.dev/concepts/http2/ for more information." + ) + http_protocol_class = import_from_string("uvicorn.protocols.http.auto_zttp_impl:AutoZttpProtocol") + self.http_protocol_class: type[asyncio.Protocol] = http_protocol_class + elif isinstance(self.http, str): + self.http_protocol_class = import_from_string(HTTP_PROTOCOLS.get(self.http, self.http)) + else: + self.http_protocol_class = self.http + + alpn_protocols: list[str] | None = getattr(self.http_protocol_class, "alpn_protocols", None) + + if self.ssl_context_factory is not None: + + def default_factory() -> ssl.SSLContext: + if not self.ssl_certfile: + raise RuntimeError( + "`default_ssl_context_factory()` requires `ssl_certfile` to be set on `Config`. " + "Either pass `ssl_certfile` (and optionally `ssl_keyfile`) or build the `SSLContext` " + "directly inside `ssl_context_factory` without calling the default factory." + ) + return create_ssl_context( + keyfile=self.ssl_keyfile, + certfile=self.ssl_certfile, + password=self.ssl_keyfile_password, + ssl_version=self.ssl_version, + cert_reqs=self.ssl_cert_reqs, + ca_certs=self.ssl_ca_certs, + ciphers=self.ssl_ciphers, + alpn_protocols=alpn_protocols, + ) + + context = self.ssl_context_factory(self, default_factory) + if not isinstance(context, ssl.SSLContext): + raise TypeError(f"`ssl_context_factory` must return an `ssl.SSLContext`, got {type(context).__name__}") + self.ssl: ssl.SSLContext | None = context + elif self.is_ssl: + assert self.ssl_certfile + self.ssl = create_ssl_context( + keyfile=self.ssl_keyfile, + certfile=self.ssl_certfile, + password=self.ssl_keyfile_password, + ssl_version=self.ssl_version, + cert_reqs=self.ssl_cert_reqs, + ca_certs=self.ssl_ca_certs, + ciphers=self.ssl_ciphers, + alpn_protocols=alpn_protocols, + ) + else: + self.ssl = None + + encoded_headers = [(key.lower().encode("latin1"), value.encode("latin1")) for key, value in self.headers] + self.encoded_headers = ( + [(b"server", b"uvicorn")] + encoded_headers + if b"server" not in dict(encoded_headers) and self.server_header + else encoded_headers + ) + + if isinstance(self.ws, str): + ws_protocol_class = import_from_string(WS_PROTOCOLS.get(self.ws, self.ws)) + self.ws_protocol_class: type[asyncio.Protocol] | None = ws_protocol_class + else: + self.ws_protocol_class = self.ws + + self.lifespan_class = import_from_string(LIFESPAN[self.lifespan]) + + self.loaded_app = self.load_app() + + try: + self.loaded_app = self.loaded_app() + except TypeError as exc: + if self.factory: + logger.error("Error loading ASGI app factory: %s", exc) + sys.exit(STARTUP_FAILURE) + else: + if not self.factory: + logger.warning( + "ASGI app factory detected. Using it, but please consider setting the --factory flag explicitly." + ) + + if self.interface == "auto": + if inspect.isclass(self.loaded_app): + use_asgi_3 = hasattr(self.loaded_app, "__await__") + elif inspect.isfunction(self.loaded_app): + use_asgi_3 = iscoroutinefunction(self.loaded_app) + else: + call = getattr(self.loaded_app, "__call__", None) + use_asgi_3 = iscoroutinefunction(call) + self.interface = "asgi3" if use_asgi_3 else "asgi2" + + if self.interface == "wsgi": + self.loaded_app = WSGIMiddleware(self.loaded_app) + self.ws_protocol_class = None + elif self.interface == "asgi2": + self.loaded_app = ASGI2Middleware(self.loaded_app) + + if logger.getEffectiveLevel() <= TRACE_LOG_LEVEL: + self.loaded_app = MessageLoggerMiddleware(self.loaded_app) + if self.proxy_headers: + self.loaded_app = ProxyHeadersMiddleware(self.loaded_app, trusted_hosts=self.forwarded_allow_ips) + + self.loaded = True + + def setup_event_loop(self) -> None: + raise AttributeError( + "The `setup_event_loop` method was replaced by `get_loop_factory` in uvicorn 0.36.0.\n" + "None of those methods are supposed to be used directly. If you are doing it, please let me know here: " + "https://github.com/Kludex/uvicorn/discussions/2706. Thank you, and sorry for the inconvenience." + ) + + def get_loop_factory(self) -> Callable[[], asyncio.AbstractEventLoop] | None: + if self.loop in LOOP_FACTORIES: + loop_factory: Callable[..., Any] | None = import_from_string(LOOP_FACTORIES[self.loop]) + else: + try: + return import_from_string(self.loop) + except ImportFromStringError as exc: + logger.error("Error loading custom loop setup function. %s" % exc) + sys.exit(STARTUP_FAILURE) + if loop_factory is None: + return None + return loop_factory(use_subprocess=self.use_subprocess) + + def bind_socket(self) -> socket.socket: + logger_args: list[str | int] + if self.uds is not None: # pragma: py-win32 + path = self.uds + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + sock.bind(path) + uds_perms = 0o666 + os.chmod(self.uds, uds_perms) + except OSError as exc: # pragma: full coverage + logger.error(exc) + sys.exit(STARTUP_FAILURE) + + message = "Uvicorn running on unix socket %s (Press CTRL+C to quit)" + sock_name_format = "%s" + color_message = "Uvicorn running on " + style(sock_name_format, bold=True) + " (Press CTRL+C to quit)" + logger_args = [self.uds] + elif self.fd is not None: # pragma: py-win32 + sock = socket.fromfd(self.fd, socket.AF_UNIX, socket.SOCK_STREAM) + message = "Uvicorn running on socket %s (Press CTRL+C to quit)" + fd_name_format = "%s" + color_message = "Uvicorn running on " + style(fd_name_format, bold=True) + " (Press CTRL+C to quit)" + logger_args = [sock.getsockname()] + else: + family = socket.AF_INET + addr_format = "%s://%s:%d" + + if self.host and ":" in self.host: # pragma: full coverage + # It's an IPv6 address. + family = socket.AF_INET6 + addr_format = "%s://[%s]:%d" + + sock = socket.socket(family=family) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + sock.bind((self.host, self.port)) + except OSError as exc: # pragma: full coverage + logger.error(exc) + sys.exit(STARTUP_FAILURE) + + message = f"Uvicorn running on {addr_format} (Press CTRL+C to quit)" + color_message = "Uvicorn running on " + style(addr_format, bold=True) + " (Press CTRL+C to quit)" + protocol_name = "https" if self.is_ssl else "http" + logger_args = [protocol_name, self.host, sock.getsockname()[1]] + logger.info(message, *logger_args, extra={"color_message": color_message}) + sock.set_inheritable(True) + return sock + + @property + def should_reload(self) -> bool: + return isinstance(self.app, str) and self.reload diff --git a/source/target-vendors/cp312/uvicorn/importer.py b/source/target-vendors/cp312/uvicorn/importer.py new file mode 100644 index 0000000..f77520e --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/importer.py @@ -0,0 +1,34 @@ +import importlib +from typing import Any + + +class ImportFromStringError(Exception): + pass + + +def import_from_string(import_str: Any) -> Any: + if not isinstance(import_str, str): + return import_str + + module_str, _, attrs_str = import_str.partition(":") + if not module_str or not attrs_str: + message = 'Import string "{import_str}" must be in format ":".' + raise ImportFromStringError(message.format(import_str=import_str)) + + try: + module = importlib.import_module(module_str) + except ModuleNotFoundError as exc: + if exc.name != module_str: + raise exc from None + message = 'Could not import module "{module_str}".' + raise ImportFromStringError(message.format(module_str=module_str)) + + instance = module + try: + for attr_str in attrs_str.split("."): + instance = getattr(instance, attr_str) + except AttributeError: + message = 'Attribute "{attrs_str}" not found in module "{module_str}".' + raise ImportFromStringError(message.format(attrs_str=attrs_str, module_str=module_str)) + + return instance diff --git a/source/target-vendors/cp312/uvicorn/lifespan/__init__.py b/source/target-vendors/cp312/uvicorn/lifespan/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/uvicorn/lifespan/off.py b/source/target-vendors/cp312/uvicorn/lifespan/off.py new file mode 100644 index 0000000..74554b1 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/lifespan/off.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from typing import Any + +from uvicorn import Config + + +class LifespanOff: + def __init__(self, config: Config) -> None: + self.should_exit = False + self.state: dict[str, Any] = {} + + async def startup(self) -> None: + pass + + async def shutdown(self) -> None: + pass diff --git a/source/target-vendors/cp312/uvicorn/lifespan/on.py b/source/target-vendors/cp312/uvicorn/lifespan/on.py new file mode 100644 index 0000000..71ac394 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/lifespan/on.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +import asyncio +import logging +from asyncio import Queue +from typing import Any + +from uvicorn import Config +from uvicorn._types import ( + LifespanScope, + LifespanShutdownCompleteEvent, + LifespanShutdownEvent, + LifespanShutdownFailedEvent, + LifespanStartupCompleteEvent, + LifespanStartupEvent, + LifespanStartupFailedEvent, +) + +LifespanReceiveMessage = LifespanStartupEvent | LifespanShutdownEvent +LifespanSendMessage = ( + LifespanStartupFailedEvent + | LifespanShutdownFailedEvent + | LifespanStartupCompleteEvent + | LifespanShutdownCompleteEvent +) + + +STATE_TRANSITION_ERROR = "Got invalid state transition on lifespan protocol." + + +class LifespanOn: + def __init__(self, config: Config) -> None: + if not config.loaded: + config.load() + + self.config = config + self.logger = logging.getLogger("uvicorn.error") + self.startup_event = asyncio.Event() + self.shutdown_event = asyncio.Event() + self.receive_queue: Queue[LifespanReceiveMessage] = asyncio.Queue() + self.error_occurred = False + self.startup_failed = False + self.shutdown_failed = False + self.should_exit = False + self.state: dict[str, Any] = {} + + async def startup(self) -> None: + self.logger.info("Waiting for application startup.") + + loop = asyncio.get_event_loop() + main_lifespan_task = loop.create_task(self.main()) # noqa: F841 + # Keep a hard reference to prevent garbage collection + # See https://github.com/Kludex/uvicorn/pull/972 + startup_event: LifespanStartupEvent = {"type": "lifespan.startup"} + await self.receive_queue.put(startup_event) + await self.startup_event.wait() + + if self.startup_failed or (self.error_occurred and self.config.lifespan == "on"): + self.logger.error("Application startup failed. Exiting.") + self.should_exit = True + else: + self.logger.info("Application startup complete.") + + async def shutdown(self) -> None: + if self.error_occurred: + return + self.logger.info("Waiting for application shutdown.") + shutdown_event: LifespanShutdownEvent = {"type": "lifespan.shutdown"} + await self.receive_queue.put(shutdown_event) + await self.shutdown_event.wait() + + if self.shutdown_failed or (self.error_occurred and self.config.lifespan == "on"): + self.logger.error("Application shutdown failed. Exiting.") + self.should_exit = True + else: + self.logger.info("Application shutdown complete.") + + async def main(self) -> None: + try: + app = self.config.loaded_app + scope: LifespanScope = { + "type": "lifespan", + "asgi": {"version": self.config.asgi_version, "spec_version": "2.0"}, + "state": self.state, + } + await app(scope, self.receive, self.send) + except BaseException as exc: + self.asgi = None + self.error_occurred = True + if self.startup_failed or self.shutdown_failed: + return + if self.config.lifespan == "auto": + msg = "ASGI 'lifespan' protocol appears unsupported." + self.logger.info(msg) + else: + msg = "Exception in 'lifespan' protocol\n" + self.logger.error(msg, exc_info=exc) + finally: + self.startup_event.set() + self.shutdown_event.set() + + async def send(self, message: LifespanSendMessage) -> None: + assert message["type"] in ( + "lifespan.startup.complete", + "lifespan.startup.failed", + "lifespan.shutdown.complete", + "lifespan.shutdown.failed", + ) + + if message["type"] == "lifespan.startup.complete": + assert not self.startup_event.is_set(), STATE_TRANSITION_ERROR + assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR + self.startup_event.set() + + elif message["type"] == "lifespan.startup.failed": + assert not self.startup_event.is_set(), STATE_TRANSITION_ERROR + assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR + self.startup_event.set() + self.startup_failed = True + if message.get("message"): + self.logger.error(message["message"]) + + elif message["type"] == "lifespan.shutdown.complete": + assert self.startup_event.is_set(), STATE_TRANSITION_ERROR + assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR + self.shutdown_event.set() + + elif message["type"] == "lifespan.shutdown.failed": + assert self.startup_event.is_set(), STATE_TRANSITION_ERROR + assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR + self.shutdown_event.set() + self.shutdown_failed = True + if message.get("message"): + self.logger.error(message["message"]) + + async def receive(self) -> LifespanReceiveMessage: + return await self.receive_queue.get() diff --git a/source/target-vendors/cp312/uvicorn/logging.py b/source/target-vendors/cp312/uvicorn/logging.py new file mode 100644 index 0000000..28c65fc --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/logging.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +import http +import logging +import sys +from copy import copy +from typing import Literal + +from uvicorn._ansi import style as ansi_style + +TRACE_LOG_LEVEL = 5 + + +class ColourizedFormatter(logging.Formatter): + """ + A custom log formatter class that: + + * Outputs the LOG_LEVEL with an appropriate color. + * If a log call includes an `extra={"color_message": ...}` it will be used + for formatting the output, instead of the plain text message. + """ + + level_name_colors = { + TRACE_LOG_LEVEL: lambda level_name: ansi_style(str(level_name), fg="blue"), + logging.DEBUG: lambda level_name: ansi_style(str(level_name), fg="cyan"), + logging.INFO: lambda level_name: ansi_style(str(level_name), fg="green"), + logging.WARNING: lambda level_name: ansi_style(str(level_name), fg="yellow"), + logging.ERROR: lambda level_name: ansi_style(str(level_name), fg="red"), + logging.CRITICAL: lambda level_name: ansi_style(str(level_name), fg="bright_red"), + } + + def __init__( + self, + fmt: str | None = None, + datefmt: str | None = None, + style: Literal["%", "{", "$"] = "%", + use_colors: bool | None = None, + ): + if use_colors in (True, False): + self.use_colors = use_colors + else: + self.use_colors = sys.stdout.isatty() + super().__init__(fmt=fmt, datefmt=datefmt, style=style) + + def color_level_name(self, level_name: str, level_no: int) -> str: + def default(level_name: str) -> str: + return str(level_name) # pragma: no cover + + func = self.level_name_colors.get(level_no, default) + return func(level_name) + + def should_use_colors(self) -> bool: + return True # pragma: no cover + + def formatMessage(self, record: logging.LogRecord) -> str: + recordcopy = copy(record) + levelname = recordcopy.levelname + separator = " " * (8 - len(recordcopy.levelname)) + if self.use_colors: + levelname = self.color_level_name(levelname, recordcopy.levelno) + if "color_message" in recordcopy.__dict__: + recordcopy.msg = recordcopy.__dict__["color_message"] + recordcopy.__dict__["message"] = recordcopy.getMessage() + recordcopy.__dict__["levelprefix"] = levelname + ":" + separator + return super().formatMessage(recordcopy) + + +class DefaultFormatter(ColourizedFormatter): + def should_use_colors(self) -> bool: + return sys.stderr.isatty() # pragma: no cover + + +class AccessFormatter(ColourizedFormatter): + status_code_colours = { + 1: lambda code: ansi_style(str(code), fg="bright_white"), + 2: lambda code: ansi_style(str(code), fg="green"), + 3: lambda code: ansi_style(str(code), fg="yellow"), + 4: lambda code: ansi_style(str(code), fg="red"), + 5: lambda code: ansi_style(str(code), fg="bright_red"), + } + + def get_status_code(self, status_code: int) -> str: + try: + status_phrase = http.HTTPStatus(status_code).phrase + except ValueError: + status_phrase = "" + status_and_phrase = f"{status_code} {status_phrase}" + if self.use_colors: + + def default(code: int) -> str: + return status_and_phrase # pragma: no cover + + func = self.status_code_colours.get(status_code // 100, default) + return func(status_and_phrase) + return status_and_phrase + + def formatMessage(self, record: logging.LogRecord) -> str: + recordcopy = copy(record) + ( + client_addr, + method, + full_path, + http_version, + status_code, + ) = recordcopy.args # type: ignore[misc] + status_code = self.get_status_code(int(status_code)) # type: ignore[arg-type] + request_line = f"{method} {full_path} HTTP/{http_version}" + if self.use_colors: + request_line = ansi_style(request_line, bold=True) + recordcopy.__dict__.update( + { + "client_addr": client_addr, + "request_line": request_line, + "status_code": status_code, + } + ) + return super().formatMessage(recordcopy) diff --git a/source/target-vendors/cp312/uvicorn/loops/__init__.py b/source/target-vendors/cp312/uvicorn/loops/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/uvicorn/loops/asyncio.py b/source/target-vendors/cp312/uvicorn/loops/asyncio.py new file mode 100644 index 0000000..ad6121e --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/loops/asyncio.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +import asyncio +import sys +from collections.abc import Callable + + +def asyncio_loop_factory(use_subprocess: bool = False) -> Callable[[], asyncio.AbstractEventLoop]: + if sys.platform == "win32" and not use_subprocess: + return asyncio.ProactorEventLoop + return asyncio.SelectorEventLoop diff --git a/source/target-vendors/cp312/uvicorn/loops/auto.py b/source/target-vendors/cp312/uvicorn/loops/auto.py new file mode 100644 index 0000000..1908399 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/loops/auto.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +import asyncio +from collections.abc import Callable + + +def auto_loop_factory(use_subprocess: bool = False) -> Callable[[], asyncio.AbstractEventLoop]: + try: + import uvloop # noqa + except ImportError: # pragma: no cover + from uvicorn.loops.asyncio import asyncio_loop_factory as loop_factory + + return loop_factory(use_subprocess=use_subprocess) + else: # pragma: no cover + from uvicorn.loops.uvloop import uvloop_loop_factory + + return uvloop_loop_factory(use_subprocess=use_subprocess) diff --git a/source/target-vendors/cp312/uvicorn/loops/uvloop.py b/source/target-vendors/cp312/uvicorn/loops/uvloop.py new file mode 100644 index 0000000..c6692c5 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/loops/uvloop.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +import asyncio +from collections.abc import Callable + +import uvloop + + +def uvloop_loop_factory(use_subprocess: bool = False) -> Callable[[], asyncio.AbstractEventLoop]: + return uvloop.new_event_loop diff --git a/source/target-vendors/cp312/uvicorn/loops/zuvloop.py b/source/target-vendors/cp312/uvicorn/loops/zuvloop.py new file mode 100644 index 0000000..2bbbc4b --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/loops/zuvloop.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +import asyncio +from collections.abc import Callable + +import zuvloop + + +def zuvloop_loop_factory(use_subprocess: bool = False) -> Callable[[], asyncio.AbstractEventLoop]: + return zuvloop.new_event_loop diff --git a/source/target-vendors/cp312/uvicorn/main.py b/source/target-vendors/cp312/uvicorn/main.py new file mode 100644 index 0000000..854c10b --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/main.py @@ -0,0 +1,656 @@ +from __future__ import annotations + +import asyncio +import logging +import os +import platform +import ssl +import sys +import warnings +from collections.abc import Callable +from configparser import RawConfigParser +from typing import IO, Any, get_args + +import click + +import uvicorn +from uvicorn._types import ASGIApplication +from uvicorn.config import ( + INTERFACES, + LIFESPAN, + LOG_LEVELS, + LOGGING_CONFIG, + SSL_PROTOCOL_VERSION, + STARTUP_FAILURE, + Config, + HTTPProtocolType, + InterfaceType, + LifespanType, + LoopFactoryType, + WSProtocolType, +) +from uvicorn.server import Server +from uvicorn.supervisors import ChangeReload, Multiprocess + +LEVEL_CHOICES = click.Choice(list(LOG_LEVELS.keys())) +LIFESPAN_CHOICES = click.Choice(list(LIFESPAN.keys())) +INTERFACE_CHOICES = click.Choice(INTERFACES) + + +def _metavar_from_type(_type: Any) -> str: + return f"[{'|'.join(key for key in get_args(_type) if key != 'none')}]" + + +logger = logging.getLogger("uvicorn.error") + + +def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> None: + if not value or ctx.resilient_parsing: + return + click.echo( + "Running uvicorn {version} with {py_implementation} {py_version} on {system}".format( # noqa: UP032 + version=uvicorn.__version__, + py_implementation=platform.python_implementation(), + py_version=platform.python_version(), + system=platform.system(), + ) + ) + ctx.exit() + + +@click.command(context_settings={"auto_envvar_prefix": "UVICORN"}) +@click.argument("app", envvar="UVICORN_APP") +@click.option( + "--host", + type=str, + default="127.0.0.1", + help="Bind socket to this host.", + show_default=True, +) +@click.option( + "--port", + type=int, + default=8000, + help="Bind socket to this port. If 0, an available port will be picked.", + show_default=True, +) +@click.option("--uds", type=str, default=None, help="Bind to a UNIX domain socket.") +@click.option("--fd", type=int, default=None, help="Bind to socket from this file descriptor.") +@click.option("--reload", is_flag=True, default=False, help="Enable auto-reload.") +@click.option( + "--reload-dir", + "reload_dirs", + multiple=True, + help="Set reload directories explicitly, instead of using the current working directory.", + type=click.Path(exists=True), +) +@click.option( + "--reload-include", + "reload_includes", + multiple=True, + help="Set glob patterns to include while watching for files. Includes '*.py' " + "by default; these defaults can be overridden with `--reload-exclude`. " + "This option has no effect unless watchfiles is installed.", +) +@click.option( + "--reload-exclude", + "reload_excludes", + multiple=True, + help="Set glob patterns to exclude while watching for files. Includes " + "'.*, .py[cod], .sw.*, ~*' by default; these defaults can be overridden " + "with `--reload-include`. This option has no effect unless watchfiles is " + "installed.", +) +@click.option( + "--reload-delay", + type=float, + default=0.25, + show_default=True, + help="Delay between previous and next check if application needs to be. Defaults to 0.25s.", +) +@click.option( + "--workers", + default=None, + type=int, + help="Number of worker processes. Defaults to the $WEB_CONCURRENCY environment" + " variable if available, or 1. Not valid with --reload.", +) +@click.option( + "--loop", + type=str, + metavar=_metavar_from_type(LoopFactoryType), + default="auto", + help="Event loop factory implementation.", + show_default=True, +) +@click.option( + "--http", + type=str, + metavar=_metavar_from_type(HTTPProtocolType), + default="auto", + help="HTTP protocol implementation.", + show_default=True, +) +@click.option( + "--http2", + is_flag=True, + default=False, + help="Enable HTTP/2 support. Requires --http zttp.", + show_default=True, +) +@click.option( + "--ws", + type=str, + metavar=_metavar_from_type(WSProtocolType), + default="auto", + help="WebSocket protocol implementation.", + show_default=True, +) +@click.option( + "--ws-max-size", + type=int, + default=16777216, + help="WebSocket max size message in bytes", + show_default=True, +) +@click.option( + "--ws-max-queue", + type=int, + default=32, + help="The maximum length of the WebSocket message queue.", + show_default=True, +) +@click.option( + "--ws-ping-interval", + type=float, + default=20.0, + help="WebSocket ping interval in seconds.", + show_default=True, +) +@click.option( + "--ws-ping-timeout", + type=float, + default=20.0, + help="WebSocket ping timeout in seconds.", + show_default=True, +) +@click.option( + "--ws-per-message-deflate", + type=bool, + default=True, + help="WebSocket per-message-deflate compression", + show_default=True, +) +@click.option( + "--lifespan", + type=LIFESPAN_CHOICES, + default="auto", + help="Lifespan implementation.", + show_default=True, +) +@click.option( + "--interface", + type=INTERFACE_CHOICES, + default="auto", + help="Select ASGI3, ASGI2, or WSGI as the application interface.", + show_default=True, +) +@click.option( + "--env-file", + type=click.Path(exists=True), + default=None, + help="Environment configuration file.", + show_default=True, +) +@click.option( + "--log-config", + type=click.Path(exists=True), + default=None, + help="Logging configuration file. Supported formats: .ini, .json, .yaml.", + show_default=True, +) +@click.option( + "--log-level", + type=LEVEL_CHOICES, + default=None, + help="Log level. [default: info]", + show_default=True, +) +@click.option( + "--access-log/--no-access-log", + is_flag=True, + default=True, + help="Enable/Disable access log.", +) +@click.option( + "--use-colors/--no-use-colors", + is_flag=True, + default=None, + help="Enable/Disable colorized logging.", +) +@click.option( + "--proxy-headers/--no-proxy-headers", + is_flag=True, + default=True, + help="Enable/Disable X-Forwarded-Proto, X-Forwarded-For to populate url scheme and remote address info.", +) +@click.option( + "--server-header/--no-server-header", + is_flag=True, + default=True, + help="Enable/Disable default Server header.", +) +@click.option( + "--date-header/--no-date-header", + is_flag=True, + default=True, + help="Enable/Disable default Date header.", +) +@click.option( + "--forwarded-allow-ips", + type=str, + default=None, + help="Comma separated list of IP Addresses, IP Networks, or literals " + "(e.g. UNIX Socket path) to trust with proxy headers. Defaults to the " + "$FORWARDED_ALLOW_IPS environment variable if available, or '127.0.0.1,::1'. " + "The literal '*' means trust everything.", +) +@click.option( + "--root-path", + type=str, + default="", + help="Set the ASGI 'root_path' for applications submounted below a given URL path.", +) +@click.option( + "--limit-concurrency", + type=int, + default=None, + help="Maximum number of concurrent connections or tasks to allow, before issuing HTTP 503 responses.", +) +@click.option( + "--backlog", + type=int, + default=2048, + help="Maximum number of connections to hold in backlog", +) +@click.option( + "--limit-max-requests", + type=int, + default=None, + help="Maximum number of requests to service before terminating the process.", +) +@click.option( + "--limit-max-requests-jitter", + type=int, + default=0, + help="Maximum jitter to add to limit_max_requests." + " Staggers worker restarts to avoid all workers restarting simultaneously.", + show_default=True, +) +@click.option( + "--timeout-keep-alive", + type=int, + default=5, + help="Close Keep-Alive connections if no new data is received within this timeout (in seconds).", + show_default=True, +) +@click.option( + "--timeout-graceful-shutdown", + type=int, + default=None, + help="Maximum number of seconds to wait for graceful shutdown.", +) +@click.option( + "--timeout-worker-healthcheck", + type=int, + default=5, + help="Maximum number of seconds to wait for a worker to respond to a healthcheck.", + show_default=True, +) +@click.option("--ssl-keyfile", type=str, default=None, help="SSL key file", show_default=True) +@click.option( + "--ssl-certfile", + type=str, + default=None, + help="SSL certificate file", + show_default=True, +) +@click.option( + "--ssl-keyfile-password", + type=str, + default=None, + help="SSL keyfile password", + show_default=True, +) +@click.option( + "--ssl-version", + type=int, + default=int(SSL_PROTOCOL_VERSION), + help="SSL version to use (see stdlib ssl module's)", + show_default=True, +) +@click.option( + "--ssl-cert-reqs", + type=int, + default=int(ssl.CERT_NONE), + help="Whether client certificate is required (see stdlib ssl module's)", + show_default=True, +) +@click.option( + "--ssl-ca-certs", + type=str, + default=None, + help="CA certificates file", + show_default=True, +) +@click.option( + "--ssl-ciphers", + type=str, + default=None, + help="Ciphers to use (see stdlib ssl module's). Defaults to OpenSSL's safe defaults.", + show_default=True, +) +@click.option( + "--header", + "headers", + multiple=True, + help="Specify custom default HTTP response headers as a Name:Value pair", +) +@click.option( + "--version", + is_flag=True, + callback=print_version, + expose_value=False, + is_eager=True, + help="Display the uvicorn version and exit.", +) +@click.option( + "--app-dir", + default="", + show_default=True, + help="Look for APP in the specified directory, by adding this to the PYTHONPATH." + " Defaults to the current working directory.", +) +@click.option( + "--h11-max-incomplete-event-size", + "h11_max_incomplete_event_size", + type=int, + default=None, + help="For h11, the maximum number of bytes to buffer of an incomplete event.", +) +@click.option( + "--reset-contextvars", + is_flag=True, + default=False, + help="Run each ASGI request in a fresh contextvars.Context. Hides context set in the lifespan.", + show_default=True, +) +@click.option( + "--factory", + is_flag=True, + default=False, + help="Treat APP as an application factory, i.e. a () -> callable.", + show_default=True, +) +def main( + app: str, + host: str, + port: int, + uds: str, + fd: int, + loop: LoopFactoryType | str, + http: HTTPProtocolType | str, + http2: bool, + ws: WSProtocolType | str, + ws_max_size: int, + ws_max_queue: int, + ws_ping_interval: float, + ws_ping_timeout: float, + ws_per_message_deflate: bool, + lifespan: LifespanType, + interface: InterfaceType, + reload: bool, + reload_dirs: list[str], + reload_includes: list[str], + reload_excludes: list[str], + reload_delay: float, + workers: int, + env_file: str, + log_config: str, + log_level: str, + access_log: bool, + proxy_headers: bool, + server_header: bool, + date_header: bool, + forwarded_allow_ips: str, + root_path: str, + limit_concurrency: int, + backlog: int, + limit_max_requests: int, + limit_max_requests_jitter: int, + timeout_keep_alive: int, + timeout_graceful_shutdown: int | None, + timeout_worker_healthcheck: int, + ssl_keyfile: str, + ssl_certfile: str, + ssl_keyfile_password: str, + ssl_version: int, + ssl_cert_reqs: int, + ssl_ca_certs: str, + ssl_ciphers: str | None, + headers: list[str], + use_colors: bool, + app_dir: str, + h11_max_incomplete_event_size: int | None, + reset_contextvars: bool, + factory: bool, +) -> None: + run( + app, + host=host, + port=port, + uds=uds, + fd=fd, + loop=loop, + http=http, + http2=http2, + ws=ws, + ws_max_size=ws_max_size, + ws_max_queue=ws_max_queue, + ws_ping_interval=ws_ping_interval, + ws_ping_timeout=ws_ping_timeout, + ws_per_message_deflate=ws_per_message_deflate, + lifespan=lifespan, + env_file=env_file, + log_config=LOGGING_CONFIG if log_config is None else log_config, + log_level=log_level, + access_log=access_log, + interface=interface, + reload=reload, + reload_dirs=reload_dirs or None, + reload_includes=reload_includes or None, + reload_excludes=reload_excludes or None, + reload_delay=reload_delay, + workers=workers, + proxy_headers=proxy_headers, + server_header=server_header, + date_header=date_header, + forwarded_allow_ips=forwarded_allow_ips, + root_path=root_path, + limit_concurrency=limit_concurrency, + backlog=backlog, + limit_max_requests=limit_max_requests, + limit_max_requests_jitter=limit_max_requests_jitter, + timeout_keep_alive=timeout_keep_alive, + timeout_graceful_shutdown=timeout_graceful_shutdown, + timeout_worker_healthcheck=timeout_worker_healthcheck, + ssl_keyfile=ssl_keyfile, + ssl_certfile=ssl_certfile, + ssl_keyfile_password=ssl_keyfile_password, + ssl_version=ssl_version, + ssl_cert_reqs=ssl_cert_reqs, + ssl_ca_certs=ssl_ca_certs, + ssl_ciphers=ssl_ciphers, + headers=[header.split(":", 1) for header in headers], # type: ignore[misc] + use_colors=use_colors, + factory=factory, + app_dir=app_dir, + h11_max_incomplete_event_size=h11_max_incomplete_event_size, + reset_contextvars=reset_contextvars, + ) + + +def run( + app: ASGIApplication | Callable[..., Any] | str, + *, + host: str = "127.0.0.1", + port: int = 8000, + uds: str | None = None, + fd: int | None = None, + loop: LoopFactoryType | str = "auto", + http: type[asyncio.Protocol] | HTTPProtocolType | str = "auto", + http2: bool = False, + ws: type[asyncio.Protocol] | WSProtocolType | str = "auto", + ws_max_size: int = 16777216, + ws_max_queue: int = 32, + ws_ping_interval: float | None = 20.0, + ws_ping_timeout: float | None = 20.0, + ws_per_message_deflate: bool = True, + lifespan: LifespanType = "auto", + interface: InterfaceType = "auto", + reload: bool = False, + reload_dirs: list[str] | str | None = None, + reload_includes: list[str] | str | None = None, + reload_excludes: list[str] | str | None = None, + reload_delay: float = 0.25, + workers: int | None = None, + env_file: str | os.PathLike[str] | None = None, + log_config: dict[str, Any] | str | os.PathLike[str] | RawConfigParser | IO[Any] | None = LOGGING_CONFIG, + log_level: str | int | None = None, + access_log: bool = True, + proxy_headers: bool = True, + server_header: bool = True, + date_header: bool = True, + forwarded_allow_ips: list[str] | str | None = None, + root_path: str = "", + limit_concurrency: int | None = None, + backlog: int = 2048, + limit_max_requests: int | None = None, + limit_max_requests_jitter: int = 0, + timeout_keep_alive: int = 5, + timeout_graceful_shutdown: int | None = None, + timeout_worker_healthcheck: int = 5, + ssl_keyfile: str | os.PathLike[str] | None = None, + ssl_certfile: str | os.PathLike[str] | None = None, + ssl_keyfile_password: str | None = None, + ssl_version: int = SSL_PROTOCOL_VERSION, + ssl_cert_reqs: int = ssl.CERT_NONE, + ssl_ca_certs: str | os.PathLike[str] | None = None, + ssl_ciphers: str | None = None, + ssl_context_factory: Callable[[Config, Callable[[], ssl.SSLContext]], ssl.SSLContext] | None = None, + headers: list[tuple[str, str]] | None = None, + use_colors: bool | None = None, + app_dir: str | None = None, + factory: bool = False, + h11_max_incomplete_event_size: int | None = None, + reset_contextvars: bool = False, +) -> None: + if app_dir is not None: + sys.path.insert(0, app_dir) + + config = Config( + app, + host=host, + port=port, + uds=uds, + fd=fd, + loop=loop, + http=http, + http2=http2, + ws=ws, + ws_max_size=ws_max_size, + ws_max_queue=ws_max_queue, + ws_ping_interval=ws_ping_interval, + ws_ping_timeout=ws_ping_timeout, + ws_per_message_deflate=ws_per_message_deflate, + lifespan=lifespan, + interface=interface, + reload=reload, + reload_dirs=reload_dirs, + reload_includes=reload_includes, + reload_excludes=reload_excludes, + reload_delay=reload_delay, + workers=workers, + env_file=env_file, + log_config=log_config, + log_level=log_level, + access_log=access_log, + proxy_headers=proxy_headers, + server_header=server_header, + date_header=date_header, + forwarded_allow_ips=forwarded_allow_ips, + root_path=root_path, + limit_concurrency=limit_concurrency, + backlog=backlog, + limit_max_requests=limit_max_requests, + limit_max_requests_jitter=limit_max_requests_jitter, + timeout_keep_alive=timeout_keep_alive, + timeout_graceful_shutdown=timeout_graceful_shutdown, + timeout_worker_healthcheck=timeout_worker_healthcheck, + ssl_keyfile=ssl_keyfile, + ssl_certfile=ssl_certfile, + ssl_keyfile_password=ssl_keyfile_password, + ssl_version=ssl_version, + ssl_cert_reqs=ssl_cert_reqs, + ssl_ca_certs=ssl_ca_certs, + ssl_ciphers=ssl_ciphers, + ssl_context_factory=ssl_context_factory, + headers=headers, + use_colors=use_colors, + factory=factory, + h11_max_incomplete_event_size=h11_max_incomplete_event_size, + reset_contextvars=reset_contextvars, + ) + if config.reload or config.workers > 1: + if not isinstance(app, str): + logger = logging.getLogger("uvicorn.error") + logger.warning("You must pass the application as an import string to enable 'reload' or 'workers'.") + sys.exit(STARTUP_FAILURE) + else: + config.load_app() + + server = Server(config=config) + + try: + if config.should_reload: + sock = config.bind_socket() + ChangeReload(config, target=server.run, sockets=[sock]).run() + elif config.workers > 1: + sock = config.bind_socket() + Multiprocess(config, sockets=[sock]).run() + else: + server.run() + except KeyboardInterrupt: # pragma: full coverage + pass + finally: + if config.uds and os.path.exists(config.uds): + os.remove(config.uds) # pragma: py-win32 + + if not server.started and not config.should_reload and config.workers == 1: + sys.exit(STARTUP_FAILURE) + + +def __getattr__(name: str) -> Any: + if name == "ServerState": + warnings.warn( + "uvicorn.main.ServerState is deprecated, use uvicorn.server.ServerState instead.", + DeprecationWarning, + ) + from uvicorn.server import ServerState + + return ServerState + raise AttributeError(f"module {__name__} has no attribute {name}") + + +if __name__ == "__main__": + main() # pragma: no cover diff --git a/source/target-vendors/cp312/uvicorn/middleware/__init__.py b/source/target-vendors/cp312/uvicorn/middleware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/uvicorn/middleware/asgi2.py b/source/target-vendors/cp312/uvicorn/middleware/asgi2.py new file mode 100644 index 0000000..4e15d15 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/middleware/asgi2.py @@ -0,0 +1,15 @@ +from uvicorn._types import ( + ASGI2Application, + ASGIReceiveCallable, + ASGISendCallable, + Scope, +) + + +class ASGI2Middleware: + def __init__(self, app: "ASGI2Application"): + self.app = app + + async def __call__(self, scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable") -> None: + instance = self.app(scope) + await instance(receive, send) diff --git a/source/target-vendors/cp312/uvicorn/middleware/message_logger.py b/source/target-vendors/cp312/uvicorn/middleware/message_logger.py new file mode 100644 index 0000000..0174bcc --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/middleware/message_logger.py @@ -0,0 +1,87 @@ +import logging +from typing import Any + +from uvicorn._types import ( + ASGI3Application, + ASGIReceiveCallable, + ASGIReceiveEvent, + ASGISendCallable, + ASGISendEvent, + WWWScope, +) +from uvicorn.logging import TRACE_LOG_LEVEL + +PLACEHOLDER_FORMAT = { + "body": "<{length} bytes>", + "bytes": "<{length} bytes>", + "text": "<{length} chars>", + "headers": "<...>", +} + + +def message_with_placeholders(message: Any) -> Any: + """ + Return an ASGI message, with any body-type content omitted and replaced + with a placeholder. + """ + new_message = message.copy() + for attr in PLACEHOLDER_FORMAT.keys(): + if message.get(attr) is not None: + content = message[attr] + placeholder = PLACEHOLDER_FORMAT[attr].format(length=len(content)) + new_message[attr] = placeholder + return new_message + + +class MessageLoggerMiddleware: + def __init__(self, app: "ASGI3Application"): + self.task_counter = 0 + self.app = app + self.logger = logging.getLogger("uvicorn.asgi") + + def trace(message: Any, *args: Any, **kwargs: Any) -> None: + self.logger.log(TRACE_LOG_LEVEL, message, *args, **kwargs) + + self.logger.trace = trace # type: ignore + + async def __call__( + self, + scope: "WWWScope", + receive: "ASGIReceiveCallable", + send: "ASGISendCallable", + ) -> None: + self.task_counter += 1 + + task_counter = self.task_counter + client = scope.get("client") + prefix = "%s:%d - ASGI" % (client[0], client[1]) if client else "ASGI" + + async def inner_receive() -> "ASGIReceiveEvent": + message = await receive() + logged_message = message_with_placeholders(message) + log_text = "%s [%d] Receive %s" + self.logger.trace( # type: ignore + log_text, prefix, task_counter, logged_message + ) + return message + + async def inner_send(message: "ASGISendEvent") -> None: + logged_message = message_with_placeholders(message) + log_text = "%s [%d] Send %s" + self.logger.trace( # type: ignore + log_text, prefix, task_counter, logged_message + ) + await send(message) + + logged_scope = message_with_placeholders(scope) + log_text = "%s [%d] Started scope=%s" + self.logger.trace(log_text, prefix, task_counter, logged_scope) # type: ignore + try: + await self.app(scope, inner_receive, inner_send) + except BaseException as exc: + log_text = "%s [%d] Raised exception" + self.logger.trace(log_text, prefix, task_counter) # type: ignore + raise exc from None + else: + log_text = "%s [%d] Completed" + self.logger.trace(log_text, prefix, task_counter) # type: ignore diff --git a/source/target-vendors/cp312/uvicorn/middleware/proxy_headers.py b/source/target-vendors/cp312/uvicorn/middleware/proxy_headers.py new file mode 100644 index 0000000..ebd210c --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/middleware/proxy_headers.py @@ -0,0 +1,187 @@ +from __future__ import annotations + +import functools +import ipaddress + +from uvicorn._types import ASGI3Application, ASGIReceiveCallable, ASGISendCallable, Scope + + +class ProxyHeadersMiddleware: + """Middleware for handling known proxy headers + + This middleware can be used when a known proxy is fronting the application, + and is trusted to be properly setting the `X-Forwarded-Proto` and + `X-Forwarded-For` headers with the connecting client information. + + Modifies the `client` and `scheme` information so that they reference + the connecting client, rather that the connecting proxy. + + References: + - + - + """ + + def __init__(self, app: ASGI3Application, trusted_hosts: list[str] | str = "127.0.0.1") -> None: + self.app = app + self.trusted_hosts = _TrustedHosts(trusted_hosts) + + async def __call__(self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: + if scope["type"] == "lifespan": + return await self.app(scope, receive, send) + + client_addr = scope.get("client") + client_host = client_addr[0] if client_addr else None + + if client_host in self.trusted_hosts: + x_forwarded_proto_value: bytes | None = None + x_forwarded_for_values: list[bytes] = [] + for name, value in scope["headers"]: + if name == b"x-forwarded-proto": + x_forwarded_proto_value = value + elif name == b"x-forwarded-for": + x_forwarded_for_values.append(value) + + if x_forwarded_proto_value is not None: + x_forwarded_proto = x_forwarded_proto_value.decode("latin1").strip() + + if x_forwarded_proto in {"http", "https", "ws", "wss"}: + if scope["type"] == "websocket": + scope["scheme"] = x_forwarded_proto.replace("http", "ws") + else: + scope["scheme"] = x_forwarded_proto + + if x_forwarded_for_values: + x_forwarded_for = b", ".join(x_forwarded_for_values).decode("latin1") + host, port = self.trusted_hosts.get_trusted_client_address(x_forwarded_for) + + if host: + # If the x-forwarded-for header is empty then host is an empty string. + # Only set the client if we actually got something usable. + # See: https://github.com/Kludex/uvicorn/issues/1068 + scope["client"] = (host, port) + + return await self.app(scope, receive, send) + + +def _parse_raw_hosts(value: str) -> list[str]: + return [item.strip() for item in value.split(",")] + + +def _parse_host_port(value: str) -> tuple[str, int]: + """Parse a forwarded host value into host and optional port. + + Accepts bare IPs, IPv4 `host:port`, and bracketed IPv6 `[host]:port`. + Any unrecognized or malformed value is treated conservatively and returned + without a port so trust checks do not silently normalize arbitrary input. + """ + + if value.startswith("["): + bracket_end = value.find("]") + if bracket_end == -1: + return value, 0 + + host = value[1:bracket_end] + remainder = value[bracket_end + 1 :] + if not remainder: + return host, 0 + if not remainder.startswith(":"): + return value, 0 + + try: + return host, int(remainder[1:]) + except ValueError: + return host, 0 + + if value.count(":") == 1: + host, port = value.rsplit(":", 1) + try: + return host, int(port) + except ValueError: + return value, 0 + + return value, 0 + + +class _TrustedHosts: + """Container for trusted hosts and networks""" + + def __init__(self, trusted_hosts: list[str] | str) -> None: + self.always_trust: bool = trusted_hosts in ("*", ["*"]) + + self.trusted_literals: set[str] = set() + self.trusted_hosts: set[ipaddress.IPv4Address | ipaddress.IPv6Address] = set() + self.trusted_networks: set[ipaddress.IPv4Network | ipaddress.IPv6Network] = set() + + # Notes: + # - We separate hosts from literals as there are many ways to write + # an IPv6 Address so we need to compare by object. + # - We don't convert IP Address to single host networks (e.g. /32 / 128) as + # it more efficient to do an address lookup in a set than check for + # membership in each network. + # - We still allow literals as it might be possible that we receive a + # something that isn't an IP Address e.g. a unix socket. + + if not self.always_trust: + if isinstance(trusted_hosts, str): + trusted_hosts = _parse_raw_hosts(trusted_hosts) + + for host in trusted_hosts: + # Note: because we always convert invalid IP types to literals it + # is not possible for the user to know they provided a malformed IP + # type - this may lead to unexpected / difficult to debug behaviour. + + if "/" in host: + # Looks like a network + try: + self.trusted_networks.add(ipaddress.ip_network(host)) + except ValueError: + # Was not a valid IP Network + self.trusted_literals.add(host) + else: + try: + self.trusted_hosts.add(ipaddress.ip_address(host)) + except ValueError: + # Was not a valid IP Address + self.trusted_literals.add(host) + + self._trusts = functools.lru_cache(maxsize=4096)(self._compute_trust) + + def __contains__(self, host: str | None) -> bool: + if self.always_trust: + return True + + if not host: + return False + + # Don't cache hosts longer than a DNS name (253); they can't be trusted and would pin huge cache keys. + if len(host) > 253: + return self._compute_trust(host) # pragma: no cover + + return self._trusts(host) + + def _compute_trust(self, host: str) -> bool: + try: + ip = ipaddress.ip_address(host) + return ip in self.trusted_hosts or any(ip in net for net in self.trusted_networks) + except ValueError: + return host in self.trusted_literals + + def get_trusted_client_address(self, x_forwarded_for: str) -> tuple[str, int]: + """Extract the client address from x_forwarded_for header. + + In general this is the first "untrusted" host in the forwarded for list. + """ + x_forwarded_for_hosts = _parse_raw_hosts(x_forwarded_for) + + if self.always_trust: + return _parse_host_port(x_forwarded_for_hosts[0]) + + # Note: each proxy appends to the header list so check it in reverse order + for host_port in reversed(x_forwarded_for_hosts): + host, port = _parse_host_port(host_port) + if host not in self: + return host, port + + # All hosts are trusted meaning that the client was also a trusted proxy + # See https://github.com/Kludex/uvicorn/issues/1068#issuecomment-855371576 + return _parse_host_port(x_forwarded_for_hosts[0]) diff --git a/source/target-vendors/cp312/uvicorn/middleware/wsgi.py b/source/target-vendors/cp312/uvicorn/middleware/wsgi.py new file mode 100644 index 0000000..b2e3cb7 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/middleware/wsgi.py @@ -0,0 +1,199 @@ +from __future__ import annotations + +import asyncio +import concurrent.futures +import io +import sys +import warnings +from collections import deque +from collections.abc import Iterable + +from uvicorn._types import ( + ASGIReceiveCallable, + ASGIReceiveEvent, + ASGISendCallable, + ASGISendEvent, + Environ, + ExcInfo, + HTTPRequestEvent, + HTTPResponseBodyEvent, + HTTPResponseStartEvent, + HTTPScope, + StartResponse, + WSGIApp, +) + + +def build_environ(scope: HTTPScope, message: ASGIReceiveEvent, body: io.BytesIO) -> Environ: + """ + Builds a scope and request message into a WSGI environ object. + """ + script_name = scope.get("root_path", "").encode("utf8").decode("latin1") + path_info = scope["path"].encode("utf8").decode("latin1") + if path_info.startswith(script_name): + path_info = path_info[len(script_name) :] + environ = { + "REQUEST_METHOD": scope["method"], + "SCRIPT_NAME": script_name, + "PATH_INFO": path_info, + "QUERY_STRING": scope["query_string"].decode("ascii"), + "SERVER_PROTOCOL": "HTTP/%s" % scope["http_version"], + "wsgi.version": (1, 0), + "wsgi.url_scheme": scope.get("scheme", "http"), + "wsgi.input": body, + "wsgi.errors": sys.stdout, + "wsgi.multithread": True, + "wsgi.multiprocess": True, + "wsgi.run_once": False, + } + + # Get server name and port - required in WSGI, not in ASGI + server = scope.get("server") + if server is None: + server = ("localhost", 80) + environ["SERVER_NAME"] = server[0] + environ["SERVER_PORT"] = server[1] + + # Get client IP address + client = scope.get("client") + if client is not None: + environ["REMOTE_ADDR"] = client[0] + + # Go through headers and make them into environ entries + for name, value in scope.get("headers", []): + name_str: str = name.decode("latin1") + if name_str == "content-length": + corrected_name = "CONTENT_LENGTH" + elif name_str == "content-type": + corrected_name = "CONTENT_TYPE" + else: + corrected_name = "HTTP_%s" % name_str.upper().replace("-", "_") + # HTTPbis say only ASCII chars are allowed in headers, but we latin1 + # just in case + value_str: str = value.decode("latin1") + if corrected_name in environ: + corrected_name_environ = environ[corrected_name] + assert isinstance(corrected_name_environ, str) + value_str = corrected_name_environ + "," + value_str + environ[corrected_name] = value_str + return environ + + +class _WSGIMiddleware: + def __init__(self, app: WSGIApp, workers: int = 10): + warnings.warn( + "Uvicorn's native WSGI implementation is deprecated, you should switch to a2wsgi (`pip install a2wsgi`).", + DeprecationWarning, + ) + self.app = app + self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=workers) + + async def __call__( + self, + scope: HTTPScope, + receive: ASGIReceiveCallable, + send: ASGISendCallable, + ) -> None: + assert scope["type"] == "http" + instance = WSGIResponder(self.app, self.executor, scope) + await instance(receive, send) + + +class WSGIResponder: + def __init__( + self, + app: WSGIApp, + executor: concurrent.futures.ThreadPoolExecutor, + scope: HTTPScope, + ): + self.app = app + self.executor = executor + self.scope = scope + self.status = None + self.response_headers = None + self.send_event = asyncio.Event() + self.send_queue: deque[ASGISendEvent | None] = deque() + self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() + self.response_started = False + self.exc_info: ExcInfo | None = None + + async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: + message: HTTPRequestEvent = await receive() # type: ignore[assignment] + body = io.BytesIO(message.get("body", b"")) + more_body = message.get("more_body", False) + if more_body: + body.seek(0, io.SEEK_END) + while more_body: + body_message: HTTPRequestEvent = ( + await receive() # type: ignore[assignment] + ) + body.write(body_message.get("body", b"")) + more_body = body_message.get("more_body", False) + body.seek(0) + environ = build_environ(self.scope, message, body) + self.loop = asyncio.get_event_loop() + wsgi = self.loop.run_in_executor(self.executor, self.wsgi, environ, self.start_response) + sender = self.loop.create_task(self.sender(send)) + try: + await asyncio.wait_for(wsgi, None) + finally: + self.send_queue.append(None) + self.send_event.set() + await asyncio.wait_for(sender, None) + if self.exc_info is not None: + raise self.exc_info[0].with_traceback(self.exc_info[1], self.exc_info[2]) + + async def sender(self, send: ASGISendCallable) -> None: + while True: + if self.send_queue: + message = self.send_queue.popleft() + if message is None: + return + await send(message) + else: # pragma: no cover + await self.send_event.wait() + self.send_event.clear() + + def start_response( + self, + status: str, + response_headers: Iterable[tuple[str, str]], + exc_info: ExcInfo | None = None, + ) -> None: + self.exc_info = exc_info + if not self.response_started: + self.response_started = True + status_code_str, _ = status.split(" ", 1) + status_code = int(status_code_str) + headers = [(name.encode("ascii"), value.encode("ascii")) for name, value in response_headers] + http_response_start_event: HTTPResponseStartEvent = { + "type": "http.response.start", + "status": status_code, + "headers": headers, + } + self.send_queue.append(http_response_start_event) + self.loop.call_soon_threadsafe(self.send_event.set) + + def wsgi(self, environ: Environ, start_response: StartResponse) -> None: + for chunk in self.app(environ, start_response): # type: ignore + response_body: HTTPResponseBodyEvent = { + "type": "http.response.body", + "body": chunk, + "more_body": True, + } + self.send_queue.append(response_body) + self.loop.call_soon_threadsafe(self.send_event.set) + + empty_body: HTTPResponseBodyEvent = { + "type": "http.response.body", + "body": b"", + "more_body": False, + } + self.send_queue.append(empty_body) + self.loop.call_soon_threadsafe(self.send_event.set) + + +try: + from a2wsgi import WSGIMiddleware +except ModuleNotFoundError: # pragma: no cover + WSGIMiddleware = _WSGIMiddleware # type: ignore[misc, assignment] diff --git a/source/target-vendors/cp312/uvicorn/protocols/__init__.py b/source/target-vendors/cp312/uvicorn/protocols/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/uvicorn/protocols/http/__init__.py b/source/target-vendors/cp312/uvicorn/protocols/http/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/uvicorn/protocols/http/auto.py b/source/target-vendors/cp312/uvicorn/protocols/http/auto.py new file mode 100644 index 0000000..a14bec1 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/protocols/http/auto.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +import asyncio + +AutoHTTPProtocol: type[asyncio.Protocol] +try: + import httptools # noqa +except ImportError: # pragma: no cover + from uvicorn.protocols.http.h11_impl import H11Protocol + + AutoHTTPProtocol = H11Protocol +else: # pragma: no cover + from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol + + AutoHTTPProtocol = HttpToolsProtocol diff --git a/source/target-vendors/cp312/uvicorn/protocols/http/auto_zttp_impl.py b/source/target-vendors/cp312/uvicorn/protocols/http/auto_zttp_impl.py new file mode 100644 index 0000000..208c1f5 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/protocols/http/auto_zttp_impl.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +import asyncio +from typing import Any, ClassVar + +from uvicorn.config import Config +from uvicorn.protocols.http.zttp_h2_impl import ZttpH2Protocol +from uvicorn.protocols.http.zttp_impl import ZttpProtocol +from uvicorn.protocols.utils import is_ssl +from uvicorn.server import ServerState + +HTTP2_PREFACE = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" + + +class AutoZttpProtocol(asyncio.Protocol): + """Dispatches each new connection to the HTTP/1.1 or HTTP/2 zttp protocol. + + Over TLS the choice is made from the ALPN result as soon as the connection + is made. On cleartext connections the first bytes are sniffed for the + HTTP/2 preface (prior-knowledge h2c); anything else is HTTP/1.1. + """ + + alpn_protocols: ClassVar[list[str]] = ["h2", "http/1.1"] + + def __init__( + self, + config: Config, + server_state: ServerState, + app_state: dict[str, Any], + _loop: asyncio.AbstractEventLoop | None = None, + ) -> None: + if not config.loaded: + config.load() + + self.config = config + self.server_state = server_state + self.app_state = app_state + self.loop = _loop or asyncio.get_event_loop() + self.connections = server_state.connections + self.transport: asyncio.Transport = None # type: ignore[assignment] + self.timeout_task: asyncio.TimerHandle | None = None + self.buffer = b"" + + def connection_made( # type: ignore[override] + self, transport: asyncio.Transport + ) -> None: + self.transport = transport + if is_ssl(transport): + ssl_object = transport.get_extra_info("ssl_object") + alpn = ssl_object.selected_alpn_protocol() if ssl_object is not None else None + self._switch(http2=alpn == "h2") + return + self.connections.add(self) + self.timeout_task = self.loop.call_later(self.config.timeout_keep_alive, self.transport.close) + + def connection_lost(self, exc: Exception | None) -> None: + self.connections.discard(self) + if self.timeout_task is not None: + self.timeout_task.cancel() + self.timeout_task = None + + def eof_received(self) -> None: + pass + + def shutdown(self) -> None: + self.transport.close() + + def data_received(self, data: bytes) -> None: + self.buffer += data + if len(self.buffer) < len(HTTP2_PREFACE) and HTTP2_PREFACE.startswith(self.buffer): + return + self.connections.discard(self) + if self.timeout_task is not None: + self.timeout_task.cancel() + self.timeout_task = None + self._switch(http2=self.buffer.startswith(HTTP2_PREFACE), initial_data=self.buffer) + + def _switch(self, http2: bool, initial_data: bytes = b"") -> None: + protocol_class: type[asyncio.Protocol] = ZttpH2Protocol if http2 else ZttpProtocol + protocol = protocol_class( # type: ignore[call-arg] + config=self.config, + server_state=self.server_state, + app_state=self.app_state, + _loop=self.loop, + ) + self.transport.set_protocol(protocol) + protocol.connection_made(self.transport) + if initial_data: + protocol.data_received(initial_data) diff --git a/source/target-vendors/cp312/uvicorn/protocols/http/flow_control.py b/source/target-vendors/cp312/uvicorn/protocols/http/flow_control.py new file mode 100644 index 0000000..2d1b5fa --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/protocols/http/flow_control.py @@ -0,0 +1,54 @@ +import asyncio + +from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope + +CLOSE_HEADER = (b"connection", b"close") + +HIGH_WATER_LIMIT = 65536 + + +class FlowControl: + def __init__(self, transport: asyncio.Transport) -> None: + self._transport = transport + self.read_paused = False + self.write_paused = False + self._is_writable_event = asyncio.Event() + self._is_writable_event.set() + + async def drain(self) -> None: + await self._is_writable_event.wait() # pragma: full coverage + + def pause_reading(self) -> None: + if not self.read_paused: + self.read_paused = True + self._transport.pause_reading() + + def resume_reading(self) -> None: + if self.read_paused: + self.read_paused = False + self._transport.resume_reading() + + def pause_writing(self) -> None: + if not self.write_paused: # pragma: full coverage + self.write_paused = True + self._is_writable_event.clear() + + def resume_writing(self) -> None: + if self.write_paused: # pragma: full coverage + self.write_paused = False + self._is_writable_event.set() + + +async def service_unavailable(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: + await send( + { + "type": "http.response.start", + "status": 503, + "headers": [ + (b"content-type", b"text/plain; charset=utf-8"), + (b"content-length", b"19"), + (b"connection", b"close"), + ], + } + ) + await send({"type": "http.response.body", "body": b"Service Unavailable", "more_body": False}) diff --git a/source/target-vendors/cp312/uvicorn/protocols/http/h11_impl.py b/source/target-vendors/cp312/uvicorn/protocols/http/h11_impl.py new file mode 100644 index 0000000..e863f8b --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/protocols/http/h11_impl.py @@ -0,0 +1,540 @@ +from __future__ import annotations + +import asyncio +import contextvars +import http +import logging +import sys +from collections.abc import Callable +from typing import Any, Literal +from urllib.parse import unquote + +import h11 +from h11._connection import DEFAULT_MAX_INCOMPLETE_EVENT_SIZE + +from uvicorn._types import ( + ASGI3Application, + ASGIReceiveEvent, + ASGISendEvent, + HTTPRequestEvent, + HTTPResponseBodyEvent, + HTTPResponseStartEvent, + HTTPScope, +) +from uvicorn.config import Config +from uvicorn.logging import TRACE_LOG_LEVEL +from uvicorn.protocols.http.flow_control import CLOSE_HEADER, HIGH_WATER_LIMIT, FlowControl, service_unavailable +from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, is_ssl +from uvicorn.server import ServerState + + +def _get_status_phrase(status_code: int) -> bytes: + try: + return http.HTTPStatus(status_code).phrase.encode() + except ValueError: + return b"" + + +STATUS_PHRASES = {status_code: _get_status_phrase(status_code) for status_code in range(100, 600)} + + +class H11Protocol(asyncio.Protocol): + def __init__( + self, + config: Config, + server_state: ServerState, + app_state: dict[str, Any], + _loop: asyncio.AbstractEventLoop | None = None, + ) -> None: + if not config.loaded: + config.load() + + self.config = config + self.app = config.loaded_app + self.loop = _loop or asyncio.get_event_loop() + self.logger = logging.getLogger("uvicorn.error") + self.access_logger = logging.getLogger("uvicorn.access") + self.access_log = self.access_logger.hasHandlers() + self.conn = h11.Connection( + h11.SERVER, + config.h11_max_incomplete_event_size + if config.h11_max_incomplete_event_size is not None + else DEFAULT_MAX_INCOMPLETE_EVENT_SIZE, + ) + self.ws_protocol_class = config.ws_protocol_class + self.root_path = config.root_path + self.asgi_version = config.asgi_version + self.limit_concurrency = config.limit_concurrency + self.app_state = app_state + + # Timeouts + self.timeout_keep_alive_task: asyncio.TimerHandle | None = None + self.timeout_keep_alive = config.timeout_keep_alive + + # Shared server state + self.server_state = server_state + self.connections = server_state.connections + self.tasks = server_state.tasks + + # Per-connection state + self.transport: asyncio.Transport = None # type: ignore[assignment] + self.flow: FlowControl = None # type: ignore[assignment] + self.server: tuple[str, int | None] | None = None + self.client: tuple[str, int] | None = None + self.scheme: Literal["http", "https"] | None = None + + # Per-request state + self.scope: HTTPScope = None # type: ignore[assignment] + self.headers: list[tuple[bytes, bytes]] = None # type: ignore[assignment] + self.cycle: RequestResponseCycle = None # type: ignore[assignment] + + # Protocol interface + def connection_made( # type: ignore[override] + self, transport: asyncio.Transport + ) -> None: + self.connections.add(self) + + self.transport = transport + self.flow = FlowControl(transport) + self.server = get_local_addr(transport) + self.client = get_remote_addr(transport) + self.scheme = "https" if is_ssl(transport) else "http" + + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection made", prefix) + + def connection_lost(self, exc: Exception | None) -> None: + self.connections.discard(self) + + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection lost", prefix) + + if self.cycle and not self.cycle.response_complete: + self.cycle.disconnected = True + if self.conn.our_state != h11.ERROR: + event = h11.ConnectionClosed() + try: + self.conn.send(event) + except h11.LocalProtocolError: + # Premature client disconnect + pass + + if self.cycle is not None: + self.cycle.message_event.set() + if self.flow is not None: + self.flow.resume_writing() + if exc is None: + self.transport.close() + self._unset_keepalive_if_required() + + def eof_received(self) -> None: + pass + + def _unset_keepalive_if_required(self) -> None: + if self.timeout_keep_alive_task is not None: + self.timeout_keep_alive_task.cancel() + self.timeout_keep_alive_task = None + + def _get_upgrade(self) -> bytes | None: + connection = [] + upgrade = None + for name, value in self.headers: + if name == b"connection": + connection = [token.lower().strip() for token in value.split(b",")] + if name == b"upgrade": + upgrade = value.lower() + if b"upgrade" in connection: + return upgrade + return None + + def _should_upgrade_to_ws(self) -> bool: + if self.ws_protocol_class is None: + return False + return True + + def _unsupported_upgrade_warning(self) -> None: + msg = "Unsupported upgrade request." + self.logger.warning(msg) + if not self._should_upgrade_to_ws(): + msg = "No supported WebSocket library detected. Please use \"pip install 'uvicorn[standard]'\", or install 'websockets' or 'wsproto' manually." # noqa: E501 + self.logger.warning(msg) + + def _should_upgrade(self) -> bool: + upgrade = self._get_upgrade() + if upgrade == b"websocket" and self._should_upgrade_to_ws(): + return True + if upgrade is not None: + self._unsupported_upgrade_warning() + return False + + def data_received(self, data: bytes) -> None: + self._unset_keepalive_if_required() + + self.conn.receive_data(data) + self.handle_events() + + def handle_events(self) -> None: + while True: + try: + event = self.conn.next_event() + except h11.RemoteProtocolError: + msg = "Invalid HTTP request received." + self.logger.warning(msg) + self.send_400_response(msg) + return + + if event is h11.NEED_DATA: + break + + elif event is h11.PAUSED: + # This case can occur in HTTP pipelining, so we need to + # stop reading any more data, and ensure that at the end + # of the active request/response cycle we handle any + # events that have been buffered up. + self.flow.pause_reading() + break + + elif isinstance(event, h11.Request): + # Pipelined HTTP requests and WebSocket upgrades may be processed after the keep-alive timer is armed. + self._unset_keepalive_if_required() + + self.headers = [(key.lower(), value) for key, value in event.headers] + raw_path, _, query_string = event.target.partition(b"?") + path = unquote(raw_path.decode("ascii")) + full_path = self.root_path + path + full_raw_path = self.root_path.encode("ascii") + raw_path + self.scope = { + "type": "http", + "asgi": {"version": self.asgi_version, "spec_version": "2.3"}, + "http_version": event.http_version.decode("ascii"), + "server": self.server, + "client": self.client, + "scheme": self.scheme, # type: ignore[typeddict-item] + "method": event.method.decode("ascii"), + "root_path": self.root_path, + "path": full_path, + "raw_path": full_raw_path, + "query_string": query_string, + "headers": self.headers, + "state": self.app_state.copy(), + } + if self._should_upgrade(): + self.handle_websocket_upgrade(event) + return + + # Handle 503 responses when 'limit_concurrency' is exceeded. + if self.limit_concurrency is not None and ( + len(self.connections) >= self.limit_concurrency or len(self.tasks) >= self.limit_concurrency + ): + app = service_unavailable + message = "Exceeded concurrency limit." + self.logger.warning(message) + else: + app = self.app + + self.cycle = RequestResponseCycle( + scope=self.scope, + conn=self.conn, + transport=self.transport, + flow=self.flow, + logger=self.logger, + access_logger=self.access_logger, + access_log=self.access_log, + default_headers=self.server_state.default_headers, + message_event=asyncio.Event(), + on_response=self.on_response_complete, + ) + if self.config.reset_contextvars: + # Opt-in workaround for https://github.com/python/cpython/issues/140947: + # asyncio can leak context vars between tasks. Hides context set in the + # lifespan or by external instrumentation. + if sys.version_info >= (3, 11): # pragma: py-lt-311 + task = self.loop.create_task(self.cycle.run_asgi(app), context=contextvars.Context()) + else: # pragma: py-gte-311 + task = contextvars.Context().run(self.loop.create_task, self.cycle.run_asgi(app)) + else: + task = self.loop.create_task(self.cycle.run_asgi(app)) + task.add_done_callback(self.tasks.discard) + self.tasks.add(task) + + elif isinstance(event, h11.Data): + if self.conn.our_state is h11.DONE: + continue + self.cycle.body += event.data + if len(self.cycle.body) > HIGH_WATER_LIMIT: + self.flow.pause_reading() + self.cycle.message_event.set() + + elif isinstance(event, h11.EndOfMessage): + if self.conn.our_state is h11.DONE: + self.transport.resume_reading() + self.conn.start_next_cycle() + continue + self.cycle.more_body = False + self.cycle.message_event.set() + if self.conn.their_state == h11.MUST_CLOSE: + break + + def handle_websocket_upgrade(self, event: h11.Request) -> None: + if self.logger.level <= TRACE_LOG_LEVEL: # pragma: full coverage + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sUpgrading to WebSocket", prefix) + + self.connections.discard(self) + output = [event.method, b" ", event.target, b" HTTP/1.1\r\n"] + for name, value in self.headers: + output += [name, b": ", value, b"\r\n"] + output.append(b"\r\n") + protocol = self.ws_protocol_class( # type: ignore[call-arg, misc] + config=self.config, + server_state=self.server_state, + app_state=self.app_state, + ) + protocol.connection_made(self.transport) + protocol.data_received(b"".join(output)) + self.transport.set_protocol(protocol) + + def send_400_response(self, msg: str) -> None: + reason = STATUS_PHRASES[400] + headers: list[tuple[bytes, bytes]] = [ + (b"content-type", b"text/plain; charset=utf-8"), + (b"connection", b"close"), + ] + event = h11.Response(status_code=400, headers=headers, reason=reason) + output = self.conn.send(event) + self.transport.write(output) + + output = self.conn.send(event=h11.Data(data=msg.encode("ascii"))) + self.transport.write(output) + + output = self.conn.send(event=h11.EndOfMessage()) + self.transport.write(output) + + self.transport.close() + + def on_response_complete(self) -> None: + self.server_state.total_requests += 1 + + if self.transport.is_closing(): + return + + # Set a short Keep-Alive timeout. + self._unset_keepalive_if_required() + + self.timeout_keep_alive_task = self.loop.call_later(self.timeout_keep_alive, self.timeout_keep_alive_handler) + + # Unpause data reads if needed. + self.flow.resume_reading() + + # Unblock any pipelined events. + if self.conn.our_state is h11.DONE and self.conn.their_state is h11.DONE: + self.conn.start_next_cycle() + self.handle_events() + + def shutdown(self) -> None: + """ + Called by the server to commence a graceful shutdown. + """ + if self.cycle is None or self.cycle.response_complete: + event = h11.ConnectionClosed() + self.conn.send(event) + self.transport.close() + else: + self.cycle.keep_alive = False + + def pause_writing(self) -> None: + """ + Called by the transport when the write buffer exceeds the high water mark. + """ + self.flow.pause_writing() # pragma: full coverage + + def resume_writing(self) -> None: + """ + Called by the transport when the write buffer drops below the low water mark. + """ + self.flow.resume_writing() # pragma: full coverage + + def timeout_keep_alive_handler(self) -> None: + """ + Called on a keep-alive connection if no new data is received after a short + delay. + """ + if not self.transport.is_closing(): + event = h11.ConnectionClosed() + self.conn.send(event) + self.transport.close() + + +class RequestResponseCycle: + def __init__( + self, + scope: HTTPScope, + conn: h11.Connection, + transport: asyncio.Transport, + flow: FlowControl, + logger: logging.Logger, + access_logger: logging.Logger, + access_log: bool, + default_headers: list[tuple[bytes, bytes]], + message_event: asyncio.Event, + on_response: Callable[..., None], + ) -> None: + self.scope = scope + self.conn = conn + self.transport = transport + self.flow = flow + self.logger = logger + self.access_logger = access_logger + self.access_log = access_log + self.default_headers = default_headers + self.message_event = message_event + self.on_response = on_response + + # Connection state + self.disconnected = False + self.keep_alive = True + self.waiting_for_100_continue = conn.they_are_waiting_for_100_continue + + # Request state + self.body = bytearray() + self.more_body = True + + # Response state + self.response_started = False + self.response_complete = False + + # ASGI exception wrapper + async def run_asgi(self, app: ASGI3Application) -> None: + try: + result = await app( # type: ignore[func-returns-value] + self.scope, self.receive, self.send + ) + except BaseException as exc: + msg = "Exception in ASGI application\n" + self.logger.error(msg, exc_info=exc) + if not self.response_started: + await self.send_500_response() + else: + self.transport.close() + else: + if result is not None: + msg = "ASGI callable should return None, but returned '%s'." + self.logger.error(msg, result) + self.transport.close() + elif not self.response_started and not self.disconnected: + msg = "ASGI callable returned without starting response." + self.logger.error(msg) + await self.send_500_response() + elif not self.response_complete and not self.disconnected: + msg = "ASGI callable returned without completing response." + self.logger.error(msg) + self.transport.close() + finally: + self.on_response = lambda: None + + async def send_500_response(self) -> None: + response_start_event: HTTPResponseStartEvent = { + "type": "http.response.start", + "status": 500, + "headers": [ + (b"content-type", b"text/plain; charset=utf-8"), + (b"connection", b"close"), + ], + } + await self.send(response_start_event) + response_body_event: HTTPResponseBodyEvent = { + "type": "http.response.body", + "body": b"Internal Server Error", + "more_body": False, + } + await self.send(response_body_event) + + # ASGI interface + async def send(self, message: ASGISendEvent) -> None: + if self.flow.write_paused and not self.disconnected: + await self.flow.drain() # pragma: full coverage + + if self.disconnected: + return # pragma: full coverage + + if not self.response_started: + # Sending response status line and headers + if message["type"] != "http.response.start": + raise RuntimeError(f"Expected ASGI message 'http.response.start', but got '{message['type']}'.") + + self.response_started = True + self.waiting_for_100_continue = False + + status = message["status"] + headers = self.default_headers + list(message.get("headers", [])) + + if CLOSE_HEADER in self.scope["headers"] and CLOSE_HEADER not in headers: + headers = headers + [CLOSE_HEADER] + + if self.access_log: + self.access_logger.info( + '%s - "%s %s HTTP/%s" %d', + get_client_addr(self.scope), + self.scope["method"], + get_path_with_query_string(self.scope), + self.scope["http_version"], + status, + ) + + # Write response status line and headers + reason = STATUS_PHRASES[status] + response = h11.Response(status_code=status, headers=headers, reason=reason) + output = self.conn.send(event=response) + self.transport.write(output) + + elif not self.response_complete: + # Sending response body + if message["type"] != "http.response.body": + raise RuntimeError(f"Expected ASGI message 'http.response.body', but got '{message['type']}'.") + + body = message.get("body", b"") + more_body = message.get("more_body", False) + + # Write response body + data = b"" if self.scope["method"] == "HEAD" else body + output = self.conn.send(event=h11.Data(data=data)) + self.transport.write(output) + + # Handle response completion + if not more_body: + self.response_complete = True + self.message_event.set() + output = self.conn.send(event=h11.EndOfMessage()) + self.transport.write(output) + + else: + # Response already sent + raise RuntimeError(f"Unexpected ASGI message '{message['type']}' sent, after response already completed.") + + if self.response_complete: + if self.conn.our_state is h11.MUST_CLOSE or not self.keep_alive: + self.conn.send(event=h11.ConnectionClosed()) + self.transport.close() + self.on_response() + + async def receive(self) -> ASGIReceiveEvent: + if self.waiting_for_100_continue and not self.transport.is_closing(): + headers: list[tuple[str, str]] = [] + event = h11.InformationalResponse(status_code=100, headers=headers, reason="Continue") + output = self.conn.send(event=event) + self.transport.write(output) + self.waiting_for_100_continue = False + + if not self.disconnected and not self.response_complete: + self.flow.resume_reading() + await self.message_event.wait() + self.message_event.clear() + + if self.disconnected or self.response_complete: + return {"type": "http.disconnect"} + + message: HTTPRequestEvent = {"type": "http.request", "body": bytes(self.body), "more_body": self.more_body} + self.body = bytearray() + return message diff --git a/source/target-vendors/cp312/uvicorn/protocols/http/httptools_impl.py b/source/target-vendors/cp312/uvicorn/protocols/http/httptools_impl.py new file mode 100644 index 0000000..dc95e23 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/protocols/http/httptools_impl.py @@ -0,0 +1,581 @@ +from __future__ import annotations + +import asyncio +import contextvars +import http +import logging +import re +import sys +import urllib +from asyncio.events import TimerHandle +from collections import deque +from collections.abc import Callable +from typing import Any, Literal + +import httptools + +from uvicorn._types import ( + ASGI3Application, + ASGIReceiveEvent, + ASGISendEvent, + HTTPRequestEvent, + HTTPScope, +) +from uvicorn.config import Config +from uvicorn.logging import TRACE_LOG_LEVEL +from uvicorn.protocols.http.flow_control import HIGH_WATER_LIMIT, FlowControl, service_unavailable +from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, is_ssl +from uvicorn.server import ServerState + +HEADER_RE = re.compile(b'[\x00-\x1f\x7f()<>@,;:\\[\\]={} \t\\\\"]') +HEADER_VALUE_RE = re.compile(b"[\x00-\x08\x0a-\x1f\x7f]") + + +def _get_status_line(status_code: int) -> bytes: + try: + phrase = http.HTTPStatus(status_code).phrase.encode() + except ValueError: + phrase = b"" + return b"".join([b"HTTP/1.1 ", str(status_code).encode(), b" ", phrase, b"\r\n"]) + + +STATUS_LINE = {status_code: _get_status_line(status_code) for status_code in range(100, 600)} + + +class HttpToolsProtocol(asyncio.Protocol): + def __init__( + self, + config: Config, + server_state: ServerState, + app_state: dict[str, Any], + _loop: asyncio.AbstractEventLoop | None = None, + ) -> None: + if not config.loaded: + config.load() + + self.config = config + self.app = config.loaded_app + self.loop = _loop or asyncio.get_event_loop() + self.logger = logging.getLogger("uvicorn.error") + self.access_logger = logging.getLogger("uvicorn.access") + self.access_log = self.access_logger.hasHandlers() + self.parser = httptools.HttpRequestParser(self) + + try: + # Enable dangerous leniencies to allow server to a response on the first request from a pipelined request. + self.parser.set_dangerous_leniencies(lenient_data_after_close=True) + except AttributeError: # pragma: no cover + # httptools < 0.6.3 + pass + + self.ws_protocol_class = config.ws_protocol_class + self.root_path = config.root_path + self.asgi_version = config.asgi_version + self.limit_concurrency = config.limit_concurrency + self.app_state = app_state + + # Timeouts + self.timeout_keep_alive_task: TimerHandle | None = None + self.timeout_keep_alive = config.timeout_keep_alive + + # Global state + self.server_state = server_state + self.connections = server_state.connections + self.tasks = server_state.tasks + + # Per-connection state + self.transport: asyncio.Transport = None # type: ignore[assignment] + self.flow: FlowControl = None # type: ignore[assignment] + self.server: tuple[str, int | None] | None = None + self.client: tuple[str, int] | None = None + self.scheme: Literal["http", "https"] | None = None + self.pipeline: deque[tuple[RequestResponseCycle, ASGI3Application]] = deque() + + # Per-request state + self.scope: HTTPScope = None # type: ignore[assignment] + self.headers: list[tuple[bytes, bytes]] = None # type: ignore[assignment] + self.expect_100_continue = False + self.cycle: RequestResponseCycle = None # type: ignore[assignment] + + # Protocol interface + def connection_made( # type: ignore[override] + self, transport: asyncio.Transport + ) -> None: + self.connections.add(self) + + self.transport = transport + self.flow = FlowControl(transport) + self.server = get_local_addr(transport) + self.client = get_remote_addr(transport) + self.scheme = "https" if is_ssl(transport) else "http" + + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection made", prefix) + + def connection_lost(self, exc: Exception | None) -> None: + self.connections.discard(self) + + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection lost", prefix) + + if self.cycle and not self.cycle.response_complete: + self.cycle.disconnected = True + if self.cycle is not None: + self.cycle.message_event.set() + if self.flow is not None: + self.flow.resume_writing() + if exc is None: + self.transport.close() + self._unset_keepalive_if_required() + + self.parser = None # type: ignore[assignment] + + def eof_received(self) -> None: + pass + + def _unset_keepalive_if_required(self) -> None: + if self.timeout_keep_alive_task is not None: + self.timeout_keep_alive_task.cancel() + self.timeout_keep_alive_task = None + + def _get_upgrade(self) -> bytes | None: + connection = [] + upgrade = None + for name, value in self.headers: + if name == b"connection": + connection = [token.lower().strip() for token in value.split(b",")] + if name == b"upgrade": + upgrade = value.lower() + if b"upgrade" in connection: + return upgrade + return None # pragma: full coverage + + def _should_upgrade_to_ws(self) -> bool: + if self.ws_protocol_class is None: + return False + return True + + def _unsupported_upgrade_warning(self) -> None: + self.logger.warning("Unsupported upgrade request.") + if not self._should_upgrade_to_ws(): + msg = "No supported WebSocket library detected. Please use \"pip install 'uvicorn[standard]'\", or install 'websockets' or 'wsproto' manually." # noqa: E501 + self.logger.warning(msg) + + def _should_upgrade(self) -> bool: + upgrade = self._get_upgrade() + return upgrade == b"websocket" and self._should_upgrade_to_ws() + + def data_received(self, data: bytes) -> None: + self._unset_keepalive_if_required() + + try: + self.parser.feed_data(data) + except httptools.HttpParserError: + msg = "Invalid HTTP request received." + self.logger.warning(msg) + self.send_400_response(msg) + return + except httptools.HttpParserUpgrade: + if self._should_upgrade(): + self.handle_websocket_upgrade() + else: + self._unsupported_upgrade_warning() + + def handle_websocket_upgrade(self) -> None: + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sUpgrading to WebSocket", prefix) + + self.connections.discard(self) + method = self.scope["method"].encode() + output = [method, b" ", self.url, b" HTTP/1.1\r\n"] + for name, value in self.scope["headers"]: + output += [name, b": ", value, b"\r\n"] + output.append(b"\r\n") + protocol = self.ws_protocol_class( # type: ignore[call-arg, misc] + config=self.config, + server_state=self.server_state, + app_state=self.app_state, + ) + protocol.connection_made(self.transport) + protocol.data_received(b"".join(output)) + self.transport.set_protocol(protocol) + + def send_400_response(self, msg: str) -> None: + content = [STATUS_LINE[400]] + for name, value in self.server_state.default_headers: + content.extend([name, b": ", value, b"\r\n"]) # pragma: full coverage + content.extend( + [ + b"content-type: text/plain; charset=utf-8\r\n", + b"content-length: " + str(len(msg)).encode("ascii") + b"\r\n", + b"connection: close\r\n", + b"\r\n", + msg.encode("ascii"), + ] + ) + self.transport.write(b"".join(content)) + self.transport.close() + + def on_message_begin(self) -> None: + self.url = b"" + self.expect_100_continue = False + self.headers = [] + self.scope = { # type: ignore[typeddict-item] + "type": "http", + "asgi": {"version": self.asgi_version, "spec_version": "2.3"}, + "http_version": "1.1", + "server": self.server, + "client": self.client, + "scheme": self.scheme, # type: ignore[typeddict-item] + "root_path": self.root_path, + "headers": self.headers, + "state": self.app_state.copy(), + } + + # Parser callbacks + def on_url(self, url: bytes) -> None: + self.url += url + + def on_header(self, name: bytes, value: bytes) -> None: + name = name.lower() + if name == b"expect" and value.lower() == b"100-continue": + self.expect_100_continue = True + self.headers.append((name, value)) + + def on_headers_complete(self) -> None: + http_version = self.parser.get_http_version() + method = self.parser.get_method() + self.scope["method"] = method.decode("ascii") + if http_version != "1.1": + self.scope["http_version"] = http_version + if self.parser.should_upgrade() and self._should_upgrade(): + return + parsed_url = httptools.parse_url(self.url) + raw_path = parsed_url.path + path = raw_path.decode("ascii") + if "%" in path: + path = urllib.parse.unquote(path) + full_path = self.root_path + path + full_raw_path = self.root_path.encode("ascii") + raw_path + self.scope["path"] = full_path + self.scope["raw_path"] = full_raw_path + self.scope["query_string"] = parsed_url.query or b"" + + # Handle 503 responses when 'limit_concurrency' is exceeded. + if self.limit_concurrency is not None and ( + len(self.connections) >= self.limit_concurrency or len(self.tasks) >= self.limit_concurrency + ): + app = service_unavailable + message = "Exceeded concurrency limit." + self.logger.warning(message) + else: + app = self.app + + existing_cycle = self.cycle + self.cycle = RequestResponseCycle( + scope=self.scope, + transport=self.transport, + flow=self.flow, + logger=self.logger, + access_logger=self.access_logger, + access_log=self.access_log, + default_headers=self.server_state.default_headers, + message_event=asyncio.Event(), + expect_100_continue=self.expect_100_continue, + keep_alive=http_version != "1.0" and self.parser.should_keep_alive(), + on_response=self.on_response_complete, + ) + if existing_cycle is None or existing_cycle.response_complete: + # Standard case - start processing the request. + self._start_asgi_task(self.cycle, app) + else: + # Pipelined HTTP requests need to be queued up. + self.flow.pause_reading() + self.pipeline.appendleft((self.cycle, app)) + + def _start_asgi_task(self, cycle: RequestResponseCycle, app: ASGI3Application) -> None: + if self.config.reset_contextvars: + # Opt-in workaround for https://github.com/python/cpython/issues/140947: + # asyncio can leak context vars between tasks. Hides context set in the + # lifespan or by external instrumentation. + if sys.version_info >= (3, 11): # pragma: py-lt-311 + task = self.loop.create_task(cycle.run_asgi(app), context=contextvars.Context()) + else: # pragma: py-gte-311 + task = contextvars.Context().run(self.loop.create_task, cycle.run_asgi(app)) + else: + task = self.loop.create_task(cycle.run_asgi(app)) + task.add_done_callback(self.tasks.discard) + self.tasks.add(task) + + def on_body(self, body: bytes) -> None: + if (self.parser.should_upgrade() and self._should_upgrade()) or self.cycle.response_complete: + return + self.cycle.body += body + if len(self.cycle.body) > HIGH_WATER_LIMIT: + self.flow.pause_reading() + self.cycle.message_event.set() + + def on_message_complete(self) -> None: + if (self.parser.should_upgrade() and self._should_upgrade()) or self.cycle.response_complete: + return + self.cycle.more_body = False + self.cycle.message_event.set() + + def on_response_complete(self) -> None: + # Callback for pipelined HTTP requests to be started. + self.server_state.total_requests += 1 + + if self.transport.is_closing(): + return + + self._unset_keepalive_if_required() + + # Unpause data reads if needed. + self.flow.resume_reading() + + # Unblock any pipelined events. If there are none, arm the + # Keep-Alive timeout instead. + if self.pipeline: + cycle, app = self.pipeline.pop() + self._start_asgi_task(cycle, app) + else: + self.timeout_keep_alive_task = self.loop.call_later( + self.timeout_keep_alive, self.timeout_keep_alive_handler + ) + + def shutdown(self) -> None: + """ + Called by the server to commence a graceful shutdown. + """ + if self.cycle is None or self.cycle.response_complete: + self.transport.close() + else: + self.cycle.keep_alive = False + + def pause_writing(self) -> None: + """ + Called by the transport when the write buffer exceeds the high water mark. + """ + self.flow.pause_writing() # pragma: full coverage + + def resume_writing(self) -> None: + """ + Called by the transport when the write buffer drops below the low water mark. + """ + self.flow.resume_writing() # pragma: full coverage + + def timeout_keep_alive_handler(self) -> None: + """ + Called on a keep-alive connection if no new data is received after a short + delay. + """ + if not self.transport.is_closing(): + self.transport.close() + + +class RequestResponseCycle: + def __init__( + self, + scope: HTTPScope, + transport: asyncio.Transport, + flow: FlowControl, + logger: logging.Logger, + access_logger: logging.Logger, + access_log: bool, + default_headers: list[tuple[bytes, bytes]], + message_event: asyncio.Event, + expect_100_continue: bool, + keep_alive: bool, + on_response: Callable[..., None], + ): + self.scope = scope + self.transport = transport + self.flow = flow + self.logger = logger + self.access_logger = access_logger + self.access_log = access_log + self.default_headers = default_headers + self.message_event = message_event + self.on_response = on_response + + # Connection state + self.disconnected = False + self.keep_alive = keep_alive + self.waiting_for_100_continue = expect_100_continue + + # Request state + self.body = bytearray() + self.more_body = True + + # Response state + self.response_started = False + self.response_complete = False + self.chunked_encoding: bool | None = None + self.expected_content_length = 0 + + # ASGI exception wrapper + async def run_asgi(self, app: ASGI3Application) -> None: + try: + result = await app( # type: ignore[func-returns-value] + self.scope, self.receive, self.send + ) + except BaseException as exc: + msg = "Exception in ASGI application\n" + self.logger.error(msg, exc_info=exc) + if not self.response_started: + await self.send_500_response() + else: + self.transport.close() + else: + if result is not None: + msg = "ASGI callable should return None, but returned '%s'." + self.logger.error(msg, result) + self.transport.close() + elif not self.response_started and not self.disconnected: + msg = "ASGI callable returned without starting response." + self.logger.error(msg) + await self.send_500_response() + elif not self.response_complete and not self.disconnected: + msg = "ASGI callable returned without completing response." + self.logger.error(msg) + self.transport.close() + finally: + self.on_response = lambda: None + + async def send_500_response(self) -> None: + await self.send( + { + "type": "http.response.start", + "status": 500, + "headers": [ + (b"content-type", b"text/plain; charset=utf-8"), + (b"content-length", b"21"), + (b"connection", b"close"), + ], + } + ) + await self.send({"type": "http.response.body", "body": b"Internal Server Error", "more_body": False}) + + # ASGI interface + async def send(self, message: ASGISendEvent) -> None: + if self.flow.write_paused and not self.disconnected: + await self.flow.drain() # pragma: full coverage + + if self.disconnected: + return # pragma: full coverage + + if not self.response_started: + # Sending response status line and headers + if message["type"] != "http.response.start": + raise RuntimeError(f"Expected ASGI message 'http.response.start', but got '{message['type']}'.") + + self.response_started = True + self.waiting_for_100_continue = False + + status_code = message["status"] + headers = self.default_headers + list(message.get("headers", [])) + + if self.access_log: + self.access_logger.info( + '%s - "%s %s HTTP/%s" %d', + get_client_addr(self.scope), + self.scope["method"], + get_path_with_query_string(self.scope), + self.scope["http_version"], + status_code, + ) + + # Write response status line and headers + content = [STATUS_LINE[status_code]] + has_connection_close = False + + for name, value in headers: + if HEADER_RE.search(name): + raise RuntimeError("Invalid HTTP header name.") # pragma: full coverage + if HEADER_VALUE_RE.search(value): + raise RuntimeError("Invalid HTTP header value.") + + name = name.lower() + if name == b"content-length" and self.chunked_encoding is None: + self.expected_content_length = int(value.decode()) + self.chunked_encoding = False + elif name == b"transfer-encoding" and value.lower() == b"chunked": + self.expected_content_length = 0 + self.chunked_encoding = True + elif name == b"connection": + connection = [token.lower().strip() for token in value.split(b",")] + if b"close" in connection: + self.keep_alive = False + has_connection_close = True + content.extend([name, b": ", value, b"\r\n"]) + + if not self.keep_alive and not has_connection_close: + content.append(b"connection: close\r\n") + + if self.chunked_encoding is None and self.scope["method"] != "HEAD" and status_code not in (204, 304): + # Neither content-length nor transfer-encoding specified + self.chunked_encoding = True + content.append(b"transfer-encoding: chunked\r\n") + + content.append(b"\r\n") + self.transport.write(b"".join(content)) + + elif not self.response_complete: + # Sending response body + if message["type"] != "http.response.body": + raise RuntimeError(f"Expected ASGI message 'http.response.body', but got '{message['type']}'.") + + body = message.get("body", b"") + more_body = message.get("more_body", False) + + # Write response body + if self.scope["method"] == "HEAD": + self.expected_content_length = 0 + elif self.chunked_encoding: + if body: + content = [b"%x\r\n" % len(body), body, b"\r\n"] + else: + content = [] + if not more_body: + content.append(b"0\r\n\r\n") + self.transport.write(b"".join(content)) + else: + num_bytes = len(body) + if num_bytes > self.expected_content_length: + raise RuntimeError("Response content longer than Content-Length") + else: + self.expected_content_length -= num_bytes + self.transport.write(body) + + # Handle response completion + if not more_body: + if self.expected_content_length != 0: + raise RuntimeError("Response content shorter than Content-Length") + self.response_complete = True + self.message_event.set() + if not self.keep_alive: + self.transport.close() + self.on_response() + + else: + # Response already sent + raise RuntimeError(f"Unexpected ASGI message '{message['type']}' sent, after response already completed.") + + async def receive(self) -> ASGIReceiveEvent: + if self.waiting_for_100_continue and not self.transport.is_closing(): + self.transport.write(b"HTTP/1.1 100 Continue\r\n\r\n") + self.waiting_for_100_continue = False + + if not self.disconnected and not self.response_complete: + self.flow.resume_reading() + await self.message_event.wait() + self.message_event.clear() + + if self.disconnected or self.response_complete: + return {"type": "http.disconnect"} + message: HTTPRequestEvent = {"type": "http.request", "body": bytes(self.body), "more_body": self.more_body} + self.body = bytearray() + return message diff --git a/source/target-vendors/cp312/uvicorn/protocols/http/zttp_h2_impl.py b/source/target-vendors/cp312/uvicorn/protocols/http/zttp_h2_impl.py new file mode 100644 index 0000000..3a7a0f9 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/protocols/http/zttp_h2_impl.py @@ -0,0 +1,521 @@ +from __future__ import annotations + +import asyncio +import contextvars +import logging +import sys +from collections.abc import Callable, Generator +from typing import Any, ClassVar, Literal +from urllib.parse import unquote + +import zttp +from zttp import Event + +from uvicorn._types import ( + ASGI3Application, + ASGIReceiveEvent, + ASGISendEvent, + HTTPRequestEvent, + HTTPResponseBodyEvent, + HTTPResponseStartEvent, + HTTPScope, +) +from uvicorn.config import Config +from uvicorn.logging import TRACE_LOG_LEVEL +from uvicorn.protocols.http.flow_control import HIGH_WATER_LIMIT, FlowControl, service_unavailable +from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, is_ssl +from uvicorn.server import ServerState + +# RFC 9113 section 8.2.2: connection-specific headers MUST NOT appear in +# HTTP/2 messages. zttp rejects them with LocalProtocolError, and an ASGI app +# or middleware tuned for HTTP/1.1 may still emit them, so strip them on the +# way out. +FORBIDDEN_HEADERS = frozenset({b"connection", b"keep-alive", b"proxy-connection", b"transfer-encoding", b"upgrade"}) + + +class ZttpH2Protocol(asyncio.Protocol): + alpn_protocols: ClassVar[list[str]] = ["h2"] + + def __init__( + self, + config: Config, + server_state: ServerState, + app_state: dict[str, Any], + _loop: asyncio.AbstractEventLoop | None = None, + ) -> None: + if not config.loaded: + config.load() + + self.config = config + self.app = config.loaded_app + self.loop = _loop or asyncio.get_event_loop() + self.logger = logging.getLogger("uvicorn.error") + self.access_logger = logging.getLogger("uvicorn.access") + self.access_log = self.access_logger.hasHandlers() + self.conn: zttp.H2Connection = zttp.Connection(zttp.SERVER, protocol=zttp.HTTP2) + self.root_path = config.root_path + self.asgi_version = config.asgi_version + self.limit_concurrency = config.limit_concurrency + self.app_state = app_state + + # Timeouts + self.timeout_keep_alive_task: asyncio.TimerHandle | None = None + self.timeout_keep_alive = config.timeout_keep_alive + + # Shared server state + self.server_state = server_state + self.connections = server_state.connections + self.tasks = server_state.tasks + + # Per-connection state + self.transport: asyncio.Transport = None # type: ignore[assignment] + self.flow: FlowControl = None # type: ignore[assignment] + self.server: tuple[str, int | None] | None = None + self.client: tuple[str, int] | None = None + self.scheme: Literal["http", "https"] | None = None + self.shutdown_requested = False + + # Per-stream state, keyed by HTTP/2 stream id + self.cycles: dict[int, RequestResponseCycle] = {} + + # Protocol interface + def connection_made( # type: ignore[override] + self, transport: asyncio.Transport + ) -> None: + self.connections.add(self) + + self.transport = transport + self.flow = FlowControl(transport) + self.server = get_local_addr(transport) + self.client = get_remote_addr(transport) + self.scheme = "https" if is_ssl(transport) else "http" + + self.conn.initiate_connection() + self.flush() + + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sHTTP/2 connection made", prefix) + + def connection_lost(self, exc: Exception | None) -> None: + self.connections.discard(self) + + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sHTTP/2 connection lost", prefix) + + for cycle in self.cycles.values(): + if not cycle.response_complete: + cycle.disconnected = True + cycle.message_event.set() + if self.flow is not None: + self.flow.resume_writing() + if exc is None: + self.transport.close() + self._unset_keepalive_if_required() + + def eof_received(self) -> None: + pass + + def _unset_keepalive_if_required(self) -> None: + if self.timeout_keep_alive_task is not None: + self.timeout_keep_alive_task.cancel() + self.timeout_keep_alive_task = None + + def data_received(self, data: bytes) -> None: + self._unset_keepalive_if_required() + + try: + self.conn.receive_data(data) + self.handle_events() + except zttp.RemoteProtocolError as exc: + msg = "Invalid HTTP/2 frame received: %s" + self.logger.warning(msg, exc) + self.flush() + self.transport.close() + return + + self.flush() + + # Frames that carry no stream-level event (SETTINGS, PING, + # WINDOW_UPDATE) cancelled the keep-alive timer above but never re-arm + # it, so re-arm now if the connection is idle. + if not self.cycles and self.timeout_keep_alive_task is None and not self.transport.is_closing(): + self.timeout_keep_alive_task = self.loop.call_later( + self.timeout_keep_alive, self.timeout_keep_alive_handler + ) + + def flush(self) -> None: + """Write out bytes zttp queued outside a cycle's send path - the + connection preface and DATA released by a WINDOW_UPDATE credit.""" + data = self.conn.data_to_send() + if data: + self.transport.write(data) + + def events(self) -> Generator[Event]: + """Yield every complete event currently available.""" + while True: + event = self.conn.next_event() + if event is zttp.NEED_DATA: + return + yield event + + def handle_events(self) -> None: + for event in self.events(): + if isinstance(event, zttp.Request): + self.handle_request(event) + elif isinstance(event, zttp.Data): + cycle = self.cycles.get(event.stream_id) + if cycle is None or cycle.response_complete: + continue + cycle.body += event.data + # Unreachable until zttp grows its 64 KiB inbound flow-control + # windows, which cap how much body can buffer per stream. + if len(cycle.body) > HIGH_WATER_LIMIT: # pragma: no cover + self.flow.pause_reading() + cycle.message_event.set() + elif isinstance(event, zttp.EndOfMessage): + cycle = self.cycles.get(event.stream_id) + if cycle is None or cycle.response_complete: + continue + cycle.more_body = False + cycle.message_event.set() + elif isinstance(event, zttp.RstStream): + self.handle_rst_stream(event) + elif isinstance(event, zttp.GoAway): + self.shutdown_requested = True + if not self.cycles: + self._close_connection() + # Settings, Ping and WindowUpdate need no action here: zttp tracks + # the send windows internally, and `flush` writes whatever bytes + # the new credit released. + + def handle_request(self, event: zttp.Request) -> None: + headers = ( + event.headers.to_list(lowercase_names=True) + if isinstance(event.headers, zttp.HeaderBlock) + else [(name.lower(), value) for name, value in event.headers] + ) + path = unquote(event.path.decode("ascii")) + full_path = self.root_path + path + full_raw_path = self.root_path.encode("ascii") + event.path + scope: HTTPScope = { + "type": "http", + "asgi": {"version": self.asgi_version, "spec_version": "2.3"}, + "http_version": "2", + "server": self.server, + "client": self.client, + "scheme": self.scheme, # type: ignore[typeddict-item] + "method": event.method.decode("ascii"), + "root_path": self.root_path, + "path": full_path, + "raw_path": full_raw_path, + "query_string": event.query, + "headers": headers, + "state": self.app_state.copy(), + } + + # Refuse new streams once a shutdown began, and handle 503 responses + # when 'limit_concurrency' is exceeded. + if self.shutdown_requested: + app = service_unavailable + elif self.limit_concurrency is not None and ( + len(self.connections) >= self.limit_concurrency or len(self.tasks) >= self.limit_concurrency + ): + app = service_unavailable + message = "Exceeded concurrency limit." + self.logger.warning(message) + else: + app = self.app + + cycle = RequestResponseCycle( + scope=scope, + conn=self.conn, + stream=self.conn.stream(event.stream_id), + transport=self.transport, + flow=self.flow, + logger=self.logger, + access_logger=self.access_logger, + access_log=self.access_log, + default_headers=self.server_state.default_headers, + message_event=asyncio.Event(), + on_response=self.on_response_complete, + resume_reading=self.resume_reading_if_idle, + ) + self.cycles[event.stream_id] = cycle + + if self.config.reset_contextvars: + if sys.version_info >= (3, 11): # pragma: py-lt-311 + task = self.loop.create_task(cycle.run_asgi(app), context=contextvars.Context()) + else: # pragma: py-gte-311 + task = contextvars.Context().run(self.loop.create_task, cycle.run_asgi(app)) + else: + task = self.loop.create_task(cycle.run_asgi(app)) + task.add_done_callback(self.tasks.discard) + self.tasks.add(task) + + def handle_rst_stream(self, event: zttp.RstStream) -> None: + cycle = self.cycles.pop(event.stream_id, None) + if cycle is None: + return + if not cycle.response_complete: + cycle.disconnected = True + cycle.message_event.set() + self.on_stream_closed() + + def resume_reading_if_idle(self) -> None: + """Resume reads only if no stream is over the body buffer high-water + mark; otherwise a slow stream's buffer would keep growing while a fast + peer ships frames on a sibling stream.""" + for cycle in self.cycles.values(): + if len(cycle.body) > HIGH_WATER_LIMIT: + return + self.flow.resume_reading() + + def on_response_complete(self, stream_id: int) -> None: + self.server_state.total_requests += 1 + self.cycles.pop(stream_id, None) + self.on_stream_closed() + + def on_stream_closed(self) -> None: + if self.transport.is_closing(): + return + + # The stream's body buffer is gone, so it can no longer hold back the + # read side of the transport. + self.resume_reading_if_idle() + + self._unset_keepalive_if_required() + + if not self.cycles: + if self.shutdown_requested: + self._close_connection() + return + self.timeout_keep_alive_task = self.loop.call_later( + self.timeout_keep_alive, self.timeout_keep_alive_handler + ) + + def _close_connection(self) -> None: + """Send GOAWAY and close the transport.""" + if self.transport.is_closing(): + return + self.conn.close() + self.flush() + self.transport.close() + + def shutdown(self) -> None: + """ + Called by the server to commence a graceful shutdown. + + Closes immediately when idle; otherwise new streams are refused with + 503 and the connection closes once the last in-flight stream finishes. + """ + self.shutdown_requested = True + if not self.cycles: + self._close_connection() + + def pause_writing(self) -> None: + """ + Called by the transport when the write buffer exceeds the high water mark. + """ + self.flow.pause_writing() # pragma: no cover + + def resume_writing(self) -> None: + """ + Called by the transport when the write buffer drops below the low water mark. + """ + self.flow.resume_writing() # pragma: no cover + + def timeout_keep_alive_handler(self) -> None: + """ + Called on a keep-alive connection if no new data is received after a short + delay. + """ + self._close_connection() + + +class RequestResponseCycle: + def __init__( + self, + scope: HTTPScope, + conn: zttp.H2Connection, + stream: zttp.Stream, + transport: asyncio.Transport, + flow: FlowControl, + logger: logging.Logger, + access_logger: logging.Logger, + access_log: bool, + default_headers: list[tuple[bytes, bytes]], + message_event: asyncio.Event, + on_response: Callable[[int], None], + resume_reading: Callable[[], None], + ) -> None: + self.scope = scope + self.conn = conn + self.stream = stream + self.transport = transport + self.flow = flow + self.logger = logger + self.access_logger = access_logger + self.access_log = access_log + self.default_headers = default_headers + self.message_event = message_event + self.on_response = on_response + self.resume_reading = resume_reading + + # Connection state + self.disconnected = False + + # Request state + self.body = bytearray() + self.more_body = True + + # Response state + self.response_started = False + self.response_complete = False + self.bodyless = False + self.expected_content_length: int | None = None + + # ASGI exception wrapper + async def run_asgi(self, app: ASGI3Application) -> None: + try: + result = await app( # type: ignore[func-returns-value] + self.scope, self.receive, self.send + ) + except BaseException as exc: + msg = "Exception in ASGI application\n" + self.logger.error(msg, exc_info=exc) + if not self.response_started: + await self.send_500_response() + else: + self.abort_stream() + else: + if result is not None: + msg = "ASGI callable should return None, but returned '%s'." + self.logger.error(msg, result) + self.abort_stream() + elif not self.response_started and not self.disconnected: + msg = "ASGI callable returned without starting response." + self.logger.error(msg) + await self.send_500_response() + elif not self.response_complete and not self.disconnected: + msg = "ASGI callable returned without completing response." + self.logger.error(msg) + self.abort_stream() + finally: + self.on_response(self.stream.stream_id) + self.on_response = lambda stream_id: None + + def abort_stream(self) -> None: + """Reset only this stream so sibling streams on the connection survive.""" + if self.transport.is_closing(): + return + self.stream.reset() + self.transport.write(self.conn.data_to_send()) + self.disconnected = True + + async def send_500_response(self) -> None: + response_start_event: HTTPResponseStartEvent = { + "type": "http.response.start", + "status": 500, + "headers": [(b"content-type", b"text/plain; charset=utf-8")], + } + await self.send(response_start_event) + response_body_event: HTTPResponseBodyEvent = { + "type": "http.response.body", + "body": b"Internal Server Error", + "more_body": False, + } + await self.send(response_body_event) + + # ASGI interface + async def send(self, message: ASGISendEvent) -> None: + if self.flow.write_paused and not self.disconnected: + await self.flow.drain() # pragma: no cover + + if self.disconnected: + return + + if not self.response_started: + # Sending response status line and headers + if message["type"] != "http.response.start": + raise RuntimeError(f"Expected ASGI message 'http.response.start', but got '{message['type']}'.") + + self.response_started = True + + status = message["status"] + headers: list[tuple[bytes, bytes]] = [] + for name, value in list(self.default_headers) + list(message.get("headers", [])): + name = name.lower() + if name in FORBIDDEN_HEADERS: + continue + if name == b"te" and value.lower().strip() != b"trailers": + continue + if name == b"content-length": + self.expected_content_length = int(value.decode()) + headers.append((name, value)) + + self.bodyless = self.scope["method"] == "HEAD" or status in (204, 304) or status < 200 + if self.bodyless: + self.expected_content_length = None + + if self.access_log: + self.access_logger.info( + '%s - "%s %s HTTP/%s" %d', + get_client_addr(self.scope), + self.scope["method"], + get_path_with_query_string(self.scope), + self.scope["http_version"], + status, + ) + + # Write response headers + self.stream.send_response(status, headers) + self.transport.write(self.conn.data_to_send()) + + elif not self.response_complete: + # Sending response body + if message["type"] != "http.response.body": + raise RuntimeError(f"Expected ASGI message 'http.response.body', but got '{message['type']}'.") + + body = message.get("body", b"") + more_body = message.get("more_body", False) + + # Write response body + if self.bodyless: + body = b"" + elif self.expected_content_length is not None: + if len(body) > self.expected_content_length: + raise RuntimeError("Response content longer than Content-Length") + self.expected_content_length -= len(body) + if body: + self.stream.send_data(body) + self.transport.write(self.conn.data_to_send()) + + # Handle response completion + if not more_body: + if self.expected_content_length not in (None, 0): + raise RuntimeError("Response content shorter than Content-Length") + self.response_complete = True + self.message_event.set() + self.stream.end_message() + self.transport.write(self.conn.data_to_send()) + self.on_response(self.stream.stream_id) + self.on_response = lambda stream_id: None + + else: + # Response already sent + raise RuntimeError(f"Unexpected ASGI message '{message['type']}' sent, after response already completed.") + + async def receive(self) -> ASGIReceiveEvent: + if not self.disconnected and not self.response_complete: + self.resume_reading() + await self.message_event.wait() + self.message_event.clear() + + if self.disconnected or self.response_complete: + return {"type": "http.disconnect"} + + message: HTTPRequestEvent = {"type": "http.request", "body": bytes(self.body), "more_body": self.more_body} + self.body = bytearray() + return message diff --git a/source/target-vendors/cp312/uvicorn/protocols/http/zttp_impl.py b/source/target-vendors/cp312/uvicorn/protocols/http/zttp_impl.py new file mode 100644 index 0000000..b64f817 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/protocols/http/zttp_impl.py @@ -0,0 +1,537 @@ +from __future__ import annotations + +import asyncio +import contextvars +import logging +import sys +from collections.abc import Callable +from typing import Any, Literal +from urllib.parse import unquote + +import zttp + +from uvicorn._types import ( + ASGI3Application, + ASGIReceiveEvent, + ASGISendEvent, + HTTPRequestEvent, + HTTPResponseBodyEvent, + HTTPResponseStartEvent, + HTTPScope, +) +from uvicorn.config import Config +from uvicorn.logging import TRACE_LOG_LEVEL +from uvicorn.protocols.http.flow_control import HIGH_WATER_LIMIT, FlowControl, service_unavailable +from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, is_ssl +from uvicorn.server import ServerState + + +class ZttpProtocol(asyncio.Protocol): + def __init__( + self, + config: Config, + server_state: ServerState, + app_state: dict[str, Any], + _loop: asyncio.AbstractEventLoop | None = None, + ) -> None: + if not config.loaded: + config.load() + + self.config = config + self.app = config.loaded_app + self.loop = _loop or asyncio.get_event_loop() + self.logger = logging.getLogger("uvicorn.error") + self.access_logger = logging.getLogger("uvicorn.access") + self.access_log = self.access_logger.hasHandlers() + self.conn = zttp.Connection(zttp.SERVER) + self.ws_protocol_class = config.ws_protocol_class + self.root_path = config.root_path + self.asgi_version = config.asgi_version + self.limit_concurrency = config.limit_concurrency + self.app_state = app_state + + # Timeouts + self.timeout_keep_alive_task: asyncio.TimerHandle | None = None + self.timeout_keep_alive = config.timeout_keep_alive + + # Shared server state + self.server_state = server_state + self.connections = server_state.connections + self.tasks = server_state.tasks + + # Per-connection state + self.transport: asyncio.Transport = None # type: ignore[assignment] + self.flow: FlowControl = None # type: ignore[assignment] + self.server: tuple[str, int | None] | None = None + self.client: tuple[str, int] | None = None + self.scheme: Literal["http", "https"] | None = None + + # Per-request state + self.scope: HTTPScope = None # type: ignore[assignment] + self.headers: list[tuple[bytes, bytes]] = None # type: ignore[assignment] + self.cycle: RequestResponseCycle = None # type: ignore[assignment] + + # Protocol interface + def connection_made( # type: ignore[override] + self, transport: asyncio.Transport + ) -> None: + self.connections.add(self) + + self.transport = transport + self.flow = FlowControl(transport) + self.server = get_local_addr(transport) + self.client = get_remote_addr(transport) + self.scheme = "https" if is_ssl(transport) else "http" + + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection made", prefix) + + def connection_lost(self, exc: Exception | None) -> None: + self.connections.discard(self) + + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection lost", prefix) + + if self.cycle and not self.cycle.response_complete: + self.cycle.disconnected = True + if self.cycle is not None: + self.cycle.message_event.set() + if self.flow is not None: + self.flow.resume_writing() + if exc is None: + self.transport.close() + self._unset_keepalive_if_required() + + def eof_received(self) -> None: + pass + + def _unset_keepalive_if_required(self) -> None: + if self.timeout_keep_alive_task is not None: + self.timeout_keep_alive_task.cancel() + self.timeout_keep_alive_task = None + + def _should_upgrade_to_ws(self) -> bool: + if self.ws_protocol_class is None: + return False + return True + + def _unsupported_upgrade_warning(self) -> None: + self.logger.warning("Unsupported upgrade request.") + if not self._should_upgrade_to_ws(): + self.logger.warning( + "No supported WebSocket library detected. " + "Please use \"pip install 'uvicorn[standard]'\", or install 'websockets' or 'wsproto' manually." + ) + + def _should_upgrade(self) -> bool: + upgrade = self.conn.upgrade() + if upgrade is not None: + upgrade = upgrade.lower() + if upgrade == b"websocket" and self._should_upgrade_to_ws(): + return True + if upgrade is not None: + self._unsupported_upgrade_warning() + return False + + def data_received(self, data: bytes) -> None: + self._unset_keepalive_if_required() + try: + self.handle_events(self.conn.receive_event(data)) + except zttp.RemoteProtocolError: + self.handle_remote_protocol_error() + + def handle_events(self, event: zttp.Event) -> None: + while event is not zttp.NEED_DATA: + if isinstance(event, zttp.Request): + # Pipelined HTTP requests and WebSocket upgrades may be processed after the keep-alive timer is armed. + self._unset_keepalive_if_required() + + assert isinstance(event.headers, zttp.HeaderBlock) + self.headers = event.headers.to_list(lowercase_names=True) + path = event.path.decode("ascii") + if "%" in path: + path = unquote(path) + full_path = self.root_path + path + full_raw_path = self.root_path.encode("ascii") + event.path + self.scope = { + "type": "http", + "asgi": {"version": self.asgi_version, "spec_version": "2.3"}, + "http_version": event.http_version.decode("ascii"), + "server": self.server, + "client": self.client, + "scheme": self.scheme, # type: ignore[typeddict-item] + "method": event.method.decode("ascii"), + "root_path": self.root_path, + "path": full_path, + "raw_path": full_raw_path, + "query_string": event.query, + "headers": self.headers, + "state": self.app_state.copy(), + } + if self._should_upgrade(): + self.handle_websocket_upgrade(event) + return + + # Handle 503 responses when 'limit_concurrency' is exceeded. + if self.limit_concurrency is not None and ( + len(self.connections) >= self.limit_concurrency or len(self.tasks) >= self.limit_concurrency + ): + app = service_unavailable + message = "Exceeded concurrency limit." + self.logger.warning(message) + else: + app = self.app + + self.cycle = RequestResponseCycle( + scope=self.scope, + conn=self.conn, + transport=self.transport, + flow=self.flow, + logger=self.logger, + access_logger=self.access_logger, + access_log=self.access_log, + default_headers=self.server_state.default_headers, + message_event=asyncio.Event(), + expect_100_continue=event.expect_continue, + on_response=self.on_response_complete, + ) + if event.end_stream: + self.cycle.more_body = False + self.cycle.message_event.set() + if self.config.reset_contextvars: + if sys.version_info >= (3, 11): # pragma: py-lt-311 + task = self.loop.create_task(self.cycle.run_asgi(app), context=contextvars.Context()) + else: # pragma: py-gte-311 + task = contextvars.Context().run(self.loop.create_task, self.cycle.run_asgi(app)) + else: + task = self.loop.create_task(self.cycle.run_asgi(app)) + task.add_done_callback(self.tasks.discard) + self.tasks.add(task) + + elif isinstance(event, zttp.Data): + if self.cycle is not None and not self.cycle.response_complete: + self.cycle.body += event.data + if len(self.cycle.body) > HIGH_WATER_LIMIT: + self.flow.pause_reading() + self.cycle.message_event.set() + + elif isinstance(event, zttp.EndOfMessage): + if self.cycle is not None: + self.cycle.more_body = False + self.cycle.message_event.set() + if self.cycle.response_complete: + self.conn.start_next_cycle() + + event = self.conn.next_event() + + def handle_remote_protocol_error(self) -> None: + msg = "Invalid HTTP request received." + self.logger.warning(msg) + self.send_400_response(msg) + + def handle_websocket_upgrade(self, event: zttp.Request) -> None: + if self.logger.level <= TRACE_LOG_LEVEL: # pragma: no cover + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sUpgrading to WebSocket", prefix) + + self.connections.discard(self) + output = bytearray(event.method + b" " + event.target + b" HTTP/1.1\r\n") + for name, value in self.headers: + output += name + b": " + value + b"\r\n" + output += b"\r\n" + protocol = self.ws_protocol_class( # type: ignore[call-arg, misc] + config=self.config, + server_state=self.server_state, + app_state=self.app_state, + ) + protocol.connection_made(self.transport) + protocol.data_received(bytes(output)) + self.transport.set_protocol(protocol) + + def send_400_response(self, msg: str) -> None: + body = msg.encode("ascii") + headers: list[tuple[bytes, bytes]] = [ + (b"content-type", b"text/plain; charset=utf-8"), + (b"content-length", str(len(body)).encode("ascii")), + (b"connection", b"close"), + ] + self.conn.send_response(400, headers) + self.conn.send_data(body) + self.conn.end_message() + self.transport.write(self.conn.data_to_send()) + self.transport.close() + + def on_response_complete(self) -> None: + self.server_state.total_requests += 1 + + if self.transport.is_closing(): + return + + # Set a short Keep-Alive timeout. + self._unset_keepalive_if_required() + + self.timeout_keep_alive_task = self.loop.call_later(self.timeout_keep_alive, self.timeout_keep_alive_handler) + + # Unpause data reads if needed. + self.flow.resume_reading() + + # Do not reset the parser until the complete request body has arrived. + # An application may respond before consuming it; any remaining body + # still belongs to the current request and must be discarded as such. + if self.cycle is not None and not self.cycle.more_body: + self.conn.start_next_cycle() + try: + self.handle_events(self.conn.next_event()) + except zttp.RemoteProtocolError: # pragma: no cover + self.handle_remote_protocol_error() + + def shutdown(self) -> None: + """ + Called by the server to commence a graceful shutdown. + """ + if self.cycle is None or self.cycle.response_complete: + self.transport.close() + else: + self.cycle.keep_alive = False + + def pause_writing(self) -> None: + """ + Called by the transport when the write buffer exceeds the high water mark. + """ + self.flow.pause_writing() # pragma: no cover + + def resume_writing(self) -> None: + """ + Called by the transport when the write buffer drops below the low water mark. + """ + self.flow.resume_writing() # pragma: no cover + + def timeout_keep_alive_handler(self) -> None: + """ + Called on a keep-alive connection if no new data is received after a short + delay. + """ + if not self.transport.is_closing(): + self.transport.close() + + +class RequestResponseCycle: + def __init__( + self, + scope: HTTPScope, + conn: zttp.H1Connection, + transport: asyncio.Transport, + flow: FlowControl, + logger: logging.Logger, + access_logger: logging.Logger, + access_log: bool, + default_headers: list[tuple[bytes, bytes]], + message_event: asyncio.Event, + expect_100_continue: bool, + on_response: Callable[..., None], + ) -> None: + self.scope = scope + self.conn = conn + self.transport = transport + self.flow = flow + self.logger = logger + self.access_logger = access_logger + self.access_log = access_log + self.default_headers = default_headers + self.message_event = message_event + self.on_response = on_response + + # Connection state + self.disconnected = False + self.keep_alive = True + self.waiting_for_100_continue = expect_100_continue + + # Request state + self.body = bytearray() + self.more_body = True + + # Response state + self.response_started = False + self.response_complete = False + self.bodyless = False + self.chunked_encoding = False + self.expected_content_length = 0 + + # ASGI exception wrapper + async def run_asgi(self, app: ASGI3Application) -> None: + try: + result = await app( # type: ignore[func-returns-value] + self.scope, self.receive, self.send + ) + except BaseException as exc: + msg = "Exception in ASGI application\n" + self.logger.error(msg, exc_info=exc) + if not self.response_started: + await self.send_500_response() + else: + self.transport.close() + else: + if result is not None: + msg = "ASGI callable should return None, but returned '%s'." + self.logger.error(msg, result) + self.transport.close() + elif not self.response_started and not self.disconnected: + msg = "ASGI callable returned without starting response." + self.logger.error(msg) + await self.send_500_response() + elif not self.response_complete and not self.disconnected: + msg = "ASGI callable returned without completing response." + self.logger.error(msg) + self.transport.close() + finally: + self.on_response = lambda: None + + async def send_500_response(self) -> None: + response_start_event: HTTPResponseStartEvent = { + "type": "http.response.start", + "status": 500, + "headers": [ + (b"content-type", b"text/plain; charset=utf-8"), + (b"connection", b"close"), + ], + } + await self.send(response_start_event) + response_body_event: HTTPResponseBodyEvent = { + "type": "http.response.body", + "body": b"Internal Server Error", + "more_body": False, + } + await self.send(response_body_event) + + # ASGI interface + async def send(self, message: ASGISendEvent) -> None: + if self.flow.write_paused and not self.disconnected: + await self.flow.drain() # pragma: no cover + + if self.disconnected: + return # pragma: no cover + + if not self.response_started: + # Sending response status line and headers + if message["type"] != "http.response.start": + raise RuntimeError(f"Expected ASGI message 'http.response.start', but got '{message['type']}'.") + + self.response_started = True + self.waiting_for_100_continue = False + + status = message["status"] + headers = self.default_headers + list(message.get("headers", [])) + + bodyless = self.scope["method"] == "HEAD" or status in (204, 304) or status < 200 + + # RFC 9112 §6.1 forbids Transfer-Encoding on 1xx and 204 responses, and + # zttp refuses to serialize one there, so drop it instead of erroring. + # HEAD and 304 may keep it: it describes the body a GET would return. + if status < 200 or status == 204: + headers = [(name, value) for name, value in headers if name.lower() != b"transfer-encoding"] + + has_content_length = False + has_transfer_encoding = False + for name, value in headers: + name = name.lower() + if name == b"content-length": + has_content_length = True + self.expected_content_length = int(value.decode()) + elif name == b"transfer-encoding": + # zttp only accepts a sole, final `chunked` coding - anything + # else raises, like h11 - and it frames the body itself, so + # there is no Content-Length accounting to do here. + has_transfer_encoding = True + self.chunked_encoding = True + + # A response carrying both Content-Length and Transfer-Encoding is + # a framing conflict that zttp rejects, so drop the Content-Length. + if has_transfer_encoding and has_content_length: + headers = [(name, value) for name, value in headers if name.lower() != b"content-length"] + has_content_length = False + self.expected_content_length = 0 + + # zttp refuses to frame the body unless the response declares + # Content-Length or Transfer-Encoding, so add chunked encoding + # ourselves when the application provides neither. + if not bodyless and not has_transfer_encoding and not has_content_length: + self.chunked_encoding = True + headers = headers + [(b"transfer-encoding", b"chunked")] + + if self.access_log: + self.access_logger.info( + '%s - "%s %s HTTP/%s" %d', + get_client_addr(self.scope), + self.scope["method"], + get_path_with_query_string(self.scope), + self.scope["http_version"], + status, + ) + + # Write response status line and headers + self.bodyless = bodyless + self.conn.send_response(status, headers) + self.transport.write(self.conn.data_to_send()) + + elif not self.response_complete: + # Sending response body + if message["type"] != "http.response.body": + raise RuntimeError(f"Expected ASGI message 'http.response.body', but got '{message['type']}'.") + + body = message.get("body", b"") + more_body = message.get("more_body", False) + + # Write response body + if self.bodyless: + self.expected_content_length = 0 + elif self.chunked_encoding: + if body: + self.conn.send_data(body) + self.transport.write(self.conn.data_to_send()) + else: + if len(body) > self.expected_content_length: + raise RuntimeError("Response content longer than Content-Length") + self.expected_content_length -= len(body) + if body: + self.conn.send_data(body) + self.transport.write(self.conn.data_to_send()) + + # Handle response completion + if not more_body: + if self.expected_content_length != 0: + raise RuntimeError("Response content shorter than Content-Length") + self.response_complete = True + self.message_event.set() + self.conn.end_message() + self.transport.write(self.conn.data_to_send()) + + else: + # Response already sent + raise RuntimeError(f"Unexpected ASGI message '{message['type']}' sent, after response already completed.") + + if self.response_complete: + # `should_close()` covers both directions: the request's `Connection: close` + # or HTTP/1.0 default, and a `close` the response we just wrote declared. + if not self.keep_alive or self.conn.should_close(): + self.transport.close() + self.on_response() + + async def receive(self) -> ASGIReceiveEvent: + if self.waiting_for_100_continue and not self.transport.is_closing(): + self.conn.send_informational(100) + self.transport.write(self.conn.data_to_send()) + self.waiting_for_100_continue = False + + if not self.disconnected and not self.response_complete: + self.flow.resume_reading() + await self.message_event.wait() + self.message_event.clear() + + if self.disconnected or self.response_complete: + return {"type": "http.disconnect"} + + message: HTTPRequestEvent = {"type": "http.request", "body": bytes(self.body), "more_body": self.more_body} + self.body = bytearray() + return message diff --git a/source/target-vendors/cp312/uvicorn/protocols/utils.py b/source/target-vendors/cp312/uvicorn/protocols/utils.py new file mode 100644 index 0000000..a9a8906 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/protocols/utils.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import asyncio +import socket +import urllib.parse + +from uvicorn._types import WWWScope + + +class ClientDisconnected(OSError): ... + + +def get_remote_addr(transport: asyncio.Transport) -> tuple[str, int] | None: + socket_info: socket.socket | None = transport.get_extra_info("socket") + if socket_info is not None: + try: + info = socket_info.getpeername() + return (str(info[0]), int(info[1])) if isinstance(info, tuple) else None + except OSError: # pragma: no cover + # This case appears to inconsistently occur with uvloop + # bound to a unix domain socket. + return None + + info = transport.get_extra_info("peername") + if info is not None and isinstance(info, list | tuple) and len(info) == 2: + return (str(info[0]), int(info[1])) + return None + + +def get_local_addr(transport: asyncio.Transport) -> tuple[str, int | None] | None: + socket_info: socket.socket | None = transport.get_extra_info("socket") + if socket_info is not None: + info = socket_info.getsockname() + if isinstance(info, tuple): + return (str(info[0]), int(info[1])) + if isinstance(info, str): + return (info, None) + return None + info = transport.get_extra_info("sockname") + if info is not None and isinstance(info, list | tuple) and len(info) == 2: + return (str(info[0]), int(info[1])) + if isinstance(info, str): + return (info, None) + return None + + +def is_ssl(transport: asyncio.Transport) -> bool: + return bool(transport.get_extra_info("sslcontext")) + + +def get_client_addr(scope: WWWScope) -> str: + client = scope.get("client") + if not client: + return "" + return "%s:%d" % client + + +def get_path_with_query_string(scope: WWWScope) -> str: + path_with_query_string = urllib.parse.quote(scope["path"]) + if scope["query_string"]: + path_with_query_string = "{}?{}".format(path_with_query_string, scope["query_string"].decode("ascii")) + return path_with_query_string diff --git a/source/target-vendors/cp312/uvicorn/protocols/websockets/__init__.py b/source/target-vendors/cp312/uvicorn/protocols/websockets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/target-vendors/cp312/uvicorn/protocols/websockets/auto.py b/source/target-vendors/cp312/uvicorn/protocols/websockets/auto.py new file mode 100644 index 0000000..23db3d0 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/protocols/websockets/auto.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +import asyncio +from collections.abc import Callable + +AutoWebSocketsProtocol: Callable[..., asyncio.Protocol] | None +try: + import websockets # noqa +except ImportError: # pragma: no cover + try: + import wsproto # noqa + except ImportError: + AutoWebSocketsProtocol = None + else: + from uvicorn.protocols.websockets.wsproto_impl import WSProtocol + + AutoWebSocketsProtocol = WSProtocol +else: + from uvicorn.protocols.websockets.websockets_sansio_impl import WebSocketsSansIOProtocol + + AutoWebSocketsProtocol = WebSocketsSansIOProtocol diff --git a/source/target-vendors/cp312/uvicorn/protocols/websockets/websockets_impl.py b/source/target-vendors/cp312/uvicorn/protocols/websockets/websockets_impl.py new file mode 100644 index 0000000..8206a58 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/protocols/websockets/websockets_impl.py @@ -0,0 +1,390 @@ +from __future__ import annotations + +import asyncio +import http +import logging +import warnings +from collections.abc import Sequence +from typing import Any, Literal, cast +from urllib.parse import unquote + +import websockets +import websockets.legacy.handshake +from websockets import __version__ as websockets_version +from websockets.datastructures import Headers +from websockets.exceptions import ConnectionClosed +from websockets.extensions.base import ServerExtensionFactory +from websockets.extensions.permessage_deflate import ServerPerMessageDeflateFactory +from websockets.legacy.server import HTTPResponse, WebSocketServerProtocol +from websockets.typing import Subprotocol + +from uvicorn._types import ( + ASGI3Application, + ASGISendEvent, + WebSocketConnectEvent, + WebSocketDisconnectEvent, + WebSocketReceiveEvent, + WebSocketScope, +) +from uvicorn.config import Config, UvicornDeprecationWarning +from uvicorn.logging import TRACE_LOG_LEVEL +from uvicorn.protocols.utils import ( + ClientDisconnected, + get_client_addr, + get_local_addr, + get_path_with_query_string, + get_remote_addr, + is_ssl, +) +from uvicorn.server import ServerState + +warnings.warn( + "The `websockets` implementation is deprecated, and `--ws websockets` will point at the " + "`websockets-sansio` implementation in a future release. " + "Use `--ws websockets-sansio` or `--ws auto` to switch now.", + UvicornDeprecationWarning, +) + + +class Server: + closing = False + + def register(self, ws: WebSocketServerProtocol) -> None: + pass + + def unregister(self, ws: WebSocketServerProtocol) -> None: + pass + + def is_serving(self) -> bool: + return not self.closing + + +class WebSocketProtocol(WebSocketServerProtocol): + extra_headers: list[tuple[str, str]] + logger: logging.Logger | logging.LoggerAdapter[Any] + + def __init__( + self, + config: Config, + server_state: ServerState, + app_state: dict[str, Any], + _loop: asyncio.AbstractEventLoop | None = None, + ): + if not config.loaded: + config.load() + + self.config = config + self.app = cast(ASGI3Application, config.loaded_app) + self.loop = _loop or asyncio.get_event_loop() + self.root_path = config.root_path + self.asgi_version = config.asgi_version + self.app_state = app_state + + # Shared server state + self.connections = server_state.connections + self.tasks = server_state.tasks + + # Connection state + self.transport: asyncio.Transport = None # type: ignore[assignment] + self.server: tuple[str, int | None] | None = None + self.client: tuple[str, int] | None = None + self.scheme: Literal["wss", "ws"] = None # type: ignore[assignment] + + # Connection events + self.scope: WebSocketScope + self.handshake_started_event = asyncio.Event() + self.handshake_completed_event = asyncio.Event() + self.closed_event = asyncio.Event() + self.initial_response: HTTPResponse | None = None + self.connect_sent = False + self.lost_connection_before_handshake = False + self.accepted_subprotocol: Subprotocol | None = None + + self.ws_server: Server = Server() # type: ignore[assignment] + + extensions: list[ServerExtensionFactory] = [] + if self.config.ws_per_message_deflate: + extensions.append(ServerPerMessageDeflateFactory()) + + super().__init__( + ws_handler=self.ws_handler, + ws_server=self.ws_server, # type: ignore[arg-type] + max_size=self.config.ws_max_size, + max_queue=self.config.ws_max_queue, + ping_interval=self.config.ws_ping_interval, + ping_timeout=self.config.ws_ping_timeout, + extensions=extensions, + logger=logging.getLogger("uvicorn.error"), + ) + self.server_header = None + self.extra_headers = [ + (name.decode("latin-1"), value.decode("latin-1")) for name, value in server_state.default_headers + ] + + def connection_made( # type: ignore[override] + self, transport: asyncio.Transport + ) -> None: + self.connections.add(self) + self.transport = transport + self.server = get_local_addr(transport) + self.client = get_remote_addr(transport) + self.scheme = "wss" if is_ssl(transport) else "ws" + + if self.logger.isEnabledFor(TRACE_LOG_LEVEL): + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection made", prefix) + + super().connection_made(transport) + + def connection_lost(self, exc: Exception | None) -> None: + self.connections.remove(self) + + if self.logger.isEnabledFor(TRACE_LOG_LEVEL): + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection lost", prefix) + + self.lost_connection_before_handshake = not self.handshake_completed_event.is_set() + self.handshake_completed_event.set() + super().connection_lost(exc) + if exc is None: + self.transport.close() + + def shutdown(self) -> None: + self.ws_server.closing = True + if self.handshake_completed_event.is_set(): + self.fail_connection(1012) + else: + self.send_500_response() + self.transport.close() + + def on_task_complete(self, task: asyncio.Task[None]) -> None: + self.tasks.discard(task) + + async def process_request(self, path: str, request_headers: Headers) -> HTTPResponse | None: + """ + This hook is called to determine if the websocket should return + an HTTP response and close. + + Our behavior here is to start the ASGI application, and then wait + for either `accept` or `close` in order to determine if we should + close the connection. + """ + path_portion, _, query_string = path.partition("?") + + websockets.legacy.handshake.check_request(request_headers) + + subprotocols: list[str] = [] + for header in request_headers.get_all("Sec-WebSocket-Protocol"): + subprotocols.extend([token.strip() for token in header.split(",")]) + + # websockets 17.0 documents that non-ASCII header values are encoded + # with ISO-8859-1. Earlier versions didn't document the behavior but + # we can see in the code that it used surrogate escape encoding. + # Move the pragma: no cover to the else: branch when 17.0 is released. + if websockets_version >= "17.0": # pragma: no cover + asgi_headers = [ + (name.encode("ascii"), value.encode("latin-1")) for name, value in request_headers.raw_items() + ] + else: + asgi_headers = [ + (name.encode("ascii"), value.encode("ascii", errors="surrogateescape")) + for name, value in request_headers.raw_items() + ] + path = unquote(path_portion) + full_path = self.root_path + path + full_raw_path = self.root_path.encode("ascii") + path_portion.encode("ascii") + + self.scope = { + "type": "websocket", + "asgi": {"version": self.asgi_version, "spec_version": "2.4"}, + "http_version": "1.1", + "scheme": self.scheme, + "server": self.server, + "client": self.client, + "root_path": self.root_path, + "path": full_path, + "raw_path": full_raw_path, + "query_string": query_string.encode("ascii"), + "headers": asgi_headers, + "subprotocols": subprotocols, + "state": self.app_state.copy(), + "extensions": {"websocket.http.response": {}}, + } + task = self.loop.create_task(self.run_asgi()) + task.add_done_callback(self.on_task_complete) + self.tasks.add(task) + await self.handshake_started_event.wait() + return self.initial_response + + def process_subprotocol( + self, headers: Headers, available_subprotocols: Sequence[Subprotocol] | None + ) -> Subprotocol | None: + """ + We override the standard 'process_subprotocol' behavior here so that + we return whatever subprotocol is sent in the 'accept' message. + """ + return self.accepted_subprotocol + + def send_500_response(self) -> None: + msg = b"Internal Server Error" + content = [ + b"HTTP/1.1 500 Internal Server Error\r\ncontent-type: text/plain; charset=utf-8\r\n", + b"content-length: " + str(len(msg)).encode("ascii") + b"\r\n", + b"connection: close\r\n", + b"\r\n", + msg, + ] + self.transport.write(b"".join(content)) + # Allow handler task to terminate cleanly, as websockets doesn't cancel it by + # itself (see https://github.com/Kludex/uvicorn/issues/920) + self.handshake_started_event.set() + + async def ws_handler(self, protocol: WebSocketServerProtocol, path: str) -> Any: # type: ignore[override] + """ + This is the main handler function for the 'websockets' implementation + to call into. We just wait for close then return, and instead allow + 'send' and 'receive' events to drive the flow. + """ + self.handshake_completed_event.set() + await self.wait_closed() + + async def run_asgi(self) -> None: + """ + Wrapper around the ASGI callable, handling exceptions and unexpected + termination states. + """ + try: + result = await self.app(self.scope, self.asgi_receive, self.asgi_send) # type: ignore[func-returns-value] + except ClientDisconnected: # pragma: full coverage + self.closed_event.set() + except BaseException: + self.closed_event.set() + self.logger.exception("Exception in ASGI application\n") + if not self.handshake_started_event.is_set(): + self.send_500_response() + else: + await self.handshake_completed_event.wait() + else: + self.closed_event.set() + if not self.handshake_started_event.is_set(): + self.logger.error("ASGI callable returned without sending handshake.") + self.send_500_response() + elif result is not None: + self.logger.error("ASGI callable should return None, but returned '%s'.", result) + await self.handshake_completed_event.wait() + self.transport.close() + + async def asgi_send(self, message: ASGISendEvent) -> None: + if not self.handshake_started_event.is_set(): + if message["type"] == "websocket.accept": + self.logger.info( + '%s - "WebSocket %s" [accepted]', + get_client_addr(self.scope), + get_path_with_query_string(self.scope), + ) + self.initial_response = None + self.accepted_subprotocol = cast(Subprotocol | None, message.get("subprotocol")) + if "headers" in message: + self.extra_headers.extend( + # ASGI spec requires bytes + # But for compatibility we need to convert it to strings + (name.decode("latin-1"), value.decode("latin-1")) + for name, value in message["headers"] + ) + self.handshake_started_event.set() + + elif message["type"] == "websocket.close": + self.logger.info( + '%s - "WebSocket %s" 403', + get_client_addr(self.scope), + get_path_with_query_string(self.scope), + ) + self.initial_response = (http.HTTPStatus.FORBIDDEN, [], b"") + self.handshake_started_event.set() + self.closed_event.set() + + elif message["type"] == "websocket.http.response.start": + self.logger.info( + '%s - "WebSocket %s" %d', + get_client_addr(self.scope), + get_path_with_query_string(self.scope), + message["status"], + ) + # websockets requires the status to be an enum. look it up. + status = http.HTTPStatus(message["status"]) + headers = [ + (name.decode("latin-1"), value.decode("latin-1")) for name, value in message.get("headers", []) + ] + self.initial_response = (status, headers, b"") + self.handshake_started_event.set() + + else: + raise RuntimeError( + "Expected ASGI message 'websocket.accept', 'websocket.close', " + f"or 'websocket.http.response.start' but got '{message['type']}'." + ) + + elif not self.closed_event.is_set() and self.initial_response is None: + await self.handshake_completed_event.wait() + + try: + if message["type"] == "websocket.send": + bytes_data = message.get("bytes") + text_data = message.get("text") + data = text_data if bytes_data is None else bytes_data + await self.send(data) # type: ignore[arg-type] + + elif message["type"] == "websocket.close": + code = message.get("code", 1000) + reason = message.get("reason", "") or "" + await self.close(code, reason) + self.closed_event.set() + + else: + raise RuntimeError( + f"Expected ASGI message 'websocket.send' or 'websocket.close', but got '{message['type']}'." + ) + except ConnectionClosed as exc: + raise ClientDisconnected from exc + + elif self.initial_response is not None: + if message["type"] == "websocket.http.response.body": + body = self.initial_response[2] + message["body"] + self.initial_response = self.initial_response[:2] + (body,) + if not message.get("more_body", False): + self.closed_event.set() + else: + raise RuntimeError(f"Expected ASGI message 'websocket.http.response.body' but got '{message['type']}'.") + + else: + raise RuntimeError( + f"Unexpected ASGI message '{message['type']}', after sending 'websocket.close' " + "or response already completed." + ) + + async def asgi_receive(self) -> WebSocketDisconnectEvent | WebSocketConnectEvent | WebSocketReceiveEvent: + if not self.connect_sent: + self.connect_sent = True + return {"type": "websocket.connect"} + + await self.handshake_completed_event.wait() + + if self.lost_connection_before_handshake: + # If the handshake failed or the app closed before handshake completion, + # use 1006 Abnormal Closure. + return {"type": "websocket.disconnect", "code": 1006} + + if self.closed_event.is_set(): + return {"type": "websocket.disconnect", "code": 1005} + + try: + data = await self.recv() + except ConnectionClosed: + self.closed_event.set() + if self.ws_server.closing: + return {"type": "websocket.disconnect", "code": 1012} + return {"type": "websocket.disconnect", "code": self.close_code or 1005, "reason": self.close_reason} + + if isinstance(data, str): + return {"type": "websocket.receive", "text": data} + return {"type": "websocket.receive", "bytes": data} diff --git a/source/target-vendors/cp312/uvicorn/protocols/websockets/websockets_sansio_impl.py b/source/target-vendors/cp312/uvicorn/protocols/websockets/websockets_sansio_impl.py new file mode 100644 index 0000000..a444433 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/protocols/websockets/websockets_sansio_impl.py @@ -0,0 +1,556 @@ +from __future__ import annotations + +import asyncio +import email.utils +import logging +import random +import struct +import sys +from asyncio import TimerHandle +from asyncio.transports import BaseTransport, Transport +from http import HTTPStatus +from typing import Any, Literal, cast +from urllib.parse import unquote + +from websockets import __version__ as websockets_version +from websockets.datastructures import Headers +from websockets.exceptions import InvalidState +from websockets.extensions.permessage_deflate import ServerPerMessageDeflateFactory +from websockets.frames import Frame, Opcode +from websockets.http11 import Request, Response +from websockets.server import ServerProtocol + +from uvicorn._types import ( + ASGIReceiveEvent, + ASGISendEvent, + WebSocketScope, +) +from uvicorn.config import Config +from uvicorn.logging import TRACE_LOG_LEVEL +from uvicorn.protocols.utils import ( + ClientDisconnected, + get_client_addr, + get_local_addr, + get_path_with_query_string, + get_remote_addr, + is_ssl, +) +from uvicorn.server import ServerState + +if sys.version_info >= (3, 11): # pragma: no cover + from typing import assert_never +else: # pragma: no cover + from typing_extensions import assert_never + + +def _get_status_phrase(status_code: int) -> str: + try: + return HTTPStatus(status_code).phrase + except ValueError: + return "" + + +STATUS_PHRASES = {status_code: _get_status_phrase(status_code) for status_code in range(100, 600)} + + +class WebSocketsSansIOProtocol(asyncio.Protocol): + def __init__( + self, + config: Config, + server_state: ServerState, + app_state: dict[str, Any], + _loop: asyncio.AbstractEventLoop | None = None, + ) -> None: + if not config.loaded: + config.load() # pragma: no cover + + self.config = config + self.app = config.loaded_app + self.loop = _loop or asyncio.get_event_loop() + self.logger = logging.getLogger("uvicorn.error") + self.root_path = config.root_path + self.asgi_version = config.asgi_version + self.app_state = app_state + + # Shared server state + self.connections = server_state.connections + self.tasks = server_state.tasks + self.default_headers = server_state.default_headers + + # Connection state + self.transport: asyncio.Transport = None # type: ignore[assignment] + self.server: tuple[str, int | None] | None = None + self.client: tuple[str, int] | None = None + self.scheme: Literal["wss", "ws"] = None # type: ignore[assignment] + + # WebSocket state + self.queue: asyncio.Queue[ASGIReceiveEvent] = asyncio.Queue() + self.handshake_initiated = False + self.handshake_complete = False + self.close_sent = False + self.disconnected = False + self.close_timer: TimerHandle | None = None + self.close_timeout = 10.0 + self.initial_response: tuple[int, list[tuple[str, str]], bytes] | None = None + + extensions = [] + if self.config.ws_per_message_deflate: + extensions = [ + ServerPerMessageDeflateFactory( + server_max_window_bits=12, + client_max_window_bits=12, + compress_settings={"memLevel": 5}, + ) + ] + self.conn = ServerProtocol( + extensions=extensions, + max_size=self.config.ws_max_size, + logger=logging.getLogger("uvicorn.error"), + ) + + self.read_paused = False + self.writable = asyncio.Event() + self.writable.set() + + # Keepalive state + self.ping_interval = config.ws_ping_interval + self.ping_timeout = config.ws_ping_timeout + self.ping_timer: TimerHandle | None = None + self.pong_timer: TimerHandle | None = None + self.pending_ping_payload: bytes | None = None + self.ping_sent_at: float = 0.0 + self.last_ping_rtt: float = 0.0 + + # Incoming message state + self.frames: list[bytes] = [] + self.curr_msg_data_type: Literal["text", "bytes"] = "bytes" + + def connection_made(self, transport: BaseTransport) -> None: + """Called when a connection is made.""" + transport = cast(Transport, transport) + self.connections.add(self) + self.transport = transport + self.server = get_local_addr(transport) + self.client = get_remote_addr(transport) + self.scheme = "wss" if is_ssl(transport) else "ws" + + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection made", prefix) + + def connection_lost(self, exc: Exception | None) -> None: + self.stop_keepalive() + code = 1005 if self.handshake_complete else 1006 + self.queue.put_nowait({"type": "websocket.disconnect", "code": code}) + self.connections.remove(self) + + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection lost", prefix) + + self.handshake_complete = True + self.disconnected = True + # Unblock any send() awaiting writable: asyncio never calls resume_writing() on a + # transport that is lost while paused, and the buffer will never drain now. + self.writable.set() + if self.close_timer is not None: + self.close_timer.cancel() + self.close_timer = None + if exc is None: + self.transport.close() + + def eof_received(self) -> None: + pass + + def pause_writing(self) -> None: + """ + Called by the transport when the write buffer exceeds the high water mark. + """ + self.writable.clear() # pragma: full coverage + + def resume_writing(self) -> None: + """ + Called by the transport when the write buffer drops below the low water mark. + """ + self.writable.set() # pragma: full coverage + + def shutdown(self) -> None: + self.stop_keepalive() + if self.close_sent: + if self.close_timer is not None: + self.close_timer.cancel() + self.close_timer = None + self.transport.close() + return + if self.handshake_complete: + self.queue.put_nowait({"type": "websocket.disconnect", "code": 1012}) + self.conn.send_close(1012) + output = self.conn.data_to_send() + self.transport.write(b"".join(output)) + else: + self.send_500_response() + self.transport.close() + + def data_received(self, data: bytes) -> None: + self.conn.receive_data(data) + if self.conn.parser_exc is not None: # pragma: no cover + self.handle_parser_exception() + return + self.handle_events() + + def handle_events(self) -> None: + for event in self.conn.events_received(): + if isinstance(event, Request): + self.handle_connect(event) + if isinstance(event, Frame): + if event.opcode == Opcode.CONT: + self.handle_cont(event) # pragma: no cover + elif event.opcode == Opcode.TEXT: + self.handle_text(event) + elif event.opcode == Opcode.BINARY: + self.handle_bytes(event) + elif event.opcode == Opcode.PING: + self.handle_ping() + elif event.opcode == Opcode.PONG: + self.handle_pong(event) + elif event.opcode == Opcode.CLOSE: + self.handle_close(event) + else: + assert_never(event.opcode) # pragma: no cover + + # Event handlers + + def handle_connect(self, event: Request) -> None: + self.request = event + self.response = self.conn.accept(event) + self.handshake_initiated = True + if self.response.status_code != 101: + self.handshake_complete = True + self.close_sent = True + self.conn.send_response(self.response) + output = self.conn.data_to_send() + self.transport.write(b"".join(output)) + self.transport.close() + return + + # websockets 17.0 documents that non-ASCII header values are encoded + # with ISO-8859-1. Earlier versions didn't document the behavior but + # we can see in the code that it used surrogate escape encoding. + # Move the pragma: no cover to the else: branch when 17.0 is released. + if websockets_version >= "17.0": # pragma: no cover + headers = [(key.encode("ascii"), value.encode("latin-1")) for key, value in event.headers.raw_items()] + else: + headers = [ + (key.encode("ascii"), value.encode("ascii", errors="surrogateescape")) + for key, value in event.headers.raw_items() + ] + raw_path, _, query_string = event.path.partition("?") + subprotocols: list[str] = [] + for header in event.headers.get_all("Sec-WebSocket-Protocol"): + subprotocols.extend([token.strip() for token in header.split(",")]) + self.scope: WebSocketScope = { + "type": "websocket", + "asgi": {"version": self.asgi_version, "spec_version": "2.4"}, + "http_version": "1.1", + "scheme": self.scheme, + "server": self.server, + "client": self.client, + "root_path": self.root_path, + "path": self.root_path + unquote(raw_path), + "raw_path": self.root_path.encode("ascii") + raw_path.encode("ascii"), + "query_string": query_string.encode("ascii"), + "headers": headers, + "subprotocols": subprotocols, + "state": self.app_state.copy(), + "extensions": {"websocket.http.response": {}}, + } + self.queue.put_nowait({"type": "websocket.connect"}) + task = self.loop.create_task(self.run_asgi()) + task.add_done_callback(self.on_task_complete) + self.tasks.add(task) + + def handle_cont(self, event: Frame) -> None: + self.frames.append(event.data) # type: ignore[arg-type] + if event.fin: + self.send_receive_event_to_app() + + def handle_text(self, event: Frame) -> None: + self.curr_msg_data_type = "text" + self.frames = [event.data] # type: ignore[list-item] + if event.fin: + self.send_receive_event_to_app() + + def handle_bytes(self, event: Frame) -> None: + self.curr_msg_data_type = "bytes" + self.frames = [event.data] # type: ignore[list-item] + if event.fin: + self.send_receive_event_to_app() + + def send_receive_event_to_app(self) -> None: + data = self.frames[0] if len(self.frames) == 1 else b"".join(self.frames) + self.frames = [] + if self.close_sent: + # The app is past `websocket.close`: discard the message rather than queueing + # it, so reads stay active until the peer's close reply arrives. + return + if self.curr_msg_data_type == "text": + try: + self.queue.put_nowait({"type": "websocket.receive", "text": data.decode()}) + except UnicodeDecodeError: # pragma: no cover + self.logger.exception("Invalid UTF-8 sequence received from client.") + self.conn.send_close(1007) + self.handle_parser_exception() + return + else: + self.queue.put_nowait({"type": "websocket.receive", "bytes": data}) + if not self.read_paused: + self.read_paused = True + self.transport.pause_reading() + + def handle_ping(self) -> None: + output = self.conn.data_to_send() + self.transport.write(b"".join(output)) + + def handle_pong(self, event: Frame) -> None: + # Ignore unsolicited pongs and stale pongs whose payload doesn't match the ping currently in flight + if self.pending_ping_payload is None or bytes(event.data) != self.pending_ping_payload: + return # pragma: no cover + + self.last_ping_rtt = self.loop.time() - self.ping_sent_at + self.pending_ping_payload = None + # The peer answered in time; cancel the pong deadline and chain the next ping. This `schedule_ping()` call is + # what keeps the keepalive loop running when ping_timeout is set. When ping_timeout is None the next ping is + # already scheduled by `send_keepalive_ping`, so we must not schedule a duplicate here. + if self.pong_timer is not None: + self.pong_timer.cancel() + self.pong_timer = None + self.schedule_ping() + + def start_keepalive(self) -> None: + if self.ping_interval is not None and self.ping_interval > 0: + self.schedule_ping() + + def stop_keepalive(self) -> None: + if self.ping_timer is not None: + self.ping_timer.cancel() + self.ping_timer = None + if self.pong_timer is not None: # pragma: no cover + self.pong_timer.cancel() + self.pong_timer = None + self.pending_ping_payload = None + + def schedule_ping(self) -> None: + assert self.ping_interval is not None + delay = max(0.0, self.ping_interval - self.last_ping_rtt) + self.ping_timer = self.loop.call_later(delay, self.send_keepalive_ping) + + def send_keepalive_ping(self) -> None: + self.ping_timer = None + if self.close_sent or self.transport.is_closing(): # pragma: no cover + return + # Random 4-byte payload identifies this ping; `handle_pong` uses it to ignore stale or unsolicited pongs. + # See https://github.com/python-websockets/websockets/blob/4d229bf9f583d593aa103287aee0a77c9fbc3a79/src/websockets/asyncio/connection.py#L624 + self.pending_ping_payload = struct.pack("!I", random.getrandbits(32)) + self.ping_sent_at = self.loop.time() + self.conn.send_ping(self.pending_ping_payload) + self.transport.write(b"".join(self.conn.data_to_send())) + if self.ping_timeout is not None: + self.pong_timer = self.loop.call_later(self.ping_timeout, self.keepalive_timeout) + else: # pragma: no cover + self.schedule_ping() + + def keepalive_timeout(self) -> None: + self.pong_timer = None + self.pending_ping_payload = None + if self.close_sent or self.transport.is_closing(): # pragma: no cover + return + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket keepalive ping timeout", prefix) + self.conn.fail(1011, "keepalive ping timeout") + self.transport.write(b"".join(self.conn.data_to_send())) + self.close_sent = True + self.transport.close() + + def handle_close(self, event: Frame) -> None: + if self.close_sent: + # The peer echoed our close frame: the closing handshake is complete. + if self.close_timer is not None: + self.close_timer.cancel() + self.close_timer = None + self.transport.close() + return + if not self.transport.is_closing(): + assert self.conn.close_rcvd is not None + code = self.conn.close_rcvd.code + reason = self.conn.close_rcvd.reason + self.queue.put_nowait({"type": "websocket.disconnect", "code": code, "reason": reason}) + + output = self.conn.data_to_send() + self.transport.write(b"".join(output)) + self.transport.close() + + def handle_parser_exception(self) -> None: # pragma: no cover + assert self.conn.close_sent is not None + code = self.conn.close_sent.code + reason = self.conn.close_sent.reason + self.queue.put_nowait({"type": "websocket.disconnect", "code": code, "reason": reason}) + + output = self.conn.data_to_send() + self.transport.write(b"".join(output)) + self.close_sent = True + self.transport.close() + + def on_task_complete(self, task: asyncio.Task[None]) -> None: + self.tasks.discard(task) + + async def run_asgi(self) -> None: + try: + result = await self.app(self.scope, self.receive, self.send) + except ClientDisconnected: + pass # pragma: full coverage + except BaseException: + self.logger.exception("Exception in ASGI application\n") + self.send_500_response() + else: + if not self.handshake_complete: + self.logger.error("ASGI callable returned without completing handshake.") + self.send_500_response() + elif result is not None: + self.logger.error("ASGI callable should return None, but returned '%s'.", result) + if self.close_timer is None: + self.transport.close() + + def send_500_response(self) -> None: + if self.initial_response or self.handshake_complete: + return + response = self.conn.reject(500, "Internal Server Error") + self.conn.send_response(response) + output = self.conn.data_to_send() + self.transport.write(b"".join(output)) + + async def send(self, message: ASGISendEvent) -> None: + await self.writable.wait() + + if self.disconnected: + raise ClientDisconnected() + + if not self.handshake_complete and self.initial_response is None: + if message["type"] == "websocket.accept": + self.logger.info( + '%s - "WebSocket %s" [accepted]', + get_client_addr(self.scope), + get_path_with_query_string(self.scope), + ) + headers = [ + (name.decode("latin-1").lower(), value.decode("latin-1")) + for name, value in (self.default_headers + list(message.get("headers", []))) + ] + accepted_subprotocol = message.get("subprotocol") + if accepted_subprotocol: + headers.append(("Sec-WebSocket-Protocol", accepted_subprotocol)) + del self.response.headers["Date"] + self.response.headers.update(headers) + + if not self.transport.is_closing(): + self.handshake_complete = True + self.conn.send_response(self.response) + output = self.conn.data_to_send() + self.transport.write(b"".join(output)) + self.start_keepalive() + + elif message["type"] == "websocket.close": + self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006}) + self.logger.info( + '%s - "WebSocket %s" 403', + get_client_addr(self.scope), + get_path_with_query_string(self.scope), + ) + response = self.conn.reject(HTTPStatus.FORBIDDEN, "") + self.conn.send_response(response) + output = self.conn.data_to_send() + self.close_sent = True + self.handshake_complete = True + self.transport.write(b"".join(output)) + self.transport.close() + elif message["type"] == "websocket.http.response.start" and self.initial_response is None: + if not (100 <= message["status"] < 600): + raise RuntimeError("Invalid HTTP status code '%d' in response." % message["status"]) + self.logger.info( + '%s - "WebSocket %s" %d', + get_client_addr(self.scope), + get_path_with_query_string(self.scope), + message["status"], + ) + headers = [ + (name.decode("latin-1"), value.decode("latin-1")) + for name, value in list(message.get("headers", [])) + ] + self.initial_response = (message["status"], headers, b"") + else: + raise RuntimeError( + "Expected ASGI message 'websocket.accept', 'websocket.close' " + f"or 'websocket.http.response.start' but got '{message['type']}'." + ) + + elif not self.close_sent and self.initial_response is None: + try: + if message["type"] == "websocket.send": + bytes_data = message.get("bytes") + text_data = message.get("text") + if bytes_data is not None: + self.conn.send_binary(bytes_data) + elif text_data is not None: + self.conn.send_text(text_data.encode()) + output = self.conn.data_to_send() + self.transport.write(b"".join(output)) + + elif message["type"] == "websocket.close": + if not self.transport.is_closing(): + code = message.get("code", 1000) + reason = message.get("reason", "") or "" + self.queue.put_nowait({"type": "websocket.disconnect", "code": code, "reason": reason}) + self.conn.send_close(code, reason) + output = self.conn.data_to_send() + self.transport.write(b"".join(output)) + self.close_sent = True + if self.read_paused: + self.read_paused = False + self.transport.resume_reading() + self.close_timer = self.loop.call_later(self.close_timeout, self.transport.close) + else: + raise RuntimeError( + f"Expected ASGI message 'websocket.send' or 'websocket.close', but got '{message['type']}'." + ) + except InvalidState: + raise ClientDisconnected() + elif self.initial_response is not None: + if message["type"] == "websocket.http.response.body": + body = self.initial_response[2] + message["body"] + self.initial_response = self.initial_response[:2] + (body,) + if not message.get("more_body", False): + status_code = self.initial_response[0] + response_headers = Headers(self.initial_response[1]) + response_headers.setdefault("Date", email.utils.formatdate(usegmt=True)) + response_headers.setdefault("Connection", "close") + response_headers.setdefault("Content-Length", str(len(body))) + response_headers.setdefault("Content-Type", "text/plain; charset=utf-8") + response = Response(status_code, STATUS_PHRASES[status_code], response_headers, body) + self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006}) + self.conn.send_response(response) + output = self.conn.data_to_send() + self.close_sent = True + self.transport.write(b"".join(output)) + self.transport.close() + else: # pragma: no cover + raise RuntimeError(f"Expected ASGI message 'websocket.http.response.body' but got '{message['type']}'.") + + else: + raise RuntimeError(f"Unexpected ASGI message '{message['type']}', after sending 'websocket.close'.") + + async def receive(self) -> ASGIReceiveEvent: + message = await self.queue.get() + if self.read_paused and self.queue.empty(): + self.read_paused = False + self.transport.resume_reading() + return message diff --git a/source/target-vendors/cp312/uvicorn/protocols/websockets/wsproto_impl.py b/source/target-vendors/cp312/uvicorn/protocols/websockets/wsproto_impl.py new file mode 100644 index 0000000..086abfd --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/protocols/websockets/wsproto_impl.py @@ -0,0 +1,500 @@ +from __future__ import annotations + +import asyncio +import logging +import random +import struct +from asyncio import TimerHandle +from io import BytesIO, StringIO +from typing import Any, Literal, cast +from urllib.parse import unquote + +import wsproto +from wsproto import ConnectionType, events +from wsproto.connection import ConnectionState +from wsproto.extensions import Extension, PerMessageDeflate +from wsproto.utilities import LocalProtocolError, RemoteProtocolError + +from uvicorn._types import ( + ASGI3Application, + ASGISendEvent, + WebSocketEvent, + WebSocketReceiveEvent, + WebSocketScope, +) +from uvicorn.config import Config +from uvicorn.logging import TRACE_LOG_LEVEL +from uvicorn.protocols.utils import ( + ClientDisconnected, + get_client_addr, + get_local_addr, + get_path_with_query_string, + get_remote_addr, + is_ssl, +) +from uvicorn.server import ServerState + + +class FrameTooLargeError(Exception): + """Raised when accumulated websocket message bytes exceed `ws_max_size`.""" + + +class WebsocketBuffer: + def __init__(self, max_length: int) -> None: + self.value: BytesIO | StringIO | None = None + self.length = 0 + self.max_length = max_length + + def extend(self, event: events.TextMessage | events.BytesMessage) -> None: + if self.value is None: + self.value = StringIO() if isinstance(event, events.TextMessage) else BytesIO() + self.value.write(event.data) # type: ignore[arg-type] + # `ws_max_size` is a byte budget, so count UTF-8 bytes for text. + self.length += len(event.data.encode()) if isinstance(event, events.TextMessage) else len(event.data) + if self.length > self.max_length: + raise FrameTooLargeError + + def clear(self) -> None: + self.value = None + self.length = 0 + + def to_message(self) -> WebSocketReceiveEvent: + if isinstance(self.value, StringIO): + return {"type": "websocket.receive", "text": self.value.getvalue()} + assert isinstance(self.value, BytesIO) + return {"type": "websocket.receive", "bytes": self.value.getvalue()} + + +class WSProtocol(asyncio.Protocol): + def __init__( + self, + config: Config, + server_state: ServerState, + app_state: dict[str, Any], + _loop: asyncio.AbstractEventLoop | None = None, + ) -> None: + if not config.loaded: + config.load() # pragma: full coverage + + self.config = config + self.app = cast(ASGI3Application, config.loaded_app) + self.loop = _loop or asyncio.get_event_loop() + self.logger = logging.getLogger("uvicorn.error") + self.root_path = config.root_path + self.asgi_version = config.asgi_version + self.app_state = app_state + + # Shared server state + self.connections = server_state.connections + self.tasks = server_state.tasks + self.default_headers = server_state.default_headers + + # Connection state + self.transport: asyncio.Transport = None # type: ignore[assignment] + self.server: tuple[str, int | None] | None = None + self.client: tuple[str, int] | None = None + self.scheme: Literal["wss", "ws"] = None # type: ignore[assignment] + + # WebSocket state + self.queue: asyncio.Queue[WebSocketEvent] = asyncio.Queue() + self.handshake_complete = False + self.close_sent = False + self.disconnected = False + self.close_timer: TimerHandle | None = None + self.close_timeout = 10.0 + + # Rejection state + self.response_started = False + + self.conn = wsproto.WSConnection(connection_type=ConnectionType.SERVER) + + self.read_paused = False + self.writable = asyncio.Event() + self.writable.set() + + # Keepalive state + self.ping_interval = config.ws_ping_interval + self.ping_timeout = config.ws_ping_timeout + self.ping_timer: TimerHandle | None = None + self.pong_timer: TimerHandle | None = None + self.pending_ping_payload: bytes | None = None + self.ping_sent_at: float = 0.0 + self.last_ping_rtt: float = 0.0 + + # Buffer + self.buffer = WebsocketBuffer(self.config.ws_max_size) + + # Protocol interface + + def connection_made(self, transport: asyncio.Transport) -> None: # type: ignore[override] + self.connections.add(self) + self.transport = transport + self.server = get_local_addr(transport) + self.client = get_remote_addr(transport) + self.scheme = "wss" if is_ssl(transport) else "ws" + + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection made", prefix) + + def connection_lost(self, exc: Exception | None) -> None: + self.stop_keepalive() + code = 1005 if self.handshake_complete else 1006 + self.queue.put_nowait({"type": "websocket.disconnect", "code": code}) + self.connections.remove(self) + + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection lost", prefix) + + self.handshake_complete = True + self.disconnected = True + # asyncio never calls resume_writing() when a paused transport is lost. + self.writable.set() + if self.close_timer is not None: + self.close_timer.cancel() + self.close_timer = None + if exc is None: + self.transport.close() + + def eof_received(self) -> None: + pass + + def data_received(self, data: bytes) -> None: + try: + self.conn.receive_data(data) + except RemoteProtocolError as err: + # TODO: Remove `type: ignore` when wsproto fixes the type annotation. + self.transport.write(self.conn.send(err.event_hint)) # type: ignore[arg-type] # noqa: E501 + self.transport.close() + except LocalProtocolError: + # Data received after the close handshake completed, e.g. a pong the client + # sent while the server's close was in flight. + self.transport.close() + else: + self.handle_events() + + def handle_events(self) -> None: + for event in self.conn.events(): + if self.close_sent: + if isinstance(event, events.CloseConnection): + self.handle_close(event) + continue + if isinstance(event, events.Request): + self.handle_connect(event) + elif isinstance(event, (events.TextMessage, events.BytesMessage)): + self.handle_message(event) + elif isinstance(event, events.CloseConnection): + self.handle_close(event) + elif isinstance(event, events.Ping): + self.handle_ping(event) + elif isinstance(event, events.Pong): + self.handle_pong(event) + + def pause_writing(self) -> None: + """ + Called by the transport when the write buffer exceeds the high water mark. + """ + self.writable.clear() # pragma: full coverage + + def resume_writing(self) -> None: + """ + Called by the transport when the write buffer drops below the low water mark. + """ + self.writable.set() # pragma: full coverage + + def shutdown(self) -> None: + self.stop_keepalive() + if self.close_sent: + if self.close_timer is not None: + self.close_timer.cancel() + self.close_timer = None + self.transport.close() + return + if self.handshake_complete: + self.queue.put_nowait({"type": "websocket.disconnect", "code": 1012}) + output = self.conn.send(wsproto.events.CloseConnection(code=1012)) + self.transport.write(output) + else: + self.send_500_response() + self.transport.close() + + def on_task_complete(self, task: asyncio.Task[None]) -> None: + self.tasks.discard(task) + + # Event handlers + + def handle_connect(self, event: events.Request) -> None: + headers = [(b"host", event.host.encode())] + headers += [(key.lower(), value) for key, value in event.extra_headers] + raw_path, _, query_string = event.target.partition("?") + path = unquote(raw_path) + full_path = self.root_path + path + full_raw_path = self.root_path.encode("ascii") + raw_path.encode("ascii") + self.scope: WebSocketScope = { + "type": "websocket", + "asgi": {"version": self.asgi_version, "spec_version": "2.4"}, + "http_version": "1.1", + "scheme": self.scheme, + "server": self.server, + "client": self.client, + "root_path": self.root_path, + "path": full_path, + "raw_path": full_raw_path, + "query_string": query_string.encode("ascii"), + "headers": headers, + "subprotocols": event.subprotocols, + "state": self.app_state.copy(), + "extensions": {"websocket.http.response": {}}, + } + self.queue.put_nowait({"type": "websocket.connect"}) + task = self.loop.create_task(self.run_asgi()) + task.add_done_callback(self.on_task_complete) + self.tasks.add(task) + + def handle_message(self, event: events.TextMessage | events.BytesMessage) -> None: + try: + self.buffer.extend(event) + except FrameTooLargeError: + self.close_sent = True + reason = f"Message exceeds the maximum size ({self.config.ws_max_size} bytes)" + self.queue.put_nowait({"type": "websocket.disconnect", "code": 1009, "reason": reason}) + if not self.transport.is_closing(): + self.transport.write(self.conn.send(wsproto.events.CloseConnection(code=1009, reason=reason))) + self.transport.close() + return + if event.message_finished: + self.queue.put_nowait(self.buffer.to_message()) + self.buffer.clear() + if not self.read_paused: + self.read_paused = True + self.transport.pause_reading() + + def handle_close(self, event: events.CloseConnection) -> None: + if self.close_sent: + # The peer echoed our close frame: the closing handshake is complete. + if self.close_timer is not None: + self.close_timer.cancel() + self.close_timer = None + self.transport.close() + return + if self.conn.state == ConnectionState.REMOTE_CLOSING: + self.transport.write(self.conn.send(event.response())) + self.queue.put_nowait({"type": "websocket.disconnect", "code": event.code, "reason": event.reason}) + self.transport.close() + + def handle_ping(self, event: events.Ping) -> None: + self.transport.write(self.conn.send(event.response())) + + def handle_pong(self, event: events.Pong) -> None: + # Ignore unsolicited pongs and stale pongs whose payload doesn't match the ping currently in flight. + if self.pending_ping_payload is None or bytes(event.payload) != self.pending_ping_payload: + return # pragma: no cover + + self.last_ping_rtt = self.loop.time() - self.ping_sent_at + self.pending_ping_payload = None + # The peer answered in time; cancel the pong deadline and chain the next ping. This `schedule_ping()` call is + # what keeps the keepalive loop running when ping_timeout is set. When ping_timeout is None the next ping is + # already scheduled by `send_keepalive_ping`, so we must not schedule a duplicate here. + if self.pong_timer is not None: + self.pong_timer.cancel() + self.pong_timer = None + self.schedule_ping() + + def start_keepalive(self) -> None: + if self.ping_interval is not None and self.ping_interval > 0: + self.schedule_ping() + + def stop_keepalive(self) -> None: + if self.ping_timer is not None: + self.ping_timer.cancel() + self.ping_timer = None + if self.pong_timer is not None: # pragma: no cover + self.pong_timer.cancel() + self.pong_timer = None + self.pending_ping_payload = None + + def schedule_ping(self) -> None: + assert self.ping_interval is not None + delay = max(0.0, self.ping_interval - self.last_ping_rtt) + self.ping_timer = self.loop.call_later(delay, self.send_keepalive_ping) + + def send_keepalive_ping(self) -> None: + self.ping_timer = None + if self.close_sent or self.transport.is_closing(): # pragma: no cover + return + # Random 4-byte payload identifies this ping; `handle_pong` uses it to ignore stale or unsolicited pongs. + self.pending_ping_payload = struct.pack("!I", random.getrandbits(32)) + self.ping_sent_at = self.loop.time() + self.transport.write(self.conn.send(wsproto.events.Ping(payload=self.pending_ping_payload))) + if self.ping_timeout is not None: + self.pong_timer = self.loop.call_later(self.ping_timeout, self.keepalive_timeout) + else: # pragma: no cover + self.schedule_ping() + + def keepalive_timeout(self) -> None: + self.pong_timer = None + self.pending_ping_payload = None + if self.close_sent or self.transport.is_closing(): # pragma: no cover + return + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket keepalive ping timeout", prefix) + reason = "keepalive ping timeout" + self.transport.write(self.conn.send(wsproto.events.CloseConnection(code=1011, reason=reason))) + self.close_sent = True + self.transport.close() + + def send_500_response(self) -> None: + if self.response_started or self.handshake_complete: + return # we cannot send responses anymore + headers: list[tuple[bytes, bytes]] = [ + (b"content-type", b"text/plain; charset=utf-8"), + (b"connection", b"close"), + (b"content-length", b"21"), + ] + output = self.conn.send(wsproto.events.RejectConnection(status_code=500, headers=headers, has_body=True)) + output += self.conn.send(wsproto.events.RejectData(data=b"Internal Server Error")) + self.transport.write(output) + + async def run_asgi(self) -> None: + try: + result = await self.app(self.scope, self.receive, self.send) # type: ignore[func-returns-value] + except ClientDisconnected: + pass # pragma: full coverage + except BaseException: + self.logger.exception("Exception in ASGI application\n") + self.send_500_response() + else: + if not self.handshake_complete: + self.logger.error("ASGI callable returned without completing handshake.") + self.send_500_response() + elif result is not None: + self.logger.error("ASGI callable should return None, but returned '%s'.", result) + if self.close_timer is None: + self.transport.close() + + async def send(self, message: ASGISendEvent) -> None: + await self.writable.wait() + + if self.disconnected: + raise ClientDisconnected() + + if not self.handshake_complete: + if message["type"] == "websocket.accept": + self.logger.info( + '%s - "WebSocket %s" [accepted]', + get_client_addr(self.scope), + get_path_with_query_string(self.scope), + ) + subprotocol = message.get("subprotocol") + extra_headers = self.default_headers + list(message.get("headers", [])) + extensions: list[Extension] = [] + if self.config.ws_per_message_deflate: + extensions.append(PerMessageDeflate()) + if not self.transport.is_closing(): + self.handshake_complete = True + output = self.conn.send( + wsproto.events.AcceptConnection( + subprotocol=subprotocol, + extensions=extensions, + extra_headers=extra_headers, + ) + ) + self.transport.write(output) + self.start_keepalive() + + elif message["type"] == "websocket.close": + self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006}) + self.logger.info( + '%s - "WebSocket %s" 403', + get_client_addr(self.scope), + get_path_with_query_string(self.scope), + ) + self.handshake_complete = True + self.close_sent = True + event = events.RejectConnection(status_code=403, headers=[]) + output = self.conn.send(event) + self.transport.write(output) + self.transport.close() + + elif message["type"] == "websocket.http.response.start": + # ensure status code is in the valid range + if not (100 <= message["status"] < 600): + msg = "Invalid HTTP status code '%d' in response." + raise RuntimeError(msg % message["status"]) + self.logger.info( + '%s - "WebSocket %s" %d', + get_client_addr(self.scope), + get_path_with_query_string(self.scope), + message["status"], + ) + self.handshake_complete = True + event = events.RejectConnection( + status_code=message["status"], + headers=list(message["headers"]), + has_body=True, + ) + output = self.conn.send(event) + self.transport.write(output) + self.response_started = True + + else: + raise RuntimeError( + "Expected ASGI message 'websocket.accept', 'websocket.close' " + f"or 'websocket.http.response.start' but got '{message['type']}'." + ) + + elif not self.close_sent and not self.response_started: + try: + if message["type"] == "websocket.send": + bytes_data = message.get("bytes") + text_data = message.get("text") + data = text_data if bytes_data is None else bytes_data + output = self.conn.send(wsproto.events.Message(data=data)) # type: ignore + if not self.transport.is_closing(): + self.transport.write(output) + + elif message["type"] == "websocket.close": + self.close_sent = True + code = message.get("code", 1000) + reason = message.get("reason", "") or "" + self.queue.put_nowait({"type": "websocket.disconnect", "code": code, "reason": reason}) + output = self.conn.send(wsproto.events.CloseConnection(code=code, reason=reason)) + if not self.transport.is_closing(): + self.transport.write(output) + if self.read_paused: + self.read_paused = False + self.transport.resume_reading() + self.close_timer = self.loop.call_later(self.close_timeout, self.transport.close) + + else: + raise RuntimeError( + f"Expected ASGI message 'websocket.send' or 'websocket.close', but got '{message['type']}'." + ) + except LocalProtocolError as exc: + raise ClientDisconnected from exc + elif self.response_started: + if message["type"] == "websocket.http.response.body": + body_finished = not message.get("more_body", False) + reject_data = events.RejectData(data=message["body"], body_finished=body_finished) + output = self.conn.send(reject_data) + self.transport.write(output) + + if body_finished: + self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006}) + self.close_sent = True + self.transport.close() + + else: + raise RuntimeError(f"Expected ASGI message 'websocket.http.response.body' but got '{message['type']}'.") + + else: + raise RuntimeError(f"Unexpected ASGI message '{message['type']}', after sending 'websocket.close'.") + + async def receive(self) -> WebSocketEvent: + message = await self.queue.get() + if self.read_paused and self.queue.empty(): + self.read_paused = False + self.transport.resume_reading() + return message diff --git a/source/target-vendors/cp312/uvicorn/py.typed b/source/target-vendors/cp312/uvicorn/py.typed new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/py.typed @@ -0,0 +1 @@ + diff --git a/source/target-vendors/cp312/uvicorn/server.py b/source/target-vendors/cp312/uvicorn/server.py new file mode 100644 index 0000000..0e822a6 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/server.py @@ -0,0 +1,356 @@ +from __future__ import annotations + +import asyncio +import contextlib +import functools +import logging +import os +import platform +import random +import signal +import socket +import sys +import threading +import time +from collections.abc import Generator, Sequence +from email.utils import formatdate +from types import FrameType +from typing import TYPE_CHECKING, TypeAlias + +from uvicorn._ansi import style +from uvicorn._compat import asyncio_run +from uvicorn.config import STARTUP_FAILURE, Config + +if TYPE_CHECKING: + from uvicorn.protocols.http.auto_zttp_impl import AutoZttpProtocol + from uvicorn.protocols.http.h11_impl import H11Protocol + from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol + from uvicorn.protocols.http.zttp_h2_impl import ZttpH2Protocol + from uvicorn.protocols.http.zttp_impl import ZttpProtocol + from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol + from uvicorn.protocols.websockets.websockets_sansio_impl import WebSocketsSansIOProtocol + from uvicorn.protocols.websockets.wsproto_impl import WSProtocol + + Protocols: TypeAlias = ( + H11Protocol + | HttpToolsProtocol + | ZttpProtocol + | ZttpH2Protocol + | AutoZttpProtocol + | WSProtocol + | WebSocketProtocol + | WebSocketsSansIOProtocol + ) + +HANDLED_SIGNALS = ( + signal.SIGINT, # Unix signal 2. Sent by Ctrl+C. + signal.SIGTERM, # Unix signal 15. Sent by `kill `. +) +if sys.platform == "win32": # pragma: py-not-win32 + HANDLED_SIGNALS += (signal.SIGBREAK,) # Windows signal 21. Sent by Ctrl+Break. + +logger = logging.getLogger("uvicorn.error") + + +class ServerState: + """ + Shared servers state that is available between all protocol instances. + """ + + def __init__(self) -> None: + self.total_requests = 0 + self.connections: set[Protocols] = set() + self.tasks: set[asyncio.Task[None]] = set() + self.default_headers: list[tuple[bytes, bytes]] = [] + + +class Server: + def __init__(self, config: Config) -> None: + self.config = config + self.server_state = ServerState() + + self.started = False + self.should_exit = False + self.force_exit = False + self.last_notified = 0.0 + + self._captured_signals: list[int] = [] + + @functools.cached_property + def limit_max_requests(self) -> int | None: + if self.config.limit_max_requests is None: + return None + return self.config.limit_max_requests + random.randint(0, self.config.limit_max_requests_jitter) + + def run(self, sockets: list[socket.socket] | None = None) -> None: + return asyncio_run(self.serve(sockets=sockets), loop_factory=self.config.get_loop_factory()) + + async def serve(self, sockets: list[socket.socket] | None = None) -> None: + with self.capture_signals(): + await self._serve(sockets) + + async def _serve(self, sockets: list[socket.socket] | None = None) -> None: + process_id = os.getpid() + + config = self.config + if not config.loaded: + config.load() + + self.lifespan = config.lifespan_class(config) + + message = "Started server process [%d]" + color_message = "Started server process [" + style("%d", fg="cyan") + "]" + logger.info(message, process_id, extra={"color_message": color_message}) + + await self.startup(sockets=sockets) + if not self.should_exit: + await self.main_loop() + if self.started: + await self.shutdown(sockets=sockets) + + message = "Finished server process [%d]" + color_message = "Finished server process [" + style("%d", fg="cyan") + "]" + logger.info(message, process_id, extra={"color_message": color_message}) + + async def startup(self, sockets: list[socket.socket] | None = None) -> None: + await self.lifespan.startup() + if self.lifespan.should_exit: + sys.exit(STARTUP_FAILURE) + + config = self.config + + def create_protocol( + _loop: asyncio.AbstractEventLoop | None = None, + ) -> asyncio.Protocol: + return config.http_protocol_class( # type: ignore[call-arg] + config=config, + server_state=self.server_state, + app_state=self.lifespan.state, + _loop=_loop, + ) + + loop = asyncio.get_running_loop() + + listeners: Sequence[socket.SocketType] + if sockets is not None: # pragma: full coverage + # Explicitly passed a list of open sockets. + # We use this when the server is run from a Gunicorn worker. + + def _share_socket( + sock: socket.SocketType, + ) -> socket.SocketType: # pragma py-not-win32 + # Windows requires the socket be explicitly shared across + # multiple workers (processes). + from socket import fromshare # type: ignore[attr-defined] + + sock_data = sock.share(os.getpid()) # type: ignore[attr-defined] + return fromshare(sock_data) + + self.servers: list[asyncio.base_events.Server] = [] + for sock in sockets: + is_windows = platform.system() == "Windows" + if config.workers > 1 and is_windows: # pragma: py-not-win32 + sock = _share_socket(sock) # type: ignore[assignment] + server = await loop.create_server(create_protocol, sock=sock, ssl=config.ssl, backlog=config.backlog) + self.servers.append(server) + listeners = sockets + + elif config.fd is not None: # pragma: py-win32 + # Use an existing socket, from a file descriptor. + sock = socket.fromfd(config.fd, socket.AF_UNIX, socket.SOCK_STREAM) + server = await loop.create_server(create_protocol, sock=sock, ssl=config.ssl, backlog=config.backlog) + assert server.sockets is not None # mypy + listeners = server.sockets + self.servers = [server] + + elif config.uds is not None: # pragma: py-win32 + # Create a socket using UNIX domain socket. + uds_perms = 0o666 + if os.path.exists(config.uds): + uds_perms = os.stat(config.uds).st_mode # pragma: full coverage + server = await loop.create_unix_server( + create_protocol, path=config.uds, ssl=config.ssl, backlog=config.backlog + ) + os.chmod(config.uds, uds_perms) + assert server.sockets is not None # mypy + listeners = server.sockets + self.servers = [server] + + else: + # Standard case. Create a socket from a host/port pair. + try: + server = await loop.create_server( + create_protocol, + host=config.host, + port=config.port, + ssl=config.ssl, + backlog=config.backlog, + ) + except OSError as exc: + logger.error(exc) + await self.lifespan.shutdown() + sys.exit(STARTUP_FAILURE) + + assert server.sockets is not None + listeners = server.sockets + self.servers = [server] + + if sockets is None: + self._log_started_message(listeners) + else: + # We're most likely running multiple workers, so a message has already been + # logged by `config.bind_socket()`. + pass # pragma: full coverage + + self.started = True + + def _log_started_message(self, listeners: Sequence[socket.SocketType]) -> None: + config = self.config + + if config.fd is not None: # pragma: py-win32 + sock = listeners[0] + logger.info( + "Uvicorn running on socket %s (Press CTRL+C to quit)", + sock.getsockname(), + ) + + elif config.uds is not None: # pragma: py-win32 + logger.info("Uvicorn running on unix socket %s (Press CTRL+C to quit)", config.uds) + + else: + addr_format = "%s://%s:%d" + host = "0.0.0.0" if config.host is None else config.host + if ":" in host: + # It's an IPv6 address. + addr_format = "%s://[%s]:%d" + + port = config.port + if port == 0: + port = listeners[0].getsockname()[1] + + protocol_name = "https" if config.ssl else "http" + message = f"Uvicorn running on {addr_format} (Press CTRL+C to quit)" + color_message = "Uvicorn running on " + style(addr_format, bold=True) + " (Press CTRL+C to quit)" + logger.info( + message, + protocol_name, + host, + port, + extra={"color_message": color_message}, + ) + + async def main_loop(self) -> None: + counter = 0 + should_exit = await self.on_tick(counter) + while not should_exit: + counter += 1 + counter = counter % 864000 + await asyncio.sleep(0.1) + should_exit = await self.on_tick(counter) + + async def on_tick(self, counter: int) -> bool: + # Update the default headers, once per second. + if counter % 10 == 0: + current_time = time.time() + current_date = formatdate(current_time, usegmt=True).encode() + + if self.config.date_header: + date_header = [(b"date", current_date)] + else: + date_header = [] + + self.server_state.default_headers = date_header + self.config.encoded_headers + + # Callback to `callback_notify` once every `timeout_notify` seconds. + if self.config.callback_notify is not None: + if current_time - self.last_notified > self.config.timeout_notify: # pragma: full coverage + self.last_notified = current_time + await self.config.callback_notify() + + # Determine if we should exit. + if self.should_exit: + return True + + max_requests = self.limit_max_requests + if max_requests is not None and self.server_state.total_requests >= max_requests: + logger.info("Maximum request limit of %d exceeded. Terminating process.", max_requests) + return True + + return False + + async def shutdown(self, sockets: list[socket.socket] | None = None) -> None: + logger.info("Shutting down") + + # Stop accepting new connections. + for server in self.servers: + server.close() + for sock in sockets or []: + sock.close() # pragma: full coverage + + # Request shutdown on all existing connections. + for connection in list(self.server_state.connections): + connection.shutdown() + await asyncio.sleep(0.1) + + # When 3.10 is not supported anymore, use `async with asyncio.timeout(...):`. + try: + await asyncio.wait_for( + self._wait_tasks_to_complete(), + timeout=self.config.timeout_graceful_shutdown, + ) + except asyncio.TimeoutError: + logger.error( + "Cancel %s running task(s), timeout graceful shutdown exceeded", + len(self.server_state.tasks), + ) + for t in self.server_state.tasks: + t.cancel(msg="Task cancelled, timeout graceful shutdown exceeded") + + # Send the lifespan shutdown event, and wait for application shutdown. + if not self.force_exit: + await self.lifespan.shutdown() + + async def _wait_tasks_to_complete(self) -> None: + # Wait for existing connections to finish sending responses. + if self.server_state.connections and not self.force_exit: + msg = "Waiting for connections to close. (CTRL+C to force quit)" + logger.info(msg) + while self.server_state.connections and not self.force_exit: + await asyncio.sleep(0.1) + + # Wait for existing tasks to complete. + if self.server_state.tasks and not self.force_exit: + msg = "Waiting for background tasks to complete. (CTRL+C to force quit)" + logger.info(msg) + while self.server_state.tasks and not self.force_exit: + await asyncio.sleep(0.1) + + for server in self.servers: + await server.wait_closed() + + @contextlib.contextmanager + def capture_signals(self) -> Generator[None, None, None]: + # Signals can only be listened to from the main thread. + if threading.current_thread() is not threading.main_thread(): + yield + return + # always use signal.signal, even if loop.add_signal_handler is available + # this allows to restore previous signal handlers later on + original_handlers = {sig: signal.signal(sig, self.handle_exit) for sig in HANDLED_SIGNALS} + try: + yield + finally: + for sig, handler in original_handlers.items(): + signal.signal(sig, handler) + # If we did gracefully shut down due to a signal, try to + # trigger the expected behaviour now; multiple signals would be + # done LIFO, see https://stackoverflow.com/questions/48434964 + for captured_signal in reversed(self._captured_signals): + signal.raise_signal(captured_signal) + + def handle_exit(self, sig: int, frame: FrameType | None) -> None: + self._captured_signals.append(sig) + if self.should_exit and sig == signal.SIGINT: + self.force_exit = True # pragma: full coverage + else: + self.should_exit = True diff --git a/source/target-vendors/cp312/uvicorn/supervisors/__init__.py b/source/target-vendors/cp312/uvicorn/supervisors/__init__.py new file mode 100644 index 0000000..cfceb6b --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/supervisors/__init__.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from uvicorn.supervisors.basereload import BaseReload +from uvicorn.supervisors.multiprocess import Multiprocess + +if TYPE_CHECKING: + ChangeReload: type[BaseReload] +else: + try: + from uvicorn.supervisors.watchfilesreload import WatchFilesReload as ChangeReload + except ImportError: # pragma: no cover + from uvicorn.supervisors.statreload import StatReload as ChangeReload + +__all__ = ["Multiprocess", "ChangeReload"] diff --git a/source/target-vendors/cp312/uvicorn/supervisors/basereload.py b/source/target-vendors/cp312/uvicorn/supervisors/basereload.py new file mode 100644 index 0000000..d8cb477 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/supervisors/basereload.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +import logging +import os +import signal +import sys +import threading +from collections.abc import Callable, Iterator +from pathlib import Path +from socket import socket +from types import FrameType + +from uvicorn._ansi import style +from uvicorn._subprocess import get_subprocess +from uvicorn.config import Config + +HANDLED_SIGNALS = ( + signal.SIGINT, # Unix signal 2. Sent by Ctrl+C. + signal.SIGTERM, # Unix signal 15. Sent by `kill `. +) + +logger = logging.getLogger("uvicorn.error") + + +class BaseReload: + def __init__( + self, + config: Config, + target: Callable[[list[socket] | None], None], + sockets: list[socket], + ) -> None: + self.config = config + self.target = target + self.sockets = sockets + self.should_exit = threading.Event() + self.pid = os.getpid() + self.is_restarting = False + self.reloader_name: str | None = None + + def signal_handler(self, sig: int, frame: FrameType | None) -> None: # pragma: full coverage + """ + A signal handler that is registered with the parent process. + """ + if sys.platform == "win32" and self.is_restarting: + self.is_restarting = False + else: + self.should_exit.set() + + def run(self) -> None: + self.startup() + for changes in self: + if changes: + logger.warning( + "%s detected changes in %s. Reloading...", + self.reloader_name, + ", ".join(map(_display_path, changes)), + ) + self.restart() + + self.shutdown() + + def pause(self) -> None: + if self.should_exit.wait(self.config.reload_delay): + raise StopIteration() + + def __iter__(self) -> Iterator[list[Path] | None]: + return self + + def __next__(self) -> list[Path] | None: + return self.should_restart() + + def startup(self) -> None: + message = f"Started reloader process [{self.pid}] using {self.reloader_name}" + color_message = "Started reloader process [{}] using {}".format( + style(str(self.pid), fg="cyan", bold=True), + style(str(self.reloader_name), fg="cyan", bold=True), + ) + logger.info(message, extra={"color_message": color_message}) + + for sig in HANDLED_SIGNALS: + signal.signal(sig, self.signal_handler) + + self.process = get_subprocess(config=self.config, target=self.target, sockets=self.sockets) + self.process.start() + + def restart(self) -> None: + if sys.platform == "win32": # pragma: py-not-win32 + self.is_restarting = True + assert self.process.pid is not None + os.kill(self.process.pid, signal.CTRL_C_EVENT) + + # This is a workaround to ensure the Ctrl+C event is processed + sys.stdout.write(" ") # This has to be a non-empty string + sys.stdout.flush() + else: # pragma: py-win32 + self.process.terminate() + self.process.join() + + self.process = get_subprocess(config=self.config, target=self.target, sockets=self.sockets) + self.process.start() + + def shutdown(self) -> None: + if sys.platform == "win32": + self.should_exit.set() # pragma: py-not-win32 + else: + self.process.terminate() # pragma: py-win32 + self.process.join() + + for sock in self.sockets: + sock.close() + + message = f"Stopping reloader process [{str(self.pid)}]" + color_message = "Stopping reloader process [{}]".format(style(str(self.pid), fg="cyan", bold=True)) + logger.info(message, extra={"color_message": color_message}) + + def should_restart(self) -> list[Path] | None: + raise NotImplementedError("Reload strategies should override should_restart()") + + +def _display_path(path: Path) -> str: + try: + return f"'{path.relative_to(Path.cwd())}'" + except ValueError: + return f"'{path}'" diff --git a/source/target-vendors/cp312/uvicorn/supervisors/multiprocess.py b/source/target-vendors/cp312/uvicorn/supervisors/multiprocess.py new file mode 100644 index 0000000..ef2dd2d --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/supervisors/multiprocess.py @@ -0,0 +1,282 @@ +from __future__ import annotations + +import logging +import os +import pickle +import signal +import threading +import time +from multiprocessing import Pipe +from socket import socket + +from uvicorn._ansi import style +from uvicorn._subprocess import get_subprocess +from uvicorn.config import STARTUP_FAILURE, Config +from uvicorn.server import Server + +SIGNALS = { + getattr(signal, f"SIG{x}"): x + for x in "INT TERM BREAK HUP QUIT TTIN TTOU USR1 USR2 WINCH".split() + if hasattr(signal, f"SIG{x}") +} + +logger = logging.getLogger("uvicorn.error") + + +class Process: + def __init__( + self, + config: Config, + sockets: list[socket], + ) -> None: + self.config = config + self._server: Server | None = None + + self.parent_conn, self.child_conn = Pipe() + self.process = get_subprocess(config, self.target, sockets) + + def _healthcheck(self, timeout: float) -> bool | None: + """Receives a timeout and returns the worker's startup flag, or None if it does not answer in time.""" + try: + self.parent_conn.send(b"ping") + if self.parent_conn.poll(timeout): + started: bool = self.parent_conn.recv() + return started + return None + except (OSError, EOFError, pickle.UnpicklingError): + return None + + def ping(self, timeout: float = 5) -> bool: + """Receives a timeout and returns True if the worker answers a healthcheck in time.""" + return self._healthcheck(timeout) is not None + + def is_ready(self, timeout: float = 5) -> bool: + """Receives a timeout and returns True if the worker answers and its server has finished startup.""" + return self._healthcheck(timeout) is True + + def pong(self) -> None: + """Receives one healthcheck ping and replies with whether the server has finished startup.""" + self.child_conn.recv() + self.child_conn.send(self.server.started) + + def always_pong(self) -> None: + while True: + self.pong() + + def target(self, sockets: list[socket] | None = None) -> None: # pragma: no cover + if os.name == "nt": # pragma: py-not-win32 + # Windows doesn't support SIGTERM, so we use SIGBREAK instead. + # And then we raise SIGTERM when SIGBREAK is received. + # https://learn.microsoft.com/zh-cn/cpp/c-runtime-library/reference/signal?view=msvc-170 + signal.signal( + signal.SIGBREAK, # type: ignore[attr-defined] + lambda sig, frame: signal.raise_signal(signal.SIGTERM), + ) + + threading.Thread(target=self.always_pong, daemon=True).start() + self.server.run(sockets) + + def is_alive(self, timeout: float = 5) -> bool: + if not self.process.is_alive(): + return False # pragma: full coverage + + return self.ping(timeout) + + def wait_until_ready(self, timeout: float, should_exit: threading.Event | None = None) -> bool: + """Receives a timeout and returns True if the worker starts serving, or False on exit, shutdown, or timeout.""" + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if should_exit is not None and should_exit.is_set(): + return False + if not self.process.is_alive(): + return False + if self.is_ready(timeout=1): + return True + time.sleep(0.1) + return False + + def start(self) -> None: + self.process.start() + + def terminate(self) -> None: + if self.process.exitcode is None: # Process is still running + assert self.process.pid is not None + if os.name == "nt": # pragma: py-not-win32 + # Windows doesn't support SIGTERM. + # So send SIGBREAK, and then in process raise SIGTERM. + os.kill(self.process.pid, signal.CTRL_BREAK_EVENT) # type: ignore[attr-defined] + else: + os.kill(self.process.pid, signal.SIGTERM) + logger.info(f"Terminated child process [{self.process.pid}]") + + self.parent_conn.close() + self.child_conn.close() + + def kill(self) -> None: + # In Windows, the method will call `TerminateProcess` to kill the process. + # In Unix, the method will send SIGKILL to the process. + self.process.kill() + self.parent_conn.close() + self.child_conn.close() + + def join(self) -> None: + logger.info(f"Waiting for child process [{self.process.pid}]") + self.process.join() + + @property + def server(self) -> Server: + if self._server is None: + self._server = Server(config=self.config) + return self._server + + @property + def pid(self) -> int | None: + return self.process.pid + + @property + def exitcode(self) -> int | None: + return self.process.exitcode + + +class Multiprocess: + def __init__( + self, + config: Config, + sockets: list[socket], + ) -> None: + self.config = config + self.sockets = sockets + + self.processes_num = config.workers + self.processes: list[Process] = [] + + self.should_exit = threading.Event() + + self.signal_queue: list[int] = [] + for sig in SIGNALS: + signal.signal(sig, lambda sig, frame: self.signal_queue.append(sig)) + + def init_processes(self) -> None: + for _ in range(self.processes_num): + process = Process(self.config, self.sockets) + process.start() + self.processes.append(process) + + def terminate_all(self) -> None: + for process in self.processes: + process.terminate() + + def join_all(self) -> None: + for process in self.processes: + process.join() + + def restart_all(self) -> None: + """Replaces each worker, bringing its replacement into service before retiring the old worker.""" + for idx, old_process in enumerate(self.processes): + if self.should_exit.is_set(): + return + + new_process = Process(self.config, self.sockets) + new_process.start() + + if not new_process.wait_until_ready(self.config.timeout_worker_healthcheck, self.should_exit): + new_process.kill() + new_process.join() + if not self.should_exit.is_set(): + logger.error( + f"New child process [{new_process.pid}] was not ready in time; " + f"keeping worker [{old_process.pid}] and aborting the restart." + ) + return + + old_process.terminate() + old_process.join() + self.processes[idx] = new_process + + def run(self) -> None: + message = f"Started parent process [{os.getpid()}]" + color_message = "Started parent process [{}]".format(style(str(os.getpid()), fg="cyan", bold=True)) + logger.info(message, extra={"color_message": color_message}) + + self.init_processes() + + while not self.should_exit.wait(0.5): + self.handle_signals() + self.keep_subprocess_alive() + + self.terminate_all() + self.join_all() + + message = f"Stopping parent process [{os.getpid()}]" + color_message = "Stopping parent process [{}]".format(style(str(os.getpid()), fg="cyan", bold=True)) + logger.info(message, extra={"color_message": color_message}) + + def keep_subprocess_alive(self) -> None: + if self.should_exit.is_set(): + return # parent process is exiting, no need to keep subprocess alive + + for idx, process in enumerate(self.processes): + if process.is_alive(timeout=self.config.timeout_worker_healthcheck): + continue + + process.kill() # process is hung, kill it + process.join() + + if process.exitcode == STARTUP_FAILURE: + # The worker failed before it started serving, so the app, TLS or socket + # bind is broken and would fail the same way on every restart. + # See https://github.com/encode/uvicorn/discussions/2440. + logger.error(f"Child process [{process.pid}] failed to start, stopping the parent process.") + self.should_exit.set() + return + + if self.should_exit.is_set(): + return # pragma: full coverage + + logger.info(f"Child process [{process.pid}] died") + process = Process(self.config, self.sockets) + process.start() + self.processes[idx] = process + + def handle_signals(self) -> None: + for sig in tuple(self.signal_queue): + self.signal_queue.remove(sig) + sig_name = SIGNALS[sig] + sig_handler = getattr(self, f"handle_{sig_name.lower()}", None) + if sig_handler is not None: + sig_handler() + else: # pragma: no cover + logger.debug(f"Received signal {sig_name}, but no handler is defined for it.") + + def handle_int(self) -> None: + logger.info("Received SIGINT, exiting.") + self.should_exit.set() + + def handle_term(self) -> None: + logger.info("Received SIGTERM, exiting.") + self.should_exit.set() + + def handle_break(self) -> None: # pragma: py-not-win32 + logger.info("Received SIGBREAK, exiting.") + self.should_exit.set() + + def handle_hup(self) -> None: # pragma: py-win32 + logger.info("Received SIGHUP, restarting processes.") + self.restart_all() + + def handle_ttin(self) -> None: # pragma: py-win32 + logger.info("Received SIGTTIN, increasing the number of processes.") + self.processes_num += 1 + process = Process(self.config, self.sockets) + process.start() + self.processes.append(process) + + def handle_ttou(self) -> None: # pragma: py-win32 + logger.info("Received SIGTTOU, decreasing number of processes.") + if self.processes_num <= 1: + logger.info("Already reached one process, cannot decrease the number of processes anymore.") + return + self.processes_num -= 1 + process = self.processes.pop() + process.terminate() + process.join() diff --git a/source/target-vendors/cp312/uvicorn/supervisors/statreload.py b/source/target-vendors/cp312/uvicorn/supervisors/statreload.py new file mode 100644 index 0000000..28693f9 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/supervisors/statreload.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import logging +from collections.abc import Callable, Iterator +from pathlib import Path +from socket import socket + +from uvicorn.config import Config +from uvicorn.supervisors.basereload import BaseReload + +logger = logging.getLogger("uvicorn.error") + + +class StatReload(BaseReload): + def __init__( + self, + config: Config, + target: Callable[[list[socket] | None], None], + sockets: list[socket], + ) -> None: + super().__init__(config, target, sockets) + self.reloader_name = "StatReload" + self.mtimes: dict[Path, float] = {} + + if config.reload_excludes or config.reload_includes: + logger.warning("--reload-include and --reload-exclude have no effect unless watchfiles is installed.") + + def should_restart(self) -> list[Path] | None: + self.pause() + + for file in self.iter_py_files(): + try: + mtime = file.stat().st_mtime + except OSError: # pragma: nocover + continue + + old_time = self.mtimes.get(file) + if old_time is None: + self.mtimes[file] = mtime + continue + elif mtime > old_time: + return [file] + return None + + def restart(self) -> None: + self.mtimes = {} + return super().restart() + + def iter_py_files(self) -> Iterator[Path]: + for reload_dir in self.config.reload_dirs: + for path in list(reload_dir.rglob("*.py")): + yield path.resolve() diff --git a/source/target-vendors/cp312/uvicorn/supervisors/watchfilesreload.py b/source/target-vendors/cp312/uvicorn/supervisors/watchfilesreload.py new file mode 100644 index 0000000..6009905 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/supervisors/watchfilesreload.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path +from socket import socket + +from watchfiles import watch + +from uvicorn.config import Config +from uvicorn.supervisors.basereload import BaseReload + + +class FileFilter: + def __init__(self, config: Config): + default_includes = ["*.py"] + self.includes = [default for default in default_includes if default not in config.reload_excludes] + self.includes.extend(config.reload_includes) + self.includes = list(set(self.includes)) + + default_excludes = [".*", ".py[cod]", ".sw.*", "~*"] + self.excludes = [default for default in default_excludes if default not in config.reload_includes] + self.exclude_dirs = [] + for e in config.reload_excludes: + p = Path(e) + try: + is_dir = p.is_dir() + except OSError: # pragma: no cover + # gets raised on Windows for values like "*.py" + is_dir = False + + if is_dir: + self.exclude_dirs.append(p) + else: + self.excludes.append(e) # pragma: full coverage + self.excludes = list(set(self.excludes)) + + def __call__(self, path: Path) -> bool: + for include_pattern in self.includes: + if path.match(include_pattern): + if str(path).endswith(include_pattern): + return True # pragma: full coverage + + for exclude_dir in self.exclude_dirs: + if exclude_dir in path.parents: + return False + + for exclude_pattern in self.excludes: + if path.match(exclude_pattern): + return False # pragma: full coverage + + return True + return False + + +class WatchFilesReload(BaseReload): + def __init__( + self, + config: Config, + target: Callable[[list[socket] | None], None], + sockets: list[socket], + ) -> None: + super().__init__(config, target, sockets) + self.reloader_name = "WatchFiles" + self.reload_dirs: list[Path] = [] + for directory in config.reload_dirs: + self.reload_dirs.append(directory) + + self.watch_filter = FileFilter(config) + self.watcher = watch( + *self.reload_dirs, + watch_filter=None, + stop_event=self.should_exit, + # using yield_on_timeout here mostly to make sure tests don't + # hang forever, won't affect the class's behavior + yield_on_timeout=True, + ignore_permission_denied=True, + ) + + def should_restart(self) -> list[Path] | None: + self.pause() + + changes = next(self.watcher) + if changes: + unique_paths = {Path(c[1]) for c in changes} + return [p for p in unique_paths if self.watch_filter(p)] + return None diff --git a/source/target-vendors/cp312/uvicorn/workers.py b/source/target-vendors/cp312/uvicorn/workers.py new file mode 100644 index 0000000..827d091 --- /dev/null +++ b/source/target-vendors/cp312/uvicorn/workers.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import asyncio +import logging +import signal +import sys +import warnings +from typing import Any + +from gunicorn.arbiter import Arbiter +from gunicorn.workers.base import Worker + +from uvicorn._compat import asyncio_run +from uvicorn.config import Config +from uvicorn.server import Server + +warnings.warn( + "The `uvicorn.workers` module is deprecated. Please use `uvicorn-worker` package instead.\n" + "For more details, see https://github.com/Kludex/uvicorn-worker.", + DeprecationWarning, +) + + +class UvicornWorker(Worker): + """ + A worker class for Gunicorn that interfaces with an ASGI consumer callable, + rather than a WSGI callable. + """ + + CONFIG_KWARGS: dict[str, Any] = {"loop": "auto", "http": "auto"} + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + logger = logging.getLogger("uvicorn.error") + logger.handlers = self.log.error_log.handlers + logger.setLevel(self.log.error_log.level) + logger.propagate = False + + logger = logging.getLogger("uvicorn.access") + logger.handlers = self.log.access_log.handlers + logger.setLevel(self.log.access_log.level) + logger.propagate = False + + config_kwargs: dict = { + "app": None, + "log_config": None, + "timeout_keep_alive": self.cfg.keepalive, + "timeout_notify": self.timeout, + "callback_notify": self.callback_notify, + "limit_max_requests": self.max_requests, + "forwarded_allow_ips": self.cfg.forwarded_allow_ips, + } + + if self.cfg.is_ssl: + ssl_kwargs = { + "ssl_keyfile": self.cfg.ssl_options.get("keyfile"), + "ssl_certfile": self.cfg.ssl_options.get("certfile"), + "ssl_keyfile_password": self.cfg.ssl_options.get("password"), + "ssl_version": self.cfg.ssl_options.get("ssl_version"), + "ssl_cert_reqs": self.cfg.ssl_options.get("cert_reqs"), + "ssl_ca_certs": self.cfg.ssl_options.get("ca_certs"), + "ssl_ciphers": self.cfg.ssl_options.get("ciphers"), + } + config_kwargs.update(ssl_kwargs) + + if self.cfg.settings["backlog"].value: + config_kwargs["backlog"] = self.cfg.settings["backlog"].value + + config_kwargs.update(self.CONFIG_KWARGS) + + self.config = Config(**config_kwargs) + + def init_signals(self) -> None: + # Reset signals so Gunicorn doesn't swallow subprocess return codes + # other signals are set up by Server.install_signal_handlers() + # See: https://github.com/Kludex/uvicorn/issues/894 + for s in self.SIGNALS: + signal.signal(s, signal.SIG_DFL) + + signal.signal(signal.SIGUSR1, self.handle_usr1) + # Don't let SIGUSR1 disturb active requests by interrupting system calls + signal.siginterrupt(signal.SIGUSR1, False) + + def _install_sigquit_handler(self) -> None: + """Install a SIGQUIT handler on workers. + + - https://github.com/Kludex/uvicorn/issues/1116 + - https://github.com/benoitc/gunicorn/issues/2604 + """ + + loop = asyncio.get_running_loop() + loop.add_signal_handler(signal.SIGQUIT, self.handle_exit, signal.SIGQUIT, None) + + async def _serve(self) -> None: + self.config.app = self.wsgi + server = Server(config=self.config) + self._install_sigquit_handler() + await server.serve(sockets=self.sockets) + if not server.started: + sys.exit(Arbiter.WORKER_BOOT_ERROR) + + def run(self) -> None: + return asyncio_run(self._serve(), loop_factory=self.config.get_loop_factory()) + + async def callback_notify(self) -> None: + self.notify() + + +class UvicornH11Worker(UvicornWorker): + CONFIG_KWARGS = {"loop": "asyncio", "http": "h11"} diff --git a/source/tests/test_archive_targets.py b/source/tests/test_archive_targets.py index 49516cb..7866839 100644 --- a/source/tests/test_archive_targets.py +++ b/source/tests/test_archive_targets.py @@ -31,7 +31,8 @@ class CandidateArchiveTests(unittest.TestCase): mock.patch.object(archive_sources, "validate_vendor", return_value=evidence), \ contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()) as stderr: archive_sources.main(args) - return self.base / "release/candidate-source" / archive_sources.ARCHIVE_NAME, stderr.getvalue() + directory = "public-source" if "--public-release" in args else "candidate-source" + return self.base / "release" / directory / archive_sources.ARCHIVE_NAME, stderr.getvalue() def test_default_archive_declares_missing_alternate_abis(self): output, warning = self.run_archive([]) @@ -64,6 +65,25 @@ class CandidateArchiveTests(unittest.TestCase): self.assertEqual(set(data["vendors"]), {"cp312", "cp313", "cp314"}) self.assertEqual(data["omitted_vendor_abis"], []) + def test_public_archive_requires_matching_cp312_and_excludes_experimental_input(self): + for args in (["--public-release"], ["--public-release", "--vendor-cp312", str(self.root), "--vendor-cp314", str(self.root)]): + with self.assertRaises(SystemExit): + self.run_archive(args) + + def test_public_archive_describes_three_profiles_without_claiming_publication(self): + vendor = self.root / "cp312"; vendor.mkdir() + (vendor / "module.py").write_text("# reviewed cp312 input\n") + output, warning = self.run_archive(["--public-release", "--vendor-cp312", str(vendor)]) + self.assertFalse(warning) + manifest = json.loads((output.parent / "SOURCE-ARCHIVE-MANIFEST.json").read_text()) + self.assertEqual(manifest["status"], "RELEASE_SOURCE_ASSEMBLED") + self.assertEqual(set(manifest["vendor_inputs"]["vendors"]), {"cp312", "cp313"}) + with tarfile.open(output) as archive: + readme = archive.extractfile(archive_sources.ARCHIVE_ROOT + "/README-SOURCE-ARCHIVE.md").read().decode() + self.assertIn("--public-release --source-manifest", readme) + self.assertNotIn("compatibility candidate source", readme) + self.assertIn("Signatures, publication", readme) + def test_alternate_vendor_symlink_is_rejected(self): vendor = self.root / "cp312"; vendor.mkdir() (vendor / "unsafe.py").symlink_to(self.input) diff --git a/source/tests/test_autostart.py b/source/tests/test_autostart.py index 16c3bf1..e01ae02 100644 --- a/source/tests/test_autostart.py +++ b/source/tests/test_autostart.py @@ -9,7 +9,7 @@ import subprocess import tempfile import unittest from unittest import mock -from framebridge_expanded import autostart, config, runtime, connection_background +from framebridge_expanded import autostart, config, runtime, connection_background, credential_helper ID = "tunnel_0123456789abcdef0123456789abcdef" MARKER = "FRAMEBRIDGE-SYNTHETIC-NOT-A-REAL-KEY-AUTOSTART" @@ -435,6 +435,161 @@ class AutostartTests(unittest.TestCase): self.assertNotIn("LoadCredentialEncrypted", unit.read_text()) self.assertFalse((directory / "credential.cred").exists()) + def test_fresh_node_with_existing_linger_authenticates_before_existing_grant_cleanup(self): + for start_now in (False, True): + with self.subTest(start_now=start_now): + state = self.home / ("node-now" if start_now else "node-boot") + config.save_config(state, config.default_config(role="node")) + before = (state / "config.json").read_bytes() + self.calls.clear() + self.system_calls.clear() + with mock.patch.object(autostart, "_linger", return_value=True), mock.patch.object(autostart, "_helper_present", return_value=True), mock.patch.object(autostart, "_node_helper_cleanup_required", return_value=True), mock.patch.object(autostart, "_credential_backend", side_effect=AssertionError("A node has no connection credential")): + autostart._enable(state, start_now=start_now, confirm=False) + _, directory, unit = autostart._paths(state) + expected_prune = ["sudo", "-n", "--", str(autostart.HELPER), "prune", autostart._profile_id(state), "-"] + # Exercise the real prune implementation through the command + # boundary: no authentication or global boot change is hidden. + self.assertEqual([["sudo", "-v"], expected_prune], [args for args, _ in self.calls]) + self.assertEqual(before, (state / "config.json").read_bytes()) + self.assertTrue(autostart._manifest(directory)["linger_before"]) + self.assertTrue(autostart._manifest(directory)["node"]) + self.assertFalse((directory / "credential.cred").exists()) + self.assertIn(("enable", "--now", unit.name) if start_now else ("enable", unit.name), self.system_calls) + self.assertEqual(0o600, stat.S_IMODE(unit.stat().st_mode)) + self.inputs.assert_not_called() + self.key_prompt.assert_not_called() + + def test_fresh_node_with_existing_linger_fails_closed_when_prune_is_not_authorized(self): + config.save_config(self.state, config.default_config(role="node")) + before = (self.state / "config.json").read_bytes() + + def denied_prune(args, **kwargs): + if len(args) > 4 and args[:4] == ["sudo", "-n", "--", str(autostart.HELPER)] and args[4] == "prune": + self.calls.append((args, kwargs)) + raise RuntimeError("Synthetic cleanup grant denied") + return self.fake_command(args, **kwargs) + + self.command.side_effect = denied_prune + with mock.patch.object(autostart, "_linger", return_value=True), mock.patch.object(autostart, "_helper_present", return_value=True), mock.patch.object(autostart, "_node_helper_cleanup_required", return_value=True): + with self.assertRaisesRegex(RuntimeError, "cleanup grant denied"): + autostart._enable(self.state, confirm=False) + _, directory, unit = autostart._paths(self.state) + self.assertEqual(before, (self.state / "config.json").read_bytes()) + self.assertFalse(unit.exists()) + self.assertFalse((directory / "installation.json").exists()) + self.assertEqual([], self.system_calls) + self.assertEqual(["sudo", "-v"], self.calls[0][0]) + self.assertFalse(any("loginctl" in args for args, _ in self.calls)) + + def test_fresh_node_with_existing_linger_and_no_managed_grants_needs_no_privilege(self): + policies = self.root / "policies" + policies.mkdir() + profile = autostart._profile_id(self.state) + # Other profiles and owners must not cause an unnecessary request. + (policies / ("framebridge-credential-" + str(os.getuid()) + "-" + "a" * 64 + "-" + "b" * 64)).touch() + (policies / ("framebridge-credential-987654321-" + profile + "-" + "b" * 64)).touch() + config.save_config(self.state, config.default_config(role="node")) + before = (self.state / "config.json").read_bytes() + with mock.patch.object(autostart, "_linger", return_value=True), mock.patch.object(autostart, "_helper_present", return_value=True), mock.patch.object(credential_helper, "SUDOERS", policies), mock.patch.object(credential_helper, "_root_path", side_effect=lambda path, **kw: path): + autostart._enable(self.state, confirm=False, start_now=False) + _, directory, unit = autostart._paths(self.state) + self.assertEqual([], self.calls) + self.assertEqual(before, (self.state / "config.json").read_bytes()) + self.assertIn(("enable", unit.name), self.system_calls) + self.assertTrue(autostart._manifest(directory)["node"]) + self.assertFalse((directory / "credential.cred").exists()) + self.key_prompt.assert_not_called() + + def test_ambiguous_node_policy_fails_before_authentication_or_managed_writes(self): + policies = self.root / "policies" + policies.mkdir() + name = "framebridge-credential-authorize-" + str(os.getuid()) + "-" + autostart._profile_id(self.state) + "-unknown" + (policies / name).touch() + config.save_config(self.state, config.default_config(role="node")) + before = (self.state / "config.json").read_bytes() + with mock.patch.object(autostart, "_linger", return_value=True), mock.patch.object(autostart, "_helper_present", return_value=True), mock.patch.object(credential_helper, "SUDOERS", policies), mock.patch.object(credential_helper, "_root_path", side_effect=lambda path, **kw: path): + with self.assertRaisesRegex(RuntimeError, "ambiguous.*administrator review"): + autostart._enable(self.state, confirm=False) + _, directory, unit = autostart._paths(self.state) + self.assertEqual([], self.calls) + self.assertEqual([], self.system_calls) + self.assertEqual(before, (self.state / "config.json").read_bytes()) + self.assertFalse(unit.exists()) + self.assertFalse((directory / "installation.json").exists()) + self.assertFalse((directory / "credential.cred").exists()) + self.assertTrue((policies / name).exists()) + + def test_fresh_node_without_linger_requires_authentication_before_managed_writes(self): + config.save_config(self.state, config.default_config(role="node")) + before = (self.state / "config.json").read_bytes() + + def denied_authentication(args, **kwargs): + if args == ["sudo", "-v"]: + self.calls.append((args, kwargs)) + raise RuntimeError("Synthetic owner authentication required") + return self.fake_command(args, **kwargs) + + self.command.side_effect = denied_authentication + with mock.patch.object(autostart, "_helper_present", return_value=True): + with self.assertRaisesRegex(RuntimeError, "owner authentication required"): + autostart._enable(self.state, confirm=False) + _, directory, unit = autostart._paths(self.state) + self.assertEqual([["sudo", "-v"]], [args for args, _ in self.calls]) + self.assertEqual(before, (self.state / "config.json").read_bytes()) + self.assertFalse(unit.exists()) + self.assertFalse((directory / "installation.json").exists()) + self.assertFalse((directory / "credential.cred").exists()) + self.assertEqual([], self.system_calls) + self.key_prompt.assert_not_called() + + def test_fresh_node_without_linger_keeps_administrative_boot_setup(self): + config.save_config(self.state, config.default_config(role="node")) + with mock.patch.object(autostart, "_helper_present", return_value=True): + autostart._enable(self.state, confirm=False) + user = autostart.pwd.getpwuid(os.getuid()).pw_name + expected_prune = ["sudo", "-n", "--", str(autostart.HELPER), "prune", autostart._profile_id(self.state), "-"] + self.assertEqual([["sudo", "-v"], expected_prune, + ["sudo", "-n", "--", "loginctl", "enable-linger", user]], + [args for args, _ in self.calls]) + self.key_prompt.assert_not_called() + + def test_central_with_existing_linger_still_authenticates_and_protects_its_key(self): + settings = config.default_config(role="central") + settings.update(tunnel_id=ID, headless="auto") + config.save_config(self.state, settings) + with mock.patch.object(autostart, "_linger", return_value=True): + autostart.enable(self.state) + self.assertIn(["sudo", "-v"], [args for args, _ in self.calls]) + encryption = [(args, options) for args, options in self.calls if "encrypt" in args and "-ll" not in args] + self.assertEqual(1, len(encryption)) + self.assertEqual(MARKER.encode(), encryption[0][1]["data"]) + self.assertNotIn(MARKER, " ".join(encryption[0][0])) + self.key_prompt.assert_called_once_with("Connection key (hidden): ") + _, directory, _ = autostart._paths(self.state) + self.assertTrue((directory / "credential.cred").exists()) + + def test_central_existing_linger_does_not_bypass_failed_owner_authentication(self): + settings = config.default_config(role="central") + settings.update(tunnel_id=ID, headless="auto") + config.save_config(self.state, settings) + before = (self.state / "config.json").read_bytes() + + def denied_authentication(args, **kwargs): + if args == ["sudo", "-v"]: + raise RuntimeError("Synthetic owner authentication required") + return self.fake_command(args, **kwargs) + + self.command.side_effect = denied_authentication + with mock.patch.object(autostart, "_linger", return_value=True): + with self.assertRaisesRegex(RuntimeError, "owner authentication required"): + autostart.enable(self.state) + _, directory, unit = autostart._paths(self.state) + self.assertEqual(before, (self.state / "config.json").read_bytes()) + self.assertFalse(unit.exists()) + self.assertFalse((directory / "installation.json").exists()) + self.assertFalse((directory / "credential.cred").exists()) + self.key_prompt.assert_not_called() + def test_private_file_reader_rejects_symlink_hardlink_fifo_and_public_file(self): original = self.root / "original" original.write_bytes(b"safe") @@ -466,6 +621,89 @@ class AutostartTests(unittest.TestCase): finally: os.close(fd) + def _service_directory_owners(self, owners): + actual_lstat = Path.lstat + + def owner_lstat(path, *args, **kwargs): + values = list(actual_lstat(path, *args, **kwargs)) + values[4] = owners.get(path, values[4]) + return os.stat_result(values) + + return mock.patch.object(Path, "lstat", owner_lstat) + + def test_root_owned_service_ancestors_allow_owned_final_directory_without_changes(self): + final = self.home / ".config/systemd/user" + final.mkdir(parents=True, mode=0o700) + directories = [self.home / ".config", final.parent, final] + before = {p: (p.stat().st_uid, p.stat().st_mode) for p in directories} + owners = {directories[0]: 0, directories[1]: 0, final: 1000} + with mock.patch.object(autostart.os, "getuid", return_value=1000), self._service_directory_owners(owners): + autostart._check_unit_directory(final / "test.service") + self.assertEqual(before, {p: (p.stat().st_uid, p.stat().st_mode) for p in directories}) + self.assertEqual([], list(final.iterdir())) + + def test_root_or_other_account_cannot_own_final_service_directory(self): + final = self.home / ".config/systemd/user" + final.mkdir(parents=True, mode=0o700) + for owner in (0, 2000): + with self.subTest(owner=owner), mock.patch.object(autostart.os, "getuid", return_value=1000), self._service_directory_owners({self.home / ".config": 0, final.parent: 0, final: owner}): + with self.assertRaisesRegex(RuntimeError, "final directory must belong"): + autostart._check_unit_directory(final / "test.service") + self.assertEqual([], list(final.iterdir())) + + def test_other_account_cannot_own_service_ancestors(self): + final = self.home / ".config/systemd/user" + final.mkdir(parents=True, mode=0o700) + for ancestor in (self.home / ".config", final.parent): + owners = {self.home / ".config": 0, final.parent: 0, final: 1000, ancestor: 2000} + with self.subTest(ancestor=ancestor.name), mock.patch.object(autostart.os, "getuid", return_value=1000), self._service_directory_owners(owners): + with self.assertRaisesRegex(RuntimeError, "safe owner"): + autostart._check_unit_directory(final / "test.service") + + def test_root_owned_service_ancestors_still_reject_writable_or_symbolic_directories(self): + final = self.home / ".config/systemd/user" + final.mkdir(parents=True, mode=0o700) + directories = [self.home / ".config", final.parent, final] + owners = {directories[0]: 0, directories[1]: 0, final: 1000} + for directory in directories: + original = directory.stat().st_mode + for mode in (0o775, 0o777): + with self.subTest(directory=directory.name, mode=mode): + directory.chmod(mode) + with mock.patch.object(autostart.os, "getuid", return_value=1000), self._service_directory_owners(owners): + with self.assertRaisesRegex(RuntimeError, "not be writable"): + autostart._check_unit_directory(final / "test.service") + self.assertEqual(mode, stat.S_IMODE(directory.stat().st_mode)) + directory.chmod(stat.S_IMODE(original)) + moved = directory.with_name(directory.name + "-target") + directory.rename(moved) + directory.symlink_to(moved, target_is_directory=True) + try: + with self.subTest(directory=directory.name, symbolic=True): + with self.assertRaisesRegex(RuntimeError, "symbolic links"): + autostart._check_unit_directory(final / "test.service") + finally: + directory.unlink() + moved.rename(directory) + + def test_root_owned_parent_creation_failure_preserves_permissions_and_explains_failure(self): + parent = self.home / ".config/systemd" + parent.mkdir(parents=True, mode=0o755) + final = parent / "user" + before = {p: (p.stat().st_uid, p.stat().st_mode) for p in (parent.parent, parent)} + actual_mkdir = Path.mkdir + + def denied_final(path, *args, **kwargs): + if path == final: + raise PermissionError("Synthetic administrator-owned parent") + return actual_mkdir(path, *args, **kwargs) + + with mock.patch.object(autostart.os, "getuid", return_value=1000), self._service_directory_owners({parent.parent: 0, parent: 0}), mock.patch.object(Path, "mkdir", denied_final): + with self.assertRaisesRegex(RuntimeError, "cannot create.*left unchanged"): + autostart._check_unit_directory(final / "test.service") + self.assertFalse(final.exists()) + self.assertEqual(before, {p: (p.stat().st_uid, p.stat().st_mode) for p in before}) + def test_unit_quotes_specifiers_and_dollars_without_shell_or_missing_package_loop(self): text = autostart.unit_text(Path('/tmp/a % b ${HOME} "quoted"'), Path('/tmp/key % file')) self.assertIn("%%", text) diff --git a/source/tests/test_build_privacy_paths.py b/source/tests/test_build_privacy_paths.py new file mode 100644 index 0000000..b95b8a3 --- /dev/null +++ b/source/tests/test_build_privacy_paths.py @@ -0,0 +1,33 @@ +"""The public vendor name root_model.py is not the build user's home directory.""" +import importlib.util +from pathlib import Path +import sys +import unittest + + +PACKAGING = Path(__file__).resolve().parents[1] / "packaging" +sys.path.insert(0, str(PACKAGING)) +spec = importlib.util.spec_from_file_location("privacy_test_builder", PACKAGING / "build_expanded.py") +builder = importlib.util.module_from_spec(spec) +spec.loader.exec_module(builder) + + +class BuildPrivacyPathTests(unittest.TestCase): + def test_public_module_name_does_not_block_a_root_user_build(self): + data = b'{"path":"source/vendor/pydantic/root_model.py"}' + self.assertFalse(builder.contains_private_path(data, "/root")) + + def test_actual_home_paths_are_still_rejected(self): + for data in (b'"/root/private-file"', b'"/root"', b'input: /root\n', + b'file:///root/private-file', b'"/root/"'): + with self.subTest(data=data): + self.assertTrue(builder.contains_private_path(data, "/root")) + + def test_neighboring_names_are_not_this_home_directory(self): + for data in (b'"/home/tester-other/file"', b'"/home/tester_backup/file"'): + self.assertFalse(builder.contains_private_path(data, "/home/tester")) + self.assertTrue(builder.contains_private_path(b'"/home/tester/file"', "/home/tester")) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/test_cli_health.py b/source/tests/test_cli_health.py new file mode 100644 index 0000000..65c01b1 --- /dev/null +++ b/source/tests/test_cli_health.py @@ -0,0 +1,70 @@ +"""Exercise the public health command without starting a profile or transport.""" +import contextlib +import io +import json +from pathlib import Path +import tempfile +from types import SimpleNamespace +import unittest +from unittest import mock + +from framebridge_expanded import cli, health, __version__ + + +class HealthCliTests(unittest.TestCase): + def invoke(self, arguments): + output, errors = io.StringIO(), io.StringIO() + with contextlib.redirect_stdout(output), contextlib.redirect_stderr(errors): + code = cli.main(arguments) + return code, output.getvalue(), errors.getvalue() + + def test_absent_profile_is_reported_without_creating_it(self): + with tempfile.TemporaryDirectory() as temporary: + profile = Path(temporary) / "absent-profile" + code, text, _ = self.invoke(["--state-dir", str(profile), "--json", "health"]) + self.assertFalse(profile.exists()) + value = json.loads(text) + self.assertEqual(code, 1) + self.assertEqual(value["version"], __version__) + self.assertEqual(value["command"], "health") + self.assertTrue(value["ok"]) + self.assertEqual(value["result"]["health"]["state"], "unconfigured") + self.assertFalse(value["result"]["health"]["healthy"]) + self.assertTrue(value["result"]["read_only"]) + + def test_query_success_does_not_make_exit_status_healthy(self): + for state, healthy, expected in (("healthy", True, 0), ("paused", True, 0), + ("degraded", False, 1), ("unavailable", None, 1)): + with self.subTest(state=state): + report = {"ok": True, "health": {"state": state, "healthy": healthy}} + module = SimpleNamespace(collect=mock.Mock(return_value=report)) + with mock.patch.object(cli, "_module", return_value=module): + code, text, _ = self.invoke(["health", "--json"]) + self.assertEqual(code, expected) + self.assertEqual(json.loads(text)["result"], report) + + def test_failed_observation_cannot_pass_even_with_a_healthy_flag(self): + module = SimpleNamespace(collect=mock.Mock(return_value={"ok": False, "health": {"healthy": True}})) + with mock.patch.object(cli, "_module", return_value=module): + code, text, _ = self.invoke(["--json", "health"]) + self.assertEqual(code, 1) + self.assertFalse(json.loads(text)["ok"]) + + def test_human_summary_names_the_local_machine_and_its_limits(self): + with tempfile.TemporaryDirectory() as temporary: + code, text, _ = self.invoke(["health", "--state-dir", str(Path(temporary) / "absent")]) + self.assertEqual(code, 1) + self.assertIn("Target: LOCAL = this computer", text) + self.assertIn("unconfigured", text) + self.assertIn("not a fleet scan", text) + + def test_json_output_never_prompts_for_authentication(self): + with tempfile.TemporaryDirectory() as temporary, mock.patch("builtins.input") as ask: + code, text, _ = self.invoke(["health", "--json", "--state-dir", str(Path(temporary) / "absent")]) + ask.assert_not_called() + self.assertEqual(code, 1) + self.assertIsNone(json.loads(text)["result"]["software"]["runtime_version"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/test_connection_background.py b/source/tests/test_connection_background.py index 95b42f0..617ee6e 100644 --- a/source/tests/test_connection_background.py +++ b/source/tests/test_connection_background.py @@ -141,6 +141,11 @@ class BackgroundTests(unittest.TestCase): record=json.loads(path.read_text());record['verified_at']=time.time()-30 record['message']='\x1b[31m'+FAKE background._write(path,record) + if record.get('heartbeat_backend') == background._HEARTBEAT_BACKEND: + heartbeat_path=background._heartbeat_directory(self.state)/'status.json' + heartbeat=background._read(heartbeat_path) + heartbeat['verified_at']=time.time()-30 + background._write(heartbeat_path,heartbeat) current=background.status(self.state) self.assertFalse(current['verified']) self.assertEqual(current['state'],'retrying') diff --git a/source/tests/test_fleet_routing.py b/source/tests/test_fleet_routing.py index 2d5effc..224751b 100644 --- a/source/tests/test_fleet_routing.py +++ b/source/tests/test_fleet_routing.py @@ -1,4 +1,5 @@ """Routing contract tests; original read provider is exercised locally.""" +import base64 import hashlib from pathlib import Path import socket @@ -10,6 +11,8 @@ from unittest import mock ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "expanded")) from framebridge_expanded import fleet +from framebridge_expanded.real_screen.screen_frontdoor import RPC_COMMAND +from framebridge_expanded.real_screen.screen_rpc import encode LOCAL_ID = "c6d827a4-219f-48fc-82d8-f410176313ca" REMOTE_ID = "6d88c5ae-343e-4413-a202-ccae2ae7694a" @@ -134,6 +137,99 @@ class FleetRoutingTests(unittest.TestCase): self.assertEqual(fleet.execute_node("test-node", "printf ok", True)["error"], "NODE_OFFLINE") self.assertEqual(fleet.node_read("test-node", "system.status", {})["error"], "NODE_OFFLINE") + @staticmethod + def screen_script(): + return RPC_COMMAND + base64.b64encode(encode({"op": "observe"})).decode("ascii") + "\n" + + def assert_identity(self, value, requested, local=False): + self.assertEqual(value["requested_target"], requested) + self.assertEqual(value["resolved_target"], "test-central" if local else "test-node") + self.assertEqual(value["node"], value["resolved_target"]) + self.assertEqual(value["target"], "LOCAL" if local else "test-node") + self.assertEqual(value["node_id"], LOCAL_ID if local else REMOTE_ID) + self.assertEqual(value["hostname"], socket.gethostname() if local else "test-node") + self.assertEqual(value["transport"], "local" if local else "broker_outbound") + + @staticmethod + def misleading_response(payload=None): + value = {"ok": True, "target": "wrong-machine", "node": "wrong-machine", + "requested_target": "wrong-machine", "resolved_target": "wrong-machine", + "node_id": "wrong-id", "hostname": "wrong-host", "transport": "wrong-route"} + if payload is not None: + value.update(job_id=payload["job_id"], command_sha256=payload.get("sha256")) + return value + + def test_read_identity_comes_from_confirmed_route(self): + for requested, local in [("LOCAL", True), ("test-central", True), (LOCAL_ID, True), + ("test-node", False), (REMOTE_ID, False)]: + with self.subTest(requested=requested), \ + mock.patch.object(fleet, "read_local", return_value=self.misleading_response()), \ + mock.patch("framebridge_expanded.broker.request", return_value=self.misleading_response()): + value = fleet.node_read(requested, "system.status", {}) + self.assertTrue(value["ok"]) + self.assert_identity(value, requested, local) + self.assertEqual(value["node"], value["resolved_target"]) + + def test_execution_routes_overwrite_upstream_identity(self): + def remote_response(node_id, capability, payload, timeout_s): + return self.misleading_response(payload) + calls = [ + lambda alias: fleet.execute_node(alias, "printf ok", True, job_id="identity-exec"), + lambda alias: fleet.execute_screen_node(alias, self.screen_script(), True, + job_id="identity-screen", include_stdout=True), + lambda alias: fleet.cua_node(alias, {"action": "OBSERVE"}, True, job_id="identity-cua"), + ] + for route in calls: + for requested, local in [("LOCAL", True), ("test-central", True), (LOCAL_ID, True), + ("test-node", False), (REMOTE_ID, False)]: + with self.subTest(route=route, requested=requested), \ + mock.patch("framebridge_expanded.runtime.adapter_request", + side_effect=self.misleading_response), \ + mock.patch("framebridge_expanded.broker.request", side_effect=remote_response): + value = route(requested) + self.assertTrue(value["ok"], value) + self.assert_identity(value, requested, local) + + def test_unknown_target_never_falls_back_to_local(self): + with mock.patch("framebridge_expanded.runtime.adapter_request") as local, \ + mock.patch("framebridge_expanded.broker.request") as remote: + values = [ + fleet.node_read("unknown-node", "system.status", {}), + fleet.execute_node("unknown-node", "printf ok", True), + fleet.execute_screen_node("unknown-node", self.screen_script(), True, include_stdout=True), + fleet.cua_node("unknown-node", {"action": "OBSERVE"}, True), + ] + local.assert_not_called() + remote.assert_not_called() + for value in values: + self.assertFalse(value["ok"]) + self.assertEqual(value["error"], "NODE_NOT_FOUND") + self.assertNotIn("resolved_target", value) + self.assertNotIn("node_id", value) + + def test_offline_target_errors_retain_confirmed_identity(self): + self.remote["operational_transport_ready"] = False + with mock.patch("framebridge_expanded.broker.request") as remote: + values = [ + fleet.node_read(REMOTE_ID, "system.status", {}), + fleet.execute_node(REMOTE_ID, "printf ok", True), + fleet.execute_screen_node(REMOTE_ID, self.screen_script(), True, include_stdout=True), + fleet.cua_node(REMOTE_ID, {"action": "OBSERVE"}, True), + ] + remote.assert_not_called() + for value in values: + self.assertFalse(value["ok"]) + self.assertEqual(value["error"], "NODE_OFFLINE") + self.assert_identity(value, REMOTE_ID) + + def test_pre_resolution_failure_does_not_claim_identity(self): + with mock.patch.object(fleet, "resolve_node") as lookup: + value = fleet.execute_node("test-node", "printf ok", False) + lookup.assert_not_called() + self.assertEqual(value["error"], "AUTHORIZED_TRUE_REQUIRED") + for field in ("requested_target", "resolved_target", "node_id", "hostname", "transport"): + self.assertNotIn(field, value) + if __name__ == "__main__": unittest.main() diff --git a/source/tests/test_health.py b/source/tests/test_health.py new file mode 100644 index 0000000..4e87f85 --- /dev/null +++ b/source/tests/test_health.py @@ -0,0 +1,196 @@ +import copy +import json +from pathlib import Path +import sys +import tempfile +import unittest +from unittest import mock + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "expanded")) +from framebridge_expanded import health + + +NODE_ID = "51f0acf5-3633-41f6-aea7-4fed5cffcb01" + + +def snapshots(): + return ({"ok": True, "configured": True, "role": "central", "name": "gateway", + "runtime": {"running": True, "ready": True, "node_id": NODE_ID, + "display_context": {"kind": "text"}, + "capabilities": {"fbexec": True, "fbterm": True, "cua_tty_module": True, "graphical_cua": False}}, + "connection": {"verified": True, "state": "ready"}, + "boot": {"configured": True, "enabled": False, "service_state": "active"}, + "desired_connection": "connected"}, + {"ok": True, "platform_compatibility": {"ok": True, "profile_id": "debian13-amd64-py313", "validation_status": "validated", "issues": []}, + "issues": [], "missing_commands": [], "capabilities": {"fbexec": True, "fbterm": True, "cua_tty_module": True, "graphical_cua": False}}) + + +class HealthTests(unittest.TestCase): + def test_release_build_identity_is_retained_without_relabelling_it_validated(self): + status, doctor = snapshots() + doctor["platform_compatibility"]["validation_status"] = "release_build" + value = health.build_report(status, doctor) + self.assertTrue(value["ok"]) + self.assertEqual(value["prerequisites"]["validation_status"], "release_build") + self.assertNotEqual(value["prerequisites"]["validation_status"], "validated") + + def test_healthy_text_mode_does_not_require_graphical_tools_or_boot_enabled(self): + status, doctor = snapshots() + value = health.build_report(status, doctor, observed_at="2026-10-28T12:00:00+00:00", host="gateway") + self.assertTrue(value["ok"]) + self.assertEqual(value["health"], {"state": "healthy", "healthy": True, "issues": []}) + self.assertEqual(value["boot"]["policy"], "disabled") + self.assertEqual(value["effective_target"]["node_id"], NODE_ID) + self.assertFalse(value["desktop"]["capture_verified_by_this_report"]) + self.assertIsNone(value["software"]["runtime_version"]) + + def test_query_success_is_not_readiness(self): + status, doctor = snapshots() + status["runtime"].update(running=False, ready=False) + status["connection"].update(verified=False, state="stopped") + value = health.build_report(status, doctor) + self.assertTrue(value["ok"]) + self.assertEqual(value["health"]["state"], "degraded") + self.assertFalse(value["health"]["healthy"]) + self.assertIn("LOCAL_TOOLS_NOT_READY", value["health"]["issues"]) + + def test_paused_connection_is_distinct_from_failure(self): + status, doctor = snapshots() + status["desired_connection"] = "disconnected" + status["connection"].update(verified=False, state="stopped") + value = health.build_report(status, doctor) + self.assertEqual(value["health"]["state"], "paused") + self.assertTrue(value["health"]["healthy"]) + self.assertEqual(value["health"]["issues"], []) + + def test_node_does_not_claim_central_connection_verified(self): + status, doctor = snapshots() + status.update(role="node", desired_connection="not_applicable") + value = health.build_report(status, doctor) + self.assertEqual(value["connection"]["state"], "managed_by_central") + self.assertIsNone(value["connection"]["verified"]) + self.assertEqual(value["health"]["state"], "healthy") + + def test_requested_pause_does_not_hide_a_still_running_tunnel(self): + status, doctor = snapshots() + status["desired_connection"] = "disconnected" + value = health.build_report(status, doctor) + self.assertEqual(value["health"]["state"], "degraded") + self.assertIn("CONNECTION_INTENT_MISMATCH", value["health"]["issues"]) + status["connection"].update(verified=False, state="stopped", alive=True) + value = health.build_report(status, doctor) + self.assertFalse(value["health"]["healthy"]) + + def test_failed_connection_query_cannot_certify_a_healthy_profile(self): + status, doctor = snapshots() + status["connection"]["ok"] = False + value = health.build_report(status, doctor) + self.assertFalse(value["ok"]) + self.assertIsNone(value["health"]["healthy"]) + self.assertIn("CONNECTION_STATUS_UNAVAILABLE", value["health"]["issues"]) + + def test_missing_boot_policy_is_unknown_rather_than_healthy(self): + status, doctor = snapshots() + status["boot"] = {} + value = health.build_report(status, doctor) + self.assertFalse(value["ok"]) + self.assertEqual(value["health"]["state"], "unavailable") + + def test_missing_runtime_status_is_not_a_stopped_runtime(self): + status, doctor = snapshots() + status["runtime"] = {"status_unavailable": True, "running": False, "ready": False} + value = health.build_report(status, doctor) + self.assertFalse(value["ok"]) + self.assertEqual(value["runtime"]["state"], "unavailable") + self.assertEqual(value["health"]["state"], "unavailable") + self.assertIsNone(value["health"]["healthy"]) + + def test_failed_runtime_query_cannot_certify_ready(self): + status, doctor = snapshots() + status["runtime"]["ok"] = False + value = health.build_report(status, doctor) + self.assertFalse(value["ok"]) + self.assertEqual(value["health"]["state"], "unavailable") + + def test_failed_profile_query_cannot_claim_unconfigured(self): + value = health.build_report({"ok": False, "configured": False}, None) + self.assertFalse(value["ok"]) + self.assertEqual(value["health"]["state"], "unavailable") + + def test_missing_diagnostic_result_is_unavailable_not_passed(self): + status, _ = snapshots() + value = health.build_report(status, {}) + self.assertFalse(value["ok"]) + self.assertEqual(value["health"]["state"], "unavailable") + + def test_unconfigured_profile_is_observed_without_running_doctor_or_creating_files(self): + with tempfile.TemporaryDirectory() as directory: + state = Path(directory) / "not-created" + with mock.patch.object(health.runtime, "doctor", side_effect=AssertionError("doctor must not run")): + value = health.collect(state) + self.assertFalse(state.exists()) + self.assertTrue(value["ok"]) + self.assertEqual(value["health"]["state"], "unconfigured") + self.assertEqual(value["health"]["issues"], []) + self.assertEqual(value["prerequisites"]["state"], "not_run") + + def test_diagnostics_failure_retains_status_without_leaking_exception(self): + status, _ = snapshots() + with mock.patch.object(health.lifecycle, "status", return_value=status), \ + mock.patch.object(health.runtime, "doctor", side_effect=RuntimeError("secret-access-key")): + value = health.collect(Path("/unused")) + self.assertFalse(value["ok"]) + self.assertEqual(value["runtime"]["state"], "ready") + self.assertEqual(value["health"]["state"], "unavailable") + self.assertNotIn("secret-access-key", json.dumps(value)) + + def test_partial_invalid_profile_does_not_guess_it_is_unconfigured(self): + with mock.patch.object(health.lifecycle, "status", side_effect=ValueError("private-input")), \ + mock.patch.object(health.runtime, "doctor") as doctor: + value = health.collect(Path("/unused")) + doctor.assert_not_called() + self.assertFalse(value["ok"]) + self.assertEqual(value["health"]["state"], "unavailable") + self.assertNotIn("private-input", json.dumps(value)) + + def test_allowlist_excludes_connection_material_paths_and_untrusted_free_text(self): + status, doctor = snapshots() + status["tunnel_id"] = "secret-tunnel" + status["runtime"].update(state_dir="/private/secret-path", error="secret-error", access_key="secret-key") + status["runtime"]["display_context"]["authority"] = "/private/secret-authority" + status["connection"].update(message="secret-message", log="/private/secret-log", token="secret-token") + doctor.update(network={"token": "secret-network"}, issues=["secret-issue"]) + doctor["platform_compatibility"]["issues"] = ["secret-compatibility"] + result = health.build_report(status, doctor, host="host") + self.assertNotIn("secret-", json.dumps(result)) + self.assertEqual(result["prerequisites"]["issue_count"], 1) + self.assertEqual(result["prerequisites"]["compatibility_issue_count"], 1) + + def test_prerequisite_failure_is_degraded_without_claiming_no_runtime(self): + status, doctor = snapshots() + doctor.update(ok=False, missing_commands=["tmux"]) + value = health.build_report(status, doctor) + self.assertTrue(value["ok"]) + self.assertEqual(value["health"]["state"], "degraded") + self.assertEqual(value["runtime"]["state"], "ready") + self.assertEqual(value["prerequisites"]["missing_commands"], ["tmux"]) + + def test_reports_do_not_modify_snapshots(self): + status, doctor = snapshots() + before = copy.deepcopy((status, doctor)) + health.build_report(status, doctor) + self.assertEqual((status, doctor), before) + + def test_terminal_output_is_explicit_about_target_and_observation_limit(self): + status, doctor = snapshots() + value = health.build_report(status, doctor, host="gateway") + text = health.render(value) + self.assertIn("Target: LOCAL = this computer (gateway)", text) + self.assertIn(NODE_ID, text) + self.assertIn("capture/input not tested", text) + self.assertIn("not proof of the running version", text) + self.assertIn("not a fleet scan", text) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/test_idle_state_io.py b/source/tests/test_idle_state_io.py new file mode 100644 index 0000000..363f80a --- /dev/null +++ b/source/tests/test_idle_state_io.py @@ -0,0 +1,152 @@ +"""Liveness stays fresh without periodically syncing the durable profile.""" +import json +import os +from pathlib import Path +import tempfile +import unittest +from unittest import mock + +from framebridge_expanded import autostart, connection_background as background + + +class IdleStateIOTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + self.state = self.root / "profile" + self.directory = background._directory(self.state) + self.runtime_parent = self.root / "runtime" + self.runtime_parent.mkdir(mode=0o700) + (self.runtime_parent / str(os.getuid())).mkdir(mode=0o700) + self.addCleanup(mock.patch.stopall) + mock.patch.object(background, "_RUNTIME_PARENT", self.runtime_parent).start() + self.identity = dict(pid=12345, uid=os.getuid(), start_ticks=99, + boot_id="boot-one", session_id=12345, + process_group=12345, state_dir=str(self.state), lock_fd=8) + + def sink(self): + runtime = background._heartbeat_directory(self.state, create=True) + return background._StatusOutput(self.directory / "status.json", self.identity, + runtime / "status.json") + + def status(self): + with mock.patch.object(background, "_valid_identity", return_value=True): + return background.status(self.state) + + def test_ready_heartbeat_fresh_beyond_timeout_without_durable_writes(self): + clock = [100.0] + with mock.patch.object(background.time, "time", side_effect=lambda: clock[0]), mock.patch.object(background.time, "monotonic", side_effect=lambda: clock[0]): + sink = self.sink() + sink.observe("ready") + sink.change("ready", "[OK] Connected.") + durable = sink.path.stat() + original = sink.path.read_bytes() + for instant in range(101, 141): + clock[0] = float(instant) + sink.observe("ready") + self.assertTrue(self.status()["verified"]) + self.assertEqual(sink.path.read_bytes(), original) + self.assertEqual(sink.path.stat().st_ino, durable.st_ino) + self.assertEqual(sink.path.stat().st_mtime_ns, durable.st_mtime_ns) + clock[0] = 156.0 + self.assertFalse(self.status()["verified"]) + self.assertEqual(self.status()["state"], "retrying") + sink.observe("ready") + self.assertTrue(self.status()["verified"]) + + def test_failure_and_recovery_publish_transitions_immediately(self): + sink = self.sink() + sink.observe("ready") + sink.change("ready", "[OK] Connected.") + self.assertTrue(self.status()["verified"]) + sink.observe("access") + self.assertFalse(self.status()["verified"]) + sink.change("retrying", "[RETRYING] Lost readiness.") + self.assertEqual(json.loads(sink.path.read_text())["state"], "retrying") + sink.observe("ready") + sink.change("ready", "[OK] Connected.") + self.assertTrue(self.status()["verified"]) + + def test_missing_or_old_worker_heartbeat_never_uses_durable_timestamp(self): + sink = self.sink() + sink.observe("ready") + sink.change("ready", "[OK] Connected.") + heartbeat = background._read(sink.heartbeat_path) + for field, value in (("pid", 99999), ("boot_id", "older-boot"), ("start_ticks", 44), ("state_dir", "/another-profile")): + wrong = dict(heartbeat, **{field: value}) + background._write(sink.heartbeat_path, wrong) + self.assertFalse(self.status()["verified"], field) + sink.heartbeat_path.unlink() + self.assertFalse(self.status()["verified"]) + + def test_no_runtime_falls_back_to_bounded_fresh_durable_heartbeat(self): + (self.runtime_parent / str(os.getuid())).rmdir() + self.assertIsNone(background._heartbeat_directory(self.state, create=True)) + clock = [100.0] + with mock.patch.object(background.time, "time", side_effect=lambda: clock[0]), mock.patch.object(background.time, "monotonic", side_effect=lambda: clock[0]), mock.patch.object(background, "_write", wraps=background._write) as write: + sink = background._StatusOutput(self.directory / "status.json", self.identity) + sink.observe("ready") + sink.change("ready", "[OK] Connected.") + write.reset_mock() + for instant in range(101, 121): + clock[0] = float(instant) + sink.observe("ready") + self.assertTrue(self.status()["verified"]) + self.assertEqual(write.call_count, 4) + sink.observe("access") + self.assertFalse(self.status()["verified"]) + + def test_legacy_durable_heartbeat_remains_readable_with_runtime_available(self): + background._write(self.directory / "status.json", dict(self.identity, state="ready", verified_at=100.0)) + with mock.patch.object(background.time, "time", return_value=110.0): + self.assertTrue(self.status()["verified"]) + with mock.patch.object(background.time, "time", return_value=116.0): + self.assertFalse(self.status()["verified"]) + + def test_runtime_permissions_and_symlinks_rejected_without_changes(self): + root = self.runtime_parent / str(os.getuid()) + root.chmod(0o755) + with self.assertRaises(RuntimeError): + background._heartbeat_directory(self.state, create=True) + self.assertEqual(root.stat().st_mode & 0o777, 0o755) + root.chmod(0o700) + directory = background._heartbeat_directory(self.state, create=True) + directory.rmdir() + directory.symlink_to(self.directory, target_is_directory=True) + with self.assertRaises(ValueError): + background._heartbeat_directory(self.state, create=True) + with self.assertRaises(RuntimeError): + background._heartbeat_directory(self.state) + + def test_runtime_parent_symlink_rejected(self): + alias = self.root / "runtime-alias" + alias.symlink_to(self.runtime_parent, target_is_directory=True) + with mock.patch.object(background, "_RUNTIME_PARENT", alias), self.assertRaises(RuntimeError): + background._heartbeat_directory(self.state, create=True) + + def test_heartbeat_file_symlink_and_hardlink_rejected(self): + sink = self.sink() + sink.heartbeat_path.symlink_to(sink.path) + with self.assertRaises(OSError): + sink.observe("ready") + sink.heartbeat_path.unlink() + os.link(sink.path, sink.heartbeat_path) + with self.assertRaises(RuntimeError): + sink.observe("ready") + + def test_worker_state_only_writes_on_transition(self): + with mock.patch.object(autostart, "_json", wraps=autostart._json) as write: + autostart._worker_status(self.directory, "connected") + first = (self.directory / "worker-status.json").read_bytes() + for _ in range(20): + autostart._worker_status(self.directory, "connected") + self.assertEqual(write.call_count, 1) + self.assertEqual((self.directory / "worker-status.json").read_bytes(), first) + autostart._worker_status(self.directory, "reconnecting") + self.assertEqual(write.call_count, 2) + self.assertEqual(json.loads((self.directory / "worker-status.json").read_text())["state"], "reconnecting") + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/test_linux_compatibility.py b/source/tests/test_linux_compatibility.py index c129807..df50b6f 100644 --- a/source/tests/test_linux_compatibility.py +++ b/source/tests/test_linux_compatibility.py @@ -1,10 +1,13 @@ """Candidate profile/ABI selection; tests use inert native-file fixtures.""" import hashlib +import contextlib +import io import json import os from pathlib import Path import tempfile import stat +import sys from types import SimpleNamespace import unittest from unittest.mock import patch @@ -64,6 +67,43 @@ class LinuxCompatibilityTests(unittest.TestCase): self.assertFalse(self.check(distro, version, (3, 12))["ok"]) self.assertIsNone(compat.profile_for({"ID": "other", "ID_LIKE": "ubuntu", "VERSION_ID": "24.04"})) + def test_public_builder_profiles_are_accepted_by_the_runtime_without_claiming_validation(self): + # Use the real public builder's profile output, rather than duplicating + # its validation_status value in a fixture. Archive/vendor verification + # has separate tests; --check-vendor does not stage/install a package. + sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "packaging")) + import archive_sources + import build_expanded + targets = [("debian13-amd64-py313", "debian", "13", (3, 13)), + ("ubuntu24.04-amd64-py312", "ubuntu", "24.04", (3, 12)), + ("mint22-amd64-py312", "linuxmint", "22.3", (3, 12))] + for profile_id, distro, version, python in targets: + with self.subTest(profile=profile_id): + self.artifact(profile_id) + output = io.StringIO() + globals_before = {name: getattr(build_expanded, name) for name in + ("VERSION", "PUBLIC_RELEASE", "SOURCE_ARCHIVE", "PUBLIC_FILENAME", "RELEASE", "STAGE", "PREFIX")} + args = ["--target", profile_id, "--public-release", "--source-manifest", str(self.prefix / "source.json"), "--check-vendor"] + if profile_id != "debian13-amd64-py313": + args += ["--vendor-dir", str(self.prefix / "reviewed-cp312")] + with patch.multiple(build_expanded, **globals_before), \ + patch.object(build_expanded.subprocess, "run"), \ + patch.object(build_expanded, "vendor_versions", return_value={}), \ + patch.object(build_expanded, "validate_vendor", return_value={"vendor_sha256": "a" * 64}), \ + patch.object(build_expanded, "release_source", return_value={"filename": "FrameBridge-0.3.4-source.tar.gz", "sha256": "b" * 64}), \ + patch.object(build_expanded, "legal_preflight"), \ + patch.object(archive_sources, "source_files", return_value={}), \ + contextlib.redirect_stdout(output): + build_expanded.main(args) + manifest = json.loads(output.getvalue()) + self.assertEqual(manifest["validation_status"], "release_build") + (self.prefix / "release-profile.json").write_text(json.dumps(manifest)) + result = self.check(distro, version, python) + self.assertTrue(result["ok"], result) + self.assertEqual(result["validation_status"], manifest["validation_status"]) + self.assertIn("Release assembly does not establish a tested installation", result["warnings"][0]) + self.assertNotEqual(result["validation_status"], "validated") + def test_interpreter_artifact_and_cpu_must_all_match(self): self.artifact("ubuntu24.04-amd64-py312") self.assertFalse(self.check("ubuntu", "24.04", (3, 13))["ok"]) diff --git a/source/tests/test_node_helper_cleanup.py b/source/tests/test_node_helper_cleanup.py new file mode 100644 index 0000000..683bd90 --- /dev/null +++ b/source/tests/test_node_helper_cleanup.py @@ -0,0 +1,79 @@ +"""Policy-name inspection only: no sudo, registry reads or rule contents.""" +import os +from pathlib import Path +import stat +import tempfile +import unittest +from unittest import mock + +from framebridge_expanded import autostart, credential_helper + + +class NodeHelperCleanupInspectionTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.state = self.root / "profile" + self.policies = self.root / "policies" + self.policies.mkdir() + self.profile = autostart._profile_id(self.state) + self.uid = str(os.getuid()) + self.addCleanup(mock.patch.stopall) + mock.patch.object(autostart, "_helper_ready").start() + mock.patch.object(credential_helper, "SUDOERS", self.policies).start() + self.trust = mock.patch.object(credential_helper, "_root_path", side_effect=lambda path, **kw: path) + self.trust.start() + + def test_own_canonical_decrypt_and_authorization_names_require_root_cleanup(self): + names = [ + "framebridge-credential-" + self.uid + "-" + self.profile + "-" + "a" * 64, + "framebridge-credential-authorize-" + self.uid + "-" + self.profile + "-host", + "framebridge-credential-authorize-" + self.uid + "-" + self.profile + "-user", + ] + for name in names: + with self.subTest(name=name): + path = self.policies / name + path.touch() + with mock.patch.object(Path, "read_bytes", side_effect=AssertionError("Do not read rule contents")): + self.assertTrue(autostart._node_helper_cleanup_required(self.state)) + path.unlink() + + def test_ambiguous_names_require_review_even_alongside_a_canonical_grant(self): + canonical = "framebridge-credential-" + self.uid + "-" + self.profile + "-" + "a" * 64 + for name in ["framebridge-credential-" + self.uid + "-" + self.profile + "-invalid", + "framebridge-credential-authorize-" + self.uid + "-" + self.profile + "-unknown"]: + with self.subTest(name=name), mock.patch.object(Path, "iterdir", return_value=iter([self.policies / canonical, self.policies / name])): + with self.assertRaisesRegex(RuntimeError, "ambiguous.*administrator review"): + autostart._node_helper_cleanup_required(self.state) + + def test_symlink_in_own_namespace_still_requires_root_cleanup(self): + path = self.policies / ("framebridge-credential-" + self.uid + "-" + self.profile + "-" + "a" * 64) + path.symlink_to(self.root / "nonexistent") + self.assertTrue(autostart._node_helper_cleanup_required(self.state)) + + def test_permission_denied_listing_is_not_treated_as_empty(self): + with mock.patch.object(Path, "iterdir", side_effect=PermissionError("synthetic denied")): + self.assertTrue(autostart._node_helper_cleanup_required(self.state)) + + def test_empty_verified_policy_directory_omits_privileged_cleanup(self): + self.assertFalse(autostart._node_helper_cleanup_required(self.state)) + + def test_insecure_parent_or_policy_directory_is_not_trusted(self): + self.trust.stop() + with mock.patch.object(credential_helper, "SUDOERS", Path("/etc/sudoers.d")): + for unsafe, kind, uid, mode in [ + (Path("/etc"), stat.S_IFDIR, 0, 0o775), + (Path("/etc/sudoers.d"), stat.S_IFLNK, 0, 0o777), + (Path("/etc/sudoers.d"), stat.S_IFDIR, 12345, 0o755), + ]: + def info(path, *, selected=unsafe, selected_kind=kind, selected_uid=uid, selected_mode=mode): + return os.stat_result((selected_kind | selected_mode if path == selected else stat.S_IFDIR | 0o755, + 1, 1, 1, selected_uid if path == selected else 0, 0, 0, 0, 0, 0)) + with self.subTest(path=unsafe, kind=kind, uid=uid, mode=mode), mock.patch.object(Path, "lstat", info), mock.patch.object(Path, "iterdir") as listing: + self.assertTrue(autostart._node_helper_cleanup_required(self.state)) + listing.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/test_package_maintainer.py b/source/tests/test_package_maintainer.py index 59fe2be..9e20a01 100644 --- a/source/tests/test_package_maintainer.py +++ b/source/tests/test_package_maintainer.py @@ -9,7 +9,7 @@ import tempfile import unittest sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "packaging")) -from build_expanded import maintainer_prerm, PRIVILEGED_HELPER_FLOOR +from build_expanded import maintainer_preinst, maintainer_prerm, PRIVILEGED_HELPER_FLOOR from build_profiles import CANDIDATE_VERSION @@ -91,6 +91,38 @@ class MaintainerScriptTests(unittest.TestCase): with self.assertRaises(ValueError): maintainer_prerm("0.3.2'; /tmp/untrusted-script; '") + def run_preinst(self, arguments, *, version="0.3.4~dev.20261029.4", fail=0): + script = maintainer_preinst(version) + script = script.replace("/usr/bin/python3 -I -S - <<'FRAMEBRIDGE_UPGRADE_SNAPSHOT'", + shlex.quote(str(self.stub)) + " -I -S - <<'FRAMEBRIDGE_UPGRADE_SNAPSHOT'") + result = subprocess.run(["/bin/sh", "-s", "--", *arguments], input=script, + text=True, capture_output=True, timeout=10, + env=dict(os.environ, TEST_CALLS=str(self.trace), TEST_CLEANUP_RC=str(fail))) + calls = [json.loads(row) for row in self.trace.read_text().splitlines()] if self.trace.exists() else [] + return result, calls + + def test_same_version_reinstall_takes_a_new_current_runtime_snapshot(self): + result, calls = self.run_preinst(["upgrade", "0.3.4~dev.20261029.4"]) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(calls, [["-I", "-S", "-"]]) + + def test_forward_upgrade_still_takes_a_runtime_snapshot(self): + result, calls = self.run_preinst(["upgrade", "0.3.4~dev.20261029.3"]) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(calls, [["-I", "-S", "-"]]) + + def test_fresh_install_and_downgrade_do_not_capture_unrelated_running_profiles(self): + for arguments in (["install"], ["upgrade", "0.3.5"]): + with self.subTest(arguments=arguments): + result, calls = self.run_preinst(arguments) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(calls, []) + + def test_failed_reinstall_snapshot_blocks_package_replacement(self): + result, calls = self.run_preinst(["upgrade", "0.3.4~dev.20261029.4"], fail=17) + self.assertEqual(result.returncode, 17) + self.assertEqual(calls, [["-I", "-S", "-"]]) + if __name__ == "__main__": unittest.main() diff --git a/source/tests/test_package_upgrade.py b/source/tests/test_package_upgrade.py index 524a746..3f60782 100644 --- a/source/tests/test_package_upgrade.py +++ b/source/tests/test_package_upgrade.py @@ -202,6 +202,58 @@ class PackageUpgradeTests(unittest.TestCase): self.assertEqual(value['phase'], 'CAPTURED') run.assert_not_called() + def test_reinstall_after_failure_preserves_receipt_and_keeps_stopped_profiles_stopped(self): + failed = {'schema': 1, 'transaction': 'a' * 16, 'phase': 'FAILED', + 'profiles': [{'state': str(self.state), 'phase': 'FAILED', + 'error': 'UPGRADE_OWNER_OPERATION_FAILED:user-ready'}]} + self.write(self.root / 'pending.json', failed) + processes = self.root / 'no-running-managers' + processes.mkdir() + marker = self.state / 'manual-stop.json' + marker.write_bytes(b'{"manual_stop": true}\n') + before_marker = marker.read_bytes() + with mock.patch.object(upgrade, 'ROOT', self.root), \ + mock.patch.object(upgrade, '_root'), \ + mock.patch.object(upgrade, '_read', side_effect=self.root_read), \ + mock.patch.object(upgrade.subprocess, 'run') as run: + value = upgrade.snapshot(processes) + self.assertEqual(value['profiles'], []) + self.assertEqual(upgrade.schedule()['phase'], 'COMPLETE') + self.assertEqual(json.loads((self.root / 'previous.json').read_text()), failed) + self.assertEqual(marker.read_bytes(), before_marker) + run.assert_not_called() + + def test_reinstall_after_failure_captures_only_current_managers_without_replaying_old_intent(self): + failed = {'schema': 1, 'phase': 'FAILED', + 'profiles': [{'state': '/home/owner/previous-profile', 'phase': 'FAILED'}]} + self.write(self.root / 'pending.json', failed) + row = {'uid': 1000, 'state': str(self.state), 'pid': 1234, 'phase': 'CAPTURED'} + with mock.patch.object(upgrade, 'ROOT', self.root), \ + mock.patch.object(upgrade, '_root'), \ + mock.patch.object(upgrade, '_read', side_effect=self.root_read), \ + mock.patch.object(upgrade, 'identify', return_value=row), \ + mock.patch.object(upgrade.subprocess, 'run', return_value=SimpleNamespace(returncode=0)) as run: + value = upgrade.snapshot(self.root) + self.assertEqual(value['profiles'], [row]) + self.assertEqual(upgrade.schedule()['phase'], 'SCHEDULED') + self.assertEqual(json.loads((self.root / 'previous.json').read_text()), failed) + self.assertEqual(run.call_count, 1) + self.assertEqual(run.call_args.args[0][-2:], [upgrade.MODULE, 'resume']) + + def test_reinstall_does_not_replace_a_snapshot_with_a_live_upgrade_worker(self): + for phase in ('SCHEDULED', 'RUNNING'): + with self.subTest(phase=phase): + self.write(self.root / 'pending.json', {'schema': 1, 'phase': phase}) + before = (self.root / 'pending.json').read_bytes() + with mock.patch.object(upgrade, 'ROOT', self.root), \ + mock.patch.object(upgrade, '_root'), \ + mock.patch.object(upgrade, '_read', side_effect=self.root_read), \ + mock.patch.object(upgrade, 'identify', return_value=None): + with self.assertRaisesRegex(RuntimeError, 'PREVIOUS_WORKER_PENDING'): + upgrade.snapshot(self.root) + self.assertEqual((self.root / 'pending.json').read_bytes(), before) + self.assertFalse((self.root / 'previous.json').exists()) + def test_optional_desktop_migration_failure_does_not_stop_runtime_recovery(self): row = {'uid': 1000, 'user': 'owner', 'home': '/home/owner', 'state': str(self.state), 'pid': 1234, 'birth': '99', diff --git a/source/tests/test_public_release_metadata.py b/source/tests/test_public_release_metadata.py new file mode 100644 index 0000000..830bca8 --- /dev/null +++ b/source/tests/test_public_release_metadata.py @@ -0,0 +1,133 @@ +"""Release assembly rejects stale or mismatched corresponding source.""" +import hashlib +import io +import json +from pathlib import Path +import sys +import tarfile +import tempfile +import unittest +from unittest import mock + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "packaging")) +import build_expanded +from release_metadata import release_source + + +class PublicReleaseSourceTests(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.root = Path(self.tmp.name) + self.file = self.root / "module.py" + self.file.write_text("# reviewed release source\n") + self.archive = self.root / "FrameBridge-0.3.4-source.tar.gz" + self.vendor_paths = { + "cp313": "source/sources/original-geranas/FrameBridge-Fleet-Hub/vendor", + "cp312": "source/target-vendors/cp312", + } + vendor_data = b"# reviewed vendor input\n" + self.vendor_files = {"module.py": vendor_data, + "package/__init__.py": b"# package\n", + "package-1.dist-info/METADATA": b"Name: package\nVersion: 1\n"} + self.payloads = {"source/module.py": self.file.read_bytes(), + **{path + "/" + relative: data for path in self.vendor_paths.values() + for relative, data in self.vendor_files.items()}} + self.write_archive() + # The reviewed vendor format uses Path component ordering. In particular + # package/__init__.py comes before package-1.dist-info/METADATA. + digest = hashlib.sha256() + for relative in ("module.py", "package/__init__.py", "package-1.dist-info/METADATA"): + digest.update(relative.encode() + b"\0") + digest.update(hashlib.sha256(self.vendor_files[relative]).digest()) + self.vendor = {"vendor_sha256": digest.hexdigest()} + self.data = { + "version": "0.3.4", "status": "RELEASE_SOURCE_ASSEMBLED", + "archive": self.archive.name, "bytes": self.archive.stat().st_size, + "sha256": hashlib.sha256(self.archive.read_bytes()).hexdigest(), + "vendor_inputs": {"vendors": {abi: {**self.vendor, "path": path} for abi, path in self.vendor_paths.items()}}, + "files": [{"path": path, "sha256": hashlib.sha256(data).hexdigest()} for path, data in self.payloads.items()], + } + self.manifest = self.root / "SOURCE-ARCHIVE-MANIFEST.json" + + def write_archive(self): + with tarfile.open(self.archive, "w:gz") as archive: + for path, data in self.payloads.items(): + info = tarfile.TarInfo("FrameBridge-0.3.4/" + path) + info.size = len(data) + archive.addfile(info, io.BytesIO(data)) + + def check(self, profile="debian13-amd64-py313", version="0.3.4"): + self.manifest.write_text(json.dumps(self.data)) + return release_source(self.manifest, version, profile, {"source/module.py": self.file}, self.vendor) + + def test_release_binds_archive_source_and_both_vendor_abis(self): + for profile in ("debian13-amd64-py313", "ubuntu24.04-amd64-py312", "mint22-amd64-py312"): + self.assertEqual(self.check(profile)["sha256"], self.data["sha256"]) + + def test_source_changed_after_archive_is_rejected(self): + self.file.write_text("# later edit\n") + with self.assertRaisesRegex(RuntimeError, "Package source differs"): + self.check() + + def test_archive_tamper_is_rejected(self): + with self.archive.open("ab") as stream: + stream.write(b"altered") + with self.assertRaisesRegex(RuntimeError, "archive hash or size"): + self.check() + + def test_changed_manifest_cannot_relabel_old_archive_contents(self): + self.file.write_text("# later edit\n") + self.data["files"][0]["sha256"] = hashlib.sha256(self.file.read_bytes()).hexdigest() + with self.assertRaisesRegex(RuntimeError, "archive content differs"): + self.check() + + def test_relabelled_alternate_vendor_bytes_are_rejected_even_by_debian_build(self): + self.payloads[self.vendor_paths["cp312"] + "/module.py"] = b"# different vendor bytes\n" + self.write_archive() + self.data.update(bytes=self.archive.stat().st_size, sha256=hashlib.sha256(self.archive.read_bytes()).hexdigest()) + self.data["files"] = [{"path": path, "sha256": hashlib.sha256(data).hexdigest()} for path, data in self.payloads.items()] + with self.assertRaisesRegex(RuntimeError, "Archived vendor bytes differ"): + self.check() + + def test_unsafe_archive_member_is_rejected_without_extracting(self): + self.payloads["../escape"] = b"unwanted path" + self.write_archive() + self.data.update(bytes=self.archive.stat().st_size, sha256=hashlib.sha256(self.archive.read_bytes()).hexdigest()) + self.data["files"].append({"path": "../escape", "sha256": hashlib.sha256(b"unwanted path").hexdigest()}) + with self.assertRaisesRegex(RuntimeError, "Unsafe release source archive path"): + self.check() + + def test_release_requires_cp312_corresponding_source(self): + del self.data["vendor_inputs"]["vendors"]["cp312"] + with self.assertRaisesRegex(RuntimeError, "cp312 and cp313"): + self.check() + + def test_vendor_input_cannot_differ_from_corresponding_source(self): + self.data["vendor_inputs"]["vendors"]["cp312"]["vendor_sha256"] = "b" * 64 + with self.assertRaisesRegex(RuntimeError, "vendor input differs"): + self.check("mint22-amd64-py312") + + def test_candidate_version_and_experimental_profile_are_not_public_release(self): + for profile, version in (("lmde7-amd64-py313", "0.3.4"), ("debian13-amd64-py313", "0.3.4~dev.1")): + with self.assertRaisesRegex(RuntimeError, "stable version and an official"): + self.check(profile, version) + + def test_public_metadata_names_source_without_claiming_signature_or_vm_qa(self): + docs = self.root / "docs" + docs.mkdir() + identity = self.check() + with mock.patch.object(build_expanded, "PUBLIC_RELEASE", True), \ + mock.patch.object(build_expanded, "SOURCE_ARCHIVE", identity), \ + mock.patch.object(build_expanded, "VERSION", "0.3.4"): + build_expanded.install_legal_material(docs) + result = json.loads((docs / "COMMUNITY-RELEASE-STATUS.json").read_text()) + self.assertEqual(result["status"], "PUBLIC_RELEASE_BUILD_ASSEMBLED") + self.assertEqual(result["source_archive"], identity) + self.assertEqual(result["signature_status"], "EXTERNAL_RELEASE_RECORD") + self.assertEqual(result["vm_lifecycle_validation"], "NOT_ASSERTED_BY_BUILDER") + self.assertNotIn("compatibility candidate", (docs / "copyright").read_text()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/README-APT.md b/tools/README-APT.md deleted file mode 100644 index 48be9d5..0000000 --- a/tools/README-APT.md +++ /dev/null @@ -1,134 +0,0 @@ -# Signed distribution repositories - -The canonical source is the existing GERANAS Gitea repository -`gerardo/framebridge`. Build all three reviewed DEBs from its release commit; -do not edit installed files to produce an update. - -One HTTPS repository at `https://framebridge.betancourt.one/debian/` contains -three independent suites: - -| Suite | Tested distribution profile | -| --- | --- | -| trixie | Debian 13, amd64, Python 3.13 | -| noble | Ubuntu 24.04, amd64, Python 3.12 | -| mint22 | Linux Mint 22.x, amd64, Python 3.12 | - -Select exactly the suite matching the computer. These are three builds of one -binary package, `framebridge-original-expanded`, with unique pool filenames. -This table does not certify untested releases or architectures. - -## Prepare and review - -Run `python3 tools/build_apt_repository.py prepare --help` in the source -repository. Supply the exact three version-matched DEBs, the existing official -public key and a new output directory. The command copies the artifacts, builds -indexes and by-hash paths, and prints the immutable input manifest SHA-256. -It does not open a private keyring, sign, publish or install anything. - -Review the manifest, package hashes and release identity before signing. -The public key must contain exactly one primary release key with fingerprint -`2B1F01BC806C0B33CB6CEDDE52859CEBC91B8808`. Official subkeys are allowed. - -## Sign in the owner's normal terminal - -Run `python3 tools/build_apt_repository.py sign --help`. Pass the reviewed -output directory and exact manifest SHA-256. The signer requires a non-root -interactive terminal and an owner-private existing signing home. Set -`GPG_TTY=$(tty)` in that normal terminal if the pinentry implementation needs it. -The owner's passphrase is entered only into normal pinentry; it is never an -argument, environment variable or chat message. - -The signer writes `Release.gpg` and `InRelease` for each suite, verifies the -signatures against the public key and exact reviewed Release plaintext, then -clears the dedicated signing agent. Existing signatures or partial results -cause refusal; inspect a partial transaction instead of blindly rerunning it. - -Run `verify` with the same pinned manifest hash after signing and again after -transport. Metadata expires after 30 days; publish a freshly signed Release -before expiry, without altering historical signed artifacts. - -## Publish through GERANAS - -Use the existing GERANAS web origin and the CLOUD HTTPS reverse proxy. Do not -create another Git backend or expose MCP/auth/broker services. Back up the -current public generation before activation. Preserve the existing signed -0.3.1 downloads and old repository metadata in an archive path. - -Publish only reviewed manifest files, the six verified signature files and the -signing receipt. Public directories must be `0755` and files `0644`; a private -staging umask must not leak into the served generation. Never publish the -signing home, private receipts, account configuration or operational worktree. -Activate a complete verified generation, preserving the web unit's existing -read-only bind mounts, then run isolated APT update and exact-version download -for all three suites through the final HTTPS authority. - -## Configure a client - -Install the verified official public key as -`/etc/apt/keyrings/framebridge-archive-keyring.asc` with mode `0644`. -Its fingerprint must match the value above. Add a Deb822 source file: - -```text -Types: deb -URIs: https://framebridge.betancourt.one/debian/ -Suites: trixie -Components: main -Architectures: amd64 -Signed-By: /etc/apt/keyrings/framebridge-archive-keyring.asc -``` - -Replace `trixie` with `noble` or `mint22` for those tested profiles. Then use -normal APT commands: - -```sh -sudo apt update -apt-cache policy framebridge-original-expanded -sudo apt install --only-upgrade framebridge-original-expanded -``` - -Do not use trusted=yes or disable signature/validity checks. APT update/download -verification does not itself prove runtime restart or reboot persistence. -Confirm the actual installed version and ready connection after each upgrade. - -## Tool checks - -`python3 tools/test_build_apt_repository.py` creates disposable packages and a -disposable signing identity in a temporary directory. It tests actual isolated -APT acceptance for each suite and rejects altered packages/indexes/signatures, -unexpected versions, path traversal and bundled extra primary trust keys. -It never uses the official private signing key or changes system APT sources. - - -## One signing session for the release - -For a complete release, also pass --source with the reviewed -FrameBridge-VERSION-source.tar.gz and --dependency-source twice with the two -existing dependency-source archives. Their original sibling .asc signatures -are verified and copied unchanged. The generator creates a downloads staging -subdirectory with the three DEBs, source, dependency sources and SHA256SUMS. -The same input manifest covers all of these bytes. The owner-terminal sign -command signs the three DEBs, project source and checksums in the same session -as the three APT suites. Existing dependency signatures are preserved. - -Map the staging downloads subdirectory to the existing public /downloads/; -the rest of the repository goes under /debian/. Do not publish a nested second -/debian/downloads/ route as the official download location. Verify every -signature with the pinned manifest before activating either path. - -## Canonical Gitea publication - -publish_gitea.py is an administrator tool for the existing GERANAS Gitea, not a -second Git host. Its default mode checks a root-frozen bundle SHA-256, exact old -main, ancestor relationship and the new release tag. --apply requires an -exclusive journal in a root-owned private directory. Run only the reviewed -root-frozen tool and bundle using the normal authorized administrator context. - -The tool creates a narrowly scoped, uniquely named temporary token using the -installed Gitea administrator CLI. It keeps the token only in process memory, -feeds normal Git ASKPASS through a private Unix socket and performs an atomic -push without a force flag. It verifies exact refs, revokes the temporary token -through normal Basic authentication and verifies HTTP 401 afterward. The -journal contains the nonsecret token name so an interrupted transaction can be -reviewed. If any step is uncertain, inspect that transaction rather than -replaying it. No token value is placed in URLs, arguments, environment variables, -files or reports. diff --git a/tools/build_apt_repository.py b/tools/build_apt_repository.py deleted file mode 100755 index fc79756..0000000 --- a/tools/build_apt_repository.py +++ /dev/null @@ -1,299 +0,0 @@ -#!/usr/bin/env python3 -"""Build and verify three signed APT suites from the reviewed distribution DEBs.""" -from __future__ import annotations - -import argparse -import datetime as dt -import email.utils -import gzip -import hashlib -import json -import os -from pathlib import Path -import re -import shutil -import subprocess -import sys -import tempfile - -PACKAGE = "framebridge-original-expanded" -FINGERPRINT = "2B1F01BC806C0B33CB6CEDDE52859CEBC91B8808" -DEPENDENCY_SOURCE_ASSETS = { - "FrameBridge-0.3.1-Community-Preview1-dependency-sources.tar.gz": (82606998, "64540a85bd65eac3f394d8c0a967cacbc00e112676d57d644dcd1bb691c1926a"), - "FrameBridge-0.3.1-tunnel-client-dependency-sources.tar.gz": (26941745, "6445d4f0e773aba833fe5c610fbae54102b92de2ce636438f769f21e890c2bd9"), -} -PROFILES = { - "debian": ("trixie", "Debian 13", "debian13-amd64-py313", "3.13"), - "ubuntu": ("noble", "Ubuntu 24.04", "ubuntu24.04-amd64-py312", "3.12"), - "mint": ("mint22", "Linux Mint 22.x", "mint22-amd64-py312", "3.12"), -} - -def require(condition, message): - if not condition: - raise ValueError(message) - -def digest(data, algorithm="sha256"): - return hashlib.new(algorithm, data).hexdigest() - -def run(argv, **kwargs): - return subprocess.run([str(x) for x in argv], check=True, capture_output=True, **kwargs) - -def dump(path, value): - path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n") - -def public_fingerprint(key): - with tempfile.TemporaryDirectory(prefix="framebridge-public-key-") as tmp: - os.chmod(tmp, 0o700) - result = run(["gpg", "--no-options", "--homedir", tmp, "--batch", - "--with-colons", "--show-keys", key]).stdout.decode() - fingerprints = [line.split(":")[9] for line in result.splitlines() - if line.startswith("fpr:")] - primary_keys = [line for line in result.splitlines() if line.startswith("pub:")] - require(len(primary_keys) == 1 and fingerprints and fingerprints[0] == FINGERPRINT, - "Expected exactly one official primary release key") - return fingerprints[0] - -def control(path): - text = run(["dpkg-deb", "--field", path]).stdout.decode() - fields = {} - for line in text.splitlines(): - if line and not line[0].isspace(): - name, separator, value = line.partition(":") - require(separator, "Invalid Debian control field") - fields[name] = value.strip() - require(not any(k in fields for k in ("Filename", "Size", "MD5sum", "SHA256", "SHA512")), - "Artifact control contains repository-generated fields") - return text.rstrip() + "\n", fields - -def release_bytes(suite, label, date, files): - lines = ["Origin: FrameBridge", "Label: FrameBridge", f"Suite: {suite}", - f"Codename: {suite}", f"Date: {email.utils.format_datetime(date)}", - f"Valid-Until: {email.utils.format_datetime(date + dt.timedelta(days=30))}", - "Architectures: amd64", "Components: main", - f"Description: FrameBridge for {label}", "Acquire-By-Hash: yes"] - for algorithm, heading in (("sha256", "SHA256"), ("sha512", "SHA512")): - lines.append(heading + ":") - lines.extend(f" {digest(data, algorithm)} {len(data):16d} {name}" - for name, data in sorted(files.items())) - return ("\n".join(lines) + "\n").encode() - - -def verify_detached(public_key, signature, payload): - with tempfile.TemporaryDirectory(prefix="framebridge-asset-verify-") as tmp: - tmp = Path(tmp) - ring = tmp / "release.gpg" - ring.write_bytes(run(["gpg", "--no-options", "--homedir", tmp, "--batch", "--output", "-", - "--dearmor", public_key]).stdout) - result = run(["gpgv", "--homedir", tmp, "--keyring", ring, "--status-fd", "1", signature, payload]) - require(f"[GNUPG:] VALIDSIG {FINGERPRINT} ".encode() in result.stdout, "Unexpected dependency-source signer") - -def prepare_downloads(args, inputs): - source = getattr(args, "source", None) - dependencies = getattr(args, "dependency_source", []) - if source is None: - require(not dependencies, "Dependency assets require the corresponding project-source archive") - return [] - require(source.is_file() and not source.is_symlink() and - source.name == f"FrameBridge-{args.version}-source.tar.gz", "Unexpected project-source archive") - require({p.name for p in dependencies} == set(DEPENDENCY_SOURCE_ASSETS) and - len(dependencies) == len(DEPENDENCY_SOURCE_ASSETS), "Exactly the reviewed two dependency-source archives required") - for path in dependencies: - expected_size, expected_sha = DEPENDENCY_SOURCE_ASSETS[path.name] - require(path.is_file() and not path.is_symlink(), "Unsafe dependency-source archive") - data = path.read_bytes() - require(len(data) == expected_size and digest(data) == expected_sha, "Dependency-source archive hash mismatch") - verify_detached(args.public_key, path.with_name(path.name + ".asc"), path) - directory = args.output / "downloads" - directory.mkdir() - assets = [] - def add(name, data, needs_signing): - target = directory / name - require(not target.exists(), "Duplicate release filename") - target.write_bytes(data) - assets.append({"filename": name, "size_bytes": len(data), "sha256": digest(data), - "signature": name + ".asc", "needs_signing": needs_signing}) - for _, _, _, profile, _, data in inputs: - add(f"FrameBridge-{args.version}-{profile}.deb", data, True) - add(source.name, source.read_bytes(), True) - for path in dependencies: - add(path.name, path.read_bytes(), False) - shutil.copyfile(path.with_name(path.name + ".asc"), directory / (path.name + ".asc")) - shutil.copyfile(args.public_key, directory / "framebridge-release-key.asc") - sums = "".join(item["sha256"] + " " + item["filename"] + "\n" for item in assets) - add("SHA256SUMS", sums.encode(), True) - return assets - -def prepare(args): - output = args.output - require(not output.exists(), "Output must be a new directory; never overwrite a published repository") - require(re.fullmatch(r"[0-9][A-Za-z0-9.+:~_-]*", args.version), "Invalid version") - public_fingerprint(args.public_key) - epoch = args.epoch if args.epoch is not None else int(dt.datetime.now(dt.timezone.utc).timestamp()) - date = dt.datetime.fromtimestamp(epoch, dt.timezone.utc) - inputs = [] - for distro, (suite, label, profile, python) in PROFILES.items(): - path = getattr(args, distro) - require(path.is_file() and not path.is_symlink(), f"{distro}: artifact is not a regular file") - text, fields = control(path) - require(fields.get("Package") == PACKAGE, f"{distro}: wrong binary package") - require(fields.get("Version") == args.version, f"{distro}: wrong version") - require(fields.get("Architecture") == "amd64", f"{distro}: wrong architecture") - require(re.search(r"python3\s*\(>=\s*" + re.escape(python), fields.get("Depends", "")), - f"{distro}: Python dependency does not match the tested profile") - data = path.read_bytes() - inputs.append((distro, suite, label, profile, text, data)) - output.mkdir(parents=True, mode=0o755) - records = [] - for distro, suite, label, profile, text, data in inputs: - relative = f"pool/main/f/framebridge/{PACKAGE}_{args.version}_{profile}.deb" - artifact = output / relative - artifact.parent.mkdir(parents=True, exist_ok=True) - artifact.write_bytes(data) - require(digest(artifact.read_bytes()) == digest(data), "Copied artifact hash mismatch") - stanza = text + f"Filename: {relative}\nSize: {len(data)}\nSHA256: {digest(data)}\nSHA512: {digest(data, 'sha512')}\n\n" - plain = stanza.encode() - indexes = {"main/binary-amd64/Packages": plain, - "main/binary-amd64/Packages.gz": gzip.compress(plain, mtime=0)} - suite_dir = output / "dists" / suite - for name, index in indexes.items(): - target = suite_dir / name - target.parent.mkdir(parents=True, exist_ok=True) - target.write_bytes(index) - for algorithm in ("sha256", "sha512"): - by_hash = target.parent / "by-hash" / algorithm.upper() / digest(index, algorithm) - by_hash.parent.mkdir(parents=True, exist_ok=True) - by_hash.write_bytes(index) - (suite_dir / "Release").write_bytes(release_bytes(suite, label, date, indexes)) - records.append({"distribution": distro, "label": label, "suite": suite, - "profile": profile, "filename": relative, - "size": len(data), "sha256": digest(data)}) - downloads = prepare_downloads(args, inputs) - shutil.copyfile(args.public_key, output / "framebridge-archive-keyring.asc") - files = {p.relative_to(output).as_posix(): {"bytes": p.stat().st_size, - "sha256": digest(p.read_bytes())} for p in sorted(output.rglob("*")) if p.is_file()} - manifest = {"schema": 1, "package": PACKAGE, "version": args.version, - "fingerprint": FINGERPRINT, "prepared_utc": date.isoformat(), - "artifacts": records, "download_assets": downloads, "files": files} - dump(output / "APT-INPUT-MANIFEST.json", manifest) - manifest_hash = digest((output / "APT-INPUT-MANIFEST.json").read_bytes()) - print(json.dumps({"prepared": True, "signed": False, "directory": str(output), - "version": args.version, "manifest_sha256": manifest_hash, - "suites": [x["suite"] for x in records]}, sort_keys=True)) - -def check_inputs(output, expected): - p = output / "APT-INPUT-MANIFEST.json" - require(p.is_file() and not p.is_symlink(), "Input manifest missing or unsafe") - raw = p.read_bytes() - require(digest(raw) == expected, "Manifest hash mismatch") - manifest = json.loads(raw) - require(manifest.get("package") == PACKAGE and manifest.get("fingerprint") == FINGERPRINT, - "Wrong package or signing identity") - require(len(manifest.get("artifacts", [])) == 3, "Exactly three distribution artifacts required") - require({x["suite"] for x in manifest["artifacts"]} == {x[0] for x in PROFILES.values()}, - "Unexpected distribution suite set") - for name, metadata in manifest["files"].items(): - relative = Path(name) - require(not relative.is_absolute() and ".." not in relative.parts, "Unsafe manifest path") - path = output / relative - require(path.is_file() and not path.is_symlink(), f"Missing or unsafe input: {name}") - data = path.read_bytes() - require(len(data) == metadata["bytes"] and digest(data) == metadata["sha256"], - f"Input changed after review: {name}") - public_fingerprint(output / "framebridge-archive-keyring.asc") - return manifest - -def signature_paths(output, sign_only=False): - for suite, *_ in PROFILES.values(): - root = output / "dists" / suite - yield root / "Release", root / "InRelease", True - yield root / "Release", root / "Release.gpg", False - manifest = json.loads((output / "APT-INPUT-MANIFEST.json").read_text()) - for asset in manifest.get("download_assets", []): - if sign_only and not asset["needs_signing"]: - continue - filename = asset["filename"] - require(Path(filename).name == filename and Path(asset["signature"]).name == asset["signature"], - "Unsafe download signature path") - yield output / "downloads" / filename, output / "downloads" / asset["signature"], False - -def verify_signatures(output, manifest): - with tempfile.TemporaryDirectory(prefix="framebridge-apt-verify-") as tmp: - tmp = Path(tmp) - keyring = tmp / "release.gpg" - keyring.write_bytes(run(["gpg", "--no-options", "--homedir", tmp, "--batch", "--output", "-", "--dearmor", - output / "framebridge-archive-keyring.asc"]).stdout) - for release, signed, clearsign in signature_paths(output): - require(signed.is_file() and not signed.is_symlink(), "Missing signature") - args = ["gpgv", "--homedir", tmp, "--keyring", keyring, "--status-fd", "1"] - result = run(args + ([signed] if clearsign else [signed, release])) - require(f"[GNUPG:] VALIDSIG {FINGERPRINT} ".encode() in result.stdout, - "Signature made by unexpected key") - if clearsign: - plaintext = run(["gpgv", "--homedir", tmp, "--keyring", keyring, - "--output", "-", signed]).stdout - require(plaintext == release.read_bytes(), "Clear-signed release differs from reviewed input") - return {"verified": True, "version": manifest["version"], - "suites": [x["suite"] for x in manifest["artifacts"]], "fingerprint": FINGERPRINT} - -def sign(args): - require(os.getuid() != 0 and sys.stdin.isatty() and os.isatty(1), - "Signing must run as the owner in a normal interactive terminal") - manifest = check_inputs(args.output, args.manifest_sha256) - require(args.key_home.is_dir() and not args.key_home.is_symlink(), "Unsafe signing home") - key_stat = args.key_home.stat() - require(key_stat.st_uid == os.getuid() and key_stat.st_mode & 0o077 == 0, "Signing home must be owner-private") - targets = list(signature_paths(args.output, sign_only=True)) - require(all(not target.exists() for _, target, _ in targets), "Signatures already exist; do not overwrite") - common = ["gpg", "--no-options", "--homedir", args.key_home, "--pinentry-mode", "default", - "--local-user", FINGERPRINT, "--digest-algo", "SHA256"] - try: - for source, target, clearsign in targets: - check_inputs(args.output, args.manifest_sha256) - temporary = target.with_name(target.name + ".partial") - require(not temporary.exists(), "Incomplete previous signing transaction needs review") - subprocess.run([str(x) for x in common + ["--output", temporary, - "--clearsign" if clearsign else "--detach-sign", source]], check=True) - temporary.rename(target) - result = verify_signatures(args.output, manifest) - result["manifest_sha256"] = args.manifest_sha256 - dump(args.output / "APT-SIGNING-RECEIPT.json", result) - print(json.dumps(result, sort_keys=True)) - finally: - run(["gpgconf", "--homedir", args.key_home, "--kill", "gpg-agent"]) - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - commands = parser.add_subparsers(dest="command", required=True) - prepare_parser = commands.add_parser("prepare") - prepare_parser.add_argument("--output", type=Path, required=True) - prepare_parser.add_argument("--version", required=True) - for distro in PROFILES: - prepare_parser.add_argument("--" + distro, type=Path, required=True) - prepare_parser.add_argument("--public-key", type=Path, required=True) - prepare_parser.add_argument("--epoch", type=int) - prepare_parser.add_argument("--source", type=Path, help="Reviewed corresponding project-source tar.gz") - prepare_parser.add_argument("--dependency-source", type=Path, action="append", default=[], - help="Reviewed dependency-source archive with its unchanged sibling .asc") - for name in ("sign", "verify"): - sub = commands.add_parser(name) - sub.add_argument("--output", type=Path, required=True) - sub.add_argument("--manifest-sha256", required=True) - if name == "sign": - sub.add_argument("--key-home", type=Path, - default=Path.home() / ".local/share/framebridge-release-signing") - args = parser.parse_args() - try: - if args.command == "prepare": - prepare(args) - elif args.command == "sign": - sign(args) - else: - manifest = check_inputs(args.output, args.manifest_sha256) - print(json.dumps(verify_signatures(args.output, manifest), sort_keys=True)) - except (ValueError, OSError, subprocess.CalledProcessError) as error: - print(f"FrameBridge APT repository: {error}", file=sys.stderr) - return 1 - return 0 - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/publish_gitea.py b/tools/publish_gitea.py deleted file mode 100755 index eff1bba..0000000 --- a/tools/publish_gitea.py +++ /dev/null @@ -1,227 +0,0 @@ -#!/usr/bin/env python3 -"""Publish a reviewed release by normal authenticated fast-forward Git push.""" -import argparse -import base64 -import hashlib -import json -import os -from pathlib import Path -import re -import shutil -import socket -import subprocess -import sys -import tempfile -import threading -import urllib.error -import urllib.parse -import urllib.request -import uuid - -REPOSITORY = "http://127.0.0.1:3005/gerardo/framebridge.git" -API = "http://127.0.0.1:3005/api/v1" -CONTAINER = "geranas-gitea" - -def require(value, message): - if not value: - raise ValueError(message) - -def run(args, **kwargs): - return subprocess.run([str(x) for x in args], check=True, capture_output=True, **kwargs) - -def remote_refs(): - raw = run(["git", "-c", "credential.helper=", "ls-remote", REPOSITORY]).stdout.decode() - return dict((ref, sha) for sha, ref in (line.split() for line in raw.splitlines())) - -def api_request(method, path, token): - request = urllib.request.Request(API + path, method=method, - headers={"Authorization": "Basic " + base64.b64encode(("gerardo:" + token).encode()).decode()}) - with urllib.request.urlopen(request, timeout=15) as response: - return response.status, response.read() - -def check_bundle(args, source): - data = args.bundle.read_bytes() - require(hashlib.sha256(data).hexdigest() == args.bundle_sha256, "Bundle hash mismatch") - run(["git", "clone", "--bare", "--no-hardlinks", args.bundle, source]) - run(["git", "--git-dir", source, "fsck", "--full"]) - actual = run(["git", "--git-dir", source, "rev-parse", args.commit + "^{commit}"]).stdout.decode().strip() - require(actual == args.commit, "Reviewed release commit missing") - run(["git", "--git-dir", source, "merge-base", "--is-ancestor", args.old_commit, args.commit]) - resolved = run(["git", "--git-dir", source, "rev-parse", args.tag + "^{}"]).stdout.decode().strip() - require(resolved == args.commit, "Release tag does not point to reviewed commit") - refs = remote_refs() - require(refs.get("refs/heads/main") == args.old_commit, "Canonical main changed; review before publishing") - require("refs/tags/" + args.tag not in refs, "Release tag already exists; never overwrite") - return refs - -def record(path, state): - temporary = path.with_suffix(".partial") - with open(temporary, "x", encoding="utf-8") as stream: - json.dump(state, stream, indent=2, sort_keys=True) - stream.write("\n") - stream.flush() - os.fsync(stream.fileno()) - temporary.replace(path) - -def askpass_server(directory, token): - address = str(directory / "credential.sock") - server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - server.bind(address) - os.chmod(address, 0o600) - server.listen(2) - server.settimeout(0.5) - stop = threading.Event() - def serve(): - while not stop.is_set(): - try: - connection, _ = server.accept() - except socket.timeout: - continue - with connection: - connection.settimeout(3) - request = connection.recv(32) - if request == b"username": - connection.sendall(b"gerardo\n") - elif request == b"password": - connection.sendall(token.encode() + b"\n") - thread = threading.Thread(target=serve, daemon=True) - thread.start() - client = directory / "askpass" - client.write_text( - "#!/usr/bin/python3\nimport socket,sys\n" - "p=sys.argv[1].lower()\n" - "kind=b'username' if p.startswith('username') else b'password' if p.startswith('password') else None\n" - "if kind is None: raise SystemExit(1)\n" - "s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM)\n" - f"s.connect({address!r})\ns.sendall(kind)\ns.shutdown(socket.SHUT_WR)\n" - "data=b''\n" - "while True:\n chunk=s.recv(4096)\n if not chunk: break\n data+=chunk\n" - "sys.stdout.buffer.write(data)\n") - client.chmod(0o700) - return server, stop, thread, client - -def root_directory(path): - require(path.is_absolute() and path.resolve(strict=True) == path, "Root staging path must be absolute without symlink parents") - require(path.is_dir() and path.stat().st_mode & 0o077 == 0, "Root staging directory must be private") - for current in (path, *path.parents): - metadata = current.stat() - require(metadata.st_uid == 0 and metadata.st_mode & 0o022 == 0, - "Root staging path has a non-root or shared-writable ancestor") - -def publish(args): - require(os.geteuid() == 0 and socket.gethostname().lower() == "geranas", - "Run on GERANAS using the authorized normal administrator context") - for sha in (args.bundle_sha256,): - require(re.fullmatch(r"[a-f0-9]{64}", sha), "Invalid bundle SHA-256") - for sha in (args.commit, args.old_commit): - require(re.fullmatch(r"[a-f0-9]{40}", sha), "Invalid source commit") - require(re.fullmatch(r"v[0-9]+\.[0-9]+\.[0-9]+", args.tag), "Invalid release tag") - require(args.bundle.is_file() and not args.bundle.is_symlink(), "Unsafe bundle path") - bundle_stat = args.bundle.stat() - require(bundle_stat.st_uid == 0 and bundle_stat.st_mode & 0o022 == 0 and bundle_stat.st_nlink == 1, - "Bundle must be a root-owned reviewed regular file without shared write access") - root_directory(args.bundle.parent) - os.umask(0o077) - with tempfile.TemporaryDirectory(prefix="fb-gitea-push-", dir="/run") as tmp: - directory = Path(tmp) - source = directory / "source.git" - before = check_bundle(args, source) - state = {"schema": 1, "repository": "gerardo/framebridge", "old_commit": args.old_commit, - "commit": args.commit, "tag": args.tag, "bundle_sha256": args.bundle_sha256, - "phase": "CHECKED", "push_performed": False, "token_revoked": False} - if not args.apply: - print(json.dumps(state, sort_keys=True)) - return - require(args.journal is not None and args.journal.parent.is_dir(), "Explicit existing journal directory required") - root_directory(args.journal.parent) - require(not args.journal.exists() and not args.journal.is_symlink(), "Existing transaction; never replay blindly") - # Reserve the exact new transaction before minting a credential. - descriptor = os.open(args.journal, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) - os.close(descriptor) - record(args.journal, state) - token_name = "framebridge-release-" + uuid.uuid4().hex - state["temporary_token_name"] = token_name - state["phase"] = "TOKEN_CREATION_REQUESTED" - record(args.journal, state) - token = None - failure = None - server = stop = thread = None - try: - # Capture the token only in this process's memory. Never log this result. - result = run(["docker", "exec", "--user", "git", CONTAINER, "gitea", - "--config", "/etc/gitea/app.ini", "admin", "user", "generate-access-token", - "--username", "gerardo", "--token-name", token_name, "--raw", - "--scopes", "write:repository,write:user"]) - token = result.stdout.decode().strip() - require(re.fullmatch(r"[a-fA-F0-9]{40,128}", token), "Unexpected credential response; token needs owner review") - state["phase"] = "TEMPORARY_TOKEN_CREATED" - record(args.journal, state) - # The installed token-management route requires Basic authentication. - # Gitea accepts a PAT as the Basic password; prove that gate before pushing. - status, _ = api_request("GET", "/users/gerardo/tokens?limit=1", token) - require(status == 200, "Normal Basic token-management authentication failed") - state["token_management_auth_verified"] = True - record(args.journal, state) - server, stop, thread, client = askpass_server(directory, token) - env = dict(os.environ, GIT_ASKPASS=str(client), GIT_TERMINAL_PROMPT="0", LC_ALL="C") - # No force flag. The normal receive-pack checks and Gitea hooks run. - run(["git", "--git-dir", source, "-c", "credential.helper=", "push", "--atomic", - REPOSITORY, args.commit + ":refs/heads/main", "refs/tags/" + args.tag], env=env, timeout=120) - state["push_performed"] = True - state["phase"] = "PUSH_COMPLETED" - after = remote_refs() - require(after.get("refs/heads/main") == args.commit, "Main ref readback mismatch") - resolved_tag = after.get("refs/tags/" + args.tag + "^{}", after.get("refs/tags/" + args.tag)) - require(resolved_tag == args.commit, "Release tag readback mismatch") - state["refs_verified"] = True - record(args.journal, state) - except Exception as error: - # Commands never contain the credential; do not print captured HTTP/Git output. - failure = f"{type(error).__name__}: {error}" - state["failure"] = failure - finally: - if stop is not None: - stop.set() - thread.join(timeout=2) - server.close() - if token is not None: - try: - status, _ = api_request("DELETE", "/users/gerardo/tokens/" + - urllib.parse.quote(token_name, safe=""), token) - require(status == 204, "Unexpected token revocation response") - try: - api_request("GET", "/user", token) - except urllib.error.HTTPError as error: - require(error.code == 401, "Revoked credential did not return HTTP 401") - else: - raise ValueError("Credential remains accepted after revocation") - state["token_revoked"] = True - except Exception as error: - state["revocation_failure"] = f"{type(error).__name__}: {error}" - failure = failure or "Temporary token revocation requires review" - token = None - state["phase"] = ("COMPLETED" if state.get("refs_verified") and - state["token_revoked"] and failure is None else "REVIEW_REQUIRED") - record(args.journal, state) - print(json.dumps(state, sort_keys=True)) - require(failure is None, failure) - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--bundle", type=Path, required=True) - parser.add_argument("--bundle-sha256", required=True) - parser.add_argument("--commit", required=True) - parser.add_argument("--old-commit", required=True) - parser.add_argument("--tag", required=True) - parser.add_argument("--journal", type=Path) - parser.add_argument("--apply", action="store_true") - args = parser.parse_args() - try: - publish(args) - except Exception as error: - print(f"FrameBridge Gitea publication: {type(error).__name__}: {error}", file=sys.stderr) - return 1 - return 0 - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/run_tests.py b/tools/run_tests.py deleted file mode 100755 index aa5c5f6..0000000 --- a/tools/run_tests.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python3 -"""Run source tests with generated fixtures and an isolated application environment.""" -from pathlib import Path -import os -import stat -import subprocess -import sys -import tempfile - - -def main(): - root = Path(__file__).resolve().parents[1] - qa = root / "source/qa" - qa.mkdir(mode=0o700, exist_ok=True) - info = qa.lstat() - if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o077: - raise SystemExit("The generated source/qa directory must be private and owned by the test user.") - environment = {key: value for key, value in os.environ.items() - if not key.startswith(("FRAMEBRIDGE_", "FBTERM_", "FB_"))} - with tempfile.TemporaryDirectory(prefix="unit-test-home-", dir=qa) as home: - environment.update( - HOME=home, XDG_DATA_HOME=str(Path(home) / ".local/share"), - XDG_CONFIG_HOME=str(Path(home) / ".config"), - PYTHONPATH=str(root / "source/expanded"), PYTHONDONTWRITEBYTECODE="1") - result = subprocess.run([sys.executable, "-m", "unittest", "discover", "-s", - str(root / "source/tests")], cwd=root, env=environment) - return result.returncode - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/test_build_apt_repository.py b/tools/test_build_apt_repository.py deleted file mode 100644 index dc3a2a4..0000000 --- a/tools/test_build_apt_repository.py +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env python3 -"""Integration checks with disposable DEBs and a disposable signing identity.""" -import argparse -import contextlib -import datetime as dt -import hashlib -import importlib.util -import io -import json -import os -from pathlib import Path -import shutil -import subprocess -import tempfile -import unittest - -spec = importlib.util.spec_from_file_location("apt_builder", Path(__file__).with_name("build_apt_repository.py")) -builder = importlib.util.module_from_spec(spec) -spec.loader.exec_module(builder) - -class RepositoryTests(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.temp = tempfile.TemporaryDirectory(prefix="framebridge-apt-tests-") - cls.base = Path(cls.temp.name) - cls.key_home = cls.base / "test-key" - cls.key_home.mkdir(mode=0o700) - subprocess.run(["gpg", "--no-options", "--homedir", str(cls.key_home), "--batch", - "--pinentry-mode", "loopback", "--passphrase", "", "--quick-generate-key", - "FrameBridge disposable test ", "ed25519", "sign", "0"], - check=True, capture_output=True) - result = subprocess.run(["gpg", "--no-options", "--homedir", str(cls.key_home), - "--with-colons", "--list-keys"], check=True, capture_output=True).stdout.decode() - cls.original_fingerprint = builder.FINGERPRINT - builder.FINGERPRINT = next(x.split(":")[9] for x in result.splitlines() if x.startswith("fpr:")) - cls.public_key = cls.base / "test.asc" - cls.public_key.write_bytes(subprocess.run( - ["gpg", "--no-options", "--homedir", str(cls.key_home), "--armor", "--export", builder.FINGERPRINT], - check=True, capture_output=True).stdout) - cls.debs = {} - for distro, (_, _, _, python) in builder.PROFILES.items(): - tree = cls.base / distro - (tree / "DEBIAN").mkdir(parents=True) - (tree / "DEBIAN").chmod(0o755) - (tree / "DEBIAN/control").write_text( - f"Package: {builder.PACKAGE}\nVersion: 0.3.3\nArchitecture: amd64\n" - f"Maintainer: FrameBridge \nDepends: python3 (>= {python})\n" - f"Description: Disposable {distro} test package\n") - (tree / "distribution.txt").write_text(distro) - deb = cls.base / (distro + ".deb") - subprocess.run(["dpkg-deb", "--build", str(tree), str(deb)], check=True, capture_output=True) - cls.debs[distro] = deb - - @classmethod - def tearDownClass(cls): - subprocess.run(["gpgconf", "--homedir", str(cls.key_home), "--kill", "gpg-agent"], capture_output=True) - builder.FINGERPRINT = cls.original_fingerprint - cls.temp.cleanup() - - def setUp(self): - self.output = Path(tempfile.mkdtemp(dir=self.base, prefix="output-")) / "repository" - - def prepare(self, **changes): - args = argparse.Namespace(output=self.output, version="0.3.3", public_key=self.public_key, - epoch=1793199600, **self.debs) - for key, value in changes.items(): - setattr(args, key, value) - with contextlib.redirect_stdout(io.StringIO()): - builder.prepare(args) - raw = (self.output / "APT-INPUT-MANIFEST.json").read_bytes() - return hashlib.sha256(raw).hexdigest(), json.loads(raw) - - def sign_test_releases(self): - for source, signed, clearsign in builder.signature_paths(self.output, sign_only=True): - subprocess.run(["gpg", "--no-options", "--homedir", str(self.key_home), "--batch", - "--pinentry-mode", "loopback", "--passphrase", "", "--local-user", builder.FINGERPRINT, - "--digest-algo", "SHA256", "--output", str(signed), - "--clearsign" if clearsign else "--detach-sign", str(source)], - check=True, capture_output=True) - - def test_actual_apt_update_accepts_three_signed_suites(self): - expected, manifest = self.prepare() - self.sign_test_releases() - self.assertTrue(builder.verify_signatures(self.output, manifest)["verified"]) - for suite, *_ in builder.PROFILES.values(): - isolated = self.output.parent / ("apt-" + suite) - isolated.mkdir() - (isolated / "state/lists/partial").mkdir(parents=True) - (isolated / "cache/archives/partial").mkdir(parents=True) - source = isolated / "sources.list" - source.write_text(f"deb [arch=amd64 signed-by={self.public_key}] file:{self.output} {suite} main\n") - result = subprocess.run(["apt-get", "-o", f"Dir::State={isolated}/state", - "-o", f"Dir::Cache={isolated}/cache", "-o", f"Dir::Etc::sourcelist={source}", - "-o", "Dir::Etc::sourceparts=-", "-o", "Acquire::Languages=none", - "-o", "Debug::NoLocking=1", "update"], capture_output=True, text=True, timeout=35) - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - self.assertIn("Reading package lists", result.stdout) - self.assertEqual(builder.check_inputs(self.output, expected)["version"], "0.3.3") - - def test_altered_package_is_rejected_before_signing(self): - expected, manifest = self.prepare() - p = self.output / manifest["artifacts"][1]["filename"] - p.write_bytes(p.read_bytes() + b"changed") - with self.assertRaisesRegex(ValueError, "Input changed"): - builder.check_inputs(self.output, expected) - - def test_altered_index_is_rejected(self): - expected, _ = self.prepare() - (self.output / "dists/noble/main/binary-amd64/Packages").write_text("altered") - with self.assertRaisesRegex(ValueError, "Input changed"): - builder.check_inputs(self.output, expected) - - def test_wrong_version_does_not_create_repository(self): - with self.assertRaisesRegex(ValueError, "wrong version"): - self.prepare(version="0.3.4") - self.assertFalse(self.output.exists()) - - def test_existing_output_never_overwritten(self): - self.output.mkdir() - sentinel = self.output / "sentinel" - sentinel.write_text("published") - with self.assertRaisesRegex(ValueError, "never overwrite"): - self.prepare() - self.assertEqual(sentinel.read_text(), "published") - - def test_manifest_traversal_is_rejected(self): - _, manifest = self.prepare() - manifest["files"]["../outside"] = {"bytes": 0, "sha256": hashlib.sha256(b"").hexdigest()} - raw = json.dumps(manifest).encode() - (self.output / "APT-INPUT-MANIFEST.json").write_bytes(raw) - with self.assertRaisesRegex(ValueError, "Unsafe manifest path"): - builder.check_inputs(self.output, hashlib.sha256(raw).hexdigest()) - - def test_public_key_bundle_with_extra_primary_key_is_rejected(self): - bundle = self.output.parent / "extra-primary-key.asc" - bundle.write_bytes(self.public_key.read_bytes() + self.public_key.read_bytes()) - with self.assertRaisesRegex(ValueError, "exactly one official primary"): - self.prepare(public_key=bundle) - self.assertFalse(self.output.exists()) - - - def test_one_release_manifest_covers_downloads_and_preserves_dependency_signatures(self): - source = self.output.parent / "FrameBridge-0.3.3-source.tar.gz" - source.write_bytes(b"disposable source-archive fixture") - original = builder.DEPENDENCY_SOURCE_ASSETS - dependencies = [] - signatures = {} - try: - builder.DEPENDENCY_SOURCE_ASSETS = {} - for index, name in enumerate(original): - path = self.output.parent / name - data = ("disposable dependency " + str(index)).encode() - path.write_bytes(data) - builder.DEPENDENCY_SOURCE_ASSETS[name] = (len(data), hashlib.sha256(data).hexdigest()) - signature = path.with_name(name + ".asc") - subprocess.run(["gpg", "--no-options", "--homedir", str(self.key_home), "--batch", - "--pinentry-mode", "loopback", "--passphrase", "", "--local-user", builder.FINGERPRINT, - "--armor", "--output", str(signature), "--detach-sign", str(path)], check=True, capture_output=True) - signatures[name] = signature.read_bytes() - dependencies.append(path) - expected, manifest = self.prepare(source=source, dependency_source=dependencies) - self.assertEqual(len(manifest["download_assets"]), 7) - for path in dependencies: - self.assertEqual((self.output / "downloads" / (path.name + ".asc")).read_bytes(), - signatures[path.name]) - self.sign_test_releases() - self.assertTrue(builder.verify_signatures(self.output, manifest)["verified"]) - self.assertEqual(builder.check_inputs(self.output, expected)["version"], "0.3.3") - source_copy = self.output / "downloads" / source.name - source_copy.write_bytes(b"altered") - with self.assertRaisesRegex(ValueError, "Input changed"): - builder.check_inputs(self.output, expected) - finally: - builder.DEPENDENCY_SOURCE_ASSETS = original - - def test_changed_signed_release_fails_verification(self): - _, manifest = self.prepare() - self.sign_test_releases() - path = self.output / "dists/trixie/InRelease" - path.write_bytes(path.read_bytes().replace(b"Components: main", b"Components: evil")) - with self.assertRaises(subprocess.CalledProcessError): - builder.verify_signatures(self.output, manifest) - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/tools/test_publish_gitea.py b/tools/test_publish_gitea.py deleted file mode 100644 index f4e76a6..0000000 --- a/tools/test_publish_gitea.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -"""Check the memory-only credential transport without real credentials or networking.""" -import importlib.util -import base64 -from unittest import mock -import os -from pathlib import Path -import stat -import subprocess -import tempfile -import unittest - -spec = importlib.util.spec_from_file_location("publisher", Path(__file__).with_name("publish_gitea.py")) -publisher = importlib.util.module_from_spec(spec) -spec.loader.exec_module(publisher) - -class AskpassTests(unittest.TestCase): - def test_transport_keeps_disposable_token_out_of_script_and_environment(self): - with tempfile.TemporaryDirectory(prefix="framebridge-askpass-test-") as tmp: - token = "a" * 40 # Disposable test data, not a credential. - server, stop, thread, client = publisher.askpass_server(Path(tmp), token) - try: - self.assertEqual(stat.S_IMODE(client.stat().st_mode), 0o700) - self.assertEqual(stat.S_IMODE((Path(tmp) / "credential.sock").stat().st_mode), 0o600) - self.assertNotIn(token, client.read_text()) - for prompt, expected in [("Username for 'http://127.0.0.1:3005':", b"gerardo\n"), - ("Password for 'http://gerardo@127.0.0.1:3005':", (token + "\n").encode())]: - result = subprocess.run([str(client), prompt], capture_output=True, check=True, timeout=5) - self.assertEqual(result.stdout, expected) - self.assertEqual(result.stderr, b"") - finally: - stop.set() - thread.join(timeout=2) - server.close() - - def test_unknown_prompt_returns_no_token(self): - with tempfile.TemporaryDirectory(prefix="framebridge-askpass-test-") as tmp: - server, stop, thread, client = publisher.askpass_server(Path(tmp), "b" * 40) - try: - result = subprocess.run([str(client), "Unexpected prompt"], capture_output=True, timeout=5) - self.assertNotEqual(result.returncode, 0) - self.assertEqual(result.stdout, b"") - finally: - stop.set() - thread.join(timeout=2) - server.close() - - def test_token_management_uses_normal_basic_auth_in_memory(self): - response = mock.MagicMock() - response.__enter__.return_value.status = 204 - response.__enter__.return_value.read.return_value = b"" - disposable = "c" * 40 - with mock.patch.object(publisher.urllib.request, "urlopen", return_value=response) as opener: - status, _ = publisher.api_request("DELETE", "/users/gerardo/tokens/disposable", disposable) - self.assertEqual(status, 204) - request = opener.call_args.args[0] - expected = "Basic " + base64.b64encode(("gerardo:" + disposable).encode()).decode() - self.assertEqual(request.get_header("Authorization"), expected) - self.assertNotIn(disposable, request.full_url) - - def test_incomplete_journal_is_never_overwritten(self): - with tempfile.TemporaryDirectory(prefix="framebridge-journal-test-") as tmp: - target = Path(tmp) / "transaction.json" - partial = target.with_suffix(".partial") - partial.write_text("unreviewed incomplete state") - with self.assertRaises(FileExistsError): - publisher.record(target, {"phase": "NEW"}) - self.assertFalse(target.exists()) - self.assertEqual(partial.read_text(), "unreviewed incomplete state") - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/website/.gitignore b/website/.gitignore deleted file mode 100644 index 70b5f7a..0000000 --- a/website/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -dist/ -qa/ -release.json -__pycache__/ -assets/framebridge-preview-0.3.1-original-r4.mp4 -assets/framebridge-preview-0.3.1-original-r4.jpg diff --git a/website/README.md b/website/README.md deleted file mode 100644 index 35ef5bd..0000000 --- a/website/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# FrameBridge website source - -The original visual design and renderer are retained. The main workflow is -administration of a home Linux network by one human administrator. Compatible -desktop access is an auxiliary tool. - -The canonical source repository is the existing GERANAS Gitea project -https://framebridge.betancourt.one/git/gerardo/framebridge. CLOUD provides the -existing HTTPS reverse proxy; GERANAS serves the website, downloads and APT -repository. Do not substitute a static Git mirror or another hosting backend. - -Copy release.example.json to release.json, then fill it from the exact verified -release inventory. Its three profiles are Debian 13 amd64/Python 3.13 (trixie), -Ubuntu 24.04 amd64/Python 3.12 (noble), and Linux Mint 22.x amd64/Python 3.12 -(mint22). A preparing catalog offers no new download or installation commands. -Use ready only after verifying all DEBs, corresponding source, dependency -source archives, signatures, checksums and the official public fingerprint. -Metadata describes release artifacts; it is not itself evidence that a public -hostname is operational. - -Project media is delivered separately. Copy the approved original R4 MP4 and -JPEG into assets under their original filenames. The renderer verifies these -identities before a ready build: - -- framebridge-preview-0.3.1-original-r4.mp4: - b2a8b0b302063edbe05d84fb6dadb7f303a6f0f1b6d79dc151287df4671adc5b -- framebridge-preview-0.3.1-original-r4.jpg: - 2e867d4b6a22f81ecbefa293a88799b9e3c93f02aa7d1090c7f0a0dbfe44dc9f - -The approved animation and original music remain unchanged. The video is -identified as a 0.3.1 conceptual demonstration; it does not claim to be a live -recording of the current release. - -Run: - - python3 -I -S build_site.py - python3 -I -S qa_site.py - python3 -I -S test_release_catalog.py - -The generator reads ../source/docs/COMMAND-GUIDE.json and requires its version -to match the release. QA checks local links/assets, loopback HTTP, the guarded -ready renderer with a disposable synthetic inventory, and syntax of all three -APT setup blocks. The test fixtures never sign, publish or install anything. -Perform final HTTPS, Gitea clone and isolated APT verification through the -actual public domain before reporting publication complete. - -The website follows the project's GPL-3.0-only license. Third-party materials -retain their own licenses and notices. Support is voluntary; the contact and -PayPal account supplied by the project owner is gerardobh1128@gmail.com. diff --git a/website/assets/favicon.svg b/website/assets/favicon.svg deleted file mode 100644 index aa24a01..0000000 --- a/website/assets/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/website/assets/logo.svg b/website/assets/logo.svg deleted file mode 100644 index 7707388..0000000 --- a/website/assets/logo.svg +++ /dev/null @@ -1 +0,0 @@ -FrameBridge terminal prompt>framebridge \ No newline at end of file diff --git a/website/assets/video-poster.svg b/website/assets/video-poster.svg deleted file mode 100644 index e61672e..0000000 --- a/website/assets/video-poster.svg +++ /dev/null @@ -1 +0,0 @@ -FRAMEBRIDGE / CONCEPTUAL PREVIEW>framebridgeA conversation becomes a workflow.Conceptual animation · Not a screen recording \ No newline at end of file diff --git a/website/build_site.py b/website/build_site.py deleted file mode 100644 index 06a0538..0000000 --- a/website/build_site.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/python3 -"""Build the self-hosted FrameBridge website with the Python standard library.""" -from pathlib import Path -import html,json,re,shutil,sys,runpy,hashlib - -BASE=Path(__file__).resolve().parent -ROOT=BASE.parent -DIST=BASE/'dist' -META=json.loads((BASE/'release.json').read_text()) -META.setdefault('git_url','/git/gerardo/framebridge') -META.setdefault('git_clone_url','https://framebridge.betancourt.one/git/gerardo/framebridge.git') -E=lambda value:html.escape(str(value),quote=True) -READY=META.get('state')=='ready' -CAT=runpy.run_path(str(BASE/'release_catalog.py')) -CAT['validate'](META,READY) -VIDEO=CAT['VIDEO'];POSTER=CAT['POSTER'] -for name,expected in [(VIDEO,CAT['VIDEO_SHA256']),(POSTER,CAT['POSTER_SHA256'])]: - asset=BASE/'assets'/name - if asset.exists(): - assert hashlib.sha256(asset.read_bytes()).hexdigest()==expected,'Approved original media changed.' - elif READY: - raise ValueError('Approved original media is required before publication.') -for path in ['assets','docs','download','blog/2026-10-26-opening-framebridge','community','support']: - (DIST/path).mkdir(parents=True,exist_ok=True) -for p in (BASE/'src').iterdir(): - if p.is_file():shutil.copyfile(p,DIST/'assets'/p.name) -for p in (BASE/'assets').iterdir(): - if p.is_file():shutil.copyfile(p,DIST/'assets'/p.name) -(DIST/'assets/release.json').write_text(json.dumps(META,indent=2)+'\n') - -def brand(): - return 'framebridge' - -def header(current): - items=[('Docs','/docs/','docs'),('Development','/blog/','blog'),('Source',META['git_url'],'source'),('Support','/support/','support'),('Download','/download/','download')] - links=[] - for title,url,key in items: - links.append(''+E(title)+'') - return '' - -def footer(): - return '''
''' - -def page(title,body,current='',light=False,description='FrameBridge connects your own ChatGPT account to Linux systems you control.'): - return ''+E(title)+' · FrameBridge'+header(current)+body+footer()+'' - -def code(text): - return '
'+E(text)+'
' - -release_label='Official release' if READY else 'Release preparation' -home='''
Home Linux networks · One administrator

Your Linux network.
One place to administer it.

Administer your own home Linux network through ChatGPT: inspect equipment, diagnose connections, manage services and run commands across enrolled computers. One administrator, one bridge.

GPL-3.0-only · Debian 13 · Ubuntu 24.04 · Linux Mint 22.x · amd64

FRAMEBRIDGE / EXAMPLE WORKFLOW

$fb connect

Connect your own account through the guided terminal flow.

$fb status

See tools, connection and startup state in one place.

$fb console

Return to your work view: Bash and FBTERM activity.

Illustrative commands. This is not a screen capture.

Local executionPersistent terminalsNetwork administrationCentral + nodes
-

From a blinking cursor
to a connected workflow.

A conceptual look at independent tasks, consoles and connected computers. The software runs your tools on your systems; the ChatGPT model remains hosted.

Conceptual animation — not a screen recording.Approved 0.3.1 concept preview with its original music. Current release packages and commands are documented separately.

-

Start with one machine.
Extend when you need to.

A standalone computer can provide local tools. A central installation can also connect approved nodes.

Where work happens
Your ChatGPT accountFrameBridgeYour Linux systems

No phone pairing is required. Initial account consent and credentials stay in the normal browser or hidden terminal prompts.

  1. 01

    Install and connect

    Install the package for your distribution, then use the guided text interface to connect your own account. Advanced role setup is optional for ordinary standalone use.

  2. 02

    Choose the right tool

    Use FBEXEC for commands, persistent terminals for ongoing shell work, and graphical CUA when a compatible desktop is available.

  3. 03

    Keep the context

    Return to a live terminal session without rebuilding its shell state between calls. Running processes do not survive an operating-system reboot.

  4. 04

    Add approved nodes

    Use the central instance for ChatGPT transport and select enrolled nodes explicitly. Nodes do not need a second ChatGPT tunnel.

-

Execution

Run authorized shell tasks on the selected machine and inspect their recorded results.

FBEXEC

Terminals

Keep a real Bash session across calls. Inspect text, send input and return to ongoing work.

FBTERM / fb tty

Desktop and text

Use compatible graphical and text access as auxiliary tools for your own equipment. Commands, diagnostics and network administration remain the main workflow.

CUA / CUA-TTY

People and agents

The same command-line interface serves human operators and authorized ChatGPT agents, with discoverable usage guidance.

fb / framebridge

Independent FBEXEC tasks have been used together in the laboratory when their objectives and changes did not overlap. This is observed practice, not an isolation guarantee. Use one controller at a time for each shared graphical desktop.

-

Free software.
Verifiable releases.

Read the code, adapt it and contribute. Official downloads are identified by their published checksums and release signatures.

  • Version'''+E(META['version'])+'''
  • Platform'''+E(META['distribution'])+'''
  • LicenseGNU GPLv3 only
  • Channel'''+E(release_label)+'''
-

A small project.
An open invitation.

Good tools improve through use, careful fixes and clear documentation. Report a reproducible issue, improve the guide or propose a change in Git.

How to contribute
''' -(DIST/'index.html').write_text(page('Administer your home Linux network',home)) - -download=CAT['download'](META) -(DIST/'download/index.html').write_text(page('Download and verify',download,'download')) - -community='''

Start with the existing behavior

Read the versioned guide, check the installed version and look for an existing issue. A reproducible report should name the command, expected result, actual result and relevant environment.

Remove credentials and personal data from logs before sharing them. Never include passwords, access keys, private keys, tokens or cookies.

Propose a focused change

Use an issue to explain the problem or a pull request to propose a patch. Include the reason for the change and proportionate validation. Keep unrelated edits separate so they can be reviewed.

'''+('

Clone the project

'+code('git clone '+META['git_clone_url']) if META.get('git_clone_url') else '')+'''

Open the project in Git

Official releases and free forks

Maintainers review which changes become official FrameBridge releases. Published release checksums and signatures identify those artifacts.

GNU GPLv3 also allows private modifications and independently distributed forks, subject to its terms. Contributing changes upstream is welcome; it is not a condition for exercising those rights. Modified builds should be clearly marked and must not be presented as official project releases.

Read the GNU GPLv3 license. Third-party components retain their existing notices and licenses.

Using an agent to contribute

The command interface and MCP guide are designed for both people and authorized agents. Normal operation uses installed commands and tools. Editing or rebuilding the application is a development task that should follow the owner’s instructions.

Project credits

Programming: ChatGPT
Original concept and project coordination: Gerardo Betancourt

FrameBridge is an independent project. The credit describes how the project was developed and does not imply endorsement by OpenAI.

''' -(DIST/'community/index.html').write_text(page('Contribute',community,'source')) - -post='''

FrameBridge began with a practical goal: use a conversation with ChatGPT to do useful work on Linux computers we control. Commands, persistent terminals and a compatible desktop should fit into the same working context.

Make the interface easier to understand

The current development work brings everyday commands into a shorter vocabulary, keeps status observation separate from diagnosis, and explains the effects of connection and startup controls. The long command framebridge and its short alias fb use the same interface.

The agent also needs a reliable starting point. A versioned guide and a brief MCP invitation are intended to help it discover how the installed software works, which computer receives an operation and what a command will change.

Keep the evidence honest

The preview video is a conceptual animation, not a recording of a live session. Real demonstrations and captures must remain distinguishable from illustrations. Independent tasks have been used together in the laboratory, but that experience does not establish automatic isolation or exclusive desktop control.

Open source, with identifiable releases

The project is being prepared for GNU GPLv3 distribution. Sources and package artifacts should be published together, with signatures and checksums identifying official releases. Community members remain free to adapt and redistribute the software under the license.

The first contribution can be small: a clearer explanation, a reproducible issue or a focused fix. Review gives those changes a path into a later official version.

Read the contribution guide →

''' -(DIST/'blog/2026-10-26-opening-framebridge/index.html').write_text(page('Opening FrameBridge',post,'blog')) -support='''

What support can help with

  • Hosting and distribution: keeping the source code, documentation and release files available.
  • Maintenance: investigating reported problems and keeping the software usable.
  • Documentation: improving clear instructions for people and agents.
  • Development: building and reviewing focused improvements.

Voluntary support and contact

Support through PayPal

Project contact and PayPal account: gerardobh1128@gmail.com.

Contributions are voluntary. FrameBridge remains free software under GNU GPLv3 only, whether or not you provide financial support.

Support without a payment

A useful bug report, a clearer explanation or a carefully scoped patch can be valuable contributions. You can also help by sharing honest demonstrations that distinguish real recordings from conceptual illustrations.

Read the contribution guide →

Review stays independent

Financial support does not grant control over official releases, bypass code review or security checks, or imply a project endorsement. Official changes follow the same review and release process.

Explore the source code →

''' -(DIST/'support/index.html').write_text(page('Support FrameBridge',support,'support')) - -blog='''
''' -(DIST/'blog/index.html').write_text(page('Development journal',blog,'blog')) - -guide_path=ROOT/'repository/source/docs/COMMAND-GUIDE.json' -if not guide_path.exists(): - guide_path=ROOT/'source/docs/COMMAND-GUIDE.json' -guides={} -guide_data={} -if guide_path.exists(): - guide_data=json.loads(guide_path.read_text()) - guides=guide_data.get('topics',{}) - assert isinstance(guides,dict) and all(isinstance(v,dict) for v in guides.values()) - assert guide_data.get('version')==META['version'],'Guide and release versions must match.' - (DIST/'assets/command-guide.json').write_text(json.dumps(guide_data,indent=2)+'\n') -order=[x for x in ['start','commands','connection','lifecycle','nodes','terminals','screens','diagnostics','security'] if x in guides] -order += [x for x in guides if x not in order] - -def docs_nav(current): - entries='
  • Guide overview
  • ' - for topic in order: - entries+='
  • '+E(guides[topic].get('title',topic.title()))+'
  • ' - return '' - -def paragraphs(body): - if isinstance(body,list):body='\n\n'.join(str(x) for x in body) - return ''.join('

    '+E(x).replace('\n','
    ')+'

    ' for x in str(body).split('\n\n') if x.strip()) - -for topic in order: - guide=guides[topic] - article='

    '+E(guide.get('title',topic))+'

    ' - for section in guide.get('sections',[]): - article+='

    '+E(section.get('heading',''))+'

    '+paragraphs(section.get('body','')) - for command in section.get('commands',[]): - article+=code(command if isinstance(command,str) else json.dumps(command,indent=2)) - article+='

    This page is generated from the same versioned guide used by the CLI and MCP help tool. Use fb help '+E(topic)+' for the local reference.

    ' - dest=DIST/'docs'/topic;dest.mkdir(parents=True,exist_ok=True) - (dest/'index.html').write_text(page(guide.get('title',topic),'
    '+docs_nav(topic)+article+'
    ','docs',True)) -hub='

    The command guide.

    One reference for human operators and authorized ChatGPT agents. The commands fb and framebridge use the same interface.

    Start with the normal connection flow. Use advanced setup only when you need to change roles or profile settings.

    '+code('fb connect\nfb status\nfb help')+'' -if not guides:hub+='
    The shared command-guide export is being finalized. This staging page is not the complete published reference yet.
    ' -else:hub+='

    Read the versioned guide data

    ' -hub+='
    ' -(DIST/'docs/index.html').write_text(page('Command guide','
    '+docs_nav('index')+hub+'
    ','docs',True)) -(DIST/'404.html').write_text(page('Page not found','

    This path ends here.

    Return to FrameBridge or open the command guide.

    ')) -(DIST/'robots.txt').write_text('User-agent: *\n'+('Allow: /\n' if READY else 'Disallow: /\n')) -print(json.dumps({'web_root':str(DIST),'release_state':META['state'],'pages':len(list(DIST.rglob('*.html'))),'guide_topics':len(guides),'video_present':(DIST/'assets'/VIDEO).is_file(),'ready_metadata_validated':READY},indent=2)) diff --git a/website/qa_site.py b/website/qa_site.py deleted file mode 100644 index 1c31c0a..0000000 --- a/website/qa_site.py +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/python3 -"""Bounded static and loopback HTTP checks for the FrameBridge website.""" -from pathlib import Path -from html.parser import HTMLParser -from urllib.parse import urlsplit,unquote -from urllib.request import urlopen -from http.server import ThreadingHTTPServer,SimpleHTTPRequestHandler -import json,hashlib,threading,tempfile,shutil,subprocess,html,os -BASE=Path(__file__).resolve().parent -DIST=BASE/'dist' -REPORT=BASE/'qa/WEBSITE-STATUS.json' -REPORT.parent.mkdir(mode=0o700,exist_ok=True) -class Inspect(HTMLParser): - def __init__(self): - super().__init__();self.links=[];self.assets=[];self.ids=set();self.lang=None;self.main=False;self.scripts=[];self.code=[];self.in_code=False;self.current='' - def handle_starttag(self,tag,attrs): - a=dict(attrs) - if tag=='html':self.lang=a.get('lang') - if 'id' in a:self.ids.add(a['id']) - if tag=='main' and a.get('id')=='content':self.main=True - if tag=='a' and 'href'in a:self.links.append(a['href']) - if tag in ('script','source','img') and 'src'in a:self.assets.append(a['src']) - if tag=='video' and 'poster'in a:self.assets.append(a['poster']) - if tag=='link' and a.get('rel') in ('stylesheet','icon'):self.assets.append(a['href']) - if tag=='script':self.scripts.append(a) - if tag=='code':self.in_code=True;self.current='' - def handle_data(self,data): - if self.in_code:self.current+=data - def handle_endtag(self,tag): - if tag=='code':self.code.append(self.current);self.in_code=False -def resolve(path): - p=DIST/unquote(path.lstrip('/')) - return p/'index.html' if path.endswith('/') or p.is_dir() else p -def main(): - os.umask(0o077);pages=sorted(DIST.rglob('*.html'));assert pages - failures=[];pending=[];infra=set();http_paths=set();external=set() - for p in pages: - raw=p.read_text();q=Inspect();q.feed(raw);rel='/'+str(p.relative_to(DIST));http_paths.add(rel) - if q.lang!='en' or not q.main:failures.append(str(p.relative_to(DIST))+': missing English or main landmark') - if 'gerabh' in raw or '/home/gerabh' in raw:failures.append(str(p.relative_to(DIST))+': personal host path') - if "'unsafe-inline'" in raw:failures.append(str(p.relative_to(DIST))+': unsafe inline CSP') - for value in q.links+q.assets: - u=urlsplit(value) - if u.scheme or u.netloc: - external.add(value);continue - if not u.path: - if u.fragment and u.fragment not in q.ids:failures.append(str(p.relative_to(DIST))+': missing anchor '+u.fragment) - continue - if any(u.path.startswith(prefix) for prefix in ['/git/','/debian/','/downloads/']): - infra.add(u.path);continue - if not u.path.startswith('/'):failures.append('Non-root relative URL '+value);continue - target=resolve(u.path) - if not target.exists(): - if u.path=='/assets/framebridge-preview-0.3.1-original-r4.mp4':pending.append('Preview video asset pending') - else:failures.append('Missing local target '+value+' referenced by '+str(p.relative_to(DIST))) - elif u.path in q.assets:http_paths.add(u.path) - if u.fragment and target.is_file() and target.suffix=='.html': - other=Inspect();other.feed(target.read_text()) - if u.fragment not in other.ids:failures.append('Missing target fragment '+value) - for item in q.scripts: - if item.get('src')!='/assets/site.js':failures.append('Unexpected script source') - css=(DIST/'assets/site.css').read_text() - assert css.count('{')==css.count('}'),'CSS brace mismatch' - class Quiet(SimpleHTTPRequestHandler): - def __init__(self,*a,**k):super().__init__(*a,directory=str(DIST),**k) - def log_message(self,*a):pass - server=ThreadingHTTPServer(('127.0.0.1',0),Quiet);thread=threading.Thread(target=server.serve_forever,daemon=True);thread.start() - http_count=0 - try: - for path in sorted(http_paths): - with urlopen('http://127.0.0.1:'+str(server.server_port)+path,timeout=3) as response: - if response.status!=200:failures.append('HTTP status '+str(response.status)+' for '+path) - response.read();http_count+=1 - finally:server.shutdown();server.server_close();thread.join(timeout=2) - # Exercise the guarded final-metadata branch using clearly synthetic values - # in a disposable directory. No signature or installation command executes. - meta=json.loads((BASE/'release.json').read_text()) - with tempfile.TemporaryDirectory(prefix='framebridge-web-qa-') as td: - temp=Path(td);web=temp/'web';web.mkdir() - shutil.copyfile(BASE/'build_site.py',web/'build_site.py') - shutil.copyfile(BASE/'release_catalog.py',web/'release_catalog.py') - shutil.copytree(BASE/'src',web/'src') - (web/'assets').mkdir() - for p in (BASE/'assets').iterdir(): - if p.is_file():shutil.copyfile(p,web/'assets'/p.name) - fixture=json.loads(json.dumps(meta));fixture.update(state='ready',signing_fingerprint='2B1F01BC806C0B33CB6CEDDE52859CEBC91B8808',checksums_url='/downloads/TEST-SHA256SUMS',checksums_signature_url='/downloads/TEST-SHA256SUMS.asc') - fixture['source']=dict(filename='TEST-SOURCE.tar.gz',url='/downloads/TEST-SOURCE.tar.gz',signature_url='/downloads/TEST-SOURCE.tar.gz.asc',sha256='a'*64,size_bytes=1) - for i,profile in enumerate(fixture['profiles']): - filename='TEST-profile-'+str(i)+'.deb' - profile.update(filename=filename,url='/downloads/'+filename,signature_url='/downloads/'+filename+'.asc',sha256='a'*64,size_bytes=1) - for dependency in fixture['dependency_sources']: - dependency.setdefault('signature_url',dependency['url']+'.asc') - (web/'release.json').write_text(json.dumps(fixture)) - result=subprocess.run(['/usr/bin/python3','-I','-S',str(web/'build_site.py')],capture_output=True,text=True,timeout=10) - if result.returncode:failures.append('Synthetic-ready rendering failed: '+result.stderr[-600:]) - else: - q=Inspect();q.feed((web/'dist/download/index.html').read_text()) - blocks=[x for x in q.code if '\n' in x] - for text in blocks: - result=subprocess.run(['/bin/bash','-n'],input=text,text=True,capture_output=True,timeout=3) - if result.returncode:failures.append('Rendered install block shell syntax: '+result.stderr.strip()) - apt_blocks=[x for x in blocks if 'sources.list.d/framebridge.sources' in x] - if len(apt_blocks)!=3:failures.append('Expected three independent APT profile setup blocks') - for apt in apt_blocks: - if 'Signed-By:' not in apt or 'primary_count' not in apt or 'apt-key' in apt: - failures.append('APT key trust must be pinned and scoped with Signed-By') - for suite in ['trixie','noble','mint22']: - if not any('Suites: '+suite in block for block in apt_blocks): - failures.append('Missing APT suite '+suite) - status={'host':'GERAMEG','website_source':str(BASE),'web_root':str(DIST),'language':'English','buildless':True,'html_pages_checked':len(pages),'loopback_http_paths_checked':http_count,'static_failures':failures,'pending_assets':sorted(set(pending)),'infra_routes_expected':sorted(infra),'external_links':sorted(external),'no_external_runtime_dependencies':True,'copy_controls_progressive':True,'ready_branch_rendered_with_synthetic_fixture':True,'install_blocks_syntax_checked_not_executed':True,'release_state':meta.get('state'),'package_sha256_present':all(p.get('sha256') for p in meta.get('profiles',[])),'signing_fingerprint_present':bool(meta.get('signing_fingerprint')),'video_present':(DIST/'assets/framebridge-preview-0.3.1-original-r4.mp4').is_file(),'visual_browser_QA_performed':False,'visual_browser_QA_limit':'No browser or user desktop was operated by this subtask. Root may inspect the staged site via the authorized GERAMEG surface.','production_network_or_services_changed':False,'site_ready_for_private_staging':not failures,'public_release_ready':not failures and not pending and meta.get('state')=='ready','homepage_sha256':hashlib.sha256((DIST/'index.html').read_bytes()).hexdigest()} - visual=BASE/'qa/VISUAL-REVIEW.json' - if visual.exists(): - status['visual_browser_QA_performed']=True - status['visual_browser_QA_limit']=json.loads(visual.read_text())['limitations'] - status['visual_browser_QA_report']=str(visual) - status.update(support_page='/support/',support_funding_channel_configured='https://www.paypal.com/donate/' in (DIST/'support/index.html').read_text(),support_payment_form_present=' 0, "Exact artifact size required") - require(re.fullmatch("[A-Za-z0-9][A-Za-z0-9._+~-]*", spec.get("filename", "")), "Safe artifact filename required") - project_url(spec.get("url", ""), "/downloads/") - project_url(spec.get("signature_url", ""), "/downloads/") - -def validate(meta, ready): - require(re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", meta.get("version", "")), "Release version must be numeric") - require(meta.get("state") in ("preparing", "ready"), "Unknown release state") - require(meta.get("site_url") == "https://framebridge.betancourt.one", "Canonical site origin required") - require(meta.get("package_name") == "framebridge-original-expanded", "Unexpected binary package") - require(meta.get("git_url") == "/git/gerardo/framebridge", "Use existing canonical Gitea repository") - require(meta.get("git_clone_url") == "https://framebridge.betancourt.one/git/gerardo/framebridge.git", - "Use existing canonical Gitea clone URL") - expected = {x[0]: x[1:] for x in PROFILES} - profiles = meta.get("profiles", []) - require(isinstance(profiles, list) and len(profiles) == 3, "Exactly three profiles required") - require(len({p.get("profile_id") for p in profiles}) == 3, "Duplicate profile") - for profile in profiles: - identity = profile.get("profile_id") - require(identity in expected, "Unexpected profile") - suite, label, python = expected[identity] - require((profile.get("suite"), profile.get("label"), profile.get("python"), - profile.get("architecture")) == (suite, label, python, "amd64"), - "Profile identity does not match its tested distribution") - if ready: - asset(profile) - if ready: - require(meta.get("signing_fingerprint") == FINGERPRINT, "Unexpected official signing fingerprint") - asset(meta.get("source")) - deps = meta.get("dependency_sources", []) - require(isinstance(deps, list) and len(deps) >= 2, "Dependency corresponding-source metadata required") - for dependency in deps: - asset(dependency) - for key, prefix in (("signing_key_url", "/debian/"), ("checksums_url", "/downloads/"), - ("checksums_signature_url", "/downloads/")): - project_url(meta.get(key, ""), prefix) - -def code(text): - return '
    ' + E(text) + '
    ' - -def apt_commands(meta, profile): - require(meta.get("state") == "ready", "Do not offer installation commands for an unsigned preparation") - validate(meta, True) - key_url = meta["site_url"] + meta["signing_key_url"] if meta["signing_key_url"].startswith("/") else meta["signing_key_url"] - return """sudo apt install ca-certificates curl gnupg -( - set -eu - key_file="$(mktemp)" - trap 'rm -f "$key_file"' EXIT - curl -fsSL '""" + key_url + """' -o "$key_file" - key_info="$(gpg --batch --show-keys --with-colons "$key_file" 2>/dev/null)" - actual="$(printf '%s\\n' "$key_info" | awk -F: '$1=="fpr" {print $10; exit}')" - primary_count="$(printf '%s\\n' "$key_info" | awk -F: '$1=="pub" {n++} END {print n+0}')" - test "$primary_count" -eq 1 - test "$actual" = '""" + FINGERPRINT + """' - sudo install -d -m 0755 /etc/apt/keyrings - sudo install -m 0644 "$key_file" /etc/apt/keyrings/framebridge-archive-keyring.asc - printf '%s\\n' \\ - 'Types: deb' \\ - 'URIs: https://framebridge.betancourt.one/debian/' \\ - 'Suites: """ + profile["suite"] + """' \\ - 'Components: main' \\ - 'Architectures: amd64' \\ - 'Signed-By: /etc/apt/keyrings/framebridge-archive-keyring.asc' \\ - | sudo tee /etc/apt/sources.list.d/framebridge.sources >/dev/null - sudo apt update - sudo apt install framebridge-original-expanded -)""" - -def download(meta): - ready = meta["state"] == "ready" - body = '
    ' - body += '

    FrameBridge helps one administrator manage their own home Linux network: inventory, services, storage, diagnostics, commands and persistent terminals. Desktop access is an auxiliary tool.

    ' - body += '

    Match the target computer

    ' + code("cat /etc/os-release\ndpkg --print-architecture\npython3 --version") - body += '

    The supported profiles below use amd64 and the distribution system Python. Select one matching suite for each computer.

    ' - if not ready: - body += '
    Release preparation. Version ' + E(meta["version"]) + ' is being prepared. Download links and installation commands appear after the exact artifacts and signatures are verified.
    ' - body += '
    ' - for profile in meta["profiles"]: - body += '

    ' + E(profile["label"]) + '

    APT suite: ' + E(profile["suite"]) + '

    ' - if ready: - body += '

    SHA-256: ' + E(profile["sha256"]) + '

    ' + E(profile["filename"]) + ' · ' + format(profile["size_bytes"], ",") + ' bytes

    Install the signed APT channel' + code(apt_commands(meta, profile)) + '
    ' - else: - body += '

    Version ' + E(meta["version"]) + ' · package and signature pending.

    ' - body += '
    ' - body += '
    ' - if ready: - body += '

    Release verification and source

    Expected signing-key fingerprint:

    ' + FINGERPRINT + '

    SHA256SUMS · Checksum signature · Public key

    Corresponding FrameBridge source · Source signature

    SHA-256: ' + E(meta["source"]["sha256"]) + '

    Third-party components retain their original licenses and notices. Unchanged dependency archives retain their historical filenames.

    Normal updates

    ' + code("sudo apt update\napt-cache policy framebridge-original-expanded\nsudo apt install --only-upgrade framebridge-original-expanded\nfb status") - body += '

    APT verifies the repository signature and package hashes. Keep the selected distribution suite and its scoped Signed-By key.

    ' - body += '

    Connect and work

    After installation, connect from the normal terminal on that computer:

    ' + code("fb connect\nfb status\nfb console") + '

    One central installation can connect enrolled Linux nodes. Nodes use the central connection. Enter passwords and connection keys only in the normal local prompts.

    Read the shared command guide · Browse the canonical source repository

    ' - return body diff --git a/website/src/site.css b/website/src/site.css deleted file mode 100644 index 138170c..0000000 --- a/website/src/site.css +++ /dev/null @@ -1,11 +0,0 @@ -@charset "UTF-8"; -:root{color-scheme:dark;--bg:#111715;--panel:#19211e;--ink:#f1f1e8;--muted:#aeb9b1;--line:#33443b;--accent:#eac27b;--mint:#9dd5b7;--mono:ui-monospace,SFMono-Regular,Consolas,"Liberation Mono",monospace;--sans:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif} -*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--ink);font:17px/1.65 var(--sans);-webkit-font-smoothing:antialiased}a{color:inherit;text-decoration-thickness:1px;text-underline-offset:.22em}button,a{-webkit-tap-highlight-color:transparent}a:hover{color:var(--accent)}a:focus-visible,button:focus-visible,summary:focus-visible{outline:3px solid var(--accent);outline-offset:5px}button{font:inherit}img,video{max-width:100%;display:block}code,pre,.mono{font-family:var(--mono)}code{font-size:.9em}pre{white-space:pre-wrap;overflow-wrap:anywhere;margin:0;font-size:14px;line-height:1.7}h1,h2,h3,p{margin-top:0}h1,h2,h3{line-height:1.13;letter-spacing:-.045em;font-weight:550}h1{font-size:clamp(48px,6.1vw,88px);max-width:12ch;margin-bottom:28px}h2{font-size:clamp(32px,4vw,52px);max-width:21ch;margin-bottom:25px}h3{font-size:23px;letter-spacing:-.025em}p{margin-bottom:20px}::selection{background:var(--accent);color:#111715}.wrap{width:min(1200px,calc(100% - 64px));margin:auto}.narrow{max-width:820px}.skip{position:fixed;z-index:99;top:-80px;left:20px;padding:12px 18px;background:var(--accent);color:#111}.skip:focus{top:12px}.site-header{border-bottom:1px solid var(--line)}.header-inner{min-height:100px;display:flex;align-items:center;justify-content:space-between;gap:28px}.brand{font-family:var(--mono);font-size:24px;letter-spacing:-.06em;text-decoration:none;white-space:nowrap}.brand .prompt{color:var(--accent);margin-right:9px}.cursor{color:var(--accent);animation:blink 1.5s steps(1) infinite}nav{display:flex;gap:28px;align-items:center;font-size:14px}nav a{text-decoration:none;color:var(--muted)}nav a[aria-current=page]{color:var(--ink)}.nav-download{padding:9px 16px;border:1px solid var(--line);border-radius:2px;color:var(--ink)} -.hero{padding:88px 0 84px;display:grid;grid-template-columns:1.15fr .85fr;gap:80px;align-items:center}.eyebrow{text-transform:uppercase;letter-spacing:.14em;font-size:11px;font-weight:650;color:var(--mint);margin-bottom:28px;display:flex;gap:10px;align-items:center}.eyebrow:before{content:"";display:block;width:7px;height:7px;border-radius:50%;background:var(--mint)}.hero .lead{max-width:42ch;font-size:20px;line-height:1.6;color:var(--muted);margin-bottom:32px}.actions{display:flex;gap:15px;align-items:center;flex-wrap:wrap}.button{display:inline-flex;align-items:center;justify-content:center;padding:13px 21px;border:1px solid var(--line);text-decoration:none;font-size:14px;font-weight:600;border-radius:2px;min-height:48px}.button.primary{background:var(--accent);border-color:var(--accent);color:#171b16}.button:hover{border-color:var(--accent)}.button.primary:hover{background:#f5d8a8;color:#171b16}.button.disabled{color:var(--muted);cursor:default}.micro{font-size:12px;color:var(--muted);margin-top:20px}.terminal{border:1px solid #42564a;background:#0b110e;box-shadow:12px 15px 0 #1b2821}.terminal-top{display:flex;justify-content:space-between;gap:12px;padding:14px 20px;border-bottom:1px solid #33443b;color:#95aa9b;font:11px var(--mono)}.terminal-body{padding:28px 26px}.term-row{border-left:1px solid #2d4134;padding:0 0 24px 20px;font-family:var(--mono)}.term-row:last-child{padding-bottom:0}.term-command{font-size:15px;color:#f3d79f;margin:0 0 8px}.term-command span{color:#668775;margin-right:10px}.term-note{color:#8fa796;font-size:11px;line-height:1.7;max-width:27ch}.terminal-caption{font-size:11px;letter-spacing:.04em;color:var(--muted);margin-top:26px;text-align:right} -.rule{border-top:1px solid var(--line)}.strip{padding:24px 0;display:flex;gap:35px;justify-content:space-between;font:12px var(--mono);color:var(--muted)}.strip span:before{content:"+";margin-right:10px;color:var(--accent)}.section{padding:85px 0}.section-head{display:grid;grid-template-columns:1fr 1fr;gap:60px;align-items:start;margin-bottom:40px}.section-head p{color:var(--muted);max-width:46ch;padding-top:6px}.section-label{font:11px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--mint);margin-bottom:24px}.video-shell{background:#080c0a;border:1px solid var(--line)}.video-shell video{width:100%;aspect-ratio:16/9;background:#080c0a;object-fit:contain}.video-caption{display:flex;justify-content:space-between;gap:24px;margin:15px 0 0;color:var(--muted);font-size:12px}.video-caption strong{color:var(--ink);font-weight:500}.workflow{display:grid;grid-template-columns:1fr 1fr;gap:85px}.workflow-list{list-style:none;padding:0;margin:0;border-top:1px solid var(--line)}.workflow-list li{display:grid;grid-template-columns:38px 1fr;gap:20px;padding:26px 0;border-bottom:1px solid var(--line)}.step-no{font:11px var(--mono);color:var(--accent);padding-top:4px}.workflow-list h3{margin:0 0 10px}.workflow-list p{color:var(--muted);font-size:15px;line-height:1.6;margin:0}.architecture{background:var(--panel);border:1px solid var(--line);padding:32px;margin-top:36px}.architecture-title{font:11px var(--mono);color:var(--muted);text-transform:uppercase;letter-spacing:.1em;margin-bottom:22px}.architecture-line{display:flex;gap:16px;align-items:center;justify-content:space-between;font:13px var(--mono)}.architecture-line .arrow{color:var(--accent)}.note{border-left:2px solid var(--accent);padding-left:19px;font-size:13px;color:var(--muted);margin:30px 0 0}.feature-row{display:grid;grid-template-columns:220px 1fr 1fr;gap:45px;border-top:1px solid var(--line);padding:30px 0}.feature-row:last-child{border-bottom:1px solid var(--line)}.feature-row h3{font-size:19px;margin:0}.feature-row p{font-size:14px;color:var(--muted);margin:0}.feature-row code{color:var(--accent);font-size:12px}.release-panel{display:grid;grid-template-columns:1fr 1fr;gap:70px;background:#202d25;border:1px solid #415747;padding:45px}.release-panel p{color:#b9c6bc}.release-panel h2{font-size:40px}.release-facts{margin:0;padding:0;list-style:none;border-top:1px solid #415747}.release-facts li{display:flex;justify-content:space-between;gap:20px;padding:12px 0;border-bottom:1px solid #415747;font-size:13px}.release-facts strong{font-weight:500;color:#f0d19a}.community{display:grid;grid-template-columns:1fr 1fr;gap:100px}.text-link{display:inline-block;font-size:14px;margin-top:8px}.news-link{display:block;text-decoration:none;border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:25px 0}.news-link time{display:block;font:11px var(--mono);color:var(--muted);margin-bottom:12px}.news-link strong{font-size:23px;font-weight:500;letter-spacing:-.03em;display:block}.news-link span{font-size:13px;color:var(--mint);display:block;margin-top:14px}.site-footer{border-top:1px solid var(--line);padding:42px 0 30px;color:var(--muted);font-size:12px}.footer-main{display:flex;justify-content:space-between;gap:40px;margin-bottom:30px}.footer-links{display:flex;gap:22px;flex-wrap:wrap}.credits{line-height:1.9;max-width:52ch}.credits strong{font-weight:500;color:var(--ink)}.footer-bottom{display:flex;justify-content:space-between;gap:30px;font-size:11px}.footer-bottom p{margin:0;max-width:60ch} -.page-header{padding:60px 0 40px;border-bottom:1px solid var(--line)}.page-header h1{font-size:clamp(38px,5vw,64px);max-width:18ch;margin-bottom:22px}.page-header .lead{max-width:65ch;color:var(--muted);font-size:18px}.page-content{padding:55px 0 90px}.prose{max-width:780px}.prose h2{font-size:30px;max-width:none;margin:42px 0 20px}.prose h3{font-size:22px;margin-top:30px}.prose p,.prose li{color:var(--muted)}.prose strong{color:var(--ink);font-weight:600}.prose ul,.prose ol{padding-left:24px}.prose li{margin-bottom:10px}.prose blockquote{border-left:2px solid var(--accent);margin:30px 0;padding-left:20px;color:var(--muted)}.codebox{position:relative;border:1px solid var(--line);background:#0b110e;padding:23px 20px;margin:22px 0}.codebox pre{padding-right:38px}.copy-button{position:absolute;top:12px;right:12px;border:1px solid var(--line);background:var(--panel);color:var(--muted);border-radius:2px;font-size:10px;padding:5px 8px;cursor:pointer}.copy-button:hover{color:var(--accent);border-color:var(--accent)}.copy-notice{font-size:11px;color:var(--mint);min-height:1.5em}.release-notice{padding:23px;border:1px solid #715d3e;background:#30291d;color:#e7c995;font-size:14px;margin:25px 0}.hash{overflow-wrap:anywhere;font-family:var(--mono);font-size:12px}.download-grid{display:grid;grid-template-columns:1.2fr .8fr;gap:80px}.download-summary{border-top:1px solid var(--line);margin-bottom:28px}.download-summary dt{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;padding-top:18px}.download-summary dd{margin:6px 0 0;padding-bottom:18px;border-bottom:1px solid var(--line);font-size:14px}.aside-note{font-size:13px;color:var(--muted)} -.docs{color-scheme:light;--bg:#f3f3ec;--panel:#e9ede5;--ink:#1c3024;--muted:#526357;--line:#cbd5c8;--accent:#795118;--mint:#3c7153}.docs .site-header{background:#19251e;color:#f3f3ec;border-color:#33443b}.docs .brand .prompt,.docs .cursor{color:#eac27b}.docs .site-header nav a{color:#c3cfc3}.docs .site-header nav a[aria-current=page]{color:#fff}.docs .nav-download{border-color:#566b5b}.docs-layout{display:grid;grid-template-columns:230px minmax(0,1fr);gap:65px;padding:50px 0 85px}.docs-nav{border-right:1px solid var(--line);padding-right:28px;font-size:13px}.docs-nav p{font:11px var(--mono);text-transform:uppercase;letter-spacing:.08em;color:var(--muted)}.docs-nav ul{list-style:none;padding:0;margin:15px 0 28px}.docs-nav a{display:block;text-decoration:none;padding:7px 0;color:var(--muted)}.docs-nav a[aria-current=page]{color:var(--ink);font-weight:650}.docs article{max-width:780px}.docs h1{font-size:46px;max-width:none}.docs .prose p,.docs .prose li{color:var(--muted)}.docs .codebox{background:#e7ece3}.docs .copy-button{background:#f3f3ec}.docs .section-label{margin-bottom:15px}.docs .site-footer{background:#e9ede5}.docs .credits strong{color:#1c3024}.topic-links{display:grid;grid-template-columns:1fr 1fr;gap:0 35px;margin:30px 0}.topic-link{display:block;padding:18px 0;border-top:1px solid var(--line);text-decoration:none}.topic-link strong{display:block;margin-bottom:6px}.topic-link span{font-size:13px;color:var(--muted)} -@keyframes blink{50%{opacity:0}}@media(max-width:1000px){.hero{gap:45px}.header-inner{min-height:90px}nav{gap:17px}.brand{font-size:21px}.feature-row{grid-template-columns:170px 1fr 1fr;gap:25px}.release-panel{gap:35px}.architecture-line{font-size:12px;gap:10px}.docs-layout{grid-template-columns:190px minmax(0,1fr);gap:35px}} -@media(max-width:760px){.wrap{width:calc(100% - 40px)}body{font-size:16px}.header-inner{align-items:flex-start;flex-direction:column;gap:14px;padding:21px 0 17px}.brand{font-size:22px}nav{gap:14px;width:100%;font-size:12px;overflow-x:auto;padding:3px 0 7px}.nav-download{padding:5px 10px}.hero{display:block;padding:50px 0 48px}.hero h1{font-size:57px;max-width:11ch}.hero .lead{font-size:18px}.terminal{margin:45px 8px 0 0;box-shadow:8px 9px 0 #1b2821}.terminal-caption{text-align:left}.terminal-body{padding:25px}.strip{flex-wrap:wrap;gap:12px 20px;font-size:10px;padding:20px 0}.section{padding:50px 0}.section-head,.workflow,.community,.download-grid{display:block}.section-head{margin-bottom:25px}.section-head p{padding:0}.video-caption{display:block}.video-caption span{display:block;margin-top:5px}.workflow-list{margin-top:30px}.architecture{padding:25px}.architecture-line{flex-direction:column;align-items:flex-start;gap:10px}.architecture-line .arrow{transform:rotate(90deg);margin-left:6px}.feature-row{grid-template-columns:1fr;gap:12px;padding:24px 0}.feature-row code{font-size:11px}.release-panel{display:block;padding:28px}.release-panel h2{font-size:34px}.release-facts{margin-top:30px}.community>div+div{margin-top:35px}.footer-main,.footer-bottom{display:block}.footer-links{margin:25px 0}.footer-bottom p+p{margin-top:12px}.page-header{padding:40px 0 25px}.page-header h1{font-size:43px}.page-content{padding:30px 0 55px}.download-grid aside{margin-top:40px}.docs-layout{display:block;padding:30px 0 55px}.docs-nav{border-right:0;border-bottom:1px solid var(--line);padding:0 0 16px;margin-bottom:30px}.docs-nav ul{display:flex;flex-wrap:wrap;gap:3px 20px;margin:10px 0}.docs-nav a{padding:3px 0}.docs h1{font-size:37px}.topic-links{grid-template-columns:1fr}.codebox{padding:20px 16px}pre{font-size:12px}.codebox pre{padding-right:0;padding-top:20px}}@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}.cursor{animation:none}} - -.profiles{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:24px;margin:40px 0}.profile-card{min-width:0;border:1px solid var(--line,#31433b);padding:24px}.profile-card h2{font-size:1.6rem}.profile-card .actions{gap:10px}.profile-card details{margin-top:24px}.profile-card pre{font-size:.75rem;overflow:auto}@media(max-width:1000px){.profiles{grid-template-columns:1fr}.profile-card{padding:20px}} diff --git a/website/src/site.js b/website/src/site.js deleted file mode 100644 index 3abdeb4..0000000 --- a/website/src/site.js +++ /dev/null @@ -1,12 +0,0 @@ -"use strict"; -document.querySelectorAll(".codebox pre").forEach(function(pre){ - if(!navigator.clipboard || !window.isSecureContext) return; - var button=document.createElement("button"); - button.type="button";button.className="copy-button";button.textContent="Copy"; - button.setAttribute("aria-label","Copy command block"); - button.addEventListener("click",async function(){ - try{await navigator.clipboard.writeText(pre.textContent);button.textContent="Copied";setTimeout(function(){button.textContent="Copy"},1800)} - catch(error){button.textContent="Select to copy";setTimeout(function(){button.textContent="Copy"},2000)} - }); - pre.parentElement.appendChild(button); -}); diff --git a/website/test_release_catalog.py b/website/test_release_catalog.py deleted file mode 100644 index ff182b1..0000000 --- a/website/test_release_catalog.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/python3 -"""Catalog guards and rendered shell syntax; no signing or installation.""" -import copy -import json -from pathlib import Path -import runpy -import subprocess -import unittest - -BASE = Path(__file__).resolve().parent -catalog = runpy.run_path(str(BASE / "release_catalog.py")) - -class CatalogTests(unittest.TestCase): - def setUp(self): - self.meta = json.loads((BASE / "release.example.json").read_text()) - - def ready(self): - meta = copy.deepcopy(self.meta) - meta.update(state="ready", signing_fingerprint=catalog["FINGERPRINT"], - checksums_url="/downloads/TEST-SHA256SUMS", - checksums_signature_url="/downloads/TEST-SHA256SUMS.asc") - meta["source"] = dict(filename="TEST-source.tar.gz", url="/downloads/TEST-source.tar.gz", - signature_url="/downloads/TEST-source.tar.gz.asc", sha256="a"*64, size_bytes=1) - for index, profile in enumerate(meta["profiles"]): - name = "TEST-profile-" + str(index) + ".deb" - profile.update(filename=name, url="/downloads/"+name, - signature_url="/downloads/"+name+".asc", sha256="a"*64, size_bytes=1) - return meta - - def test_preparation_has_no_install_or_download_claim(self): - catalog["validate"](self.meta, False) - text = catalog["download"](self.meta) - self.assertIn("Release preparation", text) - self.assertNotIn("sudo apt install", text) - self.assertNotIn("Download .deb", text) - - def test_three_profile_shell_commands_are_valid_and_key_scoped(self): - meta = self.ready() - catalog["validate"](meta, True) - for profile in meta["profiles"]: - commands = catalog["apt_commands"](meta, profile) - result = subprocess.run(["bash", "-n"], input=commands, text=True, capture_output=True) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn("Suites: " + profile["suite"], commands) - self.assertIn("Signed-By:", commands) - self.assertIn("primary_count", commands) - self.assertNotIn("trusted=yes", commands) - self.assertNotIn("apt-key", commands) - - def test_wrong_distribution_metadata_is_rejected(self): - self.meta["profiles"][1]["label"] = "Ubuntu 26.04" - with self.assertRaisesRegex(ValueError, "Profile identity"): - catalog["validate"](self.meta, False) - - def test_duplicate_profiles_are_rejected(self): - self.meta["profiles"][1] = self.meta["profiles"][0] - with self.assertRaisesRegex(ValueError, "Duplicate profile"): - catalog["validate"](self.meta, False) - - def test_extra_shell_characters_in_key_url_are_rejected(self): - meta = self.ready() - meta["signing_key_url"] = "/debian/key';echo-malicious" - with self.assertRaises(ValueError): - catalog["validate"](meta, True) - - def test_missing_artifact_checksum_is_rejected(self): - meta = self.ready() - meta["profiles"][2]["sha256"] = "" - with self.assertRaisesRegex(ValueError, "SHA-256"): - catalog["validate"](meta, True) - -if __name__ == "__main__": - unittest.main(verbosity=2)